feat(simulation): canonical spatial scale ladder + body_radius_km (T-1077)
First phase of the D-243 re-scale. Adds server/src/atlas/scale.rs as the single source of truth for the metric containment ladder (voxel 1m -> chunk 64m -> block 128m -> quarter 512m -> district 2048m -> region 204.8km), the fixed integer addressing (ChunkPos -> DistrictPos -> RegionPos), and the elastic seam regions_per_equator(R). Compile-time const asserts enforce the nesting so the rungs cannot silently drift again — the structural fix for the Q-110 failure mode (three files, three different region sizes). Wires body_radius_km (D-204) through BodyParams + the reader: the single body-specific input to the elastic seam. No behavior change — foundation only. The rename/re-scale of the carrier (RegionProfile -> DistrictProfile) and on-demand district derivation follow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
//! Canonical spatial scale ladder (D-243) — **the single source of truth** for
|
||||
//! the metric containment hierarchy. Every cascade consumer references these
|
||||
//! constants and conversions; no module re-derives its own scale.
|
||||
//!
|
||||
//! This is the *structural* fix for the Q-110 failure mode — three files had
|
||||
//! three different "region" sizes (1 024 m, 624 km, "16 chunks") because the
|
||||
//! metres-per-scale lived as scattered magic numbers. With one canonical source
|
||||
//! a file physically cannot invent a scale that disagrees, and the `const`
|
||||
//! asserts below fail the build if the rungs ever stop nesting cleanly.
|
||||
//!
|
||||
//! ```text
|
||||
//! voxel 1 m → chunk 64 m → block 128 m → quarter 512 m → district 2 048 m
|
||||
//! → region ~205 km → (elastic seam) → planet
|
||||
//! ```
|
||||
//!
|
||||
//! Below the region everything is fixed integer math; only [`regions_per_equator`]
|
||||
//! (the planetary count) varies per body. See [D-243] for the full model.
|
||||
//!
|
||||
//! [D-243]: ../../../governance/decisions/architecture.md
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Edge lengths in metres (absolute, fixed — D-243)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Voxel edge — the tile (D-228 / D-220).
|
||||
pub const VOXEL_M: i32 = 1;
|
||||
/// Chunk edge — the stream / derive unit (D-222, D-239).
|
||||
pub const CHUNK_M: i32 = 64;
|
||||
/// Block edge — generator planning unit (D-222).
|
||||
pub const BLOCK_M: i32 = 128;
|
||||
/// Quarter edge — settlement footprint cell (D-222).
|
||||
pub const QUARTER_M: i32 = 512;
|
||||
/// District edge — urban division **and** the local terrain/climate carrier cell
|
||||
/// (D-222, D-239 §2, D-243). The fine carrier `ChunkContext`/voxels consume.
|
||||
pub const DISTRICT_M: i32 = 2_048;
|
||||
/// Region edge — the top hard block: planetary grid + climate/weather/season
|
||||
/// lockdown (D-243). 100 districts on a side.
|
||||
pub const REGION_M: i32 = 204_800;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Linear nesting ratios (children per parent edge)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Voxels per chunk edge (64 m / 1 m). A chunk is 64×64 voxels.
|
||||
pub const VOXELS_PER_CHUNK: i32 = CHUNK_M / VOXEL_M;
|
||||
/// Chunks per district edge (2 048 m / 64 m = 32 = `1 << 5`).
|
||||
pub const CHUNKS_PER_DISTRICT: i32 = DISTRICT_M / CHUNK_M;
|
||||
/// `log2(CHUNKS_PER_DISTRICT)` — chunk→district is an arithmetic shift (floors
|
||||
/// toward −∞ for negative coords, the correct tiling behaviour).
|
||||
pub const CHUNK_DISTRICT_SHIFT: u32 = 5;
|
||||
/// Districts per region edge (204 800 m / 2 048 m = 100). Not a power of two,
|
||||
/// so region mapping uses division, not a shift.
|
||||
pub const DISTRICTS_PER_REGION: i32 = REGION_M / DISTRICT_M;
|
||||
|
||||
// Compile-time ladder invariants — the build fails if a rung stops nesting.
|
||||
const _: () = assert!(CHUNKS_PER_DISTRICT == 1 << CHUNK_DISTRICT_SHIFT);
|
||||
const _: () = assert!(CHUNKS_PER_DISTRICT * CHUNK_M == DISTRICT_M);
|
||||
const _: () = assert!(DISTRICTS_PER_REGION * DISTRICT_M == REGION_M);
|
||||
const _: () = assert!(DISTRICTS_PER_REGION == 100);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Grid positions + fixed integer addressing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Position on the 64 m chunk grid (chunk-units, not metres). `BTreeMap` key (D-010).
|
||||
pub type ChunkPos = (i32, i32);
|
||||
/// Position on the 2 km district grid (district-units) — the terrain/climate carrier cell.
|
||||
pub type DistrictPos = (i32, i32);
|
||||
/// Position on the ~205 km region grid (region-units) — the top hard block.
|
||||
pub type RegionPos = (i32, i32);
|
||||
|
||||
/// The district covering a chunk. Arithmetic shift floors toward −∞ so the tiling
|
||||
/// is consistent across the origin (a chunk at −1 belongs to district −1, not 0).
|
||||
#[inline]
|
||||
pub fn chunk_to_district(c: ChunkPos) -> DistrictPos {
|
||||
(c.0 >> CHUNK_DISTRICT_SHIFT, c.1 >> CHUNK_DISTRICT_SHIFT)
|
||||
}
|
||||
|
||||
/// The region covering a district. Euclidean floor division matches the shift's
|
||||
/// flooring, so chunk→district→region tiling is seamless across the origin.
|
||||
#[inline]
|
||||
pub fn district_to_region(d: DistrictPos) -> RegionPos {
|
||||
(
|
||||
d.0.div_euclid(DISTRICTS_PER_REGION),
|
||||
d.1.div_euclid(DISTRICTS_PER_REGION),
|
||||
)
|
||||
}
|
||||
|
||||
/// The region covering a chunk (chunk → district → region).
|
||||
#[inline]
|
||||
pub fn chunk_to_region(c: ChunkPos) -> RegionPos {
|
||||
district_to_region(chunk_to_district(c))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The elastic seam — region ↔ planet (the only per-body-floating quantity)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Number of ~205 km regions around the body's equator: `round(2πR / REGION_M)`,
|
||||
/// minimum 1 (D-243). `R` is `bodies.body_radius_km` (D-204) — the single
|
||||
/// body-specific input in the whole scale chain. Pole-to-pole is half this.
|
||||
pub fn regions_per_equator(body_radius_km: f64) -> u32 {
|
||||
let circumference_m = 2.0 * std::f64::consts::PI * body_radius_km * 1000.0;
|
||||
((circumference_m / REGION_M as f64).round() as i64).max(1) as u32
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ladder_nests_cleanly() {
|
||||
assert_eq!(VOXELS_PER_CHUNK, 64);
|
||||
assert_eq!(CHUNKS_PER_DISTRICT, 32);
|
||||
assert_eq!(DISTRICTS_PER_REGION, 100);
|
||||
assert_eq!(CHUNK_M * CHUNKS_PER_DISTRICT, DISTRICT_M);
|
||||
assert_eq!(DISTRICT_M * DISTRICTS_PER_REGION, REGION_M);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn addressing_floors_across_origin() {
|
||||
// chunk → district (32 chunks/district)
|
||||
assert_eq!(chunk_to_district((0, 0)), (0, 0));
|
||||
assert_eq!(chunk_to_district((31, 31)), (0, 0));
|
||||
assert_eq!(chunk_to_district((32, 32)), (1, 1));
|
||||
assert_eq!(chunk_to_district((-1, -1)), (-1, -1)); // floors, not truncates
|
||||
assert_eq!(chunk_to_district((-32, -32)), (-1, -1));
|
||||
assert_eq!(chunk_to_district((-33, -33)), (-2, -2));
|
||||
// district → region (100 districts/region)
|
||||
assert_eq!(district_to_region((99, 0)), (0, 0));
|
||||
assert_eq!(district_to_region((100, 0)), (1, 0));
|
||||
assert_eq!(district_to_region((-1, 0)), (-1, 0));
|
||||
// chunk → region (3200 chunks/region)
|
||||
assert_eq!(chunk_to_region((3199, 0)), (0, 0));
|
||||
assert_eq!(chunk_to_region((3200, 0)), (1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regions_per_equator_earth_and_moon() {
|
||||
// Earth (R≈6371 km): 2πR ≈ 40 030 km / 204.8 km ≈ 195.46 → 195.
|
||||
assert_eq!(regions_per_equator(6371.0), 195);
|
||||
// A small moon (R≈200 km): ≈ 1257 km / 204.8 ≈ 6.1 → 6.
|
||||
assert_eq!(regions_per_equator(200.0), 6);
|
||||
// Degenerate radius still yields at least one region.
|
||||
assert_eq!(regions_per_equator(0.0), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user