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:
2026-07-23 13:36:16 +02:00
co-authored by Claude Fable 5
parent c31cc6220e
commit 4320df9b60
16 changed files with 4353 additions and 78 deletions
+747 -3
View File
@@ -15,12 +15,15 @@ use bevy_ecs::prelude::Resource;
use serde::{Deserialize, Serialize};
use crate::atlas::body_params_reader::BodyParamsReader;
use crate::atlas::body_world_state::{BodyWorldState, BodyWorldStateCache, SimTick};
use crate::atlas::body_world_state::{
BodyWorldState, BodyWorldStateCache, RiverNetwork, SimTick,
};
use crate::atlas::cascade::CascadeLayer;
use crate::atlas::city_context_reader::CityContextReader;
use crate::atlas::district_profile::{BodyParams, DistrictPos};
use crate::atlas::gen_queue::{GenPriority, GenWorkItem, GenerationQueue};
use crate::atlas::layer1::Layer1Output;
use crate::atlas::river_course::{self, EdgeTerminusKind, InventedCourse};
use crate::atlas::road_graph::RoadNodeKind;
use crate::atlas::scale::DISTRICT_M;
use crate::atlas::source_resolver::{BodySourceResolver, SourceResolveError};
@@ -845,6 +848,66 @@ pub struct DistrictWindowLayer {
pub vegetation: Vec<u8>,
/// `GlaciationGrade` discriminant, 0-4 (T-1127).
pub glaciation: Vec<u8>,
/// Invented river course polylines intersecting this window (T-1170,
/// Ruling 1b/1c/3h). **Not part of the windowed-family ceiling** (D-226
/// T-1124 §2 [HARD]) — that ceiling counts windowed-QUERY fields; this is
/// content of the ONE existing windowed payload, arriving on the same
/// echo key with the same staleness semantics as the six dense arrays
/// above (governance capture: `governance/decisions/architecture.md`,
/// D-226 amendment 2026-07-23, course-invention carrier note).
/// `#[serde(default)]` — the additive T-1124 §1 pattern: a pre-T-1170
/// payload/fixture decodes to an empty `Vec`, never an error.
#[serde(default)]
pub courses: Vec<RiverCourse>,
}
/// One invented river course polyline intersecting a window (T-1170, Ruling
/// 3h). Only edges whose amplitude-inflated chord bounding box intersects the
/// window ship; `points` are cropped to the window plus one station beyond
/// each edge of it (so client-side polyline drawing has continuity into the
/// next window without needing to stitch across a request boundary).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RiverCourse {
/// The packed upstream-cell id (`river_course::pack_cell_id`) — the
/// edge's stable identity (Ruling 2d), stable across every window/rung
/// that ships this same edge.
pub edge_id: u32,
/// `river_class` at the edge's upstream cell (0=stream, 1=tributary,
/// 2=trunk) — the SAME vocabulary `RiverNetwork.river_class` uses, so
/// client-side per-rung/per-class filtering (Araminta's presentation
/// tables, Ruling 5c) reuses the existing decode path.
pub class: u8,
/// Points along the course, in absolute world metres, cropped to this
/// window (+ one station beyond each edge, Ruling 3h).
pub points: Vec<(i32, i32)>,
/// How this course's downstream end resolves (Ruling 3e/3f) — `None` when
/// the course's true downstream terminus (whether `Mouth` or
/// `ContinuesBeyondWindow`) falls outside this window's cropped point
/// range, so nothing about the terminus can be asserted from this
/// payload alone.
pub terminus: CourseTerminus,
}
/// [`RiverCourse::terminus`] — the course's downstream-end classification on
/// the wire (Ruling 3h).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CourseTerminus {
/// The course's downstream end is not within this window's cropped point
/// range — the real terminus (whatever it is) lies in a different window.
None,
/// The course reaches a real sea/lake crossing within this window (Ruling
/// 3e) — the last point in `points` is the resolved invented-coast
/// terminus.
Mouth,
/// The course reaches a grid-edge drain (Ruling 3f) — a grid artifact,
/// not a mouth; the last point in `points` is the last in-grid station,
/// with no mouth marker implied.
EdgeDrain,
/// The course's downstream end is a real river cell beyond this window's
/// crop range — i.e. an `Interior`-terminus edge whose full extent is
/// wider than what got cropped in. The client draws the polyline without
/// a terminus marker and expects it to continue in an adjacent window.
ContinuesBeyondWindow,
}
/// Key for the server-side window derive cache (T-1137, extended T-1150,
@@ -984,6 +1047,7 @@ fn derive_window_cell(
min_wavelength_m: f64,
row: i32,
col: i32,
nearby_courses: &[InventedCourse],
) -> WindowCell {
// Row 0 = northmost, matching aliveness_probe's render_window_panels
// (derive_at_metres maps negative wy to negative lat_frac = north).
@@ -1004,6 +1068,7 @@ fn derive_window_cell(
wy,
climate,
min_wavelength_m,
nearby_courses,
)
}
};
@@ -1057,6 +1122,335 @@ fn center_to_world_m(center: DistrictPos) -> (f64, f64) {
(center.0 as f64 * dm, center.1 as f64 * dm)
}
/// Peak Stage-B course amplitude never exceeds this fraction of an edge's
/// chord (mirrors `river_course::STAGE_B_PEAK_FRACTION_OF_CHORD` — kept as an
/// independent constant here, not a re-export, so the culling inflation and
/// the actual amplitude cap can never silently decouple through a shared
/// mutable import path; a `const _: () = assert!(...)` below pins the two
/// values equal). Used to inflate an edge's chord bounding box before the
/// window-intersection cull (Ruling 3h: "amplitude-inflated chord bbox").
const COURSE_BBOX_INFLATION_FRACTION: f64 = 0.08;
const _: () = assert!(
(COURSE_BBOX_INFLATION_FRACTION * 1_000_000.0) as i64
== (crate::atlas::river_course::STAGE_B_PEAK_FRACTION_OF_CHORD * 1_000_000.0) as i64
);
/// The window's world-metre rect, `(x0, y0, x1, y1)` — the SAME convention
/// [`derive_window_cell`] uses to place cells: `step = granularity.spacing_m()`,
/// `[center_world_m - half*step, center_world_m + (side-half)*step)` on each
/// axis. Shared by [`invent_courses_near_window`] and [`crop_courses_for_wire`]
/// so the rect can never drift between the two.
fn window_world_rect(
center_world_m: (f64, f64),
half_cells: i32,
side: i32,
step_m: f64,
) -> (f64, f64, f64, f64) {
(
center_world_m.0 - half_cells as f64 * step_m,
center_world_m.1 - half_cells as f64 * step_m,
center_world_m.0 + (side - half_cells) as f64 * step_m,
center_world_m.1 + (side - half_cells) as f64 * step_m,
)
}
/// Invent every river course whose amplitude-inflated chord bounding box
/// intersects this window (T-1170 A2, Ruling 1b/3h/4b) — the FULL-precision
/// [`InventedCourse`] list, NOT yet cropped to the window or converted to the
/// wire [`RiverCourse`] shape. This is the single source both consumers read
/// from: [`derive_window_cell`]'s per-cell riparian test (T-1168, Ruling 4b:
/// "in the window path, T-1170's already-invented courses") and
/// [`crop_courses_for_wire`]'s wire packing — computed ONCE per window,
/// before the per-cell derive loop, rather than twice or per-cell.
///
/// Pure function of `(seed, body, river_network, window rect, granularity,
/// min_wavelength_m)` — independent of whether the caller derives cells
/// serially or in parallel, which is why both [`build_district_window_layer`]
/// and its `#[cfg(test)]` serial twin call this SAME function.
///
/// Region granularity draws courses via the whole-body skeleton path (Ruling
/// 5a — the rung-truncated course degenerates to the straight chord at
/// Region spacing, so the skeleton dots/chords ARE the course there). No
/// windowed course invention at Region — an empty result here is correct,
/// not a gap.
#[allow(clippy::too_many_arguments)]
fn invent_courses_near_window(
seed: SeedChain,
params: &crate::atlas::district_profile::BodyParams,
ta: &crate::atlas::features::TerrainAnalysis,
river_network: &RiverNetwork,
window_rect: (f64, f64, f64, f64),
granularity: WindowGranularity,
min_wavelength_m: f64,
) -> Vec<InventedCourse> {
if granularity == WindowGranularity::Region {
return Vec::new();
}
let step_m = granularity.spacing_m();
let (win_x0, win_y0, win_x1, win_y1) = window_rect;
let edges = river_course::build_edges(river_network);
let mut courses = Vec::new();
for edge in &edges {
let anchor_a = crate::atlas::district_profile::pixel_to_world_m(
edge.upstream.1 as f64,
edge.upstream.0 as f64,
ta.w,
ta.h,
params.body_radius_km,
);
let anchor_b = crate::atlas::district_profile::pixel_to_world_m(
edge.downstream.1 as f64,
edge.downstream.0 as f64,
ta.w,
ta.h,
params.body_radius_km,
);
let chord_m = ((anchor_a.0 - anchor_b.0).powi(2) + (anchor_a.1 - anchor_b.1).powi(2)).sqrt();
let inflate_m = chord_m * COURSE_BBOX_INFLATION_FRACTION;
let (bx0, bx1) = (
anchor_a.0.min(anchor_b.0) - inflate_m,
anchor_a.0.max(anchor_b.0) + inflate_m,
);
let (by0, by1) = (
anchor_a.1.min(anchor_b.1) - inflate_m,
anchor_a.1.max(anchor_b.1) + inflate_m,
);
// Bbox-vs-window intersection cull — most edges cull to zero for any
// given window (Ruling 4b's "most cells cull to zero edges" applies
// symmetrically here: most EDGES cull out of any one window).
if bx1 < win_x0 || bx0 > win_x1 || by1 < win_y0 || by0 > win_y1 {
continue;
}
courses.push(river_course::invent_course(
seed,
edge,
ta,
params,
step_m,
min_wavelength_m,
));
}
courses
}
/// Crop the window's already-invented courses ([`invent_courses_near_window`])
/// to the wire [`RiverCourse`] shape (Ruling 3h) — window rect + one station
/// beyond each edge, terminus resolution (A3, Ruling 3e/3f).
#[allow(clippy::too_many_arguments)]
fn crop_courses_for_wire(
invented: &[InventedCourse],
window_rect: (f64, f64, f64, f64),
seed: SeedChain,
body_id: &str,
params: &crate::atlas::district_profile::BodyParams,
ta: &crate::atlas::features::TerrainAnalysis,
climate: &crate::atlas::district_profile::ClimateConstants,
min_wavelength_m: f64,
) -> Vec<RiverCourse> {
invented
.iter()
.filter_map(|course| {
crop_course_to_window(
course,
window_rect,
seed,
body_id,
params,
ta,
climate,
min_wavelength_m,
)
})
.collect()
}
/// Crop an [`InventedCourse`]'s full-edge point list to `window_rect` (+ one
/// station beyond each edge, Ruling 3h) and resolve its wire [`CourseTerminus`]
/// (A3, Ruling 3e/3f). Returns `None` when the course has zero points inside
/// (or adjacent to) the window — the caller's cull is a cheap bbox pre-filter,
/// this is the exact per-point check.
#[allow(clippy::too_many_arguments)]
fn crop_course_to_window(
course: &InventedCourse,
window_rect: (f64, f64, f64, f64),
seed: SeedChain,
body_id: &str,
params: &crate::atlas::district_profile::BodyParams,
ta: &crate::atlas::features::TerrainAnalysis,
climate: &crate::atlas::district_profile::ClimateConstants,
min_wavelength_m: f64,
) -> Option<RiverCourse> {
let (x0, y0, x1, y1) = window_rect;
let inside = |p: &(f64, f64)| p.0 >= x0 && p.0 <= x1 && p.1 >= y0 && p.1 <= y1;
let n = course.points.len();
let mut first_in: Option<usize> = None;
let mut last_in: Option<usize> = None;
for (i, p) in course.points.iter().enumerate() {
if inside(p) {
first_in.get_or_insert(i);
last_in = Some(i);
}
}
let (first_in, last_in) = match (first_in, last_in) {
(Some(a), Some(b)) => (a, b),
_ => return None, // no point of this course falls inside the window
};
// Crop range: one station beyond each edge (Ruling 3h), clamped to the
// course's own point range.
let lo = first_in.saturating_sub(1);
let hi = (last_in + 1).min(n.saturating_sub(1));
let points: Vec<(i32, i32)> = course.points[lo..=hi]
.iter()
.map(|p| (p.0.round() as i32, p.1.round() as i32))
.collect();
// Terminus resolution (A3, Ruling 3e/3f): only meaningful if the
// course's TRUE downstream end (the last point of the full, uncropped
// course) is within this cropped range — otherwise the real terminus
// lies in a different window and this one just sees a mid-course
// passthrough.
let true_end_included = hi == n.saturating_sub(1);
let terminus = if !true_end_included {
CourseTerminus::ContinuesBeyondWindow
} else {
match course.terminus {
EdgeTerminusKind::EdgeDrain => CourseTerminus::EdgeDrain,
EdgeTerminusKind::Interior => CourseTerminus::ContinuesBeyondWindow,
EdgeTerminusKind::Mouth => {
match resolve_mouth_terminus(course, seed, body_id, params, ta, climate, min_wavelength_m)
{
Some(mouth_point) => {
// Replace the cropped course's tail with the resolved
// mouth point (bisected against the last land
// station) so the wire polyline ends exactly at the
// invented-coast crossing, not at the raw upstream
// anchor placeholder `build_edges` recorded.
let mut pts = points;
if let Some(last) = pts.last_mut() {
*last = (mouth_point.0.round() as i32, mouth_point.1.round() as i32);
}
return Some(RiverCourse {
edge_id: course.edge_id,
class: course.class,
points: pts,
terminus: CourseTerminus::Mouth,
});
}
None => CourseTerminus::None, // degenerate: never found water (Ruling 3e land-at-anchor case)
}
}
}
};
Some(RiverCourse {
edge_id: course.edge_id,
class: course.class,
points,
terminus,
})
}
/// Number of bisection iterations for the mouth-terminus search (Ruling 3e,
/// binding: "fixed 6 iterations").
const MOUTH_BISECT_ITERATIONS: u32 = 6;
/// Walk a `Mouth`-terminus course's stations upstream→downstream, sampling
/// the SAME rung-consistent morphology water verdict the window's own cells
/// use (`derive_at_metres(...).morphology_zone` — Ruling 3e, binding: "never
/// raw `ocean_frac`"). First water station found → bisect against the
/// previous land station (fixed [`MOUTH_BISECT_ITERATIONS`]) → the resolved
/// terminus point. If no station (including one D8-direction cell-length
/// probe past the final anchor) samples water, returns `None` — the
/// degenerate "drawn coast receded past this edge" case (Ruling 3e), which
/// the caller renders with no mouth flag.
fn resolve_mouth_terminus(
course: &InventedCourse,
seed: SeedChain,
body_id: &str,
params: &crate::atlas::district_profile::BodyParams,
ta: &crate::atlas::features::TerrainAnalysis,
climate: &crate::atlas::district_profile::ClimateConstants,
min_wavelength_m: f64,
) -> Option<(f64, f64)> {
let is_water = |p: (f64, f64)| -> bool {
// `&[]`: the mouth-termination water-verdict probe has no use for
// the riparian signal (it only reads `morphology_zone`, never
// `vegetation_class`) — an empty course slice is a correct, cheap
// no-op here (T-1168's `nearby_courses` param never affects
// morphology, only vegetation, so this can never mis-terminate).
let prof = crate::atlas::district_profile::derive_at_metres(
seed,
body_id,
params,
ta,
p.0,
p.1,
climate,
min_wavelength_m,
&[],
);
matches!(
prof.morphology_zone,
crate::simulation::generator::MorphologyZone::OpenOcean
| crate::simulation::generator::MorphologyZone::Lake
)
};
let pts = &course.points;
if pts.is_empty() {
return None;
}
// Walk upstream -> downstream (points are already stored in that order).
let mut prev_land = pts[0];
for &p in pts.iter() {
if is_water(p) {
return Some(bisect_to_waterline(prev_land, p, is_water));
}
prev_land = p;
}
// Final anchor still land: extend one cell length along the segment's
// own direction as a single probe (Ruling 3e: "extend along the D8
// direction up to one cell length probing").
if pts.len() >= 2 {
let a = pts[pts.len() - 2];
let b = pts[pts.len() - 1];
let (dx, dy) = (b.0 - a.0, b.1 - a.1);
let len = (dx * dx + dy * dy).sqrt();
if len > 1e-6 {
let probe = (b.0 + dx / len * len, b.1 + dy / len * len); // one more segment-length step
if is_water(probe) {
return Some(bisect_to_waterline(b, probe, is_water));
}
}
}
None // degenerate: still land — terminate with no mouth flag (caller's job)
}
/// Bisect between a known-land point and a known-water point for
/// [`MOUTH_BISECT_ITERATIONS`] iterations, returning the point closest to the
/// water side of the crossing.
fn bisect_to_waterline(
land: (f64, f64),
water: (f64, f64),
is_water: impl Fn((f64, f64)) -> bool,
) -> (f64, f64) {
let mut lo = land; // land
let mut hi = water; // water
for _ in 0..MOUTH_BISECT_ITERATIONS {
let mid = ((lo.0 + hi.0) * 0.5, (lo.1 + hi.1) * 0.5);
if is_water(mid) {
hi = mid;
} else {
lo = mid;
}
}
hi
}
/// Build a [`DistrictWindowLayer`] by deriving every cell in the window
/// around `center` (T-1137, extended T-1150). Mirrors
/// `aliveness_probe::render_window_panels`'s derive loop exactly (the probe
@@ -1098,6 +1492,7 @@ pub fn build_district_window_layer(
body_id: &str,
params: &crate::atlas::district_profile::BodyParams,
ta: &crate::atlas::features::TerrainAnalysis,
river_network: &RiverNetwork,
center: DistrictPos,
n: u32,
climate: &crate::atlas::district_profile::ClimateConstants,
@@ -1118,6 +1513,17 @@ pub fn build_district_window_layer(
let mut vegetation = vec![0u8; cells];
let mut glaciation = vec![0u8; cells];
// T-1170 A2/T-1168 A5: invent this window's river courses ONCE, before
// the per-cell derive loop — this is the single source both the per-cell
// riparian test (T-1168, threaded into `derive_window_cell` below) and
// the wire course packing (crop step, after the loop) read from. Doing
// this first (not per-cell, not twice) is what keeps the window-cost
// delta close to the Discipline item 2 ~5% budget.
let step_m = granularity.spacing_m();
let window_rect = window_world_rect(center_world_m, half, side, step_m);
let invented_courses =
invent_courses_near_window(seed, params, ta, river_network, window_rect, granularity, min_wavelength_m);
// One Rayon task per row: derive_window_cell(row, ..) for every col, then
// scatter that row's results into the flat arrays. Row order in the
// output collection is preserved by `par_iter` (it yields in index
@@ -1140,6 +1546,7 @@ pub fn build_district_window_layer(
min_wavelength_m,
row,
col,
&invented_courses,
)
})
.collect()
@@ -1160,6 +1567,17 @@ pub fn build_district_window_layer(
);
}
let courses = crop_courses_for_wire(
&invented_courses,
window_rect,
seed,
body_id,
params,
ta,
climate,
min_wavelength_m,
);
DistrictWindowLayer {
center,
n,
@@ -1172,6 +1590,7 @@ pub fn build_district_window_layer(
moisture_q,
vegetation,
glaciation,
courses,
}
}
@@ -1185,6 +1604,7 @@ fn build_district_window_layer_serial(
body_id: &str,
params: &crate::atlas::district_profile::BodyParams,
ta: &crate::atlas::features::TerrainAnalysis,
river_network: &RiverNetwork,
center: DistrictPos,
n: u32,
climate: &crate::atlas::district_profile::ClimateConstants,
@@ -1202,6 +1622,10 @@ fn build_district_window_layer_serial(
let mut moisture_q = vec![0u8; cells];
let mut vegetation = vec![0u8; cells];
let mut glaciation = vec![0u8; cells];
let step_m = granularity.spacing_m();
let window_rect = window_world_rect(center_world_m, half, side, step_m);
let invented_courses =
invent_courses_near_window(seed, params, ta, river_network, window_rect, granularity, min_wavelength_m);
for row in 0..side {
let row_cells: Vec<WindowCell> = (0..side)
.map(|col| {
@@ -1217,6 +1641,7 @@ fn build_district_window_layer_serial(
min_wavelength_m,
row,
col,
&invented_courses,
)
})
.collect();
@@ -1232,6 +1657,16 @@ fn build_district_window_layer_serial(
&mut glaciation,
);
}
let courses = crop_courses_for_wire(
&invented_courses,
window_rect,
seed,
body_id,
params,
ta,
climate,
min_wavelength_m,
);
DistrictWindowLayer {
center,
n,
@@ -1244,6 +1679,7 @@ fn build_district_window_layer_serial(
moisture_q,
vegetation,
glaciation,
courses,
}
}
@@ -2101,6 +2537,18 @@ mod tests {
TerrainAnalysis::analyze(hm, &dr)
}
/// T-1170: the `RiverNetwork` companion to [`window_test_ta`] — most
/// existing window-builder tests don't care about courses at all (this
/// synthetic gradient fixture may have zero river cells), so an empty
/// default is the common case; call sites that DO care about courses use
/// a real fixture (`window_test_gj1c_network`) instead.
fn window_test_river_network(
hm: &crate::atlas::heightmap::BodyHeightmap,
) -> crate::atlas::body_world_state::RiverNetwork {
use crate::atlas::drainage;
drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level).river_network
}
fn window_test_params() -> crate::atlas::district_profile::BodyParams {
crate::atlas::district_profile::BodyParams {
hydrosphere: Some("ocean".into()),
@@ -2120,6 +2568,7 @@ mod tests {
fn build_district_window_layer_produces_dense_n_by_n_grid() {
let hm = window_test_hm();
let ta = window_test_ta(&hm);
let rn = window_test_river_network(&hm);
let params = window_test_params();
let climate = crate::atlas::district_profile::ClimateConstants::default();
let seed = SeedChain::root(42).derive(SeedDomain::Body, 1);
@@ -2130,6 +2579,7 @@ mod tests {
"test_body",
&params,
&ta,
&rn,
(10, -5),
n,
&climate,
@@ -2170,12 +2620,295 @@ mod tests {
assert_eq!(layer.glaciation[0], prof.glaciation_grade as u8);
}
/// T-1170/T-1168 A5 integration: the batch path
/// (`derive_district_profile`, sourcing courses via `near_perennial_water_at`
/// on demand) and the window path (`build_district_window_layer`,
/// sourcing courses via the pre-invented `Vec<InventedCourse>`) must
/// resolve the SAME riparian verdict for the SAME world position —
/// Ruling 4b's "batch and window paths can never silently disagree"
/// binding requirement, checked end to end (not just at the
/// `near_perennial_water`/`near_perennial_water_at` unit level).
///
/// **Design note:** this test deliberately does NOT compare the batch
/// and window paths' full `DistrictProfile` output for "the same
/// district" — `derive_district_profile`'s cell-aggregate-centre
/// sampling and the window path's district-origin sampling are
/// legitimate, PRE-EXISTING different world positions for the same
/// `DistrictPos` (a real quirk of the two derivation strategies,
/// unrelated to T-1168/T-1170), so `morphology_zone`/`elev_q`/etc.
/// routinely differ between them even before this batch's riparian work.
/// Instead this test isolates the ONE signal this batch actually wires
/// (`near_perennial_water`) at a SHARED, EXACT world position, proving
/// the two paths' independent riparian derivations agree there.
#[test]
fn window_and_batch_paths_agree_on_riparian_signal_near_a_real_river_edge() {
use crate::atlas::drainage;
use crate::atlas::heightmap::load_heightmap_png;
use crate::atlas::river_course;
let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png");
let heightmap =
load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap");
let small = heightmap.downsample(256, 128);
let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level);
let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr);
let rn = &dr.river_network;
assert!(
!rn.river_cells.is_empty(),
"GJ1c downsample must have river cells for this test to be meaningful"
);
let params = crate::atlas::district_profile::BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("temperate".into()),
body_radius_km: Some(6371.0),
..Default::default()
};
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1);
let station_spacing_m = DISTRICT_M as f64;
// Invent a real edge and sample a point exactly on its course.
let edges = river_course::build_edges(rn);
let edge = edges
.iter()
.find(|e| e.terminus == river_course::EdgeTerminusKind::Interior)
.expect("GJ1c should have an interior river edge");
let course = river_course::invent_course(seed, edge, &ta, &params, station_spacing_m, 0.0);
let on_course = course.points[course.points.len() / 2];
// Batch path: near_perennial_water_at (invents nearby edges on demand
// from `rn` directly).
let batch_signal = river_course::near_perennial_water_at(
seed,
&ta,
&params,
rn,
on_course,
station_spacing_m,
0.0,
);
// Window path: invent_courses_near_window (the SAME pre-invention step
// `build_district_window_layer` uses) around a window rect containing
// `on_course`, then near_perennial_water against that pre-invented list.
let window_rect = (
on_course.0 - 10_000.0,
on_course.1 - 10_000.0,
on_course.0 + 10_000.0,
on_course.1 + 10_000.0,
);
let invented = invent_courses_near_window(
seed,
&params,
&ta,
rn,
window_rect,
WindowGranularity::District,
0.0,
);
let window_signal = river_course::near_perennial_water(on_course, &invented);
assert!(
batch_signal,
"a point exactly on an invented course must read near_perennial_water_at == true (batch path)"
);
assert_eq!(
batch_signal, window_signal,
"batch (near_perennial_water_at) and window (invent_courses_near_window + \
near_perennial_water) paths must agree on the riparian verdict at the SAME \
world position {on_course:?}"
);
}
/// Discipline item 3(a), mandatory: two overlapping windows sharing a
/// stretch of the same edge must produce BYTE-IDENTICAL course points
/// for that shared stretch (Ruling 1e, the window-independence
/// invariant — "stations are generated at deterministic global
/// arc-length positions along the edge; the window crops, it never
/// re-parametrizes"). Two windows at different centers, both containing
/// the same real GJ1c edge, must report the identical `RiverCourse` for
/// that edge wherever both windows' cropped ranges overlap.
#[test]
fn overlapping_windows_produce_byte_identical_course_points() {
use crate::atlas::drainage;
use crate::atlas::heightmap::load_heightmap_png;
use crate::atlas::river_course;
let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png");
let heightmap =
load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap");
let small = heightmap.downsample(256, 128);
let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level);
let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr);
let rn = &dr.river_network;
let params = crate::atlas::district_profile::BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("temperate".into()),
body_radius_km: Some(6371.0),
..Default::default()
};
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1);
let station_spacing_m = DISTRICT_M as f64;
let edges = river_course::build_edges(rn);
// Find the LONGEST interior edge (by point count) so the two windows
// below can each cover a genuine, well-inside-their-bounds stretch —
// a short edge's course could produce degenerate/edge-of-range
// overlaps that don't actually exercise the invariant.
let edge = edges
.iter()
.filter(|e| e.terminus == river_course::EdgeTerminusKind::Interior)
.max_by_key(|e| {
let course =
river_course::invent_course(seed, e, &ta, &params, station_spacing_m, 0.0);
course.points.len()
})
.expect("GJ1c should have an interior river edge");
let full_course =
river_course::invent_course(seed, edge, &ta, &params, station_spacing_m, 0.0);
assert!(
full_course.points.len() >= 4,
"need a course with enough stations to construct two overlapping windows"
);
// A midpoint on the course — the shared stretch two different
// windows will both cover.
let mid = full_course.points[full_course.points.len() / 2];
// Two DIFFERENT window rects, both containing `mid` well inside
// their bounds (so both windows' crop ranges include the shared
// stretch, not just a single boundary point).
let window_a = (mid.0 - 20_000.0, mid.1 - 20_000.0, mid.0 + 5_000.0, mid.1 + 5_000.0);
let window_b = (mid.0 - 5_000.0, mid.1 - 5_000.0, mid.0 + 20_000.0, mid.1 + 20_000.0);
let invented_a =
invent_courses_near_window(seed, &params, &ta, rn, window_a, WindowGranularity::District, 0.0);
let invented_b =
invent_courses_near_window(seed, &params, &ta, rn, window_b, WindowGranularity::District, 0.0);
let course_a = invented_a
.iter()
.find(|c| c.edge_id == edge.edge_id)
.expect("edge must be invented for window A");
let course_b = invented_b
.iter()
.find(|c| c.edge_id == edge.edge_id)
.expect("edge must be invented for window B");
// Ruling 1e's actual invariant: invent_courses_near_window returns
// the FULL invented course for any edge that culls in — never
// window-cropped or re-parametrized at this layer (cropping happens
// later, in crop_courses_for_wire). So the two windows' invented
// points for the SAME edge must be byte-identical in full, not just
// over some overlap region — this is the direct proof that
// invention is independent of the window rect entirely.
assert_eq!(
course_a.points, course_b.points,
"the same edge invented from two different windows must be byte-identical (D-227/Ruling 1e)"
);
}
/// Discipline item 3(b), mandatory: Quarter course points must stay
/// within the truncated-octave amplitude bound of the District course at
/// the same world position (Ruling 3b's cross-rung invariant — "the
/// Quarter course is the District course plus octaves in the (1,024
/// m..4,096 m) band"). Checked via the perpendicular deviation between
/// the two rungs' station lists never exceeding the District-rung peak
/// amplitude cap by more than a small tolerance (Quarter's extra octaves
/// can only ADD bounded displacement on top of the District shape, never
/// diverge unboundedly).
#[test]
fn quarter_course_stays_within_district_amplitude_bound() {
use crate::atlas::drainage;
use crate::atlas::heightmap::load_heightmap_png;
use crate::atlas::river_course;
let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png");
let heightmap =
load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap");
let small = heightmap.downsample(256, 128);
let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level);
let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr);
let rn = &dr.river_network;
let params = crate::atlas::district_profile::BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("temperate".into()),
body_radius_km: Some(6371.0),
..Default::default()
};
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1);
let edges = river_course::build_edges(rn);
let edge = edges
.iter()
.find(|e| e.terminus == river_course::EdgeTerminusKind::Interior)
.expect("GJ1c should have an interior river edge");
let district_course = river_course::invent_course(
seed,
edge,
&ta,
&params,
DISTRICT_M as f64,
2.0 * DISTRICT_M as f64, // District's real Nyquist-floor cutoff
);
let quarter_course = river_course::invent_course(
seed,
edge,
&ta,
&params,
crate::atlas::scale::QUARTER_M as f64,
2.0 * crate::atlas::scale::QUARTER_M as f64, // Quarter's real cutoff
);
// For each District station, find the nearest Quarter station (by
// arc-length proxy: nearest point in world space) and confirm the
// deviation stays within the District-rung amplitude cap (Stage B's
// own hard cap, Ruling 3c) plus a small numeric tolerance — Quarter
// must refine the shape, never blow past the amplitude budget the
// SAME peak-fraction-of-chord cap governs at every rung.
let anchor_a = district_course.points[0];
let anchor_b = *district_course.points.last().unwrap();
let chord_m =
((anchor_a.0 - anchor_b.0).powi(2) + (anchor_a.1 - anchor_b.1).powi(2)).sqrt();
let cap_m = (chord_m * river_course::STAGE_B_PEAK_FRACTION_OF_CHORD)
.min(crate::atlas::scale::QUARTER_M as f64 * 0.5)
* 1.35; // widest class_scale entry (trunk)
for &dp in &district_course.points {
let nearest_q = quarter_course
.points
.iter()
.min_by(|a, b| {
let da = (a.0 - dp.0).powi(2) + (a.1 - dp.1).powi(2);
let db = (b.0 - dp.0).powi(2) + (b.1 - dp.1).powi(2);
da.partial_cmp(&db).unwrap()
})
.unwrap();
let dist = ((nearest_q.0 - dp.0).powi(2) + (nearest_q.1 - dp.1).powi(2)).sqrt();
assert!(
dist <= cap_m + 50.0, // small slack for nearest-station (not exact arc-length) matching
"Quarter course deviates {dist} m from the nearest District station — \
exceeds the {cap_m} m amplitude bound (Ruling 3b cross-rung invariant)"
);
}
}
/// Clamped-window edge: `n = 1` is the minimum valid window (a single
/// district) — no panic, no empty output, exactly one cell per array.
#[test]
fn build_district_window_layer_handles_n_equals_one() {
let hm = window_test_hm();
let ta = window_test_ta(&hm);
let rn = window_test_river_network(&hm);
let params = window_test_params();
let climate = crate::atlas::district_profile::ClimateConstants::default();
let seed = SeedChain::root(1).derive(SeedDomain::Body, 1);
@@ -2185,6 +2918,7 @@ mod tests {
"test_body",
&params,
&ta,
&rn,
(0, 0),
1,
&climate,
@@ -2209,6 +2943,7 @@ mod tests {
fn build_district_window_layer_two_passes_are_byte_identical() {
let hm = window_test_hm();
let ta = window_test_ta(&hm);
let rn = window_test_river_network(&hm);
let params = window_test_params();
let climate = crate::atlas::district_profile::ClimateConstants::default();
let seed = SeedChain::root(7).derive(SeedDomain::Body, 3);
@@ -2219,6 +2954,7 @@ mod tests {
"test_body",
&params,
&ta,
&rn,
(3, -2),
n,
&climate,
@@ -2230,6 +2966,7 @@ mod tests {
"test_body",
&params,
&ta,
&rn,
(3, -2),
n,
&climate,
@@ -2252,6 +2989,7 @@ mod tests {
fn build_district_window_layer_parallel_matches_serial() {
let hm = window_test_hm();
let ta = window_test_ta(&hm);
let rn = window_test_river_network(&hm);
let params = window_test_params();
let climate = crate::atlas::district_profile::ClimateConstants::default();
let seed = SeedChain::root(13).derive(SeedDomain::Body, 4);
@@ -2263,6 +3001,7 @@ mod tests {
"test_body",
&params,
&ta,
&rn,
center,
n,
&climate,
@@ -2274,6 +3013,7 @@ mod tests {
"test_body",
&params,
&ta,
&rn,
center,
n,
&climate,
@@ -2322,8 +3062,8 @@ mod tests {
// TerrainAnalysis::analyze from scratch on the SAME heightmap, exactly
// mirroring what a cold TerrainAnalysisCache miss does on the real
// DeriveWindow path (or a second body eviction re-pay).
let (_, ta_pass1) = crate::atlas::layer1::run_layer1(&hm);
let (_, ta_pass2) = crate::atlas::layer1::run_layer1(&hm);
let (l1_pass1, ta_pass1) = crate::atlas::layer1::run_layer1(&hm);
let (l1_pass2, ta_pass2) = crate::atlas::layer1::run_layer1(&hm);
// Confirm the two independent TerrainAnalysis derivations themselves
// agree field-by-field — a precise failure signal if drainage/analyze
@@ -2342,6 +3082,7 @@ mod tests {
"test_body",
&params,
&ta_pass1,
&l1_pass1.river_network,
center,
n,
&climate,
@@ -2353,6 +3094,7 @@ mod tests {
"test_body",
&params,
&ta_pass2,
&l1_pass2.river_network,
center,
n,
&climate,
@@ -2390,6 +3132,7 @@ mod tests {
moisture_q: vec![0; (n * n) as usize],
vegetation: vec![0; (n * n) as usize],
glaciation: vec![0; (n * n) as usize],
courses: Vec::new(),
};
assert!(cache.get(&key_a).is_none());
@@ -3595,6 +4338,7 @@ mod tests {
moisture_q: vec![90, 55, 0, 100],
vegetation: vec![6, 3, 0, 5], // includes Marine = 6
glaciation: vec![0, 0, 4, 1],
courses: Vec::new(),
};
let resp = AtlasLayerResponse {
body_id: "GJ1c".into(),