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:
@@ -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",
|
||||
¶ms,
|
||||
&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, ¶ms, 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,
|
||||
¶ms,
|
||||
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,
|
||||
¶ms,
|
||||
&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, ¶ms, 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, ¶ms, 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, ¶ms, &ta, rn, window_a, WindowGranularity::District, 0.0);
|
||||
let invented_b =
|
||||
invent_courses_near_window(seed, ¶ms, &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,
|
||||
¶ms,
|
||||
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,
|
||||
¶ms,
|
||||
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",
|
||||
¶ms,
|
||||
&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",
|
||||
¶ms,
|
||||
&ta,
|
||||
&rn,
|
||||
(3, -2),
|
||||
n,
|
||||
&climate,
|
||||
@@ -2230,6 +2966,7 @@ mod tests {
|
||||
"test_body",
|
||||
¶ms,
|
||||
&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",
|
||||
¶ms,
|
||||
&ta,
|
||||
&rn,
|
||||
center,
|
||||
n,
|
||||
&climate,
|
||||
@@ -2274,6 +3013,7 @@ mod tests {
|
||||
"test_body",
|
||||
¶ms,
|
||||
&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",
|
||||
¶ms,
|
||||
&ta_pass1,
|
||||
&l1_pass1.river_network,
|
||||
center,
|
||||
n,
|
||||
&climate,
|
||||
@@ -2353,6 +3094,7 @@ mod tests {
|
||||
"test_body",
|
||||
¶ms,
|
||||
&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(),
|
||||
|
||||
Reference in New Issue
Block a user