From 07f0fa307fa1e2594edf84bcde4865f7009288fe Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 25 Jul 2026 13:08:26 +0200 Subject: [PATCH] =?UTF-8?q?fix(simulation):=20region=20baseline=20latitude?= =?UTF-8?q?=20=E2=80=94=20equator-anchored=20signed=20rows=20(T-1186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- server/src/atlas/region_profile.rs | 68 +++++-- server/tests/golden/believability.json | 18 +- .../golden/window_derivation_golden.json | 174 +++++++++--------- 3 files changed, 150 insertions(+), 110 deletions(-) diff --git a/server/src/atlas/region_profile.rs b/server/src/atlas/region_profile.rs index b05e12317..ee5176892 100644 --- a/server/src/atlas/region_profile.rs +++ b/server/src/atlas/region_profile.rs @@ -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 { 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(), ¶ms, &constants, (0, 0)); - let equatorial = build_region_profile(body_seed(), ¶ms, &constants, (97, 15)); + let polar = build_region_profile(body_seed(), ¶ms, &constants, (0, -97)); + let equatorial = build_region_profile(body_seed(), ¶ms, &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), ¶ms, &constants, body_seed(), @@ -760,7 +800,7 @@ mod tests { let b = region_baseline_at_district( 42, "BodyB", - (50, 50), + (73, 2455), ¶ms, &constants, body_seed(), diff --git a/server/tests/golden/believability.json b/server/tests/golden/believability.json index 2711991a6..571779224 100644 --- a/server/tests/golden/believability.json +++ b/server/tests/golden/believability.json @@ -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 diff --git a/server/tests/golden/window_derivation_golden.json b/server/tests/golden/window_derivation_golden.json index 2470c00fb..fa1678e81 100644 --- a/server/tests/golden/window_derivation_golden.json +++ b/server/tests/golden/window_derivation_golden.json @@ -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 }