Skip to content

22 (2/2): auto-catchment, the outline reduction (stacked on #110) - #111

Merged
skavhaug merged 21 commits into
masterfrom
increment22-autocatchment
Sep 29, 2026
Merged

skavhaug merged 21 commits into
masterfrom
increment22-autocatchment

Conversation

@skavhaug

Copy link
Copy Markdown
Member

Increment 22, PR 2 of 2: auto-catchment, the outline reduction. Stacked on #110: review after it; this PR's base is increment22-pr1 and moves to master once #110 merges. Design: docs/increments/22-auto-catchment.md; acceptance: docs/benchmarks/2026-09-29/bygdin.md.

What it does

  • The traced catchment outline has a vertex at every node step, and every outline edge becomes a mesh constraint. This PR reduces it before meshing with an area-preserving segment collapse (Kronenfeld et al. 2020) in C++: --outline-tolerance (default twice the DEM spacing, 20 m on DTM10).
  • Guaranteed: the area is kept exactly (to 1e-9 relative), the polygon stays simple (exact predicates), the seed stays inside, every fine vertex is within the tolerance of the reduced outline and vice versa, and the result is deterministic. The distance along a reduced edge's interior is measured, not guaranteed (Bygdin 19.92 m at 20 m); an enforced whole-outline tolerance is parked in the ROADMAP (Ola).

Numbers (Bygdin, AC, M1 Max)

  • 17 812 → 740 vertices at 20 m; area 304.909550 km² before and after; reduction 0.04 s.
  • NVE: 304.91 km² against 305.54 km² (−0.21 %); node overlap with NVE's polygon 99.1 % / 99.3 %.
  • Mesh at 10 m: 52 198 triangles against 72 292 with the unreduced outline (−28 %); at 1 m: 1 129 026 (−0.8 %).
  • NVE's reservoir catchment polygon (delfelt 1187) is © NVE under NLOD; credits in docs/benchmarks/2026-09-29/bygdin/README.md.

Size

Net +406 production lines (CLAUDE.md §2).

🤖 Generated with Claude Code

skavhaug and others added 20 commits September 29, 2026 02:05
The red suite for increment 22 PR 2 (docs/increments/22-auto-catchment.md,
"The reduction", "The command" and "The red suites", PR 2). No production
code.

- tests/cpp/unit/test_area_collapse.cpp: statuses (negative, NaN, +-inf
  tolerance; clockwise; fewer than 4 vertices); tolerance 0 drops exactly the
  collinear vertices; traced rectangles reduce to at most 8 vertices at one
  cell; a keep-point in a tab stays strictly inside; a zigzag corridor 1.5
  cells wide at 5 cells stays simple; equal bits twice; 300 random star
  polygons. Every case checks the design's guarantees from input and output
  alone: area to 1e-9, simple by brute-force classify<DefaultKernel>, keep
  strictly inside by exact winding, tolerance both ways, and
  input - collinear - collapses = output. Registered behind
  if(EXISTS vector_simplify/area_collapse.hpp): REMOVE AT GREEN.
- test_core_reduce.py: the binding, on 12 random traced blobs at five
  tolerances and on the PR 1 bowl and plus catchments: the five guarantees
  (Hausdorff within tolerance plus a cell), determinism, statuses,
  ValueError on bad shapes, and a median reduction below 0.8 at two cells.
- test_cli_catchment.py: --outline-tolerance 0 writes the fine ring less its
  collinear vertices; the default (twice the cell) reduces, keeps the area
  and stays within the tolerance; the properties and the stderr line;
  -1, nan and inf refused; Bygdin (skipped without Ola's data): reduced area
  equal to fine to 1e-9, at most 17,812 / 5 vertices, and mesh --domain at
  --tolerance 10 succeeds. Test amendment: PR 1's two area checks compare at
  1e-9, the design's area guarantee, since the default output is now reduced.
  The refusal helper also rejects Typer's "No such option", so refusals are
  red until the option exists.

The C++ harness was run in the scratchpad against an identity stub: every
fixture premise holds and the 16 failures are the behaviour an identity
lacks. Red because area_collapse.hpp, _core.reduce_ring and
--outline-tolerance do not exist. Main build exits 0, ctest 814/814; the rest
of pytest, ruff, format and mypy are green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Makes the PR 2 red suite (039cf3c) pass, per docs/increments/22-auto-catchment.md,
"The reduction" and "The command". No test file changed except removing the
red-step if(EXISTS vector_simplify/area_collapse.hpp) guard.

- include/terrain/vector_simplify/area_collapse.hpp: reduce_ring<K>. Collinear
  pass, then APSC-style collapses (E on line A-B or C-D, else the foot of B-C's
  midpoint) ordered by (deviation, id). Each edge carries a fine-vertex range; a
  collapse splits its range between A-E and E-D at the split that minimises the
  larger distance. Crossing tests use classify<K> over a grid of current edges;
  keep-points use an exact winding round A-B-C-D-E. Stale heap entries are
  dropped by a per-node version.
- bindings/core.cpp, _core.pyi: reduce_ring, ReduceOutcome, ReduceStatus.
- catchment.py: CatchmentRequest.outline_tolerance (None: twice the cell);
  the fine ring is reduced from its lower-left corner, seed kept inside.
- cli.py: --outline-tolerance; writes the reduced polygon, with the
  properties reduced_vertices, reduced_area_m2 and outline_tolerance_m and a
  "reduced outline" stderr line.

Bygdin at 20 m: 17,812 -> 740 vertices, area 304.909550 km2 both, difference
-3e-7 m2. mesh --domain at --tolerance 10: 52,198 triangles in 0.7 s (the
fine outline gives 94,374); at --tolerance 1: 1,129,026 triangles in 3.2 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…project structure

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_core_reduce.py's docstring now names four guarantees (area, simple,
seed inside, tolerance at vertices) and the Hausdorff bound as measured,
not guaranteed, matching the design at 698b19f. check() says it takes traced
lattice rings only; every caller passes one (random blobs traced by
outline.trace, the PR 1 bowl and plus catchments), so no assertion moved.
The C++ random star rings (non-lattice) carry no Hausdorff assertion.
test_cli_catchment.py's one Hausdorff check gets the same note.

No test matches 'grown: touches' (the stderr tests look for 'window'), so the
coming 'grown on north' wording breaks nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- catchment.py: module and Catchment docstrings describe the reduced outline
  and its tolerance as built; LakeError (a CatchmentError) for the lake
  refusals.
- cli.py: catchment --help and --outline-tolerance help as built; a lake
  refusal is reported under --lakes, not --dem; stderr 'grown on <sides>'.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Area -0.206 % against NVE's 305.54 km2 (within the design's 2 %); node
overlap 99.12 % / 99.33 %; 740 vertices at 20 m, Hausdorff 19.92 m.
Meshes at 10 m and 1 m on the reduced and fine outlines, and with CORINE.
First run: no performance baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
As the design says: the licence and attribution terms are not recorded.
README records the curl command, the date fetched and the sha256;
analyse.py takes the polygon's path as an argument.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_cli_catchment.py: a seed in no lake, and a seed in two overlapping lakes
(a GeoJSON of the lake box and the box grown by one cell), are refused with
Typer's 'Invalid value for --lakes:' and without 'Invalid value for --dem:'
(6757925, LakeError). The whole phrase is asserted, as plain() flattens it
(Typer prints the hint unquoted): with the old --dem hint the line reads
'Invalid value for --dem: ...', which a bare '--lakes' substring would still
match when the message names the flag, and this phrase does not (checked on
a scratch Typer app raising the same BadParameter with param_hint '--dem').

Replaces test_a_seed_in_no_lake_is_refused, whose refusal it keeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	ROADMAP.md
#	docs/increments/22-auto-catchment.md
#	project_structure.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@skavhaug
skavhaug changed the base branch from increment22-pr1 to master September 29, 2026 09:06
@skavhaug skavhaug closed this Sep 29, 2026
@skavhaug skavhaug reopened this Sep 29, 2026
@skavhaug
skavhaug merged commit 7d9882d into master Sep 29, 2026
8 checks passed
@skavhaug
skavhaug deleted the increment22-autocatchment branch September 29, 2026 21:46
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