AI3D-341 — Step 6 dashed/solid labels as source of truth, propagated into the XML
Summary
Step 6 already writes a per-cluster
segment_type scalar ("solid" /
"dashed") into each run6_cluster_*.npz. Step 7 today classifies
dashed vs solid from cluster geometry (bounding-box heuristic
is_middle_lane_bounding_box in filtering-clusters) unless the opt-in flag
clustering.prefer_artifact_segment_type is set — and the XML never carries an
explicit per-segment style. This change makes the Step 6 labels the default,
authoritative classification (geometry only as fallback for missing/invalid labels)
and propagates the label onto every exported line segment in the Step 7 XML as an explicit
<Type> element on <Line> and <Spline>.
Two repos are touched: modellinglines (this repo) and
modellingexport (XML writer, backward-compatible extension). Implementation runs
as a multi-agent workflow (Grok does most of the work), committed often under
AI3D-341, delivered as draft PRs.
Current state (verified in code)
- Classification:
ClusterInfo.is_middle_laneis set by a bbox-geometry heuristic at cluster load (filtering-clusterscluster_metadata.py:200).LaneSegment._from_cluster_impl(lane_segment.py:161) branches on it: middle-lane →DASHEDline fit, else →SOLIDspline fit. - Existing label plumbing:
cluster_stepper.pyalready has_read_artifact_segment_types/_apply_artifact_segment_types(lines 52–118, 306–390) which read the NPZsegment_typeand overrideis_middle_lane— but only whenclustering.prefer_artifact_segment_typeis true (default false; CLI flag exists on both entry points). Misaligned NPZ-count vs cluster-count skips the whole override; per-file missing/invalid labels fall back to geometry individually. - XML: Feature-level
Typeis re-derived from segment types (lane.py:330: any dashed →"Center Lines", else"Axis of the Edge"). Per segment, dashed exports as<Line>and solid as<Spline>— style is only implicit in the element kind. The writerwrite_xml.create_xmllives in the separateiolabs_point_cloud_modelling_exportpackage (v0.7.1 pinned; repo exists locally at~/dev/3dai.iolabs.pointcloud.modellingexport). - Persistence: lane-state snapshots already round-trip
segment_type(lane_state.py:109/123).lane_linking.py:248re-derives a type from the axis class (DashedLineAxis→ DASHED) — consistent with creation, but an audit item.
Key decisions
| # | Decision | Rationale |
|---|---|---|
| 1 | Labels-first by default: flip
clustering.prefer_artifact_segment_type default to true
(config + both CLI entry points). Keep the flag as an escape hatch to restore
geometry-only behavior. |
All the plumbing, logging, and stats events already exist and are tested behind the flag; flipping the default is the lowest-risk way to make labels authoritative. |
| 2 | Geometry stays as fallback only: per-cluster fallback when the label is missing/invalid, whole-run fallback when NPZ count ≠ cluster count (existing behavior, now on by default). No hard failure. | Old Step 6 artifacts without segment_type must keep working; A1/A2 reruns
shouldn't crash on stale caches. |
| 3 | Per-segment label in XML: each exported segment dict gets
"type": "dashed" | "solid" (from LaneSegment.segment_type, which
now originates from Step 6). write_xml.create_xml emits it as a
<Type> child on <Line> and <Spline>
— only when the key is present (backward compatible). Export package bumped
0.7.1 → 0.8.0; this repo's pin updated. |
Explicit label survives even if a consumer doesn't know the Line-vs-Spline convention; additive schema change can't break existing consumers that ignore unknown elements. |
| 4 | Feature-level Type unchanged ("Center Lines" /
"Axis of the Edge" derivation stays as is). |
It already consumes segment types, which now come from Step 6 labels — it inherits the fix for free; consumers depend on those strings. |
| 5 | Audit, don't rewrite, re-derivation sites: verify every place a
SegmentType is assigned or inferred (lane_linking.py:248,
refine/dedupe at cluster_stepper.py:3966, road_state.py) keeps
the label rather than re-deriving from geometry. |
The label must survive chaining, merging, splitting, and snapshot round-trips to reach the XML intact. |
| 6 | Execution model: multi-agent Workflow — Grok 4.5 (cursor) for audit +
implementation, Sonnet for mechanical work, GPT 5.6 Sol (codex) + Opus for adversarial
review; Fable orchestrates. Frequent commits tagged AI3D-341; draft PRs, one
per repo. |
Requested by Miro; Cursor tokens effectively free. |
Phases
Phase 0 — Audit sweep small
Parallel Grok readers map every SegmentType assignment / re-derivation site,
both export paths (Lane.info_dict and the stepper's _export at
cluster_stepper.py:3973), snapshot round-trip, and existing tests around
prefer_artifact_segment_type. Output: a checklist of touch points for phases
1–3, plus anything that silently re-derives type from geometry after cluster load.
Phase 1 — Labels-first default (modellinglines) small
- Flip
prefer_artifact_segment_typedefault totruein_config.py/ config YAML and both CLI entry points (make the CLI flag a--no-…-capable pair or keep override semantics). - Ensure the misalignment / missing-label fallback logging stays accurate with the new default (message currently says "override skipped … falling back", which becomes the noteworthy case).
- Tests: default-on behavior, per-file fallback, whole-run misalignment fallback, escape hatch off-switch. (No config-value-assertion tests — behavior tests only.)
Phase 2 — Label propagation to export dicts (modellinglines) small–medium
- Add
"type": segment.segment_type.name.lower()to every spline dict and line dict inLane.info_dict(lane.py:348/366) and to the unassigned-lanes export path incluster_stepper._export. - Fix any audit findings from Phase 0 where the label would be lost or re-derived geometrically between cluster load and export.
- Tests:
export_dictcarries per-segment type; mixed solid/dashed lane keeps per-segment labels; snapshot-restored lanes still export labels.
Phase 3 — XML writer extension (modellingexport) cross-repo
- In
write_xml.create_xml: when a line/spline dict has"type", emit<Type>dashed|solid</Type>as a child of<Line>/<Spline>; omit otherwise. - Version bump 0.7.1 → 0.8.0, publish to Nexus (wrap-up flow), separate branch + draft
PR under AI3D-341 in
3dai.iolabs.pointcloud.modellingexport. - Update
modellinglinesdependency pin to the new export version. - Tests in the export repo: element present when key given, absent otherwise (round-trip parse).
Phase 4 — Verification local
- Full unit-test suites in both repos.
- Local Step 7b run from existing snapshots (per the established local iteration
workflow) on an abschnitt with Step 6 label-bearing NPZ caches; diff the produced XML
before/after: per-segment
<Type>present, count how many clusters changed classification label-vs-geometry, eyeball 7c overlays if classification shifts look suspicious.
Phase 5 — Adversarial review + ship small
- Independent reviews: GPT 5.6 Sol (codex) and Opus lenses over both diffs (correctness of fallback paths, label survival through chaining/merge/dedupe, XML schema compatibility).
- Fix confirmed findings, push branches, open draft PRs (both repos), report XML diff evidence.
Risks
- consumer Downstream CAD/XML consumers encountering the new
<Type>child on Line/Spline. Mitigation: additive-only, emitted alongside existing elements; coordinate release; escape hatch = old export package version. - data Step 6 label quality: if labels are wrong, Step 7 now faithfully reproduces those errors instead of masking them geometrically. Mitigation: the applied/missing/invalid stats event already logs counts per run; Phase 4 diff quantifies classification changes on real data.
- alignment NPZ-file-order ↔ loaded-cluster alignment is positional; a count mismatch silently reverts the whole run to geometry. Acceptable (logged + stats event), but the log level should make it prominent now that labels are the default.
- drift Some code path may re-derive type from geometry after load (axis-class checks, refine/dedupe). Phase 0 audit exists precisely to catch these.
- release Version-pin coordination: modellinglines must not require the new export version before it's published to Nexus. Draft PRs land together; pin bump merges after export release.
Open questions — RESOLVED 2026-07-15
- XML shape: decided child element
<Type>onLine/Splineconfirmed by Miro. - Escape hatch: decided not needed — the
prefer_artifact_segment_typeflag is removed entirely; labels are read directly by the loader (see scope extension below). - Values: verified producer StrEnum writes
lowercase
solid/dashed(np.asarray(SegmentType.value)in mask-clusteringcluster_io.py); real artifacts on battlebox (Abschnitt 4_5) checked as part of phase 6. The XML emits the same spellings.
Scope extension (Miro, 2026-07-15)
| # | Change | Detail |
|---|---|---|
| E1 | Drop the filtering-clusters dependency | iolabs-point-cloud-filtering-clusters is removed from
pyproject.toml. A local cluster_metadata module is vendored into
modellinglines: ClusterInfo (with a new segment_type field) plus
the NPZ cluster loader, which now reads segment_type directly per NPZ — this
kills both the bbox geometry heuristic and the filename-order alignment step
(_apply_artifact_segment_types). |
| E2 | Reworked fit rule | Label dashed and cluster length ≥ 0.70 m (new config value,
clustering section) → straight line-segment fit. Label dashed but shorter →
spline path (separator paint is ~24–25 cm wide; sub-0.7 m "dashes" are blobs). Label
solid → spline path. Label missing/invalid → spline path + warning (old
artifacts keep working). |
| E3 | Escape hatch removed | Config key, CLI flags (both entry points), guard, and the post-hoc label application are deleted; obsolete tests removed/rewritten against the vendored loader + fit rule. |
Investigated by Grok readers; implemented directly by Fable after two cursor-delegate implementation attempts failed to land code; reviewed by Sol + Opus (2 findings, both fixed) and a fresh-context Fable pass (SHIP-WITH-NITS, all nits addressed). All committed under AI3D-341.
Verification results (2026-07-15) PASS
- Unit suites: modellinglines 158 passed (worktree code shadowed); modellingexport 2 passed.
- Label values: exhaustive battlebox audit (before the outage) of all 5623
Abschnitt 4_5 NPZs — labels exist only in
clusters_mask/(all 2194, exactlysolid/dashed); plainclusters/has none, so the unlabeled→spline+warning fallback is load-bearing. - Real-data end-to-end (battlebox + Nexus down → Azure abschnitt_2, July
mask-clustering run, middle segments 126–128): stage-1 label counts dashed=12/11/13,
solid=4/5/4, unlabeled=0 — exact match against Step 6 ground truth; the 0.70 m gate fired
both ways on real clusters (0.74–1.93 m dashed → line fit; 0.23–0.53 m dashed → spline
path); final XML has 32
<Line>all with<Type>dashed</Type>and 12<Spline>all with<Type>solid</Type>, zero missing; 7 lanes / ~154 m axis across the corridor; no crashes, ~7 min CPU. - Second dataset (Abschnitt 3, 2026-07-16): middle segments 154–156 of the
July mask-clustering run — labels dashed=12/11/5, solid=4/4/5, unlabeled=0; gate decisions
consistent for all 41 real clusters (one genuine 0.52 m dashed blob correctly took the
spline path); all 27 dashed clusters produced straight-line fits matching exactly the 27
<Line>elements in the XML (all<Type>dashed</Type>) plus 13/13 solid<Spline>elements — zero missing Type. PASS. - Release ordering (pending, Nexus down): re-run
uv lock; release modellingexport as 0.8.0 (wrap-up flow), then merge the modellinglines pin (>=0.8.0).