[AZ-331] C1 VioStrategy: Protocol + DTOs + factory + C5 migration

Freezes the c1_vio Public API per
_docs/02_document/contracts/c1_vio/vio_strategy_protocol.md v1.0.0:

- VioStrategy Protocol (4 methods: process_frame, reset_to_warm_start,
  health_snapshot, current_strategy_label) in
  components/c1_vio/interface.py.
- DTOs (VioOutput, VioHealth, FeatureQuality, WarmStartPose) + VioState
  enum in _types/nav.py — L1 placement so C5 + C13 consume them without
  crossing the components.* boundary (AZ-270 AC-6). The new VioOutput
  shape (frame_id: str, relative_pose_T: gtsam.Pose3,
  pose_covariance_6x6, imu_bias, feature_quality, emitted_at_ns)
  replaces the AZ-263 scaffolding in _types/vio.py, which is now
  deleted.
- VioError family (VioInitializingError / VioDegradedError /
  VioFatalError) in components/c1_vio/errors.py. Documented
  rationale: the degraded-operation path returns a VioOutput with
  inflated covariance + VioHealth.state=DEGRADED rather than raising
  VioDegradedError — the error type exists only for the rare
  degraded->fatal transition.
- C1VioConfig per-component config block (strategy enum,
  lost_frame_threshold default 9, warm_start_max_frames default 5)
  with constructor-time validation rejecting unknown strategy labels.
- StrategyNotAvailableError added to runtime_root/errors.py;
  composition-time error distinct from the VioError family.
- Composition-root factory build_vio_strategy in
  runtime_root/vio_factory.py with three BUILD_* gates (BUILD_OKVIS2,
  BUILD_VINS_MONO, BUILD_KLT_RANSAC). Concrete strategy modules are
  imported lazily via __import__ AFTER the flag check — Tier-0
  workstation builds with the flag OFF MUST NOT load the strategy
  module (Risk-2 / I-5; verifiable via sys.modules).
- 36 conformance tests cover all 9 ACs + NFR-perf-factory
  (p99 build under 200 ms x 1000 calls) + NFR-reliability-error-family.
  AC-8 introspects the contract file's Shape table and asserts method
  parity against the runtime Protocol; AC-9 asserts the frame_id
  annotation is 'str' (PEP-563 stringified).

C5 migration (consumers of the new VioOutput shape):
- gtsam_isam2_estimator.py + eskf_baseline.py: replaced
  vio.timestamp -> vio.emitted_at_ns (drops _datetime_to_ns on the
  VIO path), vio.pose_se3 -> vio.relative_pose_T (gtsam.Pose3 direct;
  drops _pose_se3_to_gtsam / _pose_se3_to_array), vio.covariance_6x6
  -> vio.pose_covariance_6x6 (rename).
- key_for_frame signature widened to UUID | int | str to accept the
  new str frame_id.
- 4 C5 test files migrated to the new VioOutput shape with helper
  fixtures producing ImuBias + FeatureQuality + str frame_id.
- c5_state/interface.py TYPE_CHECKING import path updated.

Bootstrap healthcheck + test_types_importable updated to drop the
deleted _types/vio module and pick up _types/inference (AZ-297) in
the same sweep.

Full unit-test sweep: 884 passed, 2 pre-existing environment skips
(cmake, actionlint).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 04:44:31 +03:00
parent daff5d4d1c
commit 6c7d24f7e0
20 changed files with 1027 additions and 81 deletions
+13 -1
View File
@@ -2,7 +2,7 @@
These are raised at composition time (``build_*`` factory entry) and
NOT during the running flight. Components own their per-runtime error
families; this module owns the cross-component selection error.
families; this module owns the cross-component selection errors.
"""
from __future__ import annotations
@@ -22,3 +22,15 @@ class RuntimeNotAvailableError(RuntimeError):
The message MUST name the requested runtime label so the operator can
correlate against ``.env``'s ``BUILD_*`` matrix without guessing.
"""
class StrategyNotAvailableError(RuntimeError):
"""Raised when ``build_vio_strategy`` is asked for a VIO strategy whose
compile-time ``BUILD_*`` flag is OFF (AZ-331).
Distinct from :class:`RuntimeNotAvailableError` because the C1
contract names this error type explicitly (AC-5). The message
MUST name both the requested strategy label and the missing
``BUILD_*`` flag so the operator can correlate against the
binary's compile matrix.
"""
@@ -0,0 +1,114 @@
"""C1 VIO strategy composition-root factory (AZ-331).
:func:`build_vio_strategy` selects exactly one strategy by
``config.components['c1_vio'].strategy`` and respects compile-time
``BUILD_*`` gating: requesting a strategy whose flag is OFF raises
:class:`StrategyNotAvailableError` at composition time (NOT at first
frame).
Concrete strategy modules (``okvis2``, ``vins_mono``, ``klt_ransac``)
are imported lazily — a Tier-0 workstation build with
``BUILD_OKVIS2=OFF`` MUST NOT load ``c1_vio.okvis2`` (Risk-2 / I-5;
verifiable via ``sys.modules``).
"""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from gps_denied_onboard.runtime_root.errors import StrategyNotAvailableError
if TYPE_CHECKING:
from gps_denied_onboard.components.c1_vio import (
C1VioConfig,
VioStrategy,
)
from gps_denied_onboard.components.c13_fdr import FdrClient
from gps_denied_onboard.config.schema import Config
__all__ = ["build_vio_strategy"]
_STRATEGY_TO_BUILD_FLAG: dict[str, str] = {
"okvis2": "BUILD_OKVIS2",
"vins_mono": "BUILD_VINS_MONO",
"klt_ransac": "BUILD_KLT_RANSAC",
}
_STRATEGY_TO_MODULE: dict[str, tuple[str, str]] = {
"okvis2": ("gps_denied_onboard.components.c1_vio.okvis2", "Okvis2Strategy"),
"vins_mono": (
"gps_denied_onboard.components.c1_vio.vins_mono",
"VinsMonoStrategy",
),
"klt_ransac": (
"gps_denied_onboard.components.c1_vio.klt_ransac",
"KltRansacStrategy",
),
}
def _is_build_flag_on(flag_name: str) -> bool:
"""Read a compile-time ``BUILD_*`` flag from the environment.
``ON`` / ``1`` / ``true`` / ``yes`` (case-insensitive) → ``True``;
anything else (including unset) → ``False``. Defaults to OFF so
test environments must opt-in explicitly per strategy.
"""
raw = os.environ.get(flag_name, "")
return raw.strip().lower() in {"on", "1", "true", "yes"}
def _c1_config(config: "Config") -> "C1VioConfig":
"""Pull the registered C1 config block.
``c1_vio.__init__`` registers it on import; a missing
registration is a developer error and surfaces as ``KeyError``
rather than a silent fallback.
"""
return config.components["c1_vio"]
def build_vio_strategy(
config: "Config",
*,
fdr_client: "FdrClient",
) -> "VioStrategy":
"""Construct the :class:`VioStrategy` impl selected by config.
1. Reads ``config.components['c1_vio'].strategy``.
2. Checks the matching ``BUILD_*`` flag — if OFF, raises
:class:`StrategyNotAvailableError` BEFORE any import.
3. Lazily imports the concrete strategy module.
4. Constructs and returns the strategy instance, passing
``config`` and ``fdr_client``.
Raises :class:`StrategyNotAvailableError` when the compile-time
flag is OFF (canonical Tier-0 path) or when the concrete strategy
module has not been built yet (AZ-332 / AZ-333 / AZ-334 pending).
"""
block = _c1_config(config)
strategy = block.strategy
flag_name = _STRATEGY_TO_BUILD_FLAG.get(strategy)
module_info = _STRATEGY_TO_MODULE.get(strategy)
if flag_name is None or module_info is None:
raise StrategyNotAvailableError(
f"VioStrategy {strategy!r} is not buildable in this binary."
)
if not _is_build_flag_on(flag_name):
raise StrategyNotAvailableError(
f"VioStrategy {strategy!r} requires {flag_name}=ON in this "
"binary; the flag is OFF."
)
module_name, class_name = module_info
try:
module = __import__(module_name, fromlist=[class_name])
except ModuleNotFoundError as exc:
raise StrategyNotAvailableError(
f"VioStrategy {strategy!r} is configured but its concrete impl "
f"module {module_name!r} has not been built into this binary "
"yet (AZ-332 / AZ-333 / AZ-334 pending)."
) from exc
strategy_cls = getattr(module, class_name)
return strategy_cls(config, fdr_client=fdr_client)