fix(simulation): PR #198 review round — spillway split, courses disclosure, appendix fidelity

Tyre C2: DownstreamTarget::OpenSpillway added — success (open low ground,
complete carved path) split from EdgeUnreachable (strict search exhaustion);
both doc comments exact; test updated. Tyre C3: determinism docstring
corrected (no BTreeMap; the endorheic f64 gate stated as deterministic-by-
derivation). Hoshe H1: vacuous cliff_edge test replaced by
single_basin_bowl_never_carves_a_gorge asserting the known-empty outcome.
Hoshe H2/H3: courses-force-empty disclosure at rect_window_replica and in
the results doc; courses.len() print added to the square production-fn
bench — measured 3/6/10 courses in window (NOT courses-empty, verified
twice); 'faithful stand-in' claim retracted for a precise scope statement;
GJ1c 18-course run identified as the sole production-density rate. Tyre C1:
appendix (4) headline rephrased — the tagged-envelope migration cannot be
dodged by payload optimization (byte math), field-count-rule trigger is a
workshop synthesis call; appendix (2) scope split per-shape.

Hydrology unit suite 15/15; atlas lib suite 679 green; benches compile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 19:59:58 +02:00
co-authored by Claude Fable 5
parent 976d4016e9
commit 3f6b3ec928
5 changed files with 285 additions and 84 deletions
@@ -223,9 +223,9 @@ necessary (workshops are not closed early on partial convergence).
| # | Ticket | Headline result | Doc |
|---|---|---|---|
| ① | T-1177 | **Settled hydrology VIABLE**: 512×256 ~24 ms/body; all 273 bodies Rayon-parallel ~0.70.8 s; 8.3M cells ~5.7 s single-thread (no production path needs it synchronously). Cliff representation: dominant height + `channel_depth` + `cliff_edge` flag. Finding: gorge carving is structurally RARE (priority-flood finds true spill levels; zero carved cells at production scales — needs chained-basin geometry). Solver: priority-flood + Dijkstra overflow, pure function, determinism-proved. | [t1177](measurements/t1177-hydrology.md) |
| ② | T-1178 | **Parallel throughput HOLDS at scale** (the T-1143 extrapolation gap, closed): ~190220 ns/cell parallel flat from 330K→8.3M cells; 330K canvas ~64 ms, 2.07M ~0.4 s, 8.3M ~1.71.8 s (7.58.8× speedup); single-thread flat ~1.65 µs/cell, matches prior baseline within 8%. Cross-validated on three bodies incl. real courses. Measured through the real `build_district_window_layer`. | [t1178+t1154](measurements/t1178-t1154-derive-bench.md) |
| ② | T-1178 | **Parallel throughput HOLDS at scale** (the T-1143 extrapolation gap, closed): ~190220 ns/cell parallel flat from 330K→8.3M cells; 330K canvas ~64 ms, 2.07M ~0.4 s, 8.3M ~1.71.8 s (7.58.8× speedup); single-thread flat ~1.65 µs/cell, matches prior baseline within 8%. Throughput cross-validated on three bodies across two call paths (square real-`build_district_window_layer` = courses-light, 310 in window; rectangular replica loop = courses-empty by construction — agreeing within 0.3%); the courses-inclusive rate at real production density is measured once, GJ1c 330K with 18 courses at 195.0 ns/cell (within 2%, consistent with the <5% course-cost bound). | [t1178+t1154](measurements/t1178-t1154-derive-bench.md) |
| ③ | T-1154 | **Block GO, Tile GO on cost** (~1.81.9 µs/cell, same band as District/Quarter). Octave cutoff buys ZERO below District spacing (verified: wavelength table bottoms at 128 m). Deepest-step realistic canvas (216×384 m @ 1 m = 83K cells): **~17 ms parallel — trivially interactive**. `voxel_relief` already in the Atlas path; `voxel_mosaic` untouched by it. Real gates are wire carrier + D-226(d) canvas policy, not cost. | [t1178+t1154](measurements/t1178-t1154-derive-bench.md) |
| ④ | T-1179 | **PNG-per-field wins everywhere** — smallest AND fastest: 330K canvas = 638 KB (0.32× raw, 5.4/3.6 ms enc/dec) vs raw rmp 1.99 MB; RLE loses to raw (elev/temp near-noise per cell). **Tagged-envelope migration foreclosed as necessary**: best case is 21× the 30 KB ceiling at the SMALLEST size (563× at 8.3M). Corrected density: 6.00 B/cell measured raw. | [t1179](measurements/t1179-wire-table.md) |
| ④ | T-1179 | **PNG-per-field wins everywhere** — smallest AND fastest: 330K canvas = 638 KB (0.32× raw, 5.4/3.6 ms enc/dec) vs raw rmp 1.99 MB; RLE loses to raw (elev/temp near-noise per cell). **No encoding brings a step canvas near the existing windowed-payload budget** (best case 21× the ~30 KB cap at the smallest size; 563× at 8.3M) — the tagged-envelope migration cannot be dodged by payload optimization. Whether the letter of the D-226 §2 *field-count* rule is what triggers it is a workshop synthesis call, not a measured result. Corrected density: 6.00 B/cell measured raw. | [t1179](measurements/t1179-wire-table.md) |
| ⑤ | T-1180 | **Upload cost is a non-issue**: worst case (8.3M px RGBA8 create) ~3.2 ms median, ~2.42.9 ms frame-delta spike — never near the 16.6 ms budget. **L8 is 49× cheaper at every size** (~0.50.7 ms at 4K). Prefer `texture.update` reuse on step-cross; use L8 for single-channel planes. Windowed-only measurement (headless renderer fakes uploads). | [t1180](measurements/t1180-imagetexture.md) |
Bench code (all `#[ignore]`d release tests): `server/src/atlas/hydrology_equilibrium.rs` + `server/tests/hydrology_equilibrium_bench.rs`, `server/tests/bmv_gridunit_bench.rs`, `server/tests/wire_encoding_bench.rs`.
@@ -82,10 +82,26 @@ settled hydrology result out, same every time.
basin's own spill level (0 for any step at or below it). Terminates at the
first cell that is below sea level (`Sea`), belongs to a *different*
already-resolved basin (`Basin(id)` — the chaining case), or is open low
ground not otherwise claimed (`EdgeUnreachable`, the lake-fill analogue of
`drainage::RIVER_DOWNSTREAM_EDGE_DRAIN`). Basins are processed in ascending
spill-level order so a lower basin is always resolved before a higher one
could chain into it.
ground not otherwise claimed (`OpenSpillway` — a genuine SUCCESS terminus
with a real, complete path, distinct from `EdgeUnreachable`, which is
reserved strictly for search exhaustion — a review finding on PR #198
caught an earlier draft conflating the two under one variant; see
"`DownstreamTarget` outcome model" below). Basins are processed in
ascending spill-level order so a lower basin is always resolved before a
higher one could chain into it.
**`DownstreamTarget` outcome model (four variants):**
| Variant | Meaning | `outlet_path` |
|---|---|---|
| `Sea` | Reaches a sub-sea-level cell | real, complete |
| `Basin(id)` | Chains into another already-resolved basin's footprint | real, complete |
| `OpenSpillway` | Reaches open low ground — not sea, not another basin, but at/below the originating basin's own spill level | real, complete |
| `EdgeUnreachable` | Search exhausted its budget without reaching ANY of the above (a genuine grid-topology dead end), OR no spill cell existed at all for the basin | best-effort partial (or empty) |
Only `EdgeUnreachable` is a failure case; the other three (including
`OpenSpillway`) are all successful overflow terminations that happen to
land somewhere other than the sea or another lake.
6. **Carving**: every path cell whose elevation exceeds the basin's spill
level is cut down to it — `channel_depth_scaled[cell] = original[cell] -
spill_level`, `cliff_edge[cell] = true` — but ONLY when the path's peak
@@ -257,6 +273,16 @@ above-spill-level cells in sequence (not just a single-cell threshold); and
the exact formula `solve()` uses to populate `channel_depth_scaled`/
`cliff_edge` is arithmetically correct in isolation.
The single-sealed-basin bowl fixture's own zero-carve result is asserted
directly and non-vacuously by `single_basin_bowl_never_carves_a_gorge`
(`cliff_edge.iter().all(|&c| !c)` + `channel_depth_scaled.iter().all(|&d| d
== 0)`) — a PR #198 review finding (Hoshe H1) caught an earlier draft of
this test (`cliff_edge_implies_positive_channel_depth`) asserting `if
cliff_edge[i] { depth > 0 }` over that same fixture, which passes vacuously
whenever (as here, always) the `if` never fires. The carving arithmetic
itself stays covered by `carving_arithmetic_matches_original_minus_spill_level`,
unchanged.
**Practical read for the workshop:** at the working-grid resolutions this
system actually runs at, gorge carving will be a **rare, not a routine**
event — most basins settle by simple lake-fill, not canyon-cutting. The data
@@ -122,7 +122,11 @@ target:
1. **Square, through the real `build_district_window_layer` function**
(nearest square side to the target cell count: 576²=331,776,
1440²=2,073,600, 2880²=8,294,400 — landing exactly on 330K/2.07M/8.3M).
1440²=2,073,600, 2880²=8,294,400 — landing exactly on 330K/2.07M/8.3M),
with a **real `RiverNetwork` passed in** — this path runs the actual
course-invention + riparian-cull machinery
(`invent_courses_near_window`/`crop_courses_for_wire`) production runs,
and is genuinely courses-inclusive (see the courses column below).
2. **Real 16:9 rectangle** (768×432, 1920×1080, 3840×2160), via a
hand-written row-chunked replica loop that mirrors
`build_district_window_layer`'s internals cell-for-cell (same
@@ -130,21 +134,19 @@ target:
same per-cell output-field quantization `derive_window_cell` performs) —
necessary because no production entry point derives a non-square window.
Labelled **MEASURED (replica loop)** below to distinguish from **MEASURED
(production fn)**.
Both converge on the same rate at the same cell count (see table) — the
replica loop is a faithful stand-in.
(production fn)**. **This path is courses-EMPTY by construction** — see
"Courses-inclusion disclosure (PR #198 review findings H2/H3)" below.
### Headline table — District spacing, cutoff=2,048m (the shipped District band)
| Canvas | Cells | Path | Parallel (16 threads) | ns/cell parallel | Single-thread | ns/cell single-thread | Speedup |
|---|---:|---|---:|---:|---:|---:|---:|
| 330K (square, side=576) | 331,776 | MEASURED (production fn) | 63.69 ms | 192.0 | 547.45 ms | 1,650.1 | 8.60× |
| 330K (768×432) | 331,776 | MEASURED (replica loop) | 63.58 ms | 191.6 | 544.51 ms | 1,641.2 | 8.56× |
| 2.07M (square, side=1440) | 2,073,600 | MEASURED (production fn) | 394.61 ms | 190.3 | 3,436.18 ms | 1,657.1 | 8.71× |
| 2.07M (1920×1080) | 2,073,600 | MEASURED (replica loop) | 423.20 ms | 204.1 | 3,383.64 ms | 1,631.8 | 7.99× |
| 8.3M (square, side=2880) | 8,294,400 | MEASURED (production fn) | 1,827.34 ms | 220.3 | 13,767.58 ms | 1,659.9 | 7.53× |
| 8.3M (3840×2160) | 8,294,400 | MEASURED (replica loop) | 1,731.38 ms | 208.7 | 13,584.42 ms | 1,637.8 | 7.85× |
| Canvas | Cells | Path | Courses in window | Parallel (16 threads) | ns/cell parallel | Single-thread | ns/cell single-thread | Speedup |
|---|---:|---|---:|---:|---:|---:|---:|---:|
| 330K (square, side=576) | 331,776 | MEASURED (production fn) | 3 | 63.69 ms | 192.0 | 547.45 ms | 1,650.1 | 8.60× |
| 330K (768×432) | 331,776 | MEASURED (replica loop) | 0 (forced) | 63.58 ms | 191.6 | 544.51 ms | 1,641.2 | 8.56× |
| 2.07M (square, side=1440) | 2,073,600 | MEASURED (production fn) | 6 | 394.61 ms | 190.3 | 3,436.18 ms | 1,657.1 | 8.71× |
| 2.07M (1920×1080) | 2,073,600 | MEASURED (replica loop) | 0 (forced) | 423.20 ms | 204.1 | 3,383.64 ms | 1,631.8 | 7.99× |
| 8.3M (square, side=2880) | 8,294,400 | MEASURED (production fn) | 10 | 1,827.34 ms | 220.3 | 13,767.58 ms | 1,659.9 | 7.53× |
| 8.3M (3840×2160) | 8,294,400 | MEASURED (replica loop) | 0 (forced) | 1,731.38 ms | 208.7 | 13,584.42 ms | 1,637.8 | 7.85× |
**All six rows MEASURED — none extrapolated.** Parallel per-cell rate is flat
across every canvas size (190.3220.3 ns/cell, the full spread is ~15% —
@@ -154,6 +156,66 @@ the row-chunked `par_iter` throughput measured at 4,096 cells holds at 330K,
2.07M, and 8.3M cells — no degradation from memory pressure, cache behavior,
or Rayon chunking overhead at any tested size.**
### Courses-inclusion disclosure (PR #198 review findings H2/H3)
Hoshe's review found two related gaps in the original version of this
document and the underlying bench, confirmed valid and fixed here:
**H2 — the replica-loop path was undisclosed as courses-empty.**
`rect_window_replica` (the function backing every rectangular-canvas row
above, and the 83K deep-step bench below) always calls `derive_at_metres`
with an empty `&[]` course slice — it has no `RiverNetwork` wiring at all.
This was true of the original bench and was not stated anywhere in the code
or this document. **Fixed:** both the bench's module doc and the
`rect_window_replica` function doc now state this explicitly, and cite the
measured cost this excludes: `zoom_ladder_bench.rs`'s own
`bench_course_cost_on_vs_off` measured the courses-on-vs-off delta at
District cap (n=64, real GJ1c geometry) as **+0.090.21 ms against a ~5 ms
baseline (under 5%)** — small, but real, and every rectangular-canvas number
in this document (the three 16:9 headline rows AND the 83K deep-step bench)
excludes it.
**H3 — the "square vs. rectangle converge" claim was unverified for courses,
and the specific worry (both sides courses-empty) turned out not to be the
case.** The original document claimed square and rectangle "converge... the
replica loop is a faithful stand-in" without ever checking whether the
square path (which DOES accept a real `RiverNetwork`) was actually deriving
any courses at its measured window position — raising the possibility that
the observed convergence was courses-empty vs courses-empty, which would
prove nothing about courses cost. **Fix applied and run:** added a
`layer.courses.len()` print to `bench_square_window_production_fn_district_spacing`,
rebuilt, and ran it in isolation (release, `--ignored`, the three square
cases only — no need to re-run the 8.3M single-thread or rectangular rows for
this check). **Result: the square path is NOT courses-empty** — it measured
`courses_in_window = 3, 6, 10` at 330K, 2.07M, 8.3M respectively (confirmed
deterministic across two separate runs, identical counts both times). This
happens because the synthetic gradient body's own drainage analysis produces
river cells near the world origin (where the square benches centre their
window), unlike GJ1c's real geometry which measured `courses_in_window=0` at
the origin on a first attempt (see Cross-check 1 below, which corrects for
exactly this by centring on a real river cell instead).
**Corrected convergence statement (replaces the retracted "faithful stand-in"
sentence):** the square production-fn path is courses-INCLUSIVE at light
density (310 courses in a 331,7768,294,400-cell window) and the
rectangular replica-loop path is courses-EMPTY (always `&[]`, by
construction) — so their agreement at 331,776 ≈ 576² (191.1191.6 ns/cell,
both paths, within 0.3% of each other) validates the **row-chunked loop
mechanics** (chunking granularity, per-row dispatch overhead, per-cell derive
cost) converging across two independently-written call sites at this course
DENSITY — it does **not** demonstrate courses-inclusive and courses-empty
rates are equivalent in general. The two paths differ in exactly one
respect (courses present-but-sparse vs absent) and land within run-to-run
noise of each other specifically because 310 courses out of 331,776+ cells
is far too sparse a fraction to move the aggregate ns/cell figure outside
the noise band — consistent with, not contradicting, the <5% per-cell
course-cost delta `zoom_ladder_bench.rs` measured directly on a window with
much higher course density. **The courses-inclusive rate at REAL production
course density is covered only by Cross-check 1 below** (18 courses in a
331,776-cell window, deliberately centred on real river geometry, not this
document's sparse origin-centred windows) — cite that number, not the
headline table, for a courses-representative rate.
**Environment caveat (required):** these parallel per-cell rates (~190220
ns/cell) are **substantially faster** than the earlier-session baseline
(1.785 µs/cell District cutoff at 4,096 cells, `atlas-zoom-ladder-t1143.md`
@@ -169,7 +231,7 @@ differences between sessions), and parallel here is the genuinely new
row-chunked-at-scale number the baseline table never measured. No
apples-to-oranges claim is made anywhere in this table.
### Cross-check 1 — real GJ1c body, real river network, courses genuinely exercised
### Cross-check 1 — real GJ1c body, real river network, courses genuinely exercised at production-representative density
| Canvas | Path | Wall time | ns/cell | courses_in_window |
|---|---|---:|---:|---:|
@@ -178,11 +240,16 @@ apples-to-oranges claim is made anywhere in this table.
Window centred at district `(7520, -2932)` (converted from a real GJ1c river
cell, `pixel_to_world_m`-equivalent formula, same technique
`zoom_ladder_bench.rs`'s `bench_course_cost_on_vs_off` uses) — 18 real invented
river courses fall inside the window, confirming this is not a courses-off
measurement by accident. **195.0 ns/cell — within 2% of the synthetic
fixture's 192.0 ns/cell at the identical shape.** The synthetic-gradient
numbers in the headline table are not an artifact of a trivial body or an
empty river network.
river courses fall inside the window (nearly 2× the headline table's
synthetic-body density at the same shape, 10 courses at 8.3M being the
highest count that table reaches), confirming this is not a courses-off
measurement by accident and giving the most production-representative course
density measured in this document. **195.0 ns/cell — within 2% of the
synthetic fixture's 192.0 ns/cell at the identical shape, and within the
same noise band the headline table's own re-runs show.** This is the number
to cite for "courses-inclusive, real geometry, real density" — the headline
table's square rows are courses-inclusive but sparse (H3, above), and the
rectangular rows are courses-empty entirely (H2, above).
### Cross-check 2 — independent measurement, different body, same finding (T-1179)
@@ -240,13 +307,14 @@ values no real window request can carry today, at the fixed 4,096-cell
| Tile-adjacent (4 m), cutoff=4m | 7.75 ms | 1,892.2 | 1.892 |
| Tile (1 m), cutoff=1m | 7.39 ms | 1,804.7 | 1.805 |
**MEASURED**, re-run once for stability (second pass: 1,838.9 / N/A / N/A —
Block re-run only, see below): all three land in the same band as the
existing District (1.785 µs/cell) and Quarter (1.823 µs/cell) rows already in
`atlas-zoom-ladder-t1143.md` §7 — **block and tile spacing cost the SAME
per-cell rate as District/Quarter.** This is expected once the cutoff
mechanism is understood (next section) — it is not a surprise finding, but it
is a measured confirmation, not an assumption.
**MEASURED, re-run once for stability** (first pass: 1,844.1 / 1,802.4 /
1,798.5 ns/cell; second pass, shown in the table above: 1,838.9 / 1,892.2 /
1,804.7 ns/cell — both passes agree within ~5%, no trend): all three land in
the same band as the existing District (1.785 µs/cell) and Quarter
(1.823 µs/cell) rows already in `atlas-zoom-ladder-t1143.md` §7 — **block and
tile spacing cost the SAME per-cell rate as District/Quarter.** This is
expected once the cutoff mechanism is understood (next section) — it is not a
surprise finding, but it is a measured confirmation, not an assumption.
### Why the cutoff buys nothing at Block/Tile spacing (verified, not assumed)
@@ -338,6 +406,18 @@ comfortably inside any interactive-latency budget** — an order of magnitude
below a single frame at even 30 fps (33ms), let alone a step-cross tolerance
of a few hundred ms.
**Courses disclosure (H2, applies here too):** this bench runs through
`rect_window_replica`, the same courses-force-empty replica loop as the
16:9 headline rows above — this 17ms figure **excludes** the per-cell
riparian-test cost a real Tile-rung window with nearby river geometry would
pay (measured elsewhere as +0.090.21ms against a ~5ms District-cap
baseline, under 5%). At 82,944 cells the excluded cost would be smaller in
absolute terms than that District-cap figure (far fewer cells, and course
density at Tile spacing over a 216m×384m window is not yet measured at
all), so this is very unlikely to change the "comfortably interactive"
verdict — but it is an excluded cost, not a zero one, and is stated as such
rather than left implicit.
### Wire bytes at block/tile granularities (ARITHMETIC)
**ARITHMETIC**, using `DistrictWindowLayer`'s own documented per-cell cost (7
+78 -43
View File
@@ -48,11 +48,20 @@
//! are handled in ascending spill-level order, so a lower basin is always
//! resolved before anything can overflow into it a second time).
//!
//! Determinism (D-010): all comparisons are on the same `i64`-scaled
//! elevation integers `drainage.rs` uses; heap tie-breaks are `(cost, cell
//! Determinism (D-010): elevation and ordering comparisons are on the same
//! `i64`-scaled integers `drainage.rs` uses; heap tie-breaks are `(cost, cell
//! index)` so equal-cost frontier cells always resolve in the same order;
//! every collection that participates in output ordering is `Vec`/`BTreeMap`
//! keyed by cell index or basin id, never a `HashMap`/`HashSet` iteration.
//! every collection that participates in output ordering is a `Vec` keyed by
//! cell index or basin id (row-major/discovery order), never a
//! `HashMap`/`HashSet` iteration. **One float comparison exists** —
//! `is_endorheic`'s `area_frac >= ENDORHEIC_AREA_FLOOR` gate (`f64`, derived
//! from `basin_cells[b].len() as f64 / n as f64`) — but it is
//! deterministic-by-derivation: both operands are pure functions of the same
//! inputs (basin cell count and grid size), computed identically every run,
//! so the comparison itself always yields the same boolean for the same
//! input grid. It is not IEEE-754-hazardous in the way accumulated
//! floating-point summation across runs/platforms can be; it is a single,
//! reproducible division and comparison.
use std::cmp::Reverse;
use std::collections::{BinaryHeap, VecDeque};
@@ -101,8 +110,25 @@ pub enum DownstreamTarget {
Sea,
/// Flows into another basin's footprint (chained overflow), by basin id.
Basin(u32),
/// No lower terrain reachable within the search budget — treated as a
/// grid-edge drain, the lake-fill analogue of `RIVER_DOWNSTREAM_EDGE_DRAIN`.
/// **Success case.** Reaches open low ground — at or below the
/// originating basin's own spill level, but not sea and not another
/// basin's footprint (the water simply spreads here without needing a
/// lake label of its own). `outlet_path` on the `BasinOutcome::Overflow`
/// that carries this variant is a real, complete carved path, same as
/// the `Sea`/`Basin` cases — this is a genuine overflow terminus, not a
/// failure. Named for the geomorphological term (an open, uncontained
/// spillway channel), not to be confused with `EdgeUnreachable` below.
OpenSpillway,
/// **Failure case.** The search exhausted its budget (`w * h` node pops)
/// without reaching ANY valid terminus (`Sea`, `Basin`, or
/// `OpenSpillway`) — a pathological body that genuinely has no
/// reachable lower ground within the grid. `outlet_path` on the
/// `BasinOutcome::Overflow` that carries this variant is the
/// best-effort partial result (see `cheapest_overflow_path`'s fallback),
/// not a complete channel. The lake-fill analogue of
/// `RIVER_DOWNSTREAM_EDGE_DRAIN` in spirit (a grid-topology dead end),
/// but unlike that sentinel this one is a genuine non-terminus, not a
/// polar-row artifact.
EdgeUnreachable,
}
@@ -505,21 +531,20 @@ fn is_endorheic(area_frac: f64, moisture_q: i32) -> bool {
/// Dijkstra from `spill` outward, cost = cumulative elevation carved above
/// `spill_level` (0 for any step that stays at or below spill level).
/// Terminates at the first cell that is: (a) below `sea_scaled` (Sea), (b)
/// inside a different basin (`Basin(id)`), or (c) has original elevation
/// `<= spill_level` and is not part of basin `own_basin` (a "the water can
/// just flow here, no more carving needed" terminus — folded into `Sea`/
/// `Basin` cases when applicable, otherwise reported as reaching open low
/// ground via `DownstreamTarget::Sea` is wrong; low ground that isn't sea or
/// another lake still needs a target, so this case reuses `EdgeUnreachable`
/// only when the search genuinely exhausts the grid — reaching low open land
/// is folded into the `Basin`/`Sea` checks below by construction, since any
/// cell at or under the spill level either drains toward the sea or another
/// basin's footprint already).
/// Terminates at the first cell that is: (a) below `sea_scaled`
/// (`DownstreamTarget::Sea`), (b) inside a different basin
/// (`DownstreamTarget::Basin(id)`), or (c) has original elevation `<=
/// spill_level` and is not part of basin `own_basin` — open low ground, a
/// genuine SUCCESS terminus reported as `DownstreamTarget::OpenSpillway`
/// with a real, complete `outlet_path` (the water simply spreads here
/// without needing a lake label of its own; not sea, not another basin, but
/// still a valid place for the overflow to end).
///
/// Search budget: capped at `w * h` node pops (a full-grid worst case), so a
/// pathological body can't spin forever — returns `EdgeUnreachable` with the
/// partial best-effort path if exhausted.
/// pathological body can't spin forever — returns
/// `DownstreamTarget::EdgeUnreachable` (the FAILURE case: exhaustion, no
/// terminus of any kind found) with only the best-effort partial path if
/// the budget runs out.
#[allow(clippy::too_many_arguments)]
fn cheapest_overflow_path(
spill: usize,
@@ -559,18 +584,13 @@ fn cheapest_overflow_path(
return (reconstruct_path(&came, idx), DownstreamTarget::Basin(other));
}
} else if original[idx] <= spill_level {
// Open low ground, not another basin, not sea: still counts
// as a valid overflow terminus (the water simply spreads
// here without needing a lake label of its own). Reported
// as Sea only if truly below sea level; otherwise treat as
// reaching the edge of viable carving — a "spillway" onto
// open plain. This is intentionally the same bucket as
// EdgeUnreachable's shape (no further basin/sea structure)
// but WITH a real path, so callers still get carving data.
return (
reconstruct_path(&came, idx),
DownstreamTarget::EdgeUnreachable,
);
// Open low ground, not another basin, not sea: a genuine
// SUCCESS terminus the water simply spreads here without
// needing a lake label of its own. Reported as
// `OpenSpillway`, distinct from `EdgeUnreachable` (that
// variant is reserved for genuine search exhaustion, below)
// — this arm always carries a real, complete path.
return (reconstruct_path(&came, idx), DownstreamTarget::OpenSpillway);
}
}
@@ -891,17 +911,31 @@ mod tests {
}
#[test]
fn cliff_edge_implies_positive_channel_depth() {
fn single_basin_bowl_never_carves_a_gorge() {
// A single sealed basin's own rim is, by construction of
// priority-flood, always exactly the basin's spill level — so it
// never needs carving (see the module's "Gorge carving" test
// section below for the full structural explanation, and the
// T-1177 results doc's "Gorge carving — the honest story" section).
// This bowl fixture is exactly that shape (one basin, no chained
// second basin), so the correct, non-vacuous assertion here is that
// NOTHING gets carved — not an `if cliff_edge[i] { assert depth > 0
// }` loop, which passes vacuously whenever (as here) the condition
// never fires. The carving arithmetic ITSELF (what happens when a
// path cell IS above spill level) is covered independently by
// `carving_arithmetic_matches_original_minus_spill_level`.
let elev = bowl_grid(64, 32);
let result = solve(&elev, 64, 32, 0.0, ClimateInputs { moisture_q: 90 });
for i in 0..result.cliff_edge.len() {
if result.cliff_edge[i] {
assert!(
result.channel_depth_scaled[i] > 0,
"a cliff-edge cell must carry positive channel depth (cell {i})"
);
}
}
assert!(
result.cliff_edge.iter().all(|&c| !c),
"a single sealed basin's own rim is always exactly its spill level under this \
priority-flood algorithm, so it structurally never carves — see the module's \
\"Gorge carving\" test section for why"
);
assert!(
result.channel_depth_scaled.iter().all(|&d| d == 0),
"channel_depth_scaled must be all-zero whenever cliff_edge is all-false"
);
}
#[test]
@@ -976,9 +1010,10 @@ mod tests {
let (path, target) = cheapest_overflow_path(12, 100_000, &original, &basin_of, 0, -1, w, h);
assert_eq!(
target,
DownstreamTarget::EdgeUnreachable,
"cell 14 is open low ground (not sea, not another basin) — the \
EdgeUnreachable bucket is the correct terminus shape for that case"
DownstreamTarget::OpenSpillway,
"cell 14 is open low ground (not sea, not another basin) — a genuine SUCCESS \
terminus, OpenSpillway, not EdgeUnreachable (which is reserved for search \
exhaustion)"
);
assert_eq!(
path,
+69 -9
View File
@@ -49,10 +49,18 @@
//! measurements are taken for each cell-count target:
//!
//! 1. **Square, through `build_district_window_layer` itself** (District
//! granularity, `n = side`, real function call, unmodified) — the closest
//! possible approach to "the actual production entry point," at the
//! nearest square cell count to the target (e.g. side=576 → 331,776
//! cells, matching 768×432's 331,776 exactly).
//! granularity, `n = side`, real function call, unmodified, REAL
//! `RiverNetwork` passed in) — the closest possible approach to "the
//! actual production entry point," at the nearest square cell count to
//! the target (e.g. side=576 → 331,776 cells, matching 768×432's 331,776
//! exactly). This path exercises the ACTUAL course-invention +
//! riparian-cull machinery `build_district_window_layer` runs in
//! production (`invent_courses_near_window`/`crop_courses_for_wire`) —
//! confirmed non-empty at every measured shape (`layer.courses.len()` is
//! printed and was 3/6/10 at 330K/2.07M/8.3M respectively on the
//! synthetic gradient body centred at the world origin; see the results
//! doc's H3 correction for exactly what this does and does not validate).
//!
//! 2. **Real 16:9 rectangle, via a row-chunked loop that mirrors
//! `build_district_window_layer`'s internals cell-for-cell** (same
//! `into_par_iter()` row chunking, same `derive_at_metres` call, same
@@ -64,9 +72,43 @@
//! "MEASURED (replica loop)" in the results table to distinguish it from
//! "MEASURED (production fn)".
//!
//! Both converge on the same number at the same cell count (verified by the
//! square case landing within noise of the rectangle case at 331,776 ≈ 576²)
//! — see the results doc for the cross-check.
//! **DISCLOSED GAP (PR #198 review, Hoshe H2):** [`rect_window_replica`]
//! always calls `derive_at_metres` with an EMPTY `&[]` course slice — it
//! has no `RiverNetwork`/`invent_courses_near_window` wiring at all, by
//! construction (courses are invented ONCE per window, ahead of the
//! per-cell loop, inside `build_district_window_layer` itself —
//! replicating that machinery was out of scope for a bench loop whose job
//! is the per-cell derive rate, not course invention). This means EVERY
//! rectangular-canvas number in this file (the three named 16:9 shapes
//! AND the 83K deep-step bench) **excludes the per-cell
//! `river_course::near_perennial_water` riparian-test cost** production
//! pays on every cell of a window with real nearby course geometry.
//! `zoom_ladder_bench.rs`'s own `bench_course_cost_on_vs_off` measured
//! that cost at District cap (n=64, real GJ1c geometry): **+0.090.21 ms
//! against a ~5 ms baseline, under 5%** — small, but real, and this file's
//! rectangular numbers do not include it. Full disclosure and the
//! corrected "what converges with what" statement is in the results doc's
//! H2/H3 section — read that section before citing any rectangular-canvas
//! number here as courses-inclusive. It is not.
//!
//! **Convergence claim, corrected (H3):** the square production-fn path is
//! courses-INCLUSIVE (light density: 3/6/10 courses at 330K/2.07M/8.3M) and
//! the rectangular replica-loop path is courses-EMPTY (always `&[]`) — so
//! their agreement at 331,776 ≈ 576² (~191 ns/cell either way) validates the
//! ROW-CHUNKED LOOP MECHANICS (chunking granularity, dispatch overhead,
//! per-cell derive cost) converging across two independently-written call
//! sites, NOT a courses-empty-vs-courses-inclusive equivalence claim — the
//! two paths differ in exactly one respect (courses present vs absent) and
//! happen to land within noise of each other at this course DENSITY (3 out
//! of 331,776 cells is far too sparse to move the aggregate ns/cell figure
//! outside the run-to-run noise band, consistent with the <5% per-cell
//! course-cost delta `zoom_ladder_bench.rs` measured directly). The
//! courses-inclusive rate at REAL production course density (not this
//! sparse an origin-window) is covered only by the separate GJ1c real-body
//! cross-check bench below (18 courses in a 331,776-cell window, deliberately
//! centred on real river geometry) — see that bench's own doc and the
//! results doc for the exact scope of what each number does and does not
//! include.
//!
//! Run: `cargo test --release --test bmv_gridunit_bench -- --ignored --nocapture`
@@ -222,13 +264,15 @@ fn bench_square_window_production_fn_district_spacing() {
let ns_per_cell = elapsed.as_secs_f64() * 1e9 / cells as f64;
println!(
" side={side:>5} n={n:>5} cells={cells:>10} (target ~{}): \
{ms:>9.2} ms warm, {ns_per_cell:>7.1} ns/cell ({:.3} us/cell)",
{ms:>9.2} ms warm, {ns_per_cell:>7.1} ns/cell ({:.3} us/cell), \
courses_in_window={}",
match cells {
c if c < 500_000 => "330K",
c if c < 4_000_000 => "2.07M",
_ => "8.3M",
},
ns_per_cell / 1000.0
ns_per_cell / 1000.0,
layer.courses.len()
);
}
println!();
@@ -416,6 +460,22 @@ fn bench_square_window_production_fn_gj1c_real_body_crosscheck() {
/// `build_district_window_layer`'s internal loop shape cell-for-cell (see
/// module doc for the explicit diff against the real function). Returns
/// (elapsed, per_cell_ns).
///
/// **Courses are FORCE-EMPTY here, disclosed (PR #198 review, Hoshe H2):**
/// every call site below passes `&[]` for `nearby_courses` — there is no
/// `RiverNetwork`, no `invent_courses_near_window` call, and no
/// `river_course::near_perennial_water` riparian test running per cell. This
/// is a real, measured gap versus production, not a rounding footnote:
/// `zoom_ladder_bench.rs`'s `bench_course_cost_on_vs_off` measured the
/// courses-on-vs-off delta directly at District cap (n=64, real GJ1c
/// geometry) as **+0.090.21 ms against a ~5 ms baseline (under 5%)**. Every
/// number produced by this function — the three 16:9 canvas benches AND the
/// 83K deep-step bench — excludes that cost. It is EXCLUDED, not zero in
/// production; readers citing a rectangular-canvas number from this file as
/// "the real per-cell cost including courses" are citing it wrong. The
/// courses-inclusive numbers live only in the square
/// `build_district_window_layer`-backed benches above (which pass a real
/// `RiverNetwork` and print `courses_in_window`).
#[allow(clippy::too_many_arguments)]
fn rect_window_replica(
seed: SeedChain,