Files
gps-denied-onboard/_docs/03_implementation/batch_65_cycle1_report.md
T
Oleksandr Bezdieniezhnykh c5ffc14fe9 [AZ-389] C5 orthorectifier emits mid-flight tiles to C6
Adds an opt-in C5-internal orthorectifier (`_orthorectifier.py`) that
emits at most one tile-aligned JPEG candidate per nav frame to the
C6 `TileStore.write_tile` API.  Quality gates fire before any
OpenCV work: covariance Frobenius, inlier floor, source-label
(`SATELLITE_ANCHORED` only), and once-per-frame rate limit.

Cross-component import rule (AZ-507) is preserved: c5_state never
imports c6_tile_cache.  `runtime_root.state_factory` carries a new
`_C6MidFlightIngestAdapter` that builds the canonical
`TileMetadata` (`ONBOARD_INGEST` / `FRESH` / `PENDING`), hashes
the JPEG, and translates `FreshnessRejectionError` to a `None`
return so the orthorectifier silently swallows freshness
rejection per AC-NEW-3.

Wiring is opt-in via `C5StateConfig.orthorectifier.enabled`;
existing tests/binaries default to disabled and are unaffected.
Both `GtsamIsam2StateEstimator` and `EskfStateEstimator`
participate through new `attach_orthorectifier` /
`set_latest_nav_frame` extension methods (Protocol surface
unchanged).

Tests: 22 new unit tests cover AC-1..AC-9 plus inlier-floor
gate plus the composition-root adapter.  216/216 c5_state and
38/38 runtime-root + compose tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-16 09:02:33 +03:00

8.7 KiB

Batch 65 — Cycle 1 Report

Date: 2026-05-16 Tasks: AZ-389 (C5 orthorectifier → C6 mid-flight tile candidate emission) Verdict: COMPLETE — PASS (self-reviewed)

Summary

Closes the AZ-389 gap inside the C5 state estimator by introducing a component-internal orthorectifier that emits at most one tile-aligned JPEG candidate per nav-camera frame to C6 via the existing TileStore.write_tile API.

The implementation respects the AZ-507 cross-component import rule (enforced by test_az270_compose_root.test_ac6_only_compose_root_imports_concrete_strategies): c5_state never imports c6_tile_cache. The composition root's runtime_root.state_factory carries a new _C6MidFlightIngestAdapter that wraps the C6 TileStore, builds the canonical TileMetadata (TileSource.ONBOARD_INGEST, FreshnessLabel.FRESH, VotingStatus.PENDING), hashes the JPEG bytes, and translates FreshnessRejectionError into a None return so the orthorectifier silently swallows freshness rejection per AC-NEW-3 (opportunistic emission).

The orthorectifier runs entirely on the existing state-ingest thread (Invariant 1) — no new threads, no additional locks. It is wired opt-in: config.components['c5_state'].orthorectifier.enabled = false keeps the legacy steady-state path bit-for-bit unchanged. Both GtsamIsam2StateEstimator and EskfStateEstimator participate through new attach_orthorectifier(...) and set_latest_nav_frame(...) extension methods (concrete only — the StateEstimator Protocol surface is unchanged so existing implementations and tests continue to satisfy it).

Architecture decisions

  • Per-frame, per-estimator hook — the hook fires after the EstimatorOutput is built inside current_estimate(). The buffered nav frame supplies the source pixels; the orthorectifier passes duck-typed pose + cov to its kernel and rate-limits itself to one tile per frame.frame_id (AC-4).
  • No new C6 API — uses TileStore.write_tile(blob, metadata), the same atomic file + metadata insert that the C11 download path already uses. The composition-root adapter is the only new component-bridge.
  • Quality gates as cheap pre-checks — covariance Frobenius gate, inlier-floor gate, source-label gate (only SATELLITE_ANCHORED passes), and once-per-frame rate limit run BEFORE the OpenCV warp/encode work.
  • Best-effort kernel — any exception inside the warp / JPEG encode path or any non-FreshnessRejectionError writer failure is swallowed with a WARNING log and None return; the steady-state current_estimate output is never disturbed.
  • AC-7 first-emission INFO log — emitted exactly once per flight, subsequent emissions log at DEBUG.

Files added / modified

Added (2)

  • src/gps_denied_onboard/components/c5_state/_orthorectifier.py — the MidFlightTileWriter Protocol cut, OrthorectifierThresholds dataclass, and Orthorectifier class with the homography construction (_ground_plane_homography, _compose_tile_to_image_homography, _invert_se3, _quat_to_rotation_matrix).
  • tests/unit/c5_state/test_az389_orthorectifier.py — 22 tests covering AC-1..AC-9 plus the inlier-floor gate plus the composition-root _C6MidFlightIngestAdapter translation rules plus OrthorectifierConfig validation.

Modified (4)

  • src/gps_denied_onboard/components/c5_state/config.py — new OrthorectifierConfig dataclass nested as C5StateConfig.orthorectifier. Disabled by default; tunable thresholds + tile / zoom / JPEG knobs.
  • src/gps_denied_onboard/components/c5_state/gtsam_isam2_estimator.py — orthorectifier state fields, attach_orthorectifier + set_latest_nav_frame extension methods, _maybe_emit_mid_flight_tile hook in current_estimate(), and create() factory now accepts the optional mid_flight_tile_writer / camera_calibration / flight_id / companion_id params.
  • src/gps_denied_onboard/components/c5_state/eskf_baseline.py — same set of changes, plus _latest_vio cache (ESKF historically did not retain the full VIO DTO).
  • src/gps_denied_onboard/runtime_root/state_factory.py_C6MidFlightIngestAdapter class + build_state_estimator now accepts optional tile_store / camera_calibration / flight_id / companion_id and forwards them to the strategy factory when AZ-389 is enabled.

Task Results

Task Status Files Modified Focused tests AC Coverage Issues
AZ-389 Done 1 added + 4 modified under src/; 1 added under tests/unit/c5_state/; task spec moved _docs/02_tasks/todo/done/ 22/22 pass 9/9 covered None

AC Test Coverage: 9/9 covered

AC Test Status
AC-1 test_ac1_homography_projects_origin_to_principal_point + test_ac1_homography_projects_offset_within_one_pixel + test_compose_tile_to_image_homography_is_centred_on_camera Covered
AC-2 test_ac2_cov_norm_above_threshold_blocks_emission + test_ac2_cov_norm_below_threshold_emits Covered
AC-3 test_ac3_non_satellite_anchored_blocked (parametrised over VISUAL_PROPAGATED / DEAD_RECKONED) Covered
AC-4 test_ac4_same_frame_id_processed_only_once + test_ac4_distinct_frame_ids_each_emit Covered
AC-5 test_ac5_writer_called_with_onboard_ingest_metadata + test_adapter_calls_write_tile_with_onboard_ingest_metadata Covered
AC-6 test_ac6_jpeg_bytes_decode_to_expected_shape_and_quality + adapter-side content_sha256_hex == hashlib.sha256(jpeg_bytes).hexdigest() assertion Covered
AC-7 test_ac7_first_emission_logs_info_subsequent_logs_debug Covered
AC-8 test_ac8_missing_inputs_silent_return_none (parametrised over frame / pose_estimate / cov_6x6) Covered
AC-9 test_ac9_writer_returning_none_swallowed + test_ac9_writer_raising_swallowed_with_warning + test_adapter_translates_freshness_rejection_to_none Covered

Code Review Verdict: PASS (self-reviewed)

Auto-Fix Attempts: 0

Stuck Agents: None

Cross-batch verification

  • tests/unit/c5_state/ — 216 / 216 pass (all pre-AZ-389 tests still pass — Protocol surface unchanged, factory signature is backward-compatible via default None params).
  • tests/unit/test_az270_compose_root.py — 8 / 8 pass; the cross-component import lint still holds against the new _C6MidFlightIngestAdapter.
  • tests/unit/test_runtime_root_env_gate.py + tests/unit/test_az401_compose_root_replay.py + tests/unit/test_ac3_compose_files.py — 38 / 38 pass.
  • tests/unit/c6_tile_cache/ — 126 / 126 in-process tests pass (Postgres-backed tests skipped; require Docker).

Notes / leftovers

  • The Jira description for AZ-389 still references the pre-AZ-559 tile_store.put_mid_flight_candidate API surface; the local task spec was rewritten against write_tile per the History section. Logged as tracker hygiene; not blocking.
  • During investigation of the existing C11 download adapter (runtime_root/c11_factory.py::_C6DownloadAdapter.write_tile_for_download) we noticed it calls both tile_store.write_tile(blob, metadata) and metadata_store.insert_metadata(metadata) sequentially — given that PostgresFilesystemStore.write_tile is itself atomic (file write + metadata insert in a single transaction) the second call is a probable redundancy. Out of scope for AZ-389; recorded here for a future hygiene ticket.

Next Batch: All product-implementation tasks complete — proceed to Step 15 (Product Implementation Completeness Gate).