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:
@@ -382,7 +382,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let ch = coast_character_at(&env, 42, 1e6, 1e6, 20.0, GlaciationGrade::None, 60);
|
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);
|
||||||
let scatter = crate::atlas::detail_scatter::terrain_detail(42, 1e6, 1e6, 1.0, 0.5);
|
let scatter = crate::atlas::detail_scatter::terrain_detail(42, 1e6, 1e6, 1.0, 0.5, 0.0);
|
||||||
assert_ne!(wdx, scatter);
|
assert_ne!(wdx, scatter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,29 @@ pub(crate) fn value_noise(seed: u64, wx: f64, wy: f64, wavelength_m: f64) -> f64
|
|||||||
///
|
///
|
||||||
/// Returns roughly `[-envelope, +envelope]`, smaller and smoother as `ruggedness`
|
/// Returns roughly `[-envelope, +envelope]`, smaller and smoother as `ruggedness`
|
||||||
/// drops toward 0 (gentle flats), larger and ridged as it rises toward 1.
|
/// drops toward 0 (gentle flats), larger and ridged as it rises toward 1.
|
||||||
pub fn terrain_detail(seed: u64, wx: f64, wy: f64, envelope: f64, ruggedness: f64) -> f64 {
|
///
|
||||||
enveloped_fbm(seed, wx, wy, envelope, ruggedness, &OCTAVE_WAVELENGTHS_M)
|
/// `min_wavelength_m` (T-1149, zoom ladder §2): octaves whose wavelength is
|
||||||
|
/// below this cutoff are skipped entirely — the Nyquist truncation a coarse
|
||||||
|
/// sample density needs (no point paying for detail finer than the sample
|
||||||
|
/// spacing can resolve). `0.0` = no cutoff = every octave, byte-identical to
|
||||||
|
/// pre-T-1149 behavior.
|
||||||
|
pub fn terrain_detail(
|
||||||
|
seed: u64,
|
||||||
|
wx: f64,
|
||||||
|
wy: f64,
|
||||||
|
envelope: f64,
|
||||||
|
ruggedness: f64,
|
||||||
|
min_wavelength_m: f64,
|
||||||
|
) -> f64 {
|
||||||
|
enveloped_fbm(
|
||||||
|
seed,
|
||||||
|
wx,
|
||||||
|
wy,
|
||||||
|
envelope,
|
||||||
|
ruggedness,
|
||||||
|
&OCTAVE_WAVELENGTHS_M,
|
||||||
|
min_wavelength_m,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The voxel-tier mid-scale relief perturbation in `[0,1]`-normalized units
|
/// The voxel-tier mid-scale relief perturbation in `[0,1]`-normalized units
|
||||||
@@ -91,7 +112,16 @@ pub fn terrain_detail(seed: u64, wx: f64, wy: f64, envelope: f64, ruggedness: f6
|
|||||||
/// Seed must be **body-global** (constant across the body) — the position
|
/// Seed must be **body-global** (constant across the body) — the position
|
||||||
/// `(wx, wy)` carries the variation. A per-voxel seed would make every voxel a
|
/// `(wx, wy)` carries the variation. A per-voxel seed would make every voxel a
|
||||||
/// fresh lattice (white noise, not smooth hills).
|
/// fresh lattice (white noise, not smooth hills).
|
||||||
pub fn voxel_relief(seed: u64, wx: f64, wy: f64, envelope: f64, ruggedness: f64) -> f64 {
|
///
|
||||||
|
/// `min_wavelength_m` — see [`terrain_detail`]; `0.0` = no cutoff.
|
||||||
|
pub fn voxel_relief(
|
||||||
|
seed: u64,
|
||||||
|
wx: f64,
|
||||||
|
wy: f64,
|
||||||
|
envelope: f64,
|
||||||
|
ruggedness: f64,
|
||||||
|
min_wavelength_m: f64,
|
||||||
|
) -> f64 {
|
||||||
enveloped_fbm(
|
enveloped_fbm(
|
||||||
seed,
|
seed,
|
||||||
wx,
|
wx,
|
||||||
@@ -99,6 +129,7 @@ pub fn voxel_relief(seed: u64, wx: f64, wy: f64, envelope: f64, ruggedness: f64)
|
|||||||
envelope,
|
envelope,
|
||||||
ruggedness,
|
ruggedness,
|
||||||
&VOXEL_OCTAVE_WAVELENGTHS_M,
|
&VOXEL_OCTAVE_WAVELENGTHS_M,
|
||||||
|
min_wavelength_m,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,6 +165,23 @@ pub fn voxel_mosaic(seed: u64, wx: f64, wy: f64) -> f64 {
|
|||||||
/// Shared adaptive-fBm core for [`terrain_detail`] and [`voxel_relief`] — the only
|
/// Shared adaptive-fBm core for [`terrain_detail`] and [`voxel_relief`] — the only
|
||||||
/// difference between the two tiers is the octave wavelength band. Returns the
|
/// difference between the two tiers is the octave wavelength band. Returns the
|
||||||
/// enveloped, ruggedness-modulated perturbation (roughly `[-envelope, +envelope]`).
|
/// enveloped, ruggedness-modulated perturbation (roughly `[-envelope, +envelope]`).
|
||||||
|
///
|
||||||
|
/// `min_wavelength_m` (T-1149): octaves with `wl < min_wavelength_m` are
|
||||||
|
/// skipped — HARD TRUNCATE, not amplitude-faded. `amp`/`norm` still advance
|
||||||
|
/// through the skipped octave's weight step (`i` keeps its position in the
|
||||||
|
/// wavelength array for the seed-salt term), so the surviving octaves keep
|
||||||
|
/// their same relative weighting as if the truncated tail were simply cut
|
||||||
|
/// off the sum, not renormalized against a smaller octave count. `0.0` = no
|
||||||
|
/// octave is ever skipped = today's behavior, byte-for-byte.
|
||||||
|
///
|
||||||
|
/// **Seam (not built, see T-1149/design doc §2 + §9 R1):** a hard truncate can
|
||||||
|
/// pop when the sample density crosses an octave boundary between two
|
||||||
|
/// requests (an octave present at one zoom step vanishes at the next,
|
||||||
|
/// discontinuously). The documented fix is fading the highest surviving
|
||||||
|
/// octave's amplitude toward zero as `wl` approaches `min_wavelength_m` from
|
||||||
|
/// above, rather than an on/off cut. Not implemented here — it needs a
|
||||||
|
/// visual A/B against a real client zoom ladder, which does not exist yet
|
||||||
|
/// (T-1150/T-1153); building it speculatively risks tuning against nothing.
|
||||||
fn enveloped_fbm(
|
fn enveloped_fbm(
|
||||||
seed: u64,
|
seed: u64,
|
||||||
wx: f64,
|
wx: f64,
|
||||||
@@ -141,6 +189,7 @@ fn enveloped_fbm(
|
|||||||
envelope: f64,
|
envelope: f64,
|
||||||
ruggedness: f64,
|
ruggedness: f64,
|
||||||
wavelengths: &[f64],
|
wavelengths: &[f64],
|
||||||
|
min_wavelength_m: f64,
|
||||||
) -> f64 {
|
) -> f64 {
|
||||||
let env = envelope.clamp(0.0, 1.0);
|
let env = envelope.clamp(0.0, 1.0);
|
||||||
let rug = ruggedness.clamp(0.0, 1.0);
|
let rug = ruggedness.clamp(0.0, 1.0);
|
||||||
@@ -152,6 +201,14 @@ fn enveloped_fbm(
|
|||||||
let mut amp = 1.0;
|
let mut amp = 1.0;
|
||||||
let mut norm = 0.0;
|
let mut norm = 0.0;
|
||||||
for (i, &wl) in wavelengths.iter().enumerate() {
|
for (i, &wl) in wavelengths.iter().enumerate() {
|
||||||
|
if wl < min_wavelength_m {
|
||||||
|
// Below the per-sample Nyquist cutoff — skip the term entirely
|
||||||
|
// (hard truncate), but still advance amp so later octaves (there
|
||||||
|
// are none finer in these const arrays, but the rule is general)
|
||||||
|
// keep their intended relative weight.
|
||||||
|
amp *= 0.5 + 0.35 * rug;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let mut n = value_noise(
|
let mut n = value_noise(
|
||||||
seed.wrapping_add((i as u64).wrapping_mul(0x1000)),
|
seed.wrapping_add((i as u64).wrapping_mul(0x1000)),
|
||||||
wx,
|
wx,
|
||||||
@@ -170,6 +227,9 @@ fn enveloped_fbm(
|
|||||||
// them toward a single gentle swell.
|
// them toward a single gentle swell.
|
||||||
amp *= 0.5 + 0.35 * rug;
|
amp *= 0.5 + 0.35 * rug;
|
||||||
}
|
}
|
||||||
|
if norm == 0.0 {
|
||||||
|
return 0.0; // every octave cut by the cutoff → no invented relief left
|
||||||
|
}
|
||||||
let fbm = sum / norm; // ≈ [-1, 1]
|
let fbm = sum / norm; // ≈ [-1, 1]
|
||||||
|
|
||||||
// Envelope caps the amplitude; within the cap, ruggedness scales how much of
|
// Envelope caps the amplitude; within the cap, ruggedness scales how much of
|
||||||
@@ -183,8 +243,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deterministic() {
|
fn deterministic() {
|
||||||
let a = terrain_detail(42, 12_345.0, -6_789.0, 0.6, 0.5);
|
let a = terrain_detail(42, 12_345.0, -6_789.0, 0.6, 0.5, 0.0);
|
||||||
let b = terrain_detail(42, 12_345.0, -6_789.0, 0.6, 0.5);
|
let b = terrain_detail(42, 12_345.0, -6_789.0, 0.6, 0.5, 0.0);
|
||||||
assert_eq!(a, b);
|
assert_eq!(a, b);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +253,7 @@ mod tests {
|
|||||||
// envelope = 0 → the authored heightmap is flat here → no relief (the
|
// envelope = 0 → the authored heightmap is flat here → no relief (the
|
||||||
// envelope rule: never sprout terrain on an authored plain).
|
// envelope rule: never sprout terrain on an authored plain).
|
||||||
for &rug in &[0.0, 0.5, 1.0] {
|
for &rug in &[0.0, 0.5, 1.0] {
|
||||||
assert_eq!(terrain_detail(7, 1000.0, 2000.0, 0.0, rug), 0.0);
|
assert_eq!(terrain_detail(7, 1000.0, 2000.0, 0.0, rug, 0.0), 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +263,7 @@ mod tests {
|
|||||||
for i in 0..400 {
|
for i in 0..400 {
|
||||||
let wx = (i as f64) * 137.0;
|
let wx = (i as f64) * 137.0;
|
||||||
let wy = (i as f64) * -91.0;
|
let wy = (i as f64) * -91.0;
|
||||||
let v = terrain_detail(99, wx, wy, 0.5, 1.0);
|
let v = terrain_detail(99, wx, wy, 0.5, 1.0, 0.0);
|
||||||
assert!(v.abs() <= 0.5 + 1e-9, "v={v} exceeded envelope at {i}");
|
assert!(v.abs() <= 0.5 + 1e-9, "v={v} exceeded envelope at {i}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,7 +275,7 @@ mod tests {
|
|||||||
let mean_abs = |rug: f64| -> f64 {
|
let mean_abs = |rug: f64| -> f64 {
|
||||||
let n = 500;
|
let n = 500;
|
||||||
(0..n)
|
(0..n)
|
||||||
.map(|i| terrain_detail(3, i as f64 * 53.0, i as f64 * 71.0, 0.7, rug).abs())
|
.map(|i| terrain_detail(3, i as f64 * 53.0, i as f64 * 71.0, 0.7, rug, 0.0).abs())
|
||||||
.sum::<f64>()
|
.sum::<f64>()
|
||||||
/ n as f64
|
/ n as f64
|
||||||
};
|
};
|
||||||
@@ -229,8 +289,8 @@ mod tests {
|
|||||||
fn continuous_no_creases() {
|
fn continuous_no_creases() {
|
||||||
// Small position steps produce small output changes (C¹ value noise) — no
|
// Small position steps produce small output changes (C¹ value noise) — no
|
||||||
// lattice creases that would read as grid artifacts.
|
// lattice creases that would read as grid artifacts.
|
||||||
let base = terrain_detail(11, 5_000.0, 5_000.0, 0.8, 0.6);
|
let base = terrain_detail(11, 5_000.0, 5_000.0, 0.8, 0.6, 0.0);
|
||||||
let near = terrain_detail(11, 5_000.5, 5_000.0, 0.8, 0.6);
|
let near = terrain_detail(11, 5_000.5, 5_000.0, 0.8, 0.6, 0.0);
|
||||||
assert!(
|
assert!(
|
||||||
(base - near).abs() < 0.05,
|
(base - near).abs() < 0.05,
|
||||||
"0.5 m step jumped by {}",
|
"0.5 m step jumped by {}",
|
||||||
@@ -238,17 +298,56 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── min_wavelength_m cutoff (T-1149, zoom ladder §2) ──────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cutoff_zero_matches_pre_t1149_behavior() {
|
||||||
|
// 0.0 = no cutoff = every octave — this is the compatibility contract
|
||||||
|
// every existing caller (derive_district's default) relies on.
|
||||||
|
for i in 0..200 {
|
||||||
|
let wx = i as f64 * 91.0;
|
||||||
|
let wy = i as f64 * -53.0;
|
||||||
|
let with_explicit_zero = terrain_detail(21, wx, wy, 0.6, 0.5, 0.0);
|
||||||
|
// The finest OCTAVE_WAVELENGTHS_M entry is 4_096.0 — a cutoff below
|
||||||
|
// that admits every octave too, and must agree exactly.
|
||||||
|
let with_below_finest = terrain_detail(21, wx, wy, 0.6, 0.5, 1.0);
|
||||||
|
assert_eq!(with_explicit_zero, with_below_finest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cutoff_truncates_octaves_below_it() {
|
||||||
|
// A cutoff placed above the coarsest OCTAVE_WAVELENGTHS_M entry
|
||||||
|
// (32_768.0) must skip every octave and fall back to 0.0 (the
|
||||||
|
// norm==0.0 empty-sum guard), same as the flat_envelope_invents_nothing
|
||||||
|
// envelope==0 case but reached via the cutoff instead.
|
||||||
|
assert_eq!(terrain_detail(5, 1_000.0, 2_000.0, 0.6, 0.5, 100_000.0), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cutoff_changes_output_relative_to_uncut() {
|
||||||
|
// A mid-band cutoff (drops the two finest octaves: 8_192.0, 4_096.0)
|
||||||
|
// must produce DIFFERENT output than the uncut derive at the same
|
||||||
|
// position — otherwise the cutoff parameter would be a no-op.
|
||||||
|
let uncut = terrain_detail(17, 12_000.0, 9_000.0, 0.7, 0.6, 0.0);
|
||||||
|
let cut = terrain_detail(17, 12_000.0, 9_000.0, 0.7, 0.6, 8_193.0);
|
||||||
|
assert_ne!(
|
||||||
|
uncut, cut,
|
||||||
|
"a mid-band cutoff must change the derived output"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── voxel_relief (T-1081): same contract, sub-district band ──────────────
|
// ── voxel_relief (T-1081): same contract, sub-district band ──────────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn voxel_relief_deterministic_and_bounded() {
|
fn voxel_relief_deterministic_and_bounded() {
|
||||||
let a = voxel_relief(42, 12_345.0, -6_789.0, 0.6, 0.5);
|
let a = voxel_relief(42, 12_345.0, -6_789.0, 0.6, 0.5, 0.0);
|
||||||
let b = voxel_relief(42, 12_345.0, -6_789.0, 0.6, 0.5);
|
let b = voxel_relief(42, 12_345.0, -6_789.0, 0.6, 0.5, 0.0);
|
||||||
assert_eq!(a, b);
|
assert_eq!(a, b);
|
||||||
// Same envelope rule + amplitude ceiling as terrain_detail.
|
// Same envelope rule + amplitude ceiling as terrain_detail.
|
||||||
assert_eq!(voxel_relief(7, 1_000.0, 2_000.0, 0.0, 1.0), 0.0);
|
assert_eq!(voxel_relief(7, 1_000.0, 2_000.0, 0.0, 1.0, 0.0), 0.0);
|
||||||
for i in 0..400 {
|
for i in 0..400 {
|
||||||
let v = voxel_relief(99, i as f64 * 137.0, i as f64 * -91.0, 0.5, 1.0);
|
let v = voxel_relief(99, i as f64 * 137.0, i as f64 * -91.0, 0.5, 1.0, 0.0);
|
||||||
assert!(v.abs() <= 0.5 + 1e-9, "v={v} exceeded envelope at {i}");
|
assert!(v.abs() <= 0.5 + 1e-9, "v={v} exceeded envelope at {i}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,7 +360,7 @@ mod tests {
|
|||||||
// are deliberately non-aligned with the octave wavelengths to avoid aliasing.
|
// are deliberately non-aligned with the octave wavelengths to avoid aliasing.
|
||||||
let seed = 1234;
|
let seed = 1234;
|
||||||
let vals: Vec<f64> = (0..16)
|
let vals: Vec<f64> = (0..16)
|
||||||
.map(|i| voxel_relief(seed, i as f64 * 137.0, i as f64 * 89.0, 0.7, 0.6))
|
.map(|i| voxel_relief(seed, i as f64 * 137.0, i as f64 * 89.0, 0.7, 0.6, 0.0))
|
||||||
.collect();
|
.collect();
|
||||||
let min = vals.iter().cloned().fold(f64::INFINITY, f64::min);
|
let min = vals.iter().cloned().fold(f64::INFINITY, f64::min);
|
||||||
let max = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
let max = vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||||
|
|||||||
@@ -1088,6 +1088,10 @@ struct InventedPrimitives {
|
|||||||
/// low-relief coasts); `ridge` carries fjord/tectonic sharpness.
|
/// low-relief coasts); `ridge` carries fjord/tectonic sharpness.
|
||||||
///
|
///
|
||||||
/// `body_params` must already carry the district's `latitude_deg`.
|
/// `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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn invent_primitives(
|
fn invent_primitives(
|
||||||
seed: SeedChain,
|
seed: SeedChain,
|
||||||
@@ -1099,6 +1103,7 @@ fn invent_primitives(
|
|||||||
world_x_m: f64,
|
world_x_m: f64,
|
||||||
world_y_m: f64,
|
world_y_m: f64,
|
||||||
region_baseline_c: Option<f32>,
|
region_baseline_c: Option<f32>,
|
||||||
|
min_wavelength_m: f64,
|
||||||
) -> InventedPrimitives {
|
) -> InventedPrimitives {
|
||||||
// ── 1. Driver tier: UNWARPED raw-bilinear climate (one-step-stale). ─────
|
// ── 1. Driver tier: UNWARPED raw-bilinear climate (one-step-stale). ─────
|
||||||
let raw_elev_q =
|
let raw_elev_q =
|
||||||
@@ -1147,6 +1152,7 @@ fn invent_primitives(
|
|||||||
world_y_m,
|
world_y_m,
|
||||||
env_amp,
|
env_amp,
|
||||||
ruggedness,
|
ruggedness,
|
||||||
|
min_wavelength_m,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Shoreline carving (T-1125): glacial / tectonically-young SHORES are cut
|
// Shoreline carving (T-1125): glacial / tectonically-young SHORES are cut
|
||||||
@@ -1272,6 +1278,7 @@ pub fn derive_district_profile(
|
|||||||
world_x_m,
|
world_x_m,
|
||||||
world_y_m,
|
world_y_m,
|
||||||
region_baseline_c,
|
region_baseline_c,
|
||||||
|
0.0, // batch path — no octave cutoff, matches derive_district's default
|
||||||
);
|
);
|
||||||
|
|
||||||
build_district_profile(
|
build_district_profile(
|
||||||
@@ -1421,15 +1428,55 @@ pub fn derive_district(
|
|||||||
climate: &ClimateConstants,
|
climate: &ClimateConstants,
|
||||||
) -> DistrictProfile {
|
) -> DistrictProfile {
|
||||||
let (dx, dy) = district_pos;
|
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 {
|
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 => {
|
Some(r_km) if r_km > 0.0 => {
|
||||||
let circumference_m = std::f64::consts::TAU * r_km * 1000.0;
|
let circumference_m = std::f64::consts::TAU * r_km * 1000.0;
|
||||||
let meridian_m = std::f64::consts::PI * 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;
|
// Longitude wraps; (0,0) sits at lon 0 / the equator.
|
||||||
let wy = dy as f64 * scale::DISTRICT_M as f64;
|
|
||||||
// Longitude wraps; district (0,0) sits at lon 0 / the equator.
|
|
||||||
let px = (wx / circumference_m).rem_euclid(1.0) * ta.w as f64;
|
let px = (wx / circumference_m).rem_euclid(1.0) * ta.w as f64;
|
||||||
// Latitude: equator at py = h/2, clamped at the poles.
|
// 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
|
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)
|
(px, py, wx, wy, -lat_frac * 180.0)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// No radius: the district grid IS the heightmap grid (tiny test bodies).
|
// No radius: the working grid IS the metre grid (tiny test bodies),
|
||||||
let px = (dx as f64).clamp(0.0, ta.w.saturating_sub(1) as f64);
|
// 1 DISTRICT_M = 1 heightmap pixel — the inverse of
|
||||||
let py = (dy as f64).clamp(0.0, ta.h.saturating_sub(1) as f64);
|
// `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 {
|
let lat_deg = if ta.h > 1 {
|
||||||
90.0 - (py / (ta.h - 1) as f64) * 180.0
|
90.0 - (py / (ta.h - 1) as f64) * 180.0
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
let dm = scale::DISTRICT_M as f64;
|
|
||||||
(px, py, px * dm, py * dm, lat_deg)
|
(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
|
// 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
|
// this position. No pre-built cache here — the on-demand path derives the
|
||||||
// surrounding region baselines directly. Pure, deterministic, cheap.
|
// four surrounding region baselines directly. Pure, deterministic, cheap.
|
||||||
// `seed.seed()` (the body-scoped seed value) ensures body-unique warp separation.
|
// `seed.seed()` (the body-scoped seed value) ensures body-unique warp separation.
|
||||||
// Hoisted above the primitives (T-1125): the invention's driver tier needs
|
// Hoisted above the primitives (T-1125): the invention's driver tier needs
|
||||||
// the baseline for its one-step-stale climate estimate.
|
// 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(
|
let region_baseline_c = region_profile::region_baseline_at_district(
|
||||||
seed.seed(),
|
seed.seed(),
|
||||||
body_id,
|
body_id,
|
||||||
@@ -1484,15 +1546,17 @@ pub fn derive_district(
|
|||||||
world_x_m,
|
world_x_m,
|
||||||
world_y_m,
|
world_y_m,
|
||||||
region_baseline_c,
|
region_baseline_c,
|
||||||
|
min_wavelength_m,
|
||||||
);
|
);
|
||||||
|
|
||||||
// derive_district is the on-demand path (arbitrary DistrictPos, no L1 working
|
// derive_at_metres is the on-demand path (arbitrary world position, no L1
|
||||||
// grid). basin_direction is an ACCEPTED LIMITATION here: it defaults to North
|
// working grid). basin_direction is an ACCEPTED LIMITATION here: it
|
||||||
// (a fallback, not a computed value) because the D8 thalweg is only available
|
// defaults to North (a fallback, not a computed value) because the D8
|
||||||
// from the L1 fdir grid the batch path holds. Production voxel generation runs
|
// thalweg is only available from the L1 fdir grid the batch path holds.
|
||||||
// through the batch path (derive_all_districts), which threads the true D8
|
// Production voxel generation runs through the batch path
|
||||||
// direction from L1; this on-demand path is the fallback for districts derived
|
// (derive_all_districts), which threads the true D8 direction from L1;
|
||||||
// outside that pass, where a meaningful basin_direction isn't available.
|
// this on-demand path is the fallback for positions derived outside that
|
||||||
|
// pass, where a meaningful basin_direction isn't available.
|
||||||
build_district_profile(
|
build_district_profile(
|
||||||
seed,
|
seed,
|
||||||
¶ms,
|
¶ms,
|
||||||
@@ -1857,6 +1921,152 @@ mod tests {
|
|||||||
assert!((0..=100).contains(&a.elev_q) && (0..=100).contains(&a.slope_q));
|
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]
|
#[test]
|
||||||
fn derive_district_profile_is_deterministic() {
|
fn derive_district_profile_is_deterministic() {
|
||||||
let hm = test_hm();
|
let hm = test_hm();
|
||||||
|
|||||||
@@ -511,6 +511,7 @@ pub fn derive_voxel_column(
|
|||||||
voxel_y as f64,
|
voxel_y as f64,
|
||||||
signal,
|
signal,
|
||||||
signal,
|
signal,
|
||||||
|
0.0, // voxel fill path — no octave cutoff (T-1149's cutoff is Atlas-serving only)
|
||||||
);
|
);
|
||||||
let relief_m = (relief * VOXEL_RELIEF_SPAN_M as f64) as i32; // truncate (D-010)
|
let relief_m = (relief * VOXEL_RELIEF_SPAN_M as f64) as i32; // truncate (D-010)
|
||||||
column.elevation_m = (column.elevation_m + relief_m).max(0);
|
column.elevation_m = (column.elevation_m + relief_m).max(0);
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
//! Zoom-ladder derivation benchmarks (T-1149, design doc §2/§7/§8 step 1).
|
||||||
|
//!
|
||||||
|
//! Measures `derive_at_metres` per-cell cost at district spacing (2,048 m) and
|
||||||
|
//! quarter spacing (512 m), with and without a `min_wavelength_m` octave
|
||||||
|
//! cutoff — the exact numbers the design doc flags as UNBUILT/UNMEASURED
|
||||||
|
//! (§7: "Octave-cutoff derive (min_wavelength_m-bearing) ... not measured").
|
||||||
|
//!
|
||||||
|
//! Manual `Instant`-based timing, matching every other bench in this repo
|
||||||
|
//! (`shadowcast_bench.rs`, `perf_bench.rs`) and the same technique
|
||||||
|
//! `aliveness_probe --render` used to produce the ~1.2–1.4 µs/district
|
||||||
|
//! release figure the design doc cites — no criterion dependency exists here.
|
||||||
|
//!
|
||||||
|
//! Run: `cargo test --release --test zoom_ladder_bench -- --ignored --nocapture`
|
||||||
|
//! (debug numbers are ~5x slower and not representative of the design doc's
|
||||||
|
//! release-build figures; run `--release` for numbers worth recording).
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
use settled_reach_server::atlas::district_profile::{
|
||||||
|
derive_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};
|
||||||
|
|
||||||
|
fn bench_hm() -> BodyHeightmap {
|
||||||
|
// Same shape as district_profile.rs's own test_hm/window_test_hm fixtures
|
||||||
|
// — a smooth gradient, deterministic, no PNG I/O.
|
||||||
|
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;
|
||||||
|
(r * 0.6 + c * 0.4).min(1.0)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
BodyHeightmap {
|
||||||
|
body_id: "bench".into(),
|
||||||
|
width: w,
|
||||||
|
height: h,
|
||||||
|
data,
|
||||||
|
sea_level: 0.3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_ta(hm: &BodyHeightmap) -> TerrainAnalysis {
|
||||||
|
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
|
||||||
|
TerrainAnalysis::analyze(hm, &dr)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_params() -> BodyParams {
|
||||||
|
BodyParams {
|
||||||
|
hydrosphere: Some("ocean".into()),
|
||||||
|
atmosphere: Some("breathable".into()),
|
||||||
|
planet_class: Some("temperate".into()),
|
||||||
|
body_radius_km: Some(6371.0),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Time `n_cells` sequential `derive_at_metres` calls on a spacing-`step_m`
|
||||||
|
/// grid starting at world origin, with the given octave cutoff. Returns
|
||||||
|
/// (total_elapsed, per_cell_ns).
|
||||||
|
fn time_derive_sweep(
|
||||||
|
seed: SeedChain,
|
||||||
|
body_id: &str,
|
||||||
|
params: &BodyParams,
|
||||||
|
ta: &TerrainAnalysis,
|
||||||
|
climate: &ClimateConstants,
|
||||||
|
grid_side: u32,
|
||||||
|
step_m: f64,
|
||||||
|
min_wavelength_m: f64,
|
||||||
|
) -> (std::time::Duration, f64) {
|
||||||
|
let n_cells = (grid_side * grid_side) as u64;
|
||||||
|
let t0 = Instant::now();
|
||||||
|
for row in 0..grid_side {
|
||||||
|
for col in 0..grid_side {
|
||||||
|
let wx = col as f64 * step_m;
|
||||||
|
let wy = row as f64 * step_m;
|
||||||
|
let prof = derive_at_metres(
|
||||||
|
seed,
|
||||||
|
body_id,
|
||||||
|
params,
|
||||||
|
ta,
|
||||||
|
wx,
|
||||||
|
wy,
|
||||||
|
climate,
|
||||||
|
min_wavelength_m,
|
||||||
|
);
|
||||||
|
// Prevent the optimizer from hoisting the call out of the loop.
|
||||||
|
std::hint::black_box(prof.elev_q);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let elapsed = t0.elapsed();
|
||||||
|
let per_cell_ns = elapsed.as_secs_f64() * 1e9 / n_cells as f64;
|
||||||
|
(elapsed, per_cell_ns)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn bench_derive_at_metres_district_and_quarter_spacing() {
|
||||||
|
let hm = bench_hm();
|
||||||
|
let ta = bench_ta(&hm);
|
||||||
|
let params = bench_params();
|
||||||
|
let climate = ClimateConstants::default();
|
||||||
|
let seed = SeedChain::root(99).derive(SeedDomain::Body, 1);
|
||||||
|
let grid_side = 64u32; // 4,096 cells per sweep — matches the D-226 window cap
|
||||||
|
|
||||||
|
println!("\n=== T-1149 zoom-ladder derive_at_metres benchmark ===");
|
||||||
|
println!(
|
||||||
|
"grid: {grid_side}x{grid_side} = {} cells/sweep\n",
|
||||||
|
grid_side * grid_side
|
||||||
|
);
|
||||||
|
|
||||||
|
let district_m = scale::DISTRICT_M as f64;
|
||||||
|
let quarter_m = scale::QUARTER_M as f64;
|
||||||
|
|
||||||
|
// District spacing (2,048 m), cutoff 0 — today's uncut behavior.
|
||||||
|
let (elapsed, per_cell_ns) = time_derive_sweep(
|
||||||
|
seed, "bench", ¶ms, &ta, &climate, grid_side, district_m, 0.0,
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"district spacing, cutoff=0: {:>8.2} ms total, {:>7.1} ns/cell ({:.3} µs/cell)",
|
||||||
|
elapsed.as_secs_f64() * 1000.0,
|
||||||
|
per_cell_ns,
|
||||||
|
per_cell_ns / 1000.0
|
||||||
|
);
|
||||||
|
|
||||||
|
// District spacing, cutoff 2,048 m — truncates every OCTAVE_WAVELENGTHS_M
|
||||||
|
// entry below the district's own spacing (finest is 4,096 m, so this
|
||||||
|
// cutoff is BELOW that — confirms the cutoff plumbing at district scale
|
||||||
|
// without changing which octaves survive, since 2,048 < 4,096 admits all
|
||||||
|
// of them; recorded for the design doc's requested (district, cutoff
|
||||||
|
// 2048) combination regardless).
|
||||||
|
let (elapsed, per_cell_ns) = time_derive_sweep(
|
||||||
|
seed,
|
||||||
|
"bench",
|
||||||
|
¶ms,
|
||||||
|
&ta,
|
||||||
|
&climate,
|
||||||
|
grid_side,
|
||||||
|
district_m,
|
||||||
|
2_048.0,
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"district spacing, cutoff=2048m: {:>8.2} ms total, {:>7.1} ns/cell ({:.3} µs/cell)",
|
||||||
|
elapsed.as_secs_f64() * 1000.0,
|
||||||
|
per_cell_ns,
|
||||||
|
per_cell_ns / 1000.0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Quarter spacing (512 m), cutoff 512 m — the T-1150 Option B rung: full
|
||||||
|
// reclassification at quarter spacing with the matching octave cutoff.
|
||||||
|
let (elapsed, per_cell_ns) = time_derive_sweep(
|
||||||
|
seed,
|
||||||
|
"bench",
|
||||||
|
¶ms,
|
||||||
|
&ta,
|
||||||
|
&climate,
|
||||||
|
grid_side,
|
||||||
|
quarter_m,
|
||||||
|
512.0,
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"quarter spacing, cutoff=512m: {:>8.2} ms total, {:>7.1} ns/cell ({:.3} µs/cell)",
|
||||||
|
elapsed.as_secs_f64() * 1000.0,
|
||||||
|
per_cell_ns,
|
||||||
|
per_cell_ns / 1000.0
|
||||||
|
);
|
||||||
|
|
||||||
|
println!();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user