Skip to content

22 (1/2): auto-catchment, the fine catchment - #110

Merged
skavhaug merged 15 commits into
masterfrom
increment22-pr1
Sep 29, 2026
Merged

skavhaug merged 15 commits into
masterfrom
increment22-pr1

Conversation

@skavhaug

Copy link
Copy Markdown
Member

Increment 22, PR 1 of 2: auto-catchment, the fine catchment. Design: docs/increments/22-auto-catchment.md. PR 2 (the outline reduction) is stacked on this one.

What it does

  • rasputin catchment --dem … --seed X Y [--seed-crs] [--lakes …] --out catchment.geojson computes the catchment draining into a seed and writes it in the DEM's CRS, ready for rasputin mesh --domain.
  • The seed is the CORINE lake polygon containing the point (with --lakes), else the nearest DEM node.
  • A Priority-Flood pass in C++ (Barnes, Lehman and Mulla 2014) labels every node that drains to the seed: no separate filling, flat handling or accumulation. The C++ core takes arrays only.
  • The read window grows over the DEM tiles until the catchment stops touching its margin (it provably terminates, and stops early). A catchment cut off by the data's edge or NoData is refused.
  • The outline is traced along midpoints between in- and out-nodes; it is simple by construction, holes are filled and stray pieces dropped (both reported).
  • Lake refusals are reported under --lakes.

Numbers

Bygdin (Jotunheimen) over DTM10: 304.91 km² against NVE's 305.54 km² (−0.21 %), 6.3 s over 3 windows across two tiles; the fine outline has 17 812 vertices (PR 2 reduces it).

Size

Net +619 production lines (CLAUDE.md §2). Together with PR 2 the increment is 1 025 lines, hence the split.

🤖 Generated with Claude Code

skavhaug and others added 12 commits September 29, 2026 00:50
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Ola's correction)

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>
The red suite for increment 22 PR 1 (docs/increments/22-auto-catchment.md,
"The red suites", PR 1): the flood, the outline, the window loop and the
`rasputin catchment` command. No production code.

- tests/cpp/unit/test_hydrology_upstream.cpp: upstream<R> equals
  lowest-neighbour descent node for node on 300 random no-pit, no-tie DEMs
  (an oracle in the file); depressions, a flat lake with shelves, a V-valley
  (exact); NoData never in; seed on the edge; wrong seed size; 1-node rasters;
  invariants on 400 random DEMs with pits, flats and NoData (seeds in, 8-path
  to a seed, bounds, determinism, catchments of a union are the union).
  Registered behind if(EXISTS hydrology/upstream.hpp), increment 12's
  scaffold: REMOVE AT GREEN.
- test_core_upstream.py: the binding (mask uint8 (rows, cols), bounds, flags,
  ValueError on a seed of another shape).
- test_outline.py: diamond, octagon, L, saddle, pinch, holes clockwise, and
  40 random masks against the guarantee and a square-by-square area oracle.
- test_catchment.py (in-memory repository): lake seeds, refusals for no lake
  and two lakes, WGS 84 and EPSG:3035 inputs, the grown window equal to one
  flood over the whole raster, the edge (north, east), NoData and memory
  refusals, holes filled and pieces dropped with counts, to_thread.
- test_cli_catchment.py: round trip through read_domain and mesh --domain,
  stderr lines, --seed-crs default, GeoJSON and GeoPackage lakes, refusals,
  and Bygdin on Ola's data (skipped without it) within 2 % of 305.54 km^2.

Red because include/terrain/hydrology/upstream.hpp, _core.upstream,
tin_engine.outline, tin_engine.catchment and the catchment command do not
exist. Every fixture's expected value was checked in the scratchpad with the
design's flood written out, not committed. The main build exits 0, ctest
803/803, and the rest of pytest, ruff, format and mypy are green.

Names the design leaves open are chosen in each file's docstring. Design gap:
edge and NoData-adjacent nodes are outlets and labelled in only when seeds, so
touches_edge/touches_nodata as worded fire only for seeds; the C++ suite pins
only what any repair keeps, and the Python suite pins the refusals by
behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sputin catchment)

Makes the PR 1 red suite (1e1b3bb) pass, per docs/increments/22-auto-catchment.md
as amended in 06776de. No test file changed except removing the red-step
if(EXISTS hydrology/upstream.hpp) guard in tests/cpp/CMakeLists.txt.

- include/terrain/hydrology/upstream.hpp: Priority-Flood labelling of one
  catchment, keys (level, push counter); flags as "The flags" words them.
- bindings/core.cpp, _core.pyi: _core.upstream and UpstreamOutcome (read-only
  (rows, cols) uint8 mask anchored to the outcome), GIL released.
- outline.py: marching squares with the (8, 4) saddle rule, boundary squares only.
- catchment.py: CatchmentRequest, delineate, the window loop, the fine outline.
- feature_input.py: read_source split out of _Tally.source; read_lakes.
- dem_input.py: repository_for, shared by open_dem and catchment.
- cli.py: rasputin catchment, writing the fine outline as GeoJSON.

Design deviation, for review: step c of "The window" doubles the margin and
tests growth against the doubled margin, so every step grows and the loop only
stops at the data's edge (Bygdin: 8 min, then refused by 15a's coverage check).
Here growth is tested with the base 2000 m margin and the step takes the
doubled one. Bygdin: 3 windows, 304.9096 km2 against NVE's 305.54 (-0.21 %),
6.3 s end to end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two pins the amended design asks for (0e1c29a, "What a test should pin"):

- test_the_loop_stops_early_far_from_the_data_edge: the bowl, with more than
  four base margins of data beyond the catchment on every side and a
  catchment larger than the first window, is accepted in 2 or 3 windows,
  the last reaching the data's edge on no side, equal to one flood.
- test_a_catchment_inside_the_first_window_takes_one_window: a lake on a
  peak, 49 nodes, accepted in one window.

Both pass at 58f6904. Against the old rule (the check on the doubled
margin, the step on the undoubled one: a scratchpad copy of catchment.py with
those two lines changed, src_python untouched) the first fails: 6 windows,
49x49 up to the whole 400x200 raster. The second passes there too; it is the
cheaper pin the design describes, guarding the other direction.

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 and others added 3 commits September 29, 2026 09:06
…N analogy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
upstream.hpp: cast kIn to std::uint8_t so both arms of the conditional
have the same type (GCC -Wextra: enumerated and non-enumerated type).
core.cpp: hold the raster geometry by value rather than as a reference
bound through std::visit (GCC -Wdangling-reference false positive).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@skavhaug
skavhaug merged commit a7e51d1 into master Sep 29, 2026
8 checks passed
skavhaug added a commit that referenced this pull request Sep 29, 2026
22 (2/2): auto-catchment, the outline reduction (stacked on #110)
@skavhaug
skavhaug deleted the increment22-pr1 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