fix(simulation): make voxel relief sub-district + fix integration-test fallout (T-1081)

The push gate's integration tests (not in cargo test --lib) caught two regressions
from the relief change:

- derivation_harness::cross_district_elevation_blend_reduces_seam_step — the relief's
  large per-position term (span 300 → ±200 m, far above the compressed elev_q/2 base)
  swamped the T-1042 seam measurement and clamped heavily at sea level. Fixes:
  (1) shift the voxel-relief octave band to 0.13–1 km (all sub-district, dropping the
  2 km octave that competed with elev_q's district role); (2) reduce VOXEL_RELIEF_SPAN_M
  300 → 100 so relief stays mostly below the base (fewer sea-level clamp artifacts,
  proportional hills); (3) rewrite the seam test's avg_elev to average over an 8 km
  multi-wavelength y-transect so the zero-mean relief cancels, isolating the base seam.
- derivation_harness::golden_seed_determinism_regression — legitimate golden refresh
  (determinism still holds; elevation values changed intentionally).

Believability relief now ≈22 m mean (was 5 m flat; 59 m at the over-aggressive span 300)
— navigable hills without clamp artifacts, 7/8 criteria. Both goldens regenerated; full
cargo test (38 binaries) + clippy --all-targets -D warnings green. D-239 amendment +
Q-123 item 4 updated to the final span/octave/numbers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 21:43:39 +02:00
co-authored by Claude Opus 4.8
parent dc1d905ce0
commit 677de42bd2
7 changed files with 53 additions and 43 deletions
+1 -1
View File
@@ -1778,7 +1778,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
- **Implementation note (T-1025/T-1027, 2026-06-08):** §6's frozen 17-zone `MorphologyZone` enum is realised in `server/src/simulation/generator.rs` (`repr(u8)`, discriminant-pinned). The §5 8-family gated classifier + §7 compatibility-matrix invariants live in `derive_morphology_zone` (`region_profile.rs`). **16 of the 17 zones are reachable at RegionProfile scale; `BraidedPlain` is the exception** — distinguishing it from `Delta` needs a lithology signal (§8 Gravel→braided) that `RegionProfile` does not carry, so `BraidedPlain` is **deferred to ChunkContext** sub-classification. The §7 compatibility invariant is enforced as a **classifier-gate-ordering** property (a build-time test that a single region cannot yield a forbidden pair), per §7's "build-time test" language; genuine cross-region sharp seams (cliff↔fjord, lithology faults) remain permitted. §2 climate-derived fields (`precipitation_class`, `glaciation_grade`, `vegetation_class`) all derive from the temperature(+moisture) primitive (T-1025).
- **Amendment (T-1082, 2026-06-28):** §5's family set grows from **8 to 9** — a `WaterBody` generator family is added for the water zones **OpenOcean / Lake / TidalFlat**, which previously fell through to the dry-land `AlluvialPlain` fallback (the D-245 believability bug: oceans rendered as dry forested land — `water=Dry` + `Forest` on the sea). `WaterBody` lays `Water::Deep`/`Shallow` (a Shallow shoal band on the district-anchored coast line `coast_anchor_m`, Deep beyond; TidalFlat is all-Shallow), a seabed `TerrainMaterial` (`Rock` on steep districts / `Sand` on gentle / `Wetland` for tidal mud), `Vegetation::Barren`, the water surface at `elevation_m = 0` (the §8 mouths-at-sea-level convention), and seasonal `Ice` via `derive_cover` (frozen seas). This **refines §6**: open ocean / lake / tidal flat are still derived zone *labels*, but their *generator* is now `WaterBody`, not `AlluvialPlain` — so §6's "tidal flat … not [a] distinct generator famil[y]" is superseded for the dispatch of those three zones. `Wetland` stays a land zone on `AlluvialPlain` (a marsh — saturated ground, not open water). Lives in `server/src/atlas/voxel.rs` (`MorphologyFamily::WaterBody` + `generate_water_body`, dispatched by `zone_to_family`); verified by the T-1083 believability harness (water-renders-wet flips PASS for Arbour + Edict). The §5 8-family decision-tree classifier (`derive_morphology_zone`) is unchanged — this is a voxel-tier *generator* family, selected by zone, not a new RegionProfile classifier branch.
- **Amendment (T-1080, 2026-06-28):** §2's **moisture** primitive is now a **body ceiling × per-district spatial gradient**, not a single body constant. The hydrosphere+atmosphere value (`derive_moisture_q`) is the *wettest* a district on the body can be; per-district **latitude** (equator wet → pole dry), **elevation** (orographic / rain-shadow), and **continentality** (`100 ocean_fraction_q`, coast wet → interior dry) subtract from it. Integer (D-010); coefficients in `ClimateConstants` / `climate_constants.toml` `[moisture_gradient]` (provisional, Q-123 calibrates). This mirrors D-240's latitude-graded *temperature* model and fixes the T-1080 believability bug (`moisture_q = 80` for all 2 048 districts → uniform vegetation/terrain — "nothing to fuzz"). It **propagates**: `precipitation_class`, `vegetation_class`, and the `morphology_zone` Wetland gate all read `moisture_q`, so those diversify for free. Lives in `district_profile.rs::derive_moisture_q`; verified by the T-1083 believability harness (moisture-gradient flips PASS, `distinct` 1 → ~49 on Arbour). The body-scale ceiling keeps each world's character; the gradient varies it within.
- **Amendment (T-1081, 2026-06-28):** the voxel tier now carries **mid-scale relief** (D-243 §2's invented terrain). Before, the family generators set `elevation_m` from `elev_q` at a compressed scale plus only ±4 m per-voxel micro-scatter — the walkable surface read flat (the D-245 "03 m, no hills to navigate by" bug). A `voxel_relief` pass (`detail_scatter.rs`, the `terrain_detail` fBm at the **0.252 km octave band** rather than the district 440 km band, body-global `SeedDomain::VoxelRelief` seed, position-keyed; the `f64` perturbation truncated to integer metres before assignment — D-010) is added post-dispatch in `derive_voxel_column`. Two load-bearing choices: **(a) the relief envelope is `slope_q·3 + elev_q`, not slope alone** — the coarse heightmap (~4078 km/px) yields `slope_q ≈ 0` even on high ground (observed max 13 on Arbour), so gating on slope would invent nothing; folding elevation in makes high terrain read rugged and coastal flats stay gentle. **(b) Only the flat families** (AlluvialPlain / LavaField / BraidedDelta / DuneStrand / MeanderReach) take it — CliffCoast / FjordWall / IncisedGorge already generate strong internal relief and a position-varying field would warp those features (e.g. drown a gorge's wall-to-floor drop); WaterBody stays at sea level. The relief **span** (`VOXEL_RELIEF_SPAN_M`) and the believability voxel-relief threshold are provisional (Q-123 calibrates, like the moisture coefficients). Verified by the T-1083 harness — a new `voxel_relief` contrast metric (mean within-district elevation range across a district-spanning transect, since a single 64 m sample chunk is narrower than the relief band) flips PASS (≈59 m on Arbour + Edict). **Deferred:** the *absolute* elevation span is still the compressed `elev_q/N` base — a per-body hypsometric relief model (so a body's true max relief sets the ceiling, not a constant) is a later refinement; the mid-scale relief gives navigable hills now without it.
- **Amendment (T-1081, 2026-06-28):** the voxel tier now carries **mid-scale relief** (D-243 §2's invented terrain). Before, the family generators set `elevation_m` from `elev_q` at a compressed scale plus only ±4 m per-voxel micro-scatter — the walkable surface read flat (the D-245 "03 m, no hills to navigate by" bug). A `voxel_relief` pass (`detail_scatter.rs`, the `terrain_detail` fBm at a **0.131 km sub-district octave band** — all finer than the 2 km district, so it never competes with `elev_q`'s district-scale role — rather than the district 440 km band; body-global `SeedDomain::VoxelRelief` seed, position-keyed; the `f64` perturbation truncated to integer metres before assignment — D-010) is added post-dispatch in `derive_voxel_column`. Two load-bearing choices: **(a) the relief envelope is `slope_q·3 + elev_q`, not slope alone** — the coarse heightmap (~4078 km/px) yields `slope_q ≈ 0` even on high ground (observed max 13 on Arbour), so gating on slope would invent nothing; folding elevation in makes high terrain read rugged and coastal flats stay gentle. **(b) Only the flat families** (AlluvialPlain / LavaField / BraidedDelta / DuneStrand / MeanderReach) take it — CliffCoast / FjordWall / IncisedGorge already generate strong internal relief and a position-varying field would warp those features (e.g. drown a gorge's wall-to-floor drop); WaterBody stays at sea level. The relief **span** (`VOXEL_RELIEF_SPAN_M = 100`) and the believability voxel-relief threshold (≥ 8 m) are provisional (Q-123 calibrates, like the moisture coefficients). The span is held modest deliberately: relief sits on the compressed `elev_q/N` base (max ~50 m) and `elevation_m` clamps at 0 (sea level), so an oversized span clamps away on low ground (drowning the relief, biasing it positive) — a bigger span belongs with the deferred absolute-elevation model. Verified by the T-1083 harness — a new `voxel_relief` contrast metric (mean within-district elevation range across a district-spanning transect, since a single 64 m sample chunk is narrower than the relief band) flips PASS (≈22 m on Arbour + Edict). **Deferred:** the *absolute* elevation span is still the compressed `elev_q/N` base — a per-body hypsometric relief model (so a body's true max relief sets the ceiling AND gives the relief headroom to grow without clamping) is a later refinement; the mid-scale relief gives navigable hills now without it.
- **Cross-reference:** [D-227](#d-227) (derive-don't-store voxel model), [D-228](#d-228) (composite tile axes / cohesion / seasonal state), [D-210](#d-210) (temperature proxy — formalised), [D-203](#d-203) (BodyWorldState cache), [D-206](#d-206) (background analysis pass), [D-208](#d-208) (drainage / D8), [D-010](#d-010) (determinism), [D-234](#d-234) (street/footprint geometry — consumes morphology), [D-142](content.md#d-142) (zone types), [D-217](#d-217) (tile condition), [Q-102](../questions/architecture.md#q-102) (cohesion = the warp), [Q-103](../questions/architecture.md#q-103) (mutator schema — open), [Q-105](../questions/architecture.md#q-105) (seasonal/clock state — temperature/ElevationDelta forward contract)
---
+1 -1
View File
@@ -477,7 +477,7 @@ Technical foundation questions: engine, protocols, data structures, performance,
1. **Sampling power** — probes per body, number of bodies, number of seeds that constitute "anywhere" (enough that one lucky/unlucky draw is not decisive).
2. **Non-stationarity** — the similarity metric for "two same-class km² differ" (histogram distance / feature-vector cosine) and the *minimum* dissimilarity that counts as non-repeating; plus a tile-period autocorrelation ceiling (the Netherlands "no two km² alike" rule, made numeric).
3. **Intra-class variety** — minimum distinct micro-features per sampled patch (K), per terrain class (T-1084).
4. **Relief floor** — minimum relief variance, *conditioned on the macro terrain's expectation* (a plain's floor ≠ a montane floor) (T-1081). *(T-1081 landed the metric — `contrast.voxel_relief_m`, mean within-district elevation range over a 2 km transect — with a provisional `≥ 8 m` threshold and a provisional `VOXEL_RELIEF_SPAN_M = 300`; calibrate both, and make the floor macro-conditioned rather than a single global number. Also pending: the **absolute** elevation span is still the compressed `elev_q/N` base a per-body hypsometric model would set true relief ceilings. T-1081 review notes to fold in: (i) sanity-check the `slope_q*3 + elev_q` envelope on a **synthetic high-slope body** — both terms saturate the same 0100 cap, so slope's 3× weight contributes little once elev_q is already high (the tectonic-class ridge-sharpening refinement would address this); (ii) the relief transect samples a single **diagonal** slice — fine for the current isotropic fBm, but revisit if an anisotropic/ridge-aligned field is added.)*
4. **Relief floor** — minimum relief variance, *conditioned on the macro terrain's expectation* (a plain's floor ≠ a montane floor) (T-1081). *(T-1081 landed the metric — `contrast.voxel_relief_m`, mean within-district elevation range over a 2 km transect — with a provisional `≥ 8 m` threshold and a provisional `VOXEL_RELIEF_SPAN_M = 100` (≈22 m mean relief on Arbour/Edict); calibrate both, and make the floor macro-conditioned rather than a single global number. The span is capped low because relief sits on the compressed `elev_q/N` base and clamps at sea level, so it can't grow without artifacts until — also pending the **absolute** elevation span moves off that compressed base to a per-body hypsometric model (which sets true relief ceilings *and* gives the span headroom). T-1081 review notes to fold in: (i) sanity-check the `slope_q*3 + elev_q` envelope on a **synthetic high-slope body** — both terms saturate the same 0100 cap, so slope's 3× weight contributes little once elev_q is already high (the tectonic-class ridge-sharpening refinement would address this); (ii) the relief transect samples a single **diagonal** slice — fine for the current isotropic fBm, but revisit if an anisotropic/ridge-aligned field is added.)*
5. **Coherence** — the vegetation ↔ (moisture / water-distance / slope / aspect) correlation floor; the water-zone-renders-wet check (T-1082); the allowed "impossible combo" set (should be empty). *(T-1082 review note: the current 50% "water renders wet" floor is weak for ocean-dominant bodies — calibrate it **per zone**: post-T-1082 an `OpenOcean` district should render wet ~100% (Dry is impossible), while a mixed coastal district may legitimately read partial.)* Also calibrate the **vegetation-present** denominator: dividing by *all* sampled districts (incl. water) makes the criterion structurally unreachable for a 2/3-ocean world — it should be **land-relative** (exclude water-body districts).
6. **Climate-appropriateness** — how "*structured-sparse* passes / *blank* fails" is measured on cold/arid bodies, so a frozen world is not failed for being legitimately sparse.
7. **Pass budget + ratchet** — the % of land probes that must pass to ship (budgeted), and the trigger to ratchet advisory → strict (hard push-gate block).
+7 -5
View File
@@ -24,11 +24,13 @@ use crate::seed::splitmix64;
/// coarsest (~33 km) the heightmap itself carries the shape.
const OCTAVE_WAVELENGTHS_M: [f64; 4] = [32_768.0, 16_384.0, 8_192.0, 4_096.0];
/// Voxel-tier octave wavelengths in metres — the ≈0.252 km band that the
/// district-tier [`terrain_detail`] (440 km) is too coarse to carry and the
/// per-voxel scatter (<64 m) is too fine to reach. This is the [`voxel_relief`]
/// band: the rolling/ridged hills a *walking character* navigates by (T-1081).
const VOXEL_OCTAVE_WAVELENGTHS_M: [f64; 4] = [2_048.0, 1_024.0, 512.0, 256.0];
/// Voxel-tier octave wavelengths in metres — the ≈0.131 km **sub-district** band
/// (all finer than the 2 km district planning unit) that the district-tier
/// [`terrain_detail`] (440 km) is too coarse to carry and the per-voxel scatter
/// (<64 m) is too fine to reach. This is the [`voxel_relief`] band: the rolling/ridged
/// hills a *walking character* navigates by (T-1081). The coarsest octave stays below
/// the district size so the relief never competes with `elev_q`'s district-scale role.
const VOXEL_OCTAVE_WAVELENGTHS_M: [f64; 4] = [1_024.0, 512.0, 256.0, 128.0];
/// Deterministic lattice value in `[-1, 1)` for an integer noise cell.
#[inline]
+8 -2
View File
@@ -334,13 +334,19 @@ fn zone_to_family(zone: &MorphologyZone) -> MorphologyFamily {
///
/// A fully derived `VoxelColumn` with all D-228 axes populated.
/// T-1081: the metre span of the voxel-tier mid-scale relief field — the amplitude
/// ceiling for the ≈0.252 km rolling/ridged hills added to the family base
/// ceiling for the ≈0.131 km rolling/ridged hills added to the family base
/// elevation. Actual relief at a voxel is `voxel_relief(...) * SPAN`, which the
/// envelope (district ruggedness) scales down on gentle terrain, so this is the
/// steep-terrain ceiling, not a uniform amplitude. Provisional — tuned against the
/// believability probe (T-1079) like the moisture gradient; per-body *absolute*
/// elevation span is a later refinement (the T-1080 climate-fields lineage).
const VOXEL_RELIEF_SPAN_M: i32 = 300;
///
/// Kept modest so relief stays mostly *below* the family base elevation: the base is
/// the compressed `elev_q/N` scale (max ~50 m) and `elevation_m` clamps at 0 (sea
/// level), so an oversized span clamps away on low ground — drowning the relief and
/// biasing it positive. A larger span belongs with the deferred per-body absolute
/// elevation model that would give the base real headroom.
const VOXEL_RELIEF_SPAN_M: i32 = 100;
pub fn derive_voxel_column(
world_seed: u64,
+33 -31
View File
@@ -1191,40 +1191,42 @@ fn cross_district_elevation_blend_reduces_seam_step() {
"T-1042: boundary blend weight must be 128 (50-50)"
);
// Derive ChunkContext for last-A with the secondary district B at 50-50 blend.
let ctx_last_a = derive_chunk_context(
seed,
body,
&district_a,
last_a_chunk,
Some((&district_b, blend_w)),
);
// First-B: no blend (interior to B).
let ctx_first_b = derive_chunk_context(seed, body, &district_b, first_b_chunk, None);
// Interior chunks: no blend.
// Interior-A context retained for the determinism sub-check (Criterion 3).
let ctx_interior_a = derive_chunk_context(seed, body, &district_a, interior_a_chunk, None);
let ctx_interior_b = derive_chunk_context(seed, body, &district_b, interior_b_chunk, None);
// Average elevation across a full 64-voxel row through each chunk.
// We scan y=0 (along the x cross-axis for this AlluvialPlain basin).
let avg_elev = |chunk_pos: ChunkPos,
ctx: &settled_reach_server::atlas::chunk_context::ChunkContext,
dist: &DistrictProfile|
-> i64 {
let base_x = chunk_pos.0 * scale::CHUNK_M;
let base_y = chunk_pos.1 * scale::CHUNK_M;
let mut sum = 0i64;
for dx in 0..scale::VOXELS_PER_CHUNK {
let col = derive_voxel_column(seed, body, dist, ctx, base_x + dx, base_y);
sum += col.elevation_m as i64;
}
sum / scale::VOXELS_PER_CHUNK as i64
};
// Average elevation over a tall multi-wavelength y-transect at a given x-column,
// deriving a ChunkContext per y-chunk. T-1081 adds a zero-mean voxel-relief term to
// elevation_m; averaged over ≥4× the coarsest relief wavelength (1024 m) it cancels,
// leaving the elev_q-derived base — the quantity T-1042's blend actually smooths. A
// single 64 m row would carry a per-chunk relief offset that swamps the seam signal.
let avg_elev =
|x_chunk: i32, blend: Option<(&DistrictProfile, u8)>, dist: &DistrictProfile| -> i64 {
const Y_CHUNKS: i32 = 128; // 8192 m ≈ 8× the 1024 m coarsest relief octave
let base_x = x_chunk * scale::CHUNK_M;
let mut sum = 0i64;
let mut n = 0i64;
for yc in 0..Y_CHUNKS {
let cp: ChunkPos = (x_chunk, yc);
let ctx = derive_chunk_context(seed, body, dist, cp, blend);
let base_y = yc * scale::CHUNK_M;
for dx in (0..scale::VOXELS_PER_CHUNK).step_by(8) {
for dy in (0..scale::VOXELS_PER_CHUNK).step_by(8) {
let col =
derive_voxel_column(seed, body, dist, &ctx, base_x + dx, base_y + dy);
sum += col.elevation_m as i64;
n += 1;
}
}
}
sum / n
};
let elev_last_a = avg_elev(last_a_chunk, &ctx_last_a, &district_a);
let elev_first_b = avg_elev(first_b_chunk, &ctx_first_b, &district_b);
let elev_interior_a = avg_elev(interior_a_chunk, &ctx_interior_a, &district_a);
let elev_interior_b = avg_elev(interior_b_chunk, &ctx_interior_b, &district_b);
// Force the 50-50 blend with B across the whole last-A column (x=31); the interiors
// and first-B column are unblended (per the boundary detection asserted above).
let elev_last_a = avg_elev(last_a_chunk.0, Some((&district_b, blend_w)), &district_a);
let elev_first_b = avg_elev(first_b_chunk.0, None, &district_b);
let elev_interior_a = avg_elev(interior_a_chunk.0, None, &district_a);
let elev_interior_b = avg_elev(interior_b_chunk.0, None, &district_b);
// Seam step = elevation gap between the blended last-A chunk and the clean first-B chunk.
let seam_step = (elev_last_a - elev_first_b).unsigned_abs() as i64;
+2 -2
View File
@@ -28,7 +28,7 @@
"morphology_zones": 9,
"vegetation_classes": 3,
"terrain_materials": 4,
"voxel_relief_m": 49
"voxel_relief_m": 20
},
"coherence": {
"water_districts": 49,
@@ -68,7 +68,7 @@
"morphology_zones": 9,
"vegetation_classes": 1,
"terrain_materials": 4,
"voxel_relief_m": 45
"voxel_relief_m": 19
},
"coherence": {
"water_districts": 25,
+1 -1
View File
@@ -20,7 +20,7 @@
"terrain": 5,
"vegetation": 0,
"water": 0,
"elevation_m": 21,
"elevation_m": 36,
"cover": 0
},
{