diff --git a/docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md b/docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md index d724fad73..b2a3dca6d 100644 --- a/docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md +++ b/docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md @@ -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.7–0.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): ~190–220 ns/cell parallel flat from 330K→8.3M cells; 330K canvas ~64 ms, 2.07M ~0.4 s, 8.3M ~1.7–1.8 s (7.5–8.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): ~190–220 ns/cell parallel flat from 330K→8.3M cells; 330K canvas ~64 ms, 2.07M ~0.4 s, 8.3M ~1.7–1.8 s (7.5–8.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, 3–10 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.8–1.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.4–2.9 ms frame-delta spike — never near the 16.6 ms budget. **L8 is 4–9× cheaper at every size** (~0.5–0.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`. diff --git a/docs/workshops/body-map-viewer/measurements/t1177-hydrology.md b/docs/workshops/body-map-viewer/measurements/t1177-hydrology.md index 083173ad2..f96971933 100644 --- a/docs/workshops/body-map-viewer/measurements/t1177-hydrology.md +++ b/docs/workshops/body-map-viewer/measurements/t1177-hydrology.md @@ -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 diff --git a/docs/workshops/body-map-viewer/measurements/t1178-t1154-derive-bench.md b/docs/workshops/body-map-viewer/measurements/t1178-t1154-derive-bench.md index c92941fb6..fca66bea6 100644 --- a/docs/workshops/body-map-viewer/measurements/t1178-t1154-derive-bench.md +++ b/docs/workshops/body-map-viewer/measurements/t1178-t1154-derive-bench.md @@ -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.3–220.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.09–0.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 (3–10 courses in a 331,776–8,294,400-cell window) and the +rectangular replica-loop path is courses-EMPTY (always `&[]`, by +construction) — so their agreement at 331,776 ≈ 576² (191.1–191.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 3–10 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 (~190–220 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.09–0.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 diff --git a/server/src/atlas/hydrology_equilibrium.rs b/server/src/atlas/hydrology_equilibrium.rs index 90402f5af..7f581d552 100644 --- a/server/src/atlas/hydrology_equilibrium.rs +++ b/server/src/atlas/hydrology_equilibrium.rs @@ -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, diff --git a/server/tests/bmv_gridunit_bench.rs b/server/tests/bmv_gridunit_bench.rs index 2a2ceae10..14cc8e491 100644 --- a/server/tests/bmv_gridunit_bench.rs +++ b/server/tests/bmv_gridunit_bench.rs @@ -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.09–0.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.09–0.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,