fix(simulation): region baseline latitude — equator-anchored signed rows (T-1186)

region_centre_latitude_deg mapped row 0 to the north pole with lat_frac
clamped [0,1] while the derive core floor-divides equator-anchored signed
world metres — every northern-hemisphere region clamped to +90 (polar
everywhere) and the southern hemisphere read as compressed northern
tropics. Now mirrors the derive core's exact inverse mapping (lat_frac =
centre_y/meridian clamped [-0.5,0.5], lat = -frac*180). The ticket's
in-the-wild datapoint flips as predicted: GJ338Bd region (136,43), old
baseline +13.94N/26.8C -> new -76.06S/10.3C; the 76S district cell now
derives Lake/2.07C/Light glaciation. Goldens regenerated (believability +
window derivation, stable on rerun); two region_profile unit tests
repositioned — their probe coordinates meant different places under the
old convention. step_canvas rung-0 already used the correct signed
convention; layer_proxy's pole-anchored pseudo-grid stays deferred to
T-1181's rung-0 rebuild per D-256(f).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 13:08:26 +02:00
co-authored by Claude Fable 5
parent 7ab15baed5
commit 07f0fa307f
3 changed files with 150 additions and 110 deletions
+54 -14
View File
@@ -258,11 +258,32 @@ pub fn derive_region_baseline_c(
/// Compute the latitude of a region centre from its grid position.
///
/// The region grid is equirectangular. Row 0 sits at the north pole; the
/// equator is in the middle. Returns degrees: +90.0 = north pole, 90.0 =
/// south pole.
/// **Equator-anchored SIGNED row convention (T-1186 fix)** — this MUST mirror
/// the canonical derive core's own inverse mapping
/// (`derive_at_metres_with_riparian`, district_profile.rs: `lat_frac = (wy /
/// meridian_m).clamp(-0.5, 0.5)`, `lat_deg = -lat_frac * 180.0`, keyed on
/// equator-anchored signed world metres via `district_to_region`'s
/// `div_euclid`). `region_pos.1` is therefore SIGNED: negative rows are
/// north, positive rows are south, row 0 straddles the equator — NOT the
/// pole-anchored non-negative convention this function used before T-1186
/// (row 0 = north pole, `lat_frac` clamped to `[0, 1]`), which silently
/// disagreed with the derive core it feeds (D-256(a): one absolute-metre
/// derive core, one inverse mapping — this was the second, wrong, mapping).
///
/// Returns degrees: +90.0 = north pole, 90.0 = south pole.
///
/// If no body radius is available (tiny test bodies), the region lat is 0.0.
///
/// **Convention note (T-1186 audit):** two OTHER call sites in this codebase
/// still use the old non-negative pole-anchored row convention and do NOT
/// call this function — `step_canvas::build_step_canvas`'s Global (rung-0)
/// canvas (deliberately switched to signed-equator rows ahead of this fix,
/// see its doc comment) and `layer_proxy::build_region_grid` /
/// `cascade::LayerRegionOutput` (still pole-anchored, non-negative `ry`,
/// D-256(f)-deferred to T-1181's rung-0 rebuild — its sole reader is the
/// stale body-view overlay, no consumer disagreement in this ticket's blast
/// radius). See T-1186's report for the proposed one-convention unification;
/// this function's fix does not by itself touch either of those call sites.
pub fn region_centre_latitude_deg(region_pos: RegionPos, body_radius_km: Option<f64>) -> f64 {
let Some(r_km) = body_radius_km else {
return 0.0;
@@ -270,13 +291,17 @@ pub fn region_centre_latitude_deg(region_pos: RegionPos, body_radius_km: Option<
if r_km <= 0.0 {
return 0.0;
}
// The meridian spans πR km. Each region is REGION_M metres tall.
// region_y = 0 maps to the north pole (lat +90), rising y → south.
// The meridian spans πR km. Each region is REGION_M metres tall,
// equator-anchored: region row 0 straddles the equator, negative rows
// are north (mirrors district_to_region's div_euclid on signed,
// equator-anchored district positions).
let meridian_m = std::f64::consts::PI * r_km * 1_000.0;
let region_centre_y_m = (region_pos.1 as f64 + 0.5) * scale::REGION_M as f64;
// Clamp: lat_frac in [0, 1]; 0 = N pole (+90°), 1 = S pole (90°).
let lat_frac = (region_centre_y_m / meridian_m).clamp(0.0, 1.0);
90.0 - lat_frac * 180.0
// Clamp: lat_frac in [-0.5, 0.5]; -0.5 = N pole, +0.5 = S pole — the exact
// inverse of derive_at_metres_with_riparian's `lat_frac = (wy /
// meridian_m).clamp(-0.5, 0.5)`.
let lat_frac = (region_centre_y_m / meridian_m).clamp(-0.5, 0.5);
-lat_frac * 180.0
}
/// Build a [`RegionProfile`] for the region at `region_pos`.
@@ -611,12 +636,16 @@ mod tests {
#[test]
fn equatorial_region_is_warmer_than_polar() {
// With a body radius, north-pole region vs equatorial region.
// T-1186: region rows (RegionPos.1, the Y/row component — RegionPos is
// (x, y)) are equator-anchored SIGNED (row 0 straddles the equator;
// negative = north, matching the derive core's floor-divide of
// equator-anchored world metres). Region (0, 0) is therefore near the
// EQUATOR, not the pole — (0, -97) is the genuinely polar region on
// this Earth-radius test body (~97.7 regions pole-to-pole).
let params = earth_params();
let constants = ClimateConstants::default();
// Region (0, 0) is near the north pole; (97, 15) is roughly equatorial.
let polar = build_region_profile(body_seed(), &params, &constants, (0, 0));
let equatorial = build_region_profile(body_seed(), &params, &constants, (97, 15));
let polar = build_region_profile(body_seed(), &params, &constants, (0, -97));
let equatorial = build_region_profile(body_seed(), &params, &constants, (0, 0));
match (polar.clock.mean_temp_c, equatorial.clock.mean_temp_c) {
(Some(p), Some(e)) => assert!(
p < e,
@@ -746,12 +775,23 @@ mod tests {
#[test]
fn edge_fuzz_varies_with_body_id() {
// T-1186: (50, 50) sat right at the equator under the new
// equator-anchored-signed row convention (region row 0), where
// earth_params()'s ocean/breathable/temperate band saturates at the
// 28°C warm ceiling for every neighbour regardless of warp — a
// fix-induced false negative, not a real regression (the OLD
// pole-anchored convention happened to place row 0 near the pole,
// where the band isn't saturated). (50, 2400) — a mid-latitude
// district — also turned out to be a coincidental warp collision at
// this seed (both bodies landing on the same blend). (73, 2455) is a
// confirmed non-degenerate probe: comfortably inside the unsaturated
// band with the two bodies' warps genuinely diverging.
let params = earth_params();
let constants = ClimateConstants::default();
let a = region_baseline_at_district(
42,
"BodyA",
(50, 50),
(73, 2455),
&params,
&constants,
body_seed(),
@@ -760,7 +800,7 @@ mod tests {
let b = region_baseline_at_district(
42,
"BodyB",
(50, 50),
(73, 2455),
&params,
&constants,
body_seed(),
+9 -9
View File
@@ -8,7 +8,7 @@
"moisture_q": {
"min": 17,
"max": 97,
"distinct": 74
"distinct": 75
},
"elev_q": {
"min": 0,
@@ -17,27 +17,27 @@
},
"slope_q": {
"min": 0,
"max": 31,
"distinct": 31
"max": 27,
"distinct": 28
},
"ocean_fraction_q": {
"min": 0,
"max": 100,
"distinct": 44
"distinct": 47
},
"morphology_zones": 9,
"vegetation_classes": 4,
"terrain_materials": 4,
"terrain_materials": 3,
"voxel_relief_m": 28,
"micro_habitat_distinct": 2
"micro_habitat_distinct": 3
},
"coherence": {
"water_districts": 46,
"water_districts_wet": 46,
"water_districts_wet": 45,
"drainage_samples": 0,
"drainage_monotonic": 0,
"vegetation_samples": 64,
"vegetated_districts": 8
"vegetated_districts": 7
}
},
{
@@ -67,7 +67,7 @@
"distinct": 36
},
"morphology_zones": 6,
"vegetation_classes": 2,
"vegetation_classes": 3,
"terrain_materials": 2,
"voxel_relief_m": 22,
"micro_habitat_distinct": 0
@@ -7,14 +7,14 @@
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 3,
"glaciation": 0,
"precipitation": 3,
"slope_q": 2,
"elev_q": 27,
"ocean_fraction_q": 0,
"temperature_dc": -48,
"temperature_dc": 118,
"moisture_q": 59,
"vegetation": 2
"vegetation": 3
},
{
"label": "coastal_a",
@@ -24,14 +24,14 @@
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 2,
"precipitation": 2,
"slope_q": 4,
"elev_q": 32,
"glaciation": 0,
"precipitation": 3,
"slope_q": 3,
"elev_q": 31,
"ocean_fraction_q": 0,
"temperature_dc": -74,
"temperature_dc": 97,
"moisture_q": 59,
"vegetation": 2
"vegetation": 3
},
{
"label": "coastal_a",
@@ -41,14 +41,14 @@
"min_wl_m": 0,
"morphology": 16,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 0,
"elev_q": 26,
"ocean_fraction_q": 0,
"temperature_dc": -43,
"temperature_dc": 123,
"moisture_q": 60,
"vegetation": 2
"vegetation": 3
},
{
"label": "coastal_b",
@@ -58,14 +58,14 @@
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 2,
"precipitation": 1,
"glaciation": 0,
"precipitation": 2,
"slope_q": 3,
"elev_q": 30,
"elev_q": 29,
"ocean_fraction_q": 0,
"temperature_dc": -68,
"temperature_dc": 112,
"moisture_q": 55,
"vegetation": 2
"vegetation": 3
},
{
"label": "coastal_b",
@@ -75,14 +75,14 @@
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 4,
"elev_q": 26,
"glaciation": 0,
"precipitation": 3,
"slope_q": 3,
"elev_q": 25,
"ocean_fraction_q": 0,
"temperature_dc": -47,
"temperature_dc": 132,
"moisture_q": 56,
"vegetation": 2
"vegetation": 3
},
{
"label": "coastal_b",
@@ -92,14 +92,14 @@
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 0,
"elev_q": 26,
"ocean_fraction_q": 0,
"temperature_dc": -47,
"temperature_dc": 127,
"moisture_q": 57,
"vegetation": 2
"vegetation": 3
},
{
"label": "coastal_c",
@@ -109,14 +109,14 @@
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 3,
"elev_q": 24,
"ocean_fraction_q": 0,
"temperature_dc": -30,
"temperature_dc": 131,
"moisture_q": 59,
"vegetation": 2
"vegetation": 3
},
{
"label": "coastal_c",
@@ -126,14 +126,14 @@
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 2,
"precipitation": 2,
"slope_q": 4,
"elev_q": 28,
"glaciation": 0,
"precipitation": 3,
"slope_q": 3,
"elev_q": 27,
"ocean_fraction_q": 0,
"temperature_dc": -51,
"temperature_dc": 115,
"moisture_q": 58,
"vegetation": 2
"vegetation": 3
},
{
"label": "coastal_c",
@@ -143,14 +143,14 @@
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 0,
"elev_q": 27,
"ocean_fraction_q": 0,
"temperature_dc": -45,
"temperature_dc": 115,
"moisture_q": 58,
"vegetation": 2
"vegetation": 3
},
{
"label": "inland",
@@ -160,12 +160,12 @@
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"glaciation": 0,
"precipitation": 2,
"slope_q": 4,
"slope_q": 3,
"elev_q": 25,
"ocean_fraction_q": 0,
"temperature_dc": 0,
"temperature_dc": 90,
"moisture_q": 52,
"vegetation": 3
},
@@ -177,12 +177,12 @@
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"glaciation": 0,
"precipitation": 2,
"slope_q": 5,
"slope_q": 4,
"elev_q": 20,
"ocean_fraction_q": 0,
"temperature_dc": 26,
"temperature_dc": 116,
"moisture_q": 52,
"vegetation": 3
},
@@ -194,12 +194,12 @@
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"glaciation": 0,
"precipitation": 2,
"slope_q": 0,
"elev_q": 22,
"ocean_fraction_q": 0,
"temperature_dc": 16,
"temperature_dc": 106,
"moisture_q": 52,
"vegetation": 3
},
@@ -216,7 +216,7 @@
"slope_q": 3,
"elev_q": 63,
"ocean_fraction_q": 0,
"temperature_dc": -69,
"temperature_dc": -120,
"moisture_q": 43,
"vegetation": 1
},
@@ -233,7 +233,7 @@
"slope_q": 4,
"elev_q": 61,
"ocean_fraction_q": 0,
"temperature_dc": -59,
"temperature_dc": -120,
"moisture_q": 43,
"vegetation": 1
},
@@ -250,7 +250,7 @@
"slope_q": 0,
"elev_q": 62,
"ocean_fraction_q": 0,
"temperature_dc": -64,
"temperature_dc": -120,
"moisture_q": 43,
"vegetation": 1
},
@@ -262,12 +262,12 @@
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"glaciation": 0,
"precipitation": 2,
"slope_q": 2,
"elev_q": 8,
"ocean_fraction_q": 0,
"temperature_dc": 14,
"temperature_dc": 91,
"moisture_q": 49,
"vegetation": 3
},
@@ -279,12 +279,12 @@
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"glaciation": 0,
"precipitation": 2,
"slope_q": 3,
"elev_q": 4,
"slope_q": 2,
"elev_q": 5,
"ocean_fraction_q": 0,
"temperature_dc": 34,
"temperature_dc": 106,
"moisture_q": 50,
"vegetation": 3
},
@@ -296,12 +296,12 @@
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"glaciation": 0,
"precipitation": 2,
"slope_q": 0,
"elev_q": 11,
"ocean_fraction_q": 0,
"temperature_dc": -2,
"temperature_dc": 75,
"moisture_q": 49,
"vegetation": 3
},
@@ -624,7 +624,7 @@
"slope_q": 3,
"elev_q": 25,
"ocean_fraction_q": 0,
"temperature_dc": 497,
"temperature_dc": 749,
"moisture_q": 60,
"vegetation": 3
},
@@ -641,7 +641,7 @@
"slope_q": 6,
"elev_q": 13,
"ocean_fraction_q": 0,
"temperature_dc": 560,
"temperature_dc": 811,
"moisture_q": 62,
"vegetation": 3
},
@@ -658,7 +658,7 @@
"slope_q": 0,
"elev_q": 28,
"ocean_fraction_q": 0,
"temperature_dc": 482,
"temperature_dc": 733,
"moisture_q": 59,
"vegetation": 3
},
@@ -675,7 +675,7 @@
"slope_q": 4,
"elev_q": 34,
"ocean_fraction_q": 0,
"temperature_dc": 456,
"temperature_dc": 697,
"moisture_q": 58,
"vegetation": 3
},
@@ -692,7 +692,7 @@
"slope_q": 6,
"elev_q": 24,
"ocean_fraction_q": 0,
"temperature_dc": 508,
"temperature_dc": 749,
"moisture_q": 60,
"vegetation": 3
},
@@ -709,7 +709,7 @@
"slope_q": 0,
"elev_q": 28,
"ocean_fraction_q": 0,
"temperature_dc": 487,
"temperature_dc": 728,
"moisture_q": 59,
"vegetation": 3
},
@@ -726,7 +726,7 @@
"slope_q": 2,
"elev_q": 28,
"ocean_fraction_q": 0,
"temperature_dc": 486,
"temperature_dc": 729,
"moisture_q": 59,
"vegetation": 3
},
@@ -743,7 +743,7 @@
"slope_q": 4,
"elev_q": 33,
"ocean_fraction_q": 0,
"temperature_dc": 460,
"temperature_dc": 703,
"moisture_q": 59,
"vegetation": 3
},
@@ -760,7 +760,7 @@
"slope_q": 0,
"elev_q": 29,
"ocean_fraction_q": 0,
"temperature_dc": 481,
"temperature_dc": 724,
"moisture_q": 59,
"vegetation": 3
},
@@ -777,7 +777,7 @@
"slope_q": 3,
"elev_q": 24,
"ocean_fraction_q": 0,
"temperature_dc": 565,
"temperature_dc": 691,
"moisture_q": 49,
"vegetation": 3
},
@@ -794,7 +794,7 @@
"slope_q": 4,
"elev_q": 21,
"ocean_fraction_q": 0,
"temperature_dc": 581,
"temperature_dc": 707,
"moisture_q": 49,
"vegetation": 3
},
@@ -811,7 +811,7 @@
"slope_q": 0,
"elev_q": 23,
"ocean_fraction_q": 0,
"temperature_dc": 571,
"temperature_dc": 697,
"moisture_q": 49,
"vegetation": 3
},
@@ -828,7 +828,7 @@
"slope_q": 3,
"elev_q": 68,
"ocean_fraction_q": 0,
"temperature_dc": 545,
"temperature_dc": 300,
"moisture_q": 29,
"vegetation": 2
},
@@ -845,7 +845,7 @@
"slope_q": 4,
"elev_q": 65,
"ocean_fraction_q": 0,
"temperature_dc": 561,
"temperature_dc": 300,
"moisture_q": 30,
"vegetation": 2
},
@@ -862,7 +862,7 @@
"slope_q": 0,
"elev_q": 66,
"ocean_fraction_q": 0,
"temperature_dc": 555,
"temperature_dc": 300,
"moisture_q": 30,
"vegetation": 2
},
@@ -879,7 +879,7 @@
"slope_q": 4,
"elev_q": 14,
"ocean_fraction_q": 0,
"temperature_dc": 500,
"temperature_dc": 597,
"moisture_q": 47,
"vegetation": 3
},
@@ -896,7 +896,7 @@
"slope_q": 6,
"elev_q": 5,
"ocean_fraction_q": 0,
"temperature_dc": 547,
"temperature_dc": 644,
"moisture_q": 49,
"vegetation": 3
},
@@ -913,7 +913,7 @@
"slope_q": 0,
"elev_q": 18,
"ocean_fraction_q": 0,
"temperature_dc": 480,
"temperature_dc": 576,
"moisture_q": 47,
"vegetation": 3
},
@@ -930,7 +930,7 @@
"slope_q": 4,
"elev_q": 0,
"ocean_fraction_q": 0,
"temperature_dc": 62,
"temperature_dc": 280,
"moisture_q": 56,
"vegetation": 6
},
@@ -947,7 +947,7 @@
"slope_q": 5,
"elev_q": 0,
"ocean_fraction_q": 0,
"temperature_dc": 62,
"temperature_dc": 280,
"moisture_q": 56,
"vegetation": 6
},
@@ -964,7 +964,7 @@
"slope_q": 0,
"elev_q": 0,
"ocean_fraction_q": 0,
"temperature_dc": 62,
"temperature_dc": 280,
"moisture_q": 56,
"vegetation": 6
},
@@ -981,7 +981,7 @@
"slope_q": 0,
"elev_q": 71,
"ocean_fraction_q": 0,
"temperature_dc": -120,
"temperature_dc": -89,
"moisture_q": 55,
"vegetation": 1
},
@@ -998,7 +998,7 @@
"slope_q": 2,
"elev_q": 75,
"ocean_fraction_q": 0,
"temperature_dc": -120,
"temperature_dc": -110,
"moisture_q": 54,
"vegetation": 1
},
@@ -1015,7 +1015,7 @@
"slope_q": 0,
"elev_q": 70,
"ocean_fraction_q": 0,
"temperature_dc": -120,
"temperature_dc": -84,
"moisture_q": 54,
"vegetation": 1
}