"""Stage 3 acceptance for the frame adapters. The adapters are the boundary between the acquisition thread's arrays and the scene, so the tests are about *what they publish and what they refuse*: a field in the right grid, read-only, with the right unit and quality — and nothing at all when the grid does not match, with a diagnostic that says which shapes were seen. """ from __future__ import annotations from dataclasses import dataclass from typing import Optional import numpy as np import pytest from orisys_app_shared.scene3d import synthetic from orisys_app_shared.scene3d.contracts import ( ExpectedGeometry, FrameIdentity, ThreeDError, validate_surface_frame, ) from orisys_studio.scene3d.adapters import ( DEPTH_RESPONSE, FLOW_MAGNITUDE, BoxFrameAdapter, FingerFrameAdapter, flow_magnitude, ) @dataclass class FakePacket: """The parts of ``SdkPacket`` the adapters look at. The provenance flags default to *stated fresh*: that is what both production workers now write, and it keeps the older tests about grids and units about grids and units. A test that wants a stale or unstated field says so — that is the behaviour under test. """ flow: Optional[np.ndarray] = None depth: Optional[np.ndarray] = None valid_mask: Optional[np.ndarray] = None frame_id: int = 1 invalid_reason: str = "" flow_valid: Optional[bool] = True flow_frame_id: Optional[int] = None depth_valid: Optional[bool] = True depth_frame_id: Optional[int] = None forces_valid: Optional[bool] = True def _identity(generation: int = 1) -> FrameIdentity: return FrameIdentity("live", 1, 1, 0.1, generation=generation) @pytest.fixture def scene(): return synthetic.poc_scene() # ------------------------------------------------------------------- flow helper def test_the_flow_magnitude_handles_both_shapes(): flow = np.zeros((2, 3, 2), dtype=np.float32) flow[..., 0] = 3.0 flow[..., 1] = 4.0 assert np.allclose(flow_magnitude(FakePacket(flow=flow)), 5.0) planar = np.arange(4, dtype=np.float32).reshape(2, 2) assert np.array_equal(flow_magnitude(FakePacket(flow=planar)), planar) assert flow_magnitude(FakePacket()) is None assert flow_magnitude(FakePacket(flow=np.zeros((2, 2, 3), dtype=np.float32))) is None def test_the_finger_adapter_prefers_the_quantity_the_operator_chose(scene): """The choice between the two quantities is a display setting, honoured per frame. The depth response is what an operator reads as "how deep", so it is the default — and when the frame carries it, that is what is drawn. """ adapter = FingerFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) assert adapter.preferred_field == "depth" depth = np.full(scene.shape_hw, 2.5, dtype=np.float32) flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32) flow[..., 0] = 7.0 # |flow| = 7 px frame = adapter.snapshot(FakePacket(flow=flow, depth=depth), runtime=None, identity=_identity()) assert frame is not None assert frame.scalar_descriptor == DEPTH_RESPONSE assert frame.scalar_descriptor.unit == "1" assert np.allclose(np.asarray(frame.scalar), 2.5) assert adapter.last_substitution is None, "the chosen field was drawn; nothing to say" adapter.set_preferred_field("flow") frame = adapter.snapshot(FakePacket(flow=flow, depth=depth), runtime=None, identity=_identity()) assert frame is not None assert frame.scalar_descriptor == FLOW_MAGNITUDE assert np.allclose(np.asarray(frame.scalar), 7.0) assert adapter.last_substitution is None def test_a_frame_that_cannot_honour_the_choice_says_which_quantity_it_drew(scene): """Falling back is allowed; falling back silently is not. The frame is still a measurement and the legend names it, but the *setting* said something else, and the pane has to be able to say so instead of letting the picture read as the quantity the operator asked for. """ adapter = FingerFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) # No depth in this frame: the flow is all it has. flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32) flow[..., 0] = 4.0 # |flow| = 4 px frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity()) assert frame is not None assert frame.scalar_descriptor == FLOW_MAGNITUDE note = adapter.last_substitution assert note is not None and note.code == "field_substituted" assert note.details["wanted"] == "depth" and note.details["shown"] == "flow" assert "no depth response" in note.details["why"] # ...and the note goes away as soon as the chosen field can be drawn again. depth = np.full(scene.shape_hw, 1.0, dtype=np.float32) frame = adapter.snapshot( FakePacket(flow=flow, depth=depth), runtime=None, identity=_identity() ) assert frame is not None and frame.scalar_descriptor == DEPTH_RESPONSE assert adapter.last_substitution is None def test_a_depth_response_on_another_grid_is_refused_not_resized(scene): """The depth field gets the same treatment as the flow: no resizing, ever.""" adapter = FingerFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) wrong = np.zeros((7, 7), dtype=np.float32) frame = adapter.snapshot(FakePacket(depth=wrong), runtime=None, identity=_identity()) assert frame is None, "a depth response that is not on the scene's grid was coloured" item = adapter.last_diagnostic assert item is not None and item.code == "field_domain_mismatch" assert item.details["depth"] == "7x7" # ----------------------------------------------------------------- finger adapter def test_the_finger_adapter_publishes_a_field_already_in_the_domain(scene): adapter = FingerFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32) flow[0, 0, 0] = 3.0 frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity()) assert frame is not None assert validate_surface_frame(frame) == () assert frame.geometry_id == scene.geometry_id assert frame.field_domain_id == scene.field_domain_id assert frame.scalar_descriptor == FLOW_MAGNITUDE assert frame.scalar_descriptor.unit == "px" assert frame.scalar_descriptor.quality == "approximate" assert np.asarray(frame.scalar)[0, 0] == 3.0 assert np.asarray(frame.valid).all() assert not np.asarray(frame.scalar).flags["WRITEABLE"] assert frame.displacement_sensor_m is None, ( "an uncalibrated displacement must not be invented" ) def test_the_finger_adapter_remaps_through_the_unroll_map(): map_x = np.array([[0, 1, 2], [0, 1, 2], [0, 1, 2]], dtype=np.float32) map_y = np.array([[2, 2, 2], [1, 1, 1], [0, 0, 0]], dtype=np.float32) adapter = FingerFrameAdapter( geometry_id="g", field_domain_id="d", shape_hw=(3, 3), map_x=map_x, map_y=map_y, ) flow = np.zeros((3, 3, 2), dtype=np.float32) flow[..., 0] = 2.0 frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity()) assert frame is not None assert np.allclose(np.asarray(frame.scalar), 2.0) assert np.asarray(frame.valid).all() def test_a_remap_with_a_hole_is_invalid_there_and_does_not_read_as_zero(): map_x = np.array([[0, -1], [0, 0]], dtype=np.float32) map_y = np.array([[0, -1], [0, 0]], dtype=np.float32) adapter = FingerFrameAdapter( geometry_id="g", field_domain_id="d", shape_hw=(2, 2), map_x=map_x, map_y=map_y, ) flow = np.full((2, 2, 2), 1.0, dtype=np.float32) frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity()) assert frame is not None assert np.asarray(frame.valid).tolist() == [[True, False], [True, True]] assert np.isnan(np.asarray(frame.scalar)[0, 1]) def test_the_finger_adapter_refuses_a_grid_it_cannot_place(scene): """No remap and the wrong grid: say which shapes were seen, and publish nothing.""" adapter = FingerFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) flow = np.zeros((40, 30, 2), dtype=np.float32) frame = adapter.snapshot(FakePacket(flow=flow), runtime=None, identity=_identity()) assert frame is None item = adapter.last_diagnostic assert item is not None and item.code == "field_domain_mismatch" assert item.details["flow"] == "40x30" assert item.details["domain"] == f"{scene.shape_hw[0]}x{scene.shape_hw[1]}" def test_the_finger_adapter_without_flow_says_so(scene): adapter = FingerFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) assert adapter.snapshot(FakePacket(), runtime=None, identity=_identity()) is None assert adapter.last_diagnostic is not None assert adapter.last_diagnostic.code == "field_unavailable" def test_the_finger_adapter_can_be_built_from_a_domain(scene): from orisys_app_shared.scene3d.legacy import load_legacy_domain map_path = _run_map() if map_path is None: pytest.skip("the shipped unroll map is not present") domain = load_legacy_domain(map_path) adapter = FingerFrameAdapter.from_domain(domain, geometry_id="geom") assert adapter.shape_hw == domain.shape_hw assert adapter.field_domain_id == domain.domain_id assert adapter.map_x is not None and adapter.map_y is not None, ( "the domain must carry the unroll map for the adapter to use" ) assert domain.source_hw == (288, 512), domain.source_hw def test_the_shipped_map_cannot_be_applied_to_the_shipped_flow(): """Why the app must not remap the finger's flow: the map is domain→*source*. The unroll map is the same size as the UV domain, but its values are pixel coordinates in the 512x288 source image — not in the 300x280 domain. The live flow is already computed on the unrolled frame, so feeding it through this map would be a double remap: it would index the flow at source-pixel coordinates. This test pins the reason the app passes no map, so a later "improvement" has to argue with it. Note the asymmetry that makes the mistake look plausible: in x the map exceeds the domain's column count (so it would read out of range), while in y it stays below the row count (so it would silently read the wrong rows). Half of a double remap crashes and half of it lies. """ map_path = _run_map() if map_path is None: pytest.skip("the shipped unroll map is not present") from orisys_app_shared.scene3d.legacy import load_legacy_domain domain = load_legacy_domain(map_path) assert domain.map_x is not None and domain.map_y is not None rows, columns = domain.shape_hw source_rows, source_columns = domain.source_hw assert (rows, columns) == (300, 280) and (source_rows, source_columns) == (288, 512) # Bounded by the source, not by the domain: that is what makes them source pixels. assert 0.0 <= float(domain.map_x[domain.map_x >= 0].max()) <= source_columns - 1 assert 0.0 <= float(domain.map_y[domain.map_y >= 0].max()) <= source_rows - 1 assert float(domain.map_x.max()) > columns - 1, ( "x would index past the last column of a 280-wide flow field" ) assert float(domain.map_y.max()) > rows - 1 or source_rows <= rows, ( "y stays inside the 300-row field: the y half of the mistake is silent" ) def test_the_live_flow_is_already_the_scene_domain(): """The measurement behind that decision, taken from the shipping config. ``camera.resolution_stitch`` is what ``Sensor`` sizes its flow buffer from and what the unroll stage outputs, so the flow grid *is* the UV domain. If a future config changes that, this test fails and the adapter wiring has to be revisited rather than the numbers quietly not matching. """ import json from pathlib import Path config = Path(__file__).resolve().parents[1] / "config" / "sensors" / "finger" map_path = _run_map() if not (config / "sensor.json").is_file() or map_path is None: pytest.skip("the finger config and the unroll map are needed") camera = json.loads((config / "sensor.json").read_text(encoding="utf-8"))["camera"] resolution = camera["resolution_stitch"] # Sensor builds `flow` as (resolution_stitch[1], resolution_stitch[0]). flow_hw = (int(resolution[1]), int(resolution[0])) from orisys_app_shared.scene3d.legacy import load_legacy_domain domain = load_legacy_domain(map_path) assert flow_hw == tuple(domain.shape_hw) == (300, 280) def test_the_finger_adapter_does_not_colour_outside_the_uv_island(scene): """The device mask is what says where the surface is; outside it there is none.""" adapter = FingerFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32) flow[..., 0] = 1.0 mask = np.zeros(scene.shape_hw, dtype=bool) mask[1, 1] = True frame = adapter.snapshot( FakePacket(flow=flow, valid_mask=mask), runtime=None, identity=_identity() ) assert frame is not None valid = np.asarray(frame.valid) assert valid.sum() == 1 and bool(valid[1, 1]) assert np.asarray(frame.scalar)[0, 0] == 1.0, ( "the scalar is kept as measured; only validity says it is not a measurement" ) def test_a_device_mask_from_another_grid_refuses_the_frame(scene): """Neither stretching it nor ignoring it is honest — so the frame is refused.""" adapter = FingerFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) flow = np.ones((*scene.shape_hw, 2), dtype=np.float32) wrong = np.zeros((7, 7), dtype=bool) frame = adapter.snapshot( FakePacket(flow=flow, valid_mask=wrong), runtime=None, identity=_identity() ) assert frame is None, "a mask from another grid must not widen the trusted area" item = adapter.last_diagnostic assert item is not None and item.code == "field_domain_mismatch" assert item.details["mask"] == "7x7" assert item.details["domain"] == f"{scene.shape_hw[0]}x{scene.shape_hw[1]}" # ------------------------------------------------- field provenance (stage 6, R2) def _finger(scene) -> FingerFrameAdapter: return FingerFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) def test_a_stale_flow_is_refused_instead_of_coloured(scene): """R2: the array is still finite, but it is not this frame's measurement. This is the audit's reproduction: a packet with ``valid=False``/``stale_forces`` and a filled ``flow`` used to be coloured as valid data with no diagnostic. """ adapter = _finger(scene) flow = np.full((*scene.shape_hw, 2), 4.0, dtype=np.float32) frame = adapter.snapshot( FakePacket( flow=flow, flow_valid=False, invalid_reason="compute: ValueError: boom", ), runtime=None, identity=_identity(), ) assert frame is None, "a failed chain's leftover flow was coloured as current" item = adapter.last_diagnostic assert item is not None and item.code == "field_stale" assert "boom" in item.message def test_an_unstated_flow_is_unverified_not_fresh(scene): """No statement is not the same as "it is mine".""" adapter = _finger(scene) flow = np.full((*scene.shape_hw, 2), 1.0, dtype=np.float32) frame = adapter.snapshot( FakePacket(flow=flow, flow_valid=None), runtime=None, identity=_identity() ) assert frame is None item = adapter.last_diagnostic assert item is not None and item.code == "field_unverified" def test_flow_computed_for_another_frame_is_refused(scene): adapter = _finger(scene) flow = np.full((*scene.shape_hw, 2), 2.0, dtype=np.float32) frame = adapter.snapshot( FakePacket(flow=flow, flow_frame_id=3), runtime=None, identity=_identity(), # frame_id == 1 ) assert frame is None item = adapter.last_diagnostic assert item is not None and item.code == "field_stale" assert item.details["computed"] == "3" and item.details["frame"] == "1" def test_a_fresh_flow_records_the_frame_it_belongs_to(scene): """The identity travels with the frame, so the display side can re-check it.""" adapter = _finger(scene) flow = np.full((*scene.shape_hw, 2), 3.0, dtype=np.float32) frame = adapter.snapshot( FakePacket(flow=flow, flow_frame_id=1), runtime=None, identity=_identity() ) assert frame is not None assert frame.computed_frame_id == 1 assert validate_surface_frame(frame) == () def test_the_box_adapter_skips_a_stale_depth_and_uses_the_flow(scene): """A failed deformation chain does not have to cost the flow as well.""" adapter = BoxFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) depth = np.full(scene.shape_hw, 0.5, dtype=np.float32) flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32) flow[..., 0] = 1.0 frame = adapter.snapshot( FakePacket(depth=depth, depth_valid=False, flow=flow), _identity(), ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw), ) assert frame is not None and frame.scalar_descriptor == FLOW_MAGNITUDE assert np.allclose(np.asarray(frame.scalar), 1.0) def test_the_box_adapter_refuses_when_no_field_is_its_own(scene): adapter = BoxFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) depth = np.full(scene.shape_hw, 0.5, dtype=np.float32) frame = adapter.snapshot( FakePacket(depth=depth, depth_valid=False), _identity(), ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw), ) assert frame is None item = adapter.last_diagnostic assert item is not None and item.code == "field_stale" def _run_map(): from pathlib import Path root = Path(__file__).resolve().parents[1] / "config" / "sensors" / "finger" / "assets" for name in ("30fps-0703", "visualization"): path = root / name / "unroll_map_uv_s0.4.npz" if path.is_file(): return path for path in sorted(root.glob("*/unroll_map_uv*.npz")): return path return None # -------------------------------------------------------------------- box adapter def test_the_box_adapter_prefers_the_depth_response(scene): adapter = BoxFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) depth = np.full(scene.shape_hw, 0.25, dtype=np.float32) flow = np.ones((*scene.shape_hw, 2), dtype=np.float32) frame = adapter.snapshot( FakePacket(depth=depth, flow=flow), _identity(), ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw), ) assert frame is not None assert frame.scalar_descriptor == DEPTH_RESPONSE assert frame.scalar_descriptor.unit == "1", "a deformation response is not a length" assert np.allclose(np.asarray(frame.scalar), 0.25) def test_the_box_adapter_falls_back_to_the_flow_magnitude(scene): adapter = BoxFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) flow = np.zeros((*scene.shape_hw, 2), dtype=np.float32) flow[..., 0] = 1.0 frame = adapter.snapshot( FakePacket(flow=flow), _identity(), ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw), ) assert frame is not None and frame.scalar_descriptor == FLOW_MAGNITUDE assert np.allclose(np.asarray(frame.scalar), 1.0) def test_the_device_mask_wins_over_the_finiteness_test(scene): adapter = BoxFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) depth = np.ones(scene.shape_hw, dtype=np.float32) mask = np.ones(scene.shape_hw, dtype=bool) mask[0, 0] = False frame = adapter.snapshot( FakePacket(depth=depth, valid_mask=mask), _identity(), ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw), ) assert frame is not None assert not bool(np.asarray(frame.valid)[0, 0]) assert np.asarray(frame.valid).sum() == mask.sum() def test_the_box_adapter_refuses_a_device_with_another_domain(scene): adapter = BoxFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) frame = adapter.snapshot( FakePacket(depth=np.ones(scene.shape_hw, dtype=np.float32)), _identity(), ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, (9, 9)), ) assert frame is None assert adapter.last_diagnostic is not None assert adapter.last_diagnostic.code == "field_domain_mismatch" def test_the_box_adapter_with_nothing_usable_says_what_it_saw(scene): adapter = BoxFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) frame = adapter.snapshot( FakePacket(depth=np.ones((5, 5), dtype=np.float32)), _identity(), ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw), ) assert frame is None assert adapter.last_diagnostic is not None assert adapter.last_diagnostic.details["depth"] == "5x5" def test_a_published_frame_must_not_share_memory_with_the_packet(scene): """The producer keeps writing its arrays; the frame must not alias them.""" adapter = BoxFrameAdapter( geometry_id=scene.geometry_id, field_domain_id=scene.field_domain_id, shape_hw=scene.shape_hw, ) depth = np.zeros(scene.shape_hw, dtype=np.float32) frame = adapter.snapshot( FakePacket(depth=depth), _identity(), ExpectedGeometry("box", "rev", scene.geometry_id, scene.field_domain_id, scene.shape_hw), ) assert frame is not None assert not frame.scalar.flags["WRITEABLE"] with pytest.raises(ValueError): np.asarray(frame.scalar)[0, 0] = 5.0