"""Tests for scenegraph builder / ObjectNode.""" from __future__ import annotations import sys from pathlib import Path from types import SimpleNamespace import numpy as np import pytest import torch MINI_NAV_DIR = Path(__file__).resolve().parents[1] / "mini-nav" sys.path.insert(0, str(MINI_NAV_DIR)) from scenegraph.hash_codec import bits_tensor_to_hash_bytes # noqa: E402 from scenegraph.objectnode import ObjectNode # noqa: E402 def _hash_bytes() -> bytes: """Create a 512-bit hash with bit 0 set to 1.""" bits = torch.zeros(512, dtype=torch.uint8) bits[0] = 1 return bits_tensor_to_hash_bytes(bits) def test_object_node_accepts_optional_detection_metadata(): """ObjectNode should accept and preserve optional detection metadata.""" hb = _hash_bytes() node = ObjectNode( obj_id="room_a_v000_m00", room_id="room_a", position=np.array([1, 2, 3], dtype=np.float32), visual_hash=hb, semantic_hash=hb, hit_count=1, last_seen_frame=0, label="a chair", confidence=0.87, bbox_xyxy=(10, 20, 30, 40), source_view_id="room_a_v000", position_confidence=0.0, ) assert node.obj_id == "room_a_v000_m00" assert node.room_id == "room_a" np.testing.assert_array_equal(node.position, np.array([1, 2, 3], dtype=np.float32)) assert node.visual_hash == hb assert node.semantic_hash == hb assert node.hit_count == 1 assert node.last_seen_frame == 0 assert node.label == "a chair" assert node.confidence == 0.87 assert node.bbox_xyxy == (10, 20, 30, 40) assert node.source_view_id == "room_a_v000" assert node.position_confidence == 0.0 def test_room_view_equality_with_numpy_rgb_does_not_compare_payload(): """RoomView equality should not attempt element-wise numpy comparison.""" from simulator import RoomView # noqa: E402 rgb_a = np.zeros((2, 2, 3), dtype=np.uint8) rgb_b = np.ones((2, 2, 3), dtype=np.uint8) # Two views with different numpy payloads — still equal because # rgb is excluded from equality (only room_id, view_idx matter). v1 = RoomView(room_id="room_a", view_idx=0, rgb=rgb_a) v2 = RoomView(room_id="room_a", view_idx=0, rgb=rgb_b) assert v1 == v2 # Different room_id → not equal v3 = RoomView(room_id="room_b", view_idx=0, rgb=rgb_a) assert v1 != v3 # Different view_idx → not equal v4 = RoomView(room_id="room_a", view_idx=1, rgb=rgb_a) assert v1 != v4 def test_flatten_room_views_with_numpy_arrays_safe_equality(): """flatten_room_views with numpy arrays should not raise ambiguous truth value errors.""" from simulator import RoomView, flatten_room_views # noqa: E402 rgb_a = np.zeros((2, 2, 3), dtype=np.uint8) rgb_b = np.ones((2, 2, 3), dtype=np.uint8) views = flatten_room_views({"room_a": [rgb_a, rgb_b]}) # Compare with expected list — uses identity-field comparison only expected = [ RoomView(room_id="room_a", view_idx=0, rgb=rgb_a), RoomView(room_id="room_a", view_idx=1, rgb=rgb_b), ] assert views == expected # Even with wildly different numpy arrays, comparison is safe swapped = [ RoomView(room_id="room_a", view_idx=0, rgb=rgb_b), RoomView(room_id="room_a", view_idx=1, rgb=rgb_a), ] assert views == swapped # rgb payload ignored # Mismatched identity fields still differ different_room = [ RoomView(room_id="room_b", view_idx=0, rgb=rgb_a), RoomView(room_id="room_a", view_idx=1, rgb=rgb_b), ] assert views != different_room def test_flatten_room_views_preserves_room_and_view_identity(): from PIL import Image # noqa: E402 from simulator import RoomView, flatten_room_views # noqa: E402 first = Image.new("RGB", (4, 4), "red") second = Image.new("RGB", (4, 4), "blue") views = flatten_room_views({"room_a": [first, second]}) # Ensure the returned objects are the same instances assert views[0].rgb is first assert views[1].rgb is second # Equality compares only room_id and view_idx (payload excluded). # Different PIL images with same room_id/view_idx compare equal. assert views == [ RoomView(room_id="room_a", view_idx=0, rgb=first), RoomView(room_id="room_a", view_idx=1, rgb=second), ] assert views == [ RoomView(room_id="room_a", view_idx=0, rgb=second), # rgb ignored RoomView(room_id="room_a", view_idx=1, rgb=first), # rgb ignored ] # Different room_id or view_idx makes them unequal assert views != [ RoomView(room_id="room_b", view_idx=0, rgb=first), RoomView(room_id="room_a", view_idx=1, rgb=second), ] # --------------------------------------------------------------------------- # SceneGraphBuilder M0 tests # --------------------------------------------------------------------------- class FakePipeline: """Fake pipeline that records calls and returns a pre-set output.""" def __init__(self, output, calls, hash_bits=512): self.output = output self.calls = calls self._hash_bits = hash_bits def process_batch(self, images, text_labels, batch_size=32, return_debug_details=False): self.calls.append({ "images": images, "text_labels": text_labels, "batch_size": batch_size, "return_debug_details": return_debug_details, }) return self.output def test_scene_graph_builder_creates_objects_from_pipeline_debug_meta(): """Build a graph from one room view with one detection and verify all fields.""" from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] rgb = np.zeros((32, 32, 3), dtype=np.uint8) room_view = RoomView(room_id="room_a", view_idx=0, rgb=rgb) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[10.0, 20.0, 30.0, 40.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((32, 32), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[], hash_bits=512) config = SceneGraphBuildConfig(inference_batch_size=7) builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) # Pipeline was called correctly assert len(pipeline.calls) == 1 call = pipeline.calls[0] assert call["return_debug_details"] is True assert call["batch_size"] == 7 assert len(call["images"]) == 1 assert call["images"][0] is rgb assert call["text_labels"] == text_labels # Graph rooms assert list(graph.rooms.keys()) == ["room_a"] assert graph.rooms["room_a"] is room_node # Graph objects assert list(graph.objects.keys()) == ["room_a_v000_m00"] obj = graph.objects["room_a_v000_m00"] assert obj.room_id == "room_a" np.testing.assert_array_equal(obj.position, np.array([10, 0, 20], dtype=np.float32)) expected_hash = bits_tensor_to_hash_bytes(hash_bits[0]) assert obj.visual_hash == expected_hash assert obj.semantic_hash == expected_hash assert obj.label == "a chair" assert obj.confidence == 0.91 assert obj.bbox_xyxy == (10.0, 20.0, 30.0, 40.0) assert obj.source_view_id == "room_a_v000" assert obj.position_confidence == 0.0 # Artifacts assert artifacts.object_images["room_a_v000_m00"] is crop assert artifacts.debug_meta == [debug_meta_entry] def test_scene_graph_builder_handles_empty_pipeline_output(): """Pipeline returning no detections should produce rooms-only graph.""" from scenegraph import RoomNode, SceneGraphBuilder from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[0, 0, 0], bbox_extent=[5, 3, 5]) room_nodes = [room_node] rgb = np.zeros((32, 32, 3), dtype=np.uint8) room_view = RoomView(room_id="room_a", view_idx=0, rgb=rgb) room_views = [room_view] text_labels: list[str] = [] hash_bits = torch.zeros(0, 512, dtype=torch.uint8) debug_meta_entry = { "selected_indices": [], "boxes_xyxy": [], "scores": [], "labels": [], "masks": [], "num_selected": 0, } output = SimpleNamespace( cropped_images=[], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) builder = SceneGraphBuilder(pipeline=pipeline) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) # Only rooms, no objects assert list(graph.rooms.keys()) == ["room_a"] assert graph.objects == {} assert artifacts.object_images == {} def test_config_rejects_enable_fusion(): """SceneGraphBuildConfig should reject enable_fusion=True.""" from scenegraph import SceneGraphBuildConfig with pytest.raises(ValueError, match="fusion"): SceneGraphBuildConfig(enable_fusion=True) def test_config_rejects_non_room_center_strategy(): """SceneGraphBuildConfig should reject non-room_center position_strategy.""" from scenegraph import SceneGraphBuildConfig with pytest.raises(ValueError, match="room_center"): SceneGraphBuildConfig(position_strategy="global") def test_config_rejects_zero_batch_size(): """SceneGraphBuildConfig should reject inference_batch_size <= 0.""" from scenegraph import SceneGraphBuildConfig with pytest.raises(ValueError): SceneGraphBuildConfig(inference_batch_size=0) def test_scene_graph_builder_public_method_stays_small(): """build_from_room_views should stay an orchestration method, not own all details.""" import inspect from scenegraph import SceneGraphBuilder source = inspect.getsource(SceneGraphBuilder.build_from_room_views) code_lines = [ line for line in source.splitlines() if line.strip() and not line.strip().startswith("#") ] assert len(code_lines) <= 45 for helper_name in [ "_prepare_graph", "_run_pipeline", "_build_artifacts", "_validate_pipeline_output", "_add_objects_to_graph", ]: assert helper_name in source # --------------------------------------------------------------------------- # Validation error tests # --------------------------------------------------------------------------- def test_builder_raises_on_selected_indices_num_selected_mismatch(): """selected_indices length != num_selected should raise ValueError.""" from scenegraph import RoomNode, SceneGraphBuilder from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[0, 0, 0], bbox_extent=[5, 3, 5]) room_nodes = [room_node] rgb = np.zeros((32, 32, 3), dtype=np.uint8) room_view = RoomView(room_id="room_a", view_idx=0, rgb=rgb) room_views = [room_view] debug_meta_entry = { "selected_indices": [0], # len 1 "boxes_xyxy": [[10.0, 20.0, 30.0, 40.0]], "scores": [0.91], "labels": ["a chair"], "num_selected": 0, # mismatch! } output = SimpleNamespace( cropped_images=["crop"], hash_bits=torch.zeros(1, 512, dtype=torch.uint8), debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) builder = SceneGraphBuilder(pipeline=pipeline) with pytest.raises(ValueError, match="selected_indices"): builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=[], ) def test_builder_raises_on_selected_idx_out_of_labels_range(): """selected_idx >= len(labels) should raise ValueError.""" from scenegraph import RoomNode, SceneGraphBuilder from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[0, 0, 0], bbox_extent=[5, 3, 5]) room_nodes = [room_node] rgb = np.zeros((32, 32, 3), dtype=np.uint8) room_view = RoomView(room_id="room_a", view_idx=0, rgb=rgb) room_views = [room_view] debug_meta_entry = { "selected_indices": [5], # index 5, but labels has only 1 entry "boxes_xyxy": [[10.0, 20.0, 30.0, 40.0]], "scores": [0.91], "labels": ["a chair"], "num_selected": 1, } output = SimpleNamespace( cropped_images=["crop"], hash_bits=torch.zeros(1, 512, dtype=torch.uint8), debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) builder = SceneGraphBuilder(pipeline=pipeline) with pytest.raises(ValueError, match="selected_idx.*labels"): builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=[], ) def test_builder_raises_on_missing_room_before_pipeline(): """Unknown room_id in room_view should raise ValueError before pipeline call.""" from scenegraph import RoomNode, SceneGraphBuilder from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[0, 0, 0], bbox_extent=[5, 3, 5]) room_nodes = [room_node] rgb = np.zeros((32, 32, 3), dtype=np.uint8) room_view = RoomView(room_id="room_b", view_idx=0, rgb=rgb) # not in room_nodes! room_views = [room_view] output = SimpleNamespace( cropped_images=[], hash_bits=torch.zeros(0, 512, dtype=torch.uint8), debug_meta=[], ) pipeline = FakePipeline(output=output, calls=[]) builder = SceneGraphBuilder(pipeline=pipeline) with pytest.raises(ValueError, match="room_b"): builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=[], ) # Pipeline should NOT have been called — error raised before pipeline assert len(pipeline.calls) == 0 def test_builder_raises_on_debug_meta_length_mismatch(): """Mismatched debug_meta length should raise ValueError.""" from scenegraph import RoomNode, SceneGraphBuilder from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[0, 0, 0], bbox_extent=[5, 3, 5]) room_nodes = [room_node] rgb = np.zeros((32, 32, 3), dtype=np.uint8) room_view = RoomView(room_id="room_a", view_idx=0, rgb=rgb) room_views = [room_view] debug_meta_entry = { "selected_indices": [], "boxes_xyxy": [], "scores": [], "labels": [], "num_selected": 0, } # 2 debug_meta entries but only 1 room_view output = SimpleNamespace( cropped_images=[], hash_bits=torch.zeros(0, 512, dtype=torch.uint8), debug_meta=[debug_meta_entry, debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) builder = SceneGraphBuilder(pipeline=pipeline) with pytest.raises(ValueError, match="debug_meta"): builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=[], ) # --------------------------------------------------------------------------- # Scene graph depth positioning M1 — depth + pose capture in RoomView # --------------------------------------------------------------------------- class FakeAgentState: """Fake agent state with position, rotation, and optional sensor_states.""" def __init__(self, position=None, rotation=None, sensor_states=None): self.position = position if position is not None else np.array([0.0, 0.0, 0.0], dtype=np.float32) self.rotation = rotation if rotation is not None else np.eye(3, dtype=np.float32) self.sensor_states = sensor_states class FakeHabitatModuleForViews: """Fake habitat_sim module exposing AgentState.""" AgentState = FakeAgentState class FakeAgentForViews: """Fake agent that tracks set_state and returns copied state from get_state.""" def __init__(self): self._state = FakeAgentState() self.set_state_calls = [] def set_state(self, state): # Preserve sensor_states from the old state when the new state does not # have any. This mimics real habitat behaviour: creating a bare # AgentState() and calling set_state() does not destroy the sim's # sensor configuration. if state.sensor_states is None and hasattr(self, "_state"): old_ss = getattr(self._state, "sensor_states", None) if old_ss is not None: state.sensor_states = old_ss self._state = state self.set_state_calls.append(state) def get_state(self): import copy return copy.copy(self._state) class FakeSimForViews: """Fake simulator returning observation dicts and recording steps.""" def __init__(self, observations_by_step=None): self.observations_by_step = observations_by_step or {} self.steps = [] def get_sensor_observations(self): step_key = len(self.steps) if step_key in self.observations_by_step: return self.observations_by_step[step_key] return {"color_sensor": np.zeros((4, 4, 3), dtype=np.uint8), "depth_sensor": np.zeros((4, 4), dtype=np.float32)} def step(self, action): self.steps.append(action) def test_collect_room_views_by_room_populates_depth_and_pose(): """collect_room_views_by_room with depth_sensor_uuid should populate RoomView depth/pose fields.""" from types import SimpleNamespace from scenegraph import RoomNode from simulator import RoomView, collect_room_views_by_room rgb = np.ones((4, 4, 3), dtype=np.uint8) * 127 depth = np.full((4, 4), 0.5, dtype=np.float32) room_node = RoomNode(room_id="room_a", center=np.array([4, 5, 6], dtype=np.float32), bbox_extent=[2, 2, 2]) room_nodes = [room_node] sim = FakeSimForViews(observations_by_step={ 0: {"color_sensor": rgb, "depth_sensor": depth}, }) agent_pos = np.array([4, 5, 6], dtype=np.float32) camera_pos = np.array([4.0, 6.5, 6.0], dtype=np.float32) # sensor offset [0, 1.5, 0] camera_rot = np.eye(3, dtype=np.float32) agent = FakeAgentForViews() agent.set_state(FakeAgentState( position=agent_pos.copy(), rotation=np.eye(3, dtype=np.float32), sensor_states={ "depth_sensor": SimpleNamespace( position=camera_pos.copy(), rotation=camera_rot.copy(), ), }, )) result = collect_room_views_by_room( agent=agent, sim=sim, room_nodes=room_nodes, views_per_room=1, habitat_sim_module=FakeHabitatModuleForViews, sensor_uuid="color_sensor", depth_sensor_uuid="depth_sensor", turn_action="turn_left", progress_track=lambda x, desc: x, ) room_views = result["room_a"] assert len(room_views) == 1 item = room_views[0] assert isinstance(item, RoomView) assert item.rgb is rgb assert item.depth is depth np.testing.assert_array_equal(item.agent_position, agent_pos) np.testing.assert_array_equal(item.agent_rotation, np.eye(3, dtype=np.float32)) # Camera fields populated from sensor_states np.testing.assert_array_equal(item.camera_position, camera_pos) np.testing.assert_array_equal(item.camera_rotation, camera_rot) assert sim.steps == ["turn_left"] def test_collect_room_views_by_room_snapshots_pose_independently(): """Collected RoomViews must have independent snapshots of agent_rotation (not aliased).""" from scenegraph import RoomNode from simulator import RoomView, collect_room_views_by_room rgb = np.ones((4, 4, 3), dtype=np.uint8) * 127 depth = np.full((4, 4), 0.5, dtype=np.float32) room_node = RoomNode( room_id="room_a", center=np.array([1.0, 2.0, 3.0], dtype=np.float32), bbox_extent=[2, 2, 2], ) class _MutatingFakeSim(FakeSimForViews): """Fake sim that mutates agent rotation in-place on step (like real habitat).""" def __init__(self, agent, observations_by_step=None): super().__init__(observations_by_step) self.agent = agent def step(self, action): super().step(action) # In-place mutation – simulates habitat_sim rotating the agent's state step_idx = len(self.steps) self.agent._state.rotation[:] = np.full_like( self.agent._state.rotation, float(step_idx) ) agent = FakeAgentForViews() sim = _MutatingFakeSim( agent=agent, observations_by_step={ 0: {"color_sensor": rgb, "depth_sensor": depth}, 1: {"color_sensor": rgb, "depth_sensor": depth}, }, ) result = collect_room_views_by_room( agent=agent, sim=sim, room_nodes=[room_node], views_per_room=2, habitat_sim_module=FakeHabitatModuleForViews, sensor_uuid="color_sensor", depth_sensor_uuid="depth_sensor", turn_action="turn_left", progress_track=lambda x, desc: x, ) room_views = result["room_a"] assert len(room_views) == 2 rv0, rv1 = room_views[0], room_views[1] # Each snapshot should reflect rotation *at capture time* # – view 0 captured before any step → identity identity = np.eye(3, dtype=np.float32) np.testing.assert_array_equal(rv0.agent_rotation, identity) # – view 1 captured after one in-place mutation → all 1.0 expected_mutated = np.full((3, 3), 1.0, dtype=np.float32) np.testing.assert_array_equal(rv1.agent_rotation, expected_mutated) # rv0 must NOT have been affected by the in-place mutation np.testing.assert_array_equal(rv0.agent_rotation, identity) # Positions are also independent snapshots expected_pos = np.array([1.0, 2.0, 3.0], dtype=np.float32) np.testing.assert_array_equal(rv0.agent_position, expected_pos) np.testing.assert_array_equal(rv1.agent_position, expected_pos) # Post-hoc mutation of one snapshot must not affect the other rv0.agent_rotation[:] = 99.0 np.testing.assert_array_equal(rv1.agent_rotation, expected_mutated) def test_collect_room_views_by_room_snapshots_camera_pose_independently(): """Camera pose from sensor_states must be independently snapshotted (not aliased).""" from types import SimpleNamespace from scenegraph import RoomNode from simulator import RoomView, collect_room_views_by_room rgb = np.ones((4, 4, 3), dtype=np.uint8) * 127 depth = np.full((4, 4), 0.5, dtype=np.float32) room_node = RoomNode( room_id="room_a", center=np.array([1.0, 2.0, 3.0], dtype=np.float32), bbox_extent=[2, 2, 2], ) agent_pos = np.array([1.0, 2.0, 3.0], dtype=np.float32) camera_pos_base = np.array([1.0, 3.5, 3.0], dtype=np.float32) # sensor at +1.5 Y camera_rot_base = np.eye(3, dtype=np.float32) class _MutatingCameraSim(FakeSimForViews): """Fake sim that mutates sensor_states rotation in-place on step.""" def __init__(self, agent, observations_by_step=None): super().__init__(observations_by_step) self.agent = agent def step(self, action): super().step(action) # Mutate the sensor rotation in-place – like real habitat might step_idx = len(self.steps) ds = self.agent._state.sensor_states["depth_sensor"] ds.rotation[:] = np.full_like(ds.rotation, float(step_idx)) agent = FakeAgentForViews() agent._state = FakeAgentState( position=agent_pos.copy(), rotation=np.eye(3, dtype=np.float32), sensor_states={ "depth_sensor": SimpleNamespace( position=camera_pos_base.copy(), rotation=camera_rot_base.copy(), ), }, ) sim = _MutatingCameraSim( agent=agent, observations_by_step={ 0: {"color_sensor": rgb, "depth_sensor": depth}, 1: {"color_sensor": rgb, "depth_sensor": depth}, }, ) result = collect_room_views_by_room( agent=agent, sim=sim, room_nodes=[room_node], views_per_room=2, habitat_sim_module=FakeHabitatModuleForViews, sensor_uuid="color_sensor", depth_sensor_uuid="depth_sensor", turn_action="turn_left", progress_track=lambda x, desc: x, ) room_views = result["room_a"] assert len(room_views) == 2 rv0, rv1 = room_views[0], room_views[1] # view 0 captured before any step → identity rotation np.testing.assert_array_equal(rv0.camera_rotation, np.eye(3, dtype=np.float32)) # view 1 captured after one in-place mutation → all 1.0 expected_mutated = np.full((3, 3), 1.0, dtype=np.float32) np.testing.assert_array_equal(rv1.camera_rotation, expected_mutated) # rv0 rotation must NOT have been affected by in-place mutation np.testing.assert_array_equal(rv0.camera_rotation, np.eye(3, dtype=np.float32)) # Both camera positions are the same (position not mutated in this test) np.testing.assert_array_equal(rv0.camera_position, camera_pos_base) np.testing.assert_array_equal(rv1.camera_position, camera_pos_base) # Post-hoc mutation of one snapshot must not affect the other rv0.camera_rotation[:] = 99.0 np.testing.assert_array_equal(rv1.camera_rotation, expected_mutated) def test_flatten_room_views_preserves_existing_room_view_payloads(): """flatten_room_views should pass through existing RoomView instances unchanged.""" from simulator import RoomView, flatten_room_views depth = np.full((4, 4), 0.5, dtype=np.float32) agent_pos = np.array([1.0, 2.0, 3.0], dtype=np.float32) existing = RoomView( room_id="room_a", view_idx=0, rgb=np.zeros((4, 4, 3), dtype=np.uint8), depth=depth, agent_position=agent_pos, agent_rotation=np.eye(3, dtype=np.float32), ) result = flatten_room_views({"room_a": [existing]}) assert len(result) == 1 assert result[0] is existing assert result[0].depth is depth assert result[0].agent_position is agent_pos # --------------------------------------------------------------------------- # Scene graph depth positioning M1 — config + builder tests # --------------------------------------------------------------------------- def test_config_accepts_bbox_depth_center_strategy(): """Config should accept position_strategy='bbox_depth_center' with valid camera_hfov.""" from scenegraph import SceneGraphBuildConfig config = SceneGraphBuildConfig( position_strategy='bbox_depth_center', camera_hfov_degrees=90.0, ) assert config.position_strategy == 'bbox_depth_center' assert config.camera_hfov_degrees == 90.0 def test_config_rejects_invalid_camera_hfov(): """Config should reject camera_hfov_degrees outside (0, 180).""" from scenegraph import SceneGraphBuildConfig with pytest.raises(ValueError, match="camera_hfov_degrees"): SceneGraphBuildConfig(camera_hfov_degrees=180.0) with pytest.raises(ValueError, match="camera_hfov_degrees"): SceneGraphBuildConfig(camera_hfov_degrees=0.0) with pytest.raises(ValueError, match="camera_hfov_degrees"): SceneGraphBuildConfig(camera_hfov_degrees=-10.0) def test_scene_graph_builder_uses_bbox_depth_center_position_with_identity_pose(): """Bbox depth center with identity rotation should project depth correctly. Setup: - 5x5 depth image, depth=2.0 everywhere - bbox [2,2,2,2] → centre pixel (u=2, v=2) - camera_position [1, 2, 3], identity rotation - hfov=90° → fx = 5 / (2*tan(45°)) = 2.5, cx=2, cy=2 Expected camera point: [(2-2)/2.5*2, -(2-2)/2.5*2, -2] = [0, 0, -2] Expected world position: [1, 2, 3] + [0, 0, -2] = [1, 2, 1] """ from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] depth = np.full((5, 5), 2.0, dtype=np.float32) rgb = np.zeros((5, 5, 3), dtype=np.uint8) cam_pos = np.array([1.0, 2.0, 3.0], dtype=np.float32) cam_rot = np.eye(3, dtype=np.float32) room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=cam_pos.copy(), agent_rotation=cam_rot.copy(), camera_position=cam_pos.copy(), camera_rotation=cam_rot.copy(), ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[2.0, 2.0, 2.0, 2.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig( position_strategy='bbox_depth_center', camera_hfov_degrees=90.0, ) builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] np.testing.assert_array_almost_equal( obj.position, np.array([1.0, 2.0, 1.0], dtype=np.float32), ) assert obj.position_confidence == 1.0 # --------------------------------------------------------------------------- # Camera pose with non-zero sensor height # --------------------------------------------------------------------------- def test_bbox_depth_center_uses_camera_pose_with_sensor_height(): """Bbox depth center with camera pose at non-zero sensor height. Demonstrates that the camera's sensor_height offset (Y direction) is correctly honored: the depth back-projection uses camera_position (which includes [0, sensor_height, 0]) instead of agent_position (which does not). Setup: - camera_position [1, 3.5, 3] (e.g. agent at [1, 2, 3] + sensor_height 1.5) - identity rotation - depth=2.0, hfov=90°, image 5x5, bbox [2,2,2,2] Expected camera point: [0, 0, -2] Expected world position: [1, 3.5, 3] + [0, 0, -2] = [1, 3.5, 1] (NOT [1, 2, 0] which would result from using agent_position [1,2,3]) """ from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] depth = np.full((5, 5), 2.0, dtype=np.float32) rgb = np.zeros((5, 5, 3), dtype=np.uint8) room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), # agent body agent_rotation=np.eye(3, dtype=np.float32), camera_position=np.array([1.0, 3.5, 3.0], dtype=np.float32), # sensor at +1.5 Y camera_rotation=np.eye(3, dtype=np.float32), ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[2.0, 2.0, 2.0, 2.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig( position_strategy='bbox_depth_center', camera_hfov_degrees=90.0, ) builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] # [1, 3.5, 3] + [0, 0, -2] = [1, 3.5, 1] np.testing.assert_array_almost_equal( obj.position, np.array([1.0, 3.5, 1.0], dtype=np.float32), ) assert obj.position_confidence == 1.0 def test_bbox_depth_center_falls_back_without_camera_pose(): """Bbox depth center with only agent_pose (no camera_pose) falls back to room center. When a RoomView has agent_position/agent_rotation but no camera_position/camera_rotation, the builder must NOT use agent_pose for depth positioning (because sensor offset is unknowable). The position must fall back to room center with 0.0 confidence. """ from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] depth = np.full((5, 5), 2.0, dtype=np.float32) rgb = np.zeros((5, 5, 3), dtype=np.uint8) # Has agent_pose but NO camera_pose — builder must NOT use agent_pose for depth room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), agent_rotation=np.eye(3, dtype=np.float32), ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[2.0, 2.0, 2.0, 2.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig(position_strategy='bbox_depth_center') builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] # Falls back to room center with 0.0 confidence np.testing.assert_array_equal(obj.position, np.array([10, 0, 20], dtype=np.float32)) assert obj.position_confidence == 0.0 def test_scene_graph_builder_falls_back_to_room_center_without_depth_pose(): """Bbox depth strategy without depth/pose should fall back to room center + 0.0 confidence.""" from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] rgb = np.zeros((32, 32, 3), dtype=np.uint8) room_view = RoomView(room_id="room_a", view_idx=0, rgb=rgb) # no depth/pose room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[10.0, 20.0, 30.0, 40.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((32, 32), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig(position_strategy='bbox_depth_center') builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] np.testing.assert_array_equal(obj.position, np.array([10, 0, 20], dtype=np.float32)) assert obj.position_confidence == 0.0 def test_scene_graph_builder_falls_back_on_nonfinite_depth(): """Non-finite depth value should trigger fallback to room center + 0.0 confidence.""" from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] depth = np.full((5, 5), np.nan, dtype=np.float32) # non-finite depth rgb = np.zeros((5, 5, 3), dtype=np.uint8) room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), agent_rotation=np.eye(3, dtype=np.float32), ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[2.0, 2.0, 2.0, 2.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig(position_strategy='bbox_depth_center') builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] np.testing.assert_array_equal(obj.position, np.array([10, 0, 20], dtype=np.float32)) assert obj.position_confidence == 0.0 def test_bbox_depth_center_falls_back_on_invalid_bbox_length(): """Bbox with wrong length should fall back to room center + 0.0 confidence.""" from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] depth = np.full((5, 5), 2.0, dtype=np.float32) rgb = np.zeros((5, 5, 3), dtype=np.uint8) room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), agent_rotation=np.eye(3, dtype=np.float32), ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" # bbox with length 2 instead of 4 debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[10.0, 20.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig(position_strategy='bbox_depth_center') builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] np.testing.assert_array_equal(obj.position, np.array([10, 0, 20], dtype=np.float32)) assert obj.position_confidence == 0.0 def test_bbox_depth_center_falls_back_on_nonfinite_bbox(): """Bbox with non-finite values should fall back to room center + 0.0 confidence.""" from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] depth = np.full((5, 5), 2.0, dtype=np.float32) rgb = np.zeros((5, 5, 3), dtype=np.uint8) room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), agent_rotation=np.eye(3, dtype=np.float32), ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" # bbox with NaN value debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[float('nan'), 2.0, 2.0, 2.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig(position_strategy='bbox_depth_center') builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] np.testing.assert_array_equal(obj.position, np.array([10, 0, 20], dtype=np.float32)) assert obj.position_confidence == 0.0 def test_bbox_depth_center_falls_back_on_invalid_depth(): """Depth without ndim attribute (e.g. plain list) should fall back.""" from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] # Plain list — no .ndim attribute; old code would AttributeError depth = [1.0, 2.0] rgb = np.zeros((5, 5, 3), dtype=np.uint8) room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), agent_rotation=np.eye(3, dtype=np.float32), ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[2.0, 2.0, 2.0, 2.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig(position_strategy='bbox_depth_center') builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] np.testing.assert_array_equal(obj.position, np.array([10, 0, 20], dtype=np.float32)) assert obj.position_confidence == 0.0 def test_bbox_depth_center_falls_back_on_invalid_agent_position(): """Agent position with wrong shape should fall back to room center.""" from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] depth = np.full((5, 5), 2.0, dtype=np.float32) rgb = np.zeros((5, 5, 3), dtype=np.uint8) # Position with shape (2,) — can't reshape to (3,) room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=np.array([1.0, 2.0]), agent_rotation=np.eye(3, dtype=np.float32), ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[2.0, 2.0, 2.0, 2.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig(position_strategy='bbox_depth_center') builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] np.testing.assert_array_equal(obj.position, np.array([10, 0, 20], dtype=np.float32)) assert obj.position_confidence == 0.0 def test_bbox_depth_center_uses_quaternion_like_rotation(): """Rotation with transform_vector (quaternion-like) using non-identity mapping. FakeQuaternionRotation implements a 90° Y-rotation: basis x → z, basis y → y, basis z → -x → matrix = [[0, 0, -1], [0, 1, 0], [1, 0, 0]] Camera point (u=2, v=2, depth=2.0): [0, 0, -2] Rotated: [2, 0, 0] World: [1, 2, 3] + [2, 0, 0] = [3, 2, 3] """ from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView class FakeQuaternionRotation: def transform_vector(self, v): v = np.asarray(v, dtype=np.float32).ravel() # 90° Y-rotation: x→[0,0,1], y→[0,1,0], z→[-1,0,0] if np.allclose(v, [1, 0, 0]): return np.array([0, 0, 1], dtype=np.float32) if np.allclose(v, [0, 1, 0]): return np.array([0, 1, 0], dtype=np.float32) if np.allclose(v, [0, 0, 1]): return np.array([-1, 0, 0], dtype=np.float32) return v.copy() room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] depth = np.full((5, 5), 2.0, dtype=np.float32) rgb = np.zeros((5, 5, 3), dtype=np.uint8) room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), agent_rotation=FakeQuaternionRotation(), camera_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), camera_rotation=FakeQuaternionRotation(), ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[2.0, 2.0, 2.0, 2.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig( position_strategy='bbox_depth_center', camera_hfov_degrees=90.0, ) builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] np.testing.assert_array_almost_equal( obj.position, np.array([3.0, 2.0, 3.0], dtype=np.float32), ) assert obj.position_confidence == 1.0 def test_bbox_depth_center_uses_numpy_quaternion_rotation(monkeypatch): """quaternion.as_rotation_matrix path works via monkeypatched quaternion module. Monkeypatches sys.modules['quaternion'] with a fake module where as_rotation_matrix returns a non-identity 3x3 for a sentinel quaternion object. Rotation: 90° Y-rotation → matrix = [[0,0,-1],[0,1,0],[1,0,0]] Camera point (u=2, v=2, depth=2.0): [0, 0, -2] Rotated: [2, 0, 0] World: [1, 2, 3] + [2, 0, 0] = [3, 2, 3] """ import types from scenegraph import RoomNode, SceneGraphBuilder, SceneGraphBuildConfig from simulator import RoomView class FakeQuaternion: """Sentinel quaternion object — no .rotation_matrix or transform_vector.""" pass sentinel_q = FakeQuaternion() fake_module = types.ModuleType("quaternion") def as_rotation_matrix(q): assert q is sentinel_q return np.array([[0, 0, -1], [0, 1, 0], [1, 0, 0]], dtype=np.float32) fake_module.as_rotation_matrix = as_rotation_matrix monkeypatch.setitem(sys.modules, "quaternion", fake_module) room_node = RoomNode(room_id="room_a", center=[10, 0, 20], bbox_extent=[5, 3, 5]) room_nodes = [room_node] depth = np.full((5, 5), 2.0, dtype=np.float32) rgb = np.zeros((5, 5, 3), dtype=np.uint8) room_view = RoomView( room_id="room_a", view_idx=0, rgb=rgb, depth=depth, agent_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), agent_rotation=sentinel_q, camera_position=np.array([1.0, 2.0, 3.0], dtype=np.float32), camera_rotation=sentinel_q, ) room_views = [room_view] text_labels = ["a chair"] hash_bits = torch.zeros(1, 512, dtype=torch.uint8) hash_bits[0, 3] = 1 crop = "crop_data_0" debug_meta_entry = { "selected_indices": [0], "boxes_xyxy": [[2.0, 2.0, 2.0, 2.0]], "scores": [0.91], "labels": ["a chair"], "masks": [np.zeros((5, 5), dtype=np.uint8)], "num_selected": 1, } output = SimpleNamespace( cropped_images=[crop], hash_bits=hash_bits, debug_meta=[debug_meta_entry], ) pipeline = FakePipeline(output=output, calls=[]) config = SceneGraphBuildConfig( position_strategy='bbox_depth_center', camera_hfov_degrees=90.0, ) builder = SceneGraphBuilder(pipeline=pipeline, config=config) graph, artifacts = builder.build_from_room_views( room_nodes=room_nodes, room_views=room_views, text_labels=text_labels, ) obj = graph.objects["room_a_v000_m00"] np.testing.assert_array_almost_equal( obj.position, np.array([3.0, 2.0, 3.0], dtype=np.float32), ) assert obj.position_confidence == 1.0