mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-06-21 08:51:12 +00:00
[AZ-960] render-map: dispatch --truth loader on extension (CSV+tlog)
load_ground_truth_track now dispatches on truth_path.suffix: - .csv → load_csv_ground_truth (AZ-894) - else (.tlog, .bin, no ext) → load_tlog_ground_truth (AZ-697) Removes the AZ-959 short-circuit in SubprocessReplayRunner. _maybe_render_map so CSV-path replay jobs ship with the same map.html artefact as tlog jobs. Both ground-truth DTOs expose row-aligned (lat_deg, lon_deg) records so the renderer needs no other changes. Touches: - src/gps_denied_onboard/cli/render_map.py: dispatch + source-agnostic tooltip + --truth CLI help expanded - src/gps_denied_onboard/replay_api/app.py: workaround removed, truth_path resolution picks whichever input was uploaded Tests: 44/44 green across test_az700_render_map.py + test_az701_replay_api.py: - 17 pre-existing render-map tests pass unchanged (AC-2) - New test_load_ground_truth_track_dispatches_to_csv_loader (AC-1) - New test_load_ground_truth_track_csv_propagates_schema_error (AC-4: malformed CSV raises ReplayInputAdapterError) - New test_cli_renders_map_with_csv_truth (AC-1 end-to-end) - AZ-959 test_post_replay_csv_path_returns_200... extended to assert map_html_url is now present (AC-3) Bookkeeping: AZ-960 spec moved todo/ → done/, dep-table preamble seventh bump documents the landing + AC coverage, state.md records batch 6 complete with AZ-961 as next. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -8,7 +8,7 @@ status: in_progress
|
|||||||
sub_step:
|
sub_step:
|
||||||
phase: 6
|
phase: 6
|
||||||
name: implement-tasks
|
name: implement-tasks
|
||||||
detail: "batch 6 of N: AZ-960 gps-denied-render-map CSV-truth dispatch (extends --truth loader to dispatch on extension; removes the AZ-959 _maybe_render_map workaround; unblocks UI map link for CSV uploads). Filed alongside AZ-961 (ReportContext rename, sequenced after AZ-960 to avoid re-conflict on _maybe_render_report kwargs). OKVIS2 chain (AZ-943 + AZ-951 + AZ-952) sits in todo/ but is sequenced after the Derkachi e2e flight test passes per user 2026-05-29 directive."
|
detail: "batch 6 complete: AZ-960 gps-denied-render-map CSV-truth dispatch landed (44/44 tests green across render_map + replay_api; CSV-path replay jobs now ship with map_html_url populated). Next batch 7: AZ-961 ReportContext.tlog_path → ground_truth_path rename + label fix. OKVIS2 chain (AZ-943 + AZ-951 + AZ-952) sits in todo/ but is sequenced after the Derkachi e2e flight test passes per user 2026-05-29 directive."
|
||||||
retry_count: 0
|
retry_count: 0
|
||||||
cycle: 4
|
cycle: 4
|
||||||
tracker: jira
|
tracker: jira
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ from dataclasses import dataclass
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from gps_denied_onboard.replay_input import load_tlog_ground_truth
|
from gps_denied_onboard.replay_input import (
|
||||||
|
load_csv_ground_truth,
|
||||||
|
load_tlog_ground_truth,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"RenderInputs",
|
"RenderInputs",
|
||||||
@@ -102,10 +105,20 @@ def load_estimated_track(jsonl_path: Path) -> list[tuple[float, float]]:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def load_ground_truth_track(tlog_path: Path) -> list[tuple[float, float]]:
|
def load_ground_truth_track(truth_path: Path) -> list[tuple[float, float]]:
|
||||||
"""Load a ``(lat, lon)`` track from a binary tlog (AZ-697)."""
|
"""Load a ``(lat, lon)`` track from binary tlog (AZ-697) or AZ-896 CSV.
|
||||||
series = load_tlog_ground_truth(tlog_path)
|
|
||||||
return [(fix.lat_deg, fix.lon_deg) for fix in series.records]
|
Dispatches on file extension: ``.csv`` → :func:`load_csv_ground_truth`,
|
||||||
|
anything else (``.tlog``, ``.bin``, no extension) →
|
||||||
|
:func:`load_tlog_ground_truth`. Both loaders return row-aligned
|
||||||
|
DTOs whose ``records`` expose ``lat_deg`` / ``lon_deg`` attributes,
|
||||||
|
so the rest of the renderer is source-agnostic.
|
||||||
|
"""
|
||||||
|
if truth_path.suffix.lower() == ".csv":
|
||||||
|
csv_series = load_csv_ground_truth(truth_path)
|
||||||
|
return [(fix.lat_deg, fix.lon_deg) for fix in csv_series.records]
|
||||||
|
tlog_series = load_tlog_ground_truth(truth_path)
|
||||||
|
return [(fix.lat_deg, fix.lon_deg) for fix in tlog_series.records]
|
||||||
|
|
||||||
|
|
||||||
def _bounds(
|
def _bounds(
|
||||||
@@ -187,7 +200,7 @@ def render_map_html(
|
|||||||
color=_TRUTH_LINE_COLOR,
|
color=_TRUTH_LINE_COLOR,
|
||||||
weight=3,
|
weight=3,
|
||||||
opacity=0.9,
|
opacity=0.9,
|
||||||
tooltip="Ground truth (tlog)",
|
tooltip="Ground truth",
|
||||||
).add_to(m)
|
).add_to(m)
|
||||||
if inputs.estimated_track:
|
if inputs.estimated_track:
|
||||||
folium.PolyLine(
|
folium.PolyLine(
|
||||||
@@ -285,7 +298,12 @@ def _build_argparser() -> argparse.ArgumentParser:
|
|||||||
"--truth",
|
"--truth",
|
||||||
type=Path,
|
type=Path,
|
||||||
required=True,
|
required=True,
|
||||||
help="Path to the binary tlog the estimator was run against.",
|
help=(
|
||||||
|
"Path to the ground-truth source the estimator was run "
|
||||||
|
"against. Accepts either a binary tlog (default for "
|
||||||
|
"`.tlog`/`.bin`/no-extension) or an AZ-896 schema CSV "
|
||||||
|
"(when the path ends in `.csv`)."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--output",
|
"--output",
|
||||||
|
|||||||
@@ -262,16 +262,6 @@ class SubprocessReplayRunner:
|
|||||||
output_dir: Path,
|
output_dir: Path,
|
||||||
report_path: Path | None,
|
report_path: Path | None,
|
||||||
) -> Path | None:
|
) -> Path | None:
|
||||||
# gps-denied-render-map only understands binary tlog truth
|
|
||||||
# today; CSV-truth dispatch is an AZ-700 follow-up. For now,
|
|
||||||
# CSV-path runs ship without a map (report + emissions still
|
|
||||||
# render, see _maybe_render_report).
|
|
||||||
if inputs.tlog_path is None:
|
|
||||||
_LOGGER.info(
|
|
||||||
"skipping map render — CSV-path runs do not yet support "
|
|
||||||
"the gps-denied-render-map CLI (AZ-700 follow-up)"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
if not shutil.which(self._render_binary):
|
if not shutil.which(self._render_binary):
|
||||||
venv_bin = Path(sys.executable).parent / self._render_binary
|
venv_bin = Path(sys.executable).parent / self._render_binary
|
||||||
if not venv_bin.exists():
|
if not venv_bin.exists():
|
||||||
@@ -283,13 +273,18 @@ class SubprocessReplayRunner:
|
|||||||
render_bin = str(venv_bin)
|
render_bin = str(venv_bin)
|
||||||
else:
|
else:
|
||||||
render_bin = self._render_binary
|
render_bin = self._render_binary
|
||||||
|
# gps-denied-render-map dispatches on the --truth file
|
||||||
|
# extension (AZ-960) so we just pass whichever input path
|
||||||
|
# carried this job's ground truth.
|
||||||
|
truth_path = inputs.csv_path if inputs.csv_path is not None else inputs.tlog_path
|
||||||
|
assert truth_path is not None
|
||||||
map_path = output_dir / "map.html"
|
map_path = output_dir / "map.html"
|
||||||
argv = [
|
argv = [
|
||||||
render_bin,
|
render_bin,
|
||||||
"--estimated",
|
"--estimated",
|
||||||
str(emissions_path),
|
str(emissions_path),
|
||||||
"--truth",
|
"--truth",
|
||||||
str(inputs.tlog_path),
|
str(truth_path),
|
||||||
"--output",
|
"--output",
|
||||||
str(map_path),
|
str(map_path),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -709,6 +709,12 @@ def test_post_replay_csv_path_returns_200_and_dispatches_imu_flag(
|
|||||||
body = response.json()
|
body = response.json()
|
||||||
assert body["state"] == JobState.DONE.value
|
assert body["state"] == JobState.DONE.value
|
||||||
assert body["sync"] is True
|
assert body["sync"] is True
|
||||||
|
# AZ-960 AC-3: CSV-path jobs now expose a map_html_url
|
||||||
|
# (the fake runner writes a map.html fixture that the registry
|
||||||
|
# links into the snapshot).
|
||||||
|
assert body.get("map_html_url", "").endswith("/map"), (
|
||||||
|
f"expected map_html_url to point at /map; got body={body}"
|
||||||
|
)
|
||||||
# Runner saw the csv_path branch (tlog_path is None for csv jobs)
|
# Runner saw the csv_path branch (tlog_path is None for csv jobs)
|
||||||
assert len(fake_runner.calls) == 1
|
assert len(fake_runner.calls) == 1
|
||||||
inputs = fake_runner.calls[0]
|
inputs = fake_runner.calls[0]
|
||||||
|
|||||||
@@ -371,3 +371,92 @@ def test_load_ground_truth_track_returns_lat_lon_pairs(tmp_path: Path) -> None:
|
|||||||
for lat, lon in track:
|
for lat, lon in track:
|
||||||
assert 49.99 < lat < 50.01
|
assert 49.99 < lat < 50.01
|
||||||
assert 29.99 < lon < 30.01
|
assert 29.99 < lon < 30.01
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------
|
||||||
|
# AZ-960 — CSV-path dispatch for --truth
|
||||||
|
|
||||||
|
|
||||||
|
def _write_minimal_csv(path: Path) -> None:
|
||||||
|
"""Write a tiny AZ-896-schema CSV with 3 GPS rows for AZ-960 tests."""
|
||||||
|
header = (
|
||||||
|
"timestamp(ms),Time,"
|
||||||
|
"SCALED_IMU2.xacc,SCALED_IMU2.yacc,SCALED_IMU2.zacc,"
|
||||||
|
"SCALED_IMU2.xgyro,SCALED_IMU2.ygyro,SCALED_IMU2.zgyro,"
|
||||||
|
"GLOBAL_POSITION_INT.lat,GLOBAL_POSITION_INT.lon,"
|
||||||
|
"GLOBAL_POSITION_INT.alt,GLOBAL_POSITION_INT.vx,"
|
||||||
|
"GLOBAL_POSITION_INT.vy,GLOBAL_POSITION_INT.vz,"
|
||||||
|
"GLOBAL_POSITION_INT.hdg"
|
||||||
|
)
|
||||||
|
rows = [
|
||||||
|
"0,0.0,21,-3,-984,52,32,-5,50.000,30.000,141290,0,0,0,35041",
|
||||||
|
"100,0.1,-68,-9,-995,58,-17,1,50.0005,30.0005,141360,0,0,0,35042",
|
||||||
|
"200,0.2,9,108,-988,69,-65,13,50.001,30.001,141410,0,0,0,35048",
|
||||||
|
]
|
||||||
|
path.write_text(header + "\n" + "\n".join(rows) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_ground_truth_track_dispatches_to_csv_loader(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
# Arrange — AC-1: csv extension routes through load_csv_ground_truth
|
||||||
|
csv_path = tmp_path / "data_imu.csv"
|
||||||
|
_write_minimal_csv(csv_path)
|
||||||
|
|
||||||
|
# Act
|
||||||
|
track = load_ground_truth_track(csv_path)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert len(track) == 3
|
||||||
|
for lat, lon in track:
|
||||||
|
assert 49.999 < lat < 50.002
|
||||||
|
assert 29.999 < lon < 30.002
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_ground_truth_track_csv_propagates_schema_error(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
# Arrange — AC-4: malformed CSV fails fast via ReplayInputAdapterError
|
||||||
|
csv_path = tmp_path / "bad.csv"
|
||||||
|
csv_path.write_text(
|
||||||
|
"timestamp(ms),SCALED_IMU2.xacc,GLOBAL_POSITION_INT.lat\n"
|
||||||
|
"0,0,50.0\n"
|
||||||
|
)
|
||||||
|
from gps_denied_onboard.replay_input.errors import ReplayInputAdapterError
|
||||||
|
|
||||||
|
# Act / Assert
|
||||||
|
with pytest.raises(ReplayInputAdapterError, match="Time"):
|
||||||
|
load_ground_truth_track(csv_path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_cli_renders_map_with_csv_truth(tmp_path: Path) -> None:
|
||||||
|
# Arrange — AC-1 end-to-end: CLI accepts --truth foo.csv
|
||||||
|
csv_path = tmp_path / "data_imu.csv"
|
||||||
|
_write_minimal_csv(csv_path)
|
||||||
|
estimated = tmp_path / "estimator.jsonl"
|
||||||
|
_write_jsonl(
|
||||||
|
estimated,
|
||||||
|
[
|
||||||
|
{"position_wgs84": {"lat_deg": 50.0, "lon_deg": 30.0, "alt_m": 100}},
|
||||||
|
{"position_wgs84": {"lat_deg": 50.001, "lon_deg": 30.001, "alt_m": 101}},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
output = tmp_path / "map.html"
|
||||||
|
|
||||||
|
# Act
|
||||||
|
rc = main(
|
||||||
|
[
|
||||||
|
"--estimated", str(estimated),
|
||||||
|
"--truth", str(csv_path),
|
||||||
|
"--output", str(output),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
assert rc == 0
|
||||||
|
assert output.is_file()
|
||||||
|
body = output.read_text()
|
||||||
|
assert body.startswith("<!DOCTYPE html>")
|
||||||
|
assert "L.polyline" in body
|
||||||
|
assert '"color": "red"' in body
|
||||||
|
assert '"color": "blue"' in body
|
||||||
|
|||||||
Reference in New Issue
Block a user