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