feat(simulation): T-1149 derive_at_metres extraction + min_wavelength octave cutoff
Extract derive_district's fractional-metres interior into the metres- addressable derive_at_metres(seed, body, params, ta, wx, wy, climate, min_wavelength_m) — the zoom ladder's keystone (design doc §8 step 1). derive_district is now a thin DistrictPos-quantizing wrapper calling it with cutoff 0.0; all four golden/believability/derivation harnesses pass byte-identical. enveloped_fbm/terrain_detail/voxel_relief gain min_wavelength_m: octaves below the cutoff are hard-truncated (amp still advances so surviving octaves keep relative weight; norm==0 guarded). Amplitude-fade-near- cutoff is documented as a seam, not built — the pop-risk A/B needs the client ladder (T-1153). Cutoff 0.0 is bit-identical to pre-change output, asserted by test. Measured (release, 4096-cell sweeps, zoom_ladder_bench.rs): district spacing 1.200µs/cell (cutoff 0) / 1.201µs (cutoff 2048m — all octaves survive, plumbing check); quarter spacing 1.165µs/cell (cutoff 512m). Matches the design doc's ~1.2-1.4µs/cell release estimate.
This commit is contained in:
@@ -1088,6 +1088,10 @@ struct InventedPrimitives {
|
||||
/// low-relief coasts); `ridge` carries fjord/tectonic sharpness.
|
||||
///
|
||||
/// `body_params` must already carry the district's `latitude_deg`.
|
||||
///
|
||||
/// `min_wavelength_m` (T-1149, zoom ladder §2): threaded straight to the
|
||||
/// `terrain_detail` scatter call — octaves finer than this are truncated.
|
||||
/// `0.0` = no cutoff = today's behavior.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn invent_primitives(
|
||||
seed: SeedChain,
|
||||
@@ -1099,6 +1103,7 @@ fn invent_primitives(
|
||||
world_x_m: f64,
|
||||
world_y_m: f64,
|
||||
region_baseline_c: Option<f32>,
|
||||
min_wavelength_m: f64,
|
||||
) -> InventedPrimitives {
|
||||
// ── 1. Driver tier: UNWARPED raw-bilinear climate (one-step-stale). ─────
|
||||
let raw_elev_q =
|
||||
@@ -1147,6 +1152,7 @@ fn invent_primitives(
|
||||
world_y_m,
|
||||
env_amp,
|
||||
ruggedness,
|
||||
min_wavelength_m,
|
||||
);
|
||||
|
||||
// Shoreline carving (T-1125): glacial / tectonically-young SHORES are cut
|
||||
@@ -1272,6 +1278,7 @@ pub fn derive_district_profile(
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
region_baseline_c,
|
||||
0.0, // batch path — no octave cutoff, matches derive_district's default
|
||||
);
|
||||
|
||||
build_district_profile(
|
||||
@@ -1421,15 +1428,55 @@ pub fn derive_district(
|
||||
climate: &ClimateConstants,
|
||||
) -> DistrictProfile {
|
||||
let (dx, dy) = district_pos;
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
// Thin wrapper (T-1149): quantize DistrictPos -> world metres, then hand off
|
||||
// to the metres-addressable interior. `min_wavelength_m = 0.0` = no octave
|
||||
// cutoff, preserving this function's output byte-for-byte.
|
||||
derive_at_metres(
|
||||
seed,
|
||||
body_id,
|
||||
body_params,
|
||||
ta,
|
||||
dx as f64 * dm,
|
||||
dy as f64 * dm,
|
||||
climate,
|
||||
0.0,
|
||||
)
|
||||
}
|
||||
|
||||
// District → fractional heightmap pixel + world-metre coordinate + latitude.
|
||||
/// The metres-addressable derivation interior (T-1149, zoom ladder keystone,
|
||||
/// design doc §2/§8 step 1) — `derive_district`'s former inline body, extracted
|
||||
/// so a fractional-metres position (not just an integer [`DistrictPos`]) can be
|
||||
/// classified. This is what makes the quarter rung (512 m spacing, T-1150)
|
||||
/// possible without a second derivation pipeline: same function, finer step.
|
||||
///
|
||||
/// `wx`/`wy` are absolute world metres — NOT required to fall on a district-grid
|
||||
/// multiple of [`scale::DISTRICT_M`]; any fractional position is legal.
|
||||
///
|
||||
/// `min_wavelength_m` (§2): forwarded to the `terrain_detail` octave sum inside
|
||||
/// [`invent_primitives`] — octaves finer than this cutoff are truncated. `0.0`
|
||||
/// = no cutoff = [`derive_district`]'s existing behavior.
|
||||
///
|
||||
/// `body_id` is required for the D-243 §4 climate edge-fuzz warp domain separation.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn derive_at_metres(
|
||||
seed: SeedChain,
|
||||
body_id: &str,
|
||||
body_params: &BodyParams,
|
||||
ta: &TerrainAnalysis,
|
||||
wx: f64,
|
||||
wy: f64,
|
||||
climate: &ClimateConstants,
|
||||
min_wavelength_m: f64,
|
||||
) -> DistrictProfile {
|
||||
// World metres -> fractional heightmap pixel + latitude. Mirrors
|
||||
// `derive_district`'s former inline mapping exactly, just keyed on
|
||||
// fractional (wx, wy) instead of an integer DistrictPos scaled up first.
|
||||
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;
|
||||
let wx = dx as f64 * scale::DISTRICT_M as f64;
|
||||
let wy = dy as f64 * scale::DISTRICT_M as f64;
|
||||
// Longitude wraps; district (0,0) sits at lon 0 / the equator.
|
||||
// Longitude wraps; (0,0) sits at lon 0 / the equator.
|
||||
let px = (wx / circumference_m).rem_euclid(1.0) * ta.w as f64;
|
||||
// Latitude: equator at py = h/2, clamped at the poles.
|
||||
let lat_frac = (wy / meridian_m).clamp(-0.5, 0.5); // −0.5 = N pole, +0.5 = S
|
||||
@@ -1437,15 +1484,17 @@ pub fn derive_district(
|
||||
(px, py, wx, wy, -lat_frac * 180.0)
|
||||
}
|
||||
_ => {
|
||||
// No radius: the district grid IS the heightmap grid (tiny test bodies).
|
||||
let px = (dx as f64).clamp(0.0, ta.w.saturating_sub(1) as f64);
|
||||
let py = (dy as f64).clamp(0.0, ta.h.saturating_sub(1) as f64);
|
||||
// No radius: the working grid IS the metre grid (tiny test bodies),
|
||||
// 1 DISTRICT_M = 1 heightmap pixel — the inverse of
|
||||
// `pixel_to_world_m`'s own no-radius convention.
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
let px = (wx / dm).clamp(0.0, ta.w.saturating_sub(1) as f64);
|
||||
let py = (wy / dm).clamp(0.0, ta.h.saturating_sub(1) as f64);
|
||||
let lat_deg = if ta.h > 1 {
|
||||
90.0 - (py / (ta.h - 1) as f64) * 180.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
(px, py, px * dm, py * dm, lat_deg)
|
||||
}
|
||||
};
|
||||
@@ -1456,11 +1505,24 @@ pub fn derive_district(
|
||||
};
|
||||
|
||||
// D-243 §3/§4: compute the edge-fuzz-blended region baseline on-the-fly for
|
||||
// this district. No pre-built cache here — the on-demand path derives the four
|
||||
// surrounding region baselines directly. Pure, deterministic, cheap.
|
||||
// this position. No pre-built cache here — the on-demand path derives the
|
||||
// four surrounding region baselines directly. Pure, deterministic, cheap.
|
||||
// `seed.seed()` (the body-scoped seed value) ensures body-unique warp separation.
|
||||
// Hoisted above the primitives (T-1125): the invention's driver tier needs
|
||||
// the baseline for its one-step-stale climate estimate.
|
||||
//
|
||||
// `region_baseline_at_district` keys on the CONTAINING DistrictPos (via
|
||||
// `rem_euclid` inside `region_profile.rs`), not on fractional metres — so a
|
||||
// sub-district sample (e.g. a quarter, T-1150) floor-divides down to its
|
||||
// containing district here. This is D-243's design intent (climate is a
|
||||
// district-tier field, R2/zoom-ladder-design-doc §9): temperature is a hard
|
||||
// step at every district boundary at every rung, by construction — it does
|
||||
// not refine continuously the way elevation/slope do under a finer
|
||||
// min_wavelength_m.
|
||||
let district_pos: DistrictPos = (
|
||||
(wx / scale::DISTRICT_M as f64).floor() as i32,
|
||||
(wy / scale::DISTRICT_M as f64).floor() as i32,
|
||||
);
|
||||
let region_baseline_c = region_profile::region_baseline_at_district(
|
||||
seed.seed(),
|
||||
body_id,
|
||||
@@ -1484,15 +1546,17 @@ pub fn derive_district(
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
region_baseline_c,
|
||||
min_wavelength_m,
|
||||
);
|
||||
|
||||
// derive_district is the on-demand path (arbitrary DistrictPos, no L1 working
|
||||
// grid). basin_direction is an ACCEPTED LIMITATION here: it defaults to North
|
||||
// (a fallback, not a computed value) because the D8 thalweg is only available
|
||||
// from the L1 fdir grid the batch path holds. Production voxel generation runs
|
||||
// through the batch path (derive_all_districts), which threads the true D8
|
||||
// direction from L1; this on-demand path is the fallback for districts derived
|
||||
// outside that pass, where a meaningful basin_direction isn't available.
|
||||
// derive_at_metres is the on-demand path (arbitrary world position, no L1
|
||||
// working grid). basin_direction is an ACCEPTED LIMITATION here: it
|
||||
// defaults to North (a fallback, not a computed value) because the D8
|
||||
// thalweg is only available from the L1 fdir grid the batch path holds.
|
||||
// Production voxel generation runs through the batch path
|
||||
// (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.
|
||||
build_district_profile(
|
||||
seed,
|
||||
¶ms,
|
||||
@@ -1857,6 +1921,152 @@ mod tests {
|
||||
assert!((0..=100).contains(&a.elev_q) && (0..=100).contains(&a.slope_q));
|
||||
}
|
||||
|
||||
// --- derive_at_metres (T-1149 keystone extraction) -------------------------
|
||||
|
||||
/// `derive_district` is a thin wrapper: at an exact district-aligned metre
|
||||
/// position, with `min_wavelength_m = 0.0`, it must be BIT-IDENTICAL to
|
||||
/// calling `derive_at_metres` directly (the acceptance criterion the
|
||||
/// ticket names explicitly — existing callers see byte-identical output).
|
||||
#[test]
|
||||
fn derive_at_metres_matches_derive_district_at_aligned_position_zero_cutoff() {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = earth_params();
|
||||
let dp = (1234, -567);
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
|
||||
let via_wrapper = derive_district(test_seed(), "test_body", &p, &ta, dp, &climate);
|
||||
let via_metres = derive_at_metres(
|
||||
test_seed(),
|
||||
"test_body",
|
||||
&p,
|
||||
&ta,
|
||||
dp.0 as f64 * dm,
|
||||
dp.1 as f64 * dm,
|
||||
&climate,
|
||||
0.0,
|
||||
);
|
||||
assert_district_profiles_eq(&via_wrapper, &via_metres);
|
||||
}
|
||||
|
||||
/// Same equivalence check on the no-radius (tiny test body) branch — the
|
||||
/// two derivation paths diverge internally (fractional-pixel clamp vs.
|
||||
/// direct district indexing) and must be checked independently.
|
||||
#[test]
|
||||
fn derive_at_metres_matches_derive_district_no_radius() {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = BodyParams {
|
||||
planet_class: Some("temperate".into()),
|
||||
atmosphere: Some("breathable".into()),
|
||||
..Default::default() // body_radius_km: None
|
||||
};
|
||||
let dp = (20, 10);
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
|
||||
let via_wrapper = derive_district(test_seed(), "test_body", &p, &ta, dp, &climate);
|
||||
let via_metres = derive_at_metres(
|
||||
test_seed(),
|
||||
"test_body",
|
||||
&p,
|
||||
&ta,
|
||||
dp.0 as f64 * dm,
|
||||
dp.1 as f64 * dm,
|
||||
&climate,
|
||||
0.0,
|
||||
);
|
||||
assert_district_profiles_eq(&via_wrapper, &via_metres);
|
||||
}
|
||||
|
||||
/// Field-by-field `DistrictProfile` equality — the struct has no
|
||||
/// `PartialEq` derive (production type, not test-only), so the
|
||||
/// bit-identical acceptance checks above compare fields directly instead
|
||||
/// of adding a derive to non-test code for test convenience.
|
||||
fn assert_district_profiles_eq(a: &DistrictProfile, b: &DistrictProfile) {
|
||||
assert_eq!(a.morphology_zone as u8, b.morphology_zone as u8);
|
||||
assert_eq!(a.tectonic_class as u8, b.tectonic_class as u8);
|
||||
assert_eq!(a.glaciation_grade as u8, b.glaciation_grade as u8);
|
||||
assert_eq!(a.precipitation_class as u8, b.precipitation_class as u8);
|
||||
assert_eq!(a.slope_q, b.slope_q);
|
||||
assert_eq!(a.elev_q, b.elev_q);
|
||||
assert_eq!(a.ocean_fraction_q, b.ocean_fraction_q);
|
||||
assert_eq!(a.river_threshold, b.river_threshold);
|
||||
assert_eq!(a.temperature_c, b.temperature_c);
|
||||
assert_eq!(a.moisture_q, b.moisture_q);
|
||||
assert_eq!(a.vegetation_class as u8, b.vegetation_class as u8);
|
||||
assert_eq!(a.basin_direction as u8, b.basin_direction as u8);
|
||||
}
|
||||
|
||||
/// A non-district-aligned fractional metre position (e.g. a quarter-grid
|
||||
/// sample, T-1150) must derive without panicking and stay within the same
|
||||
/// value ranges as the district-aligned case — the whole point of the
|
||||
/// extraction is that ANY fractional world position is now legal input,
|
||||
/// not just integer DistrictPos multiples.
|
||||
#[test]
|
||||
fn derive_at_metres_accepts_fractional_sub_district_position() {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = earth_params();
|
||||
let dm = scale::DISTRICT_M as f64;
|
||||
|
||||
// A quarter-grid offset (512 m, D-243) inside district (1234, -567).
|
||||
let prof = derive_at_metres(
|
||||
test_seed(),
|
||||
"test_body",
|
||||
&p,
|
||||
&ta,
|
||||
1234.0 * dm + 512.0,
|
||||
-567.0 * dm + 512.0,
|
||||
&climate,
|
||||
512.0,
|
||||
);
|
||||
assert!((0..=100).contains(&prof.elev_q));
|
||||
assert!((0..=100).contains(&prof.slope_q));
|
||||
}
|
||||
|
||||
/// A `min_wavelength_m` cutoff must actually change the invented terrain
|
||||
/// primitives relative to the uncut (0.0) derive at the SAME position —
|
||||
/// otherwise the parameter would be silently inert at this layer (the
|
||||
/// enveloped_fbm-level test already covers the raw scatter function; this
|
||||
/// confirms the wiring survives through invent_primitives/derive_at_metres).
|
||||
#[test]
|
||||
fn derive_at_metres_cutoff_changes_invented_primitives() {
|
||||
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 = (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 cut = derive_at_metres(
|
||||
test_seed(),
|
||||
"test_body",
|
||||
&p,
|
||||
&ta,
|
||||
wx,
|
||||
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;
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
any_differs,
|
||||
"a mid-band min_wavelength_m cutoff must change invented terrain \
|
||||
at at least one sampled position"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_district_profile_is_deterministic() {
|
||||
let hm = test_hm();
|
||||
|
||||
Reference in New Issue
Block a user