Files
Oleksandr Bezdieniezhnykh 4fdf1968af [autodev] Step 13 partial: helpers 1-4 cycle-1 doc sync
Batch 5a of the cycle-1 doc sync. For each of the four
foundation helpers (imu_preintegrator, se3_utils,
lightglue_runtime, wgs_converter):

- Append "Cycle-1 operational reality" section to the
  existing common-helpers/<NN>_*.md, documenting what the
  shipped implementation actually exposes vs. the design-
  intent sketch (interfaces, exception types, public
  constants, AZ-task lineage).

Specific cycle-1 facts captured per helper:

- imu_preintegrator (AZ-276): make_imu_preintegrator
  factory, BMI088-class noise defaults, single
  ImuPreintegrationError exception, actual return type is
  PreintegratedCombinedMeasurements (consumer builds the
  CombinedImuFactor), destructive reset_with_bias semantics,
  first-sample-not-integrated dt=0 handling.
- se3_utils (AZ-277): SE3 = gtsam.Pose3 re-export,
  Se3InvalidMatrixError, strict caller-orthogonalisation
  invariant, _DEFAULT_ROT_ATOL=1e-6 and small-angle Taylor
  cutoff for exp_map, is_valid_rotation predicate, strict
  dtype=float64 everywhere.
- lightglue_runtime (AZ-278 / R14 fix): EngineHandle
  Protocol-typed constructor, LightGlueRuntimeError +
  LightGlueConcurrentAccessError, non-blocking concurrent-
  access guard (raises rather than serialises),
  match_batch equal-length precondition, composition-root
  single-instance into C2.5 + C3.
- wgs_converter (AZ-279 + AZ-490): WEB_MERCATOR_MAX_LAT_DEG
  and MAX_ZOOM constants, WgsConversionError, ECEF arrays
  are ndarray(3,) float64, new horizontal_distance_m method
  (AZ-490 takeoff-origin bounded-delta gate), slippy-map
  tile math hand-rolled to match satellite-provider on-disk
  layout.

Two contract files (imu_preintegrator.md and
wgs_converter.md) need follow-up minor revisions to match
shipped surface; queued for the next contracts-folder
sweep, noted inline in each helper's new section.

Also refresh D-CROSS-CVE-1 opencv-pin leftover replay
timestamp (8-min debounce — gtsam upstream state cannot
change in that window).

Bumps _docs/_autodev_state.md sub_step detail.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 17:33:59 +03:00

4.8 KiB

Common Helper — WgsConverter

Purpose

WGS84 ↔ local tangent-plane (ENU/NED) ↔ tile pixel-coordinate conversions. Required by every component that interacts with geographic positions — from C4's pose estimation, through C5's state graph, through C6's tile-bounding-box queries, through C8's per-FC encoding, through C10's bbox provisioning, through C12's operator UX.

Used By

  • C4 — Pose Estimation.
  • C5 — State Estimator.
  • C6 — Tile Cache + Spatial Index (bbox queries).
  • C8 — FC Adapter (per-FC encoding of LatLonAlt → MAVLink/MSP2).
  • C10 — Pre-flight Cache Provisioning (bbox → tile-id list).
  • C12 — Operator Pre-flight Tooling (operator-entered bbox).

Interface (sketch)

class WgsConverter:
    @staticmethod
    def latlonalt_to_ecef(p: LatLonAlt) -> Vector3
    @staticmethod
    def ecef_to_latlonalt(p: Vector3) -> LatLonAlt
    @staticmethod
    def latlonalt_to_local_enu(origin: LatLonAlt, p: LatLonAlt) -> Vector3
    @staticmethod
    def local_enu_to_latlonalt(origin: LatLonAlt, p_enu: Vector3) -> 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

Implementation Notes

  • Stateless; pure functions.
  • Backed by pyproj for the geodesy primitives; tile_xy math uses the standard slippy-map convention (matches satellite-provider's on-disk layout).
  • All conversions use WGS84 ellipsoid; no datum-shift complexity.

Caveats

  • The static-only design satisfies the coderule.mdc constraint ("only use static methods for pure self-contained computations"). If a future deployment needs alternative datum support, switch to an instance-based factory then.
  • Tile-coordinate math is zoom-level-sensitive; callers MUST pass the right zoom level for the tile in question (typically zoomLevel from TileMetadata).

Cycle-1 operational reality

The shipped surface in src/gps_denied_onboard/helpers/wgs_converter.py (AZ-279, extended by AZ-490) is the canonical entry point for every geodesy hop in the system. Stateless and pyproj-backed (EPSG:4326 ↔ EPSG:4978), with module-level Transformer instances cached on import.

  • Public constantsWEB_MERCATOR_MAX_LAT_DEG = 85.0511287798066 (the slippy-map cutoff; outside this band, latlon_to_tile_xy raises) and MAX_ZOOM = 22 (slippy-map upper bound; exposed so callers can validate operator input without hard-coding the limit).
  • WgsConversionError — single public exception type (subclasses ValueError). Raised on: non-finite lat/lon/alt; latitude/longitude out of WGS-84 range; non-ndarray or wrong-shape ECEF input; non-float64 ECEF input; zoom not a non-bool int or out of [0, MAX_ZOOM]; tile (x, y) out of [0, 2**zoom); latitude outside the Web-Mercator band for latlon_to_tile_xy.
  • ECEF arrays are np.ndarray of shape (3,) and dtype=float64 — the Interface sketch above uses "Vector3" as a placeholder. latlonalt_to_ecef returns a freshly-allocated array; ecef_to_latlonalt and local_enu_to_latlonalt validate input shape/dtype and raise WgsConversionError on mismatch.
  • horizontal_distance_m(a: LatLonAlt, b: LatLonAlt) -> float — new method added in AZ-490 for C5's set_takeoff_origin bounded-delta gate. Computes the geodesic horizontal distance in metres via the same ECEF transformer used by latlonalt_to_local_enu: convert b into the local-ENU frame anchored at a, then hypot(east, north). Altitude is ignored (flat-distance on the WGS-84 ellipsoid, NOT a 3-D distance). Accuracy ≤ sub-mm vs. Vincenty for separations ≤ a few km — the bounded-delta gate operates at ≤ ~1 km, so AZ-490's "geodesic horizontal distance" AC is satisfied.
  • Slippy-map tile math — hand-rolled (NOT pyproj) to match OSM's {zoom}/{x}/{y}.jpg convention byte-equal so files produced by satellite-provider round-trip exactly. latlon_to_tile_xy clamps the output into [0, n-1] after floor — out-of-band latitude is rejected before this clamp via the Web-Mercator range check. tile_xy_to_latlon_bounds returns a BoundingBox(min_lat_deg, min_lon_deg, max_lat_deg, max_lon_deg) matching the tile's outer extent.
  • pyproj importfrom pyproj import Transformer is tagged # type: ignore[import-not-found] because pyproj ships type stubs in a separate package; the project pin does not add the stubs. Don't drop the ignore comment in mypy passes.

Cycle-1 task lineage

  • AZ-279 — initial helper, contract producer (latlonalt_to_*, *_to_latlonalt, latlon_to_tile_xy, tile_xy_to_latlon_bounds).
  • AZ-490 — horizontal_distance_m addition for C5's takeoff-origin bounded-delta gate. Contract minor revision (v1.0.0 → v1.1.0) is queued for the next contracts-folder sweep.