from __future__ import annotations import hashlib import json from pathlib import Path import numpy as np from fastapi import APIRouter from fastapi.routing import APIRoute from k1link.compute import ( E10_LIDAR_PACK_SCHEMA, E10LidarFieldSource, K1LocalSurfaceProfile, K1LocalSurfaceV1, build_k1_local_surface, ) from k1link.web.lidar_api import build_lidar_router def _canonical_json(value: object) -> bytes: return json.dumps( value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False, ).encode() def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def _source_pack(root: Path) -> Path: frame_count = 8 available = np.asarray([True, True, True, True, True, True, True, False]) poses: list[list[float]] = [] clouds: list[np.ndarray] = [] offsets = [0] for frame_index in range(frame_count): pose_x = frame_index * 0.35 pose_y = 0.1 * np.sin(frame_index) ground_z = 0.04 * pose_x - 0.015 * pose_y poses.append([pose_x, pose_y, ground_z + 1.42]) if not available[frame_index]: offsets.append(offsets[-1]) continue axis = np.linspace(-3.5, 3.5, 12) xx, yy = np.meshgrid(axis + pose_x, axis + pose_y) zz = 0.04 * xx - 0.015 * yy + 0.008 * np.sin(xx * 2 + frame_index) ground = np.column_stack((xx.ravel(), yy.ravel(), zz.ravel())) obstacle_xy = ground[::13, :2] obstacle_z = ( 0.04 * obstacle_xy[:, 0] - 0.015 * obstacle_xy[:, 1] + 0.75 ) obstacle = np.column_stack((obstacle_xy, obstacle_z)) cloud = np.concatenate((ground, obstacle)).astype(" object: for route in router.routes: if ( isinstance(route, APIRoute) and route.path == path and route.methods is not None and "GET" in route.methods ): return route.endpoint raise AssertionError(f"GET {path} route is missing") def test_k1_local_surface_is_dynamic_source_bound_and_read_only( tmp_path: Path, ) -> None: source_path = _source_pack(tmp_path / "source") source_artifact = source_path / "lidar-pack.npz" source_sha256 = _sha256(source_artifact) profile = K1LocalSurfaceProfile( profile_id="synthetic-dynamic-local-surface/v1", local_radius_m=5.0, cell_size_m=0.5, surface_ttl_s=0.5, minimum_surface_cells=12, ) source = E10LidarFieldSource(source_path) try: output = build_k1_local_surface( source, tmp_path / "models", profile=profile, ) duplicate = build_k1_local_surface( source, tmp_path / "models", profile=profile, ) finally: source.close() assert duplicate == output assert _sha256(source_artifact) == source_sha256 model = K1LocalSurfaceV1(output) source = E10LidarFieldSource(source_path) try: detail = model.frame_detail(source, 2) review = model.review_detail(source) assert model.report["source"]["passive_processing_only"] is True assert model.report["source"]["firmware_or_device_commands_used"] is False assert model.report["surface_model"]["hardcoded_height_m"] is None assert model.report["occupancy_policy"]["absence_of_points_means_free"] is False assert model.report["metrics"]["frames"]["valid"] >= 5 temporal = model.report["metrics"]["temporal_qualification"] assert temporal["prediction"]["current_frame_excluded"] is True assert temporal["prediction"]["sample_count"] >= 4 assert temporal["prediction"]["residual_p95_m"]["p95"] < 0.05 assert temporal["stability"]["sample_count"] >= 4 assert temporal["step_candidates"]["is_ground_truth"] is False assert temporal["step_candidates"]["frames_with_candidates"] > 0 assert detail["valid"] is True assert 1.2 < detail["surface"]["sensor_height_m"] < 1.6 assert detail["counts"]["surface"] > 50 assert detail["counts"]["occupied"] > 0 assert detail["counts"]["step_candidate"] > 0 assert detail["prediction"]["current_frame_excluded"] is True assert detail["prediction"]["available"] is True assert detail["temporal"]["compared"] is True assert detail["authority"]["commands_enabled"] is False assert review["available"] is True assert review["ground_truth"] is False assert review["criteria"]["prediction_inlier_fraction_floor"] == 0.85 assert review["summary"]["item_count"] == len(review["items"]) assert "1.27" not in repr({"identity": model.identity, "report": model.report}) assert str(tmp_path) not in repr(detail) assert str(tmp_path) not in repr(review) finally: source.close() model.close() router = build_lidar_router( root_provider=lambda: None, ground_root_provider=lambda: None, field_review_root_provider=lambda: None, local_surface_root_provider=lambda: output.parent, e10_source_root_provider=lambda: source_path.parent, ) catalog_route = _endpoint(router, "/api/v1/lidar/local-surfaces") frame_route = _endpoint( router, "/api/v1/lidar/local-surfaces/{model_id}/frames/{frame_index}", ) timeline_route = _endpoint( router, "/api/v1/lidar/local-surfaces/{model_id}/timeline", ) review_route = _endpoint( router, "/api/v1/lidar/local-surfaces/{model_id}/review", ) catalog = catalog_route(limit=10) # type: ignore[operator] frame = frame_route(model_id=output.name, frame_index=2) # type: ignore[operator] timeline = timeline_route(model_id=output.name) # type: ignore[operator] review = review_route(model_id=output.name) # type: ignore[operator] assert catalog["valid_total"] == 1 assert catalog["items"][0]["status"] == "diagnostic-only" assert frame["model_id"] == output.name assert frame["access"] == "read-only" assert timeline["frame_count"] == 8 assert sum(timeline["prediction_available"]) >= 4 assert len(timeline["temporal_jump"]) == 8 assert str(tmp_path) not in repr(timeline) assert review["review_profile_id"] == "missioncore-local-surface-attention/v1" assert review["access"] == "read-only" assert str(tmp_path) not in repr(review)