Files
gps-denied-onboard/_docs/02_tasks/done/AZ-279_wgs_converter.md
T
Oleksandr Bezdieniezhnykh 3acc7f33dd [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>
2026-05-11 02:03:36 +03:00

8.8 KiB

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 LatLonAlts 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.