Low-floor operating-point sweep: recommend lowering the peak threshold 0.55 → 0.30 (#54, #55) - #79
Conversation
One low-floor extraction, four consumers (#54 operating point, #55 GT completeness, labeler#20 deployment constant, labeler#27 stage-4 promotion floor). operating_point_curve.py already built the per-city half for richmond and bend; this adds the cross-split layer and extends extraction to every split. - operating_point_curve.py: `extract` now resolves ground truth for BOTH bundle kinds, so benchmark/manual_gold (gt_source.json + independently-labelled YOLO points, no verdict review) joins the sweep alongside the verdict-reviewed city splits. Per-split cache skip makes a preempted Slurm job resumable, and the cache meta now records the model repo and device. - low_floor_sweep.py: parity gate, per-split/pooled/per-tier curves, confidence calibration, and distance stratification — all CPU-only off the cached detections, so every number re-derives without a GPU. Three choices worth stating: * Tiers are assigned PER PANO from camera provenance, not per split. Richmond alone mixes iSTAR Pulsar and GoPro Max, so split-level grouping would smear the camera effect that benchmark/README.md shows precision tracking. * Parity is measured in match radii, not pixels. Bit-exactness is the wrong bar: the GSV production path built a 4096x2048 intermediate, so bend's committed detections came from a different resample than the native-res bundle pano. Richmond reproduces 100% exactly; bend is 79% exact, 98.4% within tolerance, max displacement 0.44 R. The gate asks whether any drift could change a scoring outcome, which is the question downstream actually depends on. * budapest_district5 and manual_gold are swept but held out of the pooled and per-tier rows, each with a stated reason printed alongside the results — an omission with no reason is indistinguishable from a withheld result. manual_gold is NOT parity-gated: its committed detections were exported with flip-TTA at a 0.05 floor, so that row is a TTA delta rather than drift (#78). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The confidence-calibration table showed a cliff at the deployed floor: pooled P(real) is 0.42 in the 0.45-0.50 bin and 0.93 in 0.50-0.55. That is not a property of the model, and `gtbias` shows why. Decomposing every true positive by the origin of the GT point it matched (reviewer-confirmed detection vs reviewer's missed-ramp mark) gives a clean structural result on both cities: BELOW 0.55, every single TP comes from a missed mark and none from a reviewed detection. It cannot be otherwise — the city splits' GT was assembled from detections at or above the deployed floor, so in the band this sweep opens up, a prediction is credited only if a human independently flagged that ramp during the missed-ramp pass. A real curb ramp nobody marked is scored as a false positive. Two consequences worth carrying: * Sub-0.55 precision on the city splits is a lower bound with a KNOWN mechanism, not an unquantified worry — so the measured F1-optimal threshold is biased HIGH and the true optimum sits at or below it. That is a directional guarantee the recommendation can lean on. * benchmark/manual_gold is the control, not just another split: its GT was labelled independently of RampNet, so it carries none of this bias at any threshold. Comparing its precision-vs-threshold shape against the city splits estimates the anchoring effect directly. Also fixes a float-floor bug found by the tests: 0.9 // 0.1 is 8.0, so a 0.9 detection was being binned as 0.8. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-extracting on different hardware for one clean provenance risks something easy to miss: the #55 A/B tag ids are keyed to peak COORDINATES ({pano}_{x:.5f}_{y:.5f}), so a marginal peak that moves one heatmap cell orphans its tag. Nothing errors — the tag just stops contributing and the GT-completeness correction quietly shrinks, which is exactly the kind of silent loss of reviewer effort that is hard to notice after the fact. `tagcheck` makes that failure loud: it re-derives the incremental FPs from a cache and reports resolved vs orphaned tags, defaulting to requiring 100%. Baseline against the pre-re-extraction cache: richmond 29/29 and bend 24/24 resolve, which reproduces the documented 17% (5/29) and 29% (7/24) A-rates and confirms the check is calibrated rather than vacuous. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, #55) jonf tagged the incremental false positives for the four splits that had never been spot-checked, completing the correction that previously covered only richmond and bend. Every unmatched prediction in [0.25, 0.55) is now tagged A (a real ramp the GT missed), B (a genuine FP) or unsure: clovis 23 items A=7 B=13 U=3 (A-rate 30.4%) morgantown 30 items A=4 B=25 U=1 (13.3%) annapolis 27 items A=6 B=15 U=6 (22.2%) budapest 89 items A=23 B=59 U=7 (25.8%) plus the existing richmond 17.2% and bend 29.2%. This settles a tension the manual_gold control had left open. Comparing anchored city GT against un-anchored manual_gold showed the *mechanism* (a discontinuity at the review floor that only the anchored splits have) but NOT a large aggregate gap — sub-0.50 precision is 0.195 anchored vs 0.208 un-anchored. The tagging resolves it: 26.9% of incremental FPs in [0.30, 0.55) are real ramps, so the effect is real, and the aggregate similarity is better explained by manual_gold being in-distribution. **The correction changed the recommendation.** On raw numbers F1 peaked at 0.32 and the conservative pick was 0.35; corrected, F1 peaks at 0.30 with pooled precision 0.917 — higher than the RAW precision at 0.35. That is exactly what #55 existed to determine. Adds a `corrected` subcommand so the per-split and pooled correction is reproducible rather than hand-computed, and docs/operating_point.md with the full analysis and the recommendation (0.30, uniform, not per-tier). Two things deliberately NOT claimed: no cross-split GT-completeness constant (the A-rate spans 13-30% with no ordering by imagery quality), and no per-tier threshold (four of five tiers agree within 0.03; the GSV outlier is confounded three ways). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…lits The GT-completeness column had four ❌s and a caveat saying "only richmond and bend have been corrected". All six are now corrected (A-rates 13%-30%), so the matrix, the caveat, and the "two cities is too few" note are updated. Keeps the substantive conclusion unchanged and sharpens it: the spread still does not support a single cross-city constant, and it does not order by imagery quality — morgantown (cleanest imagery) has the LOWEST A-rate and clovis (softest) the highest, which rules out the tempting "worse imagery means more missed ramps" reading. Also records manual_gold's role correctly. It was an ❌ in that column; it is not a gap but the control — its GT is independent of RampNet, so there is no anchoring to correct, and comparing the two regimes is what isolates the effect. Links the new docs/operating_point.md, which is where those A-rates are consumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a GPU (#54) Every number in docs/operating_point.md came from one GPU pass on Hyak. Without the caches, checking any of them means re-running inference over 1,625 panos -- which also needs the gitignored native-res imagery. That is the failure mode CLAUDE.md warns about: a result nobody can re-derive later. The seven caches are image-free (x, y, confidence) + derived GT, 780 KB total, so they are committed for the same reason benchmark/*/records.jsonl is. .gitignore needed restructuring, not just a negation: git cannot re-include a file whose PARENT DIRECTORY is excluded, so `analysis_out/` + `!.../*.json` is silently a no-op. It now ignores `analysis_out/*` (contents) and re-includes `analysis_out/op_cache/*.json`. Verified both directions -- the caches are tracked, the galleries and regenerable CSVs are still ignored. richmond and bend are the re-extracted versions, so all seven now share one provenance (one L40S, one env, one code revision). Re-extraction was confirmed lossless: richmond reproduced the earlier cache with 0 of 537 peaks moved (max confidence delta 6e-05), and all 222 committed #55 tags across six splits still resolve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng (#54) Two questions from PR review, both answerable from the committed caches with no GPU, and both about a threshold that -- unlike the submission threshold -- is NOT reversible: a peak below the labeler's DETECTION_STORAGE_FLOOR is never written, so no downstream consensus can promote it. `low_floor_sweep.py floor` answers them in one pass. Greedy matching is highest-confidence-first, so dropping low-confidence predictions never changes what a higher one matched -- which means one pass over the cache yields, per GT ramp, the confidence of its best candidate, and every floor's answer falls out of that single vector. 1. WHAT A 0.1 FLOOR DISCARDS. Pooled over the 5 US splits, 38 of 1393 GT ramps (2.73%) have their best candidate in [0.05, 0.10) -- permanently unrecoverable at that floor. Per split 2.33%-3.08%; manual_gold only 0.48%. 2. THE RECALL CEILING. 0.872 at the 0.1 floor vs 0.899 at the 0.05 extraction floor and 0.744 deployed. labeler#27 stage 4 has +12.8 points of headroom over the deployed threshold and +5.4 over the 0.30 recommended here, but is capped by storage at 0.872 rather than by the model's 0.899. VERDICT: lower the storage floor to 0.05. The decisive fact is that THE TOP-50 CAP NEVER BINDS -- the busiest pano in the whole benchmark holds 14 candidates at a 0.1 floor, and still 14 at 0.05, against a cap of 50. So the cap is not the volume bound the design assumed; the floor is, and it is silently costing 2.7% of findable ramps for about one extra candidate per pano. That is labeler-side work; recorded here because this is where the measurement lives. Figure: docs/figures/storage_floor_ceiling.png, from plot_storage_floor.py. Bars are a SHARE of each split's GT, not raw counts -- manual_gold has 3,919 GT ramps against ~300 for a city split, so counts would make it tower while its rate is actually the lowest of any split. Palettes (a categorical pair, and a blue ordinal ramp for the three ordered floors) were run through the palette validator rather than eyeballed. Also commits the derived result tables (~80 KB) so a reader can check any number in the prose against the table it came from. The 181 MB of gallery crops stay ignored -- they regenerate from the caches, and their irreplaceable part (the human A/B tags) is already committed under benchmark/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both additions are in — and (1) comes out against the current floorPushed as c4921e7. New subcommand One implementation note: greedy matching is highest-confidence-first, so dropping low-confidence predictions can never change what a higher one matched. That means a single pass yields, per GT ramp, the confidence of its best candidate — and every floor's answer falls out of that one vector. There's a test asserting (1) GT-matched counts by band — this falsifies
|
| split | GT | best in [0.05, 0.10) | share | best in [0.10, 0.20) |
|---|---|---|---|---|
| richmond | 310 | 8 | 2.58% | 5 |
| bend | 327 | 8 | 2.45% | 5 |
| clovis | 195 | 6 | 3.08% | 5 |
| morgantown | 267 | 7 | 2.62% | 12 |
| annapolis | 294 | 9 | 3.06% | 10 |
| POOLED (5 US) | 1393 | 38 | 2.73% | 37 |
| budapest* | 300 | 7 | 2.33% | 18 |
| manual_gold† | 3919 | 19 | 0.48% | 48 |
The decisive fact isn't the 2.73% — it's that the top-50 cap never binds. At a 0.1 floor the busiest pano in the entire benchmark holds 14 candidates against a cap of 50 (medians 2–5). At a 0.05 floor the maximum is still 14.
So the cap is not the volume bound labeler#28's design assumed. The floor is, and it's costing ~2.7% of findable ramps for a saving of roughly one extra candidate per pano. labeler#28's own argument — storing too little is irreversible, storing too much only costs disk, and the cap bounds it regardless — is correct and points at 0.05, not 0.1.
Recommendation: lower DETECTION_STORAGE_FLOOR to 0.05. Labeler-side change; recorded here because this is where the measurement lives.
(2) Recall-at-floor — the ceiling on stage-4 consensus
| pooled (5 US) | |
|---|---|
| deployed 0.55 | 0.744 |
| recommended 0.30 | 0.818 |
| ceiling at the 0.10 storage floor | 0.872 |
| ceiling at the 0.05 extraction floor | 0.899 |
labeler#27 stage 4 has +12.8 recall points of headroom over the deployed threshold, +5.4 over the 0.30 recommended here — but it is capped at 0.872 by storage, not by the 0.899 the model actually produces. Whatever k consensus requires, it cannot promote a candidate that was never written.
Figure
Two panels rather than one, because they're different questions and a shared axis would misrepresent both. Two things I changed after rendering and looking at it: the left panel plots a share of each split's GT, not raw counts (manual_gold has 3,919 GT ramps against ~300 for a city split — counts made its bar tower while its rate is in fact the lowest of any split, which is exactly backwards), and the legends moved below the axes where they were colliding with the POOLED bars. Palettes were run through the validator, not eyeballed.
On checking in analysis_out/
Already partly done in c7098be — the seven detection caches (780 KB, image-free) were committed there, which is what makes every number re-derivable on CPU. This commit adds the derived result tables (~80 KB: the swept grid, calibration, corrected P/R, distance, storage floor), so any figure quoted in the prose can be checked against the table it came from.
Deliberately still ignored: the 181 MB of gallery crops. They regenerate from the caches plus the panos, and their irreplaceable part — @JonF's A/B tags — is already committed under benchmark/<city>/incremental_fp_tags.json. Happy to include them if you'd rather, but 181 MB of regenerable PNGs in git seemed like the wrong trade.
.gitignore needed restructuring rather than a negation, incidentally: git can't re-include a file whose parent directory is excluded, so the obvious analysis_out/ + !analysis_out/op_cache/*.json is silently a no-op.
*budapest GT is single-rater, low confidence. †manual_gold is in-distribution GSV with independent GT.
🤖 Generated with Claude Code (claude-opus-5[1m])
#54 asks for "the full PR curve (precision & recall vs. threshold) ... with the deployed 0.55 point marked", per split. I built the storage-floor figure for the review question and never made this one — the headline artifact was missing. Left panel: precision against recall, traced by sweeping the threshold, one line per split, deployed 0.55 hollow and recommended 0.30 filled on each curve. The shape is the point — nearly flat across the operating region, turning down hard only past ~0.85 recall. Right panel: F1 against threshold, which exists to make a NEGATIVE point. The pooled curve varies by under 0.01 across 0.25-0.55, so F1-optimality is not a sufficient reason to pick a number; the recommendation rests on recall-first, the density budget and reversibility instead. Per-split optima cluster tightly while the levels differ — the same pattern as the per-tier table. Two encoding notes: * Line style, not colour, carries held-out status (budapest and manual_gold dashed), leaving colour free to carry identity alone and consistent across both panels. * Direct labels were tried and REMOVED. Three of the seven categorical slots sit below 3:1 on a light surface, so the palette's relief rule demands visible labels or a table view; per-line labels were the first choice, but in the PR panel the splits converge into a cluster around (0.80, 0.88) where seven labels overlap illegibly — a worse accessibility outcome than a shared legend plus the committed CSV, which is the route taken. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Added the PR-response figure — you're right, that's the headline artifactPushed as c6fc89f. I'd built the storage-floor figure for the review question and skipped the one #54 actually asks for ("the full PR curve (precision & recall vs. threshold) … with the deployed 0.55 point marked"). Left — the PR curve, one line per split, threshold swept from 0.05 to 0.90. Hollow marker = deployed 0.55, filled = recommended 0.30, on every curve. The shape is the argument: the curves run almost flat across the operating region, so recall is bought at a shallow precision cost right up until they turn down hard past ~0.85 recall. The two operating points sit on the flat part on every split. Right — F1 vs threshold, and it's there to make a negative point. The pooled curve varies by under 0.01 across 0.25–0.55. So "F1-optimal" isn't a sufficient reason to pick any number, and the 0.30 recommendation rests on the recall-first policy, the density budget and reversibility rather than on the argmax. The per-split optima (dots) cluster tightly while the levels differ a lot — the same pattern as the per-tier table. Both panels colour by split consistently, so a line is the same entity in each. Held-out splits (budapest, manual_gold) are dashed, so line style carries the held-out status and colour is free to carry identity alone. One design note worth recordingI tried per-line direct labels first — three of the seven palette slots sit below 3:1 on a light surface, so the relief rule demands visible labels or a table view. They were illegible: in the PR panel the splits converge into a cluster around (0.80, 0.88) and seven labels can't be placed there without overlapping. Screenshotting and looking at it is what caught it. Switched to a shared legend plus the committed Same class of fix as the storage-floor figure, where rendering it revealed that raw counts made manual_gold tower on the strength of having 3,919 GT ramps. Both figures are now in 🤖 Generated with Claude Code (claude-opus-5[1m]) |
That README is the repo's index of analysis tooling — PR #65 was literally adding a row to it — and this branch added eleven entry points without touching it. Adds a row per `low_floor_sweep.py` subcommand plus the two plot scripts and a note on the Slurm launcher. Also corrects two statements in the intro that this branch made wrong: * "Everything here reads the committed benchmark bundles (benchmark/{richmond, bend}/)" — it is all seven splits now, and low_floor_sweep.py plus the plot scripts need no imagery at all. * "Outputs go to analysis_out/, git-ignored" — no longer true, and the exception is the point: op_cache/*.json and op/*.csv|json are committed so the operating-point numbers re-derive on CPU from a clean checkout. Spells out what stays ignored (the 181 MB of gallery crops) and where the irreplaceable part of those galleries lives (the committed A/B tags). Adds the parity-gate explanation too, since "bend fails to reproduce bit-exactly and that is expected" is exactly the kind of thing a future reader would otherwise file as a bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
benchmark/README.md had a five-row table for adding a city that stops at "add the split to the HF dataset". That was accurate when the benchmark was two splits and one scorer. It is now badly incomplete: a new split silently invalidates the pooled operating-point numbers, the per-tier curves, the #55 correction, both figures and four documents, and nothing tells you which. docs/adding_a_benchmark_city.md is the end-to-end runbook, with a checklist to copy into the PR. Three things it captures that were only in people's heads: * PHASE 0 — decide what the city is FOR before spending the review. Every existing split answers a question the others cannot; a city that duplicates an existing answer costs a day of review and buys a table row. Names the two highest-value additions now (a second GSV city, to disentangle bend's GSV-vs-in-domain confound; and a second rater on budapest, which is a review rather than a city). * PHASE 4 — the step that gets skipped. Extraction, the parity gate, the five sweep commands, the #55 tagging at op-threshold 0.25 (matching every existing split so A-rates stay comparable), tagcheck, and figure regeneration. * PHASE 5 — the code touchpoints that fail SILENTLY. US_SPLITS membership is what puts a split in the pooled recommendation; a missing tier_of() branch dumps a new rig into "unknown"; the figure's SERIES needs a palette colour in slot order. None of these error — they quietly omit or mislabel the city. Also records the traps with the evidence behind them: never review above model resolution (the error that forced a re-review of richmond and bend, correcting recall 0.895 -> 0.765 and 0.831 -> 0.758), don't pool GT you don't trust, and the A/B tags are committed human effort that a careless re-extraction can orphan. Every code reference was checked against the source rather than written from memory, including the still-hardcoded build_benchmark_dataset.py, flagged in place as the known HF-dataset lag. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Added a city-onboarding protocol — and everything is checked inTwo things in this push. 1. Everything is committed and pushedWorking tree clean,
I also found a gap while checking: Deliberately still ignored: the 181 MB of gallery crops. Regenerable, and the human tags are committed. 2.
|
Review of docs/adding_a_benchmark_city.md against the actual CLIs and committed data turned up five errors, applied here: - Phase 1: both auto-labeler commands were wrong as written. main.py takes the geojson positionally (there is no --area flag), and export_benchmark's --bundle takes the destination directory as its argument (--out only overrides the panos dir); the old line died in argparse. - Phase 3: the empty-group gate claimed detections on empty panos are FPs worth reading — impossible, the stratum is zero-detection by construction (verified across all six splits). What appears there is missed marks, i.e. recall failures (annapolis: 4 of 25 held 10 of them). - Phase 1: "125 panos total" — richmond is 124, so bend is not the only deviation. - Phase 2: the abstention range low end is bend at 1.9% (5 of 265), not annapolis at 2.2%. benchmark/README.md carried the same error twice: annapolis is the lowest of the Mapillary splits, not "of any split", and morgantown's "lowest abstention rate of any split" holds only for missed marks (21.7%), not detections. Both sentences now state exactly what the verdicts support. All other runbook claims verified against low_floor_sweep.py, the exporter, gt_gallery.py, score_validation.py, the gitignore, and the committed verdicts/tags; 231 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018gtiDzVGepFkABX16iFDLM
Follow-ups from the same review that produced 3c47739 — not errors, but places where following the runbook verbatim would stumble: - The sbatch block now includes mkdir -p logs, which the launcher's own usage requires (its #SBATCH output paths are under logs/ and Slurm kills the job if the directory is missing). - The parity paragraph now states the actual gate (>=95% of detections within 0.5 R, count delta <=5%) alongside the expectation, so a GSV split at 98% within tolerance is read as the pass it is. - The #55 section now warns that corrected's --op-threshold defaults to 0.35 (not the recommended 0.30, so the flag is load-bearing) and that the multi-point corrected tables in docs/operating_point.md take one run per row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018gtiDzVGepFkABX16iFDLM


Closes #54. Closes #55.
Draws the precision/recall curve below RampNet's deployed 0.55 peak threshold — which had never been characterised — across all seven benchmark splits, completes the #55 GT-completeness correction on every city split, and recommends a deployment operating point.
Recommendation: 0.55 → 0.30, uniform
Pooled over the five US/VA splits (n = 609 panos), with #55's correction applied:
+7.7 recall points for −4.8 precision points. Recall is RampNet's weak metric on every split; precision is not.
The correction changed the answer
On raw numbers F1 peaked at 0.32 and the conservative pick was 0.35. With the GT-completeness correction applied, corrected F1 peaks at 0.30, where corrected precision (0.917) exceeds the raw precision at 0.35. That is exactly what #55 existed to determine.
@JonF tagged the four splits that had never been spot-checked, so all six now carry a verdict on every incremental false positive — an unmatched prediction scoring in
[0.25, 0.55), i.e. one that only a lowered threshold surfaces:The A-rate is A over the total, i.e. what share of the apparent new false positives were real ramps all along — the correction factor on low-threshold precision.
Pooled, 26.9% of the incremental false positives in
[0.30, 0.55)were real curb ramps the ground truth had missed.Findings worth reading even if you skip the rest
The 0.92 minimum does not gate AI label submission.
AiController.submitAiLabelchecks an internal API key plus the per-city booleanai-label-submission-enabled— there is no accuracy threshold on that path.ai-validation-min-accuracybelongs to the DINOv2 validator subsystem. Andlabel_ai_infopersists each label'sconfidence, so a lower floor is reversible server-side. The constraint is a policy call about review burden, not a gate.Sub-0.55 precision is a lower bound, by construction. Decomposing every TP by the origin of the GT point it matched: below the review floor, every true positive comes from a reviewer missed-mark and none from a reviewed detection, on every split. The city GT was built from ≥0.55 detections, so a real ramp nobody flagged is scored as a false positive.
low_floor_sweep.py gtbiasreproduces this.The A-rate does not track imagery quality. Morgantown — the cleanest split in the benchmark — has the lowest A-rate (13%) and clovis, the softest, the highest (30%). So no cross-split GT-completeness constant is quoted; the correction is applied per split.
Clovis is the cheapest place to lower the threshold, not the binding constraint. It gains +10.3 recall points, the largest of any US split.
Not per-tier. Tier optima cluster at 0.32–0.35 across a 2018 GoPro Fusion and a survey-grade Trimble MX7. Only GSV/bend differs (0.50) and it is confounded three ways — one split, in-domain, and the resample caveat below. A per-tier policy would fit noise.
This is not recall-by-carpet. Density rises 1.85 → 2.23 detections/pano, leaving RampNet 25–40× sparser than the open detectors at their operating points (
docs/model_comparison.md's null-recall finding).The recall gain stacks with multi-view. It is uniform across distance bands (+0.071 near / +0.078 mid / +0.072 far), so it does not overlap with what labeler#27 would recover.
Verification
Parity gate — peaks at ≥0.55 must reproduce each split's committed
records.jsonl. Measured in match radii, since bit-exactness is the wrong bar:Every Mapillary split reproduces bit-exactly, on different hardware. Bend is the sole exception for a structural reason: it is the only GSV split, and the GSV production path built a 4096×2048 intermediate, so production fed the model a different resample than the native-res bundle does. This was predicted before the four remaining splits ran, and held. (Consequence: bend's GT points and predictions come from different resamples, making its numbers mildly pessimistic.)
†
manual_goldis deliberately not gated — its committed detections used flip-TTA, so its row is a TTA delta. Free data point for #78: TTA yields 3.5% more detections at ≥0.55, 99.9% co-located.Re-extraction is lossless. richmond re-extracted on an L40S reproduced the earlier cache with 0 of 537 peaks moved (max confidence delta 6e-05), and all 222 committed #55 tags across six splits still resolve (
low_floor_sweep.py tagcheck, which exists because tag ids are keyed to peak coordinates and a re-extraction could silently orphan reviewer work).What is in here
scripts/analysis/low_floor_sweep.py—parity | sweep | hist | gtbias | distance | corrected | tagcheck, all CPU-only off the committed cachesscripts/analysis/run_low_floor_extract.slurm— the one GPU step (one L40S, 41 min for 1,625 panos)operating_point_curve.py—extractnow reads both bundle kinds, somanual_gold(independent YOLO GT, no verdict review) joins the sweep; per-split skip makes a preempted job resumabledocs/operating_point.md— the full analysisanalysis_out/op_cache/*.json— 780 KB of image-free detections, committed so every number re-derives without a GPUHonest limits
docs/operating_point.mdreports that tension rather than the stronger claim.Hand-offs
🤖 Generated with Claude Code (claude-opus-5[1m])