mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 08:41:13 +00:00
[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>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
# 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).
|
||||
Reference in New Issue
Block a user