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:
@@ -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(), ¶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(),
|
||||
|
||||
Reference in New Issue
Block a user