"""Fake SDK surface for the S4 field-model tests (shared by two test modules). The real SDK field model is exercised by ``orisys-sdk/tests/test_field_calibration.py``; the app tests describe the *calls and data* the app owns. Keeping the fakes in one place means the pass test and the integration test cannot drift apart. """ from __future__ import annotations import hashlib import json import math from pathlib import Path from types import SimpleNamespace import numpy as np from orisys_app_shared.acquisition import sdk_worker from orisys_app_shared.analysis import service as service_module from orisys_app_shared.analysis.model import Provenance from orisys_app_shared.record.playback import RecordingFrame FIELD_PROFILE = "hhd_normal_l1_field/1" # ------------------------------------------------------------------ calibration def write_model_file(tmp_path: Path, model_type: str) -> Path: """A schema 3 calibration document the SDK accepts, with its coordinate map. ``field_gain/1`` and ``spatial_gain/1`` differ only in the model block, so a test can compare two documents that really load through the SDK's parser. """ from orisys.field_calibration import BASELINE_POLICY, FIELD_IDENTITY, MAPPING_POLICY rows, cols = np.mgrid[0:4, 0:4] uv = np.stack((cols, rows), axis=-1).astype(np.float64) attachment = tmp_path / "map.npz" np.savez( attachment, uv_by_output_px=uv, valid_mask=np.ones((4, 4), dtype=bool), origin_xy=np.array([0, 0]), ) digest = hashlib.sha256(attachment.read_bytes()).hexdigest() pipeline = { "stitch_resolution": [4, 4], "dis_mode": 1, "hhd_downsample": 1, "filters": {"measurement": {"flow_ema_alpha": 1.0}}, "feature_signature_sha256": "a" * 64, "geometry_sha256": "b" * 64, "source_scale": 0.42, "coordinates": { "frame": "autocal_flat", "unit": "px", "origin_xy": [0, 0], "map_sha256": digest, }, } params = { "points_uv": [[0, 0], [3, 0], [0, 3], [3, 3]], "gains_N_per_raw": [1.0, 1.0, 1.0, 1.0], "triangles": [[0, 1, 2], [1, 3, 2]], "valid_force_range_N": [0.0, 3.0], } if model_type == "field_gain/1": pipeline["field"] = dict(FIELD_IDENTITY) profile = FIELD_PROFILE params["mapping"] = dict(MAPPING_POLICY) params["baseline"] = dict(BASELINE_POLICY) else: profile = "fn_delta_autocal_uv/1" document = { "schema_version": 3, "unit": "N", "identity": {"sensor_type": "finger", "sn": "dd02-gl"}, "pipeline": pipeline, "model": { "type": model_type, "input_profile": profile, "output_frame": "sdk_force/1", "outputs": ["fn"], "params": params, "artifacts": [ {"role": "coordinate_map", "path": attachment.name, "sha256": digest} ], }, } path = tmp_path / ("field.json" if model_type == "field_gain/1" else "spatial.json") path.write_text(json.dumps(document), encoding="utf-8") return path # --------------------------------------------------------------------- recording def minimal_recording(root: Path, *, segments: int = 1) -> Path: """The two text files and the segment bytes a reader needs, and nothing else.""" recording = root / "recording" recording.mkdir(parents=True, exist_ok=True) (recording / "frames.csv").write_text( "frame_id,t_rel_s,acquired_at,segment,segment_frame,in_video,dropped\n" "1,0.0,,1,1,1,0\n2,0.033,,1,2,1,0\n", encoding="utf-8", ) for number in range(1, segments + 1): (recording / f"raw_{number:04d}.mp4").write_bytes(b"") (recording / "session.json").write_text( json.dumps( { "session_id": 1, "started_at": "2026-01-01T00:00:00+0000", "ended_at": "", "device": {"sensor_type": "finger", "config_ref": ""}, "stats": {"frames": 2, "video_frames": 2, "segments": segments}, } ), encoding="utf-8", ) return recording def field_rows(count: int, *, segment: int = 1) -> list[RecordingFrame]: return [ RecordingFrame(i + 1, i / 30.0, "", segment, i + 1, True) for i in range(count) ] # ------------------------------------------------------------------------- SDK class FakeFieldModel: schema_version = 3 input_profile = FIELD_PROFILE model_type = "field_gain/1" channels = {"fn": "N"} fingerprint = "abcdef0123456789" * 4 def __init__(self) -> None: self.field_baseline_N: float | None = None class FakeFieldSensor: """The slice of ``Sensor`` the pass touches, simulating the SDK's window.""" def __init__(self, *, zero_frames: int = 30) -> None: self.info = SimpleNamespace( FNORMAL=0, FSHEARX=1, FSHEARY=2, FNORMAL_RAW=3, FSHEARX_RAW=4, FSHEARY_RAW=5, ) self.calibration = FakeFieldModel() self.force_calibrated = True self.centroid_found = True # the field model must not gate on it self.flow = np.zeros((4, 4, 2), dtype=np.float32) self.valid_mask = None self.calibration_status = {"fn": "zero_unavailable"} self.calibration_result = None self.declared: list[str] = [] self.set_reference_calls: list[tuple] = [] self.pending = 0 self.frame_index = 0 self.fn_value = float("nan") self.zero_frames = zero_frames self._hhd_downsample = 2 self._hhd_grid_shape = (2, 2) self._hhd_ref_shape = (4, 4) def declare_unloaded_reference(self, *, reference_id: str) -> None: self.declared.append(str(reference_id)) self.pending = 0 self.calibration.field_baseline_N = None def set_calibration_reference(self, raw, *, reference_id) -> None: # pragma: no cover self.set_reference_calls.append((raw, reference_id)) def read_info(self, *keys): values = { 0: self.fn_value, 1: float("nan"), 2: float("nan"), 3: 100.0, 4: 2.0, 5: 3.0, } return tuple(values[key] for key in keys) def step(self): """One frame of the SDK's field model: 30-frame window, then ok frames.""" self.frame_index += 1 diagnostics: dict = {} if self.declared: if self.pending < self.zero_frames: self.pending += 1 diagnostics = { "pending_reference_frames": self.pending, "extrapolated_fraction": 0.4, "coverage_fraction": 0.9, "model_fingerprint": self.calibration.fingerprint, "frame_id": self.frame_index, } if self.pending == self.zero_frames: self.calibration.field_baseline_N = 0.25 diagnostics["baseline_N"] = 0.25 diagnostics["pending_reference_frames"] = 0 diagnostics["extrapolated_fraction"] = 0.3 self.calibration_status = {"fn": "zero_unavailable"} self.fn_value = float("nan") else: diagnostics = { "baseline_N": 0.25, "extrapolated_fraction": 0.2, "coverage_fraction": 0.97, "model_fingerprint": self.calibration.fingerprint, } self.calibration_status = {"fn": "ok"} self.fn_value = 1.5 else: diagnostics = {"model_fingerprint": self.calibration.fingerprint} self.calibration_result = SimpleNamespace( diagnostics=diagnostics, status=dict(self.calibration_status), values_N={"fn": None if math.isnan(self.fn_value) else self.fn_value}, ) return None class FakeChain: def __init__(self, sensor, *, frames: int = 35) -> None: self.sensor = sensor self.frames = int(frames) self.index = 0 self.open_seconds = 0.0 self.debug_files: list[str] = [] self.decode_position = 0 def __enter__(self): return self def __exit__(self, *args): return False def read(self): if self.index >= self.frames: return False, None self.index += 1 return True, np.zeros((4, 4), dtype=np.uint8) def feed(self, frame): return frame class FakeReader: def __init__(self, rows, *, segments: int = 1, base: Path) -> None: self._rows = list(rows) self._segments = [ base / f"raw_{number:04d}.mp4" for number in range(1, segments + 1) ] def session(self): return { "session_id": 1, "started_at": "2026-01-01T00:00:00+0000", "ended_at": "", "device": {"sensor_type": "finger", "config_ref": ""}, "stats": {"frames": len(self._rows), "segments": len(self._segments)}, } def info(self): return SimpleNamespace(frames=self._rows, video_frames=len(self._rows)) def segments(self): return list(self._segments) def segment_path(self, number: int): index = int(number) - 1 return self._segments[index] if 0 <= index < len(self._segments) else None def install_pass(monkeypatch, processor, chain: FakeChain, reader: FakeReader) -> None: """Make the service walk the fake chain instead of opening the SDK.""" monkeypatch.setattr(processor, "reader", lambda: reader) monkeypatch.setattr(processor, "_segment_offset", lambda *args: (0, True)) monkeypatch.setattr(service_module, "_OpenChain", lambda *args: chain) monkeypatch.setattr(sdk_worker, "process_chain", lambda sensor: (sensor.step(), "")) def new_pass_state(processor, tmp_path: Path, *, count: int = 35): rows = field_rows(count) return service_module._PassState( Provenance(), [1], {1: list(rows)}, tmp_path )