diff --git a/_docs/02_document/common-helpers/01_helper_imu_preintegrator.md b/_docs/02_document/common-helpers/01_helper_imu_preintegrator.md index 44f1366..7155f49 100644 --- a/_docs/02_document/common-helpers/01_helper_imu_preintegrator.md +++ b/_docs/02_document/common-helpers/01_helper_imu_preintegrator.md @@ -30,3 +30,19 @@ class ImuPreintegrator: - Bias drift is the responsibility of the consumers (C1 + C5) who call `reset_with_bias(...)` whenever their estimate of the IMU bias changes. - The preintegrator does not own a clock — every `integrate_*` call requires a monotonic timestamp on the IMU sample. + +## Cycle-1 operational reality + +The shipped surface in `src/gps_denied_onboard/helpers/imu_preintegrator.py` (AZ-276) extends the sketch above; this section is the authoritative inventory of what cycle-1 consumers actually see. Sketch return types in § Interface remain accurate for *intent*; the precise types appear here. + +- **Factory** — `make_imu_preintegrator(calibration: CameraCalibration) -> ImuPreintegrator` reads gyro/accel noise covariances from `calibration.metadata["imu_noise_model"]` and constructs `gtsam.PreintegrationCombinedParams.MakeSharedU(gravity_m_s2)`. Every key in the noise block is optional and independently defaulted, so partial blocks are honoured. +- **BMI088-class defaults** (used when `imu_noise_model` is absent — bring-up + unit tests only; production deployments MUST supply a per-deployment noise model in `CameraCalibration.metadata`): `accel_noise_density=1.86e-3 m/s²/√Hz`, `gyro_noise_density=1.87e-4 rad/s/√Hz`, `accel_bias_rw=4.33e-4 m/s³/√Hz`, `gyro_bias_rw=2.66e-5 rad/s²/√Hz`, `integration_noise=1e-8`, `gravity_m_s2=9.80665`. +- **`ImuPreintegrationError`** — single public exception type. Raised on (a) non-monotonic `sample.ts_ns`, (b) GTSAM's lower-level PIM rejection (wrapped, not propagated), (c) `current_preintegration()` / `reset_for_new_keyframe()` called with zero samples since last reset. Carries offending vs. previous `ts_ns` in its message so consumers can write FDR `kind="imu.skew"` events per AZ-276 Risk 2. +- **Actual return type** of `current_preintegration()` and `reset_for_new_keyframe()` is `gtsam.PreintegratedCombinedMeasurements` (the PIM), not a constructed `CombinedImuFactor`. Consumers build the factor at attach time as `gtsam.CombinedImuFactor(*keys, pim)` — the helper cannot know the GTSAM keys. The `CombinedImuFactor` alias is still re-exported (so consumers do NOT import GTSAM directly), but it names the **type** the consumer constructs, not the helper's return value. Contract `imu_preintegrator.md` v1.0.0 still reflects the original "returns CombinedImuFactor" wording — a contract minor revision is queued for the next contracts-folder sweep. +- **`reset_with_bias` is destructive in cycle-1** — it discards the partial integration accumulator (re-initialises the PIM with the new bias and a clean `last_ts_ns=None`). The contract's "subsequent samples only" wording is honoured at the new-window granularity: consumers MUST close the prior window via `reset_for_new_keyframe()` BEFORE changing bias if they want to retain its contribution. +- **First-sample dt handling** — the first sample after a reset is recorded (`_last_ts_ns` updated, `_sample_count` incremented) but NOT integrated (GTSAM rejects `dt==0`). The second sample is the first integration call. This matters for AC-1 length counting: 100 samples ⇒ 99 GTSAM integrations. + +### Cycle-1 task lineage + +- AZ-276 — initial helper, contract producer. +- No cycle-1 follow-up tasks touched this helper. diff --git a/_docs/02_document/common-helpers/02_helper_se3_utils.md b/_docs/02_document/common-helpers/02_helper_se3_utils.md index ea1d75b..fe62f70 100644 --- a/_docs/02_document/common-helpers/02_helper_se3_utils.md +++ b/_docs/02_document/common-helpers/02_helper_se3_utils.md @@ -28,3 +28,19 @@ def adjoint(pose: SE3) -> Matrix6 ## Caveats - Library-grade Lie-algebra functions exist in `manifpy` and `pylie`; we use GTSAM's primitives directly to avoid pulling in a second math library. If a future strategy needs richer manifold ops, evaluate `manifpy` then. + +## Cycle-1 operational reality + +The shipped surface in `src/gps_denied_onboard/helpers/se3_utils.py` (AZ-277) extends the sketch above; this section is the authoritative inventory of what cycle-1 consumers actually see. + +- **Type alias** — `SE3 = gtsam.Pose3` is re-exported by the helper. Consumers MUST import `SE3` from `helpers.se3_utils` and never `gtsam.Pose3` directly (keeps the Lie-algebra backend swappable without touching C1/C4/C5). +- **`Se3InvalidMatrixError`** — single public exception type. Raised on (a) wrong array shape, (b) `dtype != float64`, (c) bottom row != `[0, 0, 0, 1]`, (d) rotation drift `‖R^TR − I‖_F > atol`, (e) negative-determinant rotation (mirror), (f) non-ndarray inputs. `matrix_to_se3` and `exp_map` raise this; `se3_to_matrix`, `log_map`, `adjoint` are no-throw on the typed input. +- **Strict caller-orthogonalisation invariant** — the helper does NOT silently re-orthogonalise. AC-7 / `matrix_to_se3` always validates `‖R^TR − I‖_F ≤ atol` and rejects drift. Callers (C4 in particular, since `solvePnPRansac` output is not orthogonal to numerical precision) MUST run their own orthogonalisation (`cv2.Rodrigues` round-trip or `scipy.linalg.polar`) before calling `matrix_to_se3`. Default tolerance: `_DEFAULT_ROT_ATOL = 1e-6`; callers can pass a looser `atol` for relaxed contexts (none in cycle-1). +- **`exp_map` near-identity fallback** — twist vectors with `‖xi‖ < _SMALL_ANGLE_THRESHOLD = 1e-10` return the identity `SE3()` instead of delegating to GTSAM's `Pose3.Expmap`. This guards against the `sin(theta)/theta` under-flow that surfaces when iSAM2's relinearisation produces a near-identity twist after a converged step. +- **`is_valid_rotation(R_3x3, *, atol=1e-6)`** — predicate (no exception) for "is this matrix safe to feed to `matrix_to_se3`?". Returns False for non-ndarray, wrong shape, wrong dtype, orthogonality drift > atol, or negative determinant. Cycle-1 consumers: C4's `MarginalsAdapter` short-circuit (`opencv_gtsam_marginals.py` from AZ-358) and the contract test for AC-7. +- **`dtype=float64` everywhere** — every public function enforces `float64`. `np.ndarray` returned from `se3_to_matrix`, `log_map`, `adjoint` is `np.ascontiguousarray(..., dtype=np.float64)` so callers can pass it through to GTSAM/Eigen without a copy. + +### Cycle-1 task lineage + +- AZ-277 — initial helper, contract producer. +- No cycle-1 follow-up tasks touched this helper. diff --git a/_docs/02_document/common-helpers/03_helper_lightglue_runtime.md b/_docs/02_document/common-helpers/03_helper_lightglue_runtime.md index 291612d..ae018b8 100644 --- a/_docs/02_document/common-helpers/03_helper_lightglue_runtime.md +++ b/_docs/02_document/common-helpers/03_helper_lightglue_runtime.md @@ -28,3 +28,20 @@ class LightGlueRuntime: ## Caveats - The features fed in MUST come from the same backbone as the LightGlue engine was trained for (DISK in production-default; ALIKED / XFeat in alternates). Mixing backbones is a runtime error caught by the matcher's input shape check. + +## Cycle-1 operational reality + +The shipped surface in `src/gps_denied_onboard/helpers/lightglue_runtime.py` (AZ-278) is the structural fix for R14 (re-rank vs. matcher double-load of the LightGlue engine). Composition root wires ONE `LightGlueRuntime` instance and constructor-injects it into both C2.5 (`InlierBasedReranker`) and C3 (`CrossDomainMatcher`). + +- **Constructor** — `LightGlueRuntime(engine_handle: EngineHandle)`. `EngineHandle` is a `Protocol` from `_types/manifests.py` (descriptor_dim, forward(...)) — Layer 1 helper invariant means we do NOT import `gps_denied_onboard.components.*`. C7's `InferenceRuntime.deserialize_engine(LIGHTGLUE_ENGINE_CACHE_ENTRY)` returns the concrete handle at takeoff; the composition root passes it in. +- **Construction guards** — `LightGlueRuntimeError` is raised for: `engine_handle is None`; `engine_handle` missing the `descriptor_dim` Protocol attribute; `descriptor_dim < 1`. +- **Descriptor-dim mismatch** — both `match` and `match_batch` validate every `KeypointSet.descriptors` against the engine's `descriptor_dim` and raise `LightGlueRuntimeError` on mismatch (catches "DISK features fed into an ALIKED-trained LightGlue" regressions). +- **Concurrent-access guard is non-blocking** — the runtime owns a `threading.Lock` but never `.acquire(blocking=True)`. Concurrent entry raises `LightGlueConcurrentAccessError` immediately rather than serialising. This is intentional: if you see this exception, the composition root wired the runtime into more than one thread by mistake — fix the composition, do NOT add blocking serialisation. The lock guards the body of both `match` and `match_batch`; `descriptor_dim()` is lock-free. +- **`match_batch` equal-length precondition** — `LightGlueRuntimeError` if `len(features_a_list) != len(features_b_list)`. Iteration uses `zip(..., strict=True)`. Indexed validation labels (`features_a_list[i]`) so a downstream test failure points to the offending pair. +- **`descriptor_dim()` accessor** — returns the engine's descriptor dim as a plain `int` (cached on construction so per-call overhead is one attribute lookup). +- **Public exceptions** — `LightGlueRuntimeError` (construction / descriptor-dim mismatch / batch-length mismatch) and `LightGlueConcurrentAccessError` (composition-root violation). Both subclass `RuntimeError`. + +### Cycle-1 task lineage + +- AZ-278 — initial helper, contract producer. +- R14 structural fix: composition-root single-instance injection into C2.5 + C3 lands in `runtime_root/airborne_bootstrap.py` (the `lightglue_runtime` pre-constructed key consumed by both `_C2_5_STRATEGIES` and `_C3_STRATEGIES`). diff --git a/_docs/02_document/common-helpers/04_helper_wgs_converter.md b/_docs/02_document/common-helpers/04_helper_wgs_converter.md index 0d33f3f..73ef535 100644 --- a/_docs/02_document/common-helpers/04_helper_wgs_converter.md +++ b/_docs/02_document/common-helpers/04_helper_wgs_converter.md @@ -41,3 +41,19 @@ class WgsConverter: - 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 constants** — `WEB_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` import** — `from 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. diff --git a/_docs/_autodev_state.md b/_docs/_autodev_state.md index da48edd..b374075 100644 --- a/_docs/_autodev_state.md +++ b/_docs/_autodev_state.md @@ -8,7 +8,7 @@ status: in_progress sub_step: phase: 8 name: component-doc-updates - detail: "batch 4/~5 done (c10/c11/c12/c13); next: 8 helpers; then tests/" + detail: "batch 5a in progress (4 helpers: imu_preint/se3/lightglue/wgs); batch 5b=4 more helpers; then tests/" retry_count: 0 cycle: 1 tracker: jira diff --git a/_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md b/_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md index d64f7e7..8457137 100644 --- a/_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md +++ b/_docs/_process_leftovers/2026-05-11_d_cross_cve_1_opencv_pin_deferred.md @@ -1,10 +1,11 @@ # D-CROSS-CVE-1 opencv-python pin deferred — gtsam/numpy ABI block **Recorded**: 2026-05-11T02:55+03:00 (Europe/Kyiv) -**Last replay attempt**: 2026-05-19T17:18+03:00 (Europe/Kyiv) — replay re-checked -at start of next `/autodev` invocation (5 min after prior check). `gtsam==4.2.1` -still latest on PyPI with `requires_dist: numpy<2.0.0,>=1.11.0`. Replay -condition (numpy>=2 stable wheels) still NOT met. Leftover remains open. +**Last replay attempt**: 2026-05-19T17:26+03:00 (Europe/Kyiv) — replay re-checked +at start of next `/autodev` invocation (8 min after prior check). PyPI not +re-queried this round (debounced — `gtsam` upstream state cannot change in +8 minutes). Replay condition (numpy>=2 stable wheels) still NOT met. +Leftover remains open. **Status**: deferred-non-user (replay when upstream gtsam wheels target numpy>=2) ## What is blocked