#!/usr/bin/env python3 # SPDX-License-Identifier: Apache-2.0 """Build the SHIPPED samples of frnet-p150: one synthetic LiDAR sweep per serve profile, generated here (no dataset involved, Apache-2.0), and their stored fp32 CPU-reference outputs (the container smoke test's reference). CPU only, no device; the reference needs ONNX Runtime (research venv). cd bundles/frnet-p150 && OMP_NUM_THREADS=4 PYTHONPATH=code \\ /home/ubuntu/experiments/tt-models/tools/research-venv/bin/python code/scripts/make_sample.py \\ [--weights-dir ] [--check-against ] The scan is a deterministic ray cast from a sensor 2 m above a flat road (seed 0) against 25 cars, 16 building facades, 30 poles / trees and 10 pedestrians (axis-aligned boxes), with 5 % beam dropout, 2 cm range noise and random 0-255 intensities, in the **sensor frame** (x forward, y left, z up). The beam layout follows the profile's own sensor: 128 rows evenly spread over the vertical field of view (OT128 +15 / -25 deg, QT128 +-52.6 deg) and a fixed number of columns over 360 deg, chosen so that the frame lies inside Autoware's profile (the node skips a frame with more than 60,000 unique cells): OT128 512 columns, QT128 600 columns. The generator is a copy of the research reference ``research/frnet/scripts/frnet_preproc_ref.py synthetic_scan`` (``--check-against`` compares the arrays with an earlier output, e.g. the M5 bench inputs ``logs/frnet/m5_baseline/inputs``). Writes, per profile ```` (``ot128``, ``qt128``): - ``code/tt_frnet/samples/synthetic_.npz``: ``points``, a structured (x, y, z, intensity) float32 array; - ``code/tt_frnet/calib/synthetic_.json``: the calibration preset of the same name (sensor -> base_link = a pure 2 m lift: base_link on the road under the sensor), for the ego crop box; - ``code/tt_frnet/samples/synthetic_..reference.json``: the ``/predict`` body of the fp32 CPU reference (``tt_frnet.reference.pipeline`` with ONNX Runtime on the deployed ONNX, the reference of record), reduced to what the container smoke compares (``labels``, ``scores``, ``filter_keep``, npz_compressed) plus the counts. Regenerate the references whenever the reference, the weights or the post-processing changes. """ from __future__ import annotations import argparse import json import sys from pathlib import Path import numpy as np HERE = Path(__file__).resolve() CODE = HERE.parents[1] sys.path.insert(0, str(CODE)) WS = Path("/home/ubuntu/experiments/tt-models") KEEP = ("model", "frame_id", "num_points", "num_points_raw", "class_names", "class_counts") # per profile: the sensor's vertical field of view (deg) and rows (the ml_package frustum height) and the columns SENSORS = {"ot128": dict(fov_up_deg=15.0, fov_down_deg=-25.0, rows=128, cols=512), "qt128": dict(fov_up_deg=52.6, fov_down_deg=-52.6, rows=128, cols=600)} SENSOR_HEIGHT = 2.0 SEED = 0 def synthetic_scan(fov_up_deg: float, fov_down_deg: float, rows: int, n_cols: int, seed: int = 0, sensor_height: float = 2.0, max_range: float = 200.0): """Ray cast against a ground plane and boxes; returns ``(points (n, 4) float32 x, y, z, intensity, labels)`` in a seeded random order (the order a driver would not guarantee either). Same arithmetic and random stream as ``research/frnet/scripts/frnet_preproc_ref.py synthetic_scan`` (cfg = the profile's FOV, ``interp_h`` rows).""" rng = np.random.default_rng(seed) elev = np.deg2rad(np.linspace(fov_down_deg + 0.05, fov_up_deg - 0.05, rows)) azim = np.deg2rad(np.arange(n_cols) * (360.0 / n_cols) - 180.0) E, A = np.meshgrid(elev, azim, indexing="ij") d = np.stack([np.cos(E) * np.cos(A), np.cos(E) * np.sin(A), np.sin(E)], axis=-1).reshape(-1, 3) t_best = np.full(len(d), np.inf) label = np.full(len(d), -1) with np.errstate(divide="ignore", invalid="ignore"): # ground plane z = -sensor_height tg = np.where(d[:, 2] < -1e-6, -sensor_height / d[:, 2], np.inf) upd = tg < t_best t_best[upd] = tg[upd] label[upd] = 0 boxes = [] # (cx, cy, cz_bottom, lx, ly, lz, class) for _ in range(25): # cars cx, cy = rng.uniform(-60, 60), rng.choice([-1, 1]) * rng.uniform(3, 12) boxes.append((cx, cy, -sensor_height, 4.5, 1.9, 1.6, 5)) for side in (-1, 1): # building facades for k in range(8): boxes.append((-80 + 20 * k, side * rng.uniform(18, 25), -sensor_height, 15, 3, rng.uniform(6, 20), 3)) for _ in range(30): # poles / trees boxes.append((rng.uniform(-70, 70), rng.choice([-1, 1]) * rng.uniform(13, 16), -sensor_height, .4, .4, 6, 4)) for _ in range(10): # pedestrians boxes.append((rng.uniform(-30, 30), rng.choice([-1, 1]) * rng.uniform(12, 14), -sensor_height, .6, .6, 1.7, 17)) for (cx, cy, cz, lx, ly, lz, cls) in boxes: lo = np.array([cx - lx / 2, cy - ly / 2, cz]) hi = np.array([cx + lx / 2, cy + ly / 2, cz + lz]) with np.errstate(divide="ignore", invalid="ignore"): inv = 1.0 / d t1 = lo * inv t2 = hi * inv tmin = np.nanmax(np.minimum(t1, t2), axis=1) tmax = np.nanmin(np.maximum(t1, t2), axis=1) hit = (tmax >= np.maximum(tmin, 0)) & (tmin > 0.5) upd = hit & (tmin < t_best) t_best[upd] = tmin[upd] label[upd] = cls ok = np.isfinite(t_best) & (t_best < max_range) ok &= rng.random(len(d)) > 0.05 # beam dropout xyz = d[ok] * (t_best[ok, None] + rng.normal(0, 0.02, size=(ok.sum(), 1))) # range noise inten = np.clip(rng.normal(40, 25, size=ok.sum()), 0, 255).round() # 0..255 intensity pts = np.concatenate([xyz, inten[:, None]], axis=1).astype(np.float32) perm = rng.permutation(len(pts)) return pts[perm], label[ok][perm] def structured(xyzi: np.ndarray) -> np.ndarray: pts = np.zeros(len(xyzi), dtype=[("x", " int: ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) ap.add_argument("--weights-dir", default=str(WS / "assets" / "frnet" / "hf_lidar_frnet")) ap.add_argument("--threads", type=int, default=4) ap.add_argument("--variants", nargs="*", default=list(SENSORS)) ap.add_argument("--check-against", type=Path, help="directory with synthetic_.npz to compare the points with") ap.add_argument("--no-reference", action="store_true", help="write the scans and presets only") a = ap.parse_args() samples, calib = CODE / "tt_frnet" / "samples", CODE / "tt_frnet" / "calib" T = np.eye(4) T[2, 3] = SENSOR_HEIGHT for variant in a.variants: s = SENSORS[variant] xyzi, _ = synthetic_scan(s["fov_up_deg"], s["fov_down_deg"], s["rows"], s["cols"], seed=SEED, sensor_height=SENSOR_HEIGHT) pts = structured(xyzi) stem = f"synthetic_{variant}" if a.check_against is not None: old = np.load(a.check_against / f"{stem}.npz", allow_pickle=False)["points"] if not (old.dtype == pts.dtype and np.array_equal(old, pts)): sys.exit(f"{stem}: the points differ from {a.check_against}") print(f"{stem}: identical to {a.check_against / (stem + '.npz')}", flush=True) np.savez_compressed(samples / f"{stem}.npz", points=pts) preset = {"name": stem, "description": f"synthetic {variant.upper()}-like scan (code/scripts/make_sample.py): the sensor " f"{SENSOR_HEIGHT:g} m above a flat road, base_link on the road under it (a pure lift)", "source": "code/scripts/make_sample.py (generated, no dataset; Apache-2.0)", "T_base_link_from_sensor": T.tolist()} (calib / f"{stem}.json").write_text(json.dumps(preset, indent=1) + "\n") print(f"{stem}: {len(pts)} points ({s['rows']} rows x {s['cols']} columns, seed {SEED})", flush=True) if a.no_reference: continue from tt_frnet.io import encode_array from tt_frnet.reference.pipeline import FRNetReference ref = FRNetReference(a.weights_dir, variant, backend="ort", threads=a.threads) run = ref.run(samples / f"{stem}.npz", calibration={"preset": stem}) if run.prepared.skipped is not None: sys.exit(f"{stem}: outside the {variant} profile: {run.prepared.skipped}") out = run.output body = {k: v for k, v in out.to_dict().items() if k in KEEP} body["labels"] = encode_array(out.label_ids, fmt="npz_compressed", key="labels") body["scores"] = encode_array(out.scores, fmt="npz_compressed", key="scores") body["filter_keep"] = encode_array(out.filter_keep, fmt="npz_compressed", key="filter_keep") inp = run.prepared.inputs body["meta"] = {"variant": variant, "reference": "onnxruntime fp32 (deployed ONNX)", "onnx_sha256": ref.weights.onnx_sha256, "num_unique_coors": inp.num_voxels, "cropped_by_ego_box": int((~inp.keep_mask).sum()), "generator": f"code/scripts/make_sample.py synthetic_scan({s['rows']} rows, {s['cols']} " f"columns, seed {SEED})"} path = samples / f"{stem}.{variant}.reference.json" path.write_text(json.dumps(body) + "\n") print(f"{stem}: N {inp.num_points} (raw {inp.num_points_raw}) M {inp.num_voxels} -> {path.name} " f"({path.stat().st_size / 1e6:.2f} MB)", flush=True) return 0 if __name__ == "__main__": sys.exit(main())