feat(simulation): hydrosphere maritime moderation of temperature (T-1033, D-240)
Add the D-240 hydrosphere modulator: water-rich worlds compress the equator->pole temperature gradient toward the band midpoint (milder at both ends); dry worlds swing the full class band. - [hydrosphere_maritime] table in climate_constants.toml + ClimateConstants field/method (maritime_factor). Keyed on the ACTUAL bodies.hydrosphere vocab (liquid_water/ocean/extensive=0.6, rivers/moderate=0.8, ice=0.85, subsurface* =0.9-0.95; minimal/trace/none/NULL -> 1.0). - derive_temperature_c step 2 now lerps across a maritime-compressed sub-band (mid +/- band_half*factor) instead of the full band. Clamp still guarantees the class-band invariant. - New maritime_hydrosphere_compresses_the_gradient test (ocean delta < dry delta). Verified on real worlds: liquid_water/ocean temperate worlds now delta ~24 (milder) vs dry ~40, all in-band. cargo test passes, clippy -D warnings clean, fmt clean. Note: discovered derive_moisture_q has the SAME vocab mismatch (expects 'ocean', DB uses 'liquid_water' for 175 bodies -> falls to default moisture). Pre-existing, out of scope here — filing a follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -65,3 +65,22 @@ standard = 15 # ~15°C: Earth-like moderate swing
|
||||
toxic = 20 # intermediate: thick but maybe less redistribution
|
||||
breathable = 15 # synonym for standard
|
||||
dense = 3 # ~3°C: Venus-like near-uniform temperature
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
[hydrosphere_maritime]
|
||||
# Maritime moderation (D-240): scales the equator→pole temperature gradient by
|
||||
# hydrosphere. 1.0 = full gradient (dry world swings the whole class band);
|
||||
# < 1.0 compresses toward the band midpoint (water-rich worlds are milder at
|
||||
# both poles and equator). Keys = actual bodies.hydrosphere vocabulary in
|
||||
# systems.db. "minimal"/"trace"/"none"/absent default to 1.0 in code.
|
||||
liquid_water = 0.6 # large surface liquid — strongly moderated
|
||||
ocean = 0.6
|
||||
"ocean-coastal" = 0.6
|
||||
extensive = 0.6
|
||||
rivers = 0.8 # partial surface water — mild moderation
|
||||
"rivers-lakes" = 0.8
|
||||
moderate = 0.8
|
||||
ice = 0.85 # frozen surface — slight moderation
|
||||
subsurface_liquid = 0.9
|
||||
subsurface = 0.95 # buried water — minimal surface effect
|
||||
subsurface_ice = 0.95
|
||||
|
||||
@@ -634,6 +634,11 @@ pub struct ClimateConstants {
|
||||
pub greenhouse_offset_c: std::collections::BTreeMap<String, f32>,
|
||||
/// Day/night swing amplitude per atmosphere class (°C).
|
||||
pub diurnal_amplitude_c: std::collections::BTreeMap<String, f32>,
|
||||
/// Maritime-moderation factor per `hydrosphere` (D-240): scales the
|
||||
/// equator→pole gradient. `1.0` = full gradient (dry world swings the whole
|
||||
/// band); `< 1.0` compresses toward the band midpoint (water-rich worlds are
|
||||
/// milder at both ends). Absent/unknown = `1.0`.
|
||||
pub hydrosphere_maritime: std::collections::BTreeMap<String, f32>,
|
||||
}
|
||||
|
||||
impl Default for ClimateConstants {
|
||||
@@ -669,10 +674,30 @@ impl Default for ClimateConstants {
|
||||
da.insert("toxic".into(), 20.0f32);
|
||||
da.insert("dense".into(), 3.0f32);
|
||||
|
||||
// Maritime moderation: water-rich worlds compress the equator–pole gradient.
|
||||
// Keys are the actual `bodies.hydrosphere` vocabulary in systems.db.
|
||||
let mut hm = std::collections::BTreeMap::new();
|
||||
// Large surface liquid — strong moderation.
|
||||
hm.insert("liquid_water".into(), 0.6f32);
|
||||
hm.insert("ocean".into(), 0.6f32);
|
||||
hm.insert("ocean-coastal".into(), 0.6f32);
|
||||
hm.insert("extensive".into(), 0.6f32);
|
||||
// Partial surface water — mild moderation.
|
||||
hm.insert("rivers".into(), 0.8f32);
|
||||
hm.insert("rivers-lakes".into(), 0.8f32);
|
||||
hm.insert("moderate".into(), 0.8f32);
|
||||
// Frozen / subsurface — slight moderation.
|
||||
hm.insert("ice".into(), 0.85f32);
|
||||
hm.insert("subsurface_liquid".into(), 0.9f32);
|
||||
hm.insert("subsurface".into(), 0.95f32);
|
||||
hm.insert("subsurface_ice".into(), 0.95f32);
|
||||
// "minimal" / "trace" / "none" / NULL → 1.0 (full gradient) via fallback.
|
||||
|
||||
ClimateConstants {
|
||||
planet_class_temperature: pct,
|
||||
greenhouse_offset_c: gh,
|
||||
diurnal_amplitude_c: da,
|
||||
hydrosphere_maritime: hm,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -713,6 +738,16 @@ impl ClimateConstants {
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// Maritime-moderation factor for a given `hydrosphere` (D-240): `1.0` = full
|
||||
/// equator→pole gradient (dry/unknown); `< 1.0` compresses toward the band
|
||||
/// midpoint (ocean ≈ 0.6 → milder at both ends).
|
||||
pub fn maritime_factor(&self, hydrosphere: &str) -> f32 {
|
||||
self.hydrosphere_maritime
|
||||
.get(hydrosphere)
|
||||
.copied()
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// Diurnal amplitude for a given atmosphere class (°C), defaulting to 30.
|
||||
pub fn diurnal_amplitude(&self, atmosphere: &str) -> f32 {
|
||||
self.diurnal_amplitude_c
|
||||
@@ -763,9 +798,16 @@ pub fn derive_temperature_c(
|
||||
let (cold, warm) = constants.envelope(planet_class);
|
||||
let band_width = warm - cold;
|
||||
|
||||
// Step 2: latitude lerp — equator → warm end, pole → cold end.
|
||||
// Step 2: latitude lerp across the maritime-moderated band (D-240). Water-rich
|
||||
// worlds compress the equator→pole gradient toward the band midpoint — an ocean
|
||||
// world is milder at both ends; a dry world swings the full band.
|
||||
let hydrosphere = params.hydrosphere.as_deref().unwrap_or("none");
|
||||
let maritime = constants.maritime_factor(hydrosphere);
|
||||
let mid = (cold + warm) * 0.5;
|
||||
let half = band_width * 0.5 * maritime;
|
||||
let lat_frac = (params.region_latitude_deg.abs() as f32 / 90.0).clamp(0.0, 1.0);
|
||||
let t_lat = warm - band_width * lat_frac;
|
||||
// equator (frac 0) → mid + half; pole (frac 1) → mid − half.
|
||||
let t_lat = (mid + half) - (2.0 * half) * lat_frac;
|
||||
|
||||
// Step 3: atmosphere greenhouse nudge — fraction of band_width toward warm end.
|
||||
let gh_frac = constants.greenhouse(atmosphere);
|
||||
@@ -1339,6 +1381,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maritime_hydrosphere_compresses_the_gradient() {
|
||||
// D-240: a water-rich world has a SMALLER equator→pole temperature delta
|
||||
// than a dry world of the same class (maritime moderation).
|
||||
let climate = ClimateConstants::default();
|
||||
let mk = |hydro: &str, lat: f64| BodyParams {
|
||||
planet_class: Some("temperate".into()),
|
||||
atmosphere: Some("standard".into()),
|
||||
hydrosphere: Some(hydro.into()),
|
||||
region_latitude_deg: lat,
|
||||
elevation_km: 0.0,
|
||||
..Default::default()
|
||||
};
|
||||
// Same seed → only hydrosphere differs.
|
||||
let delta = |hydro: &str| {
|
||||
(derive_temperature_c(&mk(hydro, 0.0), &climate, 7).unwrap()
|
||||
- derive_temperature_c(&mk(hydro, 90.0), &climate, 7).unwrap())
|
||||
.abs()
|
||||
};
|
||||
let ocean_delta = delta("ocean");
|
||||
let dry_delta = delta("none"); // hydrosphere none ≠ airless (atmosphere is "standard")
|
||||
assert!(
|
||||
ocean_delta < dry_delta,
|
||||
"ocean gradient {ocean_delta}°C must be milder than dry {dry_delta}°C"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_planet_class_falls_back_to_temperate_band() {
|
||||
// An unrecognised class string must not panic; it falls back to temperate.
|
||||
|
||||
Reference in New Issue
Block a user