From 677de42bd26d666ea38eadcc4340b05ecc781295 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sun, 28 Jun 2026 21:43:39 +0200 Subject: [PATCH] fix(simulation): make voxel relief sub-district + fix integration-test fallout (T-1081) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- governance/decisions/architecture.md | 2 +- governance/questions/architecture.md | 2 +- server/src/atlas/detail_scatter.rs | 12 ++-- server/src/atlas/voxel.rs | 10 +++- server/tests/derivation_harness.rs | 64 +++++++++++---------- server/tests/golden/believability.json | 4 +- server/tests/golden/derivation_harness.json | 2 +- 7 files changed, 53 insertions(+), 43 deletions(-) diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index f5394c9ac..c5cdf1466 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -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 "0–3 m, no hills to navigate by" bug). A `voxel_relief` pass (`detail_scatter.rs`, the `terrain_detail` fBm at the **0.25–2 km octave band** rather than the district 4–40 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 (~40–78 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 "0–3 m, no hills to navigate by" bug). A `voxel_relief` pass (`detail_scatter.rs`, the `terrain_detail` fBm at a **0.13–1 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 4–40 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 (~40–78 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) --- diff --git a/governance/questions/architecture.md b/governance/questions/architecture.md index 2a7a5cdc9..50fca0f86 100644 --- a/governance/questions/architecture.md +++ b/governance/questions/architecture.md @@ -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 0–100 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 0–100 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). diff --git a/server/src/atlas/detail_scatter.rs b/server/src/atlas/detail_scatter.rs index e317c0e2f..a23271491 100644 --- a/server/src/atlas/detail_scatter.rs +++ b/server/src/atlas/detail_scatter.rs @@ -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.25–2 km band that the -/// district-tier [`terrain_detail`] (4–40 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.13–1 km **sub-district** band +/// (all finer than the 2 km district planning unit) that the district-tier +/// [`terrain_detail`] (4–40 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] diff --git a/server/src/atlas/voxel.rs b/server/src/atlas/voxel.rs index 16629448e..c71f12067 100644 --- a/server/src/atlas/voxel.rs +++ b/server/src/atlas/voxel.rs @@ -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.25–2 km rolling/ridged hills added to the family base +/// ceiling for the ≈0.13–1 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, diff --git a/server/tests/derivation_harness.rs b/server/tests/derivation_harness.rs index 7d924ae65..582483b2c 100644 --- a/server/tests/derivation_harness.rs +++ b/server/tests/derivation_harness.rs @@ -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; diff --git a/server/tests/golden/believability.json b/server/tests/golden/believability.json index 323a2457d..bf7235830 100644 --- a/server/tests/golden/believability.json +++ b/server/tests/golden/believability.json @@ -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, diff --git a/server/tests/golden/derivation_harness.json b/server/tests/golden/derivation_harness.json index d777174ae..6bd180592 100644 --- a/server/tests/golden/derivation_harness.json +++ b/server/tests/golden/derivation_harness.json @@ -20,7 +20,7 @@ "terrain": 5, "vegetation": 0, "water": 0, - "elevation_m": 21, + "elevation_m": 36, "cover": 0 }, {