feat(simulation): T-1170 A2/A3 + T-1168 A5 — course inventor, coast termination, riparian signal
A2 (river_course.rs): Stage A rung-independent valley-seeking control
path (chord/8 stations, k=5 bilinear-scored candidates + continuity
penalty); Stage B rung-indexed perpendicular warp on GLOBAL arc-length
(window-independence, Ruling 1e), band chord/2 down to min_wavelength_m
hard-truncate, sine taper to zero at anchors, amplitude min(8% chord,
half-cell) slope/class-scaled. SeedDomain::RiverCourse=17, distinct
salt. Wire: RiverCourse{edge_id,class,points,terminus} on
DistrictWindowLayer.courses (serde-default); bbox-culled, window-
cropped +1 station. TerrainAnalysisCache retains Layer1Output (the
gen_queue:626 discard, Ruling 4b). A3: mouth termination walks
stations sampling the window's OWN rung-consistent morphology verdict,
6-iteration bisect; land-at-anchor probes one segment then None;
EdgeDrain never probes. A5: near_perennial_water point-to-segment
predicate (D-239 §8 governed bands 1-3m class-scaled) threaded through
both batch and window paths; Region always false; never touches
moisture_q. Discipline closed: dormant zoom-ladder bench run + numbers
recorded in the design doc (District 3.009/1.785, Quarter 1.823
us/cell, Region window 0.617ms); course-cost bench CAUGHT a real
+12-36% per-cell riparian scan regression -> precomputed bbox O(1)
reject (60ns->2.2ns/call), final delta +3.4-6.9% at budget; three
determinism tests (overlapping-window byte-identity, cross-rung
amplitude bound, warp-stream cross-correlation r<0.3); goldens: window
sweep gained a verified course-bearing position (pure append), new
river_course golden at both rungs, believability verified unchanged.
Revert-verification discovered the pole-row branch is structurally
unreachable (flow_direction bounds-check) — the real edge-drain path
is k<0 flat-plateau; test fixture rewritten to exercise reality.
scale.rs stale comment fixed. Full cargo test green.
Tickets: T-1170, T-1168
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,18 @@ const D8: [(i32, i32); 8] = [
|
||||
(1, -1), // SW
|
||||
];
|
||||
|
||||
/// `(dr, dc)` for D8 direction index `k` (0-7) — the same fixed priority-order
|
||||
/// table [`extract_river_network`]/`flow_direction` use internally, exposed
|
||||
/// `pub(crate)` so downstream consumers of [`crate::atlas::body_world_state::
|
||||
/// RiverNetwork::river_downstream`] (T-1170's river course inventor) can walk
|
||||
/// a river cell's D8 pointer without duplicating the table. Panics on an
|
||||
/// out-of-range index — callers must check against the
|
||||
/// `RIVER_DOWNSTREAM_MOUTH`/`RIVER_DOWNSTREAM_EDGE_DRAIN`/
|
||||
/// `RIVER_DOWNSTREAM_TERMINAL` sentinels (values ≥ 8) before calling this.
|
||||
pub(crate) fn d8_offset(k: u8) -> (i32, i32) {
|
||||
D8[k as usize]
|
||||
}
|
||||
|
||||
/// Result of the full D8 drainage analysis for one body.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DrainageResult {
|
||||
@@ -1132,40 +1144,68 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn pole_edge_drains_are_not_mouths() {
|
||||
// Ruling 3f, binding: a river that flows off the grid's polar edge is
|
||||
// a grid artifact, not a river-meets-sea event. Build a tiny grid that
|
||||
// slopes toward the north pole (row 0) with no ocean anywhere, so any
|
||||
// river cell reaching row 0 must exit via EDGE_DRAIN, never MOUTH —
|
||||
// and must never land in `mouths`.
|
||||
let (w, h) = (16usize, 32usize);
|
||||
// Ruling 3f, binding: a river cell with no valid D8 outflow is a grid
|
||||
// artifact, not a river-meets-sea event.
|
||||
//
|
||||
// **Why this test targets the flat/no-outflow (`k < 0`) case, not a
|
||||
// literal "flow direction points past row 0" case:** `flow_direction`
|
||||
// (this file, `fn flow_direction`) bounds-checks every D8 candidate
|
||||
// BEFORE comparing drops (`if nr < 0 || nr >= h { continue; }`) — a
|
||||
// row-0 cell can therefore never even be ASSIGNED a north-pointing
|
||||
// `fdir` in the first place; the off-grid-direction branch in
|
||||
// `extract_river_network`'s `river_downstream` computation exists as
|
||||
// correct defensive code but is structurally unreachable given this
|
||||
// invariant. The real, reachable "pole-edge-drain" case (confirmed
|
||||
// against the committed GJ1c golden fixture, which has EDGE_DRAIN
|
||||
// cells at several rows including row 0) is `k < 0`: a cell with NO
|
||||
// neighbor at a strictly lower elevation — most commonly a flat
|
||||
// plateau at the grid's fringe, which the depression-fill/flow
|
||||
// algorithm cannot route off of. This fixture constructs exactly
|
||||
// that: a perfectly flat plateau at row 0 (identical elevation
|
||||
// across the whole top row, so no cell in it has a positive-drop
|
||||
// neighbor and `flow_direction` assigns `k=-1` — verified this
|
||||
// reproduces before ever reasoning about mouths) that river cells
|
||||
// from a converging valley drain into, with no ocean anywhere.
|
||||
let (w, h) = (64usize, 64usize);
|
||||
let n = w * h;
|
||||
// Slope: elevation decreases toward row 0 (the north pole), giving a
|
||||
// clean, deterministic downhill flow off the top edge. sea_level below
|
||||
// everything so no cell is ever ocean.
|
||||
let center_col = (w / 2) as f32;
|
||||
let elev: Vec<f32> = (0..n)
|
||||
.map(|i| {
|
||||
let r = i / w;
|
||||
0.2 + (r as f32 / h as f32) * 0.7
|
||||
let c = (i % w) as f32;
|
||||
if r == 0 {
|
||||
return 0.15; // flat plateau — no cell here has a strictly lower neighbor
|
||||
}
|
||||
// V-shaped valley converging on center_col, sloping down
|
||||
// toward row 0 (but never reaching the plateau's own
|
||||
// elevation until row 1, so row-1 cells drain INTO the flat
|
||||
// row-0 plateau and then have nowhere further to go).
|
||||
let dist_from_center = (c - center_col).abs() / center_col;
|
||||
let valley = 0.15 + dist_from_center * 0.6;
|
||||
let pole_gradient = (r as f32 / h as f32) * 0.25;
|
||||
(valley + pole_gradient).clamp(0.0, 1.0)
|
||||
})
|
||||
.collect();
|
||||
let result = analyze(&elev, w as u32, h as u32, 0.0);
|
||||
let rn = &result.river_network;
|
||||
if rn.river_cells.is_empty() {
|
||||
// Too small a grid to clear RIVER_THRESHOLD — nothing to assert,
|
||||
// but not a test failure (the threshold is a fixed constant this
|
||||
// synthetic tiny grid isn't guaranteed to reach).
|
||||
return;
|
||||
}
|
||||
assert!(
|
||||
rn.mouths.is_empty(),
|
||||
"an all-land, pole-draining world must have zero mouths — got {:?}",
|
||||
rn.mouths
|
||||
!rn.river_cells.is_empty(),
|
||||
"the converging-valley fixture must clear RIVER_THRESHOLD — if this starts \
|
||||
failing, the fixture (not the production code) needs retuning, since an \
|
||||
empty river_cells silently no-ops every assertion below"
|
||||
);
|
||||
assert!(
|
||||
rn.river_downstream
|
||||
.iter()
|
||||
.any(|&v| v == RIVER_DOWNSTREAM_EDGE_DRAIN),
|
||||
"expected at least one EDGE_DRAIN-sentinel river cell on a pole-draining world"
|
||||
"expected at least one EDGE_DRAIN-sentinel river cell (the flat-plateau case) \
|
||||
on this fixture — downstream values were {:?}",
|
||||
rn.river_downstream
|
||||
);
|
||||
assert!(
|
||||
rn.mouths.is_empty(),
|
||||
"an all-land, pole-draining world must have zero mouths — got {:?}",
|
||||
rn.mouths
|
||||
);
|
||||
assert!(
|
||||
!rn.river_downstream
|
||||
|
||||
Reference in New Issue
Block a user