File size: 12,446 Bytes
4d9b003
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
be62f78
 
 
 
 
 
 
 
 
 
 
 
4d9b003
 
 
 
 
 
 
 
 
 
 
 
 
 
be62f78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
223
224
225
226
227
228
229
230
231
# SPDX-License-Identifier: Apache-2.0
"""The encoder's in-graph pre-processing and the decoder's masks, computed on the host from normalised inputs.

Everything the v5.0 encoder graph does before its first matmul is tensor plumbing (slices, ``!= 0`` tests, OneHot,
``Atan`` with quadrant ``Where`` s): the TT port computes it here once per plan and uploads the results (PLAN.md 2.12;
SPEC 3.8, 8.2.6: padded lanes produce ``0/0`` headings that only a ``Where`` discards, which an arithmetic device
``where`` would propagate). The CPU reference consumes the same arrays, so the reference-vs-ONNX Runtime test also
proves this module against the graph (``/encoder/*/Where*``, ``/encoder/Concat_4`` / ``Concat_5`` taps).

Semantics (T4M ``model/module/encoder.py``; checked against the exported graph):

- ego: only the 6 OLDEST history rows are kept (rows 6..30 zeroed); the ego token is always valid and its position
  feature is the last row of the truncated history, i.e. zeros;
- neighbours: rows 0..24 zeroed; a step is valid if any of dims 0..7 is non-zero, an agent if any step is; the type
  one-hot (dims 8..10) and the position feature come from the last step; velocities (dims 4, 5) are zeroed AFTER the
  validity test and a valid-step flag is appended (9 channels); invalid agents are zeroed;
- static objects: valid if any of the 10 values is non-zero (always zeros in Autoware, so never valid);
- lanes / route: dims 0..7 per point (zeroed for invalid lanes), attributes = dims 8..32 of point 0, speed limit and
  its mask, position feature = point 10 with heading ``atan2(dy, dx)`` exported as ``Atan(dy / dx)`` plus quadrant
  ``Where`` s;
- polygons / line strings: ``[x, y, type one-hot..., dx, dy]`` with dx, dy = next point minus point (0 for the last
  point), valid if any of the first FOUR columns is non-zero, position feature = point 20 / 10 with the NON-geometric
  heading ``atan2(col3, col2)``: atan2(dx, is_intersection_area) for polygons, atan2(is_road_border, is_stop_line)
  for line strings (SPEC 3.8: port the quirk exactly);
- goal / ego shape / turn indicators: always valid; positions (goal) and (0, 0, 1, 0); turn indicators drop the
  current report (``[:, :-1]``, 30 values); the turn token reuses the ego-shape class id 8;
- fusion key mask: invalid tokens are masked with -inf, the ego key is forced valid (``encoder.py:833``);
- decoder: a neighbour is a valid attention key if its current state (``neighbor_agents_past[:, 30, :4]``) is not
  all zero; current states = (ego_current_state[:4], neighbour current states) for the prefix constraint.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Dict, Mapping

import numpy as np

from ..reference import config as C

__all__ = ["EncoderFeatures", "DecoderMasks", "encoder_features", "decoder_masks", "atan2_onnx", "line_features"]

PI_F32 = np.float32(3.1415927)  # the ONNX constant of the exported atan2 (``/encoder/lane_encoder/Constant_13``)


def atan2_onnx(y: np.ndarray, x: np.ndarray) -> np.ndarray:
    """``torch.atan2`` as exported to ONNX opset 20: ``a = Atan(y / x)``; ``Where(x < 0, Where(y > 0, a + pi, a - pi),
    a)`` (float32). Differs from IEEE atan2 only on signed zeros (x = -0, y = 0 -> -pi) and 0/0 (NaN, masked later)."""
    y = np.asarray(y, np.float32)
    x = np.asarray(x, np.float32)
    with np.errstate(divide="ignore", invalid="ignore"):
        a = np.arctan((y / x).astype(np.float32)).astype(np.float32)
    alt = np.where(y > 0, a + PI_F32, a - PI_F32).astype(np.float32)
    return np.where(x < 0, alt, a).astype(np.float32)


def _heading_pos(xy: np.ndarray, y_col: np.ndarray, x_col: np.ndarray) -> np.ndarray:
    """``[x, y, cos(h), sin(h)]`` with ``h = atan2_onnx(y_col, x_col)`` (float32)."""
    h = atan2_onnx(y_col, x_col)
    with np.errstate(invalid="ignore"):
        return np.stack([xy[..., 0], xy[..., 1], np.cos(h), np.sin(h)], axis=-1).astype(np.float32)


def _onehot_pos(pos4: np.ndarray, cls: int) -> np.ndarray:
    """Append the 10-way class one-hot (``add_class_type``)."""
    onehot = np.zeros(pos4.shape[:-1] + (C.POS_CLASS_NUM,), np.float32)
    onehot[..., cls] = 1.0
    return np.concatenate([pos4.astype(np.float32), onehot], axis=-1)


def line_features(x: np.ndarray) -> np.ndarray:
    """``LineEncoder``: ``[points..., dx, dy]`` with dx / dy = next point minus point (0 at the last point)."""
    x = np.asarray(x, np.float32)
    d = np.zeros(x.shape[:-1] + (2,), np.float32)
    d[..., :-1, 0] = x[..., 1:, 0] - x[..., :-1, 0]
    d[..., :-1, 1] = x[..., 1:, 1] - x[..., :-1, 1]
    return np.concatenate([x, d], axis=-1)


@dataclass
class EncoderFeatures:
    """Host-side inputs of the encoder network (batch 1, no batch dim). ``valid[cat]`` is True for valid entities;
    ``token_valid`` (564) gates the positional embedding, ``key_valid`` (564) is the fusion key mask (ego forced
    valid); ``pos`` (564 x 14) are the position features, with invalid rows set to 0 (the graph's NaN rows there are
    discarded by a ``Where``)."""

    ego: np.ndarray            # [31, 4]   truncated ego history
    neighbor: np.ndarray       # [320, 31, 9]
    neighbor_type: np.ndarray  # [320, 3]
    static: np.ndarray         # [5, 10]
    lane: np.ndarray           # [140, 20, 8]
    lane_attr: np.ndarray      # [140, 25]
    lane_speed: np.ndarray     # [140, 1]
    lane_has_speed: np.ndarray  # [140, 1] bool
    route: np.ndarray          # [25, 20, 8]
    route_attr: np.ndarray     # [25, 25]
    route_speed: np.ndarray    # [25, 1]
    route_has_speed: np.ndarray  # [25, 1] bool
    polygon: np.ndarray        # [10, 40, 5]
    line_string: np.ndarray    # [60, 20, 6]
    goal: np.ndarray           # [4]
    ego_shape: np.ndarray      # [3]
    turn: np.ndarray           # [30]
    valid: Dict[str, np.ndarray]
    token_valid: np.ndarray    # [564] bool
    key_valid: np.ndarray      # [564] bool
    pos: np.ndarray            # [564, 14]

    def counts(self) -> Dict[str, int]:
        return {k: int(v.sum()) for k, v in self.valid.items()}


@dataclass
class DecoderMasks:
    agent_valid: np.ndarray     # [321] bool: ego + neighbours with a non-zero current state (self-attention keys)
    current_states: np.ndarray  # [321, 4] normalised (prefix constraint)



def _host_fast() -> bool:
    """``DIFFUSION_PLANNER_HOST_FAST`` (OPT round 5 item 3), read once: the vectorised host functions (bit-exact)
    instead of the first port's ``*_ref`` versions."""
    from ..tt.config import KNOBS

    return bool(KNOBS.read().HOST_FAST)


HOST_FAST = _host_fast()


def _any_nonzero(a: np.ndarray, axes) -> np.ndarray:
    return np.any(a != 0, axis=axes)


def encoder_features(norm: Mapping[str, np.ndarray], masks: Mapping[str, np.ndarray]) -> EncoderFeatures:
    """From the normalised inputs (``normalize_inputs``) and the speed masks (``speed_masks``), batch 1."""
    f32 = lambda k: np.asarray(norm[k], np.float32)[0]  # noqa: E731  (drop the batch dim)
    # ego: keep the 6 oldest rows (encoder.py:170-175); the token is always valid, its position row is all zero
    ego = np.zeros((C.INPUT_T + 1, C.POSE_DIM), np.float32)
    ego[C.EGO_HISTORY_KEEP] = f32("ego_agent_past")[C.EGO_HISTORY_KEEP]
    pos = {"ego": _onehot_pos(ego[-1:], C.POS_CLASS["ego"])}

    # neighbours: keep the 6 newest rows (encoder.py:176-181, 441-451)
    nb_raw = f32("neighbor_agents_past")
    if HOST_FAST:
        K = C.NEIGHBOR_HISTORY_KEEP                            # the other rows are zero: computed on the kept ones
        x8k = nb_raw[:, K, :8]
        svk = _any_nonzero(x8k, -1)                            # [320, 6] valid steps
        nb_valid = svk.any(axis=-1)                            # [320]
        nb_type = nb_raw[:, -1, 8:11].copy()                   # row 30 is kept
        feat = np.zeros(nb_raw.shape[:2] + (9,), np.float32)
        fk = feat[:, K]
        fk[..., :8] = x8k
        fk[..., 8] = svk
        fk[..., 4:6] = 0.0                                     # velocities zeroed after the validity test
        fk[~nb_valid] = 0.0
        feat[:, K] = fk
        pos["neighbor"] = _onehot_pos(nb_raw[:, -1, :4], C.POS_CLASS["neighbor"])
    else:
        nb = np.zeros_like(nb_raw)
        nb[:, C.NEIGHBOR_HISTORY_KEEP] = nb_raw[:, C.NEIGHBOR_HISTORY_KEEP]
        nb_type = nb[:, -1, 8:11].copy()
        x8 = nb[..., :8]
        step_valid = _any_nonzero(x8, -1)                      # [320, 31]
        nb_valid = step_valid.any(axis=-1)                     # [320]
        feat = np.concatenate([x8, step_valid[..., None].astype(np.float32)], axis=-1)
        feat[..., 4:6] = 0.0                                   # velocities zeroed after the validity test
        feat[~nb_valid] = 0.0
        pos["neighbor"] = _onehot_pos(x8[:, -1, :4], C.POS_CLASS["neighbor"])

    static = f32("static_objects")
    st_valid = _any_nonzero(static[..., :10], -1)
    static = np.where(st_valid[:, None], static, 0.0).astype(np.float32)
    pos["static"] = _onehot_pos(f32("static_objects")[:, :4], C.POS_CLASS["static"])

    def lanes(key: str, speed_key: str, mask_key: str, cat: str):
        x = f32(key)
        attr = x[:, 0, C.LANE_FEATURE_DIM:].copy()
        x8 = x[..., :C.LANE_FEATURE_DIM]
        valid = _any_nonzero(x8, (-1, -2))
        p = x8[:, C.LANE_POS_INDEX, :4]
        pos[cat] = _onehot_pos(_heading_pos(p, p[:, 3], p[:, 2]), C.POS_CLASS[cat])
        x8 = np.where(valid[:, None, None], x8, 0.0).astype(np.float32)
        speed = f32(speed_key).reshape(-1, 1)
        has = np.asarray(masks[mask_key])[0].reshape(-1, 1).astype(bool)
        return x8, attr, speed, has, valid

    lane, lane_attr, lane_speed, lane_has, lane_valid = lanes("lanes", "lanes_speed_limit",
                                                              "lanes_has_speed_limit", "lane")
    route, route_attr, route_speed, route_has, route_valid = lanes("route_lanes", "route_lanes_speed_limit",
                                                                   "route_lanes_has_speed_limit", "route")

    def lines(key: str, cat: str, pos_index: int):
        x = line_features(f32(key))
        valid = _any_nonzero(x[..., :4], (-1, -2))
        p = x[:, pos_index, :4]
        pos[cat] = _onehot_pos(_heading_pos(p, p[:, 3], p[:, 2]), C.POS_CLASS[cat])
        return np.where(valid[:, None, None], x, 0.0).astype(np.float32), valid

    polygon, poly_valid = lines("polygons", "polygon", C.POLYGON_POS_INDEX)
    line_string, ls_valid = lines("line_strings", "line_string", C.LINE_STRING_POS_INDEX)

    goal = f32("goal_pose").reshape(-1)
    pos["goal"] = _onehot_pos(goal[None], C.POS_CLASS["goal"])
    unit = np.array([[0.0, 0.0, 1.0, 0.0]], np.float32)
    pos["ego_shape"] = _onehot_pos(unit, C.POS_CLASS["ego_shape"])
    pos["turn"] = _onehot_pos(unit, C.POS_CLASS["turn"])
    turn = np.asarray(norm["turn_indicators"], np.float32)[0, :C.TURN_INDICATOR_HISTORY].copy()

    one = np.ones(1, bool)
    valid = {"ego": one, "neighbor": nb_valid, "static": st_valid, "lane": lane_valid, "route": route_valid,
             "polygon": poly_valid, "line_string": ls_valid, "goal": one, "ego_shape": one.copy(), "turn": one.copy()}
    token_valid = np.concatenate([valid[name] for name, _ in C.TOKEN_LAYOUT])
    key_valid = token_valid.copy()
    key_valid[0] = True
    pos_all = np.concatenate([pos[name] for name, _ in C.TOKEN_LAYOUT], axis=0).astype(np.float32)
    pos_all[~token_valid] = 0.0
    return EncoderFeatures(ego, feat.astype(np.float32), nb_type, static, lane, lane_attr, lane_speed, lane_has,
                           route, route_attr, route_speed, route_has, polygon, line_string, goal,
                           f32("ego_shape").reshape(-1).copy(), turn, valid, token_valid, key_valid, pos_all)


def decoder_masks(norm: Mapping[str, np.ndarray]) -> DecoderMasks:
    """Agent key mask of the DiT self-attention (``dit.py:155-156``; the ego is always valid) and the current states
    of the prefix constraint (``multi_step_inference.cpp:300-325``), from the normalised inputs."""
    nb_now = np.asarray(norm["neighbor_agents_past"], np.float32)[0, :, C.INPUT_T, :C.POSE_DIM]
    agent_valid = np.concatenate([np.ones(1, bool), np.any(nb_now != 0, axis=-1)])
    cs = np.zeros((C.MAX_NUM_AGENTS, C.POSE_DIM), np.float32)
    cs[0] = np.asarray(norm["ego_current_state"], np.float32)[0, :C.POSE_DIM]
    cs[1:] = nb_now
    return DecoderMasks(agent_valid, cs)