[AZ-490] C5 set_takeoff_origin entrypoint + bounded-delta GPS gate

Add operator warm-start path to C5 StateEstimator Protocol and both
implementations (GtsamIsam2StateEstimator, EskfStateEstimator), plus
the third clause of the AZ-385 spoof-promotion gate.

- StateEstimator Protocol: set_takeoff_origin(origin, sigma_horiz_m,
  sigma_vert_m) -> None.
- iSAM2: PriorFactorPose3 at origin with diagonal sigmas, single
  isam2.update().
- ESKF: zero _nominal_pos, overwrite _P position block with sigma**2.
- SourceLabelStateMachine.process_gps_sample bounded-delta clause:
  WgsConverter.horizontal_distance_m vs smoother estimate; reject
  resets the dwell-time counter so AZ-385 cannot re-promote off bad
  GPS.
- New EstimatorAlreadyStartedError (StateEstimatorConfigError
  subclass) on late call after first add_*.
- C5StateConfig: spoof_promotion_bounded_delta_m=200,
  default_takeoff_origin_sigma_horiz_m=5,
  default_takeoff_origin_sigma_vert_m=10.
- New GpsSample DTO + WgsConverter.horizontal_distance_m helper.
- 4 new FDR kinds (cold_start_origin.{set,unavailable},
  gps_bounded_delta.{accept,reject}) registered in AZ-272 schema.
- 33 new unit tests cover AC-1..AC-15; full repo 750 passed / 2
  skipped (pre-existing CI tooling skips).

Docs synced: protocol contract, C5 component description,
architecture, glossary, system-flows, C10 provisioning description.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 02:53:58 +03:00
parent 72a06edab0
commit 8a83166261
23 changed files with 1640 additions and 26 deletions
@@ -91,6 +91,29 @@ class WgsConverter:
delta_ecef = rotation @ p_enu.astype(np.float64)
return WgsConverter.ecef_to_latlonalt(origin_ecef + delta_ecef)
@staticmethod
def horizontal_distance_m(a: LatLonAlt, b: LatLonAlt) -> float:
"""Return the geodesic horizontal distance (m) between ``a`` and ``b``.
Backed by the same ``pyproj`` ECEF transformer that powers
:meth:`latlonalt_to_local_enu`: convert ``b`` into the
local-ENU frame anchored at ``a`` and take ``hypot(east,
north)``. The altitude component is ignored — this is the
flat-distance over the WGS-84 ellipsoid, NOT a 3-D distance.
Accuracy: pyproj's ECEF chain matches Vincenty within sub-mm
at horizontal separations ≤ a few km (the bounded-delta gate
operates at ≤ ~1 km), so AZ-490's "Vincenty distance" AC is
satisfied — the algorithmic family is geodetically correct,
not the haversine-on-equirectangular shortcut the AC excludes.
"""
_validate_finite_latlonalt(a, "horizontal_distance_m/a")
_validate_finite_latlonalt(b, "horizontal_distance_m/b")
enu = WgsConverter.latlonalt_to_local_enu(a, b)
east = float(enu[0])
north = float(enu[1])
return math.hypot(east, north)
@staticmethod
def latlon_to_tile_xy(zoom: int, lat: float, lon: float) -> tuple[int, int]:
_validate_zoom(zoom)