[AZ-332] C1 OKVIS2 Strategy: facade + binding skeleton

Python facade (`Okvis2Strategy`) is production-quality and satisfies
AZ-331's `VioStrategy` protocol; full AC-1..10 coverage with
AC-9 + NFR-perf marked `tier2`. The C++ pybind11 binding compiles
and loads but throws `OkvisFatalException("estimator not yet wired")`
on first `add_frame` — the `okvis::ThreadedKFVio` wiring is a tier2
follow-up the Step-15 Product Completeness Gate is expected to track
as a remediation task.

Resolved contradictions:

* Constructor signature aligned with the AZ-331 factory: `(config, *,
  fdr_client, clock=None)`. Calibration / preintegrator / logger
  built internally from config. No churn on AZ-331.
* IMU substrate: OKVIS2 owns its internal estimator IMU integration;
  the AZ-276 `ImuPreintegrator` is a separate substrate consumed by
  E-C5's fusion graph. Single source of truth lives at the sample
  stream, not the integrator instance.
* FDR API: `FdrClient.enqueue(record)` with new `vio.health` kind
  added to AZ-272 `KNOWN_PAYLOAD_KEYS`.

CI matrix forces `-DBUILD_OKVIS2=OFF` until the tier2 wiring task
brings Ceres / SuiteSparse / OKVIS2 vendored submodules into the
Linux build.

Files: 17 added/modified across `c1_vio/`, `fdr_client/records.py`,
`cpp/okvis2/CMakeLists.txt`, CI workflow, AZ-332 task spec
(implementation-notes section), batch 23 report.

Tests: 17 new (15 tier1 + 2 tier2). Full Tier-1 suite: 1109 pass,
2 skipped (env), 2 deselected (tier2). No regressions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-12 09:56:45 +03:00
parent 9c35776bcb
commit 1ebab29a4f
19 changed files with 2083 additions and 49 deletions
+30 -22
View File
@@ -40,7 +40,6 @@ from gps_denied_onboard.config.schema import Config, ConfigError
from gps_denied_onboard.runtime_root.errors import StrategyNotAvailableError
from gps_denied_onboard.runtime_root.vio_factory import build_vio_strategy
_CONTRACT_PATH = (
Path(__file__).resolve().parents[3]
/ "_docs/02_document/contracts/c1_vio/vio_strategy_protocol.md"
@@ -250,6 +249,16 @@ def test_ac5_build_vio_strategy_flag_off_no_import(
assert module_name not in sys.modules
# Which strategies still have NO concrete Python module on disk?
# Once an AZ-332 / AZ-333 / AZ-334 implementation lands, the
# `flag_on_but_module_missing` semantic shifts: the factory's import
# succeeds, the constructor fails on missing native binding or other
# prerequisite. We assert the meaningful-error-before-first-frame
# property holds for BOTH cases — the exception class differs by
# strategy.
_STRATEGIES_WITHOUT_PY_MODULE: tuple[str, ...] = ("vins_mono", "klt_ransac")
@pytest.mark.parametrize("strategy", sorted(_STRATEGY_MODULES))
def test_ac5_build_vio_strategy_flag_on_but_module_missing(
monkeypatch, strategy_module_cleanup, strategy
@@ -257,9 +266,20 @@ def test_ac5_build_vio_strategy_flag_on_but_module_missing(
_, _, flag = _STRATEGY_MODULES[strategy]
monkeypatch.setenv(flag, "ON")
config = _config_with_strategy(strategy)
with pytest.raises(StrategyNotAvailableError) as exc_info:
build_vio_strategy(config, fdr_client=object())
assert strategy in str(exc_info.value)
if strategy in _STRATEGIES_WITHOUT_PY_MODULE:
# Module not yet implemented — factory's __import__ raises
# ModuleNotFoundError, rewrapped into StrategyNotAvailableError.
with pytest.raises(StrategyNotAvailableError) as exc_info:
build_vio_strategy(config, fdr_client=object())
assert strategy in str(exc_info.value)
else:
# Module IS implemented (AZ-332). Factory import succeeds, then
# the strategy constructor fails on missing native binding —
# which the strategy MUST surface as VioFatalError BEFORE any
# frame is processed (the AC-5 spirit: no silent fall-through).
with pytest.raises(VioFatalError) as exc_info:
build_vio_strategy(config, fdr_client=object())
assert "native binding" in str(exc_info.value)
# ----------------------------------------------------------------------
@@ -292,9 +312,7 @@ def test_ac7_current_strategy_label_matches_config(
config = _config_with_strategy(strategy)
instance = build_vio_strategy(config, fdr_client=object())
assert instance.current_strategy_label() == strategy
assert (
instance.current_strategy_label() == config.components["c1_vio"].strategy
)
assert instance.current_strategy_label() == config.components["c1_vio"].strategy
# ----------------------------------------------------------------------
@@ -314,9 +332,7 @@ def _methods_from_contract() -> set[str]:
def _protocol_methods(proto: type) -> set[str]:
return {
name
for name in dir(proto)
if not name.startswith("_") and callable(getattr(proto, name))
name for name in dir(proto) if not name.startswith("_") and callable(getattr(proto, name))
}
@@ -338,9 +354,7 @@ def test_ac8_contract_methods_match_protocol() -> None:
def test_ac8_contract_lists_all_three_error_subtypes() -> None:
text = _CONTRACT_PATH.read_text(encoding="utf-8")
for name in {"VioInitializingError", "VioDegradedError", "VioFatalError"}:
assert name in text, (
f"Contract file is missing the documented error subtype {name!r}"
)
assert name in text, f"Contract file is missing the documented error subtype {name!r}"
# ----------------------------------------------------------------------
@@ -358,9 +372,7 @@ def test_ac9_vio_output_frame_id_is_typed_str() -> None:
:class:`SE3`).
"""
annotation = VioOutput.__annotations__["frame_id"]
assert annotation == "str", (
f"frame_id annotation should be 'str'; got {annotation!r}"
)
assert annotation == "str", f"frame_id annotation should be 'str'; got {annotation!r}"
def test_ac9_vio_output_docstring_documents_echo_invariant() -> None:
@@ -388,9 +400,7 @@ def test_nfr_reliability_strategy_not_available_not_in_family() -> None:
assert not issubclass(StrategyNotAvailableError, VioError)
def test_nfr_perf_factory_under_200ms_p99(
monkeypatch, strategy_module_cleanup
) -> None:
def test_nfr_perf_factory_under_200ms_p99(monkeypatch, strategy_module_cleanup) -> None:
"""Factory p99 ≤ 200 ms across 1000 calls (NFR-perf-factory)."""
strategy = "klt_ransac"
_, _, flag = _STRATEGY_MODULES[strategy]
@@ -406,9 +416,7 @@ def test_nfr_perf_factory_under_200ms_p99(
durations_ms.sort()
p99 = durations_ms[int(0.99 * len(durations_ms))]
assert p99 <= 200.0, (
f"build_vio_strategy() p99={p99:.3f} ms exceeds 200 ms NFR"
)
assert p99 <= 200.0, f"build_vio_strategy() p99={p99:.3f} ms exceeds 200 ms NFR"
# ----------------------------------------------------------------------