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:
@@ -22,9 +22,11 @@ use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::atlas::body_world_state::RiverNetwork;
|
||||
use crate::atlas::coast_invention;
|
||||
use crate::atlas::features::TerrainAnalysis;
|
||||
use crate::atlas::region_profile::{self, RegionProfile};
|
||||
use crate::atlas::river_course;
|
||||
use crate::atlas::scale::{self, BasinDirection, RegionPos};
|
||||
use crate::seed::SeedChain;
|
||||
use crate::simulation::generator::MorphologyZone;
|
||||
@@ -1230,7 +1232,17 @@ fn invent_primitives(
|
||||
/// the batch path so both derivation paths key the invention noise fields on
|
||||
/// the same world-metre convention. Radius-less bodies fall back to the
|
||||
/// 1-working-pixel = 1-district convention `derive_district` uses.
|
||||
fn pixel_to_world_m(px: f64, py: f64, w: usize, h: usize, radius_km: Option<f64>) -> (f64, f64) {
|
||||
///
|
||||
/// `pub(crate)` (T-1170): also used by the river course inventor
|
||||
/// (`river_course::cell_world_m`) to resolve a river cell's pixel position to
|
||||
/// its world-metre anchor — the SAME mapping, reused rather than duplicated.
|
||||
pub(crate) fn pixel_to_world_m(
|
||||
px: f64,
|
||||
py: f64,
|
||||
w: usize,
|
||||
h: usize,
|
||||
radius_km: Option<f64>,
|
||||
) -> (f64, f64) {
|
||||
match radius_km {
|
||||
Some(r) if r > 0.0 => {
|
||||
let wx = px / w.max(1) as f64 * (std::f64::consts::TAU * r * 1000.0);
|
||||
@@ -1245,6 +1257,42 @@ fn pixel_to_world_m(px: f64, py: f64, w: usize, h: usize, radius_km: Option<f64>
|
||||
}
|
||||
}
|
||||
|
||||
/// Absolute world metres → fractional working-grid pixel — the inverse of
|
||||
/// [`pixel_to_world_m`], and the SAME mapping [`derive_at_metres`]/
|
||||
/// [`derive_orbital_at_metres`] compute inline for their own `(px, py)`
|
||||
/// derivation (T-1170: extracted as a standalone `pub(crate)` helper rather
|
||||
/// than duplicated a third time, for the river course inventor's Stage A
|
||||
/// valley-seeking control path, which needs bilinear `elev_pct` reads at
|
||||
/// arbitrary world positions without paying for a full `DistrictProfile`
|
||||
/// derive per candidate — Ruling 3b, binding: "NOT `derive_at_metres` per
|
||||
/// candidate"). Returns `(px, py)` only — callers that also need latitude
|
||||
/// (temperature-sensitive derivation) still compute it themselves; the course
|
||||
/// inventor's Stage A elevation proxy has no use for latitude.
|
||||
pub(crate) fn world_m_to_pixel(
|
||||
wx: f64,
|
||||
wy: f64,
|
||||
w: usize,
|
||||
h: usize,
|
||||
radius_km: Option<f64>,
|
||||
) -> (f64, f64) {
|
||||
match radius_km {
|
||||
Some(r_km) if r_km > 0.0 => {
|
||||
let circumference_m = std::f64::consts::TAU * r_km * 1000.0;
|
||||
let meridian_m = std::f64::consts::PI * r_km * 1000.0;
|
||||
let px = (wx / circumference_m).rem_euclid(1.0) * w as f64;
|
||||
let lat_frac = (wy / meridian_m).clamp(-0.5, 0.5);
|
||||
let py = (0.5 + lat_frac) * h.saturating_sub(1) as f64;
|
||||
(px, py)
|
||||
}
|
||||
_ => {
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
let px = (wx / dm).clamp(0.0, w.saturating_sub(1) as f64);
|
||||
let py = (wy / dm).clamp(0.0, h.saturating_sub(1) as f64);
|
||||
(px, py)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public derivation function
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1272,6 +1320,14 @@ fn pixel_to_world_m(px: f64, py: f64, w: usize, h: usize, radius_km: Option<f64>
|
||||
/// - `region_cache` — pre-computed [`RegionProfile`] map keyed by [`RegionPos`];
|
||||
/// if a neighbour region is missing it is derived on the fly. Build with
|
||||
/// [`region_profile::derive_regions_for_body`] before calling this in a loop.
|
||||
/// - `river_network` — the body's [`RiverNetwork`] (T-1168, Ruling 4b/4c),
|
||||
/// consulted for the riparian point test via
|
||||
/// [`river_course::near_perennial_water_at`] (edges near this district
|
||||
/// invented on demand, the same pure function the window path uses).
|
||||
/// `None` when no river network is available (e.g. a body with no Layer-1
|
||||
/// drainage pass, or a caller that predates T-1168) — the riparian signal
|
||||
/// degrades to `false` in that case, matching the pre-T-1168 hardcoded
|
||||
/// default exactly, never a panic or an error.
|
||||
pub fn derive_district_profile(
|
||||
seed: SeedChain,
|
||||
body_params: &BodyParams,
|
||||
@@ -1282,6 +1338,7 @@ pub fn derive_district_profile(
|
||||
body_id: &str,
|
||||
region_cache: &BTreeMap<RegionPos, RegionProfile>,
|
||||
basin_direction: BasinDirection,
|
||||
river_network: Option<&RiverNetwork>,
|
||||
) -> DistrictProfile {
|
||||
let (rx, ry) = pos;
|
||||
let w = ta.w;
|
||||
@@ -1332,6 +1389,24 @@ pub fn derive_district_profile(
|
||||
0.0, // batch path — no octave cutoff, matches derive_district's default
|
||||
);
|
||||
|
||||
// T-1168 Ruling 4b: batch-path riparian signal — edges near this
|
||||
// district invented on demand via the SAME pure function the window
|
||||
// path uses. `river_network.is_none()` degrades to `false` (see this
|
||||
// function's doc), never a panic.
|
||||
let near_perennial_water = river_network
|
||||
.map(|rn| {
|
||||
river_course::near_perennial_water_at(
|
||||
seed,
|
||||
ta,
|
||||
body_params,
|
||||
rn,
|
||||
(world_x_m, world_y_m),
|
||||
scale::DISTRICT_M as f64,
|
||||
0.0, // batch path — no octave cutoff, matches this function's own default
|
||||
)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
build_district_profile(
|
||||
seed,
|
||||
body_params,
|
||||
@@ -1344,6 +1419,7 @@ pub fn derive_district_profile(
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
0.0, // batch path — no octave cutoff, matches derive_district's default
|
||||
near_perennial_water,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1381,6 +1457,18 @@ pub fn derive_district_profile(
|
||||
/// always active wherever `VegetationEnvelope::ceiling_q > 0`, mirroring the
|
||||
/// coast invention's own always-on posture (the ceiling being zero, not a
|
||||
/// separate flag, is what turns it off on airless/dry bodies).
|
||||
///
|
||||
/// ## Riparian signal (T-1168, Ruling 4a-4d)
|
||||
///
|
||||
/// `near_perennial_water` is the T-1168 riparian point test result for
|
||||
/// `(world_x_m, world_y_m)` — a separate boolean signal into
|
||||
/// [`derive_vegetation`], computed by the caller (window path: distance to
|
||||
/// the retained `Layer1Output`'s invented courses; batch path: distance to
|
||||
/// on-demand-invented courses for nearby edges — both via the SAME pure
|
||||
/// [`crate::atlas::river_course::near_perennial_water`] function). **This
|
||||
/// value NEVER touches `moisture_q`** (Ruling 4d, binding, re-affirmed): it
|
||||
/// is threaded straight through to `derive_vegetation` unchanged, after every
|
||||
/// moisture/temperature/morphology field above it has already been resolved.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_district_profile(
|
||||
seed: SeedChain,
|
||||
@@ -1394,6 +1482,7 @@ fn build_district_profile(
|
||||
world_x_m: f64,
|
||||
world_y_m: f64,
|
||||
min_wavelength_m: f64,
|
||||
near_perennial_water: bool,
|
||||
) -> DistrictProfile {
|
||||
let tectonic_class = derive_tectonic_class(body_params);
|
||||
|
||||
@@ -1458,15 +1547,21 @@ fn build_district_profile(
|
||||
moisture_q,
|
||||
);
|
||||
|
||||
// Vegetation class (T-1025, D-239 §8). No riparian signal at district scale yet
|
||||
// (requires perennial waterway map from L2+); default to false for now.
|
||||
// L2 ChunkContext will override per-tile once drainage data is threaded through.
|
||||
// open_water = the morphology verdict (T-1126) — never a threshold of its own.
|
||||
// Vegetation class (T-1025, D-239 §8). near_perennial_water (T-1168) is
|
||||
// the caller-computed riparian point test result — see this function's
|
||||
// doc for the full threading contract (Ruling 4a-4d). open_water = the
|
||||
// morphology verdict (T-1126) — never a threshold of its own.
|
||||
let open_water = matches!(
|
||||
morphology_zone,
|
||||
MorphologyZone::OpenOcean | MorphologyZone::Lake
|
||||
);
|
||||
let vegetation_class = derive_vegetation(temperature_c, moisture_q, elev_q, false, open_water);
|
||||
let vegetation_class = derive_vegetation(
|
||||
temperature_c,
|
||||
moisture_q,
|
||||
elev_q,
|
||||
near_perennial_water,
|
||||
open_water,
|
||||
);
|
||||
|
||||
DistrictProfile {
|
||||
morphology_zone,
|
||||
@@ -1529,6 +1624,7 @@ pub fn derive_district(
|
||||
dy as f64 * dm,
|
||||
climate,
|
||||
0.0,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1546,6 +1642,19 @@ pub fn derive_district(
|
||||
/// = no cutoff = [`derive_district`]'s existing behavior.
|
||||
///
|
||||
/// `body_id` is required for the D-243 §4 climate edge-fuzz warp domain separation.
|
||||
///
|
||||
/// `nearby_courses` (T-1168, Ruling 4b/4c): pre-invented river courses
|
||||
/// (already culled to the caller's neighbourhood — the window path's own
|
||||
/// bbox cull, `layer_proxy::build_courses_for_window`) consulted for the
|
||||
/// riparian point test via [`river_course::near_perennial_water`]. Passing
|
||||
/// `&[]` (the common case for a position far from any river, and every
|
||||
/// pre-T-1168 caller via [`derive_district`]) is exactly the old hardcoded
|
||||
/// `false` default — byte-identical output for every caller that doesn't
|
||||
/// thread real course geometry through. This is a PRE-INVENTED slice, not a
|
||||
/// `RiverNetwork` — this function is called once per window CELL (thousands
|
||||
/// of times per window), so re-inventing courses on every call here (rather
|
||||
/// than once per window) would be the exact per-candidate-derive cost this
|
||||
/// whole batch's Ruling 3b was written to avoid.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn derive_at_metres(
|
||||
seed: SeedChain,
|
||||
@@ -1556,6 +1665,7 @@ pub fn derive_at_metres(
|
||||
wy: f64,
|
||||
climate: &ClimateConstants,
|
||||
min_wavelength_m: f64,
|
||||
nearby_courses: &[river_course::InventedCourse],
|
||||
) -> DistrictProfile {
|
||||
// World metres -> fractional heightmap pixel + latitude. Mirrors
|
||||
// `derive_district`'s former inline mapping exactly, just keyed on
|
||||
@@ -1645,6 +1755,13 @@ pub fn derive_at_metres(
|
||||
// (derive_all_districts), which threads the true D8 direction from L1;
|
||||
// this on-demand path is the fallback for positions derived outside that
|
||||
// pass, where a meaningful basin_direction isn't available.
|
||||
//
|
||||
// T-1168 Ruling 4a: the riparian point test against the caller-supplied
|
||||
// (already-culled) course slice — the SAME pure predicate the batch path
|
||||
// uses via `near_perennial_water_at`.
|
||||
let near_perennial_water =
|
||||
river_course::near_perennial_water((world_x_m, world_y_m), nearby_courses);
|
||||
|
||||
build_district_profile(
|
||||
seed,
|
||||
¶ms,
|
||||
@@ -1657,6 +1774,7 @@ pub fn derive_at_metres(
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
min_wavelength_m,
|
||||
near_perennial_water,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1790,12 +1908,27 @@ pub fn derive_orbital_at_metres(
|
||||
// the vegetation verdict even though slope/elevation stay
|
||||
// envelope-only at this rung.
|
||||
0.0,
|
||||
// T-1168 Ruling 4e/5a: NO windowed course invention at Region
|
||||
// granularity (`layer_proxy::build_courses_for_window` early-returns
|
||||
// for `WindowGranularity::Region` — the whole-body skeleton path
|
||||
// draws Region-rung rivers instead, Ruling 5a). Always `false` here
|
||||
// — honest, not a gap: even if courses existed at Region, the 1-3 m
|
||||
// riparian band is many orders of magnitude below Region's ~205 km
|
||||
// spacing and could never fire (Ruling 4e).
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
/// Bilinear interpolation of a row-major `f32` field at fractional `(px, py)`.
|
||||
/// Columns wrap (equirectangular); rows clamp at the poles.
|
||||
fn bilinear(field: &[f32], w: usize, h: usize, px: f64, py: f64) -> f32 {
|
||||
///
|
||||
/// `pub(crate)` (T-1170): also the elevation-proxy read the river course
|
||||
/// inventor's Stage A valley-seeking control path uses
|
||||
/// (`river_course::score_candidate`) — the SAME bilinear-`elev_pct` tradeoff
|
||||
/// the coast warp already makes (`invent_primitives`'s step 3), reused rather
|
||||
/// than re-implemented so the two invention fields can never silently drift
|
||||
/// on interpolation semantics.
|
||||
pub(crate) fn bilinear(field: &[f32], w: usize, h: usize, px: f64, py: f64) -> f32 {
|
||||
if w == 0 || h == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
@@ -1869,6 +2002,11 @@ fn bilinear_bool(mask: &[bool], w: usize, h: usize, px: f64, py: f64) -> f32 {
|
||||
/// from the map; missing entries (edge districts with no land cells) default to
|
||||
/// `BasinDirection::North`. When `None` (tests / paths before Layer 1 runs),
|
||||
/// every district gets `BasinDirection::North`.
|
||||
///
|
||||
/// `river_network` (T-1168, Ruling 4c) is threaded straight through to every
|
||||
/// [`derive_district_profile`] call for the batch-path riparian signal — the
|
||||
/// `road_graph` precedent (`cascade.rs`'s already-unpacked
|
||||
/// `layer1.river_network`, same source, same threading pattern).
|
||||
pub fn derive_all_districts(
|
||||
seed: SeedChain,
|
||||
body_params: &BodyParams,
|
||||
@@ -1876,6 +2014,7 @@ pub fn derive_all_districts(
|
||||
grid_cells_per_district: usize,
|
||||
body_id: &str,
|
||||
basin_dirs: Option<&BTreeMap<DistrictPos, BasinDirection>>,
|
||||
river_network: Option<&RiverNetwork>,
|
||||
) -> BTreeMap<DistrictPos, DistrictProfile> {
|
||||
let climate = ClimateConstants::default();
|
||||
let gcpr = grid_cells_per_district.max(1);
|
||||
@@ -1939,6 +2078,7 @@ pub fn derive_all_districts(
|
||||
body_id,
|
||||
®ion_cache,
|
||||
basin_direction,
|
||||
river_network,
|
||||
);
|
||||
out.insert(pos, profile);
|
||||
}
|
||||
@@ -1990,7 +2130,7 @@ mod tests {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let params = BodyParams::default();
|
||||
let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", None);
|
||||
let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", None, None);
|
||||
|
||||
// Expected: ceil(64/8) × ceil(32/8) = 8 × 4 = 32 districts.
|
||||
assert_eq!(districts.len(), 32, "district count mismatch");
|
||||
@@ -2008,7 +2148,7 @@ mod tests {
|
||||
let params = BodyParams::default();
|
||||
|
||||
// Real DistrictPos keys from a baseline (None) run.
|
||||
let baseline = derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", None);
|
||||
let baseline = derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", None, None);
|
||||
let mut keys = baseline.keys().copied();
|
||||
let pos_east = keys.next().expect("at least one district");
|
||||
let pos_south = keys.next().expect("at least two districts");
|
||||
@@ -2019,7 +2159,7 @@ mod tests {
|
||||
basin_dirs.insert(pos_south, BasinDirection::South);
|
||||
|
||||
let districts =
|
||||
derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", Some(&basin_dirs));
|
||||
derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", Some(&basin_dirs), None);
|
||||
|
||||
assert_eq!(districts[&pos_east].basin_direction, BasinDirection::East);
|
||||
assert_eq!(districts[&pos_south].basin_direction, BasinDirection::South);
|
||||
@@ -2182,6 +2322,7 @@ mod tests {
|
||||
dp.1 as f64 * dm,
|
||||
&climate,
|
||||
0.0,
|
||||
&[],
|
||||
);
|
||||
assert_district_profiles_eq(&via_wrapper, &via_metres);
|
||||
}
|
||||
@@ -2212,6 +2353,7 @@ mod tests {
|
||||
dp.1 as f64 * dm,
|
||||
&climate,
|
||||
0.0,
|
||||
&[],
|
||||
);
|
||||
assert_district_profiles_eq(&via_wrapper, &via_metres);
|
||||
}
|
||||
@@ -2258,6 +2400,7 @@ mod tests {
|
||||
-567.0 * dm + 512.0,
|
||||
&climate,
|
||||
512.0,
|
||||
&[],
|
||||
);
|
||||
assert!((0..=100).contains(&prof.elev_q));
|
||||
assert!((0..=100).contains(&prof.slope_q));
|
||||
@@ -2280,7 +2423,7 @@ mod tests {
|
||||
for i in 0..20 {
|
||||
let wx = (100 + i * 37) as f64 * dm;
|
||||
let wy = (100 + i * 53) as f64 * dm;
|
||||
let uncut = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 0.0);
|
||||
let uncut = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 0.0, &[]);
|
||||
let cut = derive_at_metres(
|
||||
test_seed(),
|
||||
"test_body",
|
||||
@@ -2290,6 +2433,7 @@ mod tests {
|
||||
wy,
|
||||
&climate,
|
||||
8_193.0, // above the two finest OCTAVE_WAVELENGTHS_M entries
|
||||
&[],
|
||||
);
|
||||
if uncut.elev_q != cut.elev_q || uncut.slope_q != cut.slope_q {
|
||||
any_differs = true;
|
||||
@@ -2322,8 +2466,8 @@ mod tests {
|
||||
for i in 0..12 {
|
||||
let wx = (300 + i * 41) as f64 * dm * 0.1;
|
||||
let wy = (300 + i * 29) as f64 * dm * 0.1;
|
||||
let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
|
||||
let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
|
||||
let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0, &[]);
|
||||
let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0, &[]);
|
||||
assert_district_profiles_eq(&a, &b);
|
||||
}
|
||||
}
|
||||
@@ -2355,8 +2499,8 @@ mod tests {
|
||||
for i in 0..20 {
|
||||
let wx = (150 + i * 47) as f64 * dm;
|
||||
let wy = (150 + i * 31) as f64 * dm;
|
||||
let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0);
|
||||
let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0);
|
||||
let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0, &[]);
|
||||
let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0, &[]);
|
||||
assert_district_profiles_eq(&a, &b);
|
||||
}
|
||||
}
|
||||
@@ -2410,9 +2554,9 @@ mod tests {
|
||||
let wx = (150 + i * 47) as f64 * dm;
|
||||
let wy = (150 + i * 31) as f64 * dm;
|
||||
let district =
|
||||
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0);
|
||||
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0, &[]);
|
||||
let quarter =
|
||||
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
|
||||
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0, &[]);
|
||||
if district.elev_q != quarter.elev_q
|
||||
|| district.slope_q != quarter.slope_q
|
||||
|| district.moisture_q != quarter.moisture_q
|
||||
@@ -2454,6 +2598,7 @@ mod tests {
|
||||
wy,
|
||||
&climate,
|
||||
300_000.0,
|
||||
&[],
|
||||
);
|
||||
// An even more extreme cutoff must produce the SAME result — once
|
||||
// every octave is truncated, going coarser still changes nothing.
|
||||
@@ -2466,6 +2611,7 @@ mod tests {
|
||||
wy,
|
||||
&climate,
|
||||
10_000_000.0,
|
||||
&[],
|
||||
);
|
||||
assert_district_profiles_eq(&far_above, &even_further);
|
||||
}
|
||||
@@ -2507,6 +2653,7 @@ mod tests {
|
||||
base_wy,
|
||||
&climate,
|
||||
2_048.0,
|
||||
&[],
|
||||
)
|
||||
.vegetation_class;
|
||||
|
||||
@@ -2523,7 +2670,7 @@ mod tests {
|
||||
let wx = base_wx + dx as f64 * qm;
|
||||
let wy = base_wy + dy as f64 * qm;
|
||||
let prof =
|
||||
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
|
||||
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0, &[]);
|
||||
if prof.vegetation_class != VegetationClass::Marine {
|
||||
*tally.entry(prof.vegetation_class as u8).or_insert(0) += 1;
|
||||
}
|
||||
@@ -2636,7 +2783,7 @@ mod tests {
|
||||
let wx = 40.0 * dm;
|
||||
let wy = 20.0 * dm;
|
||||
let orbital = derive_orbital_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate);
|
||||
let full = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 0.0);
|
||||
let full = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 0.0, &[]);
|
||||
|
||||
// The invented scatter is a bounded perturbation on top of the raw
|
||||
// envelope (detail_scatter's amplitude is capped well under 100 elev_q
|
||||
@@ -2717,6 +2864,7 @@ mod tests {
|
||||
"test_body",
|
||||
&BTreeMap::new(),
|
||||
BasinDirection::North,
|
||||
None,
|
||||
);
|
||||
let p2 = derive_district_profile(
|
||||
test_seed(),
|
||||
@@ -2728,6 +2876,7 @@ mod tests {
|
||||
"test_body",
|
||||
&BTreeMap::new(),
|
||||
BasinDirection::North,
|
||||
None,
|
||||
);
|
||||
// Equality via serialized fields (no PartialEq on MorphologyZone — compare by name).
|
||||
assert_eq!(
|
||||
@@ -2822,7 +2971,7 @@ mod tests {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let params = BodyParams::default();
|
||||
let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", None);
|
||||
let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", None, None);
|
||||
// BTreeMap iterates in sorted key order — verify the first key is (0,0).
|
||||
let first = districts.keys().next().expect("at least one district");
|
||||
assert_eq!(*first, (0, 0), "first district must be at origin");
|
||||
|
||||
Reference in New Issue
Block a user