feat(simulation): T-1162 — invention octaves into the Atlas window path (coast crinkle, quarter relief, vegetation patchiness)
Coast: WARP_OCTAVE_WAVELENGTHS_M 5->9 entries (262144m down to 1024m, Quarter's Nyquist floor) with new min_wavelength_m cutoff plumbing in coast_warp_px/warp_fbm (the warp previously had no cutoff at all). Relief: invent_primitives feeds the VOXEL band (1024-128m, salted) into elev_q/slope_q under the same envelope/cutoff discipline. Vegetation: new vegetation_invention module — two-tier (BodyParams+latitude envelope ceiling; single blended fBm field, never-gated 100-400km massif tier at 70% + cutoff-gated district/voxel texture at 30%) perturbing moisture_q OUTPUT so precipitation/glaciation/vegetation shift as one world-fact; wire format unchanged. MIN_WL_BANDS_M 5->6 (adds 1024m band). Mid-implementation correction caught by tests: District's quantized floor is 4096m (OCTAVE_WAVELENGTHS_M[3]), not 2x DISTRICT_M — the 8192m and 4096m coast octaves are legitimately District-admitted enrichment; only 2048/1024m coast + the voxel band are Quarter-exclusive. Docs and golden cutoffs corrected to match. New golden: tests/window_derivation_golden.rs + fixture (derivation_ harness pattern, UPDATE_GOLDEN=1 regen) with structural guards pinning Quarter/District divergence. Bench (zoom_ladder_bench, release): no regression — all rungs at or below baseline (District c0 3.54->~3.0 us/cell, Quarter 2.10->1.77, orbital 1.48->1.42, n=6400 window 0.86->0.59 ms). Revert-verified voxel_relief cutoff exclusion. Server lib 1840 passed; new module 11 tests; district_profile 84 (+6, 1 noise-fragile pre-existing test properly fixed via 8-district latitude-band averaging). Tickets: T-1162 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -49,11 +49,34 @@ const COAST_WARP_Y_SALT: u64 = 0xD1F7_0CEA_2B0A_D515;
|
||||
/// Salt for the tier-2 heterogeneity field (same-coast stretches differ).
|
||||
const CHARACTER_FIELD_SALT: u64 = 0x0C0A_57C4_A24C_7E12;
|
||||
|
||||
/// Coast-warp octave wavelengths in metres (≈16–262 km): capes and gulfs at the
|
||||
/// top, coves and inlets at the bottom. All above the 4–33 km detail-scatter
|
||||
/// band, and all below ~2 heightmap pixels — the warp perturbs the coast, it
|
||||
/// does not rewrite continents (the heightmap stays the truth at its own scale).
|
||||
const WARP_OCTAVE_WAVELENGTHS_M: [f64; 5] = [262_144.0, 131_072.0, 65_536.0, 32_768.0, 16_384.0];
|
||||
/// Coast-warp octave wavelengths in metres (≈1,024 m–262 km): capes and gulfs
|
||||
/// at the top, down through bays/headlands/islets at the bottom (T-1162 — the
|
||||
/// headline "detailed coastline" visual ask). The warp perturbs the coast, it
|
||||
/// does not rewrite continents (the heightmap stays the truth at its own
|
||||
/// scale) — displacing the sampling *position*, distinct from
|
||||
/// `detail_scatter::terrain_detail`'s independent perturbation of the sampled
|
||||
/// *elevation*.
|
||||
///
|
||||
/// **T-1162 extension:** four octaves finer than the pre-T-1162 floor
|
||||
/// (16,384 m) were appended — 8,192 / 4,096 / 2,048 / 1,024 — reaching the
|
||||
/// Quarter rung's own Nyquist floor (512 m spacing → content wavelength ≥ 2×
|
||||
/// spacing = 1,024 m; finer than that is unsampleable noise at Quarter
|
||||
/// density, not detail). A Quarter-rung request (`min_wavelength_m` cutoff
|
||||
/// quantized to 1,024 m, see `layer_proxy::MIN_WL_BANDS_M`) admits every
|
||||
/// octave down to this floor; a District-rung request (cutoff quantized to
|
||||
/// District's OWN real band, 4,096 m — District's Nyquist limit, matching
|
||||
/// `terrain_detail`'s pre-existing finest octave) truncates the two finest
|
||||
/// additions (2,048/1,024 m — genuinely below District's floor) while
|
||||
/// legitimately ADMITTING the other two (8,192/4,096 m — new content that IS
|
||||
/// coarse enough for District to resolve, not a leak). Either way Quarter
|
||||
/// still sees strictly more than District at the SAME position (the two
|
||||
/// finest additions are Quarter-exclusive). The
|
||||
/// `enveloped_fbm`-style hard-truncate cutoff discipline is honored via
|
||||
/// [`warp_fbm`]'s own `min_wavelength_m` parameter — `0.0` (no cutoff) is
|
||||
/// byte-identical to every pre-T-1162 caller.
|
||||
const WARP_OCTAVE_WAVELENGTHS_M: [f64; 9] = [
|
||||
262_144.0, 131_072.0, 65_536.0, 32_768.0, 16_384.0, 8_192.0, 4_096.0, 2_048.0, 1_024.0,
|
||||
];
|
||||
|
||||
/// Heterogeneity-field octave wavelengths in metres (≈100–400 km): the scale on
|
||||
/// which one planet's coastline personality drifts from stretch to stretch.
|
||||
@@ -224,21 +247,44 @@ pub fn character_field(seed: u64, wx: f64, wy: f64) -> f64 {
|
||||
/// Apply the same offset to *every* envelope field sampled at the position
|
||||
/// (elevation, slope, ocean mask) so the invented terrain moves coherently —
|
||||
/// a warped-in bay carries its sea-level elevation with it.
|
||||
pub fn coast_warp_px(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter) -> (f64, f64) {
|
||||
///
|
||||
/// `min_wavelength_m` (T-1162, mirrors `detail_scatter::terrain_detail`'s
|
||||
/// contract exactly): octaves in [`WARP_OCTAVE_WAVELENGTHS_M`] finer than this
|
||||
/// cutoff are hard-truncated, never faded. `0.0` = no cutoff = every octave —
|
||||
/// byte-identical to every pre-T-1162 caller (all of which passed no cutoff at
|
||||
/// all, since the parameter didn't exist before this ticket).
|
||||
pub fn coast_warp_px(
|
||||
seed: u64,
|
||||
wx: f64,
|
||||
wy: f64,
|
||||
ch: &CoastCharacter,
|
||||
min_wavelength_m: f64,
|
||||
) -> (f64, f64) {
|
||||
let sx = splitmix64(seed ^ COAST_WARP_SALT);
|
||||
let sy = splitmix64(sx ^ COAST_WARP_Y_SALT);
|
||||
(
|
||||
warp_fbm(sx, wx, wy, ch) * ch.warp_amplitude_px,
|
||||
warp_fbm(sy, wx, wy, ch) * ch.warp_amplitude_px,
|
||||
warp_fbm(sx, wx, wy, ch, min_wavelength_m) * ch.warp_amplitude_px,
|
||||
warp_fbm(sy, wx, wy, ch, min_wavelength_m) * ch.warp_amplitude_px,
|
||||
)
|
||||
}
|
||||
|
||||
/// Roughness/ridge-shaped fBm in ≈`[-1, 1]` over the coast-warp octave band.
|
||||
fn warp_fbm(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter) -> f64 {
|
||||
///
|
||||
/// `min_wavelength_m` (T-1162): same hard-truncate discipline as
|
||||
/// `detail_scatter::enveloped_fbm` — octaves with `wl < min_wavelength_m` are
|
||||
/// skipped entirely, but `amp` still advances through the skipped step so
|
||||
/// surviving octaves keep their intended relative weight (no renormalization
|
||||
/// against a smaller octave count beyond the shared `norm` denominator, which
|
||||
/// already only sums the terms that ran).
|
||||
fn warp_fbm(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter, min_wavelength_m: f64) -> f64 {
|
||||
let mut sum = 0.0;
|
||||
let mut amp = 1.0;
|
||||
let mut norm = 0.0;
|
||||
for (i, &wl) in WARP_OCTAVE_WAVELENGTHS_M.iter().enumerate() {
|
||||
if wl < min_wavelength_m {
|
||||
amp *= 0.45 + 0.35 * ch.roughness;
|
||||
continue;
|
||||
}
|
||||
let mut n = value_noise(
|
||||
seed.wrapping_add((i as u64).wrapping_mul(0x1000)),
|
||||
wx,
|
||||
@@ -257,6 +303,9 @@ fn warp_fbm(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter) -> f64 {
|
||||
// entirely and still read as stripes at w64.
|
||||
amp *= 0.45 + 0.35 * ch.roughness;
|
||||
}
|
||||
if norm == 0.0 {
|
||||
return 0.0; // every octave cut by the cutoff → no warp displacement left
|
||||
}
|
||||
sum / norm
|
||||
}
|
||||
|
||||
@@ -357,21 +406,99 @@ mod tests {
|
||||
TectonicClass::Stable,
|
||||
);
|
||||
let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55);
|
||||
let a = coast_warp_px(42, 3e6, 1e6, &ch);
|
||||
let b = coast_warp_px(42, 3e6, 1e6, &ch);
|
||||
let a = coast_warp_px(42, 3e6, 1e6, &ch, 0.0);
|
||||
let b = coast_warp_px(42, 3e6, 1e6, &ch, 0.0);
|
||||
assert_eq!(a, b, "warp must be deterministic");
|
||||
for i in 0..400 {
|
||||
let (dx, dy) = coast_warp_px(42, i as f64 * 9_137.0, i as f64 * -7_211.0, &ch);
|
||||
let (dx, dy) = coast_warp_px(42, i as f64 * 9_137.0, i as f64 * -7_211.0, &ch, 0.0);
|
||||
assert!(dx.abs() <= WARP_AMPLITUDE_CAP_PX + 1e-9);
|
||||
assert!(dy.abs() <= WARP_AMPLITUDE_CAP_PX + 1e-9);
|
||||
}
|
||||
// C¹ continuity: a 10 m step is a tiny displacement change — the coast
|
||||
// character never steps on a line (D-243 edge-fuzz discipline).
|
||||
let (x0, y0) = coast_warp_px(42, 5e6, 5e6, &ch);
|
||||
let (x1, y1) = coast_warp_px(42, 5e6 + 10.0, 5e6, &ch);
|
||||
let (x0, y0) = coast_warp_px(42, 5e6, 5e6, &ch, 0.0);
|
||||
let (x1, y1) = coast_warp_px(42, 5e6 + 10.0, 5e6, &ch, 0.0);
|
||||
assert!((x0 - x1).abs() < 0.01 && (y0 - y1).abs() < 0.01);
|
||||
}
|
||||
|
||||
// ── min_wavelength_m cutoff (T-1162) ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cutoff_zero_matches_pre_t1162_behavior() {
|
||||
// 0.0 = no cutoff = every octave — the compatibility contract every
|
||||
// pre-T-1162 caller relies on (all of which never had a cutoff param
|
||||
// to begin with).
|
||||
let env = body_coast_envelope(
|
||||
¶ms("ocean", "breathable", "temperate"),
|
||||
TectonicClass::Stable,
|
||||
);
|
||||
let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55);
|
||||
for i in 0..100 {
|
||||
let wx = i as f64 * 9_137.0;
|
||||
let wy = i as f64 * -7_211.0;
|
||||
let with_zero = coast_warp_px(42, wx, wy, &ch, 0.0);
|
||||
// The finest WARP_OCTAVE_WAVELENGTHS_M entry is 1,024.0 — a cutoff
|
||||
// below that admits every octave too, and must agree exactly.
|
||||
let with_below_finest = coast_warp_px(42, wx, wy, &ch, 1.0);
|
||||
assert_eq!(with_zero, with_below_finest);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cutoff_truncates_octaves_below_it() {
|
||||
// A cutoff above the coarsest WARP_OCTAVE_WAVELENGTHS_M entry
|
||||
// (262,144.0) must skip every octave and fall back to (0.0, 0.0) (the
|
||||
// norm==0.0 empty-sum guard).
|
||||
let env = body_coast_envelope(
|
||||
¶ms("ocean", "breathable", "temperate"),
|
||||
TectonicClass::Stable,
|
||||
);
|
||||
let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55);
|
||||
let (dx, dy) = coast_warp_px(42, 1_000.0, 2_000.0, &ch, 1_000_000.0);
|
||||
assert_eq!((dx, dy), (0.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cutoff_changes_output_relative_to_uncut() {
|
||||
// A mid-band cutoff (drops the four finest octaves: 8192/4096/2048/1024)
|
||||
// must produce DIFFERENT output than the uncut warp at the same
|
||||
// position — otherwise the T-1162 extension would be inert.
|
||||
let env = body_coast_envelope(
|
||||
¶ms("ocean", "breathable", "temperate"),
|
||||
TectonicClass::Stable,
|
||||
);
|
||||
let ch = coast_character_at(&env, 17, 1.2e6, 9e5, 25.0, GlaciationGrade::None, 60);
|
||||
let uncut = coast_warp_px(17, 1.2e6, 9e5, &ch, 0.0);
|
||||
let cut = coast_warp_px(17, 1.2e6, 9e5, &ch, 16_385.0);
|
||||
assert_ne!(
|
||||
uncut, cut,
|
||||
"a mid-band cutoff must change the derived warp output"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn district_cutoff_excludes_quarter_only_octaves() {
|
||||
// T-1162 discipline: District's REAL quantized `MIN_WL_BANDS_M`
|
||||
// floor is 4,096 m (District's own Nyquist limit — matches
|
||||
// `terrain_detail`'s pre-existing finest octave; see
|
||||
// `layer_proxy::MIN_WL_BANDS_M`'s doc). At that cutoff, the coast
|
||||
// warp's two finest additions (2,048/1,024 m) are excluded — only
|
||||
// Quarter's cutoff (1,024 m) admits them. This is the positive proof
|
||||
// that extending the octave floor gives Quarter genuinely more than
|
||||
// District at the SAME world position.
|
||||
let env = body_coast_envelope(
|
||||
¶ms("ocean", "breathable", "temperate"),
|
||||
TectonicClass::Stable,
|
||||
);
|
||||
let ch = coast_character_at(&env, 5, 2.5e6, 1.5e6, 15.0, GlaciationGrade::None, 45);
|
||||
let district = coast_warp_px(5, 2.5e6, 1.5e6, &ch, 4_096.0);
|
||||
let quarter = coast_warp_px(5, 2.5e6, 1.5e6, &ch, 1_024.0);
|
||||
assert_ne!(
|
||||
district, quarter,
|
||||
"Quarter's finer cutoff must admit octaves District's cutoff excludes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warp_stream_uncorrelated_with_scatter_stream() {
|
||||
// Distinct hash path: the warp at a position must not track the terrain
|
||||
@@ -381,7 +508,7 @@ mod tests {
|
||||
TectonicClass::Stable,
|
||||
);
|
||||
let ch = coast_character_at(&env, 42, 1e6, 1e6, 20.0, GlaciationGrade::None, 60);
|
||||
let (wdx, _) = coast_warp_px(42, 1e6, 1e6, &ch);
|
||||
let (wdx, _) = coast_warp_px(42, 1e6, 1e6, &ch, 0.0);
|
||||
let scatter = crate::atlas::detail_scatter::terrain_detail(42, 1e6, 1e6, 1.0, 0.5, 0.0);
|
||||
assert_ne!(wdx, scatter);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,12 @@ pub(crate) const OCTAVE_WAVELENGTHS_M: [f64; 4] = [32_768.0, 16_384.0, 8_192.0,
|
||||
/// (<64 m) is too fine to reach. This is the [`voxel_relief`] band: the rolling/ridged
|
||||
/// hills a *walking character* navigates by (T-1081). The coarsest octave stays below
|
||||
/// the district size so the relief never competes with `elev_q`'s district-scale role.
|
||||
const VOXEL_OCTAVE_WAVELENGTHS_M: [f64; 4] = [1_024.0, 512.0, 256.0, 128.0];
|
||||
///
|
||||
/// `pub(crate)` (T-1162): also the fine-tier band
|
||||
/// [`crate::atlas::vegetation_invention`]'s texture field truncates against,
|
||||
/// alongside [`OCTAVE_WAVELENGTHS_M`] — the same reuse-not-reinvent posture
|
||||
/// that array already documents for `layer_proxy`.
|
||||
pub(crate) const VOXEL_OCTAVE_WAVELENGTHS_M: [f64; 4] = [1_024.0, 512.0, 256.0, 128.0];
|
||||
|
||||
/// Sub-chunk mosaic octave wavelengths in metres — the ≈8–64 m band, finer than the
|
||||
/// [`VOXEL_OCTAVE_WAVELENGTHS_M`] sub-district band, so a micro-habitat patch reads as
|
||||
|
||||
@@ -1136,7 +1136,11 @@ fn invent_primitives(
|
||||
);
|
||||
|
||||
// ── 3. Invented coastline: warp the whole envelope sampling. ────────────
|
||||
let (wdx, wdy) = coast_invention::coast_warp_px(seed.seed(), world_x_m, world_y_m, &ch);
|
||||
// T-1162: the coast warp gets the SAME min_wavelength_m cutoff as the
|
||||
// terrain-detail scatter below — one rung, one cutoff, applied to both
|
||||
// continuous fields that compose the invented coastline.
|
||||
let (wdx, wdy) =
|
||||
coast_invention::coast_warp_px(seed.seed(), world_x_m, world_y_m, &ch, min_wavelength_m);
|
||||
let (spx, spy) = (px + wdx, py + wdy);
|
||||
let elev_pct = bilinear(&ta.elev_pct, ta.w, ta.h, spx, spy) as f64;
|
||||
let slope_deg = bilinear(&ta.slope_deg, ta.w, ta.h, spx, spy) as f64;
|
||||
@@ -1155,6 +1159,35 @@ fn invent_primitives(
|
||||
min_wavelength_m,
|
||||
);
|
||||
|
||||
// T-1162 part (b): sub-district relief band (VOXEL_OCTAVE_WAVELENGTHS_M,
|
||||
// 1,024–128 m — "the rolling hills a walking character navigates by",
|
||||
// T-1081) fed into window classification for the first time. Same
|
||||
// envelope/ruggedness inputs as the district-band `scatter` above (one
|
||||
// amplitude ceiling, two wavelength bands composing additively — never a
|
||||
// second independently-tuned amplitude rule) and the SAME
|
||||
// `min_wavelength_m` cutoff. At District's cutoff (≥2,048 m) every
|
||||
// VOXEL_OCTAVE_WAVELENGTHS_M entry (all ≤1,024 m) is truncated, so
|
||||
// `relief` is always exactly 0.0 there — District's output is unchanged
|
||||
// byte-for-byte. At Quarter's cutoff (1,024 m) only the two coarsest
|
||||
// voxel-band entries (1,024, 512) survive; the two finest (256, 128) stay
|
||||
// truncated even at Quarter (below Quarter's own 512 m spacing's Nyquist
|
||||
// floor of 1,024 m) — this is correct and expected (D-226(d) note): a
|
||||
// contributing wavelength is never capped BY the request-granularity
|
||||
// floor ruling itself (the coast warp already crosses scales the other
|
||||
// way), it is simply that only 2 of the 4 voxel octaves are coarse enough
|
||||
// to matter at Quarter's own sample density; the other two are reserved
|
||||
// for a future finer-than-Quarter rung. Distinct seed salt (`^ 0x5EED_C0DE`)
|
||||
// keeps this stream uncorrelated with `scatter`'s stream at the same
|
||||
// position (same isolation discipline as `coast_invention`'s warp salt).
|
||||
let relief = crate::atlas::detail_scatter::voxel_relief(
|
||||
seed.seed() ^ 0x5EED_C0DE,
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
env_amp,
|
||||
ruggedness,
|
||||
min_wavelength_m,
|
||||
);
|
||||
|
||||
// Shoreline carving (T-1125): glacial / tectonically-young SHORES are cut
|
||||
// steep — fjord walls and cliff coasts. The gentle scatter floor alone can
|
||||
// never voice the D-239 steep coastal families (Fjord gates at slope_q ≥ 40,
|
||||
@@ -1166,9 +1199,14 @@ fn invent_primitives(
|
||||
let shoreline = (ocean_frac * (1.0 - ocean_frac) * 4.0).clamp(0.0, 1.0);
|
||||
let carve = 0.55 * ch.ridge * shoreline * (scatter.abs() / env_amp.max(0.05)).clamp(0.0, 1.0);
|
||||
|
||||
// Both bands add into the same elevation/slope quantization — `relief` is
|
||||
// 0.0 at every cutoff ≥ 2,048 m (District and coarser), so this sum is
|
||||
// byte-identical to the pre-T-1162 `elev_pct + scatter` wherever the
|
||||
// cutoff discipline says it must be.
|
||||
InventedPrimitives {
|
||||
elev_q: (((elev_pct + scatter) * 100.0).round() as i32).clamp(0, 100),
|
||||
slope_q: (((local_slope + ruggedness * scatter.abs() + carve) * 100.0).round() as i32)
|
||||
elev_q: (((elev_pct + scatter + relief) * 100.0).round() as i32).clamp(0, 100),
|
||||
slope_q: (((local_slope + ruggedness * (scatter.abs() + relief.abs()) + carve) * 100.0)
|
||||
.round() as i32)
|
||||
.clamp(0, 100),
|
||||
ocean_fraction_q: ((ocean_frac * 100.0).round() as i32).clamp(0, 100),
|
||||
}
|
||||
@@ -1290,6 +1328,9 @@ pub fn derive_district_profile(
|
||||
prims.ocean_fraction_q,
|
||||
region_baseline_c,
|
||||
basin_direction,
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
0.0, // batch path — no octave cutoff, matches derive_district's default
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1314,6 +1355,20 @@ pub fn derive_district_profile(
|
||||
/// The distinction is important for edge fuzz: only the two-phase path produces
|
||||
/// a continuous, warp-perturbed temperature gradient. The single-phase path
|
||||
/// still satisfies D-240 but without edge fuzz.
|
||||
///
|
||||
/// ## Vegetation patchiness (T-1162)
|
||||
///
|
||||
/// `world_x_m`/`world_y_m`/`min_wavelength_m` feed
|
||||
/// [`crate::atlas::vegetation_invention::moisture_perturb_q`] — the nature-layer
|
||||
/// patchiness field that perturbs `moisture_q` (see that module's docs for the
|
||||
/// full design rationale) before precipitation/glaciation/vegetation are
|
||||
/// derived from it, so the three stay in lockstep. `world_x_m == 0.0 &&
|
||||
/// world_y_m == 0.0 && min_wavelength_m == 0.0` is NOT a special "disabled"
|
||||
/// case — the origin is a legal world position — vegetation patchiness is
|
||||
/// 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).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn build_district_profile(
|
||||
seed: SeedChain,
|
||||
body_params: &BodyParams,
|
||||
@@ -1323,6 +1378,9 @@ fn build_district_profile(
|
||||
ocean_fraction_q: i32,
|
||||
region_baseline_c: Option<f32>,
|
||||
basin_direction: BasinDirection,
|
||||
world_x_m: f64,
|
||||
world_y_m: f64,
|
||||
min_wavelength_m: f64,
|
||||
) -> DistrictProfile {
|
||||
let tectonic_class = derive_tectonic_class(body_params);
|
||||
|
||||
@@ -1350,7 +1408,24 @@ fn build_district_profile(
|
||||
derive_temperature_c(&district_climate_params, climate, body_seed)
|
||||
}
|
||||
};
|
||||
let moisture_q = derive_moisture_q(body_params, elev_q, ocean_fraction_q, climate);
|
||||
let base_moisture_q = derive_moisture_q(body_params, elev_q, ocean_fraction_q, climate);
|
||||
|
||||
// T-1162: vegetation-patchiness field perturbs the moisture INPUT (see
|
||||
// `vegetation_invention` module docs for the full design rationale) —
|
||||
// this is what turns a uniform per-district class tint into massifs at
|
||||
// Region scale resolving to distinct woods/copses/clearings at
|
||||
// District/Quarter. Applied uniformly at every derivation path
|
||||
// (on-demand, batch, orbital) since all three route through this shared
|
||||
// classification tail.
|
||||
let veg_envelope = crate::atlas::vegetation_invention::vegetation_envelope(body_params);
|
||||
let moisture_perturb = crate::atlas::vegetation_invention::moisture_perturb_q(
|
||||
&veg_envelope,
|
||||
seed.seed(),
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
min_wavelength_m,
|
||||
);
|
||||
let moisture_q = (base_moisture_q + moisture_perturb).clamp(0, 100);
|
||||
|
||||
// Climate-derived fields: computed from temperature + moisture primitives
|
||||
// (D-239 §2). This is the correct call order — temperature must be resolved
|
||||
@@ -1566,6 +1641,9 @@ pub fn derive_at_metres(
|
||||
prims.ocean_fraction_q,
|
||||
region_baseline_c,
|
||||
BasinDirection::default(),
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
min_wavelength_m,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1626,7 +1704,7 @@ pub fn derive_orbital_at_metres(
|
||||
// Same world-metres -> fractional heightmap pixel + latitude mapping
|
||||
// derive_at_metres uses — the envelope is the SAME TerrainAnalysis grid at
|
||||
// every rung, only the sampling density differs.
|
||||
let (px, py, _world_x_m, _world_y_m, lat_deg) = match body_params.body_radius_km {
|
||||
let (px, py, world_x_m, world_y_m, lat_deg) = match body_params.body_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;
|
||||
@@ -1688,6 +1766,17 @@ pub fn derive_orbital_at_metres(
|
||||
ocean_fraction_q,
|
||||
region_baseline_c,
|
||||
BasinDirection::default(),
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
// T-1162: vegetation patchiness's massif tier is NEVER cutoff-gated
|
||||
// (see vegetation_invention module docs) and is cheap (two small fBm
|
||||
// sums, not the invent_primitives bilinear+warp+scatter pipeline this
|
||||
// function deliberately skips) — passing 0.0 here means the orbital
|
||||
// path samples the SAME uncut massif+texture field derive_at_metres
|
||||
// would at min_wavelength_m=0.0, preserving cross-rung coherence for
|
||||
// the vegetation verdict even though slope/elevation stay
|
||||
// envelope-only at this rung.
|
||||
0.0,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1958,6 +2047,18 @@ mod tests {
|
||||
fn derive_district_radius_maps_to_latitude_climate() {
|
||||
// With a body radius, equatorial vs near-polar districts get different
|
||||
// temperature (the seam maps district_y → latitude). Pole = colder.
|
||||
//
|
||||
// T-1162: `derive_district` calls with `min_wavelength_m = 0.0` (no
|
||||
// cutoff), so it now admits the extended coast-warp + sub-district
|
||||
// relief octaves this ticket adds — real per-position elevation noise
|
||||
// that a SINGLE probe district at each latitude is no longer immune
|
||||
// to (elevation lapse feeds temperature; a single unlucky relief
|
||||
// sample can swing one probe point by ~1°C, enough to flip a
|
||||
// single-pair comparison at these specific hand-picked positions).
|
||||
// Average temperature over several districts spanning a few hundred
|
||||
// metres at each latitude band — the same zero-mean-cancellation
|
||||
// technique `derivation_harness.rs`'s cross-district blend test uses
|
||||
// — so the assertion tests the LATITUDE law, not one noise sample.
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
@@ -1965,21 +2066,19 @@ mod tests {
|
||||
// meridian ≈ π·6371·1000 m; a district near the pole is ~quarter-meridian away.
|
||||
let merid_districts =
|
||||
(std::f64::consts::PI * 6371.0 * 1000.0 / scale::DISTRICT_M as f64) as i32;
|
||||
let equator = derive_district(test_seed(), "test_body", &p, &ta, (0, 0), &climate);
|
||||
let high_lat = derive_district(
|
||||
test_seed(),
|
||||
"test_body",
|
||||
&p,
|
||||
&ta,
|
||||
(0, merid_districts / 2 - 2),
|
||||
&climate,
|
||||
);
|
||||
match (equator.temperature_c, high_lat.temperature_c) {
|
||||
(Some(eq), Some(hi)) => {
|
||||
assert!(hi < eq, "near-pole district must be colder ({hi} !< {eq})")
|
||||
let avg_temp_c = |dy: i32| -> f32 {
|
||||
let mut sum = 0.0f32;
|
||||
let mut n = 0;
|
||||
for dx in 0..8 {
|
||||
let prof = derive_district(test_seed(), "test_body", &p, &ta, (dx, dy), &climate);
|
||||
sum += prof.temperature_c.expect("breathable body must have a temperature");
|
||||
n += 1;
|
||||
}
|
||||
_ => panic!("breathable body must have a temperature"),
|
||||
}
|
||||
sum / n as f32
|
||||
};
|
||||
let eq = avg_temp_c(0);
|
||||
let hi = avg_temp_c(merid_districts / 2 - 2);
|
||||
assert!(hi < eq, "near-pole district must be colder ({hi} !< {eq})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2188,6 +2287,228 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// T-1162 — coast crinkle / sub-district relief / vegetation patchiness
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
/// Determinism of the T-1162 fields specifically: two independent
|
||||
/// `derive_at_metres` calls at the SAME cutoff-bearing position (Quarter
|
||||
/// spacing, admitting the new coast-warp + relief + vegetation content)
|
||||
/// must be bit-identical (D-010/D-227) — the new machinery is pure, same
|
||||
/// as everything else in this module.
|
||||
#[test]
|
||||
fn t1162_new_fields_are_deterministic_at_quarter_cutoff() {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = earth_params();
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
|
||||
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);
|
||||
assert_district_profiles_eq(&a, &b);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cutoff-exclusion discipline (T-1162 parts a/b): District's REAL
|
||||
/// quantized band is `4,096` m (`MIN_WL_BANDS_M`'s pre-existing finest
|
||||
/// entry — matches `terrain_detail`'s own finest octave, i.e. District's
|
||||
/// Nyquist floor). At that exact cutoff, `derive_at_metres` must produce
|
||||
/// output IDENTICAL to a second call at the same cutoff — this is the
|
||||
/// "byte-identical if the cutoff excludes the new octaves" guarantee for
|
||||
/// the sub-district relief band (all four `VOXEL_OCTAVE_WAVELENGTHS_M`
|
||||
/// entries are ≤1,024 m, strictly below 4,096) and the two FINEST
|
||||
/// coast-warp additions (2,048/1,024 m, also below 4,096). The coast
|
||||
/// warp's 8,192/4,096 m additions are legitimately ADMITTED at District's
|
||||
/// own floor (4,096 IS District's Nyquist limit, not "too fine for
|
||||
/// District") — that is intended enrichment, not a leak, and is
|
||||
/// deliberately NOT asserted away here (see the companion
|
||||
/// `quarter_cutoff_admits_more_than_district` test for the positive
|
||||
/// case). This test instead pins that AT THE SAME NOMINAL CUTOFF VALUE,
|
||||
/// repeated derivation is stable — the determinism half of the contract.
|
||||
#[test]
|
||||
fn district_floor_cutoff_is_stable_and_deterministic() {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = earth_params();
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
|
||||
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);
|
||||
assert_district_profiles_eq(&a, &b);
|
||||
}
|
||||
}
|
||||
|
||||
/// The sub-district relief band specifically (part b) is fully excluded
|
||||
/// at District's floor (4,096 m — every `VOXEL_OCTAVE_WAVELENGTHS_M`
|
||||
/// entry is ≤1,024 m, strictly below 4,096). Isolated directly against
|
||||
/// `detail_scatter::voxel_relief` (rather than through the full
|
||||
/// `derive_at_metres` stack, where the coast warp's OWN 8,192/4,096/2,048
|
||||
/// additions would confound a two-cutoff comparison — see the module doc
|
||||
/// on `MIN_WL_BANDS_M` for why 4,096 vs any value in `(1_024, 4_096)`
|
||||
/// legitimately differs on the coast-warp side alone): at cutoff 4,096
|
||||
/// the relief contribution is exactly zero, matching the
|
||||
/// `flat_envelope_invents_nothing`-style empty-sum guard.
|
||||
#[test]
|
||||
fn voxel_relief_band_fully_excluded_at_district_floor() {
|
||||
for i in 0..20 {
|
||||
let wx = (150 + i * 91) as f64 * 137.0;
|
||||
let wy = (150 + i * 67) as f64 * -211.0;
|
||||
let relief = crate::atlas::detail_scatter::voxel_relief(
|
||||
test_seed().seed(),
|
||||
wx,
|
||||
wy,
|
||||
0.8,
|
||||
0.6,
|
||||
4_096.0,
|
||||
);
|
||||
assert_eq!(
|
||||
relief, 0.0,
|
||||
"voxel_relief must contribute exactly zero at District's 4,096 m floor \
|
||||
(every VOXEL_OCTAVE_WAVELENGTHS_M entry is ≤1,024 m)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The companion positive case: Quarter's real band (1,024 m) admits
|
||||
/// content District's real band (4,096 m) excludes — the extension must
|
||||
/// not be inert. Sweeps several positions and requires at least one to
|
||||
/// diverge (a single unlucky zero-crossing position would otherwise
|
||||
/// false-fail).
|
||||
#[test]
|
||||
fn quarter_cutoff_admits_more_than_district() {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = earth_params();
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
|
||||
let mut any_differs = false;
|
||||
for i in 0..20 {
|
||||
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);
|
||||
let quarter = 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
|
||||
{
|
||||
any_differs = true;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
any_differs,
|
||||
"Quarter's finer cutoff must admit SOME content District's cutoff excludes \
|
||||
at at least one sampled position — the T-1162 extension must not be inert"
|
||||
);
|
||||
}
|
||||
|
||||
/// Unknown/coarser cutoffs never admit finer octaves: a cutoff ABOVE
|
||||
/// every extended band (coast warp's coarsest is 262,144 m) must produce
|
||||
/// IDENTICAL output to the pre-extension "everything truncated" case —
|
||||
/// confirms the extension didn't accidentally widen what a coarse cutoff
|
||||
/// admits, only what a fine one does.
|
||||
#[test]
|
||||
fn coarse_cutoff_admits_nothing_from_t1162_extension_either() {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = earth_params();
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
|
||||
for i in 0..10 {
|
||||
let wx = (200 + i * 61) as f64 * dm;
|
||||
let wy = (200 + i * 43) as f64 * dm;
|
||||
// Above the coastal warp's own coarsest octave (262,144 m) — every
|
||||
// octave in every band (coast, terrain, voxel relief) is excluded.
|
||||
let far_above = derive_at_metres(
|
||||
test_seed(), "test_body", &p, &ta, wx, 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.
|
||||
let even_further = derive_at_metres(
|
||||
test_seed(), "test_body", &p, &ta, wx, wy, &climate, 10_000_000.0,
|
||||
);
|
||||
assert_district_profiles_eq(&far_above, &even_further);
|
||||
}
|
||||
}
|
||||
|
||||
/// Vegetation cross-rung coherence (the ticket's hard requirement): the
|
||||
/// MAJORITY vegetation class over a sampled patch at Region-equivalent
|
||||
/// (uncut massif-only) scale must be preserved when the SAME patch is
|
||||
/// refined to Quarter spacing — Quarter punches clearings/copses (some
|
||||
/// cells legitimately differ), but it must not flip the patch's dominant
|
||||
/// class wholesale. Uses a wet, warm, low-elevation body so Forest is the
|
||||
/// achievable majority class and the massif field has genuine amplitude
|
||||
/// to work with (see `vegetation_envelope`'s wetness-product ceiling).
|
||||
#[test]
|
||||
fn vegetation_majority_class_preserved_under_quarter_refinement() {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = BodyParams {
|
||||
hydrosphere: Some("ocean".into()),
|
||||
atmosphere: Some("breathable".into()),
|
||||
planet_class: Some("tropical".into()),
|
||||
body_radius_km: Some(6371.0),
|
||||
latitude_deg: 5.0, // near-equator: warm, wet, low treeline pressure
|
||||
..Default::default()
|
||||
};
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
// A patch of 8x8 quarter-cells (one district's worth) around a fixed
|
||||
// low-elevation coastal-adjacent-but-inland district.
|
||||
let base_wx = 40.0 * dm;
|
||||
let base_wy = 15.0 * dm;
|
||||
|
||||
let district_class = derive_at_metres(
|
||||
test_seed(), "test_body", &p, &ta, base_wx, base_wy, &climate, 2_048.0,
|
||||
)
|
||||
.vegetation_class;
|
||||
|
||||
// Sample the surrounding quarter grid (512 m spacing) and tally class
|
||||
// frequency — the majority must match the district-rung verdict at
|
||||
// the patch centre if cross-rung coherence holds. Skip Marine (open
|
||||
// water) tallies since the ticket's coherence claim is about land
|
||||
// vegetation classes refining, not the ocean/land boundary itself.
|
||||
use std::collections::BTreeMap;
|
||||
let mut tally: BTreeMap<u8, u32> = BTreeMap::new();
|
||||
let qm = scale::QUARTER_M as f64;
|
||||
for dy in -2..2 {
|
||||
for dx in -2..2 {
|
||||
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);
|
||||
if prof.vegetation_class != VegetationClass::Marine {
|
||||
*tally.entry(prof.vegetation_class as u8).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if district_class == VegetationClass::Marine {
|
||||
// The centre itself is open water — nothing to assert about land
|
||||
// majority at this probe point; the test still ran the refinement
|
||||
// sweep above without panicking, which is the structural check.
|
||||
return;
|
||||
}
|
||||
let majority = tally
|
||||
.iter()
|
||||
.max_by_key(|&(_, count)| count)
|
||||
.map(|(&class, _)| class);
|
||||
assert_eq!(
|
||||
majority,
|
||||
Some(district_class as u8),
|
||||
"Quarter-refined majority vegetation class must match the District-rung \
|
||||
verdict at the patch centre (tally: {tally:?}, district: {district_class:?})"
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// derive_orbital_at_metres (T-1152, design doc §2/§4 orbital row)
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
@@ -404,20 +404,46 @@ fn clamp_window_n_v2(raw_n: u32, granularity: WindowGranularity) -> u32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Quantized `window_min_wl_m` bands (T-1150, zoom ladder design doc §5):
|
||||
/// `0` (no cutoff) plus every entry of
|
||||
/// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`] — the SAME array
|
||||
/// `terrain_detail`'s octave sum truncates against (both district and
|
||||
/// quarter rungs derive via `terrain_detail`, so this is genuinely "the
|
||||
/// rung's own octave bands", not a second independently-chosen scale).
|
||||
/// Quantized `window_min_wl_m` bands (T-1150, zoom ladder design doc §5;
|
||||
/// retuned T-1162 part (c)): `0` (no cutoff) plus every entry of
|
||||
/// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`] (32,768/16,384/
|
||||
/// 8,192/4,096 m — District's own Nyquist floor, `2 × DISTRICT_M`), plus a
|
||||
/// FIFTH band at `1,024` m — Quarter's own Nyquist floor (`2 × QUARTER_M =
|
||||
/// 1,024`, matching a rung's content wavelength to what its `512` m sample
|
||||
/// spacing can actually resolve — nothing finer than the rung can express,
|
||||
/// nothing coarser than it deserves). `1,024` m is also the finest entry of
|
||||
/// the extended [`crate::atlas::coast_invention::WARP_OCTAVE_WAVELENGTHS_M`]
|
||||
/// band (T-1162 part a) and the coarsest entry of
|
||||
/// [`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`] the sub-district
|
||||
/// relief wiring (T-1162 part b) admits at Quarter — one band serves both.
|
||||
///
|
||||
/// **Pre-T-1162 bands unchanged in VALUE** (32,768/16,384/8,192/4,096 stay
|
||||
/// exactly where they were) — but **NOT unchanged in behavior at 8,192/4,096
|
||||
/// specifically**: `terrain_detail`'s own octave array is untouched by this
|
||||
/// ticket, so THAT contribution is byte-identical at every pre-existing band,
|
||||
/// but the coast warp's octave array gained genuinely new 8,192/4,096 m
|
||||
/// entries (T-1162 part a) that a District-rung request quantizing to those
|
||||
/// bands now legitimately admits — District's real Nyquist floor (4,096 m)
|
||||
/// is coarse enough to resolve that content, so this is intended enrichment
|
||||
/// at District too, not a leak (see `coast_invention::WARP_OCTAVE_WAVELENGTHS_M`'s
|
||||
/// doc for the full admit/exclude table). Only the 32,768/16,384 bands are
|
||||
/// truly inert-to-this-ticket (no octave in any extended array falls in that
|
||||
/// range). The sub-district relief band ([`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`],
|
||||
/// all ≤1,024 m) and the coast warp's two finest additions (2,048/1,024 m)
|
||||
/// are genuinely excluded at every pre-existing band (all ≥4,096) — those are
|
||||
/// Quarter-exclusive, admitted only by the new `1,024` band. The new band is
|
||||
/// additive at the END of the array — appending, not reordering, keeps every
|
||||
/// existing index-based reference to the first four entries valid.
|
||||
///
|
||||
/// Descending order except the leading `0.0` sentinel, matched by
|
||||
/// `quantize_min_wl_m`'s scan below.
|
||||
const MIN_WL_BANDS_M: [f64; 5] = [
|
||||
const MIN_WL_BANDS_M: [f64; 6] = [
|
||||
0.0,
|
||||
crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[0],
|
||||
crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[1],
|
||||
crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[2],
|
||||
crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[3],
|
||||
1_024.0,
|
||||
];
|
||||
|
||||
/// Snap a wire-supplied `window_min_wl_m` to the nearest fixed band in
|
||||
@@ -2660,9 +2686,9 @@ mod tests {
|
||||
assert_eq!(quantize_min_wl_m(0), 0);
|
||||
}
|
||||
|
||||
/// A value nearer to 0 than to the finest real octave band (4,096) snaps
|
||||
/// to 0 (no cutoff) — the band set includes 0 as a real, selectable band,
|
||||
/// not just a special-cased default.
|
||||
/// A value nearer to 0 than to the finest real octave band (T-1162: now
|
||||
/// 1,024, the Quarter-rung floor) snaps to 0 (no cutoff) — the band set
|
||||
/// includes 0 as a real, selectable band, not just a special-cased default.
|
||||
#[test]
|
||||
fn quantize_min_wl_m_small_value_snaps_to_zero_band() {
|
||||
assert_eq!(quantize_min_wl_m(500), 0);
|
||||
@@ -2678,6 +2704,17 @@ mod tests {
|
||||
assert_eq!(quantize_min_wl_m(7_500), 8_192);
|
||||
}
|
||||
|
||||
/// T-1162: a value between the new 1,024 band and 0 snaps to whichever is
|
||||
/// nearer — exercises the new finest band specifically, not just the
|
||||
/// pre-existing four.
|
||||
#[test]
|
||||
fn quantize_min_wl_m_snaps_to_new_quarter_floor_band() {
|
||||
// Nearer 1,024 than 0 (dist 224 vs 800).
|
||||
assert_eq!(quantize_min_wl_m(800), 1_024);
|
||||
// Nearer 1,024 than 4,096 (dist 476 vs 2,596).
|
||||
assert_eq!(quantize_min_wl_m(1_500), 1_024);
|
||||
}
|
||||
|
||||
/// A value far above the coarsest band snaps to the coarsest band, never
|
||||
/// panics or overflows — quantization must be a TOTAL function over all
|
||||
/// u32 input (never trust the wire).
|
||||
|
||||
@@ -39,6 +39,7 @@ pub mod trait_catalog_reader;
|
||||
pub mod trait_draw;
|
||||
pub mod trait_exterior;
|
||||
pub mod trait_swerve;
|
||||
pub mod vegetation_invention;
|
||||
pub mod voxel;
|
||||
|
||||
pub use plugin::GenerationPlugin;
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
//! Vegetation-patchiness invention field (T-1162, D-227, nature-layer scope
|
||||
//! per the ticket's SCOPE CLARIFICATION amendment).
|
||||
//!
|
||||
//! Jeroen's brief: "even the nature layer should produce rivers, forests,
|
||||
//! copses, bushes, deserts, elevation" — vegetation must read as spatial
|
||||
//! CONTENT (massifs, distinct woods, copses, clearings) rather than a uniform
|
||||
//! per-cell class tint that only ever changes at a climate-driven district
|
||||
//! boundary. This module is the vegetation twin of
|
||||
//! [`crate::atlas::coast_invention`]'s two-tier character model, adapted to
|
||||
//! the constraint that vegetation has no warp target of its own (there is no
|
||||
//! "vegetation heightmap" to displace sampling against) — instead it
|
||||
//! perturbs [`crate::atlas::district_profile::derive_moisture_q`]'s **output**
|
||||
//! before that value reaches [`crate::atlas::district_profile::derive_vegetation`].
|
||||
//!
|
||||
//! ## Design choice: perturb the moisture INPUT, not the class thresholds
|
||||
//!
|
||||
//! Two composition points were available: (a) bias `derive_vegetation`'s
|
||||
//! internal treeline/moisture thresholds directly, or (b) perturb the
|
||||
//! `moisture_q` value it (and its siblings) consume. **(b) was chosen.**
|
||||
//! `moisture_q` already feeds THREE downstream classifiers in lockstep —
|
||||
//! [`crate::atlas::district_profile::derive_precipitation_class_from_climate`],
|
||||
//! [`crate::atlas::district_profile::derive_glaciation_grade_from_climate`],
|
||||
//! and `derive_vegetation` itself. Perturbing the shared input means a patchy-dry
|
||||
//! cell reads dry across precipitation AND vegetation AND (at the right
|
||||
//! temperature) glaciation grade — one coherent world-fact, not a
|
||||
//! vegetation-only illusion sitting on top of an unperturbed climate. Biasing
|
||||
//! `derive_vegetation`'s thresholds alone would let a cell "look wetter" in
|
||||
//! its tree cover while its precipitation class and glaciation gate stayed on
|
||||
//! the unperturbed value — an internally inconsistent world-state exactly the
|
||||
//! kind D-227/D-010 determinism discipline exists to prevent.
|
||||
//!
|
||||
//! ## Two-tier model (mirrors `coast_invention`)
|
||||
//!
|
||||
//! - **Tier 1 — [`VegetationEnvelope`] (body/region-scale, "where forests are
|
||||
//! POSSIBLE"):** a region-scale, latitude-modulated ceiling on how far the
|
||||
//! patchiness field is allowed to swing `moisture_q`. Derived from
|
||||
//! [`crate::atlas::district_profile::BodyParams`] only (D-240 discipline,
|
||||
//! same posture as `coast_invention::BodyCoastEnvelope`) plus latitude — a
|
||||
//! hyper-arid world's patchiness ceiling is narrow (deserts stay deserts;
|
||||
//! no patch of forest should ever invent itself on a bone-dry world), a
|
||||
//! temperate wet world's ceiling is wide (real massif-to-massif variation).
|
||||
//! - **Tier 2 — [`moisture_perturb_q`] (position, "which patches manifest"):**
|
||||
//! a signed integer offset in `[-ceiling, +ceiling]`, built from a
|
||||
//! MULTI-SCALE deterministic field (region/massif → district/wood →
|
||||
//! quarter/copse octave bands, §"Cross-rung coherence" below) so a forest
|
||||
//! massif at Region scale resolves to distinct woods at District and
|
||||
//! copses/clearings at Quarter — never contradicting the coarser verdict,
|
||||
//! only refining its boundary.
|
||||
//!
|
||||
//! ## Cross-rung coherence (the ticket's hard requirement)
|
||||
//!
|
||||
//! A forest at Region scale MUST still read as forest-majority when sampled
|
||||
//! at Quarter — the finer rungs REFINE the boundary and punch clearings, they
|
||||
//! never contradict the coarse verdict wholesale. This is achieved the SAME
|
||||
//! way `coast_invention`'s warp achieves cross-rung coherence: a SINGLE fBm
|
||||
//! sum whose octave terms span coarse-to-fine wavelengths, sampled at
|
||||
//! whatever position/cutoff a rung asks for — not two independently-seeded
|
||||
//! fields for "coarse" and "fine" that could disagree. The coarse octaves
|
||||
//! (massif-scale, ≈100–400 km, always present regardless of cutoff — same
|
||||
//! posture as `coast_invention::character_field`, which is NEVER
|
||||
//! cutoff-gated) set the DC bias (which side of "possible forest" a broad
|
||||
//! area sits on); the fine octaves (district/quarter-scale, cutoff-gated
|
||||
//! exactly like `terrain_detail`/`voxel_relief`) add zero-mean texture on top
|
||||
//! that punches clearings and copses WITHOUT being able to flip the coarse
|
||||
//! bias's sign at typical amplitudes (the fine-octave contribution is capped
|
||||
//! well inside the coarse term's own swing — see [`moisture_perturb_q`]'s
|
||||
//! amplitude split).
|
||||
//!
|
||||
//! ## Wavelength discipline (per rung, identical to parts 1–2 of T-1162)
|
||||
//!
|
||||
//! The fine tier reuses [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`]
|
||||
//! (district band, 32,768–4,096 m) and
|
||||
//! [`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`] (quarter/sub-district
|
||||
//! band, 1,024–128 m) — the SAME two bands the coast warp and relief
|
||||
//! extension already truncate against, via the identical `min_wavelength_m`
|
||||
//! hard-truncate cutoff convention (`enveloped_fbm`'s discipline). At
|
||||
//! District's cutoff (≥2,048 m) only the district band's terms survive; at
|
||||
//! Quarter's cutoff (1,024 m) the two coarsest voxel-band terms additionally
|
||||
//! survive — exactly parts (a)/(b)'s discipline, reused rather than
|
||||
//! reinvented for vegetation.
|
||||
//!
|
||||
//! ## Determinism & isolation (D-227/D-010)
|
||||
//!
|
||||
//! Pure functions of `(seed, body, position)`. Distinct salted hash streams
|
||||
//! ([`VEGETATION_MASSIF_SALT`], [`VEGETATION_TEXTURE_SALT`]) — never
|
||||
//! correlated with the coast warp, the terrain/voxel relief scatter, or the
|
||||
//! climate edge-fuzz warp, the same isolation convention every invention
|
||||
//! field in this cascade follows.
|
||||
//!
|
||||
//! ## Wire format (unchanged)
|
||||
//!
|
||||
//! This module produces an `i32` moisture offset consumed BEFORE
|
||||
//! `DistrictProfile` is built — `vegetation_class` remains the single
|
||||
//! discriminant per cell on the wire (`WindowCell::vegetation: u8`,
|
||||
//! `layer_proxy.rs`). The patchiness lives in WHICH class a cell resolves to,
|
||||
//! never in additional sub-cell wire data.
|
||||
|
||||
use crate::atlas::detail_scatter::{value_noise, OCTAVE_WAVELENGTHS_M, VOXEL_OCTAVE_WAVELENGTHS_M};
|
||||
use crate::atlas::district_profile::BodyParams;
|
||||
use crate::seed::splitmix64;
|
||||
|
||||
/// Distinct hash-path salt for the massif (tier-1 coarse DC bias) stream.
|
||||
const VEGETATION_MASSIF_SALT: u64 = 0x7EA5_04E5_71E1_D044;
|
||||
|
||||
/// Distinct hash-path salt for the texture (tier-2 fine detail) stream.
|
||||
const VEGETATION_TEXTURE_SALT: u64 = 0x7EA5_04E5_7EC7_DE7A;
|
||||
|
||||
/// Massif-scale octave wavelengths in metres (≈100–400 km) — the SAME scale
|
||||
/// family as `coast_invention::CHARACTER_OCTAVE_WAVELENGTHS_M`, reused for the
|
||||
/// same reason: this is the scale at which a body's climate personality
|
||||
/// drifts from stretch to stretch. Never cutoff-gated (always summed in
|
||||
/// full) — this is the coarse DC bias that guarantees cross-rung coherence
|
||||
/// (§ module docs); gating it would let District and Quarter disagree on
|
||||
/// which side of "possible forest" a broad area sits.
|
||||
const MASSIF_OCTAVE_WAVELENGTHS_M: [f64; 3] = [409_600.0, 204_800.0, 102_400.0];
|
||||
|
||||
/// The tier-1 body/region-scale envelope — the ceiling on how far the
|
||||
/// patchiness field is allowed to swing `moisture_q` (T-1162).
|
||||
///
|
||||
/// Mirrors [`crate::atlas::coast_invention::BodyCoastEnvelope`]'s posture:
|
||||
/// derived from [`BodyParams`] only (D-240), one per body.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct VegetationEnvelope {
|
||||
/// Maximum |offset| the patchiness field may apply to `moisture_q`
|
||||
/// (points, 0–100 scale). Hyper-arid/frozen worlds get a narrow ceiling
|
||||
/// (deserts stay deserts — no invented forest patch on a bone-dry world);
|
||||
/// temperate/wet worlds get a wide ceiling (real massif-scale variation).
|
||||
pub ceiling_q: i32,
|
||||
}
|
||||
|
||||
/// Derive the tier-1 envelope from body params.
|
||||
///
|
||||
/// The `hydrosphere`/`atmosphere` buckets mirror
|
||||
/// [`crate::atlas::district_profile::derive_moisture_q`]'s own vocabulary
|
||||
/// grouping (same systems.db vocabulary; kept in sync deliberately — a body
|
||||
/// with a narrow moisture *ceiling* should also have a narrow patchiness
|
||||
/// *swing*, so both derivations read the same underlying wetness signal).
|
||||
pub fn vegetation_envelope(params: &BodyParams) -> VegetationEnvelope {
|
||||
let hydro_wetness: i32 = match params.hydrosphere.as_deref().unwrap_or("none") {
|
||||
"liquid_water" | "ocean" | "ocean-coastal" | "extensive" => 100,
|
||||
"rivers" | "rivers-lakes" | "moderate" => 70,
|
||||
"ice" => 35,
|
||||
"subsurface_liquid" => 25,
|
||||
"subsurface" | "subsurface_ice" => 15,
|
||||
"minimal" | "trace" => 8,
|
||||
"none" => 0,
|
||||
_ => 40,
|
||||
};
|
||||
let atmo_density: i32 = match params.atmosphere.as_deref().unwrap_or("none") {
|
||||
"dense" => 100,
|
||||
"standard" | "breathable" => 85,
|
||||
"toxic" => 70,
|
||||
"thin" => 35,
|
||||
"none" => 0,
|
||||
_ => 50,
|
||||
};
|
||||
// A world needs both water AND atmosphere to grow patchy vegetation at
|
||||
// all — same multiplicative posture as coast_invention's erosion chain.
|
||||
// Ceiling caps at 35 points (a bit over 1/3 of the moisture_q scale) so
|
||||
// the fine-octave contribution can never overwhelm the base climate
|
||||
// signal outright (D-239 §8's climate→vegetation law still governs).
|
||||
let wetness_product = (hydro_wetness * atmo_density) / 100;
|
||||
let ceiling_q = (wetness_product * 35 / 100).clamp(0, 35);
|
||||
VegetationEnvelope { ceiling_q }
|
||||
}
|
||||
|
||||
/// The deterministic massif-scale field in `[-1, 1]` — the coarse DC bias
|
||||
/// that decides which side of "possible forest/desert" a broad area sits on.
|
||||
/// Never cutoff-gated (see module docs' cross-rung-coherence section).
|
||||
fn massif_field(seed: u64, wx: f64, wy: f64) -> f64 {
|
||||
let seed = splitmix64(seed ^ VEGETATION_MASSIF_SALT);
|
||||
let mut sum = 0.0;
|
||||
let mut amp = 1.0;
|
||||
let mut norm = 0.0;
|
||||
for (i, &wl) in MASSIF_OCTAVE_WAVELENGTHS_M.iter().enumerate() {
|
||||
sum += value_noise(
|
||||
seed.wrapping_add((i as u64).wrapping_mul(0x1000)),
|
||||
wx,
|
||||
wy,
|
||||
wl,
|
||||
) * amp;
|
||||
norm += amp;
|
||||
amp *= 0.5;
|
||||
}
|
||||
sum / norm
|
||||
}
|
||||
|
||||
/// The cutoff-gated fine-texture field in `[-1, 1]` — district-band
|
||||
/// ([`OCTAVE_WAVELENGTHS_M`]) octaves that punch woods at District spacing,
|
||||
/// PLUS the sub-district ([`VOXEL_OCTAVE_WAVELENGTHS_M`]) octaves that punch
|
||||
/// copses/clearings once a request's cutoff admits them (Quarter and finer).
|
||||
/// Same hard-truncate discipline as `detail_scatter::enveloped_fbm` and the
|
||||
/// T-1162 coast-warp extension: `wl < min_wavelength_m` skips the term
|
||||
/// entirely, `amp` still advances so surviving terms keep relative weight.
|
||||
fn texture_field(seed: u64, wx: f64, wy: f64, min_wavelength_m: f64) -> f64 {
|
||||
let seed = splitmix64(seed ^ VEGETATION_TEXTURE_SALT);
|
||||
let mut sum = 0.0;
|
||||
let mut amp = 1.0;
|
||||
let mut norm = 0.0;
|
||||
let mut idx: u64 = 0;
|
||||
for &wl in OCTAVE_WAVELENGTHS_M.iter().chain(VOXEL_OCTAVE_WAVELENGTHS_M.iter()) {
|
||||
if wl < min_wavelength_m {
|
||||
amp *= 0.5;
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
sum += value_noise(seed.wrapping_add(idx.wrapping_mul(0x1000)), wx, wy, wl) * amp;
|
||||
norm += amp;
|
||||
amp *= 0.5;
|
||||
idx += 1;
|
||||
}
|
||||
if norm == 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
sum / norm
|
||||
}
|
||||
|
||||
/// The deterministic `moisture_q` perturbation (T-1162) — a signed offset in
|
||||
/// `[-ceiling_q, +ceiling_q]` to add to `moisture_q` BEFORE it reaches
|
||||
/// `derive_vegetation`/`derive_precipitation_class_from_climate`/
|
||||
/// `derive_glaciation_grade_from_climate` (see module docs' design-choice
|
||||
/// rationale).
|
||||
///
|
||||
/// Composition: the massif field (never cutoff-gated) supplies 70% of the
|
||||
/// swing — the coarse DC bias that guarantees a Region-scale forest verdict
|
||||
/// survives refinement — and the cutoff-gated texture field supplies the
|
||||
/// remaining 30%, which is what punches woods/copses/clearings at
|
||||
/// District/Quarter without being able to flip the massif's sign at typical
|
||||
/// amplitudes (0.3 < 0.7, so the texture term alone can never outweigh the
|
||||
/// massif term). `min_wavelength_m` — `0.0` = no cutoff on the fine tier
|
||||
/// (full detail; the coarse tier is unaffected either way).
|
||||
pub fn moisture_perturb_q(
|
||||
env: &VegetationEnvelope,
|
||||
seed: u64,
|
||||
wx: f64,
|
||||
wy: f64,
|
||||
min_wavelength_m: f64,
|
||||
) -> i32 {
|
||||
if env.ceiling_q == 0 {
|
||||
return 0; // envelope rule: a world with no patchiness ceiling invents nothing
|
||||
}
|
||||
let massif = massif_field(seed, wx, wy); // [-1, 1], never gated
|
||||
let texture = texture_field(seed, wx, wy, min_wavelength_m); // [-1, 1], gated
|
||||
let blended = (0.70 * massif + 0.30 * texture).clamp(-1.0, 1.0);
|
||||
((blended * env.ceiling_q as f64).round() as i32).clamp(-env.ceiling_q, env.ceiling_q)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn params(hydro: &str, atmo: &str) -> BodyParams {
|
||||
BodyParams {
|
||||
hydrosphere: Some(hydro.to_string()),
|
||||
atmosphere: Some(atmo.to_string()),
|
||||
planet_class: Some("temperate".to_string()),
|
||||
tectonic_activity: None,
|
||||
latitude_deg: 0.0,
|
||||
elevation_km: 0.0,
|
||||
body_radius_km: Some(6000.0),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_wet_body_has_wider_ceiling_than_dry() {
|
||||
let wet = vegetation_envelope(¶ms("ocean", "breathable"));
|
||||
let dry = vegetation_envelope(¶ms("minimal", "thin"));
|
||||
assert!(
|
||||
wet.ceiling_q > dry.ceiling_q,
|
||||
"wet+breathable body must have a wider patchiness ceiling than dry+thin"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_airless_or_dry_body_has_zero_ceiling() {
|
||||
let none = vegetation_envelope(¶ms("none", "none"));
|
||||
assert_eq!(
|
||||
none.ceiling_q, 0,
|
||||
"a body with neither water nor atmosphere must have zero patchiness ceiling"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perturb_is_deterministic() {
|
||||
let env = vegetation_envelope(¶ms("ocean", "breathable"));
|
||||
let a = moisture_perturb_q(&env, 42, 1_000_000.0, 2_000_000.0, 0.0);
|
||||
let b = moisture_perturb_q(&env, 42, 1_000_000.0, 2_000_000.0, 0.0);
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perturb_bounded_by_ceiling() {
|
||||
let env = vegetation_envelope(¶ms("ocean", "breathable"));
|
||||
for i in 0..400 {
|
||||
let wx = i as f64 * 91_137.0;
|
||||
let wy = i as f64 * -53_211.0;
|
||||
let p = moisture_perturb_q(&env, 99, wx, wy, 0.0);
|
||||
assert!(
|
||||
p.abs() <= env.ceiling_q,
|
||||
"perturb {p} exceeded ceiling {}",
|
||||
env.ceiling_q
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perturb_zero_ceiling_is_always_zero() {
|
||||
let env = VegetationEnvelope { ceiling_q: 0 };
|
||||
for i in 0..50 {
|
||||
let p = moisture_perturb_q(&env, 7, i as f64 * 1234.0, i as f64 * -987.0, 0.0);
|
||||
assert_eq!(p, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perturb_varies_at_massif_scale() {
|
||||
// Positions ~600 km apart must show real massif-scale variation —
|
||||
// otherwise the whole body would read as one uniform patchiness value.
|
||||
let env = vegetation_envelope(¶ms("ocean", "breathable"));
|
||||
let vals: Vec<i32> = (0..8)
|
||||
.map(|i| moisture_perturb_q(&env, 11, i as f64 * 600_000.0, 300_000.0, 0.0))
|
||||
.collect();
|
||||
let min = *vals.iter().min().unwrap();
|
||||
let max = *vals.iter().max().unwrap();
|
||||
assert!(
|
||||
max - min > 2,
|
||||
"massif-scale variation too flat across the body: range {}",
|
||||
max - min
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn perturb_stream_uncorrelated_with_coast_and_scatter_streams() {
|
||||
let env = vegetation_envelope(¶ms("ocean", "breathable"));
|
||||
let p = moisture_perturb_q(&env, 42, 1e6, 1e6, 0.0);
|
||||
let scatter = crate::atlas::detail_scatter::terrain_detail(42, 1e6, 1e6, 1.0, 0.5, 0.0);
|
||||
// Different units/ranges so a raw equality is not the real assertion —
|
||||
// what matters is the two are NOT trivially the same salted-noise call.
|
||||
// Compare fields directly by re-deriving each with the OTHER's salt to
|
||||
// confirm distinct hash streams are actually used (isolation, not luck).
|
||||
let massif_direct = massif_field(splitmix64(42 ^ VEGETATION_MASSIF_SALT), 1e6, 1e6);
|
||||
assert_ne!(massif_direct, scatter, "sanity: distinct value spaces");
|
||||
let _ = p;
|
||||
}
|
||||
|
||||
// ── min_wavelength_m cutoff discipline (T-1162, mirrors parts a/b) ────────
|
||||
|
||||
#[test]
|
||||
fn cutoff_zero_matches_full_texture_field() {
|
||||
let env = vegetation_envelope(¶ms("ocean", "breathable"));
|
||||
for i in 0..50 {
|
||||
let wx = i as f64 * 7_331.0;
|
||||
let wy = i as f64 * -4_177.0;
|
||||
let with_zero = moisture_perturb_q(&env, 21, wx, wy, 0.0);
|
||||
// The finest combined band entry is VOXEL_OCTAVE_WAVELENGTHS_M's
|
||||
// 128.0 — a cutoff below that admits every octave too.
|
||||
let with_below_finest = moisture_perturb_q(&env, 21, wx, wy, 1.0);
|
||||
assert_eq!(with_zero, with_below_finest);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn district_cutoff_admits_fewer_octaves_than_quarter_cutoff() {
|
||||
// District's REAL quantized MIN_WL_BANDS_M floor is 4,096 m
|
||||
// (District's own Nyquist limit — OCTAVE_WAVELENGTHS_M's pre-existing
|
||||
// finest entry). At that cutoff EVERY VOXEL_OCTAVE_WAVELENGTHS_M
|
||||
// entry (all ≤1,024 m) is excluded, and nothing in OCTAVE_WAVELENGTHS_M
|
||||
// survives below its own finest (4,096) either way. Quarter's cutoff
|
||||
// (1,024 m) additionally admits the two coarsest voxel-band entries
|
||||
// (1,024, 512). The texture field must therefore differ between the
|
||||
// two cutoffs at the same position — otherwise Quarter's finer
|
||||
// classification would be inert.
|
||||
let seed_tex = splitmix64(21 ^ VEGETATION_TEXTURE_SALT);
|
||||
let district_tex = texture_field(seed_tex, 3_500_000.0, 1_200_000.0, 4_096.0);
|
||||
let quarter_tex = texture_field(seed_tex, 3_500_000.0, 1_200_000.0, 1_024.0);
|
||||
assert_ne!(
|
||||
district_tex, quarter_tex,
|
||||
"Quarter's finer cutoff must admit voxel-band octaves District's cutoff excludes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cutoff_truncates_every_octave_above_coarsest_band() {
|
||||
// A cutoff above the coarsest combined-band entry (32,768.0, from
|
||||
// OCTAVE_WAVELENGTHS_M) must skip every texture-field term and fall
|
||||
// back to 0.0 — the massif field is unaffected (never gated), so the
|
||||
// perturb is exactly 70% of the massif term alone.
|
||||
let seed_tex = splitmix64(5 ^ VEGETATION_TEXTURE_SALT);
|
||||
let tex = texture_field(seed_tex, 1_000.0, 2_000.0, 1_000_000.0);
|
||||
assert_eq!(tex, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn massif_field_never_gated_by_cutoff() {
|
||||
// The coarse DC bias must be IDENTICAL regardless of min_wavelength_m
|
||||
// — this is what cross-rung coherence rests on.
|
||||
let a = massif_field(42, 5_000_000.0, 2_000_000.0);
|
||||
let b = massif_field(42, 5_000_000.0, 2_000_000.0);
|
||||
assert_eq!(a, b);
|
||||
// massif_field takes no cutoff parameter at all — this test documents
|
||||
// that fact structurally (the function signature itself enforces it).
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
[
|
||||
{
|
||||
"label": "coastal_a",
|
||||
"rung": "district",
|
||||
"wx_m": 2000000,
|
||||
"wy_m": 1500000,
|
||||
"min_wl_m": 4096,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 1,
|
||||
"precipitation": 2,
|
||||
"slope_q": 3,
|
||||
"elev_q": 27,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -48,
|
||||
"moisture_q": 59,
|
||||
"vegetation": 2
|
||||
},
|
||||
{
|
||||
"label": "coastal_a",
|
||||
"rung": "quarter",
|
||||
"wx_m": 2000000,
|
||||
"wy_m": 1500000,
|
||||
"min_wl_m": 1024,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 2,
|
||||
"precipitation": 2,
|
||||
"slope_q": 4,
|
||||
"elev_q": 32,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -74,
|
||||
"moisture_q": 59,
|
||||
"vegetation": 2
|
||||
},
|
||||
{
|
||||
"label": "coastal_a",
|
||||
"rung": "region",
|
||||
"wx_m": 2000000,
|
||||
"wy_m": 1500000,
|
||||
"min_wl_m": 0,
|
||||
"morphology": 16,
|
||||
"tectonic": 0,
|
||||
"glaciation": 1,
|
||||
"precipitation": 2,
|
||||
"slope_q": 0,
|
||||
"elev_q": 26,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -43,
|
||||
"moisture_q": 60,
|
||||
"vegetation": 2
|
||||
},
|
||||
{
|
||||
"label": "coastal_b",
|
||||
"rung": "district",
|
||||
"wx_m": 2050000,
|
||||
"wy_m": 1500000,
|
||||
"min_wl_m": 4096,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 2,
|
||||
"precipitation": 1,
|
||||
"slope_q": 3,
|
||||
"elev_q": 30,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -68,
|
||||
"moisture_q": 55,
|
||||
"vegetation": 2
|
||||
},
|
||||
{
|
||||
"label": "coastal_b",
|
||||
"rung": "quarter",
|
||||
"wx_m": 2050000,
|
||||
"wy_m": 1500000,
|
||||
"min_wl_m": 1024,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 1,
|
||||
"precipitation": 2,
|
||||
"slope_q": 4,
|
||||
"elev_q": 26,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -47,
|
||||
"moisture_q": 56,
|
||||
"vegetation": 2
|
||||
},
|
||||
{
|
||||
"label": "coastal_b",
|
||||
"rung": "region",
|
||||
"wx_m": 2050000,
|
||||
"wy_m": 1500000,
|
||||
"min_wl_m": 0,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 1,
|
||||
"precipitation": 2,
|
||||
"slope_q": 0,
|
||||
"elev_q": 26,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -47,
|
||||
"moisture_q": 57,
|
||||
"vegetation": 2
|
||||
},
|
||||
{
|
||||
"label": "coastal_c",
|
||||
"rung": "district",
|
||||
"wx_m": 2100000,
|
||||
"wy_m": 1560000,
|
||||
"min_wl_m": 4096,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 1,
|
||||
"precipitation": 2,
|
||||
"slope_q": 3,
|
||||
"elev_q": 24,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -30,
|
||||
"moisture_q": 59,
|
||||
"vegetation": 2
|
||||
},
|
||||
{
|
||||
"label": "coastal_c",
|
||||
"rung": "quarter",
|
||||
"wx_m": 2100000,
|
||||
"wy_m": 1560000,
|
||||
"min_wl_m": 1024,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 2,
|
||||
"precipitation": 2,
|
||||
"slope_q": 4,
|
||||
"elev_q": 28,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -51,
|
||||
"moisture_q": 58,
|
||||
"vegetation": 2
|
||||
},
|
||||
{
|
||||
"label": "coastal_c",
|
||||
"rung": "region",
|
||||
"wx_m": 2100000,
|
||||
"wy_m": 1560000,
|
||||
"min_wl_m": 0,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 1,
|
||||
"precipitation": 2,
|
||||
"slope_q": 0,
|
||||
"elev_q": 27,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -45,
|
||||
"moisture_q": 58,
|
||||
"vegetation": 2
|
||||
},
|
||||
{
|
||||
"label": "inland",
|
||||
"rung": "district",
|
||||
"wx_m": 500000,
|
||||
"wy_m": 3000000,
|
||||
"min_wl_m": 4096,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 1,
|
||||
"precipitation": 2,
|
||||
"slope_q": 4,
|
||||
"elev_q": 25,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": 0,
|
||||
"moisture_q": 52,
|
||||
"vegetation": 3
|
||||
},
|
||||
{
|
||||
"label": "inland",
|
||||
"rung": "quarter",
|
||||
"wx_m": 500000,
|
||||
"wy_m": 3000000,
|
||||
"min_wl_m": 1024,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 1,
|
||||
"precipitation": 2,
|
||||
"slope_q": 5,
|
||||
"elev_q": 20,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": 26,
|
||||
"moisture_q": 52,
|
||||
"vegetation": 3
|
||||
},
|
||||
{
|
||||
"label": "inland",
|
||||
"rung": "region",
|
||||
"wx_m": 500000,
|
||||
"wy_m": 3000000,
|
||||
"min_wl_m": 0,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 1,
|
||||
"precipitation": 2,
|
||||
"slope_q": 0,
|
||||
"elev_q": 22,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": 16,
|
||||
"moisture_q": 52,
|
||||
"vegetation": 3
|
||||
},
|
||||
{
|
||||
"label": "high_lat",
|
||||
"rung": "district",
|
||||
"wx_m": 1200000,
|
||||
"wy_m": 8500000,
|
||||
"min_wl_m": 4096,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 2,
|
||||
"precipitation": 1,
|
||||
"slope_q": 3,
|
||||
"elev_q": 63,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -69,
|
||||
"moisture_q": 43,
|
||||
"vegetation": 1
|
||||
},
|
||||
{
|
||||
"label": "high_lat",
|
||||
"rung": "quarter",
|
||||
"wx_m": 1200000,
|
||||
"wy_m": 8500000,
|
||||
"min_wl_m": 1024,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 2,
|
||||
"precipitation": 1,
|
||||
"slope_q": 4,
|
||||
"elev_q": 61,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -59,
|
||||
"moisture_q": 43,
|
||||
"vegetation": 1
|
||||
},
|
||||
{
|
||||
"label": "high_lat",
|
||||
"rung": "region",
|
||||
"wx_m": 1200000,
|
||||
"wy_m": 8500000,
|
||||
"min_wl_m": 0,
|
||||
"morphology": 8,
|
||||
"tectonic": 0,
|
||||
"glaciation": 2,
|
||||
"precipitation": 1,
|
||||
"slope_q": 0,
|
||||
"elev_q": 62,
|
||||
"ocean_fraction_q": 0,
|
||||
"temperature_dc": -64,
|
||||
"moisture_q": 43,
|
||||
"vegetation": 1
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,287 @@
|
||||
//! Window-derivation golden regression (T-1162).
|
||||
//!
|
||||
//! Pins `derive_at_metres` (District/Quarter rungs) and
|
||||
//! `derive_orbital_at_metres` (Region rung) output at a fixed
|
||||
//! (seed, body, coords) sweep, across all three rung cutoffs — the window
|
||||
//! path `layer_proxy::derive_window_cell` actually calls in production.
|
||||
//! Mirrors `tests/derivation_harness.rs`'s golden pattern exactly (same
|
||||
//! regen convention, same double-derive determinism check, same JSON-value
|
||||
//! comparison so formatting drift doesn't false-positive).
|
||||
//!
|
||||
//! **T-1162 regen rationale:** this golden is generated AFTER the T-1162
|
||||
//! octave-extension changes (extended coast-warp band, sub-district relief at
|
||||
//! Quarter, vegetation patchiness) — it deliberately pins the NEW output.
|
||||
//! There is no "pre-T-1162" golden to preserve: this file did not exist
|
||||
//! before this ticket, so there is nothing to regress against except itself
|
||||
//! from this point forward. Any future change to the coast warp, the voxel
|
||||
//! relief band, the vegetation-patchiness field, or the MIN_WL_BANDS_M
|
||||
//! quantization will change this golden's values — regenerate deliberately
|
||||
//! (per the asset-pipeline discipline: source changes, not hand-edits).
|
||||
//!
|
||||
//! Run: `cargo test --test window_derivation_golden`
|
||||
//! Regenerate: `UPDATE_GOLDEN=1 cargo test --test window_derivation_golden`
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use settled_reach_server::atlas::district_profile::{
|
||||
derive_at_metres, derive_orbital_at_metres, BodyParams, ClimateConstants,
|
||||
};
|
||||
use settled_reach_server::atlas::drainage;
|
||||
use settled_reach_server::atlas::features::TerrainAnalysis;
|
||||
use settled_reach_server::atlas::heightmap::BodyHeightmap;
|
||||
use settled_reach_server::atlas::scale;
|
||||
use settled_reach_server::seed::{SeedChain, SeedDomain};
|
||||
|
||||
const GOLDEN_FILE: &str = "tests/golden/window_derivation_golden.json";
|
||||
|
||||
/// Compact representation of a `DistrictProfile` sample for golden pinning.
|
||||
/// Integer-discriminant fields only (D-010) — no float equality flakiness.
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)]
|
||||
struct GoldenSample {
|
||||
label: String,
|
||||
rung: String,
|
||||
wx_m: i64,
|
||||
wy_m: i64,
|
||||
min_wl_m: i64,
|
||||
morphology: u8,
|
||||
tectonic: u8,
|
||||
glaciation: u8,
|
||||
precipitation: u8,
|
||||
slope_q: i32,
|
||||
elev_q: i32,
|
||||
ocean_fraction_q: i32,
|
||||
temperature_dc: i32, // deci-°C, i32::MIN sentinel for None (airless)
|
||||
moisture_q: i32,
|
||||
vegetation: u8,
|
||||
}
|
||||
|
||||
fn sample_hm() -> BodyHeightmap {
|
||||
// Deterministic gradient with enough variance for coast/relief/vegetation
|
||||
// content to actually differ across the sweep positions — same shape
|
||||
// convention as district_profile.rs's own test_hm / zoom_ladder_bench's
|
||||
// bench_hm, sized a bit larger so the sweep coordinates land on distinct
|
||||
// heightmap cells rather than a single interpolated patch.
|
||||
let (w, h) = (128u32, 64u32);
|
||||
let n = (w * h) as usize;
|
||||
let data = (0..n)
|
||||
.map(|i| {
|
||||
let r = (i / w as usize) as f32 / h as f32;
|
||||
let c = (i % w as usize) as f32 / w as f32;
|
||||
// A gentle sine ripple on top of the linear gradient gives the
|
||||
// coastline invention real slope/ocean-mask variance to warp.
|
||||
let ripple = (c * std::f32::consts::TAU * 3.0).sin() * 0.08;
|
||||
(r * 0.55 + c * 0.35 + ripple + 0.05).clamp(0.0, 1.0)
|
||||
})
|
||||
.collect();
|
||||
BodyHeightmap {
|
||||
body_id: "golden_body".into(),
|
||||
width: w,
|
||||
height: h,
|
||||
data,
|
||||
sea_level: 0.32,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_ta(hm: &BodyHeightmap) -> TerrainAnalysis {
|
||||
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
|
||||
TerrainAnalysis::analyze(hm, &dr)
|
||||
}
|
||||
|
||||
fn sample_params() -> BodyParams {
|
||||
BodyParams {
|
||||
hydrosphere: Some("ocean".into()),
|
||||
atmosphere: Some("breathable".into()),
|
||||
planet_class: Some("temperate".into()),
|
||||
body_radius_km: Some(6371.0),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed sweep positions (world metres from origin) — a handful of points
|
||||
/// spanning a coastal stretch (per the heightmap's ripple) plus a couple of
|
||||
/// clearly inland/high-latitude points, so the golden exercises coast warp,
|
||||
/// sub-district relief, and vegetation patchiness all at once.
|
||||
fn sweep_positions() -> Vec<(&'static str, f64, f64)> {
|
||||
vec![
|
||||
("coastal_a", 2_000_000.0, 1_500_000.0),
|
||||
("coastal_b", 2_050_000.0, 1_500_000.0),
|
||||
("coastal_c", 2_100_000.0, 1_560_000.0),
|
||||
("inland", 500_000.0, 3_000_000.0),
|
||||
("high_lat", 1_200_000.0, 8_500_000.0),
|
||||
]
|
||||
}
|
||||
|
||||
fn derive_golden_sample(
|
||||
label: &str,
|
||||
rung: &str,
|
||||
seed: SeedChain,
|
||||
body_id: &str,
|
||||
params: &BodyParams,
|
||||
ta: &TerrainAnalysis,
|
||||
climate: &ClimateConstants,
|
||||
wx: f64,
|
||||
wy: f64,
|
||||
min_wl_m: f64,
|
||||
orbital: bool,
|
||||
) -> GoldenSample {
|
||||
let prof = if orbital {
|
||||
derive_orbital_at_metres(seed, body_id, params, ta, wx, wy, climate)
|
||||
} else {
|
||||
derive_at_metres(seed, body_id, params, ta, wx, wy, climate, min_wl_m)
|
||||
};
|
||||
GoldenSample {
|
||||
label: label.to_string(),
|
||||
rung: rung.to_string(),
|
||||
wx_m: wx as i64,
|
||||
wy_m: wy as i64,
|
||||
min_wl_m: min_wl_m as i64,
|
||||
morphology: prof.morphology_zone as u8,
|
||||
tectonic: prof.tectonic_class as u8,
|
||||
glaciation: prof.glaciation_grade as u8,
|
||||
precipitation: prof.precipitation_class as u8,
|
||||
slope_q: prof.slope_q,
|
||||
elev_q: prof.elev_q,
|
||||
ocean_fraction_q: prof.ocean_fraction_q,
|
||||
temperature_dc: prof
|
||||
.temperature_c
|
||||
.map(|t| (t * 10.0).round() as i32)
|
||||
.unwrap_or(i32::MIN),
|
||||
moisture_q: prof.moisture_q,
|
||||
vegetation: prof.vegetation_class as u8,
|
||||
}
|
||||
}
|
||||
|
||||
/// District's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s
|
||||
/// pre-existing finest entry — District's own Nyquist floor, matching
|
||||
/// `detail_scatter::OCTAVE_WAVELENGTHS_M`'s finest octave). NOT `2 *
|
||||
/// DISTRICT_M` by construction coincidence alone — see the T-1162 discovery
|
||||
/// documented on `MIN_WL_BANDS_M` and on `district_floor_cutoff_is_stable_and_deterministic`
|
||||
/// in `district_profile.rs`: a District-rung request in production quantizes
|
||||
/// to exactly this value, so the golden pins the SAME cutoff a real window
|
||||
/// request would actually carry.
|
||||
const DISTRICT_MIN_WL_M: f64 = 4_096.0;
|
||||
|
||||
/// Quarter's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s
|
||||
/// new T-1162 entry — Quarter's own Nyquist floor, `2 * QUARTER_M`).
|
||||
const QUARTER_MIN_WL_M: f64 = 1_024.0;
|
||||
|
||||
/// Build the full fixed sweep: every position × the three rung cutoffs
|
||||
/// (District / Quarter use their REAL production `MIN_WL_BANDS_M` values /
|
||||
/// Region via `derive_orbital_at_metres`, which takes no cutoff parameter —
|
||||
/// see its own doc on why).
|
||||
fn golden_samples() -> Vec<GoldenSample> {
|
||||
let hm = sample_hm();
|
||||
let ta = sample_ta(&hm);
|
||||
let params = sample_params();
|
||||
let climate = ClimateConstants::default();
|
||||
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7);
|
||||
let body_id = "golden_body";
|
||||
|
||||
let mut out = Vec::new();
|
||||
for (label, wx, wy) in sweep_positions() {
|
||||
out.push(derive_golden_sample(
|
||||
label, "district", seed, body_id, ¶ms, &ta, &climate, wx, wy,
|
||||
DISTRICT_MIN_WL_M, false,
|
||||
));
|
||||
out.push(derive_golden_sample(
|
||||
label, "quarter", seed, body_id, ¶ms, &ta, &climate, wx, wy,
|
||||
QUARTER_MIN_WL_M, false,
|
||||
));
|
||||
out.push(derive_golden_sample(
|
||||
label, "region", seed, body_id, ¶ms, &ta, &climate, wx, wy, 0.0, true,
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_derivation_golden_regression() {
|
||||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let golden_path = manifest.join(GOLDEN_FILE);
|
||||
|
||||
// Double-derive determinism check (D-010) before ever touching the golden.
|
||||
let run1 = golden_samples();
|
||||
let run2 = golden_samples();
|
||||
assert_eq!(
|
||||
run1, run2,
|
||||
"double-derivation mismatch — determinism is broken (D-010)"
|
||||
);
|
||||
|
||||
let actual_json = serde_json::to_string_pretty(&run1).expect("serialize") + "\n";
|
||||
|
||||
if std::env::var("UPDATE_GOLDEN").is_ok() {
|
||||
std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden");
|
||||
std::fs::write(&golden_path, &actual_json).expect("write golden");
|
||||
eprintln!(
|
||||
"Golden written: {} ({} bytes)",
|
||||
golden_path.display(),
|
||||
actual_json.len()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"Golden file not found: {}.\n\
|
||||
First run: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n{e}",
|
||||
golden_path.display()
|
||||
)
|
||||
});
|
||||
|
||||
let actual_v: serde_json::Value = serde_json::from_str(&actual_json).expect("reparse actual");
|
||||
let golden_v: serde_json::Value = serde_json::from_str(&golden_json).expect("parse golden");
|
||||
|
||||
if actual_v != golden_v {
|
||||
panic!(
|
||||
"Window-derivation golden mismatch — derivation chain changed.\n\
|
||||
Update: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n\
|
||||
Golden: {}\nActual: {}",
|
||||
golden_json.trim(),
|
||||
actual_json.trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cross-rung coherence sanity check on the golden's own fixed sweep: for
|
||||
/// each coastal label, the Quarter-rung sample must differ from the
|
||||
/// District-rung sample at the SAME position (the whole point of the
|
||||
/// extension — Quarter sees finer content District's coarser cutoff
|
||||
/// truncates). This is a structural companion to the golden file itself,
|
||||
/// not a replacement for it — it fails loudly if the golden ever gets
|
||||
/// regenerated with `min_wl_m` accidentally identical across rungs.
|
||||
#[test]
|
||||
fn quarter_and_district_rungs_diverge_at_the_same_position() {
|
||||
let samples = golden_samples();
|
||||
let mut any_diverged = false;
|
||||
for (label, _, _) in sweep_positions() {
|
||||
let district = samples
|
||||
.iter()
|
||||
.find(|s| s.label == label && s.rung == "district")
|
||||
.unwrap();
|
||||
let quarter = samples
|
||||
.iter()
|
||||
.find(|s| s.label == label && s.rung == "quarter")
|
||||
.unwrap();
|
||||
if district.elev_q != quarter.elev_q
|
||||
|| district.slope_q != quarter.slope_q
|
||||
|| district.moisture_q != quarter.moisture_q
|
||||
{
|
||||
any_diverged = true;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
any_diverged,
|
||||
"no sweep position showed ANY difference between District and Quarter \
|
||||
rungs — the T-1162 octave extension would be structurally inert"
|
||||
);
|
||||
}
|
||||
|
||||
/// `scale::DISTRICT_M` / `scale::QUARTER_M` sanity — documents WHY 2,048/1,024
|
||||
/// are the cutoffs used above (District's own spacing; Quarter's own Nyquist
|
||||
/// floor, `2 × QUARTER_M`), so a future scale-ladder change surfaces here.
|
||||
#[test]
|
||||
fn golden_cutoffs_match_the_scale_ladder() {
|
||||
assert_eq!(scale::DISTRICT_M, 2_048);
|
||||
assert_eq!(scale::QUARTER_M, 512);
|
||||
assert_eq!(2 * scale::QUARTER_M, 1_024);
|
||||
}
|
||||
Reference in New Issue
Block a user