mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 18:01:13 +00:00
[AZ-270] [AZ-272] [AZ-279] [AZ-281] [AZ-283] Compose root + FDR schema + 3 Layer-1 helpers
AZ-270: composition root with strategy registry, tier-gated lookup, topo-order construction, all-or-nothing teardown, StrategyNotLinkedError payload. AZ-272: orjson-backed FdrRecord serialise/parse with forward-compat for unknown payload + top-level fields and canonical overrun-record shape. AZ-279: pyproj-backed WGS84/ECEF/ENU + OSM slippy-map tile math with WgsConversionError for shape/range/zoom guards. AZ-281: strict EngineFilenameSchema build/parse/matches_host with anchored regex + enum validation; round-trip identity by construction. AZ-283: dtype-preserving (fp16/fp32) single + batch L2 normaliser with zero-norm safety and descriptor_metric() source-of-truth. pyproject.toml pins pyproj>=3.6 and orjson>=3.9 (named-backend deps per the AZ-272 / AZ-279 contracts). New DTOs LatLonAlt + BoundingBox and EngineCacheKey + HostCapabilities land in _types/ to back the helper contracts. 203 unit tests pass (64 new). Review verdict: PASS_WITH_WARNINGS; findings are perf-NFR deferrals + dep amendment + minor docstring polish. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# WgsConverter Helper Module
|
||||
|
||||
**Task**: AZ-279_wgs_converter
|
||||
**Name**: WgsConverter Helper
|
||||
**Description**: Implement the shared `WgsConverter` helper for WGS84 ↔ local-tangent-plane (ENU) ↔ tile-pixel coordinate conversions, backed by `pyproj`. Used by C4, C5, C6, C8, C10, C11, and C12 — every component that crosses the geographic-vs-local-frame boundary. Stateless static-only design (per `coderule.mdc`); slippy-map tile convention matches `satellite-provider`'s on-disk layout.
|
||||
**Complexity**: 2 points
|
||||
**Dependencies**: AZ-263_initial_structure
|
||||
**Component**: shared.helpers.wgs_converter (cross-cutting; epic AZ-264 / E-CC-HELPERS)
|
||||
**Tracker**: AZ-279
|
||||
**Epic**: AZ-264 (E-CC-HELPERS)
|
||||
|
||||
### Document Dependencies
|
||||
|
||||
- `_docs/02_document/contracts/shared_helpers/wgs_converter.md` — frozen public interface this task produces.
|
||||
- `_docs/02_document/common-helpers/04_helper_wgs_converter.md` — design rationale and consumer mapping.
|
||||
|
||||
## Problem
|
||||
|
||||
Seven components (C4, C5, C6, C8, C10, C11, C12) need to cross the geographic-vs-local-frame boundary:
|
||||
- C4 compares pose-in-WGS to pose-in-ENU; C5 initialises iSAM2 from a WGS origin.
|
||||
- C6's tile bbox queries map between lat/lon and tile-pixel `(zoom, x, y)`.
|
||||
- C8 encodes pose for FC emission; C10 / C11 resolve operator-entered bboxes to tile lists; C12 takes the operator's bbox input.
|
||||
|
||||
Without a shared helper:
|
||||
- Each component re-derives the WGS84 → ECEF → ENU pipeline; sign conventions (ENU vs NED) drift; altitude treatment (ellipsoidal vs orthometric) diverges.
|
||||
- Tile-xy conversions go through OSM-style math in some places and Mercator-projection in others, breaking on-disk compatibility with `satellite-provider`'s `{zoom}/{x}/{y}.jpg` layout.
|
||||
- A future datum or geoid change becomes a 7-place coordinated edit instead of a single helper update.
|
||||
|
||||
## Outcome
|
||||
|
||||
- A single `helpers.wgs_converter` module is the only place that performs WGS84 / ECEF / ENU / tile-xy conversions across the codebase. Component imports go through the helper.
|
||||
- All conversions are pure static functions: same input → byte-equal output (deep-equal numpy / `LatLonAlt`).
|
||||
- ENU sign convention is locked to `(east, north, up)` and documented; consumers cannot drift to NED accidentally.
|
||||
- Slippy-map tile convention matches `satellite-provider`'s on-disk layout — the contract test pins the `(zoom=18, lat=50.45, lon=30.52) → (x, y)` round-trip against a known-good fixture.
|
||||
- Out-of-range inputs (zoom > 22, lat outside Web-Mercator-valid range, ECEF shape mismatch, tile-xy out of `[0, 2^zoom)`) raise `WgsConversionError` rather than silently producing garbage.
|
||||
|
||||
## Scope
|
||||
|
||||
### Included
|
||||
|
||||
- Static methods on `WgsConverter`: `latlonalt_to_ecef`, `ecef_to_latlonalt`, `latlonalt_to_local_enu`, `local_enu_to_latlonalt`, `latlon_to_tile_xy`, `tile_xy_to_latlon_bounds`.
|
||||
- `WgsConversionError` exception type.
|
||||
- Public interface contract published at `_docs/02_document/contracts/shared_helpers/wgs_converter.md`.
|
||||
|
||||
### Excluded
|
||||
|
||||
- Datum-shift logic / non-WGS84 datums — out of scope for v1.0.0.
|
||||
- UTM / MGRS conversions — out of scope.
|
||||
- Geoid-height corrections (orthometric vs. ellipsoidal altitude) — out of scope; the contract documents that altitude is ellipsoidal.
|
||||
- Vincenty / great-circle distance helpers — out of scope.
|
||||
- Body-frame ↔ ECEF rotation transforms — `helpers.se3_utils` + per-deployment `CameraCalibration`.
|
||||
- The `LatLonAlt` / `BoundingBox` DTOs themselves — owned by `_types/` (AZ-263).
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
**AC-1: ECEF round-trip**
|
||||
Given `p = LatLonAlt(50.0, 30.0, 100.0)`
|
||||
When `ecef_to_latlonalt(latlonalt_to_ecef(p))` runs
|
||||
Then the returned `LatLonAlt` matches `p` within `atol=1e-9` deg lat/lon and `1e-6` m altitude
|
||||
|
||||
**AC-2: ENU round-trip within 10 km**
|
||||
Given an `origin` and a `p` ~10 km away
|
||||
When `local_enu_to_latlonalt(origin, latlonalt_to_local_enu(origin, p))` runs
|
||||
Then the returned `LatLonAlt` matches `p` within 1 m horizontal + 1 cm vertical
|
||||
|
||||
**AC-3: Slippy-map tile round-trip at z18**
|
||||
Given `(zoom=18, lat=50.45, lon=30.52)`
|
||||
When `tile_xy_to_latlon_bounds(zoom, *latlon_to_tile_xy(zoom, lat, lon))` runs
|
||||
Then the returned bounding box contains the input lat/lon AND the `(x, y)` matches the OSM-pinned fixture for the same coordinates
|
||||
|
||||
**AC-4: Web-Mercator latitude range guard**
|
||||
Given `lat = 95.0` passed to `latlon_to_tile_xy`
|
||||
When the call runs
|
||||
Then `WgsConversionError` is raised mentioning the Web-Mercator-valid range `[-85.0511, 85.0511]`
|
||||
|
||||
**AC-5: Zoom range guard**
|
||||
Given `zoom = 25`
|
||||
When `latlon_to_tile_xy` or `tile_xy_to_latlon_bounds` runs
|
||||
Then `WgsConversionError` is raised mentioning the supported zoom range `[0, 22]`
|
||||
|
||||
**AC-6: Tile-xy range guard**
|
||||
Given `(zoom=18, x=2^18, y=0)`
|
||||
When `tile_xy_to_latlon_bounds` runs
|
||||
Then `WgsConversionError` is raised mentioning the valid `(x, y)` range `[0, 2^zoom)`
|
||||
|
||||
**AC-7: ECEF shape contract**
|
||||
Given an array of shape `(2,)` passed to `ecef_to_latlonalt`
|
||||
When the call runs
|
||||
Then `WgsConversionError` is raised mentioning the expected shape `(3,)`
|
||||
|
||||
**AC-8: Determinism**
|
||||
Given the same input
|
||||
When any helper function is called twice
|
||||
Then both outputs are byte-equal
|
||||
|
||||
**AC-9: No upward imports (Layer 1 invariant)**
|
||||
Given the helper module
|
||||
When a static-import check runs
|
||||
Then it imports ONLY from `_types`, `pyproj`, numpy, and stdlib — no `gps_denied_onboard.components.*` imports anywhere
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
**Performance**
|
||||
- No specific latency budget per `_docs/02_document/common-helpers/04_helper_wgs_converter.md` (consumers are pre-flight / post-landing). Each function p99 ≤ 200 µs on Tier-2 as a sanity bound.
|
||||
|
||||
**Reliability**
|
||||
- Pure deterministic; same input → byte-equal output.
|
||||
- `WgsConversionError` is the ONLY exception type the public surface raises on shape / range violations. `pyproj`'s lower-level exceptions MUST be wrapped.
|
||||
|
||||
## Unit Tests
|
||||
|
||||
| AC Ref | What to Test | Required Outcome |
|
||||
|--------|-------------|-----------------|
|
||||
| AC-1 | ECEF round-trip on 100 random valid `LatLonAlt`s | all match within `atol=1e-9` deg + `1e-6` m |
|
||||
| AC-2 | ENU round-trip on 100 origin/point pairs within 10 km | all match within 1 m + 1 cm |
|
||||
| AC-3 | Slippy-map round-trip at z18 with OSM-pinned fixture | `(x, y)` matches fixture; bounds contain input |
|
||||
| AC-4 | `latlon_to_tile_xy(18, 95.0, 0.0)` | `WgsConversionError`; mentions Web-Mercator range |
|
||||
| AC-5 | `latlon_to_tile_xy(25, 0, 0)` | `WgsConversionError`; mentions zoom range |
|
||||
| AC-6 | `tile_xy_to_latlon_bounds(18, 2**18, 0)` | `WgsConversionError`; mentions tile-xy range |
|
||||
| AC-7 | `ecef_to_latlonalt(np.zeros(2))` | `WgsConversionError`; mentions shape `(3,)` |
|
||||
| AC-8 | each helper called twice with same input | byte-equal outputs |
|
||||
| AC-9 | importlinter / grep gate | no `components.*` imports |
|
||||
| NFR-perf | microbench each helper (10k iterations on Tier-2 fixture) | p99 ≤ 200 µs each |
|
||||
|
||||
## Constraints
|
||||
|
||||
- Public surface frozen by `_docs/02_document/contracts/shared_helpers/wgs_converter.md` v1.0.0.
|
||||
- Layer 1 Foundation only.
|
||||
- `pyproj` is the single geodesy backend; pinned in `pyproject.toml` at AZ-263 / E-BOOT.
|
||||
- Static-only design satisfies `coderule.mdc` ("only use static methods for pure self-contained computations") — every operation is a pure mathematical function of its arguments.
|
||||
- No new dependency beyond what AZ-263 / E-BOOT pinned.
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1: Tangent-plane approximation degrades silently beyond 100 km**
|
||||
- *Risk*: A consumer (e.g., C12 operator tooling with a continent-scale bbox) calls `latlonalt_to_local_enu` on a point 500 km from origin; the helper returns a result with O(1 km) error; consumer uses it as ground truth.
|
||||
- *Mitigation*: The contract `Invariants` section documents the 100 km validity range. Consumers that need wider range explicitly chain ECEF↔ENU through a closer origin.
|
||||
|
||||
**Risk 2: Datum drift if `pyproj` upgrades silently change WGS84 parameters**
|
||||
- *Risk*: A future `pyproj` minor version changes the WGS84 ellipsoid parameters; all conversions shift by sub-metre amounts, breaking the round-trip ACs.
|
||||
- *Mitigation*: `pyproj` is pinned at AZ-263; round-trip ACs are the canary that detects drift on dependency upgrade.
|
||||
|
||||
## Runtime Completeness
|
||||
|
||||
- **Named capability**: WGS84 ↔ ECEF ↔ ENU ↔ tile-xy conversions via `pyproj` (architecture / E-CC-HELPERS / `04_helper_wgs_converter.md`).
|
||||
- **Production code that must exist**: real `pyproj`-backed conversions; real slippy-map tile math matching `satellite-provider`'s on-disk layout.
|
||||
- **Allowed external stubs**: none — `pyproj` is the production runtime.
|
||||
- **Unacceptable substitutes**: hand-rolled flat-earth ENU approximation (silently breaks AC-2 beyond a few km); custom Mercator tile math that drifts from OSM convention (breaks `satellite-provider` compatibility); skipping out-of-range guards (silent garbage for high latitudes).
|
||||
|
||||
## Contract
|
||||
|
||||
This task produces the contract at `_docs/02_document/contracts/shared_helpers/wgs_converter.md`.
|
||||
Consumers MUST read that file — not this task spec — to discover the interface.
|
||||
Reference in New Issue
Block a user