Files
gps-denied-onboard/e2e/fixtures/sitl_replay_builder
Oleksandr Bezdieniezhnykh 7fb3cb3f34 [AZ-600] Batch 80: refactor sitl_replay_builder to strategy pattern
Replace per-scenario fixture builders with a parameterized strategy
framework so future Derkachi-based scenarios compose existing pieces
instead of duplicating ~200 lines of orchestration per scenario.

New e2e/fixtures/sitl_replay_builder/builder.py:
- VideoSource ABC + StillImagesSource, Mp4PassthroughSource
- TlogSource ABC + SyntheticStationaryTlog, ImuCsvTlog
- FdrProjection ABC + RawFdrPassthrough, OutboundMessagesProjection
- FixtureBuilderConfig + build_fixtures(cfg) orchestrator
- Consolidated MAVLink pack_raw_imu / pack_attitude helpers
- Consolidated run_gps_denied_replay + write_observer_fixture

build_p01_fixtures.py: 423 -> 107 lines (75% reduction).
build_p02_fixtures.py: 292 -> 98 lines (66% reduction).
_common.py: deleted (folded into builder.py).

Tests reorganized:
- test_sitl_replay_builder_builder.py (new, 33 strategy-level tests)
- test_sitl_replay_builder.py (slimmed, 6 FT-P-01 integration)
- test_sitl_replay_builder_p02.py (slimmed, 7 FT-P-02 integration)

README documents the strategy framework + a worked example for
adding FT-P-04 in ~30 lines (no new strategy code required).

Regression gate: 700 passing (was 686; +14 from finer-grained
coverage of new strategy classes and the build_fixtures orchestrator).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-17 14:19:08 +03:00
..

SITL Replay Fixture Builder (AZ-598, AZ-599, AZ-600)

Parameterized fixture-builder framework for the offline FDR-replay path used by the b75 sitl_observer module + FT-* blackbox scenarios. A new scenario typically only writes a ~60-line config factory + CLI on top of the framework — no new strategy code required.

Scenario Builder Inputs Outputs
FT-P-01 (still-image accuracy) build_p01_fixtures.py 60 AD0000NN.jpg outbound_messages_<fc>_<host>.json + observer_<fc>_<host>.json + stills.mp4 + stationary.tlog + fdr.jsonl
FT-P-02 (Derkachi drift) build_p02_fixtures.py flight_derkachi.mp4 + data_imu.csv derkachi.tlog + fdr/fdr.jsonl (FDR archive) + observer_<fc>_<host>.json

Other scenarios (FT-P-03 / 04 / 05 / 07 / 08 / 10 / 11, FT-N-01..04) will land as follow-ups; each will reuse the strategies below.

Framework (builder.py)

Three strategy ABCs decompose the per-scenario variance:

Strategy Concrete impls Used by
VideoSource — materialize the MP4 the replay CLI consumes StillImagesSource(image_paths, fps), Mp4PassthroughSource(mp4_path) b78 / b79
TlogSource — materialize the tlog the replay CLI consumes SyntheticStationaryTlog(duration_s, hz), ImuCsvTlog(csv_path, schema=DEFAULT_DERKACHI_IMU_SCHEMA) b78 / b79
FdrProjection — translate the FDR JSONL into scenario fixture shape RawFdrPassthrough(verify_estimates=True), OutboundMessagesProjection(image_ids, fdr_kind="outbound_position_estimate") b79 / b78

The build_fixtures(cfg: FixtureBuilderConfig) orchestrator composes the three strategies plus the shared run_gps_denied_replay subprocess driver and write_observer_fixture helper.

Shared helpers (in builder.py):

  • run_gps_denied_replay(video, tlog, fdr_out, *, time_offset_ms=0, ...) — shells out to the production CLI.
  • write_observer_fixture(output_path) — writes the minimal observer_*.json sitl_observer.get_observer requires.
  • pack_raw_imu(time_usec, *, xacc=0, yacc=0, zacc=0, xgyro=0, ygyro=0, zgyro=0) — parameterized RAW_IMU packer. Stationary callers pass zacc=STATIONARY_Z_ACCEL_MG (gravity).
  • pack_attitude(time_boot_ms, *, roll=0.0, pitch=0.0, yaw=0.0) — parameterized ATTITUDE packer.
  • parse_fdr_for_outbound_estimates(fdr_path, *, fdr_kind, lat_key, lon_key) — read FDR JSONL into per-image dicts.
  • verify_fdr_has_estimates(fdr_path) — assert ≥1 record_type=="estimate" record.
  • hdg_centideg_to_rad(hdg_cdeg) — utility for ATTITUDE yaw synthesis.

Adding a new scenario (worked example: FT-P-04)

FT-P-04 (Derkachi frame-to-frame registration) reuses the same Derkachi MP4

  • IMU CSV as FT-P-02 but consumes the FDR archive differently. With the framework in place, the new builder is purely a config factory:
# e2e/fixtures/sitl_replay_builder/build_p04_fixtures.py  (sketch)
from dataclasses import dataclass
from pathlib import Path
from e2e.fixtures.sitl_replay_builder.builder import (
    DEFAULT_CLI_BIN,
    FixtureBuilderConfig,
    ImuCsvTlog,
    Mp4PassthroughSource,
    RawFdrPassthrough,
    build_fixtures,
)


@dataclass(frozen=True)
class P04BuilderConfig:
    derkachi_dir: Path
    output_dir: Path
    fc_kind: str = "ardupilot"
    host: str = "sitl-host"


def build_p04_fixtures(cfg, **deps):
    mp4 = cfg.derkachi_dir / "flight_derkachi.mp4"
    csv_path = cfg.derkachi_dir / "data_imu.csv"
    builder_cfg = FixtureBuilderConfig(
        video_source=Mp4PassthroughSource(mp4_path=mp4),
        tlog_source=ImuCsvTlog(csv_path=csv_path),
        fdr_projection=RawFdrPassthrough(verify_estimates=True),
        output_dir=cfg.output_dir,
        fc_kind=cfg.fc_kind, host=cfg.host,
        tlog_filename="derkachi.tlog", fdr_subdir="fdr",
    )
    return build_fixtures(builder_cfg, **deps)

Total new code: ~30 lines + argparse CLI. No new strategy class is needed because every Derkachi-based scenario consumes the same Mp4PassthroughSource + ImuCsvTlog + RawFdrPassthrough triple. A scenario that emits a new fixture shape (e.g. FT-P-13's "anchor-search-region" record extraction) writes a new FdrProjection subclass alongside.

Per-scenario usage

FT-P-01

python -m e2e.fixtures.sitl_replay_builder.build_p01_fixtures \
  --input-dir _docs/00_problem/input_data \
  --output-dir e2e/fixtures/sitl_replay/p01 \
  --fc-kind ardupilot \
  --host sitl-host

Activation:

E2E_SITL_REPLAY_DIR=e2e/fixtures/sitl_replay/p01 \
    pytest e2e/tests/positive/test_ft_p_01_still_image_accuracy.py

FT-P-02

python -m e2e.fixtures.sitl_replay_builder.build_p02_fixtures \
  --derkachi-dir _docs/00_problem/input_data/flight_derkachi \
  --output-dir e2e/fixtures/sitl_replay/p02 \
  --fc-kind ardupilot \
  --host sitl-host

Limitations

  • The synthesised ATTITUDE has roll/pitch = 0 — acceptable for fixed-wing cruise but unrealistic for aggressive manoeuvres. Override the packer call inside a custom TlogSource when needed.
  • RAW_IMU is packed from SCALED_IMU2 columns as pass-through (no true scaled → raw unit conversion). If the SUT's tlog parser strictly demands true raw counts the builder will need a units conversion pass — surfaced as a follow-up after the first live run.
  • Auto-sync (time_offset_ms != 0) is bypassed by every scenario currently; operators running this against truly independent tlog+video pairs should override FixtureBuilderConfig.time_offset_ms.
  • iNav adapter is NOT supported by the existing builders — ArduPilot only.
  • The FDR record kind/record_type schemas are assumed to match the production contract; overrides live on each projection class.

Testing

Unit tests under e2e/_unit_tests/fixtures/:

  • test_sitl_replay_builder_builder.py — strategy-level tests (VideoSource, TlogSource, FdrProjection impls + shared helpers + build_fixtures orchestrator).
  • test_sitl_replay_builder.py — FT-P-01 scenario integration.
  • test_sitl_replay_builder_p02.py — FT-P-02 scenario integration.

All external dependencies (OpenCV, pymavlink, subprocess) are mocked via the underscore-prefixed _runner / _video_writer_factory / _imread / _mavlink_writer_factory injection points so the suite runs without a real gps-denied-replay install. The actual end-to-end run requires the SUT to be installed (pip install -e . at repo root) and is documented as a manual step until CI infrastructure catches up.