Files
gps-denied-onboard/e2e/fixtures/sitl_replay_builder/README.md
T
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

149 lines
6.4 KiB
Markdown

# 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:
```python
# 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
```bash
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:
```bash
E2E_SITL_REPLAY_DIR=e2e/fixtures/sitl_replay/p01 \
pytest e2e/tests/positive/test_ft_p_01_still_image_accuracy.py
```
### FT-P-02
```bash
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.