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:
@@ -97,7 +97,8 @@ pub enum GenWorkItem {
|
||||
/// `context` is the D-199 economic read-set pre-resolved at dispatch time.
|
||||
/// All 6 required fields must be populated before this item is submitted
|
||||
/// (D-199: "Missing fields abort the task … generation does not proceed with
|
||||
/// partial context").
|
||||
/// partial context") — EXCEPT `morphology_zone`, which is resolved during
|
||||
/// execution (see below, D-256(d)).
|
||||
///
|
||||
/// `body_id` routes the resulting `SkeletonGenerated` completion into the
|
||||
/// correct `BodyWorldState` cache entry (D-230).
|
||||
@@ -112,6 +113,11 @@ pub enum GenWorkItem {
|
||||
city_id: u64,
|
||||
body_id: String,
|
||||
/// D-199 economic read-set + all other context fields.
|
||||
///
|
||||
/// `context.morphology_zone` still starts life as
|
||||
/// `context_from_read_set`'s `AlluvialPlain` stub at dispatch time — it
|
||||
/// is overwritten during execution (D-256(d), see `settlement_world_m`
|
||||
/// below), not by `build_skeleton_work_item` as it was pre-D-256.
|
||||
context: Box<CityGenerationContext>,
|
||||
/// Stable content-addressable quarter id (D-194/D-230).
|
||||
quarter_id: u64,
|
||||
@@ -130,6 +136,33 @@ pub enum GenWorkItem {
|
||||
/// regardless of its contents' size (same reasoning already documented
|
||||
/// for `FillChunk.block_tags` below), so this needs no `Box`.
|
||||
exterior_catalog: ExteriorCatalog,
|
||||
/// D-256(d) exact-position `morphology_zone` judgment inputs — the
|
||||
/// settlement's own world metres (NOT the survey cell's centre, which
|
||||
/// can be hundreds of km off for a settlement near a survey-cell edge)
|
||||
/// and body params, resolved during `run_work_item` execution (where
|
||||
/// the terrain cache is reachable, unlike `BodyWorldState`'s
|
||||
/// D-203/T-1048 dropped `TerrainAnalysis`) via `derive_at_metres`.
|
||||
/// `None` body_params (no DB row for this body) skips the resolution
|
||||
/// and leaves `context.morphology_zone` at its `AlluvialPlain` stub —
|
||||
/// the same fallback the pre-D-256 dispatch-time lookup used for an
|
||||
/// empty district grid.
|
||||
settlement_world_m: (f64, f64),
|
||||
body_params: Option<Box<BodyParams>>,
|
||||
/// `SeedChain::for_body(world_seed, body_id)` — the SAME body-scoped
|
||||
/// chain `derive_all_districts`/`derive_district_profile` use, distinct
|
||||
/// from `chain` (the quarter-level chain derived further for skeleton
|
||||
/// RNG). Needed because `derive_at_metres`'s domain-separated warp
|
||||
/// fields must key on the same seed the rest of the cascade uses for
|
||||
/// this body, not a chain re-derived from the quarter seed.
|
||||
body_seed: SeedChain,
|
||||
/// Shared per-body working-grid heightmap (D-256(d)) — an `Arc` so
|
||||
/// every settlement dispatched for the same `BodyAnalyzed` completion
|
||||
/// clones a pointer, not the multi-hundred-KB `Vec<f32>`. Reconstructed
|
||||
/// once at dispatch time from `BodyWorldState.heightmap`/dims/`sea_level`
|
||||
/// (already in memory — no disk re-read) so `run_work_item` can feed
|
||||
/// [`TerrainAnalysisCache::get_or_derive`] without touching the
|
||||
/// filesystem on the Rayon worker thread.
|
||||
heightmap: std::sync::Arc<crate::atlas::heightmap::BodyHeightmap>,
|
||||
},
|
||||
/// Derive the building shell for one 64 m chunk of an existing quarter
|
||||
/// (D-230 derive phase, T-987).
|
||||
@@ -617,8 +650,12 @@ impl Default for GenerationQueue {
|
||||
/// itself is small (a `RiverNetwork` + basin list + attractor list, not the
|
||||
/// full grid) relative to `TerrainAnalysis`'s ~1.5–2 MB of dense per-cell
|
||||
/// Vecs.
|
||||
/// `pub(crate)` (D-256(d), T-1174): `plugin.rs`'s tests construct one
|
||||
/// directly to unit-test `resolve_settlement_morphology_zone` without
|
||||
/// spinning up a full `GenerationQueue`. Otherwise entirely internal to this
|
||||
/// module's Rayon-thread execution path.
|
||||
#[derive(Debug)]
|
||||
struct TerrainAnalysisCache {
|
||||
pub(crate) struct TerrainAnalysisCache {
|
||||
entries: std::collections::BTreeMap<String, (Layer1Output, TerrainAnalysis, u64)>,
|
||||
/// Monotonic access counter (substitutes for `BodyWorldStateCache`'s
|
||||
/// `SimTick` — there is no tick concept on a background Rayon thread).
|
||||
@@ -639,6 +676,13 @@ impl TerrainAnalysisCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Test-only constructor alias (D-256(d)) — `new` stays private-module
|
||||
/// idiomatic; this is the `pub(crate)` door for `plugin.rs`'s tests.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn new_for_test(capacity: usize) -> Self {
|
||||
Self::new(capacity)
|
||||
}
|
||||
|
||||
/// Look up a cached `(Layer1Output, TerrainAnalysis)` pair for `body_id`,
|
||||
/// re-deriving via `run_layer1` on a miss and inserting the result
|
||||
/// (evicting the LRU entry first if at capacity). Bumps the access clock
|
||||
@@ -688,6 +732,46 @@ impl TerrainAnalysisCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// D-256(d): resolve a settlement's `MorphologyZone` at its EXACT world
|
||||
/// position via `derive_at_metres`, using the terrain cache to reach a
|
||||
/// `TerrainAnalysis` without a disk re-read (`heightmap` is already in
|
||||
/// memory, reconstructed once at dispatch time from `BodyWorldState`'s
|
||||
/// cached working-grid data).
|
||||
///
|
||||
/// Returns `None` when `body_params` is absent (no DB row for this body) —
|
||||
/// the caller then leaves `context.morphology_zone` at its dispatch-time
|
||||
/// `AlluvialPlain` stub, the same fallback the pre-D-256 dispatch-time
|
||||
/// district-grid lookup used for an empty grid.
|
||||
///
|
||||
/// Extracted from `run_work_item`'s `GenerateSkeleton` arm so the resolution
|
||||
/// itself is unit-testable without going through the full skeleton-generation
|
||||
/// pipeline (`QuarterSkeleton` does not expose `morphology_zone` directly —
|
||||
/// it only affects derived fields like `layout_mode`/corridors).
|
||||
pub(crate) fn resolve_settlement_morphology_zone(
|
||||
terrain_cache: &Arc<Mutex<TerrainAnalysisCache>>,
|
||||
body_id: &str,
|
||||
body_params: Option<&BodyParams>,
|
||||
settlement_world_m: (f64, f64),
|
||||
body_seed: SeedChain,
|
||||
heightmap: &crate::atlas::heightmap::BodyHeightmap,
|
||||
) -> Option<crate::simulation::generator::MorphologyZone> {
|
||||
let params = body_params?;
|
||||
let (_l1, ta) = terrain_cache.lock().unwrap().get_or_derive(body_id, heightmap);
|
||||
let climate = ClimateConstants::default();
|
||||
let profile = crate::atlas::district_profile::derive_at_metres(
|
||||
body_seed,
|
||||
body_id,
|
||||
params,
|
||||
&ta,
|
||||
settlement_world_m.0,
|
||||
settlement_world_m.1,
|
||||
&climate,
|
||||
0.0,
|
||||
&[],
|
||||
);
|
||||
Some(profile.morphology_zone)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Work execution stub
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -761,13 +845,36 @@ fn run_work_item(
|
||||
population,
|
||||
founding_age_years,
|
||||
exterior_catalog,
|
||||
settlement_world_m,
|
||||
body_params,
|
||||
body_seed,
|
||||
heightmap,
|
||||
} => {
|
||||
// D-256(d): resolve morphology_zone at the settlement's EXACT world
|
||||
// position, here at execution time — this is where TerrainAnalysis
|
||||
// is reachable (BodyWorldState drops it, D-203/T-1048). A survey
|
||||
// cell's centre can be hundreds of km from a settlement near its
|
||||
// edge; the exact-position derive closes that annotation-vs-canvas
|
||||
// disagreement class before T-1181/T-1182 draw batch-judged
|
||||
// annotations onto window-derived canvases.
|
||||
let mut resolved_context = (**context).clone();
|
||||
if let Some(zone) = resolve_settlement_morphology_zone(
|
||||
terrain_cache,
|
||||
body_id,
|
||||
body_params.as_deref(),
|
||||
*settlement_world_m,
|
||||
*body_seed,
|
||||
heightmap,
|
||||
) {
|
||||
resolved_context.morphology_zone = zone;
|
||||
}
|
||||
|
||||
// Build the Phase 1 skeleton from the pre-resolved D-199 context.
|
||||
// `economic_role`, `population`, and `founding_age_years` are the
|
||||
// D-199 raw fields carried alongside the context because
|
||||
// `generate_quarter_skeleton` accepts them as separate parameters.
|
||||
let skeleton = generate_quarter_skeleton(
|
||||
context,
|
||||
&resolved_context,
|
||||
*population,
|
||||
economic_role,
|
||||
*quarter_id,
|
||||
@@ -780,7 +887,7 @@ fn run_work_item(
|
||||
// same pass.
|
||||
let block_tags = assign_all_block_tags(
|
||||
&skeleton,
|
||||
context,
|
||||
&resolved_context,
|
||||
economic_role,
|
||||
*founding_age_years,
|
||||
*chain,
|
||||
@@ -824,9 +931,13 @@ fn run_work_item(
|
||||
Ok(hm) => {
|
||||
// Same GRID_W×GRID_H downsample AnalyzeBody applies (D-202) — the
|
||||
// window derive must run on the SAME working-grid resolution the
|
||||
// whole-body cascade uses, or district positions between the two
|
||||
// views would disagree (derive_district maps DistrictPos through
|
||||
// ta.w/ta.h, T-1137 decision note).
|
||||
// whole-body cascade uses, or the TRUE DistrictPos → world-metres
|
||||
// mapping between the two views would disagree (derive_district
|
||||
// maps DistrictPos through ta.w/ta.h, T-1137 decision note). This
|
||||
// is resolution consistency (ta.w/ta.h must match), a DIFFERENT
|
||||
// concern from D-256's survey-raster/true-district namespace
|
||||
// collision — both views here already address the true D-243
|
||||
// grid, so D-256 doesn't touch this comment's claim.
|
||||
let working = if hm.width > GRID_W || hm.height > GRID_H {
|
||||
hm.downsample(GRID_W, GRID_H)
|
||||
} else {
|
||||
@@ -990,6 +1101,20 @@ mod tests {
|
||||
population: 500_000,
|
||||
founding_age_years: 200,
|
||||
exterior_catalog: ExteriorCatalog::default(),
|
||||
// These tests exercise queue mechanics, not the D-256(d)
|
||||
// exact-position resolution — `body_params: None` skips it
|
||||
// entirely (the same fallback path an empty district grid used
|
||||
// pre-D-256), so the position/heightmap values below are inert.
|
||||
settlement_world_m: (0.0, 0.0),
|
||||
body_params: None,
|
||||
body_seed: SeedChain::for_body(42 + city_id, &format!("TestBody{city_id}")),
|
||||
heightmap: std::sync::Arc::new(crate::atlas::heightmap::BodyHeightmap {
|
||||
body_id: format!("TestBody{city_id}"),
|
||||
width: 1,
|
||||
height: 1,
|
||||
data: vec![0.5],
|
||||
sea_level: 0.3,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user