fix(simulation): one absolute-metre derive core (D-256, T-1174)
derive_district_profile is now a thin wrapper over the shared derive_at_metres_with_riparian core at survey-cell-centre world metres — one derive core, two position sets. The batch pseudo-grid and the true D-243 district grid were two coordinate systems sharing one bare (i32,i32) type; the new SurveyCellPos newtype re-keys every batch product (BodyWorldState.districts, Layer1Output.survey_basin_dirs) so the compiler rejects cross-namespace passing. Fixes two latent same-position divergences the T-1174 investigation surfaced: three inconsistent latitude conventions collapse into the core's single inverse mapping, and the region-climate baseline now floor-divides true world metres instead of collapsing the whole body onto region (0,0)'s baseline — batch climate becomes latitude/region graded (D-245 direction: every changed believability metric increased). Binding preservations per D-256(c): basin_direction rides a post-call override with the true L1 D8 survey-cell aggregate (layer1's map re-keyed to SurveyCellPos, identity lookup — a floor-divide lookup against the pseudo-keyed map would have silently defaulted every cell North); the riparian verdict comes from near_perennial_water_at, never the empty-slice default (which would have flipped riverside vegetation_class). Quarter-skeleton morphology_zone now resolves at the settlement's exact world position via derive_at_metres at work-item execution (where TerrainAnalysisCache lives), replacing the survey-cell-centre map lookup (D-256(d)); settlement_district_pos fixed to true-district floor-division in passing (same doc/impl mismatch class). Second pixel-vs-metre conflation fixed in aliveness_probe's anchor-walk math. Window path byte-unchanged (window_derivation_golden 6/6 byte- identical); derivation_harness golden untouched; believability golden regenerated. Full lib + integration suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+293
-27
@@ -667,19 +667,33 @@ pub struct AtlasLayerResponse {
|
||||
/// Build the coarse [`DistrictGridLayer`] from a body's cached state (T-1046).
|
||||
/// Returns `None` when the DistrictProfile layer has not run (empty `districts`).
|
||||
/// The grid is dense `[0, cols) × [0, rows)` (the cascade tiles the full
|
||||
/// heightmap), so the extent comes from the maximum `DistrictPos`.
|
||||
/// heightmap), so the extent comes from the maximum `SurveyCellPos` (D-256(b)
|
||||
/// — this is the coarse survey raster, not the true district grid).
|
||||
pub fn build_district_grid(
|
||||
state: &crate::atlas::body_world_state::BodyWorldState,
|
||||
) -> Option<DistrictGridLayer> {
|
||||
if state.districts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let cols = state.districts.keys().map(|(x, _)| *x).max().unwrap_or(0) as u32 + 1;
|
||||
let rows = state.districts.keys().map(|(_, y)| *y).max().unwrap_or(0) as u32 + 1;
|
||||
let cols = state
|
||||
.districts
|
||||
.keys()
|
||||
.map(|p| p.0)
|
||||
.max()
|
||||
.unwrap_or(0) as u32
|
||||
+ 1;
|
||||
let rows = state
|
||||
.districts
|
||||
.keys()
|
||||
.map(|p| p.1)
|
||||
.max()
|
||||
.unwrap_or(0) as u32
|
||||
+ 1;
|
||||
let n = (cols * rows) as usize;
|
||||
let mut morphology = vec![0u8; n];
|
||||
let mut elev_q = vec![0u8; n];
|
||||
for (&(x, y), profile) in &state.districts {
|
||||
for (pos, profile) in &state.districts {
|
||||
let (x, y) = (pos.0, pos.1);
|
||||
if x < 0 || y < 0 {
|
||||
continue;
|
||||
}
|
||||
@@ -2286,13 +2300,13 @@ pub fn handle_atlas_request(
|
||||
// working grid all Layer-1 positions are expressed in (#960).
|
||||
grid_w: state.heightmap_width,
|
||||
grid_h: state.heightmap_height,
|
||||
// district_basin_dirs is transient — it is aggregated during run_layer1
|
||||
// survey_basin_dirs is transient — it is aggregated during run_layer1
|
||||
// and consumed by derive_all_districts before being stored on
|
||||
// BodyWorldState. When reconstructing Layer1Output from the cache for
|
||||
// the client response, the per-district direction is already encoded in
|
||||
// DistrictProfile.basin_direction (BodyWorldState.districts) and is not
|
||||
// needed again here. Supply an empty map.
|
||||
district_basin_dirs: std::collections::BTreeMap::new(),
|
||||
// the client response, the per-survey-cell direction is already encoded
|
||||
// in DistrictProfile.basin_direction (BodyWorldState.districts) and is
|
||||
// not needed again here. Supply an empty map.
|
||||
survey_basin_dirs: std::collections::BTreeMap::new(),
|
||||
};
|
||||
let district_grid = build_district_grid(state);
|
||||
let road_graph = build_road_graph_layer(state);
|
||||
@@ -2450,6 +2464,7 @@ mod tests {
|
||||
heightmap: vec![],
|
||||
heightmap_width: 16,
|
||||
heightmap_height: 8,
|
||||
sea_level: 0.3,
|
||||
river_network: RiverNetwork::default(),
|
||||
drainage_basins: vec![],
|
||||
attractors: vec![],
|
||||
@@ -2461,12 +2476,14 @@ mod tests {
|
||||
last_accessed: 0,
|
||||
};
|
||||
// 3×2 grid with two distinct zones at the corners.
|
||||
state
|
||||
.districts
|
||||
.insert((0, 0), dp(MorphologyZone::AlluvialPlain, 10));
|
||||
state
|
||||
.districts
|
||||
.insert((2, 1), dp(MorphologyZone::Alpine, 90));
|
||||
state.districts.insert(
|
||||
crate::atlas::scale::SurveyCellPos(0, 0),
|
||||
dp(MorphologyZone::AlluvialPlain, 10),
|
||||
);
|
||||
state.districts.insert(
|
||||
crate::atlas::scale::SurveyCellPos(2, 1),
|
||||
dp(MorphologyZone::Alpine, 90),
|
||||
);
|
||||
|
||||
let grid = build_district_grid(&state).expect("districts present → Some grid");
|
||||
assert_eq!((grid.cols, grid.rows), (3, 2));
|
||||
@@ -2661,7 +2678,7 @@ mod tests {
|
||||
assert_eq!(layer.glaciation[0], prof.glaciation_grade as u8);
|
||||
}
|
||||
|
||||
/// T-1170/T-1168 A5 integration: the batch path
|
||||
/// T-1170/T-1168 A5 integration, strengthened by D-256: the batch path
|
||||
/// (`derive_district_profile`, sourcing courses via `near_perennial_water_at`
|
||||
/// on demand) and the window path (`build_district_window_layer`,
|
||||
/// sourcing courses via the pre-invented `Vec<InventedCourse>`) must
|
||||
@@ -2670,17 +2687,21 @@ mod tests {
|
||||
/// binding requirement, checked end to end (not just at the
|
||||
/// `near_perennial_water`/`near_perennial_water_at` unit level).
|
||||
///
|
||||
/// **Design note:** this test deliberately does NOT compare the batch
|
||||
/// and window paths' full `DistrictProfile` output for "the same
|
||||
/// district" — `derive_district_profile`'s cell-aggregate-centre
|
||||
/// sampling and the window path's district-origin sampling are
|
||||
/// legitimate, PRE-EXISTING different world positions for the same
|
||||
/// `DistrictPos` (a real quirk of the two derivation strategies,
|
||||
/// unrelated to T-1168/T-1170), so `morphology_zone`/`elev_q`/etc.
|
||||
/// routinely differ between them even before this batch's riparian work.
|
||||
/// Instead this test isolates the ONE signal this batch actually wires
|
||||
/// (`near_perennial_water`) at a SHARED, EXACT world position, proving
|
||||
/// the two paths' independent riparian derivations agree there.
|
||||
/// **Design note (D-256):** pre-D-256 this test deliberately did NOT
|
||||
/// compare the batch and window paths' full `DistrictProfile` output for
|
||||
/// "the same district" — `derive_district_profile`'s cell-aggregate-centre
|
||||
/// sampling and the window path's district-origin sampling resolved
|
||||
/// genuinely different world positions for a shared nominal `DistrictPos`
|
||||
/// (the batch namespace collision D-256 rules on). With sampling unified
|
||||
/// (one derive core, D-256(c)), that framing is now FALSE: batch and
|
||||
/// window positions agree BY CONSTRUCTION — `derive_district_profile` is
|
||||
/// a thin wrapper over the exact same [`crate::atlas::district_profile::derive_at_metres`]
|
||||
/// family the window path calls, at the survey cell's own D-256(b) centre
|
||||
/// world metres. `full_district_profile_matches_derive_at_metres_at_shared_survey_cell_centre`
|
||||
/// below is the strengthened full-profile bit-identical check this design
|
||||
/// note used to explicitly rule out; this test keeps the riparian-signal
|
||||
/// check as a focused shared-exact-position regression (the ONE hand-wired
|
||||
/// signal T-1168 added, worth its own targeted assertion).
|
||||
#[test]
|
||||
fn window_and_batch_paths_agree_on_riparian_signal_near_a_real_river_edge() {
|
||||
use crate::atlas::drainage;
|
||||
@@ -2763,6 +2784,249 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// D-256(c): the batch survey-cell profile == `derive_at_metres` at the
|
||||
/// SAME survey-cell-centre world metres — bit-identical, field for field
|
||||
/// (basin overridden on the expectation since the core always returns the
|
||||
/// default and the wrapper post-call-overrides it; riparian threaded
|
||||
/// equivalently through both paths' own mechanism). This is the
|
||||
/// strengthened replacement for the pre-D-256 "legitimate different
|
||||
/// positions" design note on the riparian-only test above — positions now
|
||||
/// agree by construction, so the FULL profile must too.
|
||||
#[test]
|
||||
fn full_district_profile_matches_derive_at_metres_at_shared_survey_cell_centre() {
|
||||
use crate::atlas::district_profile::{self, ClimateConstants};
|
||||
use crate::atlas::drainage;
|
||||
use crate::atlas::heightmap::load_heightmap_png;
|
||||
use crate::atlas::scale::{self, BasinDirection, SurveyCellPos};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png");
|
||||
let heightmap =
|
||||
load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap");
|
||||
let small = heightmap.downsample(256, 128);
|
||||
let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level);
|
||||
let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr);
|
||||
let rn = &dr.river_network;
|
||||
|
||||
let params = crate::atlas::district_profile::BodyParams {
|
||||
hydrosphere: Some("ocean".into()),
|
||||
atmosphere: Some("breathable".into()),
|
||||
planet_class: Some("temperate".into()),
|
||||
body_radius_km: Some(6371.0),
|
||||
..Default::default()
|
||||
};
|
||||
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1);
|
||||
let climate = ClimateConstants::default();
|
||||
let gcpr = scale::HEIGHTMAP_CELLS_PER_DISTRICT;
|
||||
|
||||
// An interior survey cell (not clamped at the grid edge — that case
|
||||
// is covered by the dedicated edge-truncation test below).
|
||||
let cell = SurveyCellPos(10, 6);
|
||||
let basin = BasinDirection::East;
|
||||
|
||||
let batch_profile = district_profile::derive_district_profile(
|
||||
seed,
|
||||
¶ms,
|
||||
&ta,
|
||||
cell,
|
||||
gcpr,
|
||||
&climate,
|
||||
"GJ1c",
|
||||
&BTreeMap::new(),
|
||||
basin,
|
||||
Some(rn),
|
||||
);
|
||||
|
||||
// The SAME survey-cell-centre world metres, resolved via the SAME
|
||||
// D-256(b) bridge function derive_district_profile uses internally.
|
||||
let (world_x_m, world_y_m) =
|
||||
district_profile::survey_cell_centre_world_m(cell, gcpr, ta.w, ta.h, params.body_radius_km);
|
||||
|
||||
// Riparian equivalent: near_perennial_water_at against the same
|
||||
// RiverNetwork, same station spacing/cutoff derive_district_profile
|
||||
// uses internally — the SAME on-demand course invention, not the
|
||||
// window path's pre-invented slice (this is the batch-vs-on-demand
|
||||
// core identity, not the batch-vs-window riparian check above).
|
||||
//
|
||||
// derive_at_metres's PUBLIC signature only accepts a pre-invented
|
||||
// `nearby_courses` slice (never a raw bool — Ruling 4a), so to force
|
||||
// the SAME riparian verdict through the public path a single-point
|
||||
// synthetic course exactly at (world_x_m, world_y_m) is threaded when
|
||||
// the on-demand signal is true; an empty slice when false. Either way
|
||||
// `near_perennial_water((wx,wy), courses)` evaluates to the identical
|
||||
// bool the batch core read.
|
||||
let near_perennial_water = river_course::near_perennial_water_at(
|
||||
seed,
|
||||
&ta,
|
||||
¶ms,
|
||||
rn,
|
||||
(world_x_m, world_y_m),
|
||||
DISTRICT_M as f64,
|
||||
0.0,
|
||||
);
|
||||
let riparian_equivalent: Vec<InventedCourse> = if near_perennial_water {
|
||||
vec![InventedCourse {
|
||||
edge_id: 0,
|
||||
class: 0,
|
||||
terminus: EdgeTerminusKind::Interior,
|
||||
points: vec![(world_x_m, world_y_m)],
|
||||
bbox: (world_x_m, world_y_m, world_x_m, world_y_m),
|
||||
}]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let mut expected = district_profile::derive_at_metres(
|
||||
seed,
|
||||
"GJ1c",
|
||||
¶ms,
|
||||
&ta,
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
&climate,
|
||||
0.0,
|
||||
&riparian_equivalent,
|
||||
);
|
||||
// basin_direction (inert field, D-256(c) proof) — the core always
|
||||
// returns the default; the wrapper post-call-overrides it.
|
||||
expected.basin_direction = basin;
|
||||
|
||||
assert_eq!(batch_profile.morphology_zone as u8, expected.morphology_zone as u8);
|
||||
assert_eq!(batch_profile.tectonic_class as u8, expected.tectonic_class as u8);
|
||||
assert_eq!(batch_profile.glaciation_grade as u8, expected.glaciation_grade as u8);
|
||||
assert_eq!(
|
||||
batch_profile.precipitation_class as u8,
|
||||
expected.precipitation_class as u8
|
||||
);
|
||||
assert_eq!(batch_profile.slope_q, expected.slope_q);
|
||||
assert_eq!(batch_profile.elev_q, expected.elev_q);
|
||||
assert_eq!(batch_profile.ocean_fraction_q, expected.ocean_fraction_q);
|
||||
assert_eq!(batch_profile.river_threshold, expected.river_threshold);
|
||||
assert_eq!(batch_profile.temperature_c, expected.temperature_c);
|
||||
assert_eq!(batch_profile.moisture_q, expected.moisture_q);
|
||||
assert_eq!(
|
||||
batch_profile.vegetation_class as u8,
|
||||
expected.vegetation_class as u8
|
||||
);
|
||||
assert_eq!(batch_profile.basin_direction as u8, expected.basin_direction as u8);
|
||||
}
|
||||
|
||||
/// D-256(c) invariant: the wrapper (`derive_district_profile`) ≡ core
|
||||
/// (`derive_at_metres`) identity holds at survey-grid edge cases — the
|
||||
/// anti-meridian column (`px` near the wrap boundary), the pole rows
|
||||
/// (latitude clamp), and edge-truncated cells (the covering pixel block
|
||||
/// itself clamped at `w`/`h`, not just the resulting position). No
|
||||
/// riparian signal threaded here (both sides get `river_network: None` /
|
||||
/// `nearby_courses: &[]` — the riparian equivalence is the DEDICATED
|
||||
/// concern of the test above; this one isolates the geometric position
|
||||
/// mapping across the grid's hard edges).
|
||||
#[test]
|
||||
fn wrapper_matches_core_at_survey_grid_edge_cases() {
|
||||
use crate::atlas::district_profile::{self, ClimateConstants};
|
||||
use crate::atlas::heightmap::load_heightmap_png;
|
||||
use crate::atlas::scale::{self, BasinDirection, SurveyCellPos};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// The committed GJ1c heightmap (same fixture the other integration
|
||||
// tests in this module use), downsampled to the standard 256×128
|
||||
// working grid so the survey raster is a real body's shape.
|
||||
let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png");
|
||||
let heightmap =
|
||||
load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap");
|
||||
let hm = heightmap.downsample(256, 128);
|
||||
let dr = crate::atlas::drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
|
||||
let ta = crate::atlas::features::TerrainAnalysis::analyze(&hm, &dr);
|
||||
|
||||
let params = crate::atlas::district_profile::BodyParams {
|
||||
hydrosphere: Some("ocean".into()),
|
||||
atmosphere: Some("breathable".into()),
|
||||
planet_class: Some("temperate".into()),
|
||||
body_radius_km: Some(6371.0),
|
||||
..Default::default()
|
||||
};
|
||||
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 2);
|
||||
let climate = ClimateConstants::default();
|
||||
let gcpr = scale::HEIGHTMAP_CELLS_PER_DISTRICT;
|
||||
let survey_cols = (hm.width as usize).div_ceil(gcpr) as i32; // 32
|
||||
let survey_rows = (hm.height as usize).div_ceil(gcpr) as i32; // 16
|
||||
|
||||
let cases: &[(&str, SurveyCellPos)] = &[
|
||||
("west edge / row 0 corner", SurveyCellPos(0, 0)),
|
||||
(
|
||||
"anti-meridian column (max col)",
|
||||
SurveyCellPos(survey_cols - 1, survey_rows / 2),
|
||||
),
|
||||
("north pole row", SurveyCellPos(survey_cols / 2, 0)),
|
||||
(
|
||||
"south pole row",
|
||||
SurveyCellPos(survey_cols / 2, survey_rows - 1),
|
||||
),
|
||||
(
|
||||
"edge-truncated SE corner",
|
||||
SurveyCellPos(survey_cols - 1, survey_rows - 1),
|
||||
),
|
||||
];
|
||||
|
||||
for &(label, cell) in cases {
|
||||
let batch_profile = district_profile::derive_district_profile(
|
||||
seed,
|
||||
¶ms,
|
||||
&ta,
|
||||
cell,
|
||||
gcpr,
|
||||
&climate,
|
||||
"GJ1c",
|
||||
&BTreeMap::new(),
|
||||
BasinDirection::North, // default — no override under test here
|
||||
None,
|
||||
);
|
||||
let (world_x_m, world_y_m) = district_profile::survey_cell_centre_world_m(
|
||||
cell,
|
||||
gcpr,
|
||||
ta.w,
|
||||
ta.h,
|
||||
params.body_radius_km,
|
||||
);
|
||||
let expected = district_profile::derive_at_metres(
|
||||
seed,
|
||||
"GJ1c",
|
||||
¶ms,
|
||||
&ta,
|
||||
world_x_m,
|
||||
world_y_m,
|
||||
&climate,
|
||||
0.0,
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
batch_profile.morphology_zone as u8, expected.morphology_zone as u8,
|
||||
"[{label}] morphology_zone mismatch at {cell:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
batch_profile.elev_q, expected.elev_q,
|
||||
"[{label}] elev_q mismatch at {cell:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
batch_profile.slope_q, expected.slope_q,
|
||||
"[{label}] slope_q mismatch at {cell:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
batch_profile.ocean_fraction_q, expected.ocean_fraction_q,
|
||||
"[{label}] ocean_fraction_q mismatch at {cell:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
batch_profile.temperature_c, expected.temperature_c,
|
||||
"[{label}] temperature_c mismatch at {cell:?}"
|
||||
);
|
||||
assert_eq!(
|
||||
batch_profile.moisture_q, expected.moisture_q,
|
||||
"[{label}] moisture_q mismatch at {cell:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Discipline item 3(a), mandatory: two overlapping windows sharing a
|
||||
/// stretch of the same edge must produce BYTE-IDENTICAL course points
|
||||
/// for that shared stretch (Ruling 1e, the window-independence
|
||||
@@ -5226,6 +5490,7 @@ mod tests {
|
||||
heightmap: vec![],
|
||||
heightmap_width: 16,
|
||||
heightmap_height: 8,
|
||||
sea_level: 0.3,
|
||||
river_network: RiverNetwork::default(),
|
||||
drainage_basins: vec![],
|
||||
attractors: vec![],
|
||||
@@ -5572,6 +5837,7 @@ mod tests {
|
||||
heightmap: vec![0.0; 4],
|
||||
heightmap_width: 2,
|
||||
heightmap_height: 2,
|
||||
sea_level: 0.3,
|
||||
river_network: RiverNetwork::default(),
|
||||
drainage_basins: vec![],
|
||||
attractors: vec![],
|
||||
|
||||
Reference in New Issue
Block a user