fix(simulation): pre-build the true-keyed region cache (PR #199 review)

Hoshe finding 1: derive_all_districts passed a permanently-empty
region_cache, so every survey cell paid four on-the-fly baseline
derivations — and derive_district_profile's docstring (build the cache
before calling in a loop) was contradicted by its sole production
caller. The cache is now pre-built on the TRUE region keys the shared
core looks up (survey-cell centre metres -> containing district ->
region ±1 ring, BTreeSet-deduped), restoring the per-body build-once
cost model. Cache-hit == cache-miss byte-identity holds by D-227 purity
(build_region_profile and the miss branch share the same expressions) —
verified by the believability harness passing against the UNCHANGED
golden. Finding 2: the cascade.rs call-site comment and the stale
module header now state the pre-build truthfully.

Found in passing, filed as T-1186 (pre-existing, out of scope here):
region_centre_latitude_deg is pole-anchored while the derive core keys
equator-anchored signed regions — northern-hemisphere baselines clamp
to +90.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 01:31:06 +02:00
co-authored by Claude Fable 5
parent 9068fcbc0b
commit 236725eb3d
2 changed files with 63 additions and 30 deletions
+4 -2
View File
@@ -306,8 +306,10 @@ pub fn run_cascade_from_heightmap(
// shared via scale::HEIGHTMAP_CELLS_PER_DISTRICT so plugin.rs
// converts CityPlacement pixel coords with the same constant.
// body_id is required for the D-243 §4 climate edge-fuzz warp
// domain separation — derive_all_districts builds the region
// cache internally.
// domain separation — derive_all_districts pre-builds the
// region-baseline cache internally on TRUE region keys (each
// survey cell's centre world metres → containing district →
// region ±1 ring; D-256(c) + PR #199 review).
//
// Layer1Output.survey_basin_dirs threads the true D8 thalweg
// direction into each DistrictProfile.basin_direction (T-1047)
+59 -28
View File
@@ -1,10 +1,14 @@
//! DistrictProfile carrier — ~1 km scale tier of the D-239 refinement chain (T-1023).
//! DistrictProfile carrier — the coarsest derivation stage of the D-239 refinement
//! chain (T-1023).
//!
//! A `DistrictProfile` is the coarsest derivation stage: ~1 km² cells (~6 000 per body,
//! roughly 80 × 75), each a pure deterministic function of
//! `(seed, body_params, terrain_analysis, pos)`. Stored in `BodyWorldState.districts`
//! so the Atlas can read zone labels without triggering voxel derivation (D-239 §10,
//! D-203).
//! In the batch pass a `DistrictProfile` summarizes one survey cell (D-256: an
//! 8×8 working-pixel block, 64×32 ≈ 2 048 cells per body), each a pure
//! deterministic function of `(seed, body_params, terrain_analysis, pos)` sampled
//! at the cell's centre world metres through the shared
//! `derive_at_metres_with_riparian` core; the on-demand pass derives the same
//! struct at any absolute world position. Stored in `BodyWorldState.districts`
//! so the Atlas can read zone labels without triggering voxel derivation
//! (D-239 §10, D-203).
//!
//! ## D-010 compliance
//!
@@ -18,7 +22,7 @@
//! enum from `simulation::generator`. The enum is the canonical freeze; adding,
//! renaming, or removing a zone requires a D-record amendment.
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
@@ -2038,23 +2042,25 @@ fn bilinear_bool(mask: &[bool], w: usize, h: usize, px: f64, py: f64) -> f32 {
///
/// `grid_cells_per_district = 8` means each survey cell is 8×8 heightmap cells.
///
/// ## Region baseline (D-256(c))
/// ## Region baseline (D-256(c), cache pre-build per PR #199 review)
///
/// No pre-built region cache here the pre-D-256 pre-build keyed on the
/// SURVEY grid's own pseudo-coordinates (`district_to_region((rx, ry))`),
/// which does not correspond to the true region a survey cell's world-metres
/// centre actually falls in (the survey grid can span up to ~2048 distinct
/// true regions on a big body — it covers the whole body surface in metres,
/// not a handful of degenerate cells). Since the shared core now floor-divides
/// the survey cell's TRUE world metres for its own region lookup
/// ([`derive_at_metres_with_riparian`]'s `district_pos`), the simplest
/// deterministic option is to let each cell resolve its region baseline
/// on-the-fly through [`derive_district_profile`]'s own `region_cache`
/// parameter — passing an empty map here means every lookup misses and
/// derives on the fly (pure, cheap, four `derive_region_baseline_c` calls per
/// miss; D-227-legal). This also auto-fixes the former body-uniform
/// region-(0,0) climate collapse: every survey cell now reads its OWN
/// region's baseline instead of the pseudo-grid's degenerate region index.
/// A region-baseline cache is pre-built here on the TRUE region keys the
/// shared core looks up: each survey cell's centre world metres floor-divides
/// to its containing district (the same mapping
/// [`derive_at_metres_with_riparian`] applies internally), and that
/// district's region ±1 neighbour ring covers every key
/// [`region_profile::region_baseline_at_district`]'s edge-fuzz blend can
/// read (base + signed x/y/xy neighbours). The pre-D-256 pre-build keyed on
/// the SURVEY grid's own pseudo-coordinates (`district_to_region((rx, ry))`)
/// — the wrong key space entirely, which is what produced the body-uniform
/// region-(0,0) climate collapse; the true-key rebuild can span up to ~2048
/// distinct regions plus ring on a big body (the survey grid covers the
/// whole body surface in metres). Cache-hit and cache-miss are
/// byte-identical ([`region_profile::build_region_profile`] and the miss
/// branch in `region_baseline_at_district` compute `mean_temp_c` with the
/// same expressions — D-227 purity, guarded by the wrapper≡core agreement
/// tests), so the cache is purely the cost model: each covering region
/// derives once per body instead of four misses per survey cell.
///
/// `body_id` is the body's string identifier, required for the climate edge-fuzz
/// warp domain separation.
@@ -2089,11 +2095,36 @@ pub fn derive_all_districts(
let survey_cols = ta.w.div_ceil(gcpr) as i32;
let survey_rows = ta.h.div_ceil(gcpr) as i32;
// D-256(c): no pre-built region cache (see this function's doc) — every
// survey cell derives its region baseline on-the-fly through the empty
// map below, keyed on the cell's TRUE world-metres region, not a
// pseudo-grid index.
let region_cache: BTreeMap<RegionPos, RegionProfile> = BTreeMap::new();
// Pre-build the region-baseline cache on the TRUE region keys the shared
// core will look up (see this function's doc): each survey cell's centre
// world metres → containing district → region ±1 ring. Deduped via
// BTreeSet (D-010: deterministic iteration), derived once per body.
let mut covering_regions: BTreeSet<RegionPos> = BTreeSet::new();
for ry in 0..survey_rows {
for rx in 0..survey_cols {
let (wx, wy) = survey_cell_centre_world_m(
SurveyCellPos(rx, ry),
gcpr,
ta.w,
ta.h,
body_params.body_radius_km,
);
// The SAME floor-divide derive_at_metres_with_riparian applies to
// reach its region key — one mapping, never a second one.
let district_pos: DistrictPos = (
(wx / scale::DISTRICT_M as f64).floor() as i32,
(wy / scale::DISTRICT_M as f64).floor() as i32,
);
let base = scale::district_to_region(district_pos);
for ndy in -1..=1i32 {
for ndx in -1..=1i32 {
covering_regions.insert((base.0 + ndx, base.1 + ndy));
}
}
}
}
let region_cache =
region_profile::derive_regions_for_body(seed, body_params, &climate, covering_regions);
let mut out = BTreeMap::new();
for ry in 0..survey_rows {