fix(simulation): wire region baseline into production district temp + rename latitude field (T-1078)
Addresses PR #167 review (Tyre T1, T3). T1 — the region climate tier was built but no live path consulted it: both derive_district_profile (cascade) and derive_district (on-demand) passed region_baseline_c: None, so the D-243 §4 edge-fuzz blend was dead in production and district temperature was still derived from scratch. Now both paths compute region_baseline_at_district(...) and pass it through build_district_profile so the two-phase modulation (D-243 §3) actually runs. The cascade path (derive_all_districts) builds a BTreeMap<RegionPos, RegionProfile> covering all district positions + their +/-1 neighbours once per body and looks up from it; the on-demand path derives the four surrounding region baselines on the fly. No production path passes None except genuinely airless bodies (no planet_class). T3 — renamed BodyParams.district_latitude_deg -> latitude_deg (it is the region centre latitude in the region fns, a call-site trap under the old name); dual-scale doc; propagated to body_params_reader.rs and all literal sites. Golden fixtures are unaffected (they construct districts directly via make_region, bypassing build_district_profile) — derivation_harness 37/37 green, no re-pin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
//!
|
||||
//! `tectonic_activity` is **not** in the current schema; `BodyParams.tectonic_activity`
|
||||
//! is left `None` so the derivation falls back to `planet_class` as documented
|
||||
//! on the struct. The per-district fields `district_latitude_deg` and `elevation_km`
|
||||
//! on the struct. The per-district fields `latitude_deg` and `elevation_km`
|
||||
//! are set by `derive_all_districts` / `derive_district_profile`, not here; they
|
||||
//! remain at their struct defaults (0.0) from this reader.
|
||||
//!
|
||||
@@ -121,7 +121,7 @@ impl BodyParamsReader {
|
||||
tectonic_activity: None,
|
||||
// Per-district fields are set by derive_all_districts / derive_district_profile,
|
||||
// not at the body level. Leave at struct defaults (0.0).
|
||||
district_latitude_deg: 0.0,
|
||||
latitude_deg: 0.0,
|
||||
elevation_km: 0.0,
|
||||
body_radius_km,
|
||||
}),
|
||||
@@ -216,7 +216,7 @@ mod tests {
|
||||
assert_eq!(params.planet_class.as_deref(), Some("temperate"));
|
||||
assert_eq!(params.body_radius_km, Some(6371.0));
|
||||
// Per-district fields always start at 0.0 from the reader.
|
||||
assert_eq!(params.district_latitude_deg, 0.0);
|
||||
assert_eq!(params.latitude_deg, 0.0);
|
||||
assert_eq!(params.elevation_km, 0.0);
|
||||
// tectonic_activity not in schema → None.
|
||||
assert!(params.tectonic_activity.is_none());
|
||||
|
||||
@@ -258,8 +258,15 @@ pub fn run_cascade_from_heightmap(
|
||||
// ~8 cells per district on a 128×64 working grid → ~80×32 = ~2 560 districts;
|
||||
// at full working resolution the budget is ~6 000/body (D-203).
|
||||
const CELLS_PER_REGION: usize = 8;
|
||||
let districts =
|
||||
district_profile::derive_all_districts(body_seed, params, &ta, CELLS_PER_REGION);
|
||||
// body_id is required for the D-243 §4 climate edge-fuzz warp domain
|
||||
// separation — derive_all_districts builds the region cache internally.
|
||||
let districts = district_profile::derive_all_districts(
|
||||
body_seed,
|
||||
params,
|
||||
&ta,
|
||||
CELLS_PER_REGION,
|
||||
&snapshot.body_id,
|
||||
);
|
||||
snapshot.layer_district = Some(LayerDistrictOutput { districts });
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,8 @@ use std::collections::BTreeMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::atlas::features::TerrainAnalysis;
|
||||
use crate::atlas::scale;
|
||||
use crate::atlas::region_profile::{self, RegionProfile};
|
||||
use crate::atlas::scale::{self, RegionPos};
|
||||
use crate::seed::SeedChain;
|
||||
use crate::simulation::generator::MorphologyZone;
|
||||
|
||||
@@ -148,9 +149,17 @@ pub struct BodyParams {
|
||||
/// "stable" | "active" | "volcanic" | "tidally_forced". If absent, derived
|
||||
/// from `planet_class`.
|
||||
pub tectonic_activity: Option<String>,
|
||||
/// Latitude of the district's centre in the body's reference frame, in degrees.
|
||||
/// Latitude of this cell's centre in the body's reference frame, in degrees.
|
||||
/// 0.0 = equator, ±90.0 = poles. Used for latitude-band temperature gradient.
|
||||
pub district_latitude_deg: f64,
|
||||
///
|
||||
/// This field serves at **both** the district (2 km) and region (~205 km) scales:
|
||||
/// when building a `DistrictProfile` it holds the district centre latitude; when
|
||||
/// passed to [`crate::atlas::region_profile::derive_region_baseline_c`] it is
|
||||
/// expected to carry the **region centre latitude** (callers override it via
|
||||
/// struct-update syntax before passing the params down). The name was changed
|
||||
/// from `latitude_deg` to `latitude_deg` (T-1078) to remove the
|
||||
/// misleading scale implication.
|
||||
pub latitude_deg: f64,
|
||||
/// Mean elevation of this district relative to sea level, in km. Used for lapse rate.
|
||||
pub elevation_km: f64,
|
||||
/// `bodies.body_radius_km` (D-204) — the body's radius in km. The single
|
||||
@@ -813,7 +822,7 @@ pub fn derive_temperature_c(
|
||||
let maritime = constants.maritime_factor(hydrosphere);
|
||||
let mid = (cold + warm) * 0.5;
|
||||
let half = band_width * 0.5 * maritime;
|
||||
let lat_frac = (params.district_latitude_deg.abs() as f32 / 90.0).clamp(0.0, 1.0);
|
||||
let lat_frac = (params.latitude_deg.abs() as f32 / 90.0).clamp(0.0, 1.0);
|
||||
// equator (frac 0) → mid + half; pole (frac 1) → mid − half.
|
||||
let t_lat = (mid + half) - (2.0 * half) * lat_frac;
|
||||
|
||||
@@ -975,9 +984,20 @@ pub fn derive_moisture_q(params: &BodyParams) -> i32 {
|
||||
/// cell; default is 8 (at 128×64 working grid, that yields ~80×64 districts ≈
|
||||
/// ~5 000 districts/body, within the D-203 ~6 000/body budget).
|
||||
///
|
||||
/// Temperature and moisture are derived inline via D-239 §2 / D-240 climate
|
||||
/// functions (T-1024). Pass a `&ClimateConstants` to control the tuning constants.
|
||||
/// The seed chain provides the body-scoped seed for the D-240 temperature nudge.
|
||||
/// Temperature and moisture are derived via the D-243 §3/§4 two-phase stack:
|
||||
/// the edge-fuzz-blended region baseline (from `region_cache` / on-the-fly
|
||||
/// derivation via [`region_profile::region_baseline_at_district`]) feeds the
|
||||
/// district modulation ([`derive_district_temperature_c`]). Pass a
|
||||
/// `&ClimateConstants` to control the tuning constants. The seed chain provides
|
||||
/// the body-scoped seed.
|
||||
///
|
||||
/// ## Parameters
|
||||
///
|
||||
/// - `body_id` — the body's string identifier; required for the D-243 §4 climate
|
||||
/// edge-fuzz warp domain separation (distinct bodies get distinct warps).
|
||||
/// - `region_cache` — pre-computed [`RegionProfile`] map keyed by [`RegionPos`];
|
||||
/// if a neighbour region is missing it is derived on the fly. Build with
|
||||
/// [`region_profile::derive_regions_for_body`] before calling this in a loop.
|
||||
pub fn derive_district_profile(
|
||||
seed: SeedChain,
|
||||
body_params: &BodyParams,
|
||||
@@ -985,6 +1005,8 @@ pub fn derive_district_profile(
|
||||
pos: DistrictPos,
|
||||
grid_cells_per_district: usize,
|
||||
climate: &ClimateConstants,
|
||||
body_id: &str,
|
||||
region_cache: &BTreeMap<RegionPos, RegionProfile>,
|
||||
) -> DistrictProfile {
|
||||
let (rx, ry) = pos;
|
||||
let w = ta.w;
|
||||
@@ -1025,9 +1047,19 @@ pub fn derive_district_profile(
|
||||
(0, 0, 0)
|
||||
};
|
||||
|
||||
// No region layer in the eager coarse-grid path — use legacy single-phase
|
||||
// temperature derivation (region baseline threading is wired in derive_district
|
||||
// for the on-demand 2 km path; T-1078).
|
||||
// D-243 §3/§4: compute the edge-fuzz-blended region baseline for this district,
|
||||
// then pass it through build_district_profile so the two-phase derivation path runs.
|
||||
// The warp uses `seed.seed()` (the body-scoped seed) for domain separation.
|
||||
let region_baseline_c = region_profile::region_baseline_at_district(
|
||||
seed.seed(),
|
||||
body_id,
|
||||
pos,
|
||||
body_params,
|
||||
climate,
|
||||
seed,
|
||||
Some(region_cache),
|
||||
);
|
||||
|
||||
build_district_profile(
|
||||
seed,
|
||||
body_params,
|
||||
@@ -1035,7 +1067,7 @@ pub fn derive_district_profile(
|
||||
slope_q,
|
||||
elev_q,
|
||||
ocean_fraction_q,
|
||||
None, // region_baseline_c: legacy path
|
||||
region_baseline_c,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1072,7 +1104,7 @@ fn build_district_profile(
|
||||
let tectonic_class = derive_tectonic_class(body_params);
|
||||
|
||||
// District-local BodyParams: elevation_km comes from the district's own
|
||||
// elev_q (0–100 scaled to the body's elevation span). district_latitude_deg
|
||||
// elev_q (0–100 scaled to the body's elevation span). latitude_deg
|
||||
// is already set per-district by the caller. Per-cell refinement at ChunkContext.
|
||||
let district_climate_params = BodyParams {
|
||||
elevation_km: (elev_q as f64 / 100.0) * MAX_REGION_ELEVATION_KM,
|
||||
@@ -1143,10 +1175,22 @@ fn build_district_profile(
|
||||
/// to direct heightmap indexing.
|
||||
///
|
||||
/// Pure and deterministic (D-227/D-010): a function of
|
||||
/// `(seed, body_params, terrain, district_pos)`; the f64 scatter is quantised to
|
||||
/// integer `slope_q`/`elev_q` at the decision boundary.
|
||||
/// `(seed, body_id, body_params, terrain, district_pos)`; the f64 scatter is
|
||||
/// quantised to integer `slope_q`/`elev_q` at the decision boundary.
|
||||
///
|
||||
/// ## Region baseline (D-243 §3/§4, T-1078)
|
||||
///
|
||||
/// The district temperature is derived as a **modulation** of the edge-fuzz-blended
|
||||
/// region baseline ([`region_profile::region_baseline_at_district`]). No pre-built
|
||||
/// region cache is required here — the on-demand path derives the four surrounding
|
||||
/// region baselines on-the-fly (pure, deterministic, cheap: four `derive_region_baseline_c`
|
||||
/// calls). For batch derivation of many districts use [`derive_all_districts`], which
|
||||
/// builds a region cache once per body.
|
||||
///
|
||||
/// `body_id` is required for the D-243 §4 climate edge-fuzz warp domain separation.
|
||||
pub fn derive_district(
|
||||
seed: SeedChain,
|
||||
body_id: &str,
|
||||
body_params: &BodyParams,
|
||||
ta: &TerrainAnalysis,
|
||||
district_pos: DistrictPos,
|
||||
@@ -1209,16 +1253,24 @@ pub fn derive_district(
|
||||
let ocean_fraction_q = ((ocean_frac * 100.0).round() as i32).clamp(0, 100);
|
||||
|
||||
let params = BodyParams {
|
||||
district_latitude_deg: lat_deg,
|
||||
latitude_deg: lat_deg,
|
||||
..body_params.clone()
|
||||
};
|
||||
// The on-demand derive_district path does not yet carry a pre-computed region
|
||||
// baseline (wiring the region cache into the per-district call site is the
|
||||
// production integration step; the structure is ready in region_profile.rs via
|
||||
// region_baseline_at_district). Pass None here → legacy single-phase derivation.
|
||||
// T-1078: production callers that have a region cache should call
|
||||
// build_district_profile(..., region_baseline_at_district(...))
|
||||
// directly instead.
|
||||
|
||||
// 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.
|
||||
// `seed.seed()` (the body-scoped seed value) ensures body-unique warp separation.
|
||||
let region_baseline_c = region_profile::region_baseline_at_district(
|
||||
seed.seed(),
|
||||
body_id,
|
||||
district_pos,
|
||||
¶ms,
|
||||
climate,
|
||||
seed,
|
||||
None, // no pre-built cache; derive on-the-fly
|
||||
);
|
||||
|
||||
build_district_profile(
|
||||
seed,
|
||||
¶ms,
|
||||
@@ -1226,7 +1278,7 @@ pub fn derive_district(
|
||||
slope_q,
|
||||
elev_q,
|
||||
ocean_fraction_q,
|
||||
None,
|
||||
region_baseline_c,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1291,17 +1343,53 @@ fn bilinear_bool(mask: &[bool], w: usize, h: usize, px: f64, py: f64) -> f32 {
|
||||
/// District latitude is derived from the row index: ry=0 maps to the north pole
|
||||
/// (+90°), ry=district_rows-1 maps to the south pole (-90°). This is a linear
|
||||
/// mapping across the equirectangular heightmap.
|
||||
///
|
||||
/// ## Region cache (D-243 §3/§4, T-1078)
|
||||
///
|
||||
/// A [`RegionProfile`] cache is built once per body from all region positions
|
||||
/// that cover the district grid, then passed to each [`derive_district_profile`]
|
||||
/// call so the D-243 §4 edge-fuzz blend reads a consistent set of region
|
||||
/// baselines — all four corners of every blend come from the same derived set.
|
||||
///
|
||||
/// `body_id` is the body's string identifier, required for the climate edge-fuzz
|
||||
/// warp domain separation.
|
||||
pub fn derive_all_districts(
|
||||
seed: SeedChain,
|
||||
body_params: &BodyParams,
|
||||
ta: &TerrainAnalysis,
|
||||
grid_cells_per_district: usize,
|
||||
body_id: &str,
|
||||
) -> BTreeMap<DistrictPos, DistrictProfile> {
|
||||
let climate = ClimateConstants::default();
|
||||
let gcpr = grid_cells_per_district.max(1);
|
||||
let district_cols = ta.w.div_ceil(gcpr) as i32;
|
||||
let district_rows = ta.h.div_ceil(gcpr) as i32;
|
||||
|
||||
// Build the region cache once for the whole body before district derivation.
|
||||
// Collect all unique region positions that cover this district grid, plus
|
||||
// their immediate neighbours (the edge-fuzz blend samples up to one region
|
||||
// beyond the district's own region). Using a BTreeSet for determinism (D-010).
|
||||
let region_positions: std::collections::BTreeSet<RegionPos> = {
|
||||
let mut set = std::collections::BTreeSet::new();
|
||||
for ry in 0..district_rows {
|
||||
for rx in 0..district_cols {
|
||||
let district_pos = (rx, ry);
|
||||
let rpos = scale::district_to_region(district_pos);
|
||||
// The edge-fuzz blend samples the base region and one neighbour
|
||||
// in each axis direction (±1). Pre-populate all 9 candidates so
|
||||
// cache hits dominate and on-the-fly derivations are rare.
|
||||
for dy in -1i32..=1 {
|
||||
for dx in -1i32..=1 {
|
||||
set.insert((rpos.0 + dx, rpos.1 + dy));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
set
|
||||
};
|
||||
let region_cache =
|
||||
region_profile::derive_regions_for_body(seed, body_params, &climate, region_positions);
|
||||
|
||||
let mut out = BTreeMap::new();
|
||||
for ry in 0..district_rows {
|
||||
// Map ry to latitude: row 0 → +90°, row (rows-1) → -90°.
|
||||
@@ -1318,10 +1406,19 @@ pub fn derive_all_districts(
|
||||
// deriving elevation_km from the district's own elev_q (not the caller's
|
||||
// body_params.elevation_km). Per-cell refinement happens at ChunkContext (D-239).
|
||||
let district_params = BodyParams {
|
||||
district_latitude_deg: lat_deg,
|
||||
latitude_deg: lat_deg,
|
||||
..body_params.clone()
|
||||
};
|
||||
let profile = derive_district_profile(seed, &district_params, ta, pos, gcpr, &climate);
|
||||
let profile = derive_district_profile(
|
||||
seed,
|
||||
&district_params,
|
||||
ta,
|
||||
pos,
|
||||
gcpr,
|
||||
&climate,
|
||||
body_id,
|
||||
®ion_cache,
|
||||
);
|
||||
out.insert(pos, profile);
|
||||
}
|
||||
}
|
||||
@@ -1372,7 +1469,7 @@ mod tests {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let params = BodyParams::default();
|
||||
let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8);
|
||||
let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body");
|
||||
|
||||
// Expected: ceil(64/8) × ceil(32/8) = 8 × 4 = 32 districts.
|
||||
assert_eq!(districts.len(), 32, "district count mismatch");
|
||||
@@ -1396,8 +1493,8 @@ mod tests {
|
||||
let ta = test_ta(&hm);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = earth_params();
|
||||
let a = derive_district(test_seed(), &p, &ta, (1234, -567), &climate);
|
||||
let b = derive_district(test_seed(), &p, &ta, (1234, -567), &climate);
|
||||
let a = derive_district(test_seed(), "test_body", &p, &ta, (1234, -567), &climate);
|
||||
let b = derive_district(test_seed(), "test_body", &p, &ta, (1234, -567), &climate);
|
||||
assert_eq!(a.elev_q, b.elev_q);
|
||||
assert_eq!(a.slope_q, b.slope_q);
|
||||
assert_eq!(a.morphology_zone, b.morphology_zone);
|
||||
@@ -1415,9 +1512,15 @@ 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(), &p, &ta, (0, 0), &climate);
|
||||
let high_lat =
|
||||
derive_district(test_seed(), &p, &ta, (0, merid_districts / 2 - 2), &climate);
|
||||
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})")
|
||||
@@ -1441,7 +1544,7 @@ mod tests {
|
||||
let ta = test_ta(&flat);
|
||||
let climate = ClimateConstants::default();
|
||||
let p = earth_params();
|
||||
let d = derive_district(test_seed(), &p, &ta, (500, 100), &climate);
|
||||
let d = derive_district(test_seed(), "flat", &p, &ta, (500, 100), &climate);
|
||||
// Flat land everywhere → slope 0 → no invented relief, no ocean.
|
||||
assert_eq!(
|
||||
d.slope_q, 0,
|
||||
@@ -1462,8 +1565,8 @@ mod tests {
|
||||
atmosphere: Some("breathable".into()),
|
||||
..Default::default() // body_radius_km: None
|
||||
};
|
||||
let a = derive_district(test_seed(), &p, &ta, (20, 10), &climate);
|
||||
let b = derive_district(test_seed(), &p, &ta, (20, 10), &climate);
|
||||
let a = derive_district(test_seed(), "test_body", &p, &ta, (20, 10), &climate);
|
||||
let b = derive_district(test_seed(), "test_body", &p, &ta, (20, 10), &climate);
|
||||
assert_eq!(a.elev_q, b.elev_q);
|
||||
assert!((0..=100).contains(&a.elev_q) && (0..=100).contains(&a.slope_q));
|
||||
}
|
||||
@@ -1481,8 +1584,26 @@ mod tests {
|
||||
};
|
||||
let pos = (2, 1);
|
||||
let climate = ClimateConstants::default();
|
||||
let p1 = derive_district_profile(test_seed(), ¶ms, &ta, pos, 8, &climate);
|
||||
let p2 = derive_district_profile(test_seed(), ¶ms, &ta, pos, 8, &climate);
|
||||
let p1 = derive_district_profile(
|
||||
test_seed(),
|
||||
¶ms,
|
||||
&ta,
|
||||
pos,
|
||||
8,
|
||||
&climate,
|
||||
"test_body",
|
||||
&BTreeMap::new(),
|
||||
);
|
||||
let p2 = derive_district_profile(
|
||||
test_seed(),
|
||||
¶ms,
|
||||
&ta,
|
||||
pos,
|
||||
8,
|
||||
&climate,
|
||||
"test_body",
|
||||
&BTreeMap::new(),
|
||||
);
|
||||
// Equality via serialized fields (no PartialEq on MorphologyZone — compare by name).
|
||||
assert_eq!(
|
||||
format!("{:?}", p1.morphology_zone),
|
||||
@@ -1576,7 +1697,7 @@ mod tests {
|
||||
let hm = test_hm();
|
||||
let ta = test_ta(&hm);
|
||||
let params = BodyParams::default();
|
||||
let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8);
|
||||
let districts = derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body");
|
||||
// BTreeMap iterates in sorted key order — verify the first key is (0,0).
|
||||
let first = districts.keys().next().expect("at least one district");
|
||||
assert_eq!(*first, (0, 0), "first district must be at origin");
|
||||
@@ -1607,7 +1728,7 @@ mod tests {
|
||||
let params = BodyParams {
|
||||
planet_class: Some(planet_class.into()),
|
||||
atmosphere: Some(atmosphere.into()),
|
||||
district_latitude_deg: lat,
|
||||
latitude_deg: lat,
|
||||
elevation_km: elev_km,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1683,7 +1804,7 @@ mod tests {
|
||||
let params = BodyParams {
|
||||
atmosphere: Some("thin".into()),
|
||||
planet_class: Some("arid".into()),
|
||||
district_latitude_deg: 45.0,
|
||||
latitude_deg: 45.0,
|
||||
elevation_km: 1.5,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1700,7 +1821,7 @@ mod tests {
|
||||
let params = BodyParams {
|
||||
atmosphere: Some("breathable".into()),
|
||||
planet_class: Some("temperate".into()),
|
||||
district_latitude_deg: 30.0,
|
||||
latitude_deg: 30.0,
|
||||
elevation_km: 0.0,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1755,7 +1876,7 @@ mod tests {
|
||||
planet_class: Some((*class).into()),
|
||||
atmosphere: Some((*atmo).into()),
|
||||
hydrosphere: Some(hydro.into()),
|
||||
district_latitude_deg: lat,
|
||||
latitude_deg: lat,
|
||||
elevation_km: elev,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1784,7 +1905,7 @@ mod tests {
|
||||
planet_class: Some("temperate".into()),
|
||||
atmosphere: Some("standard".into()),
|
||||
hydrosphere: Some(hydro.into()),
|
||||
district_latitude_deg: lat,
|
||||
latitude_deg: lat,
|
||||
elevation_km: 0.0,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -1810,7 +1931,7 @@ mod tests {
|
||||
let params = BodyParams {
|
||||
planet_class: Some("unknown_alien_class".into()),
|
||||
atmosphere: Some("breathable".into()),
|
||||
district_latitude_deg: 0.0,
|
||||
latitude_deg: 0.0,
|
||||
elevation_km: 0.0,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -2643,7 +2764,7 @@ mod tests {
|
||||
atmosphere: Some("breathable".into()),
|
||||
planet_class: Some("temperate".into()),
|
||||
elevation_km: 2.0,
|
||||
district_latitude_deg: 0.0, // this should be irrelevant for the modulation
|
||||
latitude_deg: 0.0, // this should be irrelevant for the modulation
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ pub struct RegionProfile {
|
||||
/// ## Inputs
|
||||
///
|
||||
/// - `body_params` — `planet_class`, `atmosphere`, `hydrosphere`, and the
|
||||
/// **region centre latitude** in `district_latitude_deg` (repurposed for
|
||||
/// **region centre latitude** in `latitude_deg` (repurposed for
|
||||
/// the region's central latitude — the field name is historical).
|
||||
/// - `constants` — tunable climate constants (D-240 table).
|
||||
/// - `body_seed` — the body-scoped seed for the deterministic nudge (D-010).
|
||||
@@ -226,7 +226,7 @@ pub fn derive_region_baseline_c(
|
||||
let maritime = constants.maritime_factor(hydrosphere);
|
||||
let mid = (cold + warm) * 0.5;
|
||||
let half = band_width * 0.5 * maritime;
|
||||
let lat_frac = (body_params.district_latitude_deg.abs() as f32 / 90.0).clamp(0.0, 1.0);
|
||||
let lat_frac = (body_params.latitude_deg.abs() as f32 / 90.0).clamp(0.0, 1.0);
|
||||
// equator (frac 0) → mid + half; pole (frac 1) → mid − half.
|
||||
let t_lat = (mid + half) - (2.0 * half) * lat_frac;
|
||||
|
||||
@@ -283,7 +283,7 @@ pub fn region_centre_latitude_deg(region_pos: RegionPos, body_radius_km: Option<
|
||||
///
|
||||
/// Pure and deterministic: `(seed, body_params, region_pos)` → `RegionProfile`.
|
||||
///
|
||||
/// `body_params.district_latitude_deg` is **overridden** to the region centre's
|
||||
/// `body_params.latitude_deg` is **overridden** to the region centre's
|
||||
/// latitude internally — callers do not need to pre-set it.
|
||||
pub fn build_region_profile(
|
||||
seed: SeedChain,
|
||||
@@ -295,7 +295,7 @@ pub fn build_region_profile(
|
||||
|
||||
// Build region-local params: override latitude to the region centre.
|
||||
let region_params = BodyParams {
|
||||
district_latitude_deg: lat_deg,
|
||||
latitude_deg: lat_deg,
|
||||
// Elevation at region level is sea level (baseline only; no lapse here).
|
||||
elevation_km: 0.0,
|
||||
..body_params.clone()
|
||||
@@ -444,7 +444,7 @@ pub fn region_baseline_at_district(
|
||||
// Derive on the fly (test path / cache miss).
|
||||
let lat = region_centre_latitude_deg(rpos, body_params.body_radius_km);
|
||||
let r_params = BodyParams {
|
||||
district_latitude_deg: lat,
|
||||
latitude_deg: lat,
|
||||
elevation_km: 0.0,
|
||||
..body_params.clone()
|
||||
};
|
||||
@@ -668,7 +668,7 @@ mod tests {
|
||||
let params = BodyParams {
|
||||
atmosphere: Some("breathable".into()),
|
||||
planet_class: Some("temperate".into()),
|
||||
district_latitude_deg: 30.0,
|
||||
latitude_deg: 30.0,
|
||||
elevation_km: 5.0, // This must be ignored by the region baseline
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
//! Update golden: `UPDATE_GOLDEN=1 cargo test --test derivation_harness`
|
||||
//! Budget gate: `BUDGET_ASSERT=1 cargo test --test derivation_harness -- budget`
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -1872,7 +1873,7 @@ fn kallast_body_params() -> BodyParams {
|
||||
atmosphere: Some("standard".into()),
|
||||
planet_class: Some("temperate".into()),
|
||||
tectonic_activity: Some("active".into()),
|
||||
district_latitude_deg: 0.0,
|
||||
latitude_deg: 0.0,
|
||||
elevation_km: 0.0,
|
||||
body_radius_km: None,
|
||||
}
|
||||
@@ -1887,7 +1888,7 @@ fn gloedberg_body_params() -> BodyParams {
|
||||
atmosphere: Some("standard".into()),
|
||||
planet_class: Some("volcanic".into()),
|
||||
tectonic_activity: Some("volcanic".into()),
|
||||
district_latitude_deg: 0.0,
|
||||
latitude_deg: 0.0,
|
||||
elevation_km: 0.5,
|
||||
body_radius_km: None,
|
||||
}
|
||||
@@ -1903,7 +1904,7 @@ fn marevna_body_params() -> BodyParams {
|
||||
atmosphere: Some("standard".into()),
|
||||
planet_class: Some("oceanic".into()),
|
||||
tectonic_activity: Some("active".into()),
|
||||
district_latitude_deg: 0.0,
|
||||
latitude_deg: 0.0,
|
||||
elevation_km: 0.0,
|
||||
body_radius_km: None,
|
||||
}
|
||||
@@ -1920,7 +1921,16 @@ fn derive_profile_for_body(params: &BodyParams) -> DistrictProfile {
|
||||
let climate = ClimateConstants::default();
|
||||
let seed = SeedChain::root(42).derive(SeedDomain::Body, 1);
|
||||
let ta = make_dry_terrain_analysis();
|
||||
derive_district_profile(seed, params, &ta, (0, 0), 8, &climate)
|
||||
derive_district_profile(
|
||||
seed,
|
||||
params,
|
||||
&ta,
|
||||
(0, 0),
|
||||
8,
|
||||
&climate,
|
||||
"test_body",
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user