The Laurens deficit is the rig, not the town: add the GSV arm and score all 11 challengers on both (#151) - #153
Conversation
Laurens is the only city in the benchmark run on both imagery sources over one footprint (area hash d8dd392b...), which is what makes it the control for RampNet#151. With a second arm arriving, the bare city name stops being an identifier and starts being a lie: `laurens` silently meant Mapillary. The convention, now written into docs/adding_a_benchmark_city.md phase 0: A split carries a `_<source>` suffix if and only if its city has more than one imagery arm. So `bend`, `richmond` and the rest stay bare and pay nothing. If bend ever gains a Mapillary arm it gets renamed at that moment, not pre-emptively. Timing is the whole argument for doing this now rather than later. Checked the Hub directly: projectsidewalk/rampnet-benchmark was last modified 2026-08-17, its four configs are per-split parquets named <split>.parquet, and there are zero laurens files -- 0c8e0f4 registered the split in BENCHMARK_SPLITS but never ran build -> verify -> push. So the one irreversible cost of a rename, breaking every existing load_dataset(..., "laurens"), does not exist yet. It will the moment the dataset is republished. Doing it inside this PR also keeps the bare name out of main's history entirely. Mechanical only -- this changes no number, only its label: benchmark/laurens/ -> benchmark/laurens_mapillary/ (4 tracked files; panos/, gallery/ and index.csv are gitignored) analysis_out/op_cache/laurens.json -> laurens_mapillary.json, and its internal "city" key the `split` column of six committed tables under analysis_out/op/, plus scoreboard.json US_SPLITS (low_floor_sweep, miss_decomposition), TIER, BENCHMARK_SPLITS, SERIES, SPLIT_HEADER, and the frozen-study comments in farfield_forensics and silent_activation Two things the blind rewrite got wrong and were fixed by hand. SPLIT_HEADER is documented as "short enough that the by-split matrix stays readable", so it takes `laur_mly` -- mly being this project's own abbreviation for Mapillary, cf. the mly1_public tiles -- not the full 17-character name; the provenance and coverage tables, which have room, keep the full id. And all three model_scoreboard.md hits were inside <!-- BEGIN GENERATED --> blocks, so that file was reverted and regenerated with scoreboard.py rather than edited. Only scoreboard_by_split.png moved; the other four figures rebuilt byte-identical. pytest: 8 failed, 1314 passed, 1 skipped -- byte-identical to the pre-change baseline, same eight test_scoreboard.py tests 0c8e0f4 flagged as unresolved. Nothing here addresses those; they are still the open question on this PR. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SF2s8wWUM6HphV1xuYju21
Phase 1 of docs/adding_a_benchmark_city.md for the second Laurens arm.
Laurens is the only city run on both imagery sources over one footprint
(area hash d8dd392b...), so reviewing the GSV arm separates the two
explanations for its recall 0.390 -- the benchmark's worst by 2x, and the
only split whose misses are NEAR rather than far-field:
GSV recall ~0.35 -> the cause is ramp GEOMETRY. Flush / minimal-reveal
rural ramps are a real blind spot; the fix is training data. Escalate.
GSV recall ~0.75 -> the cause is RIG or SEASON, and Laurens becomes
evidence about consumer 360 rigs and autumn capture instead.
python scripts/export_benchmark.py runs/laurens_gsv/results.jsonl \
--bundle ../RampNet/benchmark/laurens_gsv # in the auto-labeler
86 panos: 5 top / 56 random / 25 empty, from 2,137 GSV panos carrying 473
operational detections over 230 panos. Reconcile is 86/86 with zero decay.
Sampler defaults are untouched (sample 100, empty 25, seed 0, spacing 30 m),
identical to the Mapillary arm, so the two are comparable on the unbiased
column.
The 56 rather than 95 is the same geometric saturation that capped the
Mapillary arm at 64: _spread walks the whole shuffled candidate pool and
the town simply runs out of detection panos 30 m apart. Unbiased column is
81 against that arm's 89 and the usual 120, so both arms' intervals are
wider than other splits'. Do not "fix" this by lowering --min-spacing.
Measured, and it changes the analysis plan: the two independent samples
already overlap heavily, because both are 30 m greedy walks over the same
small road network. 51 of 86 GSV panos (59%) sit within 20 m of a
laurens_mapillary bundle pano; median nearest neighbour 17.2 m, and 7.7 m
across the `top` stratum. So the standard protocol hands us a paired subset
of ~51 corners seen by both rigs for free -- same physical ramps, rig and
season the only variables -- on top of the two independent recalls. A
bespoke site-matched sampler was considered and is not needed, which also
keeps this split comparable with the other ten.
imagery_manifest.json pins the bytes now, before review, so the verdicts
can never be re-paired with re-fetched pixels (--cities laurens_gsv, which
does not require the split to be registered).
NOT registered yet, deliberately. BENCHMARK_SPLITS is test-locked to
ALL_SPLITS minus manual_gold, and split_date_range reads every listed
split's verdicts.json -- so registering an unreviewed split breaks the
suite. Registration is phase 5, after the GT review produces verdicts.
When it lands, the arm goes into HELD_OUT, not US_SPLITS: it samples the
same physical ramps as laurens_mapillary, so pooling both would
double-count them and break the independence the Wilson intervals assume.
That is a third kind of held-out reason, and the convention plus the
CITY_OF mechanism are written up in phase 0 of the runbook.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SF2s8wWUM6HphV1xuYju21
86/86 panos reviewed. P 0.925 (0.864-0.960) / R 0.505 (0.439-0.570); unbiased 0.904 / 0.459 (0.389-0.531). #151's decision rule was GSV ~0.35 -> ramp geometry, ~0.75 -> rig or season. 0.505 is neither, so the answer is the split verdict the issue listed as a real possible outcome. It decomposes cleanly, though, and the two halves point in different directions: RIG/SEASON IS REAL, AND WORTH ~12 RECALL POINTS. GSV 0.505 vs Mapillary 0.390 is +11.5 pts with barely-overlapping Wilson intervals. The paired subset confirms it is not a sampling artifact -- restricting both arms to the corners both rigs saw reproduces the same gap at every radius: 15 m 33 panos/arm mly 0.415 gsv 0.545 +0.130 20 m 49/51 panos mly 0.393 gsv 0.518 +0.125 25 m 54/57 panos mly 0.398 gsv 0.502 +0.104 THE DEFICIT MOSTLY SURVIVES ANYWAY. 0.505 is still the worst of any US split by ~18 points -- next lowest is paterson at 0.684, on a uniform TP/(TP+FN) recomputation across all ten splits. Sept-2024 leaf-free GSV at 16384x8192 does not rescue Laurens. So roughly a third of the gap is the camera and two thirds is the place, and the escalation in #151 stands. BUT THE SPECIFIC EVIDENCE FOR FLUSH RAMPS WEAKENED, AND THE ISSUE TEXT NEEDS AMENDING. The near-miss signature -- the thing that made Laurens unique among ten splits, and the whole basis for "near, well-resolved ramps are being missed" -- is ARM-SPECIFIC. Median normalized-y delta (missed minus detected), abstentions excluded: nine other splits -0.013 .. -0.030 (misses are far-field) laurens_mapillary +0.0038 (inverted -- the #151 finding) laurens_gsv -0.0053 (back to the normal direction) So on better imagery the inversion disappears. laurens_gsv is still an outlier -- at half the magnitude of the nearest split (richmond -0.0128) it has the weakest far-field tilt in the benchmark -- but it is no longer inverted. "Near, well-resolved ramps are being missed" was substantially a GoPro-Max-in-November artifact, not a property of rural ramps. The flush/minimal-reveal hypothesis therefore survives as an explanation of the LEVEL and loses its distinctive supporting signature. Making it quantitative still needs the curb-reveal measurement from GSV depth (#111), which this arm can supply and the Mapillary arm cannot. Method note: recall here is TP/(TP+FN) with `unsure` misses excluded, which reproduces score_validation.py's all-panos column exactly on both arms; the delta method reproduces every published per-split delta in #151. Missed-ramp abstention is high in both arms -- 20% mapillary, 15% gsv -- itself a signal about how hard these corners are for a human to call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SF2s8wWUM6HphV1xuYju21
…lation mismatch PR #152 left eight test_scoreboard failures unsilenced and asked for a decision: re-run the challengers on Laurens, or hold it out of the comparison basis. This is the first option. Every registered leg now has laurens_mapillary detections, so the scoreboard compares eight splits against eight rather than eight against seven. What the re-run settles: - test_ap_ordering_is_not_an_artifact_of_the_floor passes on its own terms. It read 0.829 (RampNet, 8 splits) vs 0.730 (YOLO, 7) = 0.0993, just under the asserted 0.1. Measured on the same eight splits it is 0.8289 vs 0.7229 = 0.1061. Adding Laurens lowers both -- RampNet 0.849 -> 0.829, y11x 0.730 -> 0.723 -- and the margin widens because the comparison is finally like-for-like. The threshold was never the problem and is untouched. - The pooled operating point moves onto the eight-split basis, and docs/operating_point.md moves with it: 0.55 goes 0.964/0.722/0.826 -> 0.959/0.686/0.800 over n=953. Precision is untouched; the recall drop is Laurens' own 0.390 entering the pool. The F1 optimum is still 0.32 and the recommendation is unchanged. Two results worth recording: - The supervised YOLO baseline beats RampNet on Laurens at the #71 operating point -- y26_pano 0.574 and y11l_pano 0.563 against 0.543 -- the first split where that happens. It is an operating-point result, not a curve result: read full-range at the 0.05 floor RampNet's AP is 0.691, still the highest of the four. The YOLO arms win on recall (0.450, 0.402 vs 0.390) while RampNet keeps precision (0.898 vs 0.794). - The Qwen-32B inversion recurs a fourth time, most extreme instance yet: 0.066 against 8B's 0.210, at 0.27 boxes/pano and recall 0.036. Four splits, three of them HIGH-confidence US GT, so the "it stops firing on unusual infrastructure" mechanism is established rather than suggestive. Against zero-shot challengers RampNet's lead survives its worst split: +0.200 F1 over gemini-3.1-pro. OWLv2's 0.851 recall is density (68.0 boxes/pano), not a usable ceiling. Detections: 11 files, all --verify identical to the cache. The YOLO export needs --tiling none --yolo-imgsz 1280 or the reconstructed signature misses every cache entry and writes empty files without failing; that trap and the exact commands are now in docs/replication.md, whose file-count ledger a test caught going stale at 114/125. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… the rig, not the town The GSV arm was run through the identical leg list as laurens_mapillary, so the two differ only in imagery. That makes it the #151 discriminator, and it answers cleanly. Every zero-shot challenger is flat or WORSE on the GSV arm -- seven of eight move down, none moves up more than 0.008 -- while RampNet gains +0.115 (F1 0.543 -> 0.659, recall 0.390 -> 0.509). If rural streetscape were intrinsically hard, the arm that is easier for RampNet would be easier for them too. It is not. Laurens' 0.390 is RampNet meeting an out-of-domain rig, not a town that defeats detectors. Two consequences: - The RampNet-vs-YOLO ordering FLIPS between the arms. On Mapillary the supervised baseline wins (y26 0.574, y11l 0.563 vs 0.543); on GSV RampNet leads by 0.072 (0.659 vs 0.587). The previous commit's "YOLO beats RampNet on the rural split" is really "on non-GSV imagery" and does not survive changing the rig over the same ground. - Being GSV-trained is not the whole mechanism. The YOLO arms trained on the same GSV-derived dataset barely move (+0.039, +0.024, and y26 goes the other way) against RampNet's +0.115, so RampNet is markedly more rig-sensitive than a YOLO trained on its own data. That points at preprocessing and the 2048x4096 input rather than the training distribution, and is an open question, not a closed one. Caveats travel with the table: the arms are unpaired (249 GT ramps vs 220), and the ~51 corners within 20 m of each other are the paired subset that would turn this into a measurement -- both arms' detections are committed, so it needs no new inference. Registration follows docs/adding_a_benchmark_city.md, which prescribed all of this and described machinery that did not exist. CITY_OF is now real, and so is the one-pooled-split-per-city test it exists for. Held out for non-independence, not GT quality: 59% of gsv panos sit within 20 m of a mapillary one (median NN 17.2 m), so the arms largely see the same physical ramps and pooling both would double-count them. The pooled operating point is unchanged, which is what held-out has to mean. Three latent bugs surfaced, each found by a test rather than by reading: - pool_of() named the three held-out splits literally instead of deriving from HELD_OUT, so a newly held-out split was pooled anyway -- laurens_gsv would have been pooled alongside its own sibling arm while HELD_OUT said otherwise. Now derived. - The AP provenance table had a binary (substituted, or "bundle, already at 0.05") that held only while manual_gold was the sole unsubstituted row. It printed "already at 0.05" against laurens_gsv, whose bundle is truncated at 0.55 with no op_cache to swap in -- a false provenance claim on the page whose job is provenance. Cells now carry bundle_floor and the table distinguishes three cases. - docs/replication.md's file ledger went stale again (125 -> 136). Detections: 11 more files, all --verify identical to the cache. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Deep reviewRe-ran everything from the branch head ( What follows is mostly provenance and one real bug, not the science. Merge against 1. Medium —
|
| ts (UTC, 2026-09-01) | bundle | leg | panos | calls | est. cost |
|---|---|---|---|---|---|
| 12:32 | laurens_mapillary | gemini-3.6-flash (smoke) | 2 | 12 | $0.03 |
| 13:14 | laurens_mapillary | gemini-3.1-pro-preview | 94 | 564 | $1.54 |
| 13:15 | laurens_mapillary | gemini-3.6-flash | 94 | 402 | $0.99 |
| 13:35 | laurens_mapillary | gemini-3.7-flash | 94 | 564 | $1.10 |
| 14:05 | laurens_gsv | gemini-3.1-pro-preview | 86 | 516 | $1.38 |
| 14:16 | laurens_gsv | gemini-3.6-flash | 86 | 516 | $1.23 |
| 14:26 | laurens_gsv | gemini-3.7-flash | 86 | 516 | $0.96 |
| total | $7.22 |
This is the #143 failure mode again: the rows were written, to a file that is not on the branch that publishes the detections. None of the 22 legs has a wall-clock figure (the rows predate #147's timing fields; the sixteen GPU legs — A40 and 2×L40S per replication.md:139–146 — wrote no row at all), and docs/replication.md's Laurens section states no cost.
Fix: commit the seven rows into analysis_out/usage_log.jsonl on this branch (they can be read from the main checkout's working tree with git diff -- analysis_out/usage_log.jsonl without modifying anything; de-duplicate on ts if they have since landed elsewhere), and add one line to the Laurens section of replication.md: $7.22 across six Gemini legs, wall-clock unrecorded for all 22 legs, GPU-hours for the sixteen local legs unrecorded — as a stated gap, not silence.
4. Medium — the radius table and the overlap statistics have no script
The numbers are right. Recomputed from records.jsonl coordinates (haversine) and verdicts.json through rampnet.validation.collect on each subset:
| radius | panos mly/gsv | mly R | gsv R | Δ |
|---|---|---|---|---|
| 15 m | 33/33 | 0.415 (59/142) | 0.545 (79/145) | +0.129 |
| 20 m | 49/51 | 0.393 (79/201) | 0.518 (99/191) | +0.125 |
| 25 m | 54/57 | 0.398 (86/216) | 0.502 (103/205) | +0.104 |
gsv→mly nearest neighbour: median 17.2 m, 51/86 = 59.3% within 20 m. Every cell matches the body (the 15 m delta is +0.129 unrounded; the body's +0.130 is a subtraction of rounded cells). But that is my 40-line script in a scratch directory, not one in the repo — the table appears in the PR body, test_miss_decomposition.py:131, low_floor_sweep.py:84, adding_a_benchmark_city.md and model_comparison.md, and the only thing standing behind it is prose.
The "rig, not the town" ΔF1 table is likewise hand-assembled from scoreboard.json. Eleven of twelve deltas match the unrounded values; y11l_pano reads +0.024 where the unrounded delta is +0.023 (0.5870 − 0.5634), because the table subtracts rounded cells.
Fix: a committed scripts/analysis/laurens_arm_overlap.py that writes analysis_out/laurens_arm_overlap.json (nearest-neighbour distances both directions, the three radius rows, the twelve ΔF1 rows from scoreboard.json), a test pinning the counts and recalls above, and the doc table regenerated from it.
5. Medium — the doc argues both sides sixty lines apart
Both passages are new in this PR (model_comparison.md). The laurens_mapillary paragraph, point 1: "the rural deficit is not RampNet-specific in the zero-shot field — whatever makes this town hard makes it hard for everything that has never seen a curb ramp label." Then "The rig, not the town", point 1: "The town is not the problem." The second is the finding; the first is the pre-GSV-arm reading it replaced, still stated as a conclusion.
Fix: amend the first to say the GSV arm reversed it and point to the section below, or delete the sentence.
6. Medium — eight collapsed line continuations in the new runbook block
docs/replication.md lines 139–143, 146, 150 and 151 (all from 5111834) are single physical lines with five-space gaps where \-newline used to be, e.g.
python scripts/model_comparison/compare.py benchmark/laurens_mapillary --models rampnet,owlv2,gdino --sweep
This is exactly the class #136 guards against, and #136's test walks replication.md. I ran that test against this branch: it fails (first on the pre-existing model_comparison.md:1306, which #136 itself repairs, then on these eight). Once #136 and this PR are both on main, the suite is red.
Fix: restore the continuations, written via chr(92) so no patch layer eats them (as #136 documents).
7. Low — the GSV headline P/R and the CI beside it come from two scorers
scripts/score_validation.py benchmark/laurens_gsv (the runbook's Phase 3 instrument) reports P 0.925 / R 0.505 (CI 0.439–0.570): 111 correct, 9 incorrect, and one detection marked duplicate, scored as a false positive by default. The doc table and body say 0.933 / 0.509 (112/8/108) — the scoreboard's scorer, which counts that one duplicate as a hit. The body then quotes the 0.505 CI next to the 0.933 precision. The mapillary arm has no duplicate mark, so its two scorers agree and nobody would notice the split.
Fix: one sentence under the laurens_gsv table naming the scorer and the one-detection difference.
8. Low — replication gaps not stated where the numbers are
- Neither arm's panoramas are published (Add Laurens, IA — the first rural split (recall 0.390) and register it as the 8th US split #152: 8.3 GB Mapillary and 40 GB GSV on makelab2).
replication.md's Laurens section does not say so or name what unblocks it — which per this PR's own runbook text is an HF publish under the new config names, before the first publish. benchmark/README.md:81still says "All nine splits are published" and:109"the seven US splits"; neither arm is listed.- The split coverage matrix at the top of
model_comparison.md(the| annapolis | ✅ ...table) has no row for either arm. operating_point.md:212, 419, 772still say "seven US splits" for the tag-based sections. That is correct — the GT-completeness correction: spot-check the low-confidence incremental FPs from the operating-point curve #55 pass has not run on Laurens — but it now sits under a table that is eight-split, with nothing saying the two populations differ.
Fix: the four statements, each where the reader is.
9. Low — rename leftovers inside the moved bundle
benchmark/laurens_mapillary/verdicts.json still carries run_key: "laurens", run_name: "laurens", source: "benchmark\laurens\records.jsonl"; imagery_manifest.json has "city": "laurens". box_gallery.py:1047 reads run_key. The manifest digest is over pano hashes only (imagery_manifest.py:48, digest_of), so correcting city leaves 7485886ea08f671b unchanged.
Fix: update the four fields.
10. Low — register
replication.md:155 (new): "load-bearing". Out of scope but in the same file: :588 "naming wart", and adding_a_benchmark_city.md:224 "load-bearing".
Decisions for Jon, not fixes
- A 0.05-floor
op_cacheforlaurens_gsv. It needs the GSV panos (makelab2) and a model pass, not just scoring. With it the AP column becomes comparable across the arms, the operating-point figure can carry the arm, and the "AP is not comparable" caveat can go. - The paired ~51-corner analysis. Finding 4's script gives the recall half for free (it already does: +0.125 at 20 m). Whether the precision/F1 half lands here or in Recall collapses on flush / minimal-reveal curb ramps: Laurens R 0.390, and the experiment that isolates it #151 is a scope call.
- Merge order and the one conflict. Clean against
main, Record time as well as money for every non-free run (#143) #147, Vistas at resolution parity: the handicap was real, and it was recall (#126) #137, Actually fix the two collapsed runbook snippets from #129, and guard the class #136 and The RampNet-vs-YOLO gap at matched operating points is 0.039, not 0.252 — plus the label rescue and the equirect control (#51) #154. Against claude-opus-5 on nine splits: the annapolis displacement does not generalise (#139) #146 it conflicts onanalysis_out/scoreboard.json,docs/model_scoreboard.md,docs/replication.mdand fourscoreboard_*.png— all regenerated files. Whichever lands second re-runsscoreboard.pyand takes the fresh output; nothing to resolve by hand except the replication ledger paragraph. - The body's "next lowest paterson 0.684" matches neither committed source:
scoreboard.jsonhas 0.686 (bundle detections),operating_point.md:304has 0.681 (op_cacheat 0.55). - Reviewer attestation is prose only. Neither arm's
verdicts.jsonhas thereview_notesblockscore_validation.pyprints when present (reviewer, date, confidence, caveats), so "reviewer confidence HIGH" exists only in the doc. Every other split is the same, so this is benchmark-wide rather than this PR's defect — and only the reviewer can write it, which is why it is not on the fix list.
What holds up
- The scoreboard is exactly what the script produces, on Windows: JSON, rendered doc and all five figures byte-identical;
--checkcurrent. - Every number in both split tables and the twelve-row ΔF1 table matches
scoreboard.json, recomputed independently: RampNet +0.115 (0.5434 → 0.6588); the eight zero-shot challengers move −0.064, −0.049, −0.048, −0.032, −0.020, −0.007, −0.003 and +0.008 — seven down, none up by more than 0.008, as written; the ordering flip (y26 0.574 / y11l 0.563 vs 0.543 on Mapillary; 0.659 vs 0.587 on GSV); the YOLO deltas +0.039 / +0.024 / −0.036; AP 0.691 (op_cache) vs 0.494 (bundle, floor 0.552). - The radius table and the overlap numbers reproduce exactly with the repo's own verdict scorer (finding 4 is about the missing script, not the numbers).
pool_of()is now derived fromHELD_OUTand "found by a test" is true:test_pool_holds_out_budapest_and_gold_by_defaultassertsset(pool) == set(US_SPLITS), which the old literal list would have failed the momentlaurens_gsventeredCITY_SPLITS.- The three-way AP provenance (
bundle_floor,scoreboard.py:356;ap_provenance_table,scoreboard_render.py:213–224) and its test check the real cells:manual_goldfloor < 0.4,laurens_gsvfloor 0.552 withap == ap_bundle. CITY_OFand both guards intest_miss_decomposition.pydo what the body says; the prefix heuristic in the second one resolves every current split correctly.- The 22 detection files: identical top-level schema to the existing files,
signature["model_id"]equal to the published label on all 22, eleven legs × two arms with nothing missing. The--verifyclaim cannot be checked here — no.model_cache— and is taken on the body's word. - The replication ledger arithmetic: 82 + 12 + 36 + 4 + 2 = 136 files on disk.
- Bundle order:
imagery_manifest.jsoncommitted (62cc220) beforeverdicts.json(e9dfd86);tier_of()resolves the GSV arm viasource: "launch"; no ToS or licensing text anywhere in the diff. - CI: the body's "this is what makes Add Laurens, IA — the first rural split (recall 0.390) and register it as the 8th US split #152 green" is true —
test_ap_ordering_is_not_an_artifact_of_the_floorand the threshold-mark test both pass with the challengers on eight splits.
Fix list
- [F1] Medium
plot_operating_point.collect()skips splits with noop_cachefile and prints which; regeneratedocs/figures/operating_point_pr.pngfrom the committed cache; givelaurens_gsva legend mark not already used byoperating_point.md(§ is sao_paulo's) — files: scripts/analysis/plot_operating_point.py, docs/figures/operating_point_pr.png - [F2] Medium
git rmthe three unexplained Score crop-window rules against the manual_gold boxes: containment, margin, and context ratio (crop-clarity acceptance test) #114 artifacts — files: analysis_out/crop_window_eval_annapolis.json, analysis_out/crop_window_eval_paterson.json, analysis_out/crop_window_eval_sao_paulo.json - [F3] Medium commit the seven Laurens ledger rows (read them from the main checkout's working tree with
git diff, never modify that checkout; de-duplicate onts) and add the cost/time line to the Laurens section: $7.22 over six Gemini legs, no wall-clock for any of the 22 legs, no GPU-hours for the sixteen local legs — files: analysis_out/usage_log.jsonl, docs/replication.md - [F4] Medium add
scripts/analysis/laurens_arm_overlap.py(nearest-neighbour distances both ways, the 15/20/25 m radius-recall rows viarampnet.validation.collect, the twelve ΔF1 rows fromscoreboard.json) writinganalysis_out/laurens_arm_overlap.json(LF, rounded floats, whitelisted in .gitignore), a test pinning 33/33 · 49/51 · 54/57, 0.415/0.545, 0.393/0.518, 0.398/0.502, 59.3%, 17.2 m; regenerate the doc's ΔF1 table from it (y11l_pano becomes +0.023) — files: scripts/analysis/laurens_arm_overlap.py, analysis_out/laurens_arm_overlap.json, tests/test_laurens_arm_overlap.py, docs/model_comparison.md, .gitignore - [F5] Medium reconcile
laurens_mapillarypoint 1 ("whatever makes this town hard makes it hard for everything") with "The rig, not the town" — amend to say the GSV arm reversed the reading and point down — files: docs/model_comparison.md - [F6] Medium restore the
\continuations on replication.md lines 139–143, 146, 150, 151 (write viachr(92)); confirm with Actually fix the two collapsed runbook snippets from #129, and guard the class #136'stest_no_runbook_snippet_has_a_collapsed_line_continuationlogic — files: docs/replication.md - [F7] Low one sentence under the
laurens_gsvtable: table is the scoreboard scorer (112/8/108, P 0.933 / R 0.509);score_validation.pyreads 0.925 / 0.505 because its oneduplicatemark scores as a false positive; the CI quoted is the latter's — files: docs/model_comparison.md - [F8] Low state the replication gaps where the numbers are: Laurens panoramas unpublished for both arms (makelab2; unblocked by an HF publish under the new config names) in replication.md's Laurens section;
benchmark/README.mdnine→twelve bundles with both arms listed and marked unpublished, "seven US splits"→eight; a row for each arm in model_comparison.md's coverage matrix; a one-line note at operating_point.md's tag-based "seven US splits" sections that they predate Laurens — files: docs/replication.md, benchmark/README.md, docs/model_comparison.md, docs/operating_point.md - [F9] Low update
run_key/run_name/sourcein benchmark/laurens_mapillary/verdicts.json andcityin benchmark/laurens_mapillary/imagery_manifest.json tolaurens_mapillary(digest unchanged; verify withimagery_manifest.py'sdigest_of) — files: benchmark/laurens_mapillary/verdicts.json, benchmark/laurens_mapillary/imagery_manifest.json - [F10] Low replace "load-bearing" at replication.md:155 with plain wording — files: docs/replication.md
🤖 Generated with Claude Code (claude-fable-5-1)
Stacked on #152 — base is
benchmark/laurens, notmain. Merge #152 first, then this. (We have been bitten before by a stacked PR being merged into its base branch and never reaching main; checkmainafterwards.)#152 adds Laurens as the 8th US split and records its recall of 0.390, the benchmark's worst by a factor of two. This PR adds the second imagery arm over the same footprint — the experiment #151 pre-registered to discriminate rural ramp geometry from rig and season — reviews its ground truth, and scores all 11 challengers on both arms.
What it answers
#151 pre-registered a decision rule: GSV recall ≈ 0.35 → ramp geometry, ≈ 0.75 → rig or season. The GSV arm reads P 0.933 / R 0.509 / F1 0.659 (86/86 panos reviewed, 220 GT ramps; verdict scorer 0.505, Wilson 0.439–0.570). That is neither, so the answer is the split verdict the issue listed as a real possible outcome — and it decomposes into two halves that point in different directions.
Rig/season is real, worth ~12 recall points. Confirmed on the corners both rigs actually saw, so it is not a sampling artifact:
The deficit mostly survives anyway. 0.509 is still the worst US split by ~18 points (next lowest paterson 0.684). Leaf-free September GSV at 16384×8192 does not rescue Laurens, so roughly a third of the gap is the camera and two thirds is the place.
The specific evidence for flush ramps weakened. The inverted near-miss delta — the thing that made Laurens unique among ten splits — is arm-specific:
laurens_mapillary+0.0038,laurens_gsv−0.0053, back to the normal far-field direction. #151's body is amended and that bullet withdrawn.The rig, not the town
Every model was run through the identical leg list on both arms, so imagery is the only thing that moves. Every zero-shot challenger is flat or worse on the arm RampNet prefers — seven of eight move down, none moves up by more than 0.008 — while RampNet gains +0.115 (F1 0.543 → 0.659). If rural streetscape were intrinsically hard, the arm that is easier for RampNet would be easier for them too. Laurens' 0.390 is RampNet meeting an out-of-domain rig, not a town that defeats detectors.
Two consequences worth flagging to reviewers:
y26_pano0.574,y11l_pano0.563 vs 0.543); on GSV RampNet leads by 0.072 (0.659 vs 0.587). "YOLO beats RampNet on the rural split" is really "on non-GSV imagery". This qualifies Train a supervised YOLO baseline (YOLO11 / YOLO26) on the RampNet dataset — isolate architecture vs. data #51 rather than contradicting it.y26_panogoes the other way) against RampNet's +0.115, so RampNet is markedly more rig-sensitive than a YOLO trained on its own data. That points at preprocessing and the 2048×4096 input rather than the training distribution — an open question this split hands to Recall collapses on flush / minimal-reveal curb ramps: Laurens R 0.390, and the experiment that isolates it #151 and Experiment: curb-ramp detection performance vs. input resolution (Mapillary high-res) #25, not one it closes.Registration
laurens_gsvis held out of the pooled basis for non-independence, not GT quality: 59% of its panos sit within 20 m of alaurens_mapillaryone (median NN 17.2 m), so the arms largely see the same physical ramps and pooling both would double-count them. The pooled operating point is unchanged, which is what held-out has to mean.laurensis renamedlaurens_mapillarysince it is now one of two arms, andCITY_OFplus the one-pooled-split-per-city test — both prescribed bydocs/adding_a_benchmark_city.mdand neither of which existed — are now real.Three latent bugs, each found by a test rather than by reading
pool_of()named the three held-out splits literally instead of deriving fromHELD_OUT, so a newly held-out split was pooled anyway —laurens_gsvwould have been pooled alongside its own sibling arm whileHELD_OUTsaid otherwise. Now derived.manual_goldwas the sole unsubstituted row; it read "already at 0.05" againstlaurens_gsv, whose bundle is truncated at 0.55 with noop_cacheto swap in. Cells now carrybundle_floorand the table distinguishes three cases.docs/replication.md's file ledger went stale again (125 → 136).Caveats, carried in the docs beside the numbers
laurens_mapillaryhas a 0.05-floorop_cacheandlaurens_gsvdoes not, so 0.494 is truncated at 0.55 while the other arm's 0.691 is not. P/R/F1 are comparable — both read at 0.55.Detections: 22 new files, all
--verifyidentical to the cache.Closes nothing on its own — #151 stays open on the flush-ramp escalation and the rig-sensitivity question it opened.
🤖 Generated with Claude Code (claude-opus-5[1m])