"""Stage 6 acceptance for the *device's own* geometry description. The review's R3 finding was that the internal-consistency check was never wired to the acquisition side: the adapter was built from the asset, so "the frame matches the scene" was a sentence the asset wrote about itself. These tests are about the other half — the description the acquisition side derives from the files the runtime actually loads, and what happens when those files and the package disagree. The bundle used here is the shipped one (``apps/studio/config/sensors/finger``); the tests that need it skip when it is absent, because the module is also imported by runs that only have the SDK. """ from __future__ import annotations import io import json import shutil import zipfile from dataclasses import replace from pathlib import Path import numpy as np import pytest from orisys_app_shared.acquisition.geometry import ( describe_sensor_geometry, map_path_for, ) from orisys_app_shared.scene3d.contracts import ThreeDError from orisys_app_shared.scene3d.package import PackagedAssetLoader BUNDLE = ( Path(__file__).resolve().parents[2] / "apps" / "studio" / "config" / "sensors" / "finger" ) PACKAGE = BUNDLE / "scene" / "finger-display-1.0.0.oryscene" #: The **runtime** SDK asset: the mesh the device actually runs, in source units. SDK_ASSET_DIR = Path(r"D:\orisys_ws\orisys-sdk\sensors\finger\assets\30fps-0703") SDK_MESH = SDK_ASSET_DIR / "mesh.npz" #: The physical statement about that asset: one source unit is one millimetre. #: Documented in ``tools/scene_assets/README.md`` and stated by the compiler's #: ``--meters-per-source-unit``; it is the *design* export that is in other units. SDK_METERS_PER_SOURCE_UNIT = 0.001 #: What the SDK asset measures when the file above is not on this machine (metres): #: the same 15.81 x 15.0 x 7.5 mm envelope, kept as a documented constant so the #: guard still runs — and can still fail — on a checkout without the SDK. SENSOR_ENVELOPE_FALLBACK_M = np.array([0.01580961, 0.015, 0.0075]) #: How far the shipped package may be from the SDK asset's own envelope. The defect #: this guards against is a 54.38x error, so one percent is three orders of magnitude #: tighter and still leaves room for float32 storage and the export's own rounding. ENVELOPE_RELATIVE_TOLERANCE = 0.01 def sensor_frame_envelope_m() -> np.ndarray: """The sensing surface's true envelope, in the sensor frame, in metres. Derived from the **SDK asset mesh** — the file the device runs — and *not* from the package: a guard that reads its expectation out of the thing it is checking cannot fail. ``manifest.length_unit`` is ``m`` and ``coordinate_frame`` is ``sensor``, so the package's arrays are directly comparable with this. """ if not SDK_MESH.is_file(): return SENSOR_ENVELOPE_FALLBACK_M.copy() with np.load(SDK_MESH, allow_pickle=False) as mesh: extent = np.ptp(np.asarray(mesh["vertices"], dtype=np.float64), axis=0) return extent * SDK_METERS_PER_SOURCE_UNIT def _package_arrays(package: Path) -> tuple[dict, dict[str, np.ndarray]]: """The raw manifest and entries, read straight out of the zip. Deliberately not through the loader: this guard is about the *written file*, and a check that goes through code which might normalise or repair would not see what actually shipped. """ with zipfile.ZipFile(package) as archive: manifest = json.loads(archive.read("manifest.json")) arrays = { name: np.load(io.BytesIO(archive.read(name))) for name in archive.namelist() if name.endswith(".npy") } return manifest, arrays def sensing_envelope_m(package: Path) -> np.ndarray: _, arrays = _package_arrays(package) return np.ptp(np.asarray(arrays["sensing/positions.npy"], dtype=np.float64), axis=0) def assert_package_geometry_is_sensor_scale(package: Path) -> None: """The scale check: the shipped envelope must be the SDK asset's envelope. Two independent statements inside the package have to be right, and the second is the one that reaches the renderer's own framing: * ``sensing/positions.npy``, in the metres ``manifest.length_unit`` claims; * ``manifest.bounds_sensor_m``, which the loader trusts for the camera. """ expected = sensor_frame_envelope_m() manifest, arrays = _package_arrays(package) length_unit = str(manifest.get("length_unit") or "") assert length_unit == "m", ( f"{package.name} declares length_unit={length_unit!r}; this guard compares " "metres, so a different unit statement has to be converted first" ) extent = np.ptp(np.asarray(arrays["sensing/positions.npy"], dtype=np.float64), axis=0) np.testing.assert_allclose( extent, expected, rtol=ENVELOPE_RELATIVE_TOLERANCE, err_msg=( f"{package.name}: the sensing envelope in the units the manifest claims " "(m) does not match the SDK asset's own sensor-frame envelope. Measured " f"{np.sort(extent)} m, the SDK asset states {np.sort(expected)} m " f"(a factor of {float(np.sort(extent)[-1] / np.sort(expected)[-1]):.5f} on " "the largest axis). This is the unit statement, not a formatting choice: " "the design export is not in millimetres, and calling its units " "millimetres compiles cleanly and is 54.38x too large." ), ) bounds = np.asarray(manifest.get("bounds_sensor_m"), dtype=np.float64) assert bounds.shape == (2, 3), f"{package.name}: bounds_sensor_m is {bounds.shape}" np.testing.assert_allclose( bounds[1] - bounds[0], expected, rtol=ENVELOPE_RELATIVE_TOLERANCE, err_msg=( f"{package.name}: manifest.bounds_sensor_m spans {bounds[1] - bounds[0]} m, " f"but the SDK asset states {expected} m: the camera would frame the wrong box" ), ) def _require_bundle() -> None: if not (BUNDLE / "sensor.json").is_file(): pytest.skip("the shipped finger bundle is not present") def _require_package() -> None: _require_bundle() if not PACKAGE.is_file(): pytest.skip("the compiled finger package is not present") def _bundle_map_name() -> str: """The map filename this bundle's own config resolves to — derived, never hard-coded. The filename follows ``camera.source_scale`` (``scaled_map_cache_path``), and the bundle moved from ``0.4`` to ``0.42`` when the package was recompiled from the authoritative design export. Deriving it here keeps these tests about the behaviour they name — the device description comes from the files the runtime loads — instead of about one filename that any scale change renames. """ _require_bundle() config = json.loads((BUNDLE / "sensor.json").read_text(encoding="utf-8")) scale = config.get("camera", {}).get("source_scale") return map_path_for(BUNDLE / "assets" / "30fps-0703", scale).name def test_the_shipped_package_looks_at_the_sensing_side_of_the_surface(): """The default camera must be in front of the plate, not behind it. The shipped ``default`` preset used to be a fixed vector, which is only correct if the export's frame happens to equal the sensor frame. The design export's does not: that preset ended up 122.7° from the sensing surface's own normal — behind the surface — where the frame hides essentially all of it. Both the package's baked preset and the presets the loader derives are checked here. """ _require_package() from orisys_app_shared.scene3d.contracts import DisplaySettings from orisys_app_shared.scene3d.legacy import ( default_finger_presets, sensing_surface_normal, ) from orisys_app_shared.scene3d.package import read_package, scene_from_contents scene = scene_from_contents(read_package(PACKAGE)) sensing = scene.sensing_part() normal = sensing_surface_normal( np.asarray(sensing.positions_sensor_m, dtype=np.float64), np.asarray(sensing.triangles, dtype=np.int64), ) frame = _package_arrays(PACKAGE)[0] baked = next(item for item in frame["camera_presets"] if item["name"] == "default") centre = np.asarray(scene.bounds_sensor_m, dtype=np.float64).mean(axis=0) def angle(preset) -> float: eye = np.asarray(preset.eye_sensor_m, dtype=np.float64) offset = eye - centre offset = offset / np.linalg.norm(offset) return float(np.degrees(np.arccos(np.clip(offset @ normal, -1.0, 1.0)))) baked_angle = angle( type(scene.camera_presets[0])( name="default", eye_sensor_m=tuple(baked["eye"]), target_sensor_m=tuple(baked["target"]), up_sensor=tuple(baked["up"]), vertical_fov_rad=float(baked["fov"]), ) ) assert baked_angle < 60.0, ( f"the baked default preset is {baked_angle:.1f}° from the sensing surface's " "normal: it points at the back of the plate" ) loaded_angle = angle(next(p for p in scene.camera_presets if p.name == "default")) assert loaded_angle < 60.0, f"the loaded default preset is {loaded_angle:.1f}° off" derived = default_finger_presets( scene.bounds_sensor_m, positions_sensor_m=np.asarray(sensing.positions_sensor_m, dtype=np.float64), triangles=np.asarray(sensing.triangles, dtype=np.int64), ) assert angle(derived[0]) < 40.0 assert all(abs(angle(preset)) <= 90.0 + 1e-6 for preset in derived) # The housing stays hidden by default: the operator turns it on for inspection. assert DisplaySettings().housing_visible is False def test_a_preset_behind_the_surface_is_replaced_on_load(tmp_path): """The repair path, driven with a deliberately wrong preset. A package whose ``default`` preset points at the back of the plate is loaded through the real container, and the loader replaces the whole set with one derived from the mesh. The point is that the guard is on the *consuming* side: a package baked by an older compiler is corrected without being rebuilt. """ _require_package() from dataclasses import replace from orisys_app_shared.scene3d.legacy import sensing_surface_normal from orisys_app_shared.scene3d.package import ( PackagedAssetLoader, read_package, scene_from_contents, write_package, ) scene = scene_from_contents(read_package(PACKAGE)) normal = sensing_surface_normal( np.asarray(scene.sensing_part().positions_sensor_m, dtype=np.float64), np.asarray(scene.sensing_part().triangles, dtype=np.int64), ) centre = np.asarray(scene.bounds_sensor_m, dtype=np.float64).mean(axis=0) backwards = tuple(centre - normal * 0.02) broken = replace( scene.camera_presets[0], eye_sensor_m=backwards, target_sensor_m=tuple(centre), ) assert float(np.dot(np.asarray(backwards) - centre, normal)) < 0.0, "fixture is not behind" path = tmp_path / "backwards.oryscene" write_package(replace(scene, camera_presets=(broken,) + tuple(scene.camera_presets[1:])), path) # The file keeps what was written... assert _package_arrays(path)[0]["camera_presets"][0]["eye"] == list(backwards) # ...and the loader does not present a view of the back of the sensor. from orisys_app_shared.scene3d.contracts import ExpectedGeometry expected = ExpectedGeometry( "finger", "*", scene.geometry_id, scene.field_domain_id, scene.shape_hw, scene.source_geometry_id, ) loaded = PackagedAssetLoader().load(path, expected) fixed = next(preset for preset in loaded.camera_presets if preset.name == "default") offset = np.asarray(fixed.eye_sensor_m, dtype=np.float64) - centre offset = offset / np.linalg.norm(offset) angle = float(np.degrees(np.arccos(np.clip(offset @ normal, -1.0, 1.0)))) assert angle < 60.0, f"the loader kept a preset {angle:.1f}° off the surface normal" def test_the_shipped_package_is_at_sensor_scale_not_design_scale(): """The scale guard that was missing. Every other check in this file is about *identity*: the geometry id, the domain id and the source-geometry id are digests of unit-free data, so a package whose geometry is 54.38x too large passes all of them — the ids are correct and only the scale is wrong (measured: ``sensing/positions.npy`` at 0.8157 x 0.4079 x 0.8598 m in a package that claims millimetres-as-metres). This asserts the package agrees with the SDK asset mesh's own physical envelope. """ _require_package() assert_package_geometry_is_sensor_scale(PACKAGE) def test_the_guard_itself_discriminates(): """A guard that cannot fail is not a guard: 54.38x must be rejected.""" _require_package() expected = sensor_frame_envelope_m() too_large = expected * 54.381958120648946 with pytest.raises(AssertionError): np.testing.assert_allclose( too_large, expected, rtol=ENVELOPE_RELATIVE_TOLERANCE ) assert float(np.max(too_large / expected)) == pytest.approx(54.38196, rel=1e-4) def test_the_expected_envelope_comes_from_the_sdk_asset_not_the_package(): """The expectation is external, so a wrong package cannot define it as right.""" if not SDK_MESH.is_file(): pytest.skip("the SDK asset mesh is not present on this machine") with np.load(SDK_MESH, allow_pickle=False) as mesh: extent_source_units = np.ptp( np.asarray(mesh["vertices"], dtype=np.float64), axis=0 ) assert np.allclose(np.sort(extent_source_units), [7.5, 15.0, 15.81], atol=0.02), ( extent_source_units ) assert np.allclose( np.sort(sensor_frame_envelope_m()), [0.0075, 0.015, 0.01581], atol=2e-5 ) if PACKAGE.is_file(): # And it really is a different statement from the package's own numbers. assert not np.allclose( sensing_envelope_m(PACKAGE), extent_source_units ), "the SDK asset and the package are in the same units: this guard would be vacuous" def test_the_map_name_follows_the_scale_the_sdk_would_use(tmp_path): assert map_path_for(tmp_path, 0.4).name == "unroll_map_uv_s0.4.npz" assert map_path_for(tmp_path, 1.0).name == "unroll_map_uv.npz" assert map_path_for(tmp_path, None).name == "unroll_map_uv.npz" def test_the_device_description_comes_from_the_files_the_runtime_loads(): _require_bundle() device = describe_sensor_geometry("finger", BUNDLE / "sensor.json") assert device.verified, device.detail assert device.expected.sensor_model == "finger" # The metre-scaled identity needs the operator's unit statement; a device that # cannot make it must say nothing rather than echo the asset's id back. assert device.expected.geometry_id == "" assert device.expected.field_domain_id assert device.expected.source_geometry_id assert tuple(device.expected.shape_hw) == (300, 280) assert Path(device.detail["map"]).name == _bundle_map_name() assert device.detail["shape_hw"] == "300x280" def test_the_shipped_package_passes_the_shipped_device_description(): _require_package() device = describe_sensor_geometry("finger", BUNDLE / "sensor.json") assets = PackagedAssetLoader().load(PACKAGE, device.expected) assert assets.source_geometry_id == device.expected.source_geometry_id assert assets.compatible_sensor_model == "finger" def test_a_same_size_different_mapping_is_refused(tmp_path): """The risk the review named: 300x280 is not one mapping.""" _require_package() fake = tmp_path / "finger" shutil.copytree(BUNDLE, fake) map_path = fake / "assets" / "30fps-0703" / _bundle_map_name() with np.load(map_path, allow_pickle=False) as handle: data = {key: handle[key] for key in handle.files} # The same grid, the same valid area — a different mapping. data["map_x"] = np.asarray(data["map_x"], dtype=np.float32).copy() data["map_x"][0, 0] = float(data["map_x"][0, 0]) + 3.0 np.savez(map_path, **data) shipped = describe_sensor_geometry("finger", BUNDLE / "sensor.json") other = describe_sensor_geometry("finger", fake / "sensor.json") assert tuple(other.expected.shape_hw) == tuple(shipped.expected.shape_hw) assert other.expected.field_domain_id != shipped.expected.field_domain_id, ( "two different mappings of the same size produced the same domain id" ) with pytest.raises(ThreeDError) as excinfo: PackagedAssetLoader().load(PACKAGE, other.expected) assert excinfo.value.diagnostic.code == "field_domain_mismatch" def test_a_changed_mesh_behind_the_same_map_is_refused(tmp_path): """The other half: the map is identical, the surface behind it is not.""" _require_package() device = describe_sensor_geometry("finger", BUNDLE / "sensor.json") other_geometry = replace(device.expected, source_geometry_id="somebody-elses-surface") with pytest.raises(ThreeDError) as excinfo: PackagedAssetLoader().load(PACKAGE, other_geometry) assert excinfo.value.diagnostic.code == "geometry_mismatch" def test_a_bundle_that_cannot_be_described_is_unverified_not_a_match(tmp_path): _require_bundle() unknown = tmp_path / "nowhere" device = describe_sensor_geometry("finger", unknown) assert device.verified is False assert device.expected.geometry_id == "" assert device.expected.source_geometry_id == "" assert "could not be located" in device.detail["reason"] def test_a_bundle_whose_map_disagrees_with_the_config_is_refused(tmp_path): """``resolution_stitch`` and the map are two statements of one grid.""" _require_bundle() fake = tmp_path / "finger" shutil.copytree(BUNDLE, fake) config = json.loads((fake / "sensor.json").read_text(encoding="utf-8")) config["camera"]["resolution_stitch"] = [280, 299] (fake / "sensor.json").write_text( json.dumps(config, ensure_ascii=False), encoding="utf-8" ) with pytest.raises(ThreeDError) as excinfo: describe_sensor_geometry("finger", fake / "sensor.json") assert excinfo.value.diagnostic.code == "field_domain_mismatch" def test_a_missing_map_names_the_file_it_looked_for(tmp_path): _require_bundle() fake = tmp_path / "finger" (fake / "assets" / "30fps-0703").mkdir(parents=True) (fake / "manifest.json").write_text( json.dumps({"assets_dir": "assets/30fps-0703"}), encoding="utf-8" ) shutil.copy(BUNDLE / "sensor.json", fake / "sensor.json") with pytest.raises(ThreeDError) as excinfo: describe_sensor_geometry("finger", fake / "sensor.json") assert excinfo.value.diagnostic.code == "asset_rejected" assert _bundle_map_name() in excinfo.value.diagnostic.message def test_a_bundle_without_an_assets_dir_is_unverified(tmp_path): fake = tmp_path / "finger" fake.mkdir() (fake / "manifest.json").write_text(json.dumps({"name": "finger"}), encoding="utf-8") (fake / "sensor.json").write_text(json.dumps({"camera": {}}), encoding="utf-8") device = describe_sensor_geometry("finger", fake / "sensor.json") assert device.verified is False assert "assets_dir" in device.detail["reason"]