mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 17:51:14 +00:00
Decompose Step 6 snapshot: 140 task specs + contract docs
Closes out greenfield Step 6 (Decompose) for all 14 components (C1-C13 + cross-cutting helpers/replay). Covers tasks AZ-266..AZ-446 plus the _dependencies_table.md and component contract documents. State file updated to greenfield Step 7 (Implement), not_started. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
# Contract: wgs_converter
|
||||
|
||||
**Component**: shared_helpers / `helpers.wgs_converter` (cross-cutting concern owned by E-CC-HELPERS / AZ-264)
|
||||
**Producer task**: AZ-279 — `_docs/02_tasks/todo/AZ-279_wgs_converter.md`
|
||||
**Consumer tasks**: every C4 pose-estimation task that compares pose-in-WGS to pose-in-ENU; every C5 state-estimator task that initialises the iSAM2 graph from a WGS origin; every C6 task that maps a tile bbox to lat/lon; every C8 task that encodes pose for FC emission; every C10 / C11 task that resolves a bbox to a tile-id list; every C12 task where the operator enters a bbox
|
||||
**Version**: 1.0.0
|
||||
**Status**: draft
|
||||
**Last Updated**: 2026-05-10
|
||||
|
||||
## Purpose
|
||||
|
||||
Centralise WGS84 ↔ local-tangent-plane (ENU) ↔ tile-pixel coordinate conversions. Required by every component that interacts with geographic positions. Per `_docs/02_document/common-helpers/04_helper_wgs_converter.md`. Backed by `pyproj` for the geodesy primitives; tile_xy math uses the standard slippy-map convention so it matches `satellite-provider`'s on-disk layout.
|
||||
|
||||
## Shape
|
||||
|
||||
### For function / method APIs
|
||||
|
||||
```python
|
||||
class WgsConverter:
|
||||
@staticmethod
|
||||
def latlonalt_to_ecef(p: LatLonAlt) -> np.ndarray: ... # shape (3,)
|
||||
@staticmethod
|
||||
def ecef_to_latlonalt(p_ecef: np.ndarray) -> LatLonAlt: ...
|
||||
@staticmethod
|
||||
def latlonalt_to_local_enu(origin: LatLonAlt, p: LatLonAlt) -> np.ndarray: ... # shape (3,)
|
||||
@staticmethod
|
||||
def local_enu_to_latlonalt(origin: LatLonAlt, p_enu: np.ndarray) -> LatLonAlt: ...
|
||||
@staticmethod
|
||||
def latlon_to_tile_xy(zoom: int, lat: float, lon: float) -> tuple[int, int]: ...
|
||||
@staticmethod
|
||||
def tile_xy_to_latlon_bounds(zoom: int, x: int, y: int) -> BoundingBox: ...
|
||||
```
|
||||
|
||||
| Name | Signature | Throws / Errors | Blocking? |
|
||||
|------|-----------|-----------------|-----------|
|
||||
| `latlonalt_to_ecef` | `(LatLonAlt) -> np.ndarray (3,)` | `WgsConversionError` if lat / lon / alt are out of range | sync, pure |
|
||||
| `ecef_to_latlonalt` | `(np.ndarray (3,)) -> LatLonAlt` | `WgsConversionError` on shape mismatch | sync, pure |
|
||||
| `latlonalt_to_local_enu` | `(origin, p) -> np.ndarray (3,)` | `WgsConversionError` on origin / point validation | sync, pure |
|
||||
| `local_enu_to_latlonalt` | `(origin, p_enu) -> LatLonAlt` | `WgsConversionError` on origin / shape | sync, pure |
|
||||
| `latlon_to_tile_xy` | `(zoom, lat, lon) -> (int, int)` | `WgsConversionError` if zoom < 0 or > 22, lat out of `[-85.0511, 85.0511]`, lon out of `[-180, 180]` | sync, pure |
|
||||
| `tile_xy_to_latlon_bounds` | `(zoom, x, y) -> BoundingBox` | `WgsConversionError` if `x` or `y` out of `[0, 2^zoom)` | sync, pure |
|
||||
|
||||
`LatLonAlt` and `BoundingBox` are imported from `gps_denied_onboard._types`. Numpy arrays use `dtype=float64`. `WgsConversionError` is the only exception type the public surface raises.
|
||||
|
||||
## Invariants
|
||||
|
||||
- **Stateless**: no module-level state; static methods only. The static-only design satisfies the coderule.mdc constraint ("only use static methods for pure self-contained computations") because every operation is a pure mathematical function of its arguments.
|
||||
- **WGS84 ellipsoid only**: all conversions use the WGS84 ellipsoid; no datum-shift logic. If a future deployment needs alternative datum support, switch to an instance-based factory then.
|
||||
- **Slippy-map tile convention**: `latlon_to_tile_xy` matches OSM / `satellite-provider`'s on-disk `{zoom}/{x}/{y}.jpg` layout. Latitude is clamped to the Web-Mercator-valid range `[-85.0511, 85.0511]`; values outside raise `WgsConversionError`.
|
||||
- **ENU sign convention**: `latlonalt_to_local_enu` returns `(east, north, up)` in metres. Origin altitude IS used (height above ellipsoid); zero altitude is NOT silently substituted.
|
||||
- **Round-trip identity**: `local_enu_to_latlonalt(origin, latlonalt_to_local_enu(origin, p)) ≈ p` within `atol=1e-6` metres (lat/lon to ~1 m, alt to ~1 cm) for `p` within 100 km of `origin`. Beyond 100 km the tangent-plane approximation degrades — the contract documents this limit.
|
||||
- **Zoom-level dependence**: `tile_xy_to_latlon_bounds` and `latlon_to_tile_xy` are sensitive to `zoom`; callers MUST pass the right zoom for the tile in question (typically `zoomLevel` from `TileMetadata`).
|
||||
- **No upward imports** (Layer 1): the module imports ONLY from `_types`, `pyproj`, numpy, and stdlib. NO `gps_denied_onboard.components.*` imports.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- 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; consumers using altitude do so under the ellipsoid convention or apply geoid correction themselves.
|
||||
- Vincenty / great-circle distance helpers — out of scope.
|
||||
- Coordinate transforms involving rotation (body-frame ↔ ECEF) — owned by `helpers.se3_utils` plus the per-deployment `CameraCalibration`.
|
||||
|
||||
## Versioning Rules
|
||||
|
||||
- **Breaking changes** (function renamed/removed, signature changed, ENU sign convention flipped, return shape changed) require a new major version + a deprecation pass through C4, C5, C6, C8, C10, C11, C12.
|
||||
- **Non-breaking additions** (new helper function, new optional kwarg with safe default) require a minor version bump.
|
||||
- Adding a new datum is a major version (the static-only design assumes WGS84).
|
||||
|
||||
## Test Cases
|
||||
|
||||
| Case | Input | Expected | Notes |
|
||||
|------|-------|----------|-------|
|
||||
| valid-roundtrip-ecef | `LatLonAlt(50.0, 30.0, 100.0)` | `ecef_to_latlonalt(latlonalt_to_ecef(p))` matches `p` within `atol=1e-9 deg, 1e-6 m` | Round-trip happy path |
|
||||
| valid-roundtrip-enu | origin + point ~10 km away | `local_enu_to_latlonalt(origin, latlonalt_to_local_enu(origin, p))` matches `p` within 1 m horizontal + 1 cm vertical | ENU round-trip |
|
||||
| valid-tile-roundtrip-z18 | `(zoom=18, lat=50.45, lon=30.52)` | `latlon_to_tile_xy` returns valid `(x, y)`; `tile_xy_to_latlon_bounds(zoom, x, y)` contains the input lat/lon | Slippy-map convention |
|
||||
| valid-tile-bounds-z18 | `(zoom=18, x=148000, y=89400)` | bounds returned with non-zero area; corners at expected slippy-map lat/lon | Tile bounds |
|
||||
| invalid-lat-out-of-range | lat = 95.0 in `latlon_to_tile_xy` | `WgsConversionError` mentions Web-Mercator latitude range | Slippy-map invariant |
|
||||
| invalid-zoom-too-high | zoom = 25 | `WgsConversionError` mentions zoom range `[0, 22]` | Zoom guard |
|
||||
| invalid-tile-xy-out-of-range | `(zoom=18, x=2^18, y=0)` | `WgsConversionError` mentions tile-xy range | Tile-xy guard |
|
||||
| invalid-shape | `ecef_to_latlonalt(np.array([1.0, 2.0]))` (shape (2,)) | `WgsConversionError` mentions expected shape (3,) | Shape contract |
|
||||
| no-upward-imports | static import scan | only `_types`, `pyproj`, numpy, stdlib | Layer 1 invariant |
|
||||
| determinism | same input through any function twice | byte-equal outputs | Pure-function determinism |
|
||||
|
||||
## Change Log
|
||||
|
||||
| Version | Date | Change | Author |
|
||||
|---------|------|--------|--------|
|
||||
| 1.0.0 | 2026-05-10 | Initial contract derived from `_docs/02_document/common-helpers/04_helper_wgs_converter.md` | autodev decompose Step 2 |
|
||||
Reference in New Issue
Block a user