Conversation
There was a problem hiding this comment.
Copilot wasn't able to review any files in this pull request.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
9af7734 to
6d8c799
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
🔵 Needs a closer look
It makes safety-relevant state-estimation/tracker behaviour changes across many packages while being labelled documentation-only, so it needs human verification of correctness, tuning safety, and the mislabeled scope.
Review details
- Files reviewed: 52/55 changed files
- Comments generated: 1
- Review effort level: Balanced
c863b09 to
a4ebf9c
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The change is large and cross-component (config schema, tracking, clustering, sweep objective, perf harness), the shown diff is only a subset of the actual runtime changes, and the description understates the scope, so it needs human review.
Review details
- Files reviewed: 122/127 changed files
- Comments generated: 1
- Review effort level: Balanced
913f7aa to
cfe54be
Compare
🤖 Version Bump AdvisoryWarnings
Version Updates✅ Radar: 0.5.1-pre33 → 0.5.1-pre34 📖 See CHANGELOG.md for detailed guidelines. This is an automated advisory. Review the detected changes and update versions accordingly. |
4c98066 to
6af3784
Compare
Investigation into the reported defect where a tracked vehicle's bounding box steps roughly one metre laterally for a few frames and returns. The defect was reproduced using production l4perception.DBSCAN and EstimateOBBFromCluster against a synthetic Pandar40P with zero measurement noise, a rigid box vehicle, and a straight path at constant speed: maximum frame-to-frame excursion 1.119 m, mean lateral bias 0.676 m. Root cause is the measurement definition, not the filter. computeClusterMetrics sets WorldCluster.CentroidX/Y to the medoid: the real LiDAR return nearest the arithmetic mean. A medoid can only sit on a visible face, so it carries a viewpoint-dependent bias of up to W/2 and hops between faces as visibility changes. Because that error is a smooth function of viewing geometry and stays correlated over tens of frames, it violates the zero-mean white-noise assumption, and CV, CA, CTRA, UKF and IMM all inherit it unchanged. The first increment must therefore be an observation model, which reorders the sequencing recommended in pipeline-review-open-questions Q5. Measured comparison of candidate measurements over the same 40-frame pass: medoid centroid (today) 0.676 m mean bias / 0.680 m max hop OBB centre 0.279 m / 0.565 m nearest OBB corner 0.202 m / 1.398 m (corner identity flips) corner plus temporal identity 0.337 m / 0.675 m near edge plus dimension prior 0.035 m / 0.370 m Further findings measured against sensor_data.db (55,315 tracks and 3,526,860 observations): - moving tracks are associated on only 43.6 % of sensor frames, so the effective observation rate is about 5 Hz rather than the sensor's 10 Hz - 11.3 % of moving tracks show a lateral excursion above 0.5 m in the already filtered output - lidar_clusters holds 0 rows and InsertCluster has no caller, so raw observations are never persisted - lidar_track_observations stores the Kalman estimate, not the observation - six quality columns in lidar_tracks are never written by InsertTrack - the visualiser drops the observation for every tracked object, and the debug collector that captures innovations is implemented but never wired The plan covers current architecture with source references, the five requested comparison matrices with per-criterion scoring and decision gates, proposed Go types and interfaces, observation uncertainty and residual designs, persistence schema with retention policy, evaluation corpus and partitioning, diagnostic harness requirements, and a nine-phase roadmap with per-phase files, tests, migrations, costs, risks and acceptance criteria. Documentation only. No runtime code changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iment Three changes, in response to review. 1. Split the plan. The single document held two workstreams with a strict one-way dependency: producing a trustworthy trajectory (L4b/L5) and measuring road-user behaviour from it (L7/L8). They have different decision types, different gates and different audiences, and combining them pushed the document past 3,000 lines. lidar-state-estimation-plan.md (renamed) owns Phases 0-5 and 8 lidar-behaviour-analytics-plan.md (new) owns Phases 6 and 7 Phase numbering is shared across both so cross-references survive. The estimation plan keeps the jerk-observability and empirical-path conclusions, because both are grounded in sampling-rate evidence that belongs with the estimator, and gains a Section 13 stating the contract behaviour analytics depends on. 2. Behaviour analytics specification. Incorporates the attached specification with literature grounding. Structure: five-way distinction between observable, surrogate, legal, population-relative and longitudinal statements; benchmark taxonomy; opportunity normalisation; a thirteen-group feature matrix with definitions, units, scope, map dependency, roadside suitability and benchmark kind; equations for THW, TTC, PET and DRAC; uncertainty propagation with derived suppression rules; L8 data model; dataset strategy; phased roadmap 6A/6B/6C/7. Measured constraint that shapes it: a typical vehicle passage is 9.6 s median, 42 observations, 51 m of observed path at 6-10 m/s, over 1,710 confirmed tracks. Four conclusions that diverge from the brief's starting assumptions: - SDLP cannot be measured here. It is the outcome of a ~1 hour standardised on-the-road test (Verster and Roth 2011). A ten-second passage is three orders of magnitude short. The roadside quantity gets a different name and must never be compared to SDLP norms. - The two-second following rule is driver education, not a research threshold. The 100-Car study records headway but defines near-crashes by evasive manoeuvre, not a headway cutoff. THW bands are no_established_threshold. - Point-count-style confidence is wrong for passing clearance: its sigma is dominated by extent, not position, so cyclist width must come from a class prior rather than a per-track estimate. - PET needs no map. A conflict point can be derived from observed path intersections, which moves PET into Phase 6B. Inspection finding: there is no posted speed limit anywhere in the schema. The only one in the codebase is a per-request report parameter. Legal speed benchmarks need it in site_config_periods, which already carries the effective-date pattern. DOIs are given only where directly observed; publisher URLs are used elsewhere rather than constructing plausible-looking DOIs. The SSAM PET default of 5 s is flagged as secondary, since the FHWA techbrief and validation report state the TTC default and not the PET default. 3. Experiment E1 on the soma static captures. Verified the four recordings: 38 m 01 s total, ~22,810 frames, 5,062 MB, all Hesai Pandar40P on port 2369, same sensor, 2025-12-06 across four placements. "static" means the sensor was stationary, not that the scene was empty. They give viewpoint diversity and nothing else, which happens to be exactly the axis the medoid-bias hypothesis depends on, and the axis kirk0 cannot probe. There is no ground truth, so all four tests are designed to need none: the decisive one bins signed lateral offset by aspect angle, where random error has a zero conditional mean and a geometric bias does not. Includes partition assignment across the plan's three partitions, the settling budget constraint (soma2 is 69 s against a 60 s settling duration), and an explicit statement that a negative result fires Section 15's first invalidating condition rather than counting as experimental failure. Documentation only. No runtime code changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tion Architectural corrections from review. Preserves the investigation, matrices, gates, experiments and measured findings; changes the architecture around them. Cross-cutting: added an explicit principles section. No black boxes, and estimate the most probable physical path rather than cosmetically smoothing. Product language moves from "smoothed trajectory" to "final trajectory estimate" and "retrospectively refined estimate"; algorithm names stay technical. Both principles were being violated, which is why they are now stated first. State-estimation plan: - Split Observation into immutable DetectionObservation and derived, versioned MeasurementInterpretation. The previous type mixed sensor evidence with fields requiring a track prediction, so two estimator versions could not consume the same evidence reproducibly. Recorded as defect P12. - Resolved the state dimensionality contradiction. The draft proposed a six-element [x,y,psi,v,a,omega] state AND a linear filter; those are incompatible. Option A adopted: [x,y,vx,vy] with a 4x4 covariance, with orientation, dimensions and vertical position as separate beliefs. This keeps the filter honestly linear and isolates the observation-model change that G-GEO-1 exists to test. Gate G-EST-4 added for migrating to Option B, with low-speed conditioning as the deciding criterion. - Added road-user motion models: rigid_vehicle, two_wheeler, pedestrian, unknown, with prior strength scaled by class posterior so classification uncertainty never becomes motion certainty. - Added an estimation lifecycle distinct from the track lifecycle, with explicit initialisation rules. The position seed keeps the medoid but sizes its covariance to the known bias rather than the medoid's precision. - Removed the medoid fallback during model degradation. It silently redefined X and Y mid-track from estimated physical pose to raw cluster point, which was the most dangerous line in the previous draft. - Made abnormal-motion thresholds class-conditioned. A 90-degree heading change is a spin for a car and an ordinary turn for a pedestrian. - Replaced the dimension sketch with a frame-admissibility taxonomy, a bounded class-anchored quantile estimator, and three defences against the merged-cluster ratchet. Elevation, per the answer that deployment sites are graded: - Added defect P11: ground removal is a flat height band on absolute sensor-frame Z and is documented as not slope-aware. On a grade it clips vehicles at one end of the scene and admits ground at the other, corrupting the cluster extents the near-edge measurement depends on. Remedy pulled into Phase 1. - P11 also threatens Experiment E1: on a straight graded approach, range correlates with both filter error and aspect angle, so a ground artefact could fake or mask the aspect-conditioned bias E1 exists to detect. Added a three-step mitigation: measure grade first, record GroundClipped per observation, stratify E1.1 by range as well as aspect. - Added the road-surface frame, the separation of road-user dynamics from road-surface geometry, discontinuous intersection surfaces, and a capability split. Substance stays in the L7 scene plan; this plan owns the interface and the honest planar fallback. - Added class-general and grade-aware synthetic test coverage. Behaviour plan: - Development is unblocked from G-SMO-1; production emission is not. The gate now covers the genuinely dangerous case, computing metrics from today's biased tracks, while allowing work against fixture trajectory streams. - Added a metric framework ahead of the feature matrix: three-tier road-user scope, declared per-metric applicability, a closed SuppressionReason vocabulary, an observation-support taxonomy separating occluded_inferred from missed_unknown, and interaction classification as a posterior that gates which formulae may run. - Split results into BehaviourMeasurement and BehaviourOutcome so categorical outcomes keep references to the evidence they were derived from. - Generalised passing clearance to minimum synchronised surface-to-surface separation, with lateral clearance as a projection for confident overtakes. - Replaced scalar sigma with an Uncertainty struct supporting intervals, bounds and a propagation method, and carried three uncertainty scopes so pairwise common-mode cancellation is a later refinement rather than a migration. - Marked jerk experimental with a published-boundary acceptance test. - Separated engineering dependency order from product priority, with vehicle-cyclist passing clearance leading the product list. The two orderings are close to inverted at the top. Both documents end with a "Changes introduced by this revision" section and answer the questions the revision raised. Documentation only. No runtime code changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Decisions taken, recorded in state-estimation Section 21.1: - D1: P11 (slope-unaware ground removal) is a current defect, not future work. Deployment sites are graded. Severity still to be confirmed by measuring the grade per capture. - D2: ship the OBB centre as an immediate measurement stopgap. 0.279 m mean lateral bias against the medoid's 0.676 m, a 2.4x improvement for a change of measurement source. Fenced with four conditions: it is still a visible-surface artefact, validate via E1.3 first, persist which source produced each estimate, and re-baseline G-GEO-1's regression numbers afterwards. Without the source field the stopgap silently splits the historical record into two incomparable regimes, which is the same class of error as the medoid fallback removed from Section 12. - D3: gate set confirmed as G-PER-1, G-GEO-1, G-UNC-1, G-EST-1, G-SMO-1. - D4: product priority leads with vulnerable-road-user interactions. Experiment E1 corpus corrected against the real capture set: - soma2 dropped: at 69 s against a 60 s settling duration it cannot yield a meaningful partition. - soma1 and soma3 re-split, now carrying a -0-1 segment suffix. - clar0-1 added, and it matters more than the rest because it is a different site rather than a fourth placement at the same one. It becomes the held-out regression partition for that reason. - Row values measured on the superseded splits are marked for re-measurement. /Volumes/lidar was not readable from this session, so only kirk0 was re-verified on disk. Added the two known-defect VRLOGs (0fb02f22 from clar0-1, 60a4774c from kirk1) as the labelling source for the regression set, with an explicit note that a VRLOG replays decisions already made and so cannot run a candidate measurement. They supply labelled cases; E1 runs from the source captures. The two were produced by different builds under different tuning hashes and are not comparable to each other. Simplifications: - Estimator matrix cut from six rows to three. CTRV, CTRA and a stationary mode all depend on evidence that does not exist, and carrying them implied a choice that is not open. - G-EST-2/3/4 folded into a deferred-conditions table. Writing thresholds for them now would be guessing. - Behaviour Section 8 gains a scope table: eight groups in, five deferred. Note this resolves toward the confirmed VRU-first product priority rather than the literal groups 1-3 plus 9 originally proposed, because that set would have deferred PET and yielding, which are product priorities 2 and 3. - Shared production-database figures now cite state-estimation Section 1.5 as canonical instead of being restated in both plans. Backlog: nine entries added, five under v0.5.5 tracker correctness and four under v1.0, including the missing posted speed limit in site config. Documentation only. No runtime code changes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…105d8) Run f84105d8-b3be-416f-8809-551ef6bfce10 supplies real-data confirmation of the mechanism that Section 3 previously demonstrated only synthetically, through a second symptom the synthetic model predicts but that had not been checked. 6,846 frames, 11 m 03 s, 2,038 tracks, replayed from soma1-static-0.pcap at playback rate 0.1. The slow playback is a control: the frame-rate throttle was not engaged, so nothing below is a throughput artefact. Estimated width collapses on moving tracks. Of 288 tracks with max speed at or above 3 m/s, 172 (60 %) carry an estimated width below 0.5 m, narrower than a pedestrian, and 210 (73 %) are below 1.0 m, narrower than any car. The mode is 0.1 to 0.3 m, which is the thickness of a single observed face rather than a plausible object width. That is the same cause as the position bias. With only the near face visible, the medoid sits at W/2 from the true centre, and the extent across the object collapses to the face's own thickness. One mechanism, two symptoms, and the second is now measured on real traffic. Corroborating, same run: 1,648 of 2,038 tracks (81 %) carry no class; the 15 classified car have a mean width of 0.85 m against a real 1.8 m; mean observations per moving track is 6. The track in the inspector screenshots, trk_4a27c73c, is classified car at 8.0 m/s with L x W x H of 0.8 x 0.2 x 0.2 m and Hits 0 while Confirmed. Consequences recorded: - P7 restated. It is not that a running mean is biased low by partial views; the per-frame extents are themselves the wrong quantity under single-face visibility. Severity raised to High, since it starves the classifier and breaks every downstream clearance measurement. Section 9.2's admissibility rules are load-bearing rather than fastidious. - The 81 % unclassified rate is arithmetic, not an independent defect. The classifier reads dimensions and speed; sub-metre widths starve it. - Mean 6 observations per moving track is well below the 42-observation median in Section 1.5. The tuning hash differs from production, so it is recorded as run-specific rather than as a new baseline. f84105d8 promoted to primary labelling source in 16.5, ahead of 0fb02f22 and 60a4774c. Two properties noted so they are not rediscovered: playback 0.1 as above, and its source being the superseded soma1-static-0.pcap split rather than the current soma1-static-0-1.pcap, so a re-run will not reproduce it frame for frame. Cases are to be labelled by what they show, not by frame index. Documentation only. No runtime code changes. All queries were read-only against a WAL database and no live server was touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…haviour analytics
…1.1) Nothing in the pipeline measured whether a bounding box points where its vehicle is going. AlignmentMeanRad compares Kalman velocity against displacement: both describe motion, neither describes the box. HeadingJitterDeg measures how much the box moves, so a box locked at the wrong angle scores perfectly on it. Add course alignment: |OBB heading - direction of travel|, folded to [0, 90]. An OBB is symmetric, so a box pointing backwards along the course is correctly oriented and folds to 0; a 90 degree length/width swap is the worst case. Sampled only on live frames at or above 2 m/s, where course is meaningful. The tracker holds it as a 20-bin histogram so per-track cost is constant on the Pi; the offline analysis path computes percentiles directly. Measured on run baf20f02 (600 frames, 346 tracks), across the 55 tracks with samples: median per-track course error 50.3 deg, p85 67.2 deg, worst 85.0 deg. Track 18952226, one half of the split car in the report, sits at 66.4 deg while its box moves 1.95 deg per frame. Jitter called that track healthy. Two defects surfaced while wiring it up, fixed here: - The offline and live HeadingJitterDeg measured different quantities. The analysis path used Track.HeadingRad, the Kalman course; the tracker used OBB heading deltas. These are the two paths an A/B comparison puts side by side. The box quantity is now reported separately as OBBHeadingJitterDeg, and both fields state which is which. - Run-level aggregates key on final track state, and 211 of 346 tracks end DELETED, so the confirmed-only rollups covered 13 tracks. Course alignment rolls up over every track that produced samples instead. The existing jitter and alignment aggregates still have this flaw and are understated. Also adds the two-day sprint plan this is the first task of, with the measured evidence for the heading lock ratchet and the objective function that rewards it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Guard 3 in tracking_update.go rejects any OBB heading delta between 60 and 120 degrees, measured against the smoothed heading. Once that smoothed value has itself drifted more than 60 degrees from the truth, every correct measurement is rejected and the lock cannot release. Nothing recorded whether that had happened to a track. Add per-track HeadingLockedFrames, LongestLockRun, EnteredSustainedLock and ReleasedAfterLock, with a derived LockTrapped for the case that matters: a sustained lock that never released. A release needs five consecutive unlocked frames, because Guard 3 rejects per frame and a single frame slipping through between rejections is not the lock letting go. Both the live tracker and the offline analysis path compute this, and a test asserts the two agree, since they are the two sides of every A/B comparison. Measured on run baf20f02, over the 239 tracks living at least five live frames: 83 tracks (35 per cent) enter a sustained lock and 54 of those (65 per cent) never release. The longest single run is 152 frames, 15.2 seconds. Splitting tracks by that outcome, trapped tracks sit at a median 60.1 degrees of course error against 38.5 for the rest, so the lock costs about 22 degrees. That is the measured case for the D1.3 ratchet fix. Fixes a third defect found while wiring it up. The offline reconstruction counted DELETED ghost frames, which carry heading source pca rather than the lock the track died in, so fifty frames of ghost turned a track trapped for its whole life into a clean one: trapped read 11 per cent of locked tracks instead of 65. Lock stats now skip non-live frames. Excluding them dropped the pca frame count from 15,069 to 4,459, a difference of 10,610, matching the 10,610 DELETED track-frames counted independently. Run-level ratios are also taken only over tracks that lived long enough for a sustained lock to be detectable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (D1.3) Guard 3 rejects any OBB heading delta between 60 and 120 degrees, measured against the smoothed heading. That comparison is what makes it self-sustaining: once the smoothed value has itself drifted more than 60 degrees from the truth, every correct measurement lands inside the rejection band and is thrown away, so the lock can never release. On run baf20f02, 65 per cent of locked tracks never recovered, sitting a median 60 degrees from their direction of travel against 38 for the rest. Add a rejection counter. After OBBHeadingLockMaxRejections consecutive Guard 3 rejections the tracker stops believing its own smoothed heading and accepts the measurement. Default 5; zero restores the previous behaviour. Two details the fix depends on: - The release snaps rather than eases. The EMA moves 8 per cent of the gap per update, so easing across a delta wide enough to be rejected would re-trigger the guard on the next frame and never converge. - Only Guard 3 drives the counter. Guards 1 and 2 fire when the cluster is genuinely unusable, too few points or too near square, and snapping to such a measurement would replace a wrong answer with a random one. Releasing also restores dimension updates, which are only written when the heading update is accepted, so a released track stops carrying the frozen length and width it locked with. Proven on synthetic sequences reproducing the trap: a confirmed track whose smoothed heading sits 90 degrees from its course, fed correct measurements, stays above 80 degrees of course error with the release disabled and converges below 5 with it armed. The two pre-existing Guard 3 tests still pass. The end-to-end figure on baf20f02 is not measured here. A VRLOG replays decisions already made and cannot re-run the estimator, so confirming the fix needs a pipeline re-run over the source PCAP. Config is strict on missing keys by design, so the new parameter is added to the three shipped tuning files, to config-migrate (writing the shipped default rather than the zero value, which would quietly migrate a config back onto the ratchet), and to the runtime tuning endpoint's accepted keys. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
….5, D1.6) D1.5, fragment guard. The association gate is a 6 m radius on position alone, with nothing to stop a scrap of a cluster capturing a vehicle-sized track. On run baf20f02 a 0.11 by 0.08 metre cluster was associated to a track carrying a 4.33 metre car and became its dimensions for the next 28 frames. Forbid the pairing in the cost matrix rather than refusing it at update time, so the fragment stays unassociated and can seed its own track instead of being consumed. The guard fires only when a track has at least three observations, believes it is at least two metres, and the cluster's longest extent is below min_associable_extent_metres (default 0.5). It reads the running average extent, not the latest frame, because the latest frame is exactly the value a fragment would already have corrupted. The guard is deliberately narrow. Pedestrian and cyclist tracks legitimately carry sub-metre extents whose clusters vary by a similar amount frame to frame, so a small-cluster rule applied there would reject ordinary observations. Dimension consistency in the assignment cost proper is a separate change. D1.6, ghost fade. Deleted tracks are published with a fade-out alpha, which is a deliberate rendering feature. The defect was that its duration was deleted_track_grace_period: one number doing two unrelated jobs. That period exists so re-association can still find a track seconds later, and borrowing it as a render timer held a frozen box on screen for five seconds while the real object drove out from under it. It also corrupted every metric computed over the recorded stream, as D1.2 found. Split out deleted_track_render_fade, default 500ms. Re-association is untouched: the track stays in the map for the full grace period, it simply stops being drawn. Measured exactly on run baf20f02, since the fade is a pure filter on publication and needs no pipeline re-run: published DELETED track-frames fall from 10,610 (45.8 per cent of all track-frames) to 1,170 (5.0 per cent), an 89 per cent reduction in ghost frames. Both parameters follow the D1.3 pattern through the strict config schema: the three shipped tuning files, config-migrate defaults, and the runtime tuning endpoint's accepted keys. GetRecentlyDeletedTracks now windows on the render fade, so its tests are renamed to say so rather than asserting the old semantics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… 1 gate A VRLOG stores the decisions the pipeline already made, so replaying one shows what the old code concluded, not what the new code would conclude. Measuring a change to L4, L5 or L6 means re-running perception over the packets, and the only route to that was the live server's replay endpoint, which binds ports and shares state with whatever else the server is doing. D1.3 and D1.5 were therefore proven on synthetic sequences but unmeasured on real data. Add internal/lidar/replayeval, exposed as `velocity lidar pcap-replay`. It replays a PCAP through the full L1-L6 pipeline and records a VRLOG with no server, no database and no listening port, following the pattern settlingeval already established for L3. Because the output is an ordinary VRLOG, nothing downstream needed changing: GenerateReport for metrics, CompareReports for A/B, and the macOS visualiser for looking at it. --compare-to runs all three. Sixty seconds of capture replays in about 12 seconds. The frame-rate throttle is off, because dropping frames would make two runs disagree for reasons unrelated to the change under test. Point clouds are omitted unless --include-points is passed: 45 s of the SoMa capture is 347 MB with them and 9.1 MB without. Track persistence is disabled explicitly rather than by leaving the DB nil, so a future pipeline change that starts assuming a database fails loudly here instead of writing into the production store. Day 1 gate, both arms replaying the same PCAP through the same binary and differing only in the tuning file. The before arm restores the pre-sprint behaviour: no lock release, no fragment guard, render fade back on the 5 s grace period. median per-track course error 40.8 -> 28.3 to 29.6 deg (-28%) locked share of live frames 24.8% -> 16.1 to 16.6% (-34%) longest single lock run 70 -> 20 frames (-71%) published DELETED track-frames 8,617 (54.0%) -> 951 (11.4%) (-89%) total published track-frames 15,970 -> 8,377 (-48%) Four qualifications recorded in the plan rather than buried. The trapped count barely moves because LockTrapped needs five consecutive unlocked frames to clear, and once locks start breaking they also re-form, so the flag stays set; the longest-run collapse is the direct evidence the ratchet is gone, and the metric needs splitting before it is used as a gate. Twenty frames is still two seconds of wrong heading, which is the case for D2.1. Fragmentation rises slightly, as intended, because a scrap now seeds its own short track instead of corrupting a vehicle's. And frame assembly is not bit-deterministic, so a regression fixture will need a tolerance rather than an equality. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pressure The harness was silently dropping frames. FrameBuilder discards a frame when the callback channel is full, which is correct for a live sensor and wrong for offline analysis: the PCAP reader outruns clustering and tracking, so frames were lost at whatever rate the machine happened to impose. Two runs of the same capture disagreed on frame count (588 to 592), median course error (28.3 to 30.2 degrees) and trapped tracks (30 to 35), which is fatal for an A/B because the change under test cannot be separated from the scheduler. pcapsplit and the server's own analysis mode both call SetBlockOnFrameChannel for exactly this reason. The FrameBuilder's own comment on the blocking path says the buffer would otherwise deliver "only ~12% of the rotations". The harness now does the same. With back-pressure the harness is reproducible to the digit: three consecutive runs agree on frame count, track count, every percentile and the trapped count. Frame count rises from about 588 to 601, so roughly 13 frames per run were being discarded. The A/B was re-run and the conclusions survive unchanged, because both arms were losing frames at the same rate: median per-track course error 40.8 -> 29.7 deg (-27%) locked share of live frames 24.5% -> 16.0% (-35%) longest single lock run 70 -> 20 frames (-71%) published DELETED track-frames 8,639 (53.9%) -> 942 (11.3%) (-89%) Also confirms the fragmentation mechanism rather than asserting it. The ratio rises from 0.301 to 0.313, and the census shows why: four more tracks under half a second, and six fewer tracks whose average bounding box is under half a metre. Six vehicle tracks that were previously shrunk to fragment size no longer are. The ratio counts tracks, not correctness. An earlier attempt pinned the frame-assembly timers on the theory that wall-clock frame cutting was the cause. That was wrong and is not kept. TestRunFrameCountIsStableAcrossRuns was passing on luck with a short window. Replaced with TestRunIsDeterministic, which compares frame count, track count and fragmentation across two runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Carried over from another worktree, where it had been left uncommitted rather than on a branch. One new proposal plus the two cross-references that make it reachable: an entry in the MATHS.md proposal roadmap and a research-extension note at the top of classification-maths.md. The proposal argues for one semantic vocabulary and evidence contract across L3 to L7, with specialised estimators staying where they are, and separates class, behaviour, and measurement quality into independent attributes so that a stationary car does not become street furniture. It is explicitly research, not active in the runtime, and says plainly that no model has been evaluated on our captures yet. Checked before taking it: no broken relative links, and the six citations resolve to real repositories and papers rather than constructed identifiers. The header-metadata hook stripped a Date key, which the house format does not allow, and folded the research-extension note into the metadata list. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…itivity findings Batch 1 (L3 background-settling broad sweep, 24 sites, 408 runs) finished on its own: neighbour_confirmation_count and noise_relative are genuinely sensitive (21/24 and 6/24 sites regress at their tested extremes, one site never converges), closeness_multiplier and safety_margin_metres are robust across the full corpus. Deterministic rule and full numbers in sensitivity-analysis.json / analyze_sensitivity.py. results.csv/summary.json are still being appended to by the in-progress replicate pass (Batch 2) and follow in the next commit once that settles. Adds a small supervisor (data/experiments/try/campaign/) that reuses this harness rather than building a new one: waits for the operator-launched Batch 1 process (never starts/restarts it), then runs the narrowed sweep, replicate pass, and interaction grid this analysis justifies, attempts the L5 ground-truth-scored sweep, and stops after a configurable wall-clock budget. manifest.json is the editable future-work list; status.json is the compact check-in rollup. Also lands cmd/tools/lidar-ground-truth-eval, a tested standalone CLI over the already-implemented adapters.EvaluateGroundTruth (previously reachable only through the live HINT HTTP flow), needed to score the L5 sweep without the circular alignment metric the earlier preliminary pass used. Running it against real replayed candidates is blocked on a confirmed architecture conflict: the live server couples the PCAP-replay BPF filter port to the live-listen bind port with no override, and the operator's dev server already holds the port the reference capture needs. Diagnosed by hand (cmd/tools/lidar-ground-truth-eval/main.go, run_l5_gt_sweep.py, and Batch 3 in the campaign plan have the exact code references); the sweep script self-blocks with a written reason rather than guessing past it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t pass save_manifest/write_status and the four analysis/planning scripts' JSON writers were missing a trailing newline, which the pre-commit end-of-file-fixer hook flags every time -- caught chasing it across several commit attempts. Also records the supervisor's first real pass against this manifest: wait_l3_batch1 confirmed genuinely done (408/408 rows), the three Go tools built successfully. status.json is the compact rollup a check-in reads first. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…pdate status counts
…eep to multiple sites Pass 1 of the unattended campaign finished cleanly (11/11 stages done, ~1.53h) but review at check-in found two defects a clean exit code had hidden: plan_interaction_levels.py cast neighbour_confirmation_count's worst value to float, so settling-eval's strict Go unmarshal rejected it and 48/96 interaction-grid rows silently errored while the stage still recorded "done". Fix the cast (plus a defensive int-cast in run_interaction_grid.py) and add a supervisor guard that fails the stage if any combo has zero non-error rows instead of trusting the exit code alone. Also found the L5 GT sweep's numbers were a windowing artifact: it replayed only 60s of kirk1.pcapng's 177.8s capture, so most of the 49 labelled reference tracks fell outside the window regardless of tuning. Add out_dir plumbing to the L5 stage handler and queue a full-duration rerun plus two more labelled reference sites (MIN_POSITIVE_LABELS=8 floor) so L5 evidence isn't drawn from a single recording, with a cross-site ranking check (analyze_l5_results.py). Extends manifest.json with 7 new stages; no terminal stage from pass 1 is modified. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…3 keys, repeat check Second-pass review found the L3 interaction verdict overstated (two sites scored "additive" only because the replay window capped them; the grid used noise_relative=0.05 though the never-analyzed 0.065 rows flag 13/24 sites), settling-eval non-deterministic at slow sites (reruns disagreed with Batch 1 at 3/48 comparisons), and the L5 noise grid blind by construction: EvaluateGroundTruth matches on temporal IoU only, so process/measurement noise is invisible to it. Adds a ground-truth one-at-a-time sweep over keys that change track existence (L4 eps/min points, L5 hits_to_confirm/max_misses/gating), with every POST verified against the live config and a discarded warm-up plus start/middle/end baselines, because the first replay after server start is a cold outlier. Extends L3 to 8 unswept keys settling-eval actually consumes (it forces warmup and settling_period, so those are excluded). Adds a repeat check, censoring-aware interaction analysis, inert-key detection, and param_types.coerce at every config write to close the int/float bug class. Queues 16 stages as a strict priority chain, budget 7h. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…weeps The L3 results.csv (Batch 1 + narrowed + replicate), interaction-results.csv (including the 48 rows voided by the neighbour_confirmation_count=1.0 type bug, kept as history), and the four L5 ground-truth sweep CSVs were never committed because data/**/*.csv is gitignored. Force-added so the evidence the campaign's conclusions rest on is reproducible from git. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… concurrency guard pgrep -f matched any process merely mentioning run_sweep.py, so the shell that launched the supervisor made l3_interaction_grid_v3 report "another L3 driver is active" on its first pass. The same would happen with an operator's `less run_sweep.py`, silently stalling the campaign. Require a Python interpreter followed by the script as a whole path component. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Updated the `updated_at` timestamp to reflect the latest data. - Increased `total_rows` from 823 to 1139. - Added new metrics for various parameters including `background_update_fraction`, `seed_from_first`, `post_settle_update_fraction`, `reacquisition_boost_multiplier`, `min_confidence_floor`, `locked_baseline_threshold`, `locked_baseline_multiplier`, and `freeze_threshold_multiplier`. - Provided detailed statistics for each new parameter, including number of runs, convergence status, and settling frame metrics.
- Updated timestamp to reflect the latest data collection. - Increased total rows from 1149 to 1358. - Adjusted metrics for background_update_fraction, seed_from_first, post_settle_update_fraction, and freeze_threshold_multiplier based on new runs. - Notable changes include: - background_update_fraction: n_runs increased to 235, n_not_converged to 4, mean_settling_frame adjusted to 164.7. - seed_from_first: n_runs increased to 47, n_not_converged remains at 47. - post_settle_update_fraction: n_runs increased to 141, mean_settling_frame adjusted to 61.1. - freeze_threshold_multiplier: n_runs increased to 22, mean_settling_frame adjusted to 11.5.
… rows, queue pass 4 l3_extended_sweep_b lost 344 of 456 rows and was recorded done: a commit ran the mixed-line-ending hook, which replaced results.csv while run_sweep.py held it open, so later rows went to the unlinked file. All raw reports survived. - row_appender.RowAppender reopens the CSV for every row; all five sweep drivers use it - run_l3_sweep fails a stage when a planned value has fewer rows than sites - recover_rows_from_raw.py rebuilds rows from raw reports, verifying capture, config value and timestamp window; supervisor stage type recover_raw_rows - plan_interaction_levels --policy mildest, and drops keys no site converges at (seed_from_first=false made the top-2 grid score 0 of 24 sites) - analyze_interaction_grid splits lost convergence into joint_only/single_key - manifest: 9 stages, budget_hours 2.5 (remainder of the 7h) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… sweep of the five unseen L3 keys The supervisor saved the manifest it read before a stage started, discarding any edit to a pending stage made while a long stage ran. It now reloads first. The recovered 24-site data shows frames identical across every tested value of reacquisition_boost_multiplier, min_confidence_floor, locked_baseline_*, freeze_threshold_multiplier, so settling-eval cannot see them; queue a ground-truth one-at-a-time sweep of their extremes on kirk0 and kirk1 instead, ahead of the low-value ordinal-1 replicate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Updated timestamp to reflect the latest data collection. - Increased total rows from 1358 to 2139. - Adjusted metrics for various parameters including: - reacquisition_boost_multiplier: n_runs from 24 to 188, mean_settling_frame from 11.5 to 61.2, max_settling_frame from 12 to 722. - min_confidence_floor: n_runs from 24 to 188, mean_settling_frame from 11.5 to 60.4, max_settling_frame from 12 to 717. - locked_baseline_threshold: n_runs from 24 to 188, mean_settling_frame from 11.5 to 60.1, max_settling_frame from 12 to 723. - locked_baseline_multiplier: n_runs from 18 to 141, mean_settling_frame from 11.5 to 60.1, max_settling_frame from 12 to 712. - freeze_threshold_multiplier: n_runs from 22 to 188, mean_settling_frame from 11.5 to 61.0, max_settling_frame from 12 to 723.
…-window L3 stages, queue pass 5 Every raw settling report holds the four convergence criteria at every frame; only the first converged frame was ever read. analyze_post_settle.py measures whether a setting keeps the grid settled (self-check: the recorded frame equals the first all-criteria frame for 2078/2078 rows) and drops exact-equality "inert" tests, which are invalid because no two runs have identical per-frame metrics. run_gt_oat_sweep.py --label-free records track statistics for captures with no labelled reference (reproduces the scored candidate counts exactly), with analyze_label_free_oat.py comparing directions against the labelled captures under the GT analyzer's reliability and threshold rules. run_l3_sweep takes out_dir/ordinal and guards _baseline row counts; the supervisor refuses to start a stage with under min_free_gb free. Manifest: 37 stages, budget 8.6 h. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…, findings, and source inventory
- Enhance Makefile with new targets for scene asset management: - `scene-assets`: Rebuild outstanding scenes and update pages. - `scene-assets-status`: Report on published and outstanding scenes. - `scene-assets-clean`: Remove `.rebuilt` markers to trigger rebuilds. - `scene-corpus-verify`: Validate corpus against its manifest. - Introduce `publish-scenes.py` for generating web scene assets from the trimmed corpus, allowing for reproducible scene exports. - Implement `verify-corpus.py` to check the integrity of the corpus, ensuring all captures are present and match expected sizes and digests. - Add unit tests for `publish-scenes.py` and `verify-corpus.py` to ensure correct functionality and error handling. - Update documentation to reflect new features and usage instructions for scene asset management.
…ts and revised stage statuses
- Changed the path of the frozen artefact to reflect its actual location on the capture volume. - Added detailed information about the frozen artefact, including size, SHA-256 checksum, and schema versions. - Expanded the section on header fields to clarify extraction parameters and provenance. - Provided a comprehensive breakdown of candidate records, including their structure and review statuses. - Included a Python command for selecting the frozen set from the candidates.
…t stage counts in manifest and status files
…a-normalised bg re-score Reviewed against the 2026-09-19 gap-analysis revision after two stages: the default config's median track lifetime is 0.37 s on Columbus-Broadway, and closeness_multiplier=1.5 recovers 13 of 16 labelled tracks on kirk0 against a baseline of 7. Cut the low-information L3 confirmation stages (300 s guardrails at o3, second hold-out, six label-free segments), added the acceptance-window dose-response against labels and B7's closeness 7.5/9.75 prediction, and recorded track statistics and mean matched IoU in scored mode so recall can be told from fragmentation. analyze_alpha_normalised.py re-scores the background_update_fraction sweep with the spread-delta criterion scaled by alpha: 0 of 24 sites flagged at every value in both windows (was 6/17/24), so that verdict was the criterion. Pass 6 (launch_pass6.py waits for the pass-5 supervisor to exit, merges pass6-stages.json, relaunches): run_nis_sweep.py drives lidar-state-estimation-baseline -tuning per config and site, offline and repeat-verified, reading per-band NIS from tracking_baseline.json and removing the recordings; analyze_nis_sweep.py measures distance from the consistency targets per band and never minimises. first-segment-corpus.json declares one capture per case, because the tool digests every declared capture before it replays (641 s for a 20 s window at Columbus otherwise). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit fa89e3a)
The baseline tool nests the case id under -out, so the frames and index live two levels down; the cleanup looked one level up and removed nothing (the smoke test recorded recordings_removed=0). Verified on the smoke output: four items removed per config, every JSON kept, 12 MB to 92 KB. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit 8100df7)
The 20 s smoke test at Columbus produced 43 and 41 accepted observations in the 2 and 5 m/s bands, so 120 s of scoring risked leaving the moving bands under the analyzer's floor. 70 s warm-up plus 200 s scoring fits the 300 s first segment; at 300 observations the standard error of a chi-squared(2) mean is 0.12, under the 0.10 log-ratio floor at NIS 2. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit ded4a54)
- Updated timestamp to reflect the latest data collection. - Increased total rows from 2139 to 2539. - Adjusted parameters for baseline and various metrics: - Baseline runs increased from 47 to 69 with a mean settling frame change from 58.4 to 53.0. - Closeness multiplier runs increased from 188 to 236; mean settling frame updated to 66.5. - Noise relative runs increased from 212 to 322; mean settling frame updated to 69.4. - Neighbour confirmation count runs increased from 188 to 276; mean settling frame adjusted to 101.1. - Background update fraction runs increased from 235 to 345; mean settling frame updated to 157.4. - Seed from first runs increased from 47 to 69, with all not converged cases updated accordingly. (cherry picked from commit eed1b9a)
…riptions, add new findings from recent experiments, and clarify association cost impacts on trajectory quality. (cherry picked from commit cc35de2)
…eclassify gaps, and clarify mathematical discrepancies. (cherry picked from commit fc05db5)
…easurements and corrections (cherry picked from commit a8179f9)
… rule; pin the gap-analysis tests; E1.2/E1.4 analysis Three TrackerConfig/DBSCANParams options, all default false so each is measured against the shipped behaviour before it ships: - CascadedAssociation (S2/S3): confirmed tracks are matched first and tentative tracks compete only for what they leave, after DeepSORT's cascade. The joint assignment let a one-frame-old tentative track outbid a confirmed track coasting through one miss (test pins both behaviours). - ScaleMinPtsWhenSubsampled (D6): when the input cap subsamples a frame, MinPts scales with the kept fraction so the busiest frames keep the density threshold the quiet ones run at; plumbed as pipeline DensityPreservingCap. - OBBHeadingFlipRule (P3): AB3DMOT's orientation correction before Guard 3; without it a stationary track whose PCA sign alternates walks its heading 26.6 degrees in 40 frames (test pins that too). Tests for D1, D2, D3, H1, H2, K4 (the extent cost's first evaluation on unlike-class neighbours: swap at weight 0, kept apart at 1), V1 (quadratic coast error), B1 (spread converges to 0.798 sigma) and B2 (settled spread-delta floor is 0.483 alpha sigma). lidar-e1-analysis gains E1.4 (stationary noise floor per candidate and range band against the configured sigma_R) and E1.2 (Ljung-Box whiteness of the lateral residual on straight segments, for the four candidates and the filter posterior). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit aa9617fb6507f3ab7a41bb41373cd1a9385facd2)
…loor, red lateral residuals Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> (cherry picked from commit bd6a03c4e78322dda43476f184dc05e0ff35463a)
a8179f9 to
5ab136d
Compare
Purpose
LiDAR tracking produces jittery, split, and duplicate-identity boxes on real captures. This PR
is the measurement and reference-data foundation the
state estimation plan calls for, plus the fixes
that could be validated without it. It is large — two agents worked this branch in parallel —
and this description reflects the state as merged, not the sequence it was built in.
Changes
Shipped, on by default
and snap to measurement after repeated rejections, instead of holding forever.
longer overwrite a track's dimensions or seed a duplicate track on the same vehicle; a
deleted track's frozen box now fades in ~500 ms instead of 5 s.
time.Now(); it now seeds from the point content, so replaying the same capture reproducesthe same clusters. This is load-bearing for every measurement below — nothing is comparable
run-to-run without it.
|OBB heading - direction of travel|, folded to [0, 90], plus per-track lock-episode outcomes (none/never_recovered/relocked/released), replacing a lifetime-only flag that couldn'tdistinguish a genuine trap from a track that already recovered.
Experimental, off by default
obb_axis_coherence_enabled): chooses between thealigned and quarter-turn-swapped interpretation of an observed rectangle against a
corroborated-maximum extent belief — the largest span several confidently-assigned
observations have reached, not a running mean (a mean shrinks the object: most views are
partial). Measured on two captures: median course error improves (42.3->37.3 deg, 36.3->10.7
deg) but heading acceptance drops ~23 points on both, so it stays off pending D2.2.
association_extent_cost_weight): converts thehard fragment guard into a finite penalty so a genuine partial view isn't forced into seeding
a duplicate track. Reduces duplicate-identity candidates (overlapping boxes) on both test
captures, 9% and 44%, but course error moves in opposite directions between them at sample
sizes too small to trust — stays at weight 0 pending a physical-object reference indepenent
of predicted track IDs.
Reference-data foundation
internal/lidar/annotation/: immutable, content-addressed point packs cut from a VRLOG(digest-verified, atomic write, refuses to overwrite), plus a revisable sidecar of
human-reviewed object/mask/pose records. Exists because a tracker's own track IDs cannot be
the reference for judging that tracker — they split and merge between runs.
velocity lidar annotation-exportcuts a pack; round-trip, tamper-detection, and proposal/reviewedseparation are under test.
internal/lidar/l4bobserve/: early observation-model types (Phase 1 of the stateestimation plan) — the first step toward persisting what the sensor actually saw, separately
from the filter's estimate of it.
Phase 0 measurement instrumentation
(normalised innovation squared), computed where the Kalman gain already has the innovation
and its inverse covariance. Published as
tracking_baseline.jsonbeside every replay.Measured on two site windows: the fixed scalar measurement noise is underconfident at low
speed (NIS ~0.2-0.8) and overconfident above 5 m/s (NIS up to 20, small samples) — evidence
for Phase 3's observation-conditioned uncertainty, not just an assumption.
and never enabled; now reachable.
Performance matrix
pi/mac/ci): the baselinenamespace previously had one slot per capture+profile, so a Pi and a Mac collided on the
same file and the Pi could not be gated without destroying the Mac baseline. Comparisons
across host classes are now refused rather than silently reported as a regression, with
per-class thresholds (20%/30%/50%) and repeat counts reflecting each class's actual noise
floor.
procedure for capturing the one matrix cell that answers "fast enough for the sensor" —
nothing runs it yet, tracked as a backlog item.
Reference client and operator view
tools/visualiser-macos/.../Annotation/): the selection UIthe annotation packs were always missing. Orthographic lasso and rectangle selection over a
depth slab against canonical pack indices (never GPU buffer positions), add/subtract,
stroke-level undo/redo, a second view for the contamination check, operator provenance, and
dirty-navigation protection. The sidecar writer follows the Go store's revision protocol, so a
save that cannot prove it edits the snapshot it loaded is refused rather than overwriting
another writer. 94 Swift tests, with the pack reader tested against bytes the Go writer
actually produced and pinned on both sides. Selection is through-slab, not hidden-surface
picking; a radius brush and reviewed propagation remain future work.
/app/lidar/tracksnow renders throughthe same three.js player as the published scenes, driven by a session built over live database
observations, so seeking, playback and trail reconstruction are one implementation rather than
two. Track picking and missed-region marking were ported into the 3D view and the 932-line flat
canvas map deleted, along with the dead background-grid fetch, per-track observation cache and
overlay-offset controls it had kept alive.
Paper-implementation gap remediation
Three gaps from the gap analysis were
implemented and measured. In each case the measurement contradicted what the gap row
predicted, which is recorded there alongside the numbers.
predicted failure mode reproduced. K1's coupled process noise behaves as the maths says but is
2.7% of the normalised distance to a 2 m/s² manoeuvre against a gate 173x wider, so association
here is governed by gate width and not the form of Q. K2's premise did not reproduce at all:
the shipped naive covariance form lost exactly zero symmetry over 10,000 cycles with
adversarial noise. They are Go-level options rather than tuning keys on purpose — a new tuning
key moves the config fingerprint and would invalidate the committed perf baselines before
anyone had measured whether the change helps.
Guard 2 — which exists to refuse an ambiguous heading from a near-square cluster — guarded its
division with
maxDim > 0and so skipped the check entirely for a zero-extent box, acceptingthe most ambiguous measurement while correctly refusing a 5 cm square. P2's wrong-answer
fallback branch is removed rather than corrected, by computing the discriminant as the
non-cancelling
(c00-c11)² + 4c01².lidar-closeness-auditagainst 367 deployedbackground snapshots and 24,007,669 settled cells: the sensor beats its own specification in
situ (median spread 6 mm close in, 47 mm past 100 m), while the modelled noise term supplies
86-98% of the acceptance window against the measured spread's 1.5-5%. The median effective
window reaches 1.11 m at 10-20 m and 8.99 m past 100 m. A follow-up ring-elevation pass
disconfirmed the obvious explanation for the tail: the rings reporting the most foreground
carry the fewest wide-spread cells, inverting the feedback-loop prediction. The detection
cost is not measured and is filed as a backlog item.
Phase 2 measurement model
l4perceptionas the seed of the evaluation corpus,so Section 3's evidence is reproducible in the repository rather than living in a scratchpad.
It records one correction to the plan: §3.1's stated ring band cannot see its own vehicle for
most of the pass, so the generator fires the real hardware elevation table and reproduces the
mechanism — the medoid pinned at exactly -0.900 m,
W/2, on 20 of 40 frames — rather than§3.2's per-frame figures.
property the plan identifies as missing: one visible face constrains its own normal and leaves
the perpendicular direction to the prediction untouched, with a covariance tight along the
constraint and effectively unbounded across it. Lateral error against the synthetic pass is
0.0000 m mean and max, against the OBB centre's 0.1391 m and 0.8038 m. Its bias is exactly the
dimension-prior error one-for-one, which is what makes the solid body's per-dimension sigma and
provenance load-bearing rather than decorative.
own covariance block, a bimodal orientation belief carrying the 180-degree ambiguity explicitly
rather than resolving it with a guard, per-dimension beliefs with sigma and admissible-frame
counts, motion class as a posterior, and the estimation lifecycle.
ProjectSurfacereturnsunavailable — for suppression, never substitution — whenever pose, heading and dimensions
cannot be bounded together. Seeding states the medoid's known bias, ~0.95 m for a vehicle,
where the measurement noise would have claimed ~0.22 m about a position that may be a metre
out. Not yet wired into the live pipeline.
Experiment E1: the question this branch existed to answer
Run against all three corpus sites: 43,068 scored frames matching the committed Phase 0/1
baseline exactly, 195,389 immutable observations, 160,011 linked estimates, every site
repeat-verified byte-identical.
E1.1, the decisive conditional-mean-by-aspect test, confirms Section 3's hypothesis. The
medoid's lateral offset is near zero end-on and rises monotonically toward broadside, to 0.35-0.41
of the body's half-width, at all three placements; the near-edge candidate stays flat and of
opposite sign. E1.3 agrees independently at 0.58-0.65 of a half-width, needing no fit and no truth.
The trend survives range stratification in every well-populated cell, which is the
discriminator that separates a geometric bias from the P11 grade confound — and it is as strong at
the flattest placement (0.14% grade) as the steepest (4.35%).
So the defect is in the measurement definition and no estimator can filter it out, which is what
Phase 2 was predicated on. A sign bug in the first E1.3 implementation initially inverted its
conclusion by letting vehicles passing on opposite sides cancel; E1.1's independent reference
exposed it, and that is recorded rather than quietly fixed. Full record, including three limits
stated rather than implied — no ground truth in this corpus, the OBB centre's circularity, and an
empty ground-clipped stratum:
E1 lateral-error record.
Known gaps (tracked, not blocking on their own)
Resolved. Themac/ciperf baselines are stalemacbaselines are recaptured andmake test-perf-allpasses both gated profiles. Diagnosed before recapturing, since recapturinga refused baseline on sight risks enshrining a regression: the committed file recorded a workload
the branch does not produce (1,611,256 foreground points against an actual 985,224), and the
branch's actual workload matches main's baseline to within one cluster, so nothing had
regressed and the stale file had captured a transient intermediate state. No
pibaselineexists yet; that is the runbook item below.
track IDs. The selection UI that blocked this now exists (see the annotation client above), so
what remains is operator hours rather than engineering: reviewed masks across a site's
keyframes, then the frozen object-disjoint dataset splits.
floor, the one place real data supplies an exact expected value) are not yet run; G-GEO-1 needs
both plus held-out geometry, excursion and fragmentation criteria.
lidar-benchbinary for the Pi (native build only); no scheduled Pi run.config-validate/config-migratetest fixtures after schema additions) were found and fixed in this branch's own history —
flagging the pattern: a strict schema addition needs every inline test fixture updated in
the same commit, and
make test-goalone did not always catch it (stale test cache).Checklist
DESIGN.md.recaptured; E1 recorded)
README.mdis up to date.docs/DEVLOG.mdis up to date.🤖 Generated with Claude Code