File size: 6,583 Bytes
4d9b003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# SPDX-License-Identifier: Apache-2.0
"""End-to-end output agreement vs the CPU reference on the shipped and the public sample scenes (needs one p150).

    bin/devrun -t 1800 -- python -m pytest -q -s code/tt_diffusion_planner/tests/test_e2e_device.py

No dataset-level metric: the gate is agreement with the fp32 CPU reference of the same Autoware network on the same
inputs (PLAN.md 2.12, SPEC 6.3) on every scene with stored reference outputs: the shipped samples and research scenes
(``research/diffusion-planner/goldens/<scene>.npz``) and the nuScenes-derived public-data instants
(``research/diffusion-planner/goldens/public/<dataset>/<id>.npz``; inputs in ``public_data/inputs``, CC BY-NC-SA, local
only), all written by ``code/scripts/ref_golden.py``. Without the research directory (an installed package) the two
shipped samples are run through the CPU reference on the fly. Plus API == server bit identity.

- ego trajectory (post-processed x, y): max displacement <= 1.0 m and mean displacement <= 0.3 m per scene;
- turn-indicator command identical on every scene;
- neighbours (multi-modal futures, SPEC 10.2): the median over valid neighbours of the per-agent max displacement
  <= 1.5 m.

``GATES`` freeze like the PCC gates (``test_e2e_device.gates.json``); keep ``server/smoke_test.py``
``REFERENCE_GATES`` in line with them. ``DIFFUSION_PLANNER_E2E_REPORT=<path>`` writes the per-scene numbers as JSON.
"""
from __future__ import annotations

import base64
import json
import os
from pathlib import Path

import numpy as np
import pytest

from tt_diffusion_planner.reference import config as C
from tt_diffusion_planner.ttaw.golden import GateRegistry

pytestmark = pytest.mark.device

PKG = Path(__file__).resolve().parents[1]
RESEARCH = PKG.parents[3] / "research" / "diffusion-planner"
GOLDENS = RESEARCH / "goldens"
SAMPLE = PKG / "samples" / "kashiwanoha_dense.npz"
GATES = GateRegistry.for_test(__file__, {
    "ego.max_err_m": (1.0, "max", "max_abs_err"),
    "ego.mean_err_m": (0.3, "max", "ade"),
    "turn.command_agreement": (1.0, "min", "agreement"),
    "neighbors.median_max_err_m": (1.5, "max", "center_err"),
})


def _raw(path: Path, prefix: str = ""):
    with np.load(path, allow_pickle=False) as z:
        return {k: np.array(z[prefix + k]) for k in C.INPUT_NAMES}


def _expected(golden: Path):
    with np.load(golden, allow_pickle=False) as z:
        return z["out.trajectory"], z["out.predicted_agents"], int(z["out.turn_command"])


def _cases():
    """``[(name, raw inputs, (trajectory, predicted_agents, turn_command) or None)]``."""
    cases = []
    for p in sorted((PKG / "samples").glob("*.npz")):
        g = GOLDENS / f"{p.stem}.npz"
        cases.append((p.stem, _raw(p), _expected(g) if g.is_file() else None))
    for p in sorted((RESEARCH / "ort").glob("golden_*.npz")):
        g = GOLDENS / f"{p.stem[len('golden_'):]}.npz"
        if g.is_file():
            cases.append((g.stem, _raw(p, "raw/"), _expected(g)))
    for g in sorted(GOLDENS.glob("public/*/*.npz")):
        src = RESEARCH / "public_data" / "inputs" / g.parent.name / g.name
        if src.is_file():
            cases.append((f"{g.parent.name}/{g.stem}", _raw(src, "raw/"), _expected(g)))
    return cases


@pytest.fixture(scope="module")
def model(device):
    from tt_diffusion_planner import DiffusionPlanner
    from tt_diffusion_planner.reference.weights import find_weights_dir

    wd = find_weights_dir()        # $DIFFUSION_PLANNER_WEIGHTS_DIR > workspace assets > the HF cache (offline)
    if wd is None:
        pytest.skip("v5.0 weights not found (DIFFUSION_PLANNER_WEIGHTS_DIR, workspace assets or the HF cache)")
    with DiffusionPlanner.from_pretrained(device=device, weights_dir=str(wd)) as m:
        yield m


def test_scene_agreement(model):
    ego_max, ego_mean, turn_equal, nb_median = [], [], [], []
    report = {}
    reference = None
    for name, raw, want in _cases():
        if want is None:  # no stored goldens (installed package): the CPU reference of the shipped sample
            if reference is None:
                from tt_diffusion_planner.reference.pipeline import ReferencePlanner

                reference = ReferencePlanner(threads=4)
            out = reference(inputs=raw)
            want = (out.poses, out.predicted_agents, out.turn_indicator["command"])
        dev = model(inputs=raw)
        d = np.hypot(*(dev.poses[:, :2] - want[0][:, :2]).T)
        ego_max.append(float(d.max()))
        ego_mean.append(float(d.mean()))
        turn_equal.append(dev.turn_indicator["command"] == want[2])
        if want[1].shape[0]:
            per_agent = np.hypot(*(dev.predicted_agents[..., :2] - want[1][..., :2]).transpose(2, 0, 1))
            nb_median.append(float(np.median(per_agent.max(axis=1))))
        report[name] = {"ego_max_m": round(ego_max[-1], 4), "ego_mean_m": round(ego_mean[-1], 4),
                        "turn_equal": bool(turn_equal[-1]),
                        "nb_median_max_m": round(nb_median[-1], 4) if want[1].shape[0] else None,
                        "device_ms": round(float(dev.timing_ms.get("device", float("nan"))), 3)}
        print(f"{name}: ego max {ego_max[-1]:.3f} m mean {ego_mean[-1]:.3f} m turn {turn_equal[-1]} "
              f"nb median-max {nb_median[-1] if want[1].shape[0] else float('nan'):.3f} m")
    print(f"{len(ego_max)} scenes")
    out = os.environ.get("DIFFUSION_PLANNER_E2E_REPORT")
    if out:
        Path(out).parent.mkdir(parents=True, exist_ok=True)
        Path(out).write_text(json.dumps({"scenes": report, "info": model.info}, indent=1, default=str) + "\n")
    GATES.require("ego.max_err_m", max(ego_max))
    GATES.require("ego.mean_err_m", max(ego_mean))
    GATES.require("turn.command_agreement", float(np.mean(turn_equal)))
    GATES.require("neighbors.median_max_err_m", max(nb_median))


def test_api_equals_server(model, monkeypatch):
    """The server's ``/predict`` handler and ``model(...)`` give identical outputs (same decoders, same graph)."""
    pytest.importorskip("fastapi")
    from tt_diffusion_planner.server.app import PredictRequest, app

    state = app.state.ttaw
    monkeypatch.setattr(state, "model", model)
    monkeypatch.setattr(state, "ready", True)
    req = PredictRequest(inputs={"format": "npz", "data": base64.b64encode(SAMPLE.read_bytes()).decode()})
    body = state.predict(req)
    want = model(inputs=str(SAMPLE)).to_dict()
    assert body["trajectory"] == want["trajectory"] and body["turn_indicator"] == want["turn_indicator"]
    assert body["predicted_agents"] == want["predicted_agents"]