Skip to content

ch36: seed-driven elevation validation — ElevationSource, SeedModel, ElevationValidator - #39

Open
jesusr04 wants to merge 3 commits into
mainfrom
chapter-36/seed-driven-elevation-validation
Open

jesusr04 wants to merge 3 commits into
mainfrom
chapter-36/seed-driven-elevation-validation

Conversation

@jesusr04

Copy link
Copy Markdown
Owner

Summary

  • Core inversion: trusted AI-verified seeds define which contour levels exist; raw OCR values are candidates validated against that model — not the governing distribution
  • Motivation: IQR on Edgewater's contaminated input (86% noise, 14% signal) produces fence [-5422, 7605 ft] and keeps everything. Seed validator on same input: keeps 3 real elevations, rejects all 18 noise values
  • ElevationSource enum: explicit provenance taxonomy — ai_verified_seed | ocr_raw | propagated | rejected_outlier — eliminates the flat elevation pool that let the filter get poisoned
  • SeedModel: built from trusted anchors; detects contour interval via modal diff, extrapolates valid level set (seed ± n×interval), computes site bounds. Tolerance proportional to interval (no floor clamp — a 0.5ft minimum on a 1ft interval would cover the entire number line)
  • ElevationValidator: accepts/rejects with reason + distance_ft provenance. No model → all_low_confidence=True, no filtering (conservative fallback for sheets without enough trusted seeds)

Test plan

  • 32 tests in test_ch36_seed_elevation_validator.py — all pass locally
  • Interval detection: 1ft, 2ft, 5ft uniform spacings; inconsistent spacing
  • SeedModel: deduplication, extrapolation, tolerance proportionality
  • Adversarial: full Edgewater scenario (18 noise / 3 signal) — seed validator perfect, IQR would fail
  • ruff check . clean
  • Full CI test suite passes

🤖 Generated with Claude Code

jesusr04 and others added 3 commits March 18, 2026 09:46
…ElevationValidator

Core inversion: trusted AI-verified seeds define which contour levels exist;
raw OCR values are candidates validated against that model, not the
governing distribution.

Motivation: IQR on contaminated input (Edgewater: 86% noise, 14% signal)
produces a poisoned fence [-5422, 7605 ft], keeping everything.
Seed-driven validation on the same input: keeps only the 3 real elevations.

New module: app/pipeline/layer2/elevation_validator.py
  - ElevationSource enum: ai_verified_seed | ocr_raw | propagated | rejected_outlier
  - SeedModel.from_seeds(): detects contour interval from sorted seeds,
    extrapolates valid level set (seed ± n*interval), computes site bounds
  - ElevationValidator.validate(): accepts candidates near a valid level
    (distance ≤ interval*0.30 tolerance); rejects with reason + distance
  - Fallback: model=None → all_low_confidence=True, no values filtered

Interval detection (_detect_interval):
  - Computes modal diff from consecutive seed pairs
  - Requires ≥60% consensus to mark interval_consistent=True
  - Tolerance is proportional to interval (no floor clamp — a 0.5ft minimum
    on a 1ft interval would cover the entire number line)

Rejection provenance: each rejected value carries reason (outside_site_bounds
or off_interval), source (rejected_outlier), and distance_ft to nearest level.

Fallback when no model: marks all_low_confidence=True, returns all values
unfiltered. Conservative by design — fabricating a model from insufficient
seeds is worse than staying uncertain.

32 tests covering interval detection, SeedModel construction, accepts(),
ElevationValidator with and without model, and the full Edgewater adversarial
scenario (18 noise / 3 signal — seed validator passes all 3, rejects all 18).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cular-certainty lock

Addresses four follow-up concerns after initial seed validator review:

1. Interval diagnostics (IntervalDiagnostics class)
   - Exposes raw_diffs, n_diffs, modal_interval, modal_count,
     consensus_ratio, consistent, method for every SeedModel
   - to_dict() emits full audit trail; included in ValidationResult.to_dict()
   - method tag: interval_consistent | interval_inconsistent | no_interval

2. Inconsistency guard (fixes 819/836/847 -> 11ft nonsense)
   - Previously: inconsistent interval was still used for extrapolation/tolerance
   - Now: inconsistent interval → no extrapolation (valid_levels = seed values only)
     + tolerance falls back to _DEFAULT_MATCH_TOLERANCE_FT
   - Also: single diff (2 seeds) is never marked consistent — requires ≥2 matching
     diffs; 1 diff cannot distinguish coherent contour system from two seeds in
     different spatial zones

3. potentially_mixed_zones flag
   - Set True when n_seeds ≥ 3 and interval_consistent is False
   - Flags that seeds may come from spatially disjoint contour neighborhoods
   - Zone-aware per-zone SeedModel is deferred; this flag is the diagnostic signal

4. Circular certainty guard (from_ai_verified_only)
   - Hard API boundary: raises ValueError if any non-AI_VERIFIED_SEED source
     is passed (propagated, ocr_raw, etc.)
   - Prevents: seeds → propagation → propagated values redefine seed model
     → model expands itself with its own output

44 tests — all pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds KT Civil user-verified seeds (proposed contours 736-756, 1ft
interval) and two diagnostic overlay scripts for the elevation
validator.  These complete the visual validation work for Ch36.

seeds_ktcivil.json
  - 21 proposed contour elevations verified by user against PDF
  - zone center at confirmed 738 detection (x=2824, y=1003)
  - excluded_ocr_values documents legend/garbage reads

visualize_elevation_text.py
  - marks OCR text bounding boxes at actual pixel positions (not
    curve centroids) — answers spatial plausibility directly
  - green box = seed model accepted, red = rejected, orange = no model
  - optional --link-lines draws line from each label to nearest curve
  - confirmed: Edgewater 3 accepted, KT Civil 4 accepted (736/738/742/748),
    all junk correctly rejected

visualize_elevation_validator.py
  - first-pass curve-centroid overlay; retained for reference

RGB rendering experiment: tried csRGB for better Tesseract contrast
(improved KT Civil 2→4 accepted) but caused line-count shifts on all
3 golden drawings (KT Civil −50, Easton −118, Simpson +86). Reverted.
runner.py stays on csGRAY. RGB rendering tracked as future work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant