From cff2308d85d86075170fb0ca242a797b6c18660f Mon Sep 17 00:00:00 2001 From: Jon Froehlich Date: Fri, 31 Jul 2026 12:33:57 -0700 Subject: [PATCH 1/4] Phase 0 of the far-field visible anomaly (#46): mild survivorship, and the pixel floor is refuted by the model's own hits The reviewer pass rated 34/36 rateable far-field silent-miss crops visible on the model-resolution panel, contradicting E1's pixel-starvation framing. Before letting the verdicts mean anything, this checks the sample and the framing from committed data alone (no GPU, no network, no imagery). - scripts/analysis/farfield_forensics.py (+21 tests): populations from the low-floor caches, the witness list, the gallery manifest/verdicts, and the imagery manifests' width fields. - Survivorship (H1) is real but mild: AUC(rated vs unrated far-silent px) 0.600, rated median at the 62nd percentile. The 9 below-floor exclusions are the extreme tail (median 117 m, 3 above-horizon clamps), so the 94% generalizes to the 18-50 m far-silent core, not the deep tail. - The decisive readout needs no reviewer: matched-size detection rate is median 0.57 (q1 0.31, q3 0.74) - the model detects most OTHER far-field ramps at the same apparent size as its silent misses. Band recall declines 0.777 / 0.549 / 0.292 (18-25 / 25-40 / 40-150 m), never cliffs to zero. Far-field failure is graded sensitivity, not a floor. - docs/curb_ramp_data_sourcing.md: new section 0c with the tables and what it does and does not change (0.013 deliberately NOT revised - Phases 1-3 first); section 0a's "fixable by" binary qualified in place; two stale 0b claims fixed (the reviewer pass IS done; the point estimate exists). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017KCSfZm3MjoJ62LVtkvNeb --- .gitignore | 1 + analysis_out/farfield_forensics.json | 186 ++++++++++++ docs/curb_ramp_data_sourcing.md | 117 +++++++- scripts/analysis/farfield_forensics.py | 378 +++++++++++++++++++++++++ tests/test_farfield_forensics.py | 197 +++++++++++++ 5 files changed, 876 insertions(+), 3 deletions(-) create mode 100644 analysis_out/farfield_forensics.json create mode 100644 scripts/analysis/farfield_forensics.py create mode 100644 tests/test_farfield_forensics.py diff --git a/.gitignore b/.gitignore index 1c0094a..dfa74d6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ analysis_out/op/* # Which silent misses another model witnessed (#46). Small, and it carries the # unwitnessed list that is the gallery's actual work queue. !analysis_out/silent_witness.json +!analysis_out/farfield_forensics.json # Generated tagging pages live next to their (committed) galleries; regenerate with # scripts/analysis/make_tagger.py rather than tracking a build artifact. diff --git a/analysis_out/farfield_forensics.json b/analysis_out/farfield_forensics.json new file mode 100644 index 0000000..c9cea5c --- /dev/null +++ b/analysis_out/farfield_forensics.json @@ -0,0 +1,186 @@ +{ + "threshold": 0.3, + "boundary_m": 18.0, + "judgeable_source_px": 30.0, + "match_tolerance": 0.2, + "counts": { + "far_gt": 700, + "far_hits": 453, + "far_silent": 83, + "unwitnessed_far": 46, + "rated": 37, + "below_floor": 9 + }, + "verdicts": { + "visible": 34, + "context-only": 2, + "unclear": 1 + }, + "per_split": { + "richmond": { + "far_silent": 13, + "unwitnessed": 5, + "rated": 4, + "stored_width_min": 4096, + "stored_width_max": 12288 + }, + "bend": { + "far_silent": 14, + "unwitnessed": 9, + "rated": 9, + "stored_width_min": 13312, + "stored_width_max": 16384 + }, + "clovis": { + "far_silent": 9, + "unwitnessed": 4, + "rated": 3, + "stored_width_min": 5760, + "stored_width_max": 5760 + }, + "morgantown": { + "far_silent": 6, + "unwitnessed": 2, + "rated": 1, + "stored_width_min": 4096, + "stored_width_max": 4096 + }, + "annapolis": { + "far_silent": 14, + "unwitnessed": 8, + "rated": 2, + "stored_width_min": 8000, + "stored_width_max": 8000 + }, + "paterson": { + "far_silent": 7, + "unwitnessed": 4, + "rated": 4, + "stored_width_min": 16384, + "stored_width_max": 16384 + }, + "gainesville": { + "far_silent": 20, + "unwitnessed": 14, + "rated": 14, + "stored_width_min": 16384, + "stored_width_max": 16384 + } + }, + "populations": { + "rated (reached the deck)": { + "n": 37, + "dist_q1_med_q3": [ + 23.593050428658366, + 27.600046715707496, + 39.39802964423609 + ], + "px_q1_med_q3": [ + 19.855774091985104, + 28.34337145668962, + 33.157152723882376 + ] + }, + "below the floor (excluded)": { + "n": 9, + "dist_q1_med_q3": [ + 55.415367641067064, + 116.69577442978571, + 150.0 + ], + "px_q1_med_q3": [ + 5.215189175235227, + 6.703570717172544, + 14.116632435829143 + ] + }, + "witnessed (never queued)": { + "n": 37, + "dist_q1_med_q3": [ + 23.35001330569262, + 29.546807172079987, + 36.30290837799241 + ], + "px_q1_med_q3": [ + 21.548642002455033, + 26.47590217546387, + 33.50226683145265 + ] + }, + "ALL far-field silent misses": { + "n": 83, + "dist_q1_med_q3": [ + 23.641568560526633, + 31.24504274559346, + 42.60726425142641 + ], + "px_q1_med_q3": [ + 18.360211340231608, + 25.03687969495929, + 33.08910634599019 + ] + }, + "far-field hits (for contrast)": { + "n": 453, + "dist_q1_med_q3": [ + 20.269464509192268, + 21.346794177516905, + 33.89167310588107 + ], + "px_q1_med_q3": [ + 23.081727887595456, + 36.646175991578325, + 38.59393403957555 + ] + } + }, + "auc_rated_vs_unrated_px": 0.600470035252644, + "rated_median_px_percentile": 0.6204819277108434, + "auc_hit_vs_silent_px": 0.7181706960291497, + "bands": { + "18-25 m": { + "n_gt": 395, + "recall": 0.7772151898734178, + "silent": 25, + "rated": 14, + "visible": 12 + }, + "25-40 m": { + "n_gt": 226, + "recall": 0.5486725663716814, + "silent": 32, + "rated": 14, + "visible": 13 + }, + "40-150 m": { + "n_gt": 72, + "recall": 0.2916666666666667, + "silent": 22, + "rated": 9, + "visible": 9 + }, + "clamp>=150": { + "n_gt": 5, + "recall": 0.2, + "silent": 3, + "rated": 0, + "visible": 0 + } + }, + "matched_size_detection_rate_q1_med_q3": [ + 0.3076923076923077, + 0.5700934579439252, + 0.7402912621359223 + ], + "above_horizon": { + "rated (reached the deck)": 0, + "below the floor (excluded)": 3, + "witnessed (never queued)": 0, + "ALL far-field silent misses": 3, + "far-field hits (for contrast)": 1 + }, + "rated_by_tier": { + "mapillary": 10, + "gsv": 27 + } +} \ No newline at end of file diff --git a/docs/curb_ramp_data_sourcing.md b/docs/curb_ramp_data_sourcing.md index 0289546..0a5d88d 100644 --- a/docs/curb_ramp_data_sourcing.md +++ b/docs/curb_ramp_data_sourcing.md @@ -98,6 +98,11 @@ benchmark keeps surfacing (Paterson's paired tactile surfaces, Gainesville's dia ramps) plausibly are fixable that way. Those are two different populations with two different programmes attached, and nobody had sized them. +> **Since qualified by §0c.** The split below stands as a measurement, but its "fixable by" +> column's hard binary does not: the model detects other far-field ramps of the *same apparent +> size* as its silent misses at a median 57% rate, so far-field failure is graded sensitivity, +> not a floor. Read the far/near boundary as a difficulty gradient, not a reachability partition. + Script: `scripts/analysis/miss_decomposition.py` (15 tests). Reads the committed low-floor caches, so no GPU, no network, no imagery. Threshold 0.30 (the #79 recommendation); boundary 18 m, the last distance at which the model still has adequate signal. @@ -287,8 +292,9 @@ what the gallery is for. - **`silent` is still an upper bound**, now with a floor under it. It means the cached detections witness nothing there. Occlusion, deep shadow, debris and GT disagreement all still live inside - the unwitnessed remainder, and separating them needs the imagery — that is #46's gallery half, - **the crops are built but the reviewer pass is not done**. + the unwitnessed remainder, and separating them needs the imagery — that is #46's gallery half. + **The reviewer pass is now done** (one rater, no second — `docs/replication.md` §"What the first + pass produced"), and its far-field verdicts raised their own question: **§0c**. - **The witness test is one-directional.** A witnessed ramp is confirmed recognizable; an unwitnessed one is not confirmed *un*recognizable, since every challenger is weaker than RampNet on this task and may simply have missed it too. @@ -320,7 +326,10 @@ sourcing-addressable: | `unclear` | cannot tell from this imagery | excluded from every rate | **The `visible` rate over those 50, applied to the 59, is what converts the bracket into a point -estimate.** Until it is run, quote the bracket. +estimate.** It has been run (2026-07-31, one rater): near-field `visible` 7 of 13, which puts the +sourcing-addressable population at **~0.013 recall points** (~19 chance-corrected witnessed + 7 +visible, against 2,060 pooled GT — `docs/replication.md`). Single-rater caveat applies, and the +**far-field** verdicts from the same pass raised the question §0c takes up. - **Some of `merged` may be double-marked GT.** 24 of 124 pairs sit below 8 px (~25 cm at 10 m), which is not a physical spacing for two ramps; on the verdict splits that is plausibly one ramp marked twice. If so they are *spurious GT* and leave the population entirely rather than changing @@ -330,6 +339,108 @@ estimate.** Until it is run, quote the bracket. - **`sub_threshold` is not free recall.** Those ramps are recoverable by lowering the threshold, which #54/#55 already evaluated and priced in precision; 0.30 was chosen knowing it. +## 0c. The far-field `visible` anomaly: the pixel floor does not survive its own hits + +The reviewer pass produced a result §0a's framing did not predict. Of the **37 far-field** +silent-miss crops: **34 `visible`, 2 `context-only`, 1 `unclear`** — a 94% visible rate over +rateable crops, with **zero** `occluded` and **zero** `lighting` verdicts. Three facts sharpen it: + +- the rubric licenses `visible` only on the **model-resolution panel** (`benchmark/RUBRICS.md`), + so this is not the reviewer spending the 4× stored pixels the model never received; +- every rated crop is **unwitnessed** — none of the 8 challenger models put anything in radius + either; +- the deepest crops (40–150 m, down to **10.5 model px**) were rated visible **9 of 9**. + +At face value: ramps resolvable at the model's own pixel budget, invisible to all eight models — +against the reading that far-field misses are pixel-starved and unreachable by any training-side +fix. The four-hypothesis study design is on #46 (2026-07-31); this section is **Phase 0**: check +the *sample* (the rated 37 passed two selection filters) and check the framing against the model's +own far-field behaviour, before the verdicts are allowed to mean anything. + +Script: `scripts/analysis/farfield_forensics.py` (21 tests); result JSON +`analysis_out/farfield_forensics.json`. Committed inputs only — the low-floor caches, the witness +list, the gallery manifest and verdicts, and the imagery manifests' `width` fields. No GPU, no +network, no imagery. + +### The sample: survivorship is real, mild, and now quantified + +The 83 far-field silent misses reduce to 37 rated through two filters — **witnessed** (37, +already explained by another model's detection) and the **30-source-pixel judgeability floor** +(9). The floor is not one floor: stored panoramas run 4096–16384 px wide while `geom()` sizes +ramps at the model's 4096-px input, so 30 source px is a different model-pixel cut per split: + +| split | tier | stored px | floor (model px) | far-silent | unwitnessed | rated | +| :--- | :--- | ---: | ---: | ---: | ---: | ---: | +| richmond | mapillary | 4096–12288 | 10.0–30.0 | 13 | 5 | 4 | +| bend | gsv | 13312–16384 | 7.5–9.2 | 14 | 9 | 9 | +| clovis | mapillary | 5760 | 21.3 | 9 | 4 | 3 | +| morgantown | mapillary | 4096 | 30.0 | 6 | 2 | 1 | +| annapolis | mapillary | 8000 | 15.4 | 14 | 8 | 2 | +| paterson | gsv | 16384 | 7.5 | 7 | 4 | 4 | +| gainesville | gsv | 16384 | 7.5 | 20 | 14 | 14 | + +Which split a miss happened in decides whether a reviewer ever saw it — the 16384-px GSV splits +admit far misses down to 7.5 model px while morgantown stops at 30, and the deck comes out +**27 GSV / 10 Mapillary**. + +| population | n | dist q1/med/q3 (m) | px q1/med/q3 | +| :--- | ---: | :---: | :---: | +| rated (reached the deck) | 37 | 23.6 / 27.6 / 39.4 | 19.9 / 28.3 / 33.2 | +| below the floor (excluded) | 9 | 55.4 / 116.7 / 150.0 | 5.2 / 6.7 / 14.1 | +| witnessed (never queued) | 37 | 23.4 / 29.5 / 36.3 | 21.5 / 26.5 / 33.5 | +| **all far-field silent misses** | **83** | 23.6 / 31.2 / 42.6 | 18.4 / 25.0 / 33.1 | +| far-field hits, for contrast | 453 | 20.3 / 21.3 / 33.9 | 23.1 / 36.6 / 38.6 | + +- **AUC(rated px vs unrated far-silent px) = 0.600**; the rated median sits at the **62nd + percentile** of the far-silent size distribution. A bias toward bigger/closer exists and is mild. +- The 9 excluded items are the **extreme tail** — median 117 m, three of them above-horizon clamps + (i.e. not distances at all). So the 94% generalizes to the far-silent *core* (~18–50 m); it says + nothing about the deep tail, which is exactly where pixel starvation is most plausible. +- Mis-binning guards: **zero** above-horizon clamps among the rated 37, and the deck is majority + GSV — the tier where flat-ground distance is trustworthy (Spearman 0.95 vs 0.81). + +### The framing: the model's own hits refute a hard pixel floor + +The decisive check needs no reviewer at all. If far-field silence were pixel-starvation, the model +should not be detecting *other* ramps at the same apparent size. It is: + +| band | GT | recall | silent misses | rated | rated `visible` | +| :--- | ---: | ---: | ---: | ---: | ---: | +| 18–25 m | 395 | 0.777 | 25 | 14 | 12 | +| 25–40 m | 226 | 0.549 | 32 | 14 | 13 | +| 40–150 m | 72 | 0.292 | 22 | 9 | 9 | +| clamp ≥ 150 m | 5 | 0.200 | 3 | 0 | 0 | + +- **Matched-size detection rate**: for each `visible` miss, the model's recall over all far-field + GT within ±20% of that miss's apparent size is **median 0.57** (q1 0.31, q3 0.74). A hard pixel + floor would put these near zero. +- **AUC(far-hit px vs far-silent px) = 0.718** — size matters, but it is far from deciding. +- Recall declines **0.777 → 0.549 → 0.292** across the bands. Even at 40–150 m the model finds + roughly 3 in 10 (and the pooled 25–40 m rate agrees with E1's gold-set 0.49 at the same range). + +**Far-field failure is graded sensitivity, not a cliff.** A silent far-field miss is not a ramp +below a physical detection floor — it is the unlucky tail of a process that succeeds on most +same-sized ramps. That is consistent with `docs/detection_recall_analysis.md`'s sensitivity +finding and with the human verdicts, and inconsistent with reading "more examples do not add +pixels" as a claim about *reachability*. (As a claim about pixels it remains true; the error was +inferring unreachability from it.) + +### What changes, what does not, and what is still open + +- **§0a's measured split stands** (247 far / 180 near at 18 m). What falls is the hard binary in + its "fixable by" column: the far field is *harder*, not *unreachable*. +- **The sourcing bracket (§0b) excluded all 83 far-field silent misses from the addressable + population because of that binary.** That exclusion is no longer safe. The 0.013 point estimate + is deliberately **not revised** here: whether these specific misses are recoverable is what + Phases 1–3 of the #46 study measure (heatmap activation forensics; the scale counterfactual; + the decoy control on the verdicts themselves). Quote 0.013 with this section attached. +- **Multi-view's remedy logic is untouched** — a ramp invisible at 30 m is at 8 m two panoramas + later whatever the failure mechanism — but §0a's "MV ceiling" column shares the binary + assumption and will move with the same phases. +- **The human-side caveat is live.** One rater; and the 9-of-9 `visible` rate in the deepest band + (down to 10.5 model px) is where pointed-verification bias would show most strongly. Phase 3's + decoy deck should therefore be **stratified by distance band**, oversampling 40–150 m. + ## 1. The current training corpus is mostly one city Stage 1 is built from three cities' open-government inventories (`docs/data_provenance.md` §1). diff --git a/scripts/analysis/farfield_forensics.py b/scripts/analysis/farfield_forensics.py new file mode 100644 index 0000000..788816e --- /dev/null +++ b/scripts/analysis/farfield_forensics.py @@ -0,0 +1,378 @@ +"""Phase 0 of the far-field `visible` anomaly study: is the rated sample representative? (#46) + +The reviewer pass over the silent misses (``benchmark/miss_taxonomy_46/silent__jonf.json``) +rated **34 of 36** rateable far-field crops ``visible`` — the ramp's own pixels present +and carrying its appearance *in the model-resolution panel*. At face value that +contradicts the pixel-starvation framing E1 attached to the far field +(``docs/curb_ramp_data_sourcing.md`` §0a: "a 1.2 m ramp at 30 m is ~25 px — more +examples do not add pixels"), which is the assumption the 18 m far/near split — and +through it the sourcing bracket and the multi-view sizing — stands on. + +Before that contradiction is allowed to mean anything, the sample has to be checked +(#46, hypothesis H1): the 37 rated far-field crops are not a random draw from the 83 +far-field silent misses. They passed two filters — **unwitnessed** (no other model +detected anything there either) and the **30-source-pixel judgeability floor** — and +the floor bites at a different apparent size on every split, because the stored +panoramas range from 4096 to 16384 px wide while ``geom()`` sizes ramps at the model's +4096-px input. On a 16384-px split the floor admits ramps down to **7.5 model px**; on +morgantown it stops at **30**. If the rated crops are the biggest and closest of the +far field, a 94% visible rate there says little about the population. + +This measures that, from committed data alone: + +* **the two filters' effect on the sample**, split by split — which populations could + even reach the deck, and where the rated 37 sit in the far-silent size distribution; +* **whether apparent size discriminates far-field hits from far-field silent misses** + at all — if the model detects other ramps of the *same* apparent size at a healthy + rate, a hard pixel floor cannot be what makes these particular ramps silent; +* **the mis-binning guards**: above-horizon clamps (``geom()`` sends y <= 0.5 straight + to 150 m) and the GSV/Mapillary tier split, since the flat-ground distance estimate + is weaker on Mapillary rigs (Spearman 0.81 vs 0.95, §0a). + +Everything is read from committed files (``analysis_out/op_cache``, +``analysis_out/silent_witness.json``, ``benchmark/miss_taxonomy_46/``, +``benchmark//imagery_manifest.json``): no GPU, no network, no imagery. + + python scripts/analysis/farfield_forensics.py + python scripts/analysis/farfield_forensics.py --json-out analysis_out/farfield_forensics.json +""" +import argparse +import json +import os +import sys + +REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.environ.get("RAMPNET_ANALYSIS_OUT", os.path.join(REPO, "analysis_out")) +sys.path.insert(0, REPO) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import miss_taxonomy as mt # noqa: E402 +from miss_decomposition import ( # noqa: E402 + DEFAULT_THRESHOLD, FAR_BOUNDARY_M, TIER, US_SPLITS) +from miss_gallery import ( # noqa: E402 + JUDGEABLE_SOURCE_PX, MODEL_WIDTH, load_queue, source_px, tag_key) + +# Distance bands inside the far field, in metres. The first two match E1's gold-set +# bins (recall 0.90 at 18-25, 0.49 at 25-40), so the pooled-benchmark rates here are +# directly comparable to the numbers the pixel-starvation framing was built on. +# ``geom()`` clamps above-horizon points to 150 m, so the clamp is its own band — +# those distances are not measurements, they are the estimator giving up. +FAR_BANDS = ((18.0, 25.0), (25.0, 40.0), (40.0, 150.0)) + +# Tolerance for "same apparent size" when asking how often the model detects OTHER +# far-field ramps of a rated miss's size. +/-20% of the target px is ~ +/-20% of +# distance (px is 1/d), comfortably inside one of E1's bins. +MATCH_TOL = 0.20 + + +# --------------------------------------------------------------------------- # +# Pure core (no I/O) — unit-tested in tests/test_farfield_forensics.py +# --------------------------------------------------------------------------- # +def auc(a, b): + """P(a random draw from ``a`` exceeds one from ``b``), ties counting half. + + The Mann-Whitney statistic scaled to [0, 1]: 0.5 means the two samples are + indistinguishable on this variable, 1.0 means every ``a`` exceeds every ``b``. + O(n*m); the populations here are dozens-to-hundreds, so clarity wins. + """ + if not a or not b: + return float("nan") + wins = ties = 0 + for x in a: + for y in b: + if x > y: + wins += 1 + elif x == y: + ties += 1 + return (wins + 0.5 * ties) / (len(a) * len(b)) + + +def quartiles(values): + """``(q1, median, q3)`` by simple index — enough for reporting, no interpolation.""" + if not values: + return (float("nan"),) * 3 + v = sorted(values) + n = len(v) + return v[n // 4], v[n // 2], v[(3 * n) // 4] + + +def effective_floor_model_px(source_width, floor=JUDGEABLE_SOURCE_PX, + model_width=MODEL_WIDTH): + """The judgeability floor translated into MODEL pixels for one stored width. + + ``source_px = model_px * source_width / model_width``, so a floor fixed in + source pixels admits smaller model-pixel ramps the wider the stored pano is. + This asymmetry is what shapes the rated deck's composition across splits. + """ + return floor * model_width / source_width + + +def band_of(row, bands=FAR_BANDS): + """The far band a row falls in, ``'clamp'`` for above-horizon rows, else None. + + The clamp test is on ``y``, mirroring ``miss_decomposition.above_horizon`` — + a ground ramp cannot sit at or above the horizon, so its 150 m is an artifact + of an unleveled rig or a hill, not a distance. + """ + if row["y"] <= 0.5: + return "clamp" + for lo, hi in bands: + if lo <= row["dist"] < hi: + return (lo, hi) + return None + + +def matched_rate(rows, px, tol=MATCH_TOL): + """``(hits, n)`` among ``rows`` whose apparent size is within ``tol`` of ``px``. + + The counterfactual the floor question needs: of every far-field GT ramp the + benchmark scores at (about) this apparent size, how many did the model find? + """ + sel = [r for r in rows if abs(r["px"] - px) <= tol * px] + return sum(1 for r in sel if r["hit"]), len(sel) + + +def percentile_rank(population, x): + """Fraction of ``population`` strictly below ``x``, ties counting half.""" + if not population: + return float("nan") + below = sum(1 for v in population if v < x) + ties = sum(1 for v in population if v == x) + return (below + 0.5 * ties) / len(population) + + +def row_key(row): + """The identity a row shares with the witness list and the gallery manifest.""" + return (row["city"], row["pano"], round(float(row["x"]), 6), + round(float(row["y"]), 6)) + + +# --------------------------------------------------------------------------- # +# I/O +# --------------------------------------------------------------------------- # +def stored_widths(city): + """``{pano_id: width}`` from the committed imagery manifest, or ``{}``. + + The manifest was committed to pin the imagery by content hash (#94); its + ``width`` field is what lets this script translate the source-pixel floor + into model pixels without touching a single image. + """ + path = os.path.join(REPO, "benchmark", city, "imagery_manifest.json") + if not os.path.exists(path): + return {} + with open(path, encoding="utf-8") as fh: + payload = json.load(fh) + return {pid: rec["width"] for pid, rec in payload.get("panos", {}).items() + if rec.get("width")} + + +def load_rated(gallery_dir): + """The reviewer's far-field items: manifest entry + verdict, keyed like the tags.""" + with open(os.path.join(gallery_dir, "silent_gallery", "manifest.json"), + encoding="utf-8") as fh: + manifest = json.load(fh) + with open(os.path.join(gallery_dir, "silent__jonf.json"), encoding="utf-8") as fh: + verdicts = json.load(fh)["verdicts"] + out = {} + for key, item in manifest["items"].items(): + if item.get("field") != "far": + continue + v = verdicts.get(key) + out[key] = {**item, "verdict": v if isinstance(v, str) + else (v or {}).get("verdict")} + return out + + +def main(argv=None): + p = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + p.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD) + p.add_argument("--witness", default=os.path.join(OUT, "silent_witness.json")) + p.add_argument("--gallery", default=os.path.join(REPO, "benchmark", + "miss_taxonomy_46")) + p.add_argument("--json-out", default=None) + args = p.parse_args(argv) + + # Populations, all at the deployed threshold. -------------------------------- + pooled = [] + for city in US_SPLITS: + loaded = mt.load_rows(city, args.threshold, rng=None) + if loaded is not None: + pooled.extend(loaded[0]) + far = [r for r in pooled if r["field"] == "far"] + far_hits = [r for r in far if r["hit"]] + far_silent = [r for r in far if not r["hit"] and r["bucket"] == "silent"] + + queue = load_queue(args.witness) + unw_far = [r for r in far_silent if row_key(r) in queue] + + rated = load_rated(args.gallery) + rated_keys = {(v["city"], v["pano"], round(float(v["x"]), 6), + round(float(v["y"]), 6)) for v in rated.values()} + rated_rows = [r for r in unw_far if row_key(r) in rated_keys] + excluded_rows = [r for r in unw_far if row_key(r) not in rated_keys] + + widths = {city: stored_widths(city) for city in US_SPLITS} + visible = [v for v in rated.values() if v["verdict"] == "visible"] + + print(f"=== Far-field 'visible' anomaly, Phase 0: sample forensics " + f"(threshold {args.threshold}, boundary {FAR_BOUNDARY_M:.0f} m, #46) ===\n") + print(f"far-field GT {len(far)}, hits {len(far_hits)} " + f"(recall {len(far_hits)/len(far):.3f}), silent misses {len(far_silent)}; " + f"unwitnessed {len(unw_far)}, rated {len(rated_rows)}, " + f"below the floor {len(excluded_rows)}") + tally = {} + for v in rated.values(): + tally[v["verdict"]] = tally.get(v["verdict"], 0) + 1 + print(f"reviewer verdicts over the rated set: " + + ", ".join(f"{k} {n}" for k, n in sorted(tally.items(), + key=lambda kv: -kv[1]))) + + # 1. The floor, split by split. ---------------------------------------------- + print(f"\n{'-'*78}\n1. THE FLOOR IS NOT ONE FLOOR — {JUDGEABLE_SOURCE_PX:.0f} " + f"source px in model pixels, per split\n{'-'*78}") + print(f"{'split':>12} {'tier':>10} {'stored px':>12} {'floor(model px)':>16} " + f"{'far-silent':>11} {'unwitn.':>8} {'rated':>6}") + per_split = {} + for city in US_SPLITS: + fs = [r for r in far_silent if r["city"] == city] + if not fs: + continue + ws = sorted({widths[city].get(r["pano"]) for r in fs + if widths[city].get(r["pano"])}) + w_lo, w_hi = (ws[0], ws[-1]) if ws else (None, None) + floor_lo = effective_floor_model_px(w_hi) if w_hi else float("nan") + floor_hi = effective_floor_model_px(w_lo) if w_lo else float("nan") + floor_s = (f"{floor_lo:.1f}" if w_lo == w_hi else + f"{floor_lo:.1f}-{floor_hi:.1f}") + n_unw = sum(1 for r in unw_far if r["city"] == city) + n_rated = sum(1 for r in rated_rows if r["city"] == city) + stored_s = (f"{w_lo}" if w_lo == w_hi else f"{w_lo}-{w_hi}") if ws else "?" + print(f"{city:>12} {TIER.get(city, '-'):>10} {stored_s:>12} {floor_s:>16} " + f"{len(fs):>11} {n_unw:>8} {n_rated:>6}") + per_split[city] = {"far_silent": len(fs), "unwitnessed": n_unw, + "rated": n_rated, "stored_width_min": w_lo, + "stored_width_max": w_hi} + print("\n The deck's composition follows the floor: the 16384-px splits admit") + print(" far misses down to 7.5 model px, morgantown stops at 30. Which split a") + print(" miss happened in decides whether a reviewer ever saw it.") + + # 2. Where the rated set sits in the far-silent population. ------------------- + print(f"\n{'-'*78}\n2. SURVIVORSHIP — where the rated {len(rated_rows)} sit " + f"among all {len(far_silent)} far-field silent misses\n{'-'*78}") + print(f"{'population':>34} {'n':>4} {'dist q1/med/q3 (m)':>20} " + f"{'px q1/med/q3':>15}") + pops = { + "rated (reached the deck)": rated_rows, + "below the floor (excluded)": excluded_rows, + "witnessed (never queued)": [r for r in far_silent + if row_key(r) not in queue], + "ALL far-field silent misses": far_silent, + "far-field hits (for contrast)": far_hits, + } + stats = {} + for name, rows in pops.items(): + dq = quartiles([r["dist"] for r in rows]) + pq = quartiles([r["px"] for r in rows]) + stats[name] = {"n": len(rows), "dist_q": dq, "px_q": pq} + print(f"{name:>34} {len(rows):>4} " + f"{dq[0]:>6.1f}/{dq[1]:>5.1f}/{dq[2]:>5.1f} " + f"{pq[0]:>5.1f}/{pq[1]:>4.1f}/{pq[2]:>4.1f}") + auc_rated = auc([r["px"] for r in rated_rows], + [r["px"] for r in far_silent + if row_key(r) not in rated_keys]) + med_rank = percentile_rank([r["px"] for r in far_silent], + quartiles([r["px"] for r in rated_rows])[1]) + print(f"\n AUC(rated px vs unrated far-silent px) = {auc_rated:.3f} " + f"(0.5 = no size bias)") + print(f" the rated median px sits at the {med_rank:.0%} percentile of the " + f"far-silent population") + + # 3. Does apparent size even separate hit from silent out here? --------------- + print(f"\n{'-'*78}\n3. DISCRIMINATION — is a far-field silent miss the size " + f"the model cannot see?\n{'-'*78}") + auc_hit = auc([r["px"] for r in far_hits], [r["px"] for r in far_silent]) + print(f" AUC(far-hit px vs far-silent px) = {auc_hit:.3f} " + f"(1.0 would mean size alone decides)\n") + print(f"{'band':>12} {'GT':>6} {'recall':>8} {'silent':>8} {'rated':>6} " + f"{'visible':>8}") + band_rows = {} + key_of = {tag_key(v["pano"], v["x"], v["y"]): v for v in rated.values()} + for band in list(FAR_BANDS) + ["clamp"]: + sel = [r for r in far if band_of(r) == band] + if not sel: + continue + n_hit = sum(1 for r in sel if r["hit"]) + n_sil = sum(1 for r in sel if not r["hit"] and r["bucket"] == "silent") + n_rated = sum(1 for r in rated_rows if band_of(r) == band) + n_vis = sum(1 for r in rated_rows if band_of(r) == band + and key_of.get(tag_key(r["pano"], r["x"], r["y"]), + {}).get("verdict") == "visible") + label = "clamp>=150" if band == "clamp" else f"{band[0]:.0f}-{band[1]:.0f} m" + print(f"{label:>12} {len(sel):>6} {n_hit/len(sel):>8.3f} {n_sil:>8} " + f"{n_rated:>6} {n_vis:>8}") + band_rows[label] = {"n_gt": len(sel), "recall": n_hit / len(sel), + "silent": n_sil, "rated": n_rated, "visible": n_vis} + + rates = [] + for v in visible: + h, n = matched_rate(far, v["model_px"]) + if n: + rates.append(h / n) + rq = quartiles(rates) + print(f"\n MATCHED-SIZE DETECTION RATE: for each of the {len(visible)} " + f"'visible' misses, the model's") + print(f" recall over ALL far-field GT within ±{MATCH_TOL:.0%} of that miss's " + f"apparent size:") + print(f" q1/median/q3 = {rq[0]:.2f} / {rq[1]:.2f} / {rq[2]:.2f}") + print(f" A hard pixel floor would put these near zero. The model routinely") + print(f" detects other ramps of the same apparent size; silence at that size is") + print(f" therefore not size-fated, and 'pixel-starved' is not a sufficient") + print(f" explanation for these misses.") + + # 4. Mis-binning guards. ------------------------------------------------------ + print(f"\n{'-'*78}\n4. GUARDS — how much of this could the distance estimator " + f"be inventing?\n{'-'*78}") + n_clamp = {name: sum(1 for r in rows if r["y"] <= 0.5) + for name, rows in pops.items()} + print(f" above-horizon clamps: rated {n_clamp['rated (reached the deck)']}, " + f"excluded {n_clamp['below the floor (excluded)']}, " + f"all far-silent {n_clamp['ALL far-field silent misses']}") + tiers = {} + for r in rated_rows: + tiers[TIER.get(r["city"], "-")] = tiers.get(TIER.get(r["city"], "-"), 0) + 1 + print(f" rated set by tier: " + + ", ".join(f"{k} {n}" for k, n in sorted(tiers.items())) + + " (flat-ground distance is Spearman 0.95 on gsv, 0.81 on mapillary)") + + if args.json_out: + os.makedirs(os.path.dirname(os.path.abspath(args.json_out)), exist_ok=True) + payload = { + "threshold": args.threshold, "boundary_m": FAR_BOUNDARY_M, + "judgeable_source_px": JUDGEABLE_SOURCE_PX, + "match_tolerance": MATCH_TOL, + "counts": {"far_gt": len(far), "far_hits": len(far_hits), + "far_silent": len(far_silent), + "unwitnessed_far": len(unw_far), + "rated": len(rated_rows), + "below_floor": len(excluded_rows)}, + "verdicts": tally, + "per_split": per_split, + "populations": {name: {"n": s["n"], "dist_q1_med_q3": s["dist_q"], + "px_q1_med_q3": s["px_q"]} + for name, s in stats.items()}, + "auc_rated_vs_unrated_px": auc_rated, + "rated_median_px_percentile": med_rank, + "auc_hit_vs_silent_px": auc_hit, + "bands": band_rows, + "matched_size_detection_rate_q1_med_q3": rq, + "above_horizon": n_clamp, + "rated_by_tier": tiers, + } + with open(args.json_out, "w", encoding="utf-8") as fh: + json.dump(payload, fh, indent=2) + print(f"\nWrote {args.json_out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_farfield_forensics.py b/tests/test_farfield_forensics.py new file mode 100644 index 0000000..1d6b100 --- /dev/null +++ b/tests/test_farfield_forensics.py @@ -0,0 +1,197 @@ +"""Tests for the far-field anomaly's Phase 0 sample forensics (#46). + +Two layers, matching the script: + +* pure-function tests — the AUC, the per-split floor translation, the band + classifier (including the above-horizon clamp), and the matched-size rate, + since each one carries a headline claim; +* integration tests against the **committed** inputs (`analysis_out/op_cache`, + `analysis_out/silent_witness.json`, `benchmark/miss_taxonomy_46/`, + `benchmark//imagery_manifest.json`) pinning the population arithmetic the + write-up quotes: 83 far-field silent misses = 37 rated + 9 below the floor + + 37 witnessed. If a cache or manifest changes, these numbers must be re-derived, + not assumed — that is exactly the failure this file exists to catch. + +CPU-only, no network, no imagery, no GPU. +""" +import json +import os +import sys + +import pytest + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, REPO) +sys.path.insert(0, os.path.join(REPO, "scripts", "analysis")) + +import farfield_forensics as ff # noqa: E402 + + +# --------------------------------------------------------------------------- # +# auc — the survivorship and discrimination statistic +# --------------------------------------------------------------------------- # +def test_auc_is_half_for_identical_samples(): + assert ff.auc([1, 2, 3], [1, 2, 3]) == pytest.approx(0.5) + + +def test_auc_is_one_when_a_dominates(): + assert ff.auc([10, 11], [1, 2]) == 1.0 + + +def test_auc_is_zero_when_b_dominates(): + assert ff.auc([1, 2], [10, 11]) == 0.0 + + +def test_auc_counts_ties_half(): + # a = [1], b = [1]: one comparison, tied. + assert ff.auc([1], [1]) == pytest.approx(0.5) + + +def test_auc_empty_is_nan(): + assert ff.auc([], [1]) != ff.auc([], [1]) # NaN + + +# --------------------------------------------------------------------------- # +# effective_floor_model_px — the per-split floor translation +# --------------------------------------------------------------------------- # +def test_floor_on_a_16384_split_is_seven_and_a_half_model_px(): + assert ff.effective_floor_model_px(16384) == pytest.approx(7.5) + + +def test_floor_at_parity_is_the_floor_itself(): + assert ff.effective_floor_model_px(4096) == pytest.approx(30.0) + + +def test_floor_scales_inversely_with_stored_width(): + assert ff.effective_floor_model_px(8000) == pytest.approx(30.0 * 4096 / 8000) + + +# --------------------------------------------------------------------------- # +# band_of — the far bands and the clamp +# --------------------------------------------------------------------------- # +def _row(dist, y=0.7): + return {"dist": dist, "y": y} + + +def test_band_edges_are_half_open(): + assert ff.band_of(_row(18.0)) == (18.0, 25.0) + assert ff.band_of(_row(25.0)) == (25.0, 40.0) + assert ff.band_of(_row(40.0)) == (40.0, 150.0) + + +def test_above_horizon_is_the_clamp_band_regardless_of_distance(): + # geom() sends y <= 0.5 to 150 m; the y is the tell, not the distance. + assert ff.band_of(_row(150.0, y=0.5)) == "clamp" + assert ff.band_of(_row(150.0, y=0.4)) == "clamp" + + +def test_near_field_rows_fall_in_no_band(): + assert ff.band_of(_row(10.0)) is None + + +# --------------------------------------------------------------------------- # +# matched_rate — "how often does the model find OTHER ramps this size?" +# --------------------------------------------------------------------------- # +def _sized(px, hit): + return {"px": px, "hit": hit} + + +def test_matched_rate_counts_only_rows_within_tolerance(): + rows = [_sized(20, True), _sized(24, False), _sized(50, True)] + hits, n = ff.matched_rate(rows, 20.0, tol=0.20) + assert (hits, n) == (1, 2) # the 50-px row is out of band + + +def test_matched_rate_tolerance_is_symmetric_and_inclusive(): + rows = [_sized(16.0, True), _sized(24.0, False)] + hits, n = ff.matched_rate(rows, 20.0, tol=0.20) + assert (hits, n) == (1, 2) + + +def test_matched_rate_with_no_neighbours_is_zero_of_zero(): + assert ff.matched_rate([_sized(100, True)], 20.0) == (0, 0) + + +# --------------------------------------------------------------------------- # +# percentile_rank / quartiles +# --------------------------------------------------------------------------- # +def test_percentile_rank_midpoint(): + assert ff.percentile_rank([1, 2, 3, 4], 2.5) == pytest.approx(0.5) + + +def test_percentile_rank_ties_count_half(): + assert ff.percentile_rank([1, 2, 2, 3], 2) == pytest.approx(0.5) + + +def test_quartiles_are_ordered(): + q1, med, q3 = ff.quartiles(list(range(100))) + assert q1 < med < q3 + + +# --------------------------------------------------------------------------- # +# Integration — the committed populations the write-up quotes +# --------------------------------------------------------------------------- # +WITNESS = os.path.join(REPO, "analysis_out", "silent_witness.json") +GALLERY = os.path.join(REPO, "benchmark", "miss_taxonomy_46") + +needs_committed = pytest.mark.skipif( + not (os.path.exists(WITNESS) + and os.path.exists(os.path.join(GALLERY, "silent__jonf.json"))), + reason="committed witness/gallery files not present") + + +@pytest.fixture(scope="module") +def far_populations(): + import miss_taxonomy as mt + from miss_decomposition import DEFAULT_THRESHOLD, US_SPLITS + pooled = [] + for city in US_SPLITS: + loaded = mt.load_rows(city, DEFAULT_THRESHOLD, rng=None) + if loaded is not None: + pooled.extend(loaded[0]) + far_silent = [r for r in pooled if r["field"] == "far" and not r["hit"] + and r["bucket"] == "silent"] + from miss_gallery import load_queue + queue = load_queue(WITNESS) + unw = [r for r in far_silent if ff.row_key(r) in queue] + rated = ff.load_rated(GALLERY) + rated_keys = {(v["city"], v["pano"], round(float(v["x"]), 6), + round(float(v["y"]), 6)) for v in rated.values()} + return far_silent, unw, rated, rated_keys + + +@needs_committed +def test_the_population_arithmetic_the_writeup_quotes(far_populations): + far_silent, unw, rated, rated_keys = far_populations + assert len(far_silent) == 83 + assert len(unw) == 46 + assert len(rated) == 37 + below = [r for r in unw if ff.row_key(r) not in rated_keys] + assert len(below) == 9 + + +@needs_committed +def test_every_rated_item_is_an_unwitnessed_far_silent_miss(far_populations): + far_silent, unw, rated, rated_keys = far_populations + unw_keys = {ff.row_key(r) for r in unw} + assert rated_keys <= unw_keys + + +@needs_committed +def test_the_far_verdict_tally_matches_the_committed_pass(far_populations): + _, _, rated, _ = far_populations + tally = {} + for v in rated.values(): + tally[v["verdict"]] = tally.get(v["verdict"], 0) + 1 + assert tally == {"visible": 34, "context-only": 2, "unclear": 1} + + +@needs_committed +def test_imagery_manifests_cover_every_rated_pano(): + # The floor table translates the source-px floor through the committed widths; + # a rated pano missing from its manifest would silently drop from that table. + rated = ff.load_rated(GALLERY) + for v in rated.values(): + widths = ff.stored_widths(v["city"]) + assert v["pano"] in widths, (v["city"], v["pano"]) + assert widths[v["pano"]] == v["source_width"] From 59d5a9eb2009c88167e5ce81ecc22feb981dc254 Mon Sep 17 00:00:00 2001 From: Jon Froehlich Date: Fri, 31 Jul 2026 12:44:21 -0700 Subject: [PATCH 2/4] Phase 1 instrument (#46): silent-miss activation forensics with a self-excluding azimuth null silent is a statement about PEAKS (nothing >= 0.05 via peak_local_max), not about the heatmap - 0.04 and 0.0001 are different failures. For all 128 pooled silent misses this reads the max heatmap value inside the match radius (the scaled matcher space IS the 512x1024 heatmap grid, asserted) and compares it to the pano's own elevation-band null. The null rejects draws whose window overlaps the site's (wrapped column distance < 2R): a draw reading the site's own faint bump back would contaminate exactly the sparse-heatmap case the analysis exists to detect - caught by the unit test, fixed before any numbers were produced. Model: the published projectsidewalk/rampnet-model weights via threshold_sweep.load_model() - the same checkpoint every committed cache came from; single-pass fp32, matching analysis_out/op_cache. farfield_forensics.load_rated grows a field=None mode so Phase 1 can join the near-field verdicts too. 10 new tests; suite 529. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017KCSfZm3MjoJ62LVtkvNeb --- scripts/analysis/farfield_forensics.py | 10 +- scripts/analysis/silent_activation.py | 266 +++++++++++++++++++++++++ tests/test_silent_activation.py | 114 +++++++++++ 3 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 scripts/analysis/silent_activation.py create mode 100644 tests/test_silent_activation.py diff --git a/scripts/analysis/farfield_forensics.py b/scripts/analysis/farfield_forensics.py index 788816e..7b843da 100644 --- a/scripts/analysis/farfield_forensics.py +++ b/scripts/analysis/farfield_forensics.py @@ -166,8 +166,12 @@ def stored_widths(city): if rec.get("width")} -def load_rated(gallery_dir): - """The reviewer's far-field items: manifest entry + verdict, keyed like the tags.""" +def load_rated(gallery_dir, field="far"): + """The reviewer's rated items: manifest entry + verdict, keyed like the tags. + + ``field`` restricts to one distance population; ``None`` returns all 50 + (Phase 1's activation forensics wants the near-field verdicts too). + """ with open(os.path.join(gallery_dir, "silent_gallery", "manifest.json"), encoding="utf-8") as fh: manifest = json.load(fh) @@ -175,7 +179,7 @@ def load_rated(gallery_dir): verdicts = json.load(fh)["verdicts"] out = {} for key, item in manifest["items"].items(): - if item.get("field") != "far": + if field is not None and item.get("field") != field: continue v = verdicts.get(key) out[key] = {**item, "verdict": v if isinstance(v, str) diff --git a/scripts/analysis/silent_activation.py b/scripts/analysis/silent_activation.py new file mode 100644 index 0000000..b1edc7d --- /dev/null +++ b/scripts/analysis/silent_activation.py @@ -0,0 +1,266 @@ +"""Phase 1 of the far-field `visible` anomaly study: attenuated, or absent? (#46) + +The ``silent`` bucket is defined by peak extraction: no ``peak_local_max`` peak at or +above the 0.05 score floor within the match radius. That is a statement about *peaks*, +not about the heatmap — 0.04 and 0.0001 are both "silent", and they are different +failures. If the model produces a real-but-faint localized response at these ramps, +the silent bucket is the tail of the same confidence continuum ``sub_threshold`` +lives on (a calibration/threshold/training story, gainesville's mechanism). If the +heatmap is flat at chance level, the model has no representation of the ramp at all — +a genuine vocabulary or scale gap, which is what Phase 2's scale counterfactual then +separates. + +For every pooled silent miss (near and far — the near-field verdicts feed the 0.013 +sourcing estimate just as directly), this loads the published model, runs one forward +pass per panorama, and reads: + +* ``act`` — the max heatmap value within the match radius of the missed ramp (the + radius and grid are exactly the matcher's: the scaled space *is* the 512x1024 + heatmap). Note ``act`` can exceed the 0.05 floor without contradicting ``silent`` + — a shoulder of a neighbouring peak is not a local maximum; those cases are + counted separately rather than silently pooled. +* a per-miss **null**: the same radius-max at ``NULL_TRIALS`` random azimuths in the + same panorama at the same elevation — the same null shape every #46 analysis uses, + because both ramps and heatmap mass crowd the horizon band. ``act`` is reported as + a percentile of its own panorama's null, so "there is signal here" is a claim + against that pano's actual noise floor, not against zero. + +Model: the published ``projectsidewalk/rampnet-model`` weights — the same checkpoint +every committed cache came from (``operating_point_curve.py`` extract). Single-pass, +no TTA, fp32, matching ``analysis_out/op_cache``. Needs the panorama imagery +(``benchmark//panos``, git-ignored — ``--panos-root`` from a worktree) and a +GPU-ish machine; the RTX 3070 does ~10 s/pano. + + python scripts/analysis/silent_activation.py --panos-root D:/Git/RampNet + python scripts/analysis/silent_activation.py --json-out analysis_out/silent_activation.json +""" +import argparse +import json +import os +import random +import sys + +REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.environ.get("RAMPNET_ANALYSIS_OUT", os.path.join(REPO, "analysis_out")) +sys.path.insert(0, REPO) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import miss_taxonomy as mt # noqa: E402 +from miss_decomposition import DEFAULT_THRESHOLD, US_SPLITS # noqa: E402 +from farfield_forensics import load_rated, quartiles, row_key # noqa: E402 +from rampnet.detection_eval import ( # noqa: E402 + PANO_SCALE_X, PANO_SCALE_Y, radius_sq_for) + +NULL_TRIALS = 200 +NULL_SEED = 20260731 + +# The heatmap grid IS the matcher's scaled space (PANO_SCALE_X x PANO_SCALE_Y = +# 1024 x 512), asserted at runtime so a future resolution change cannot silently +# desynchronize the two. +HEAT_W, HEAT_H = int(PANO_SCALE_X), int(PANO_SCALE_Y) + + +# --------------------------------------------------------------------------- # +# Pure core (no torch, no I/O) — unit-tested in tests/test_silent_activation.py +# --------------------------------------------------------------------------- # +def radius_max(heat, x, y, radius_sq=None): + """Max heatmap value within the match radius of normalized point ``(x, y)``. + + Columns wrap at the 360-degree seam (a radius crossing x=0 continues at x=1); + rows clamp — there is nothing above the top of a panorama. Values are clipped + to [0, 1] exactly as ``peaks_to_dets`` clips before peak extraction, so an + ``act`` here and a peak score there are on the same scale. + """ + if radius_sq is None: + radius_sq = radius_sq_for() + H, W = len(heat), len(heat[0]) + r = radius_sq ** 0.5 + cx, cy = x * W, y * H + best = 0.0 + for row in range(max(0, int(cy - r)), min(H, int(cy + r) + 2)): + dy2 = (row - cy) ** 2 + if dy2 >= radius_sq: + continue + span = (radius_sq - dy2) ** 0.5 + for col in range(int(cx - span), int(cx + span) + 2): + dx = col - cx + if dx * dx + dy2 >= radius_sq: + continue + v = heat[row][col % W] + if v > best: + best = min(float(v), 1.0) + return best + + +def null_percentile(heat, x, y, rng, trials=NULL_TRIALS, radius_sq=None): + """``(act, percentile, null_med, null_p95)`` of the site's radius-max vs its pano. + + The null keeps the ramp's elevation and randomizes azimuth — the shape every + other #46 null uses. Draws whose window would overlap the site's own window + (wrapped column distance under 2R) are rejected and redrawn: the question is + whether the site's response exceeds what the *rest* of the elevation band + produces, and a draw that reads the site's own bump back would contaminate + exactly the sparse-heatmap case this analysis exists to detect. The percentile + counts ties as half, so a flat heatmap reads 0.5, not 1.0. + """ + if radius_sq is None: + radius_sq = radius_sq_for() + act = radius_max(heat, x, y, radius_sq) + W = len(heat[0]) + exclude = 2.0 * (radius_sq ** 0.5) / W # normalized column distance + draws = [] + while len(draws) < trials: + nx = rng.random() + dx = abs(nx - x) + if min(dx, 1.0 - dx) < exclude: + continue + draws.append(radius_max(heat, nx, y, radius_sq)) + draws.sort() + below = sum(1 for d in draws if d < act) + ties = sum(1 for d in draws if d == act) + pct = (below + 0.5 * ties) / trials + return act, pct, draws[trials // 2], draws[int(trials * 0.95)] + + +def group_of(row, queue_keys, rated_by_rowkey): + """Which selection stratum a silent miss belongs to (Phase 0's partition).""" + key = row_key(row) + if key not in queue_keys: + return "witnessed" + if key in rated_by_rowkey: + return "rated" + return "below_floor" + + +# --------------------------------------------------------------------------- # +# I/O +# --------------------------------------------------------------------------- # +def main(argv=None): + p = argparse.ArgumentParser(description=__doc__.split("\n")[0]) + p.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD) + p.add_argument("--witness", default=os.path.join(OUT, "silent_witness.json")) + p.add_argument("--gallery", default=os.path.join(REPO, "benchmark", + "miss_taxonomy_46")) + p.add_argument("--panos-root", default=REPO, + help="Checkout holding benchmark//panos (git-ignored, so " + "in a worktree it lives in the main checkout instead).") + p.add_argument("--cities", default=",".join(US_SPLITS)) + p.add_argument("--limit", type=int, default=None, + help="Stop after this many panos (smoke test).") + p.add_argument("--json-out", default=None) + args = p.parse_args(argv) + + import torch + import threshold_sweep as ts + assert (HEAT_W, HEAT_H) == (1024, 512), "heatmap grid != matcher space" + + from miss_gallery import load_queue, pano_path + queue_keys = load_queue(args.witness) + rated = load_rated(args.gallery, field=None) + rated_by_rowkey = {(v["city"], v["pano"], round(float(v["x"]), 6), + round(float(v["y"]), 6)): v for v in rated.values()} + + cities = [c.strip() for c in args.cities.split(",") if c.strip()] + by_pano = {} + for city in cities: + loaded = mt.load_rows(city, args.threshold, rng=None) + if loaded is None: + continue + for r in loaded[0]: + if not r["hit"] and r["bucket"] == "silent": + by_pano.setdefault((city, r["pano"]), []).append(r) + n_miss = sum(len(v) for v in by_pano.values()) + print(f"=== Silent-miss activation forensics (threshold {args.threshold}, " + f"{n_miss} misses in {len(by_pano)} panos, #46 Phase 1) ===", flush=True) + + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + model = ts.load_model().to(device) + print(f"device={device} model=projectsidewalk/rampnet-model " + f"(single-pass fp32, as op_cache)", flush=True) + + rng = random.Random(NULL_SEED) + radius_sq = radius_sq_for() + results, skipped = [], 0 + for i, ((city, pano), misses) in enumerate(sorted(by_pano.items()), 1): + path = pano_path(city, pano, args.panos_root) + if not os.path.exists(path): + skipped += len(misses) + continue + heat = ts.heatmap_for(model, device, path, use_fp16=False) + for r in misses: + act, pct, null_med, null_p95 = null_percentile( + heat, r["x"], r["y"], rng, radius_sq=radius_sq) + key = row_key(r) + v = rated_by_rowkey.get(key) + results.append({ + "city": city, "pano": pano, "x": r["x"], "y": r["y"], + "field": r["field"], "dist_m": round(r["dist"], 1), + "px": round(r["px"], 1), + "group": group_of(r, queue_keys, rated_by_rowkey), + "verdict": v["verdict"] if v else None, + "act": round(act, 5), "null_pct": round(pct, 3), + "null_med": round(null_med, 5), "null_p95": round(null_p95, 5), + "above_own_null_p95": act > null_p95, + }) + del heat + if i % 10 == 0: + print(f" {i}/{len(by_pano)} panos", flush=True) + if args.limit and i >= args.limit: + print(f" --limit {args.limit} reached", flush=True) + break + if skipped: + print(f" [!] {skipped} misses skipped — panorama not on disk under " + f"{args.panos_root}", flush=True) + + # ----------------------------------------------------------------------- # + print(f"\n{'-'*78}\nACTIVATION AT THE MISSED RAMP, by field and stratum\n{'-'*78}") + print(f"{'population':>34} {'n':>4} {'act q1/med/q3':>18} " + f"{'>p95 of own null':>17} {'act>=0.01':>10}") + groups = {} + for field in ("near", "far"): + for grp in ("rated", "below_floor", "witnessed"): + sel = [r for r in results if r["field"] == field and r["group"] == grp] + if not sel: + continue + name = f"{field} / {grp}" + groups[name] = sel + for name, sel in list(groups.items()) + [("ALL silent misses", results)]: + q = quartiles([r["act"] for r in sel]) + n_sig = sum(1 for r in sel if r["above_own_null_p95"]) + n_01 = sum(1 for r in sel if r["act"] >= 0.01) + print(f"{name:>34} {len(sel):>4} " + f"{q[0]:>6.4f}/{q[1]:>6.4f}/{q[2]:>6.4f} " + f"{n_sig:>7}/{len(sel):<7} {n_01:>10}") + + vis = [r for r in results if r["verdict"] == "visible"] + if vis: + q = quartiles([r["act"] for r in vis]) + n_sig = sum(1 for r in vis if r["above_own_null_p95"]) + print(f"\n rated `visible` only (n={len(vis)}): act q1/med/q3 " + f"{q[0]:.4f}/{q[1]:.4f}/{q[2]:.4f}; {n_sig}/{len(vis)} above their " + f"own pano's null p95") + shoulders = [r for r in results if r["act"] >= 0.05] + print(f"\n `act` >= 0.05 without a peak (shoulder of a neighbouring mode): " + f"{len(shoulders)}") + + print(f"\n Reading: 'above own null p95' is the localized-signal test. A miss") + print(f" passing it has a real, faint, spatially specific response — attenuation,") + print(f" the sub_threshold continuum's tail. A miss failing it is indistinguishable") + print(f" from the panorama's noise floor — absence. The two populations continue") + print(f" to different phases: attenuation prices into threshold/calibration work,") + print(f" absence goes to Phase 2's scale counterfactual.") + + if args.json_out: + os.makedirs(os.path.dirname(os.path.abspath(args.json_out)), exist_ok=True) + with open(args.json_out, "w", encoding="utf-8") as fh: + json.dump({"threshold": args.threshold, "null_trials": NULL_TRIALS, + "null_seed": NULL_SEED, "n": len(results), + "skipped_no_imagery": skipped, + "model": "projectsidewalk/rampnet-model", + "tta": False, "results": results}, fh, indent=2) + print(f"\nWrote {args.json_out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_silent_activation.py b/tests/test_silent_activation.py new file mode 100644 index 0000000..22eff87 --- /dev/null +++ b/tests/test_silent_activation.py @@ -0,0 +1,114 @@ +"""Unit tests for the silent-miss activation forensics (#46, Phase 1). + +Pure core only — no torch, no imagery, no GPU. The heavy path (model inference) +is exercised by running the script itself; what these protect is the geometry: +``radius_max`` must read the heatmap through exactly the matcher's coordinate +convention, or the activation numbers describe the wrong locations. +""" +import os +import random +import sys + +import pytest + +REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, REPO) +sys.path.insert(0, os.path.join(REPO, "scripts", "analysis")) + +import silent_activation as sa # noqa: E402 +from rampnet.detection_eval import radius_sq_for # noqa: E402 + +RSQ = radius_sq_for() +R = RSQ ** 0.5 # 22.5 heatmap px + + +def _heat(value=0.0): + return [[value] * 1024 for _ in range(512)] + + +# --------------------------------------------------------------------------- # +# radius_max — the matcher's window, applied to the heatmap +# --------------------------------------------------------------------------- # +def test_reads_a_peak_at_the_site(): + h = _heat() + h[256][512] = 0.7 + assert sa.radius_max(h, 512 / 1024, 256 / 512) == pytest.approx(0.7) + + +def test_ignores_a_peak_outside_the_radius(): + h = _heat() + h[256][512 + int(R) + 2] = 0.9 + assert sa.radius_max(h, 512 / 1024, 256 / 512) == 0.0 + + +def test_sees_a_peak_just_inside_the_radius(): + h = _heat() + h[256][512 + int(R) - 1] = 0.9 + assert sa.radius_max(h, 512 / 1024, 256 / 512) == pytest.approx(0.9) + + +def test_columns_wrap_at_the_seam(): + # A site at x~0 must see a peak stored at the right edge of the heatmap. + h = _heat() + h[256][1023] = 0.8 + assert sa.radius_max(h, 2 / 1024, 256 / 512) == pytest.approx(0.8) + + +def test_rows_clamp_at_the_top(): + # A site near the top row must not crash reaching above the panorama. + h = _heat() + h[0][512] = 0.6 + assert sa.radius_max(h, 512 / 1024, 0.0) == pytest.approx(0.6) + + +def test_values_clip_to_one_like_peak_extraction(): + h = _heat() + h[256][512] = 1.7 + assert sa.radius_max(h, 512 / 1024, 256 / 512) == 1.0 + + +# --------------------------------------------------------------------------- # +# null_percentile — signal against the pano's own noise floor +# --------------------------------------------------------------------------- # +def test_flat_heatmap_reads_as_chance(): + act, pct, med, p95 = sa.null_percentile(_heat(0.003), 0.5, 0.5, + random.Random(0), trials=50) + assert act == pytest.approx(0.003) + assert pct == pytest.approx(0.5) # every draw ties the site + assert med == p95 == pytest.approx(0.003) + + +def test_a_lone_bump_at_the_site_beats_its_null(): + h = _heat() + h[256][512] = 0.04 + act, pct, _, p95 = sa.null_percentile(h, 512 / 1024, 256 / 512, + random.Random(0), trials=100) + assert act == pytest.approx(0.04) + assert pct > 0.9 + assert act > p95 + + +def test_a_site_no_better_than_the_horizon_band_fails_the_test(): + # Strong response everywhere along the site's row: the site is nothing special. + h = _heat() + for c in range(0, 1024, 8): + h[256][c] = 0.5 + act, pct, _, p95 = sa.null_percentile(h, 512 / 1024, 256 / 512, + random.Random(0), trials=100) + assert act == pytest.approx(0.5) + assert not act > p95 + + +# --------------------------------------------------------------------------- # +# group_of — Phase 0's partition, reused +# --------------------------------------------------------------------------- # +def _row(city="bend", pano="p1", x=0.25, y=0.6): + return {"city": city, "pano": pano, "x": x, "y": y} + + +def test_group_partition(): + r = _row() + key = sa.row_key(r) + assert sa.group_of(r, set(), {}) == "witnessed" + assert sa.group_of(r, {key}, {}) == "below_floor" + assert sa.group_of(r, {key}, {key: {}}) == "rated" From 64763d0d051df03c546156990e625370a15de8eb Mon Sep 17 00:00:00 2001 From: Jon Froehlich Date: Fri, 31 Jul 2026 12:56:12 -0700 Subject: [PATCH 3/4] Phase 1 results (#46): only 10 of 128 silent misses are heatmap-absent; 62% are an adjacent confident mode's tail Ran silent_activation.py over all 128 pooled silent misses on the local RTX 3070 (published checkpoint, single-pass fp32, matching op_cache). - absent (act < 0.01): 10 of 128 (8%). "Silent = the model saw nothing" is wrong for 92% of the bucket - silent was peak bookkeeping. - tail (act >= 0.05): 79 (62%) - argmax in the window's outer quarter in 75/79, nearest cached floor peak ~1.4R away at median score 0.685: a CONFIDENT adjacent detection. The sigma/representation family again, not vocabulary. (A floor-level annulus peak leaves a miss "silent" - the localization bucket only inspects kept peaks.) - faint local (0.01-0.05): 39 (30%) - mass on-site in 30/39, nothing else within ~3.8R: the sub_threshold continuum below the floor. - The anomaly population (far-field rated-visible, 34): 3 absent / 12 faint-local / 19 tail - the model responds at or next to ~91% of the far ramps a human called resolvable. - Near-field consequence for 0.013: of the 45 near silent misses, only 6 are heatmap-absent. Not revised either direction until Phases 2-3. Instrument additions since the first run: site_profile (argmax offset + value at the site) and nearest_peak (matcher-unit distance to cached floor peaks, wrapped) separate a site response from a neighbour's tail; 4 new tests (suite 533). Doc: Phase 1 section added to 0c. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017KCSfZm3MjoJ62LVtkvNeb --- .gitignore | 1 + analysis_out/silent_activation.json | 2571 +++++++++++++++++++++++++ docs/curb_ramp_data_sourcing.md | 58 +- scripts/analysis/silent_activation.py | 106 +- tests/test_silent_activation.py | 33 + 5 files changed, 2750 insertions(+), 19 deletions(-) create mode 100644 analysis_out/silent_activation.json diff --git a/.gitignore b/.gitignore index dfa74d6..7bfc780 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ analysis_out/op/* # unwitnessed list that is the gallery's actual work queue. !analysis_out/silent_witness.json !analysis_out/farfield_forensics.json +!analysis_out/silent_activation.json # Generated tagging pages live next to their (committed) galleries; regenerate with # scripts/analysis/make_tagger.py rather than tracking a build artifact. diff --git a/analysis_out/silent_activation.json b/analysis_out/silent_activation.json new file mode 100644 index 0000000..ffdb45c --- /dev/null +++ b/analysis_out/silent_activation.json @@ -0,0 +1,2571 @@ +{ + "threshold": 0.3, + "null_trials": 200, + "null_seed": 20260731, + "n": 128, + "skipped_no_imagery": 0, + "model": "projectsidewalk/rampnet-model", + "tta": false, + "results": [ + { + "city": "annapolis", + "pano": "1040516234061456", + "x": 0.06834458144797188, + "y": 0.5705305826510851, + "field": "near", + "dist_m": 11.1, + "px": 70.5, + "group": "witnessed", + "verdict": null, + "act": 0.43735, + "null_pct": 0.75, + "null_med": 0.09615, + "null_p95": 0.66166, + "above_own_null_p95": false, + "argmax_off_px": 22.0, + "act_at_site": 0.16377, + "nearest_peak_px": 30.0, + "nearest_peak_score": 0.518 + }, + { + "city": "annapolis", + "pano": "1040516234061456", + "x": 0.07834260319860353, + "y": 0.5472727272727272, + "field": "near", + "dist_m": 16.7, + "px": 46.8, + "group": "witnessed", + "verdict": null, + "act": 0.51325, + "null_pct": 0.805, + "null_med": 0.07194, + "null_p95": 0.62719, + "above_own_null_p95": false, + "argmax_off_px": 21.7, + "act_at_site": 0.18835, + "nearest_peak_px": 22.5, + "nearest_peak_score": 0.518 + }, + { + "city": "annapolis", + "pano": "1074557820562424", + "x": 0.3886869118430398, + "y": 0.6736363821318655, + "field": "near", + "dist_m": 4.1, + "px": 189.9, + "group": "witnessed", + "verdict": null, + "act": 0.02373, + "null_pct": 1.0, + "null_med": 0.00237, + "null_p95": 0.02284, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.01009, + "nearest_peak_px": 41.6, + "nearest_peak_score": 0.097 + }, + { + "city": "annapolis", + "pano": "1074557820562424", + "x": 0.6681082605432581, + "y": 0.5894862321196448, + "field": "near", + "dist_m": 8.7, + "px": 90.4, + "group": "witnessed", + "verdict": null, + "act": 0.03074, + "null_pct": 0.81, + "null_med": 0.01836, + "null_p95": 0.07319, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.00146, + "nearest_peak_px": 37.6, + "nearest_peak_score": 0.065 + }, + { + "city": "annapolis", + "pano": "1074557820562424", + "x": 0.6844444876006155, + "y": 0.566969696969697, + "field": "near", + "dist_m": 11.7, + "px": 66.8, + "group": "witnessed", + "verdict": null, + "act": 0.00593, + "null_pct": 0.39, + "null_med": 0.01071, + "null_p95": 0.07319, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.00262, + "nearest_peak_px": 50.3, + "nearest_peak_score": 0.065 + }, + { + "city": "annapolis", + "pano": "1074557820562424", + "x": 0.7112355588852785, + "y": 0.5416619902731012, + "field": "far", + "dist_m": 19.0, + "px": 41.2, + "group": "witnessed", + "verdict": null, + "act": 0.01281, + "null_pct": 0.505, + "null_med": 0.01246, + "null_p95": 0.06504, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.00209, + "nearest_peak_px": 77.6, + "nearest_peak_score": 0.065 + }, + { + "city": "annapolis", + "pano": "1265191387234427", + "x": 0.45847228033833237, + "y": 0.5232714292347035, + "field": "far", + "dist_m": 34.1, + "px": 22.9, + "group": "witnessed", + "verdict": null, + "act": 0.11185, + "null_pct": 0.875, + "null_med": 0.00197, + "null_p95": 0.72167, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.04899, + "nearest_peak_px": 31.5, + "nearest_peak_score": 0.166 + }, + { + "city": "annapolis", + "pano": "263456786691932", + "x": 0.5268878662384862, + "y": 0.49995505498717574, + "field": "far", + "dist_m": 150.0, + "px": 5.2, + "group": "below_floor", + "verdict": null, + "act": 0.13246, + "null_pct": 0.9, + "null_med": 0.00153, + "null_p95": 0.49731, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.00845, + "nearest_peak_px": 42.2, + "nearest_peak_score": 0.826 + }, + { + "city": "annapolis", + "pano": "263456786691932", + "x": 0.4183953537314284, + "y": 0.4974932147254313, + "field": "far", + "dist_m": 150.0, + "px": 5.2, + "group": "below_floor", + "verdict": null, + "act": 0.38089, + "null_pct": 0.97, + "null_med": 0.00131, + "null_p95": 0.19137, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.02107, + "nearest_peak_px": 33.2, + "nearest_peak_score": 0.719 + }, + { + "city": "annapolis", + "pano": "297886629678677", + "x": 0.49330807772549706, + "y": 0.5068181818181818, + "field": "far", + "dist_m": 116.7, + "px": 6.7, + "group": "below_floor", + "verdict": null, + "act": 0.04198, + "null_pct": 0.74, + "null_med": 0.00693, + "null_p95": 0.61309, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.01469, + "nearest_peak_px": 61.8, + "nearest_peak_score": 0.575 + }, + { + "city": "annapolis", + "pano": "3946684388792708", + "x": 0.47874228458533047, + "y": 0.5073934572348755, + "field": "far", + "dist_m": 107.6, + "px": 7.3, + "group": "witnessed", + "verdict": null, + "act": 0.48864, + "null_pct": 0.935, + "null_med": 0.00194, + "null_p95": 0.59463, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.13056, + "nearest_peak_px": 31.1, + "nearest_peak_score": 0.55 + }, + { + "city": "annapolis", + "pano": "846876249550191", + "x": 0.6507399277472887, + "y": 0.5029791914869305, + "field": "far", + "dist_m": 150.0, + "px": 5.2, + "group": "below_floor", + "verdict": null, + "act": 0.00131, + "null_pct": 0.495, + "null_med": 0.00159, + "null_p95": 0.72933, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.0, + "nearest_peak_px": 118.7, + "nearest_peak_score": 0.086 + }, + { + "city": "annapolis", + "pano": "865533918497811", + "x": 0.5506606758523545, + "y": 0.509002398377811, + "field": "far", + "dist_m": 88.4, + "px": 8.9, + "group": "witnessed", + "verdict": null, + "act": 0.0567, + "null_pct": 0.865, + "null_med": 0.00068, + "null_p95": 0.10336, + "above_own_null_p95": false, + "argmax_off_px": 22.1, + "act_at_site": 0.02945, + "nearest_peak_px": 32.0, + "nearest_peak_score": 0.073 + }, + { + "city": "annapolis", + "pano": "869773754548856", + "x": 0.057741298825384924, + "y": 0.5468686837861031, + "field": "near", + "dist_m": 16.9, + "px": 46.4, + "group": "witnessed", + "verdict": null, + "act": 0.50114, + "null_pct": 0.985, + "null_med": 0.00292, + "null_p95": 0.06203, + "above_own_null_p95": true, + "argmax_off_px": 22.5, + "act_at_site": 0.11586, + "nearest_peak_px": 31.4, + "nearest_peak_score": 0.64 + }, + { + "city": "annapolis", + "pano": "877430540067191", + "x": 0.4602280449077488, + "y": 0.5382795461355264, + "field": "far", + "dist_m": 20.7, + "px": 37.8, + "group": "witnessed", + "verdict": null, + "act": 0.61482, + "null_pct": 0.93, + "null_med": 0.00059, + "null_p95": 0.68466, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.19303, + "nearest_peak_px": 25.1, + "nearest_peak_score": 0.64 + }, + { + "city": "annapolis", + "pano": "877430540067191", + "x": 0.543034271131033, + "y": 0.5143504507599699, + "field": "far", + "dist_m": 55.4, + "px": 14.1, + "group": "below_floor", + "verdict": null, + "act": 0.35867, + "null_pct": 0.91, + "null_med": 0.00093, + "null_p95": 0.64023, + "above_own_null_p95": false, + "argmax_off_px": 22.1, + "act_at_site": 0.13544, + "nearest_peak_px": 33.4, + "nearest_peak_score": 0.685 + }, + { + "city": "annapolis", + "pano": "900958664931957", + "x": 0.45630241240838754, + "y": 0.5082816065558751, + "field": "far", + "dist_m": 96.1, + "px": 8.1, + "group": "below_floor", + "verdict": null, + "act": 0.19369, + "null_pct": 0.765, + "null_med": 0.00145, + "null_p95": 0.64171, + "above_own_null_p95": false, + "argmax_off_px": 22.0, + "act_at_site": 0.03081, + "nearest_peak_px": 40.1, + "nearest_peak_score": 0.907 + }, + { + "city": "annapolis", + "pano": "928118321383655", + "x": 0.9267534093454298, + "y": 0.5339254534343074, + "field": "far", + "dist_m": 23.4, + "px": 33.5, + "group": "rated", + "verdict": "visible", + "act": 0.07621, + "null_pct": 0.815, + "null_med": 0.0, + "null_p95": 0.38994, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.01318, + "nearest_peak_px": 42.1, + "nearest_peak_score": 0.636 + }, + { + "city": "annapolis", + "pano": "967427853833872", + "x": 0.784888461045941, + "y": 0.5372327375413968, + "field": "far", + "dist_m": 21.3, + "px": 36.8, + "group": "rated", + "verdict": "visible", + "act": 0.10143, + "null_pct": 0.69, + "null_med": 0.00459, + "null_p95": 0.69251, + "above_own_null_p95": false, + "argmax_off_px": 21.9, + "act_at_site": 0.00894, + "nearest_peak_px": 32.9, + "nearest_peak_score": 0.184 + }, + { + "city": "annapolis", + "pano": "967427853833872", + "x": 0.8266666851621686, + "y": 0.5766666666666667, + "field": "near", + "dist_m": 10.2, + "px": 76.9, + "group": "witnessed", + "verdict": null, + "act": 0.59154, + "null_pct": 0.865, + "null_med": 0.01575, + "null_p95": 0.68965, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.07891, + "nearest_peak_px": 25.0, + "nearest_peak_score": 0.65 + }, + { + "city": "annapolis", + "pano": "997534874804455", + "x": 0.573055605680311, + "y": 0.5427722231683144, + "field": "far", + "dist_m": 18.5, + "px": 42.3, + "group": "witnessed", + "verdict": null, + "act": 0.7841, + "null_pct": 1.0, + "null_med": 0.00065, + "null_p95": 0.01352, + "above_own_null_p95": true, + "argmax_off_px": 22.1, + "act_at_site": 0.24718, + "nearest_peak_px": 24.9, + "nearest_peak_score": 0.816 + }, + { + "city": "bend", + "pano": "0SgaioiGd5Yi_5wc6m-nJg", + "x": 0.5369325345142446, + "y": 0.5218091707287849, + "field": "far", + "dist_m": 36.4, + "px": 21.5, + "group": "rated", + "verdict": "visible", + "act": 0.65561, + "null_pct": 1.0, + "null_med": 0.00068, + "null_p95": 0.4966, + "above_own_null_p95": true, + "argmax_off_px": 22.3, + "act_at_site": 0.06793, + "nearest_peak_px": 23.5, + "nearest_peak_score": 0.673 + }, + { + "city": "bend", + "pano": "62TiKmlmxt_xn_68w1lgAw", + "x": 0.6216161554509944, + "y": 0.56, + "field": "near", + "dist_m": 13.1, + "px": 59.7, + "group": "rated", + "verdict": "visible", + "act": 0.19676, + "null_pct": 0.905, + "null_med": 0.00383, + "null_p95": 0.67697, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.0, + "nearest_peak_px": 38.5, + "nearest_peak_score": 0.943 + }, + { + "city": "bend", + "pano": "8KtYwvo9b34f3zm-afipNg", + "x": 0.6496969696969697, + "y": 0.5402020171194366, + "field": "far", + "dist_m": 19.7, + "px": 39.7, + "group": "rated", + "verdict": "visible", + "act": 0.01822, + "null_pct": 0.89, + "null_med": 0.00177, + "null_p95": 0.58302, + "above_own_null_p95": false, + "argmax_off_px": 17.7, + "act_at_site": 0.011, + "nearest_peak_px": 114.0, + "nearest_peak_score": 0.583 + }, + { + "city": "bend", + "pano": "A5AVKFhm7unr9Rs55aQ1Tw", + "x": 0.5327272727272727, + "y": 0.5305050474224668, + "field": "far", + "dist_m": 26.0, + "px": 30.1, + "group": "rated", + "verdict": "visible", + "act": 0.03169, + "null_pct": 0.94, + "null_med": 0.00041, + "null_p95": 0.52899, + "above_own_null_p95": false, + "argmax_off_px": 10.2, + "act_at_site": 0.02331, + "nearest_peak_px": 85.6, + "nearest_peak_score": 0.654 + }, + { + "city": "bend", + "pano": "E15SCY6RTiDuNUu_trZk4Q", + "x": 0.43373736757220643, + "y": 0.5777777746951941, + "field": "near", + "dist_m": 10.0, + "px": 78.0, + "group": "witnessed", + "verdict": null, + "act": 0.77546, + "null_pct": 0.665, + "null_med": 0.58039, + "null_p95": 0.98708, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.07158, + "nearest_peak_px": 24.2, + "nearest_peak_score": 0.812 + }, + { + "city": "bend", + "pano": "I8Y9zDjNKL1k5HUv6BSyFA", + "x": 0.4805241360875154, + "y": 0.5284267909756982, + "field": "far", + "dist_m": 27.9, + "px": 28.0, + "group": "witnessed", + "verdict": null, + "act": 0.04119, + "null_pct": 0.6, + "null_med": 0.02167, + "null_p95": 0.94633, + "above_own_null_p95": false, + "argmax_off_px": 2.6, + "act_at_site": 0.03581, + "nearest_peak_px": 76.1, + "nearest_peak_score": 0.171 + }, + { + "city": "bend", + "pano": "I8Y9zDjNKL1k5HUv6BSyFA", + "x": 0.06549370841548144, + "y": 0.5253159080351347, + "field": "far", + "dist_m": 31.4, + "px": 24.9, + "group": "witnessed", + "verdict": null, + "act": 0.05574, + "null_pct": 0.65, + "null_med": 0.02482, + "null_p95": 0.94633, + "above_own_null_p95": false, + "argmax_off_px": 22.0, + "act_at_site": 0.01167, + "nearest_peak_px": 31.9, + "nearest_peak_score": 0.065 + }, + { + "city": "bend", + "pano": "NS7lT-itv0JiN6UHVvBxsg", + "x": 0.023434337269176136, + "y": 0.5490909090909091, + "field": "near", + "dist_m": 16.1, + "px": 48.6, + "group": "witnessed", + "verdict": null, + "act": 0.29271, + "null_pct": 0.745, + "null_med": 0.00555, + "null_p95": 0.89445, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.05392, + "nearest_peak_px": 29.1, + "nearest_peak_score": 0.349 + }, + { + "city": "bend", + "pano": "OedGesfk606bGhg4OQESiQ", + "x": 0.5339577798669883, + "y": 0.5185880969654076, + "field": "far", + "dist_m": 42.8, + "px": 18.3, + "group": "rated", + "verdict": "visible", + "act": 0.54807, + "null_pct": 0.94, + "null_med": 2e-05, + "null_p95": 0.62738, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.03405, + "nearest_peak_px": 23.8, + "nearest_peak_score": 0.101 + }, + { + "city": "bend", + "pano": "PeJlsIXjn8VkEoryRRBBMg", + "x": 0.5296969881924716, + "y": 0.5233333518288352, + "field": "far", + "dist_m": 34.0, + "px": 23.0, + "group": "witnessed", + "verdict": null, + "act": 0.04689, + "null_pct": 0.745, + "null_med": 0.00099, + "null_p95": 0.78178, + "above_own_null_p95": false, + "argmax_off_px": 2.6, + "act_at_site": 0.0447, + "nearest_peak_px": 91.4, + "nearest_peak_score": 0.099 + }, + { + "city": "bend", + "pano": "Tn5W3kvbdH7_Fpk29a_hrQ", + "x": 0.5673504791405647, + "y": 0.538278379142594, + "field": "far", + "dist_m": 20.7, + "px": 37.8, + "group": "witnessed", + "verdict": null, + "act": 0.8038, + "null_pct": 1.0, + "null_med": 0.0, + "null_p95": 0.58952, + "above_own_null_p95": true, + "argmax_off_px": 22.2, + "act_at_site": 0.04501, + "nearest_peak_px": 26.3, + "nearest_peak_score": 0.885 + }, + { + "city": "bend", + "pano": "VIMFndX_oqm7vJJD8gDyEA", + "x": 0.2727272727272727, + "y": 0.6214141383315578, + "field": "near", + "dist_m": 6.2, + "px": 125.5, + "group": "witnessed", + "verdict": null, + "act": 0.3702, + "null_pct": 1.0, + "null_med": 0.00201, + "null_p95": 0.06855, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.01045, + "nearest_peak_px": 34.4, + "nearest_peak_score": 0.619 + }, + { + "city": "bend", + "pano": "WGdZz7jOkmmmyQYpbw3F8w", + "x": 0.9244565295638657, + "y": 0.5513961301799177, + "field": "near", + "dist_m": 15.3, + "px": 51.0, + "group": "witnessed", + "verdict": null, + "act": 0.05529, + "null_pct": 0.83, + "null_med": 0.00862, + "null_p95": 0.26395, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.00916, + "nearest_peak_px": 40.4, + "nearest_peak_score": 0.189 + }, + { + "city": "bend", + "pano": "X769L3ombUWjLtZDlE2fqg", + "x": 0.41818181818181815, + "y": 0.5462626231800426, + "field": "near", + "dist_m": 17.1, + "px": 45.8, + "group": "witnessed", + "verdict": null, + "act": 0.50922, + "null_pct": 0.97, + "null_med": 0.00084, + "null_p95": 0.48416, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.02012, + "nearest_peak_px": 33.2, + "nearest_peak_score": 0.978 + }, + { + "city": "bend", + "pano": "bVadNrPq_zXgbJjhhDSwDw", + "x": 0.007272727272727273, + "y": 0.525656562573982, + "field": "far", + "dist_m": 30.9, + "px": 25.3, + "group": "rated", + "verdict": "visible", + "act": 0.79537, + "null_pct": 0.955, + "null_med": 0.00161, + "null_p95": 0.74986, + "above_own_null_p95": true, + "argmax_off_px": 7.5, + "act_at_site": 0.66475, + "nearest_peak_px": 101.8, + "nearest_peak_score": 0.951 + }, + { + "city": "bend", + "pano": "h_1bUHhTLKugR5XhzA1-nA", + "x": 0.03515151515151515, + "y": 0.5208080777254972, + "field": "far", + "dist_m": 38.2, + "px": 20.5, + "group": "rated", + "verdict": "visible", + "act": 0.02126, + "null_pct": 0.9, + "null_med": 0.00034, + "null_p95": 0.93052, + "above_own_null_p95": false, + "argmax_off_px": 7.1, + "act_at_site": 0.01241, + "nearest_peak_px": 66.3, + "nearest_peak_score": 0.931 + }, + { + "city": "bend", + "pano": "mdQY8aMSdYrnHfHNXqj6GQ", + "x": 0.47636365485913823, + "y": 0.5322222160570549, + "field": "far", + "dist_m": 24.6, + "px": 31.8, + "group": "rated", + "verdict": "visible", + "act": 0.01836, + "null_pct": 0.935, + "null_med": 0.00081, + "null_p95": 0.02377, + "above_own_null_p95": false, + "argmax_off_px": 5.5, + "act_at_site": 0.01557, + "nearest_peak_px": null, + "nearest_peak_score": null + }, + { + "city": "bend", + "pano": "qNYRurrxEhqZvO8GrwE9nA", + "x": 0.05818181818181818, + "y": 0.5353535322709517, + "field": "far", + "dist_m": 22.4, + "px": 34.9, + "group": "rated", + "verdict": "visible", + "act": 0.82096, + "null_pct": 0.995, + "null_med": 0.00065, + "null_p95": 0.5015, + "above_own_null_p95": true, + "argmax_off_px": 22.3, + "act_at_site": 0.10248, + "nearest_peak_px": 26.0, + "nearest_peak_score": 1.009 + }, + { + "city": "bend", + "pano": "rQtHh0Wai-D1FvbvqBlWdw", + "x": 0.012121212121212121, + "y": 0.5620201989376183, + "field": "near", + "dist_m": 12.7, + "px": 61.8, + "group": "witnessed", + "verdict": null, + "act": 0.84289, + "null_pct": 1.0, + "null_med": 0.00344, + "null_p95": 0.48298, + "above_own_null_p95": true, + "argmax_off_px": 12.8, + "act_at_site": 0.65267, + "nearest_peak_px": 127.7, + "nearest_peak_score": 0.395 + }, + { + "city": "bend", + "pano": "sUMfdZmScVLBGMjvZ0MjCA", + "x": 0.05757575757575758, + "y": 0.5268686837861032, + "field": "far", + "dist_m": 29.5, + "px": 26.5, + "group": "witnessed", + "verdict": null, + "act": 0.57787, + "null_pct": 0.93, + "null_med": 0.00035, + "null_p95": 0.63499, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.02106, + "nearest_peak_px": 24.8, + "nearest_peak_score": 0.626 + }, + { + "city": "bend", + "pano": "xKoseQiPNzFwaWfw9Rl6hg", + "x": 0.9183838815400095, + "y": 0.5435353966915246, + "field": "far", + "dist_m": 18.2, + "px": 43.1, + "group": "rated", + "verdict": "visible", + "act": 0.5538, + "null_pct": 0.99, + "null_med": 0.00212, + "null_p95": 0.37167, + "above_own_null_p95": true, + "argmax_off_px": 22.3, + "act_at_site": 0.00749, + "nearest_peak_px": 31.1, + "nearest_peak_score": 0.718 + }, + { + "city": "clovis", + "pano": "1197447770701938", + "x": 0.18847373570874265, + "y": 0.5232198555538492, + "field": "far", + "dist_m": 34.2, + "px": 22.9, + "group": "rated", + "verdict": "visible", + "act": 0.01759, + "null_pct": 0.825, + "null_med": 0.00329, + "null_p95": 0.47612, + "above_own_null_p95": false, + "argmax_off_px": 13.5, + "act_at_site": 0.01128, + "nearest_peak_px": 119.5, + "nearest_peak_score": 0.865 + }, + { + "city": "clovis", + "pano": "170392751596494", + "x": 0.5034343372691762, + "y": 0.6679798103101325, + "field": "near", + "dist_m": 4.3, + "px": 182.4, + "group": "rated", + "verdict": "definition", + "act": 0.00903, + "null_pct": 0.52, + "null_med": 0.00703, + "null_p95": 0.22006, + "above_own_null_p95": false, + "argmax_off_px": 16.6, + "act_at_site": 0.0, + "nearest_peak_px": 98.4, + "nearest_peak_score": 0.598 + }, + { + "city": "clovis", + "pano": "170392751596494", + "x": 0.1819896265401673, + "y": 0.5323912532511264, + "field": "far", + "dist_m": 24.5, + "px": 32.0, + "group": "witnessed", + "verdict": null, + "act": 0.04457, + "null_pct": 0.855, + "null_med": 0.00366, + "null_p95": 0.45153, + "above_own_null_p95": false, + "argmax_off_px": 2.5, + "act_at_site": 0.04081, + "nearest_peak_px": 170.1, + "nearest_peak_score": 0.821 + }, + { + "city": "clovis", + "pano": "2493443314123605", + "x": 0.26485329169322935, + "y": 0.52684806088005, + "field": "far", + "dist_m": 29.6, + "px": 26.5, + "group": "rated", + "verdict": "context-only", + "act": 0.20188, + "null_pct": 0.965, + "null_med": 0.00078, + "null_p95": 0.01867, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.00061, + "nearest_peak_px": 36.4, + "nearest_peak_score": 0.704 + }, + { + "city": "clovis", + "pano": "2652019851757090", + "x": 0.7288364699667899, + "y": 0.5275368230970484, + "field": "far", + "dist_m": 28.8, + "px": 27.1, + "group": "witnessed", + "verdict": null, + "act": 0.51119, + "null_pct": 0.995, + "null_med": 0.00158, + "null_p95": 0.23289, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.06502, + "nearest_peak_px": 27.2, + "nearest_peak_score": 0.571 + }, + { + "city": "clovis", + "pano": "308441850838672", + "x": 0.2837870519216822, + "y": 0.5241603733501503, + "field": "far", + "dist_m": 32.9, + "px": 23.8, + "group": "witnessed", + "verdict": null, + "act": 0.18835, + "null_pct": 0.905, + "null_med": 0.00312, + "null_p95": 0.49247, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.08307, + "nearest_peak_px": 36.9, + "nearest_peak_score": 0.412 + }, + { + "city": "clovis", + "pano": "490105898988611", + "x": 0.7212718231654784, + "y": 0.5165137807332276, + "field": "far", + "dist_m": 48.1, + "px": 16.2, + "group": "below_floor", + "verdict": null, + "act": 0.03129, + "null_pct": 0.69, + "null_med": 0.01351, + "null_p95": 0.124, + "above_own_null_p95": false, + "argmax_off_px": 8.8, + "act_at_site": 0.02599, + "nearest_peak_px": 119.4, + "nearest_peak_score": 0.332 + }, + { + "city": "clovis", + "pano": "506669223705998", + "x": 0.4197979736328125, + "y": 0.7164646587949811, + "field": "near", + "dist_m": 3.1, + "px": 253.1, + "group": "rated", + "verdict": "visible", + "act": 0.00587, + "null_pct": 0.865, + "null_med": 0.00139, + "null_p95": 0.00991, + "above_own_null_p95": false, + "argmax_off_px": 7.3, + "act_at_site": 0.00234, + "nearest_peak_px": 67.9, + "nearest_peak_score": 0.067 + }, + { + "city": "clovis", + "pano": "517195446066836", + "x": 0.7853786777937632, + "y": 0.5312450987720753, + "field": "far", + "dist_m": 25.4, + "px": 30.8, + "group": "witnessed", + "verdict": null, + "act": 0.04203, + "null_pct": 0.785, + "null_med": 0.00449, + "null_p95": 0.84255, + "above_own_null_p95": false, + "argmax_off_px": 7.4, + "act_at_site": 0.02923, + "nearest_peak_px": 98.5, + "nearest_peak_score": 0.388 + }, + { + "city": "clovis", + "pano": "744451279581577", + "x": 0.8218503168644709, + "y": 0.5419524444382605, + "field": "far", + "dist_m": 18.9, + "px": 41.5, + "group": "rated", + "verdict": "context-only", + "act": 0.02171, + "null_pct": 0.78, + "null_med": 0.003, + "null_p95": 0.39018, + "above_own_null_p95": false, + "argmax_off_px": 2.1, + "act_at_site": 0.02074, + "nearest_peak_px": 82.7, + "nearest_peak_score": 0.201 + }, + { + "city": "clovis", + "pano": "995842280952828", + "x": 0.8439816441860816, + "y": 0.5412045611449773, + "field": "far", + "dist_m": 19.2, + "px": 40.7, + "group": "witnessed", + "verdict": null, + "act": 0.16884, + "null_pct": 0.835, + "null_med": 0.01262, + "null_p95": 0.70261, + "above_own_null_p95": false, + "argmax_off_px": 21.8, + "act_at_site": 0.04023, + "nearest_peak_px": 27.8, + "nearest_peak_score": 0.193 + }, + { + "city": "gainesville", + "pano": "82y6sIvPm-_shG58klusgw", + "x": 0.9838377058871877, + "y": 0.529215300788914, + "field": "far", + "dist_m": 27.2, + "px": 28.8, + "group": "witnessed", + "verdict": null, + "act": 0.7172, + "null_pct": 1.0, + "null_med": 0.02369, + "null_p95": 0.08254, + "above_own_null_p95": true, + "argmax_off_px": 22.0, + "act_at_site": 0.14758, + "nearest_peak_px": 23.4, + "nearest_peak_score": 0.761 + }, + { + "city": "gainesville", + "pano": "BtYzecFoq3-H5qaGzBh4iw", + "x": 0.5141463694807856, + "y": 0.5136182314475036, + "field": "far", + "dist_m": 58.4, + "px": 13.4, + "group": "witnessed", + "verdict": null, + "act": 0.66627, + "null_pct": 1.0, + "null_med": 0.00142, + "null_p95": 0.00317, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.06909, + "nearest_peak_px": 23.8, + "nearest_peak_score": 0.688 + }, + { + "city": "gainesville", + "pano": "DXKg24FIKMrGUX6ilHn5Ew", + "x": 0.04402622544390468, + "y": 0.5304555636627928, + "field": "far", + "dist_m": 26.0, + "px": 30.0, + "group": "rated", + "verdict": "visible", + "act": 0.02989, + "null_pct": 0.85, + "null_med": 0.0, + "null_p95": 0.34754, + "above_own_null_p95": false, + "argmax_off_px": 6.8, + "act_at_site": 0.02565, + "nearest_peak_px": 91.1, + "nearest_peak_score": 0.28 + }, + { + "city": "gainesville", + "pano": "GRTC3zOCFSc8QMIrAtwtzw", + "x": 0.5538633036587365, + "y": 0.5287539011201986, + "field": "far", + "dist_m": 27.6, + "px": 28.3, + "group": "rated", + "verdict": "visible", + "act": 0.71013, + "null_pct": 0.92, + "null_med": 0.00504, + "null_p95": 0.71175, + "above_own_null_p95": false, + "argmax_off_px": 22.1, + "act_at_site": 0.11245, + "nearest_peak_px": 24.9, + "nearest_peak_score": 0.814 + }, + { + "city": "gainesville", + "pano": "HoOapW5g53qbEFjLA7yv3Q", + "x": 0.5438323641216953, + "y": 0.5186555832260843, + "field": "far", + "dist_m": 42.6, + "px": 18.4, + "group": "rated", + "verdict": "visible", + "act": 0.01008, + "null_pct": 0.425, + "null_med": 0.01636, + "null_p95": 0.77996, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.00919, + "nearest_peak_px": 98.4, + "nearest_peak_score": 0.94 + }, + { + "city": "gainesville", + "pano": "HoOapW5g53qbEFjLA7yv3Q", + "x": 0.49645185038970213, + "y": 0.5153353168595021, + "field": "far", + "dist_m": 51.9, + "px": 15.1, + "group": "rated", + "verdict": "visible", + "act": 0.02859, + "null_pct": 0.695, + "null_med": 0.01124, + "null_p95": 0.69386, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 4e-05, + "nearest_peak_px": 52.0, + "nearest_peak_score": 0.94 + }, + { + "city": "gainesville", + "pano": "KEwb17BzIjKVur60MpItuA", + "x": 0.4149449639405539, + "y": 0.5201712934675728, + "field": "far", + "dist_m": 39.4, + "px": 19.9, + "group": "rated", + "verdict": "visible", + "act": 0.00993, + "null_pct": 0.455, + "null_med": 0.01613, + "null_p95": 0.97489, + "above_own_null_p95": false, + "argmax_off_px": 22.0, + "act_at_site": 0.00286, + "nearest_peak_px": 77.5, + "nearest_peak_score": 0.975 + }, + { + "city": "gainesville", + "pano": "KEwb17BzIjKVur60MpItuA", + "x": 0.5282188547813629, + "y": 0.5106295616442303, + "field": "far", + "dist_m": 74.8, + "px": 10.5, + "group": "rated", + "verdict": "visible", + "act": 0.34073, + "null_pct": 0.885, + "null_med": 0.00752, + "null_p95": 0.65191, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.02027, + "nearest_peak_px": 37.6, + "nearest_peak_score": 0.881 + }, + { + "city": "gainesville", + "pano": "KEwb17BzIjKVur60MpItuA", + "x": 0.06060247094985196, + "y": 0.5317774160151664, + "field": "far", + "dist_m": 25.0, + "px": 31.3, + "group": "rated", + "verdict": "visible", + "act": 0.00068, + "null_pct": 0.12, + "null_med": 0.01494, + "null_p95": 0.88099, + "above_own_null_p95": false, + "argmax_off_px": 14.4, + "act_at_site": 0.0, + "nearest_peak_px": 93.7, + "nearest_peak_score": 0.387 + }, + { + "city": "gainesville", + "pano": "KbZFzQai9wfXijoQDscWFw", + "x": 0.5275994119413667, + "y": 0.5188418251833842, + "field": "far", + "dist_m": 42.2, + "px": 18.5, + "group": "rated", + "verdict": "visible", + "act": 0.40851, + "null_pct": 0.895, + "null_med": 0.00695, + "null_p95": 0.61287, + "above_own_null_p95": false, + "argmax_off_px": 21.9, + "act_at_site": 0.04844, + "nearest_peak_px": 30.8, + "nearest_peak_score": 0.467 + }, + { + "city": "gainesville", + "pano": "Mm5gyUYqmJmjeGopC1IgKg", + "x": 0.51082045775724, + "y": 0.5301554461174719, + "field": "far", + "dist_m": 26.3, + "px": 29.7, + "group": "rated", + "verdict": "visible", + "act": 0.16738, + "null_pct": 0.87, + "null_med": 0.0004, + "null_p95": 0.92031, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.01692, + "nearest_peak_px": 27.9, + "nearest_peak_score": 0.235 + }, + { + "city": "gainesville", + "pano": "PSS2iz32Sj0nE2ILJ6i8rA", + "x": 0.3940450819064303, + "y": 0.5218858587521047, + "field": "far", + "dist_m": 36.3, + "px": 21.5, + "group": "witnessed", + "verdict": null, + "act": 0.2538, + "null_pct": 0.785, + "null_med": 0.00652, + "null_p95": 0.95693, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.10707, + "nearest_peak_px": 23.5, + "nearest_peak_score": 0.26 + }, + { + "city": "gainesville", + "pano": "XuX3LCIaNmEI9FSBenjToA", + "x": 0.3716333464134779, + "y": 0.5417326217955608, + "field": "far", + "dist_m": 19.0, + "px": 41.3, + "group": "rated", + "verdict": "visible", + "act": 0.03549, + "null_pct": 0.685, + "null_med": 0.00299, + "null_p95": 0.39622, + "above_own_null_p95": false, + "argmax_off_px": 8.9, + "act_at_site": 0.0314, + "nearest_peak_px": 170.1, + "nearest_peak_score": 0.465 + }, + { + "city": "gainesville", + "pano": "_2GSllZzac7Zfd02VoVgCA", + "x": 0.07699117603437579, + "y": 0.5336038037736293, + "field": "far", + "dist_m": 23.6, + "px": 33.2, + "group": "rated", + "verdict": "visible", + "act": 0.04277, + "null_pct": 0.875, + "null_med": 0.00096, + "null_p95": 0.5174, + "above_own_null_p95": false, + "argmax_off_px": 13.2, + "act_at_site": 0.0384, + "nearest_peak_px": 43.9, + "nearest_peak_score": 0.118 + }, + { + "city": "gainesville", + "pano": "amZHKZbJ7pOJ9kvr2VlK5A", + "x": 0.8177778209339489, + "y": 0.7714141660748106, + "field": "near", + "dist_m": 2.2, + "px": 358.1, + "group": "witnessed", + "verdict": null, + "act": 0.04526, + "null_pct": 0.92, + "null_med": 0.00107, + "null_p95": 0.18638, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.00097, + "nearest_peak_px": 43.2, + "nearest_peak_score": 0.232 + }, + { + "city": "gainesville", + "pano": "dVrNCxUTFAfeyVNMXQ1LTw", + "x": 0.9172647622999139, + "y": 0.5348749208434239, + "field": "far", + "dist_m": 22.7, + "px": 34.4, + "group": "rated", + "verdict": "unclear", + "act": 0.03819, + "null_pct": 0.555, + "null_med": 0.0266, + "null_p95": 0.48771, + "above_own_null_p95": false, + "argmax_off_px": 8.8, + "act_at_site": 0.02392, + "nearest_peak_px": 168.3, + "nearest_peak_score": 0.446 + }, + { + "city": "gainesville", + "pano": "ipoVm9spHhQGCMROwSdmmA", + "x": 0.8437018555235514, + "y": 0.5851702349969968, + "field": "near", + "dist_m": 9.1, + "px": 85.8, + "group": "rated", + "verdict": "unclear", + "act": 0.00637, + "null_pct": 0.445, + "null_med": 0.00782, + "null_p95": 0.33869, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.0, + "nearest_peak_px": 62.0, + "nearest_peak_score": 0.874 + }, + { + "city": "gainesville", + "pano": "jL3wxV95A0lO2xKUQukpIg", + "x": 0.42924051720987244, + "y": 0.5374236192304312, + "field": "far", + "dist_m": 21.2, + "px": 37.0, + "group": "witnessed", + "verdict": null, + "act": 0.2477, + "null_pct": 0.915, + "null_med": 0.00081, + "null_p95": 0.69921, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.05639, + "nearest_peak_px": 27.5, + "nearest_peak_score": 0.254 + }, + { + "city": "gainesville", + "pano": "kozsgHrge4ofusqK2D_Hlw", + "x": 0.5380980504469067, + "y": 0.5128683014061106, + "field": "far", + "dist_m": 61.8, + "px": 12.7, + "group": "rated", + "verdict": "visible", + "act": 0.0995, + "null_pct": 0.91, + "null_med": 0.00249, + "null_p95": 0.21676, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.0059, + "nearest_peak_px": 45.7, + "nearest_peak_score": 0.737 + }, + { + "city": "gainesville", + "pano": "ld_LKRyg3hte-YCeFY3avQ", + "x": 0.9792845532492789, + "y": 0.5339509336319833, + "field": "far", + "dist_m": 23.4, + "px": 33.5, + "group": "witnessed", + "verdict": null, + "act": 0.0649, + "null_pct": 0.935, + "null_med": 0.0014, + "null_p95": 0.49211, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.03981, + "nearest_peak_px": 48.7, + "nearest_peak_score": 0.754 + }, + { + "city": "gainesville", + "pano": "q2yX7pwQF7_3SFGxEqlDHQ", + "x": 0.5433173942511713, + "y": 0.5106520927609703, + "field": "far", + "dist_m": 74.7, + "px": 10.5, + "group": "witnessed", + "verdict": null, + "act": 0.01476, + "null_pct": 0.685, + "null_med": 0.0029, + "null_p95": 0.7242, + "above_own_null_p95": false, + "argmax_off_px": 16.5, + "act_at_site": 0.00856, + "nearest_peak_px": 201.8, + "nearest_peak_score": 0.812 + }, + { + "city": "gainesville", + "pano": "qAP6awcZVDK19stGxM31jQ", + "x": 0.37373736757220644, + "y": 0.7051515151515152, + "field": "near", + "dist_m": 3.3, + "px": 235.2, + "group": "witnessed", + "verdict": null, + "act": 0.03248, + "null_pct": 1.0, + "null_med": 0.0, + "null_p95": 0.00858, + "above_own_null_p95": true, + "argmax_off_px": 17.7, + "act_at_site": 0.0129, + "nearest_peak_px": 203.8, + "nearest_peak_score": 0.148 + }, + { + "city": "gainesville", + "pano": "vbW2W3-AE2tr1NJYHCuHgw", + "x": 0.7385858524206913, + "y": 0.6914141845703125, + "field": "near", + "dist_m": 3.6, + "px": 214.7, + "group": "witnessed", + "verdict": null, + "act": 0.00489, + "null_pct": 0.87, + "null_med": 0.0, + "null_p95": 0.0349, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.0, + "nearest_peak_px": 59.0, + "nearest_peak_score": 0.092 + }, + { + "city": "gainesville", + "pano": "wwbkgmoKv4MS5sMy3PGhqA", + "x": 0.5989353478897853, + "y": 0.5208345420613185, + "field": "far", + "dist_m": 38.1, + "px": 20.5, + "group": "rated", + "verdict": "visible", + "act": 0.01115, + "null_pct": 0.79, + "null_med": 0.00551, + "null_p95": 0.01803, + "above_own_null_p95": false, + "argmax_off_px": 1.4, + "act_at_site": 0.01075, + "nearest_peak_px": 62.3, + "nearest_peak_score": 0.616 + }, + { + "city": "morgantown", + "pano": "1045092623940908", + "x": 0.6476767615116004, + "y": 0.637272764263731, + "field": "near", + "dist_m": 5.4, + "px": 144.0, + "group": "witnessed", + "verdict": null, + "act": 0.02084, + "null_pct": 0.66, + "null_med": 0.00886, + "null_p95": 0.13928, + "above_own_null_p95": false, + "argmax_off_px": 15.6, + "act_at_site": 0.01231, + "nearest_peak_px": 101.4, + "nearest_peak_score": 0.062 + }, + { + "city": "morgantown", + "pano": "1061541095152683", + "x": 0.10989899606415719, + "y": 0.5685858524206913, + "field": "near", + "dist_m": 11.4, + "px": 68.5, + "group": "witnessed", + "verdict": null, + "act": 0.08295, + "null_pct": 0.745, + "null_med": 0.01619, + "null_p95": 0.70041, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.02479, + "nearest_peak_px": 32.3, + "nearest_peak_score": 0.148 + }, + { + "city": "morgantown", + "pano": "1061541095152683", + "x": 0.019393939393939394, + "y": 0.5378788063742898, + "field": "far", + "dist_m": 20.9, + "px": 37.4, + "group": "witnessed", + "verdict": null, + "act": 0.59163, + "null_pct": 1.0, + "null_med": 0.0151, + "null_p95": 0.42915, + "above_own_null_p95": true, + "argmax_off_px": 21.9, + "act_at_site": 0.31872, + "nearest_peak_px": 31.9, + "nearest_peak_score": 0.61 + }, + { + "city": "morgantown", + "pano": "1159131791921689", + "x": 0.2137373721960819, + "y": 0.6720201896898674, + "field": "near", + "dist_m": 4.2, + "px": 187.7, + "group": "rated", + "verdict": "unclear", + "act": 0.09927, + "null_pct": 0.99, + "null_med": 0.00426, + "null_p95": 0.07942, + "above_own_null_p95": true, + "argmax_off_px": 22.5, + "act_at_site": 0.00911, + "nearest_peak_px": 33.9, + "nearest_peak_score": 0.117 + }, + { + "city": "morgantown", + "pano": "1314106923314560", + "x": 0.21137609272522312, + "y": 0.5872850842920392, + "field": "near", + "dist_m": 8.9, + "px": 88.0, + "group": "rated", + "verdict": "visible", + "act": 0.00541, + "null_pct": 0.465, + "null_med": 0.0068, + "null_p95": 0.94761, + "above_own_null_p95": false, + "argmax_off_px": 9.1, + "act_at_site": 0.00268, + "nearest_peak_px": 107.9, + "nearest_peak_score": 0.177 + }, + { + "city": "morgantown", + "pano": "1413692656166618", + "x": 0.6690909322102865, + "y": 0.6211110987807765, + "field": "near", + "dist_m": 6.3, + "px": 125.2, + "group": "witnessed", + "verdict": null, + "act": 0.02416, + "null_pct": 0.525, + "null_med": 0.0177, + "null_p95": 0.89464, + "above_own_null_p95": false, + "argmax_off_px": 6.6, + "act_at_site": 0.01795, + "nearest_peak_px": 238.9, + "nearest_peak_score": 0.87 + }, + { + "city": "morgantown", + "pano": "1475722139747056", + "x": 0.017508421618529037, + "y": 0.5198316847675978, + "field": "far", + "dist_m": 40.1, + "px": 19.5, + "group": "witnessed", + "verdict": null, + "act": 0.07888, + "null_pct": 0.885, + "null_med": 0.00121, + "null_p95": 0.37287, + "above_own_null_p95": false, + "argmax_off_px": 18.0, + "act_at_site": 0.022, + "nearest_peak_px": 46.8, + "nearest_peak_score": 0.871 + }, + { + "city": "morgantown", + "pano": "1560632374758055", + "x": 0.014141420306581439, + "y": 0.6687879157788825, + "field": "near", + "dist_m": 4.3, + "px": 183.5, + "group": "rated", + "verdict": "visible", + "act": 0.75602, + "null_pct": 1.0, + "null_med": 0.00117, + "null_p95": 0.0436, + "above_own_null_p95": true, + "argmax_off_px": 18.6, + "act_at_site": 0.19581, + "nearest_peak_px": 163.1, + "nearest_peak_score": 0.938 + }, + { + "city": "morgantown", + "pano": "272487405737151", + "x": 0.652121230616714, + "y": 0.7067676890980114, + "field": "near", + "dist_m": 3.3, + "px": 237.7, + "group": "witnessed", + "verdict": null, + "act": 0.02667, + "null_pct": 0.91, + "null_med": 0.00186, + "null_p95": 0.03559, + "above_own_null_p95": false, + "argmax_off_px": 19.0, + "act_at_site": 0.01583, + "nearest_peak_px": 149.6, + "nearest_peak_score": 0.674 + }, + { + "city": "morgantown", + "pano": "307295108505220", + "x": 0.33939395788944127, + "y": 0.6679798103101325, + "field": "near", + "dist_m": 4.3, + "px": 182.4, + "group": "rated", + "verdict": "visible", + "act": 0.13628, + "null_pct": 0.99, + "null_med": 0.00831, + "null_p95": 0.10338, + "above_own_null_p95": true, + "argmax_off_px": 22.1, + "act_at_site": 0.06337, + "nearest_peak_px": 32.1, + "nearest_peak_score": 0.16 + }, + { + "city": "morgantown", + "pano": "307295108505220", + "x": 0.7777778209339489, + "y": 0.623535341205019, + "field": "near", + "dist_m": 6.1, + "px": 127.9, + "group": "witnessed", + "verdict": null, + "act": 0.04, + "null_pct": 0.59, + "null_med": 0.02187, + "null_p95": 0.29731, + "above_own_null_p95": false, + "argmax_off_px": 17.1, + "act_at_site": 0.02551, + "nearest_peak_px": 143.3, + "nearest_peak_score": 0.107 + }, + { + "city": "morgantown", + "pano": "356422300574178", + "x": 0.1517460980720612, + "y": 0.5434221681507451, + "field": "far", + "dist_m": 18.2, + "px": 43.0, + "group": "witnessed", + "verdict": null, + "act": 0.02629, + "null_pct": 0.42, + "null_med": 0.11222, + "null_p95": 0.88157, + "above_own_null_p95": false, + "argmax_off_px": 10.2, + "act_at_site": 0.02426, + "nearest_peak_px": 79.5, + "nearest_peak_score": 0.338 + }, + { + "city": "morgantown", + "pano": "356422300574178", + "x": 0.09898990515506628, + "y": 0.5483838815400095, + "field": "near", + "dist_m": 16.3, + "px": 47.9, + "group": "rated", + "verdict": "definition", + "act": 0.307, + "null_pct": 0.815, + "null_med": 0.0385, + "null_p95": 0.81614, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.01305, + "nearest_peak_px": 25.5, + "nearest_peak_score": 0.338 + }, + { + "city": "morgantown", + "pano": "390232193697883", + "x": 0.4667830239590223, + "y": 0.5324315816214311, + "field": "far", + "dist_m": 24.5, + "px": 32.0, + "group": "rated", + "verdict": "visible", + "act": 0.0094, + "null_pct": 0.65, + "null_med": 0.00158, + "null_p95": 0.58822, + "above_own_null_p95": false, + "argmax_off_px": 10.3, + "act_at_site": 0.00366, + "nearest_peak_px": 53.2, + "nearest_peak_score": 0.195 + }, + { + "city": "morgantown", + "pano": "413429987710546", + "x": 0.9138790294879624, + "y": 0.528191989328837, + "field": "far", + "dist_m": 28.2, + "px": 27.8, + "group": "witnessed", + "verdict": null, + "act": 0.01013, + "null_pct": 0.31, + "null_med": 0.02036, + "null_p95": 0.28096, + "above_own_null_p95": false, + "argmax_off_px": 13.0, + "act_at_site": 0.00425, + "nearest_peak_px": 109.3, + "nearest_peak_score": 0.854 + }, + { + "city": "morgantown", + "pano": "559825463037840", + "x": 0.469440809267355, + "y": 0.5187997158948636, + "field": "far", + "dist_m": 42.3, + "px": 18.5, + "group": "below_floor", + "verdict": null, + "act": 0.59538, + "null_pct": 1.0, + "null_med": 0.00145, + "null_p95": 0.34272, + "above_own_null_p95": true, + "argmax_off_px": 21.8, + "act_at_site": 0.0929, + "nearest_peak_px": 23.6, + "nearest_peak_score": 0.619 + }, + { + "city": "morgantown", + "pano": "716002773841007", + "x": 0.7119106513357678, + "y": 0.5478255162205575, + "field": "near", + "dist_m": 16.5, + "px": 47.4, + "group": "rated", + "verdict": "unclear", + "act": 0.04758, + "null_pct": 0.735, + "null_med": 0.00188, + "null_p95": 0.65085, + "above_own_null_p95": false, + "argmax_off_px": 5.4, + "act_at_site": 0.04533, + "nearest_peak_px": 65.0, + "nearest_peak_score": 0.202 + }, + { + "city": "morgantown", + "pano": "730248625779882", + "x": 0.12202020818536932, + "y": 0.5839394309303977, + "field": "near", + "dist_m": 9.3, + "px": 84.5, + "group": "witnessed", + "verdict": null, + "act": 0.04757, + "null_pct": 0.845, + "null_med": 0.00222, + "null_p95": 0.86664, + "above_own_null_p95": false, + "argmax_off_px": 0.9, + "act_at_site": 0.04639, + "nearest_peak_px": 147.7, + "nearest_peak_score": 0.961 + }, + { + "city": "morgantown", + "pano": "7393628424017132", + "x": 0.6549494887843277, + "y": 0.6720202451763732, + "field": "near", + "dist_m": 4.2, + "px": 187.7, + "group": "witnessed", + "verdict": null, + "act": 0.21051, + "null_pct": 1.0, + "null_med": 0.00071, + "null_p95": 0.0364, + "above_own_null_p95": true, + "argmax_off_px": 22.5, + "act_at_site": 0.01909, + "nearest_peak_px": 37.2, + "nearest_peak_score": 0.794 + }, + { + "city": "morgantown", + "pano": "775966364379699", + "x": 0.7951515336470171, + "y": 0.6922222345525568, + "field": "near", + "dist_m": 3.6, + "px": 215.9, + "group": "witnessed", + "verdict": null, + "act": 0.17681, + "null_pct": 1.0, + "null_med": 0.00294, + "null_p95": 0.16715, + "above_own_null_p95": true, + "argmax_off_px": 22.5, + "act_at_site": 0.06538, + "nearest_peak_px": 27.6, + "nearest_peak_score": 0.221 + }, + { + "city": "paterson", + "pano": "0Drku25sOlOlWGiVf7uetw", + "x": 0.4169921875, + "y": 0.568359375, + "field": "near", + "dist_m": 11.5, + "px": 68.3, + "group": "witnessed", + "verdict": null, + "act": 0.85164, + "null_pct": 0.795, + "null_med": 0.01799, + "null_p95": 0.89765, + "above_own_null_p95": false, + "argmax_off_px": 22.1, + "act_at_site": 0.64725, + "nearest_peak_px": 24.0, + "nearest_peak_score": 0.855 + }, + { + "city": "paterson", + "pano": "3BTAarqIRrxbpFHUFJhtSQ", + "x": 0.4417136387718572, + "y": 0.5472501048800457, + "field": "near", + "dist_m": 16.7, + "px": 46.8, + "group": "witnessed", + "verdict": null, + "act": 0.79798, + "null_pct": 0.935, + "null_med": 0.00479, + "null_p95": 0.87761, + "above_own_null_p95": false, + "argmax_off_px": 21.9, + "act_at_site": 0.51615, + "nearest_peak_px": 22.9, + "nearest_peak_score": 0.799 + }, + { + "city": "paterson", + "pano": "UOMaPWpm2CRHbfqMgY_hbQ", + "x": 0.9690277847985136, + "y": 0.5280885169135339, + "field": "far", + "dist_m": 28.3, + "px": 27.7, + "group": "witnessed", + "verdict": null, + "act": 0.02564, + "null_pct": 0.835, + "null_med": 0.00099, + "null_p95": 0.87846, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.00382, + "nearest_peak_px": 54.0, + "nearest_peak_score": 0.833 + }, + { + "city": "paterson", + "pano": "WNiUHjkafGBEnMc0nc4wkQ", + "x": 0.04029821423614809, + "y": 0.5180162161163532, + "field": "far", + "dist_m": 44.1, + "px": 17.7, + "group": "rated", + "verdict": "visible", + "act": 0.21378, + "null_pct": 0.87, + "null_med": 0.00076, + "null_p95": 0.92191, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.0505, + "nearest_peak_px": 36.4, + "nearest_peak_score": 0.965 + }, + { + "city": "paterson", + "pano": "Xs3lDDc1o50wJmW9eGx_WA", + "x": 0.2323375734540243, + "y": 0.5969844818988245, + "field": "near", + "dist_m": 7.9, + "px": 98.4, + "group": "rated", + "verdict": "context-only", + "act": 0.77821, + "null_pct": 0.915, + "null_med": 0.00485, + "null_p95": 0.8825, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.02851, + "nearest_peak_px": 30.6, + "nearest_peak_score": 0.904 + }, + { + "city": "paterson", + "pano": "Z929dNP7sDp8cU6sjIeKAw", + "x": 0.3301010039358428, + "y": 0.583939393939394, + "field": "near", + "dist_m": 9.3, + "px": 84.5, + "group": "witnessed", + "verdict": null, + "act": 0.81958, + "null_pct": 0.96, + "null_med": 0.00293, + "null_p95": 0.79713, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.4664, + "nearest_peak_px": 27.2, + "nearest_peak_score": 0.957 + }, + { + "city": "paterson", + "pano": "Z929dNP7sDp8cU6sjIeKAw", + "x": 0.6488888827237216, + "y": 0.5887879157788826, + "field": "near", + "dist_m": 8.7, + "px": 89.6, + "group": "witnessed", + "verdict": null, + "act": 0.81925, + "null_pct": 0.91, + "null_med": 0.00293, + "null_p95": 0.9567, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.38388, + "nearest_peak_px": 25.3, + "nearest_peak_score": 0.927 + }, + { + "city": "paterson", + "pano": "_FfdhRGlkbTABymQR3t1_A", + "x": 0.534812514379721, + "y": 0.5261813014280383, + "field": "far", + "dist_m": 30.3, + "px": 25.8, + "group": "rated", + "verdict": "visible", + "act": 0.44835, + "null_pct": 0.79, + "null_med": 0.00213, + "null_p95": 0.87199, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.1239, + "nearest_peak_px": 23.4, + "nearest_peak_score": 0.459 + }, + { + "city": "paterson", + "pano": "cuFkPrv4UsqzeJCdibl1FQ", + "x": 0.4699032749323206, + "y": 0.5301038638689053, + "field": "far", + "dist_m": 26.4, + "px": 29.7, + "group": "rated", + "verdict": "visible", + "act": 0.15272, + "null_pct": 0.915, + "null_med": 0.00236, + "null_p95": 0.21229, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.09947, + "nearest_peak_px": 37.4, + "nearest_peak_score": 0.212 + }, + { + "city": "paterson", + "pano": "gZ3Acgi38PrzOVBkW-Il4Q", + "x": 0.9907032660546297, + "y": 0.5340394347088994, + "field": "far", + "dist_m": 23.3, + "px": 33.6, + "group": "witnessed", + "verdict": null, + "act": 0.76016, + "null_pct": 1.0, + "null_med": 0.0, + "null_p95": 0.09112, + "above_own_null_p95": true, + "argmax_off_px": 5.7, + "act_at_site": 0.71407, + "nearest_peak_px": 104.0, + "nearest_peak_score": 0.902 + }, + { + "city": "paterson", + "pano": "hV6LwTvkCzyX_vQF3ZG5qQ", + "x": 0.21933867134549537, + "y": 0.5496919988638095, + "field": "near", + "dist_m": 15.9, + "px": 49.2, + "group": "rated", + "verdict": "visible", + "act": 0.04318, + "null_pct": 0.735, + "null_med": 0.01158, + "null_p95": 0.8787, + "above_own_null_p95": false, + "argmax_off_px": 4.9, + "act_at_site": 0.03762, + "nearest_peak_px": 51.5, + "nearest_peak_score": 0.879 + }, + { + "city": "paterson", + "pano": "hV6LwTvkCzyX_vQF3ZG5qQ", + "x": 0.495205520930847, + "y": 0.519235968880955, + "field": "far", + "dist_m": 41.3, + "px": 18.9, + "group": "rated", + "verdict": "visible", + "act": 0.03976, + "null_pct": 0.83, + "null_med": 0.00875, + "null_p95": 0.57709, + "above_own_null_p95": false, + "argmax_off_px": 21.9, + "act_at_site": 0.02798, + "nearest_peak_px": 31.8, + "nearest_peak_score": 0.054 + }, + { + "city": "paterson", + "pano": "ijlwBkjXDfD2wCO_jSjCGA", + "x": 0.05030611525018135, + "y": 0.5254146881445788, + "field": "far", + "dist_m": 31.2, + "px": 25.0, + "group": "witnessed", + "verdict": null, + "act": 0.79011, + "null_pct": 0.95, + "null_med": 7e-05, + "null_p95": 0.82639, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.23676, + "nearest_peak_px": 25.5, + "nearest_peak_score": 0.906 + }, + { + "city": "paterson", + "pano": "o5_tCsE_7cR4lTvyrqy8mg", + "x": 0.43838388154000946, + "y": 0.5693939393939393, + "field": "near", + "dist_m": 11.3, + "px": 69.3, + "group": "witnessed", + "verdict": null, + "act": 0.83803, + "null_pct": 0.97, + "null_med": 0.00579, + "null_p95": 0.78774, + "above_own_null_p95": true, + "argmax_off_px": 21.9, + "act_at_site": 0.31219, + "nearest_peak_px": 28.9, + "nearest_peak_score": 0.86 + }, + { + "city": "richmond", + "pano": "1023400645666420", + "x": 0.2294949433297822, + "y": 0.5919191857540246, + "field": "near", + "dist_m": 8.4, + "px": 93.0, + "group": "witnessed", + "verdict": null, + "act": 0.23686, + "null_pct": 0.915, + "null_med": 0.01908, + "null_p95": 0.35074, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.01796, + "nearest_peak_px": 24.3, + "nearest_peak_score": 0.242 + }, + { + "city": "richmond", + "pano": "1130501775077894", + "x": 0.7943434281782671, + "y": 0.6032323293974905, + "field": "near", + "dist_m": 7.4, + "px": 105.2, + "group": "witnessed", + "verdict": null, + "act": 0.13406, + "null_pct": 0.44, + "null_med": 0.18214, + "null_p95": 0.96163, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.01453, + "nearest_peak_px": 35.0, + "nearest_peak_score": 0.363 + }, + { + "city": "richmond", + "pano": "1263975528308076", + "x": 0.9712878880356297, + "y": 0.5335353504527698, + "field": "far", + "dist_m": 23.6, + "px": 33.1, + "group": "rated", + "verdict": "visible", + "act": 0.52351, + "null_pct": 0.72, + "null_med": 0.01795, + "null_p95": 0.95893, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.07301, + "nearest_peak_px": 33.1, + "nearest_peak_score": 0.894 + }, + { + "city": "richmond", + "pano": "1288163659019705", + "x": 0.2791919130267519, + "y": 0.6537373675722065, + "field": "near", + "dist_m": 4.8, + "px": 164.1, + "group": "rated", + "verdict": "visible", + "act": 0.12626, + "null_pct": 0.86, + "null_med": 0.00255, + "null_p95": 0.36649, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.01046, + "nearest_peak_px": 42.9, + "nearest_peak_score": 0.512 + }, + { + "city": "richmond", + "pano": "1338631107629110", + "x": 0.5325252463600852, + "y": 0.4973737312085701, + "field": "far", + "dist_m": 150.0, + "px": 5.2, + "group": "below_floor", + "verdict": null, + "act": 0.7454, + "null_pct": 0.99, + "null_med": 0.00262, + "null_p95": 0.40661, + "above_own_null_p95": true, + "argmax_off_px": 22.4, + "act_at_site": 0.2072, + "nearest_peak_px": 29.0, + "nearest_peak_score": 0.875 + }, + { + "city": "richmond", + "pano": "1759178911495771", + "x": 0.5197303339935238, + "y": 0.5219953845879944, + "field": "far", + "dist_m": 36.1, + "px": 21.7, + "group": "rated", + "verdict": "visible", + "act": 0.18581, + "null_pct": 0.855, + "null_med": 0.00299, + "null_p95": 0.45947, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.03628, + "nearest_peak_px": 39.5, + "nearest_peak_score": 0.693 + }, + { + "city": "richmond", + "pano": "372149922554566", + "x": 0.03474748091264204, + "y": 0.5273737312085701, + "field": "far", + "dist_m": 29.0, + "px": 27.0, + "group": "witnessed", + "verdict": null, + "act": 0.60787, + "null_pct": 0.935, + "null_med": 0.00026, + "null_p95": 0.66182, + "above_own_null_p95": false, + "argmax_off_px": 22.2, + "act_at_site": 0.08643, + "nearest_peak_px": 26.8, + "nearest_peak_score": 0.648 + }, + { + "city": "richmond", + "pano": "386363531219281", + "x": 0.4905050936612216, + "y": 0.5144444876006156, + "field": "far", + "dist_m": 55.1, + "px": 14.2, + "group": "rated", + "verdict": "visible", + "act": 0.05344, + "null_pct": 0.765, + "null_med": 0.00616, + "null_p95": 0.71793, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.04411, + "nearest_peak_px": 51.3, + "nearest_peak_score": 0.847 + }, + { + "city": "richmond", + "pano": "3956582084664486", + "x": 0.46505054820667613, + "y": 0.5192929169625947, + "field": "far", + "dist_m": 41.2, + "px": 19.0, + "group": "witnessed", + "verdict": null, + "act": 0.82894, + "null_pct": 0.985, + "null_med": 0.00038, + "null_p95": 0.42644, + "above_own_null_p95": true, + "argmax_off_px": 21.7, + "act_at_site": 0.33381, + "nearest_peak_px": 26.2, + "nearest_peak_score": 0.891 + }, + { + "city": "richmond", + "pano": "423989957171190", + "x": 0.42667711943494924, + "y": 0.5250655456146334, + "field": "far", + "dist_m": 31.7, + "px": 24.7, + "group": "witnessed", + "verdict": null, + "act": 0.03764, + "null_pct": 0.855, + "null_med": 0.00513, + "null_p95": 0.85755, + "above_own_null_p95": false, + "argmax_off_px": 2.1, + "act_at_site": 0.03594, + "nearest_peak_px": 57.4, + "nearest_peak_score": 0.858 + }, + { + "city": "richmond", + "pano": "423989957171190", + "x": 0.5133691152905883, + "y": 0.5247863607532633, + "field": "far", + "dist_m": 32.0, + "px": 24.4, + "group": "witnessed", + "verdict": null, + "act": 0.73132, + "null_pct": 0.9, + "null_med": 0.00532, + "null_p95": 0.85773, + "above_own_null_p95": false, + "argmax_off_px": 22.5, + "act_at_site": 0.11268, + "nearest_peak_px": 30.9, + "nearest_peak_score": 0.861 + }, + { + "city": "richmond", + "pano": "423989957171190", + "x": 0.5291509811918936, + "y": 0.5252428801194406, + "field": "far", + "dist_m": 31.5, + "px": 24.9, + "group": "witnessed", + "verdict": null, + "act": 0.86011, + "null_pct": 1.0, + "null_med": 0.00649, + "null_p95": 0.85773, + "above_own_null_p95": true, + "argmax_off_px": 21.7, + "act_at_site": 0.29615, + "nearest_peak_px": 22.7, + "nearest_peak_score": 0.861 + }, + { + "city": "richmond", + "pano": "593421160518191", + "x": 0.7702458544241675, + "y": 0.5173858634506956, + "field": "far", + "dist_m": 45.7, + "px": 17.1, + "group": "witnessed", + "verdict": null, + "act": 0.01255, + "null_pct": 0.115, + "null_med": 0.0757, + "null_p95": 0.90758, + "above_own_null_p95": false, + "argmax_off_px": 10.2, + "act_at_site": 0.00951, + "nearest_peak_px": 70.4, + "nearest_peak_score": 0.339 + }, + { + "city": "richmond", + "pano": "639645829096010", + "x": 0.23492423780036692, + "y": 0.5502974155481681, + "field": "near", + "dist_m": 15.7, + "px": 49.9, + "group": "witnessed", + "verdict": null, + "act": 0.02843, + "null_pct": 0.475, + "null_med": 0.05058, + "null_p95": 0.84704, + "above_own_null_p95": false, + "argmax_off_px": 10.7, + "act_at_site": 0.02378, + "nearest_peak_px": 42.5, + "nearest_peak_score": 0.143 + }, + { + "city": "richmond", + "pano": "662385816800591", + "x": 0.44464645848129736, + "y": 0.5070707009055397, + "field": "far", + "dist_m": 112.5, + "px": 7.0, + "group": "witnessed", + "verdict": null, + "act": 0.14685, + "null_pct": 0.88, + "null_med": 0.00949, + "null_p95": 0.60457, + "above_own_null_p95": false, + "argmax_off_px": 22.3, + "act_at_site": 0.00867, + "nearest_peak_px": 44.1, + "nearest_peak_score": 0.835 + }, + { + "city": "richmond", + "pano": "662385816800591", + "x": 0.5236363821318656, + "y": 0.5209091094045928, + "field": "far", + "dist_m": 38.0, + "px": 20.6, + "group": "witnessed", + "verdict": null, + "act": 0.42569, + "null_pct": 0.91, + "null_med": 0.01023, + "null_p95": 0.82634, + "above_own_null_p95": false, + "argmax_off_px": 22.4, + "act_at_site": 0.21966, + "nearest_peak_px": 36.0, + "nearest_peak_score": 0.605 + }, + { + "city": "richmond", + "pano": "723487737079243", + "x": 0.006868693033854166, + "y": 0.5435353966915246, + "field": "far", + "dist_m": 18.2, + "px": 43.1, + "group": "rated", + "verdict": "visible", + "act": 0.94587, + "null_pct": 0.965, + "null_med": 0.01808, + "null_p95": 0.945, + "above_own_null_p95": true, + "argmax_off_px": 7.4, + "act_at_site": 0.77932, + "nearest_peak_px": 117.4, + "nearest_peak_score": 0.945 + } + ] +} \ No newline at end of file diff --git a/docs/curb_ramp_data_sourcing.md b/docs/curb_ramp_data_sourcing.md index 0a5d88d..c82caed 100644 --- a/docs/curb_ramp_data_sourcing.md +++ b/docs/curb_ramp_data_sourcing.md @@ -425,15 +425,65 @@ finding and with the human verdicts, and inconsistent with reading "more example pixels" as a claim about *reachability*. (As a claim about pixels it remains true; the error was inferring unreachability from it.) +### Phase 1: attenuated or absent? Almost never absent + +`silent` is a statement about **peaks** — no `peak_local_max` peak ≥ 0.05 within the match radius. +Phase 1 makes the statement about the **heatmap**: `scripts/analysis/silent_activation.py` +(14 tests) loads the published checkpoint (`projectsidewalk/rampnet-model` — the weights every +committed cache came from), runs one pass per panorama holding a silent miss (single-pass fp32, +matching `op_cache`), and reads the max heatmap value inside the match radius. The scaled matcher +space *is* the 512×1024 heatmap grid, so the window is exactly the matcher's. Result JSON: +`analysis_out/silent_activation.json`; run on the local RTX 3070, all 128 pooled silent misses. + +| population | n | act q1 / med / q3 | act ≥ 0.01 | +| :--- | ---: | :---: | ---: | +| near / rated | 13 | 0.009 / 0.099 / 0.197 | 9 | +| near / witnessed | 32 | 0.033 / 0.211 / 0.592 | 30 | +| far / rated | 37 | 0.022 / 0.076 / 0.409 | 34 | +| far / below-floor | 9 | 0.042 / 0.194 / 0.381 | 8 | +| far / witnessed | 37 | 0.045 / 0.188 / 0.615 | 37 | +| **all silent misses** | **128** | 0.032 / 0.136 / 0.548 | **118** | + +What that in-window mass *is* (classes are act ranges; the offset and nearest-peak columns +confirm the intended reading rather than define it): + +| class | definition | n | near / far | rated `visible` | argmax offset med | nearest floor peak med | +| :--- | :--- | ---: | :---: | ---: | ---: | ---: | +| **absent** | act < 0.01 | **10** | 6 / 4 | 5 | 22.0 px | 77.5 px (3.4 R) | +| **faint local** | 0.01 ≤ act < 0.05 | 39 | 12 / 27 | 13 | **10.2 px** | 85.6 px (3.8 R) | +| **tail** | act ≥ 0.05 | 79 | 27 / 52 | 23 | 22.3 px | **31.1 px (1.4 R)** | + +- **Only 10 of 128 silent misses (8%) have a genuinely flat heatmap.** "Silent = the model saw + nothing" is wrong for 92% of the bucket; `silent` was peak bookkeeping, not absence of response. +- **62% are a neighbouring mode's tail.** The argmax sits in the window's outer quarter in 75 of + 79, and the nearest cached floor peak is ~1.4 R away with **median score 0.685** — a *confident* + adjacent detection (70/79 within 2 R). That mode is a neighbour ramp's TP, an FP, or plausibly + this very ramp localized just outside the radius — the `localization` bucket only inspects + *kept* (≥ 0.30) annulus peaks, so a floor-level one leaves a miss "silent". Whichever it is, + this is the σ/representation family again (`merged`'s mechanism), not vocabulary. +- **30% are a faint local response at the site itself** (mass on-site in 30 of 39, nothing else + within ~3.8 R) — the `sub_threshold` continuum extending below the floor. Attenuation, not + blindness. +- For the far-field rated-`visible` population — the anomaly itself — the split is **3 absent / + 12 faint-local / 19 tail**: the model is responding at or next to ~91% of the far ramps a human + called resolvable. Consistent with Phase 0's graded-sensitivity reading; squarely against a + vocabulary hole. +- The strict per-pano null (azimuth-randomized at the site's elevation, self-excluding within 2 R) + passes 31/128 at its p95 — a deliberately hard bar, since the p95 is set by the pano's strongest + modes; the decomposition above is the sharper lens. + ### What changes, what does not, and what is still open - **§0a's measured split stands** (247 far / 180 near at 18 m). What falls is the hard binary in its "fixable by" column: the far field is *harder*, not *unreachable*. - **The sourcing bracket (§0b) excluded all 83 far-field silent misses from the addressable - population because of that binary.** That exclusion is no longer safe. The 0.013 point estimate - is deliberately **not revised** here: whether these specific misses are recoverable is what - Phases 1–3 of the #46 study measure (heatmap activation forensics; the scale counterfactual; - the decoy control on the verdicts themselves). Quote 0.013 with this section attached. + population because of that binary.** That exclusion is no longer safe — but Phase 1 cuts the + other way too: of the 45 *near-field* silent misses the 0.013 estimate rests on, only **6 are + heatmap-absent**; the rest are faint-local (12) or an adjacent confident mode (27), i.e. the + calibration and σ families §0b already prices separately. The 0.013 point estimate is + deliberately **not revised** in either direction until Phase 2 (the scale counterfactual, whose + primary target is now the 10 absent sites plus whether scale lifts faint-local over the floor) + and Phase 3 (the decoy control on the verdicts) run. Quote 0.013 with this section attached. - **Multi-view's remedy logic is untouched** — a ramp invisible at 30 m is at 8 m two panoramas later whatever the failure mechanism — but §0a's "MV ceiling" column shares the binary assumption and will move with the same phases. diff --git a/scripts/analysis/silent_activation.py b/scripts/analysis/silent_activation.py index b1edc7d..be873ff 100644 --- a/scripts/analysis/silent_activation.py +++ b/scripts/analysis/silent_activation.py @@ -71,12 +71,23 @@ def radius_max(heat, x, y, radius_sq=None): to [0, 1] exactly as ``peaks_to_dets`` clips before peak extraction, so an ``act`` here and a peak score there are on the same scale. """ + return site_profile(heat, x, y, radius_sq)[0] + + +def site_profile(heat, x, y, radius_sq=None): + """``(act, off_px, center)`` for the window around normalized ``(x, y)``. + + ``off_px`` is how far from the site the in-window maximum sits, and ``center`` + is the value at the site itself — together they separate a response *at* the + ramp from a neighbouring mode's tail reaching *into* the window, which ``act`` + alone cannot do (and 62% of silent misses turn out to need the distinction). + """ if radius_sq is None: radius_sq = radius_sq_for() H, W = len(heat), len(heat[0]) r = radius_sq ** 0.5 cx, cy = x * W, y * H - best = 0.0 + best, off = 0.0, 0.0 for row in range(max(0, int(cy - r)), min(H, int(cy + r) + 2)): dy2 = (row - cy) ** 2 if dy2 >= radius_sq: @@ -86,10 +97,33 @@ def radius_max(heat, x, y, radius_sq=None): dx = col - cx if dx * dx + dy2 >= radius_sq: continue - v = heat[row][col % W] + v = min(float(heat[row][col % W]), 1.0) if v > best: - best = min(float(v), 1.0) - return best + best, off = v, (dx * dx + dy2) ** 0.5 + center = min(float(heat[min(H - 1, max(0, round(cy)))] + [round(cx) % W]), 1.0) + return best, off, max(center, 0.0) + + +def nearest_peak(preds, x, y): + """``(dist_px, score)`` of the closest cached floor peak, in matcher units. + + ``preds`` are the panorama's cached ``(x, y, score)`` floor peaks (>= 0.05). + For a silent miss any such peak is by definition OUTSIDE the match radius, so + this measures how far away the nearest thing the model actually said is — + the difference between "a neighbouring mode's tail reaches the site" (~1-2 + radii) and "the nearest response is nowhere near" (many radii). + """ + if not preds: + return float("inf"), None + best, score = float("inf"), None + for p in preds: + dx = abs(p[0] - x) * PANO_SCALE_X + dx = min(dx, PANO_SCALE_X - dx) + d = (dx * dx + ((p[1] - y) * PANO_SCALE_Y) ** 2) ** 0.5 + if d < best: + best, score = d, p[2] + return best, score def null_percentile(heat, x, y, rng, trials=NULL_TRIALS, radius_sq=None): @@ -160,8 +194,9 @@ def main(argv=None): rated_by_rowkey = {(v["city"], v["pano"], round(float(v["x"]), 6), round(float(v["y"]), 6)): v for v in rated.values()} + from operating_point_curve import CACHE_DIR, read_cache cities = [c.strip() for c in args.cities.split(",") if c.strip()] - by_pano = {} + by_pano, preds_by = {}, {} for city in cities: loaded = mt.load_rows(city, args.threshold, rng=None) if loaded is None: @@ -169,6 +204,9 @@ def main(argv=None): for r in loaded[0]: if not r["hit"] and r["bucket"] == "silent": by_pano.setdefault((city, r["pano"]), []).append(r) + panos, _ = read_cache(os.path.join(CACHE_DIR, f"{city}.json")) + for pd in panos: + preds_by[(city, pd["pano"])] = pd["preds"] n_miss = sum(len(v) for v in by_pano.values()) print(f"=== Silent-miss activation forensics (threshold {args.threshold}, " f"{n_miss} misses in {len(by_pano)} panos, #46 Phase 1) ===", flush=True) @@ -190,6 +228,9 @@ def main(argv=None): for r in misses: act, pct, null_med, null_p95 = null_percentile( heat, r["x"], r["y"], rng, radius_sq=radius_sq) + _, off_px, center = site_profile(heat, r["x"], r["y"], radius_sq) + npk_px, npk_score = nearest_peak(preds_by.get((city, pano), []), + r["x"], r["y"]) key = row_key(r) v = rated_by_rowkey.get(key) results.append({ @@ -201,6 +242,12 @@ def main(argv=None): "act": round(act, 5), "null_pct": round(pct, 3), "null_med": round(null_med, 5), "null_p95": round(null_p95, 5), "above_own_null_p95": act > null_p95, + "argmax_off_px": round(off_px, 1), + "act_at_site": round(center, 5), + "nearest_peak_px": (round(npk_px, 1) + if npk_px != float("inf") else None), + "nearest_peak_score": (round(npk_score, 3) + if npk_score is not None else None), }) del heat if i % 10 == 0: @@ -239,16 +286,45 @@ def main(argv=None): print(f"\n rated `visible` only (n={len(vis)}): act q1/med/q3 " f"{q[0]:.4f}/{q[1]:.4f}/{q[2]:.4f}; {n_sig}/{len(vis)} above their " f"own pano's null p95") - shoulders = [r for r in results if r["act"] >= 0.05] - print(f"\n `act` >= 0.05 without a peak (shoulder of a neighbouring mode): " - f"{len(shoulders)}") - - print(f"\n Reading: 'above own null p95' is the localized-signal test. A miss") - print(f" passing it has a real, faint, spatially specific response — attenuation,") - print(f" the sub_threshold continuum's tail. A miss failing it is indistinguishable") - print(f" from the panorama's noise floor — absence. The two populations continue") - print(f" to different phases: attenuation prices into threshold/calibration work,") - print(f" absence goes to Phase 2's scale counterfactual.") + + # What the in-window mass actually IS. A silent miss has no floor peak in + # radius by definition, so act >= 0.05 can only be an outside mode's tail; + # the argmax offset and the nearest cached peak make that checkable rather + # than asserted. + print(f"\n{'-'*78}\nDECOMPOSITION — what the in-window response is\n{'-'*78}") + r_px = radius_sq ** 0.5 + cls = {"absent": [], "faint_local": [], "tail": []} + for r in results: + if r["act"] < 0.01: + cls["absent"].append(r) + elif r["act"] >= 0.05: + cls["tail"].append(r) + else: + cls["faint_local"].append(r) + for name, sel in cls.items(): + if not sel: + continue + med_off = quartiles([r["argmax_off_px"] for r in sel])[1] + npks = [r["nearest_peak_px"] for r in sel if r["nearest_peak_px"]] + med_npk = quartiles(npks)[1] if npks else float("nan") + n_vis = sum(1 for r in sel if r["verdict"] == "visible") + print(f" {name:>12}: {len(sel):>3} (rated visible {n_vis:>2}) " + f"argmax off med {med_off:>4.1f} px nearest floor peak med " + f"{med_npk:>5.1f} px ({med_npk/r_px:.1f}R)") + tail_near_edge = sum(1 for r in cls['tail'] + if r['argmax_off_px'] > 0.75 * r_px) + print(f" tail cases with argmax in the window's outer quarter: " + f"{tail_near_edge}/{len(cls['tail'])} — the mass is entering from " + f"outside, not centred on the ramp") + + print(f"\n Reading: 'absent' = the heatmap is genuinely flat at the site.") + print(f" 'faint_local' = a real sub-floor response at the site itself.") + print(f" 'tail' = a neighbouring supra-floor mode's slope reaches the window —") + print(f" the site contributed no mode of its own, but the model responded to") + print(f" something adjacent (cf. the merged bucket's sigma story). The three") + print(f" continue differently: absent -> Phase 2's scale counterfactual;") + print(f" faint_local -> threshold/calibration (the sub_threshold continuum);") + print(f" tail -> representation (sigma), not vocabulary.") if args.json_out: os.makedirs(os.path.dirname(os.path.abspath(args.json_out)), exist_ok=True) diff --git a/tests/test_silent_activation.py b/tests/test_silent_activation.py index 22eff87..3de2bb6 100644 --- a/tests/test_silent_activation.py +++ b/tests/test_silent_activation.py @@ -99,6 +99,39 @@ def test_a_site_no_better_than_the_horizon_band_fails_the_test(): assert not act > p95 +# --------------------------------------------------------------------------- # +# site_profile / nearest_peak — separating a site response from a neighbour's tail +# --------------------------------------------------------------------------- # +def test_site_profile_centred_bump_has_zero_offset(): + h = _heat() + h[256][512] = 0.4 + act, off, center = sa.site_profile(h, 512 / 1024, 256 / 512) + assert act == pytest.approx(0.4) + assert off == pytest.approx(0.0) + assert center == pytest.approx(0.4) + + +def test_site_profile_offset_bump_reports_its_distance(): + h = _heat() + h[256][512 + 15] = 0.4 + act, off, center = sa.site_profile(h, 512 / 1024, 256 / 512) + assert act == pytest.approx(0.4) + assert off == pytest.approx(15.0) + assert center == 0.0 # nothing at the ramp itself + + +def test_nearest_peak_measures_in_matcher_units_and_wraps(): + # A peak across the seam: x=0.999 vs site x=0.001 is ~2 px away, not ~1022. + d, score = sa.nearest_peak([(0.999, 0.5, 0.7)], 0.001, 0.5) + assert d == pytest.approx(0.002 * 1024, abs=0.01) + assert score == 0.7 + + +def test_nearest_peak_with_no_peaks_is_infinite(): + d, score = sa.nearest_peak([], 0.5, 0.5) + assert d == float("inf") and score is None + + # --------------------------------------------------------------------------- # # group_of — Phase 0's partition, reused # --------------------------------------------------------------------------- # From acc5c770433ddb779abd00b5a044443f9973d8f1 Mon Sep 17 00:00:00 2001 From: Jon Froehlich Date: Fri, 31 Jul 2026 16:14:19 -0700 Subject: [PATCH 4/4] State the 0c takeaway explicitly (#46): distance is a stressor on already-priced families, not a new failure category Phases 0-1 established every piece of this but the section ended on caveats; the synthesis itself - the sentence a reader should leave with - was never stated. Now it is: far-field recall degrades threefold but decomposes into 62% sigma/representation, 30% sub_threshold, 8% absent, so the lever is decoder-side (target sigma, peak spacing, calibration, Phase 2 scale), not far-field training vocabulary, and multi-view sidesteps all three mechanisms at once. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_017KCSfZm3MjoJ62LVtkvNeb --- docs/curb_ramp_data_sourcing.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/curb_ramp_data_sourcing.md b/docs/curb_ramp_data_sourcing.md index c82caed..0bf3b76 100644 --- a/docs/curb_ramp_data_sourcing.md +++ b/docs/curb_ramp_data_sourcing.md @@ -491,6 +491,14 @@ confirm the intended reading rather than define it): (down to 10.5 model px) is where pointed-verification bias would show most strongly. Phase 3's decoy deck should therefore be **stratified by distance band**, oversampling 40–150 m. +**The takeaway.** "Are far ramps harder?" — yes, threefold (recall 0.777 → 0.292 across the +bands), but Phases 0–1 show distance acting as a **stressor on failure families this taxonomy +already prices, not as a new category of failure**: 62% the σ/representation family, 30% the +`sub_threshold` continuum, 8% genuine absence. The implied lever is therefore decoder- and +representation-side — target σ, peak spacing, threshold calibration, and Phase 2's scale question +for the residual — **not far-field training vocabulary**; and multi-view remains the one remedy +that sidesteps all three mechanisms at once, by re-presenting the same ramp near-field. + ## 1. The current training corpus is mostly one city Stage 1 is built from three cities' open-government inventories (`docs/data_provenance.md` §1).