File size: 12,196 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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# SPDX-License-Identifier: Apache-2.0
"""Host pre-processing (no device, no weights): the node's normalization rule and the encoder's in-graph
pre-processing as host features, against the verified research reference (``dp_common.py``) and against the rules
of ``preprocessing_utils.cpp:34-84`` / SPEC 3.8 on hand-made cases. (The features are proven against the exported
graph itself, tensor by tensor, in ``test_reference_cpu.py``.)

    TT_VISIBLE_DEVICES=none python -m pytest -q code/tt_diffusion_planner/tests/test_host_preprocess.py
"""
from __future__ import annotations

import json
import math

import numpy as np
import pytest

from tt_diffusion_planner.host.features import atan2_onnx, decoder_masks, encoder_features, line_features
from tt_diffusion_planner.host.normalize import FLT_EPSILON, normalize_array, normalize_inputs, speed_masks
from tt_diffusion_planner.host.pipeline import prepare
from tt_diffusion_planner.reference import config as C
from tt_diffusion_planner.reference.weights import load_param_json
from tt_diffusion_planner.tests import _research as R

PARAM_JSON = None
for _cand in (R.RESEARCH.parents[1] / "assets" / "diffusion-planner" / "hf_diffusion_planner" / C.PARAM_JSON,):
    if _cand.is_file():
        PARAM_JSON = _cand


@pytest.fixture(scope="module")
def normalization():
    if PARAM_JSON is None:
        pytest.skip("diffusion_planner.param.json not found")
    return load_param_json(PARAM_JSON)


def _zero_raw():
    raw = {k: np.zeros(s, np.float32) for k, s in C.INPUT_SHAPES.items()}
    raw["ego_current_state"][0, :4] = (0, 0, 1, 0)
    raw["ego_agent_past"][0, :, 2] = 1.0
    raw["turn_indicators"][:] = 1.0
    raw["ego_shape"][0] = (2.79, 4.89, 1.896)
    return raw


# ------------------------------------------------------------------------------------------- normalization

def test_normalize_rule_zero_rows_and_columns():
    mean = np.array([10.0, 0.0, 0.0, 0.0], np.float32)
    std = np.array([20.0, 20.0, 1.0, 1.0], np.float32)
    v = np.array([[0, 0, 0, 0],                                   # padding: untouched
                  [1e-8, -1e-8, 0, 1e-9],                          # every |v| < FLT_EPSILON: untouched (kept as is)
                  [30.0, 2.0, 1.0, 0.0],                           # a real row
                  [0, 0, 1.0, 0]], np.float32)                     # cos = 1 makes the row real: x -> -0.5
    out = normalize_array(v, mean, std)
    np.testing.assert_array_equal(out[0], v[0])
    np.testing.assert_array_equal(out[1], v[1])
    np.testing.assert_array_equal(out[2], ((v[2] - mean) / std).astype(np.float32))
    np.testing.assert_array_equal(out[3], np.array([-0.5, 0, 1, 0], np.float32))
    one = normalize_array(np.array([[0.0], [10.0], [-FLT_EPSILON]], np.float32), np.array([0.0]), np.array([20.0]))
    np.testing.assert_array_equal(one.ravel(), np.array([0.0, 0.5, -FLT_EPSILON / 20], np.float32))
    with pytest.raises(ValueError, match="Standard deviation is zero"):
        normalize_array(v, mean, np.array([20, 20, 0, 1], np.float32))
    with pytest.raises(KeyError, match="Missing key lanes"):
        normalize_inputs({"lanes": np.zeros(C.INPUT_SHAPES["lanes"], np.float32)}, {})


def test_normalize_skips_raw_keys(normalization):
    raw = _zero_raw()
    raw["sampled_trajectories"][0, 0, 5] = (1, 2, 3, 4)
    norm = normalize_inputs(raw, normalization.observation)
    for k in C.SKIP_NORMALIZATION:
        np.testing.assert_array_equal(norm[k], raw[k])
    assert norm["ego_agent_past"][0, 0].tolist() == [-0.5, 0.0, 1.0, 0.0]


@pytest.mark.skipif(R.load_script("dp_common") is None, reason="research scripts not present")
@pytest.mark.parametrize("scene", R.research_scenes() or ["none"])
def test_normalization_and_masks_equal_research_reference(normalization, scene):
    dp = R.load_script("dp_common")
    raw = R.research_scene_raw(scene)
    if raw is None:
        pytest.skip("no research scene")
    _, obs, _ = dp.load_param_json(str(PARAM_JSON))
    want = dp.normalize_inputs(raw, obs)
    got = normalize_inputs(raw, normalization.observation)
    for k in want:
        np.testing.assert_array_equal(got[k], want[k], err_msg=k)
    for k, v in dp.speed_masks(want).items():
        np.testing.assert_array_equal(speed_masks(got)[k], v, err_msg=k)
    with np.load(R.ORT_GOLDENS / f"golden_{scene}.npz") as z:  # what the ORT goldens were fed
        for k in C.ENCODER_INPUTS:
            src = got[k] if k in got else speed_masks(got)[k]
            np.testing.assert_array_equal(src, z["in/" + k], err_msg=k)


# ------------------------------------------------------------------------------------------------- features

def test_atan2_onnx_quadrants_and_quirks():
    rng = np.random.default_rng(0)
    y = rng.normal(size=1000).astype(np.float32)
    x = rng.normal(size=1000).astype(np.float32)
    np.testing.assert_allclose(atan2_onnx(y, x), np.arctan2(y, x), rtol=0, atol=2e-6)
    # x = 0: dy / 0 = +-inf -> +-pi/2 like atan2
    np.testing.assert_allclose(atan2_onnx(np.float32([2.0, -3.0]), np.float32([0.0, 0.0])),
                               [math.pi / 2, -math.pi / 2], atol=1e-7)
    # the exported decomposition maps y = 0, x < 0 to -pi (IEEE atan2 gives +pi); 0 / 0 is NaN (masked later)
    assert atan2_onnx(np.float32(0.0), np.float32(-1.0)) == np.float32(-3.1415927)
    assert np.isnan(atan2_onnx(np.float32(0.0), np.float32(0.0)))


def test_line_features_append_point_deltas():
    x = np.zeros((2, 4, 3), np.float32)
    x[0, :, 0] = [0, 1, 3, 6]
    x[0, :, 1] = [0, -1, -1, 2]
    x[0, :, 2] = 1.0
    f = line_features(x)
    assert f.shape == (2, 4, 5)
    np.testing.assert_array_equal(f[0, :, 3], [1, 2, 3, 0])
    np.testing.assert_array_equal(f[0, :, 4], [-1, 0, 3, 0])
    np.testing.assert_array_equal(f[1], 0)


def test_encoder_features_rules(normalization):
    raw = _zero_raw()
    raw["ego_agent_past"][0, :, 0] = np.arange(31) - 30.0                    # x: -30 .. 0
    nb = raw["neighbor_agents_past"][0]
    nb[0, :, :4] = (12.0, 3.0, 1.0, 0.0)                                       # agent 0: valid everywhere
    nb[0, :, 4:8] = (5.0, 1.0, 1.8, 4.5)
    nb[0, :, 8] = 1.0
    nb[1, 10, 0] = 50.0                                                        # agent 1: only an old (dropped) row
    nb[2, 30, 4] = 2.0                                                         # agent 2: only a velocity at t = 30
    nb[3, 27, :4] = (20.0, 1.0, 0.0, 1.0)                                      # agent 3: a pose at t = 27 only
    raw["lanes"][0, 0, :, 0] = np.linspace(0, 19, 20)                          # lane 0 along +x
    raw["lanes"][0, 0, :19, 2] = 1.0
    raw["lanes"][0, 0, 0, 12] = 1.0                                            # no traffic light (dim 12)
    raw["lanes_speed_limit"][0, 0, 0] = 8.0
    raw["polygons"][0, 0, :, 0] = np.linspace(0, 39, 40)
    raw["polygons"][0, 0, :, 2] = 1.0
    raw["line_strings"][0, 1, :, 1] = np.linspace(5, 24, 20)
    raw["line_strings"][0, 1, :, 3] = 1.0                                      # road border
    norm = normalize_inputs(raw, normalization.observation)
    norm.update(speed_masks(norm))
    f = encoder_features(norm, norm)
    # ego: the 6 OLDEST rows kept, the rest zeroed; the token is valid with an all-zero position
    np.testing.assert_array_equal(f.ego[:6], norm["ego_agent_past"][0, :6])
    assert not f.ego[6:].any() and f.valid["ego"].all() and not f.pos[0, :4].any() and f.pos[0, 4] == 1
    # neighbours: rows 0..24 dropped; agent 1 (old row only) invalid; agent 2's t = 30 row is non-zero (a velocity),
    # so the normalisation moved its x to (0 - 10) / 20 = -0.5; the velocity is zeroed after the validity test and
    # the valid-step flag set
    assert f.valid["neighbor"][:4].tolist() == [True, False, True, True]
    assert not f.neighbor[:, :25].any()
    np.testing.assert_array_equal(f.neighbor[0, 25:, 4:6], 0)
    np.testing.assert_array_equal(f.neighbor[0, 25:, 8], 1)
    np.testing.assert_array_equal(f.neighbor[2, 30], [-0.5, 0, 0, 0, 0, 0, 0, 0, 1])
    np.testing.assert_array_equal(f.neighbor[3, :, 8], (np.arange(31) == 27).astype(np.float32))
    np.testing.assert_array_equal(f.neighbor_type[0], [1, 0, 0])
    sl = C.TOKEN_SLICES
    np.testing.assert_allclose(f.pos[sl["neighbor"].start, :4], [0.1, 0.15, 1.0, 0.0], atol=1e-7)
    # lanes: heading of point 10 from (dx, dy); speed and its TRT mask; attributes from point 0
    assert f.valid["lane"][0] and not f.valid["lane"][1:].any()
    np.testing.assert_allclose(f.pos[sl["lane"].start, :4], [norm["lanes"][0, 0, 10, 0], 0, 1, 0], atol=1e-7)
    assert f.lane_has_speed[0, 0] and not f.lane_has_speed[1:].any() and f.lane_speed[0, 0] == np.float32(0.4)
    assert f.lane_attr[0, 12 - C.LANE_FEATURE_DIM] == 1
    # polygons: the pseudo-heading atan2(dx, is_intersection_area) of point 20 (dx = 1/20 normalised)
    h = math.atan2(1.0 / 20.0, 1.0)
    np.testing.assert_allclose(f.pos[sl["polygon"].start, 2:4], [math.cos(h), math.sin(h)], atol=1e-6)
    # line strings: atan2(is_road_border, is_stop_line) = pi/2 for a road border, whatever its geometry
    np.testing.assert_allclose(f.pos[sl["line_string"].start + 1, 2:4], [0, 1], atol=1e-6)
    assert not f.valid["line_string"][0] and f.valid["line_string"][1]
    # static objects are zeros in Autoware: never valid; goal / ego shape / turn always valid
    assert not f.valid["static"].any() and f.valid["goal"].all() and f.valid["turn"].all()
    np.testing.assert_array_equal(f.pos[sl["turn"].start, :4 + 9], [0, 0, 1, 0] + [0] * 8 + [1])
    np.testing.assert_array_equal(f.turn, raw["turn_indicators"][0, :30])
    # masks: invalid tokens have no position feature; the ego key is always valid
    assert not f.pos[~f.token_valid].any()
    assert f.key_valid[0] and (f.key_valid[1:] == f.token_valid[1:]).all()
    assert f.token_valid.sum() == 1 + 3 + 1 + 1 + 1 + 3
    # the decoder keys on the CURRENT (normalised) pose only (dit.py:155-156): agent 3, whose only pose is at t = 27,
    # is an encoder entity but not a decoder key; agent 2's normalised x = -0.5 makes it a key
    d = decoder_masks(norm)
    assert d.agent_valid[:5].tolist() == [True, True, False, True, False]
    np.testing.assert_array_equal(d.current_states[0], [-0.5, 0, 1, 0])
    np.testing.assert_array_equal(d.current_states[1], norm["neighbor_agents_past"][0, 0, 30, :4])


def test_prepare_on_the_shipped_samples(normalization):
    for stem, n_agents in (("kashiwanoha_dense", 88), ("straight_road", 12)):
        raw = R.sample_raw(stem)
        p = prepare(raw, normalization.observation)
        assert p.neighbor_rows.tolist() == list(range(n_agents))
        assert p.enable_force_stop and p.prev_report == 1
        assert p.features.valid["neighbor"].sum() == n_agents == p.decoder.agent_valid.sum() - 1
        np.testing.assert_array_equal(p.x_T, raw["sampled_trajectories"][0])
        assert set(p.norm) >= set(C.INPUT_NAMES) | {"lanes_has_speed_limit", "route_lanes_has_speed_limit"}


def test_prepare_refuses_bad_inputs(normalization):
    from tt_diffusion_planner.ttaw.io import InputError

    raw = _zero_raw()
    del raw["delay"]
    with pytest.raises(InputError, match="missing"):
        prepare(raw, normalization.observation)
    raw = _zero_raw()
    raw["lanes"] = np.zeros((1, 70, 20, 33), np.float32)
    with pytest.raises(InputError, match="shape"):
        prepare(raw, normalization.observation)


def test_param_json_contract(normalization):
    assert normalization.major_version == 5
    mean, std = normalization.state()
    assert mean.shape == (C.MAX_NUM_AGENTS, 1, 4)
    np.testing.assert_array_equal(np.unique(mean.reshape(-1, 4), axis=0), [[10, 0, 0, 0]])
    np.testing.assert_array_equal(np.unique(std.reshape(-1, 4), axis=0), [[20, 20, 1, 1]])
    assert set(normalization.observation) == set(C.INPUT_NAMES) - set(C.SKIP_NORMALIZATION)
    args = normalization.args
    assert (args["agent_num"], args["time_len"], args["future_len"], args["hidden_dim"]) == (320, 31, 80, 256)
    assert (args["encoder_mixer_depth"], args["encoder_fusion_depth"], args["decoder_depth"]) == (6, 6, 3)
    assert json.loads(json.dumps(args))["diffusion_model_type"] == "x_start"