mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-22 12:11:13 +00:00
[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:
@@ -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"
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user