fix(simulation): Region's seam and speckle, and a conservation gate that runs (T-1213)

The fresh descent ladder (first since T-1240) showed Region carrying a
straight diagonal band of blocky, grid-locked stair-steps, and a scatter of
stray ValleyFloor cells. D-258 (1) says a biome edge is a gradient, never a
line. Two separate causes, each traced to a plane before it was fixed:

1. THE SEAM — the coast/terrain character read the CATEGORICAL glaciation
   grade. It flips None -> Light on the 6 C isotherm, `fjord` jumped 0 ->
   0.30, and ridge, scatter floor and warp amplitude jumped with it. Dumping
   the Region canvas planes put the seam in relief_q itself, not the paint;
   zeroing that one step removed it. The character now reads a continuous
   `glacial_weight` — linear through the grade bands' centres, where it
   agrees with the old per-grade weights, and a ramped moisture gate. The
   grade stays the classification; only the character stopped stepping.

2. THE SPECKLE — composition's rare inclusions ignored the rung's octave
   cutoff. At 379 m cells the 192 m inclusion field was sampled below
   Nyquist: instead of vanishing (what the constant's doc promised) it
   aliased, a +25 slope push scattered at random, tipping 8,453 cells into
   slope-gated ValleyFloor. Now gated like every other invented octave:
   ValleyFloor at Region 8,453 -> 0, and District/Quarter captures are
   byte-identical, since they resolve inclusions.

Region, lum p1-p99 63.45 -> 63.60 (structure kept), distinct colours
26,701 -> 2,102 (noise removed).

THE CONSERVATION GATE was `#[ignore]`d and did not compare anything: its
doc said it derived with and without composition, but it derived once and
checked some class held >50%, which a forest patch turned wholly to scrub
passes. Now every patch is derived twice through the same core (a new
`invent_composition` switch on the private derive; both production paths
pass true), the composed plurality must EQUAL the uncomposed one, across 13
patches spread over the body, with the judged count and "anything changed at
all" asserted so it cannot pass vacuously. It runs in plain `cargo test`
(~4 s debug). Mutation-proven both ways: inclusions over most ground fails
it by name (Scrub 90% -> Barren 52%), composition switched off fails it on
"the fine tier is off".

Goldens: every window-derivation change is an inclusion that a 4,096 or
1,024 m cutoff can no longer receive (e.g. slope 27 -> 2, ValleyFloor ->
AlluvialPlain); believability moved one point on two contrast figures.

Version 0.4.15: derived bytes change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 22:42:02 +02:00
co-authored by Claude Opus 5.5
parent 4baaec7e90
commit 6b50829d69
8 changed files with 410 additions and 125 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ config/name="The Settled Reach"
; in the editor and in a shipped build, where res://../project.yaml does not
; exist at all (T-1241). Kept honest by `reach check client-version`, which the
; pre-push hook runs — do not edit this by hand without moving project.yaml too.
config/version="0.4.14"
config/version="0.4.15"
run/main_scene="res://scenes/main_menu.tscn"
config/features=PackedStringArray("4.6", "GL Compatibility")
config/icon="res://icon.svg"
+6 -1
View File
@@ -71,7 +71,12 @@ name: The Settled Reach
# paths renamed to their reach verbs, T-1289/T-1253) touched
# server/src/atlas/trait_catalog_reader.rs, canvas_sources.py itself, and two
# client files in the registry. Path-based gate, spent rather than dodged.
version: 0.4.14
# 0.4.15 changes DERIVED BYTES (T-1213): the coast/terrain character reads a
# continuous glacial weight instead of the categorical grade, removing a seam
# the None->Light isotherm cut through the relief field; and the composition
# tier's rare inclusions now honour the rung's octave cutoff, so Region no
# longer carries aliased ValleyFloor speckle. Warm caches are genuinely wrong.
version: 0.4.15
repository: settled-reach
+193 -27
View File
@@ -17,7 +17,9 @@
//! rainier → higher erosion → smoother mature coasts; dry / thin-atmosphere →
//! sharp young coasts.
//! - **Tier 2 — position character** ([`coast_character_at`]): latitude,
//! driver-tier `GlaciationGrade` (high-latitude glaciated coasts go fjordy),
//! driver-tier glacial weight (high-latitude glaciated coasts go fjordy —
//! a continuous ramp over the grade bands, [`glacial_weight`], so the
//! character never steps on an isotherm either),
//! local wetness, and a seeded long-wavelength heterogeneity field
//! ([`character_field`], the T-1084 `voxel_mosaic` pattern at 100–400 km
//! octaves) so stretches of the *same* coast differ. Longitude participates
@@ -173,30 +175,88 @@ pub struct CoastCharacter {
pub ridge: f64,
}
/// Resolve the invention character at `(wx, wy)` world metres.
///
/// `driver_glaciation` / `driver_moisture_q` are the **one-step-stale** climate
/// drivers computed from the *unwarped* raw-bilinear primitives (the T-1125
/// circularity ruling — see the call site in `district_profile`).
pub fn coast_character_at(
env: &BodyCoastEnvelope,
seed: u64,
wx: f64,
wy: f64,
lat_deg: f64,
driver_glaciation: GlaciationGrade,
driver_moisture_q: i32,
) -> CoastCharacter {
// Seeded heterogeneity in [0,1]: same-coast stretches differ (Jeroen).
let hetero = character_field(seed, wx, wy);
let fjord: f64 = match driver_glaciation {
/// The fjord weight of each glaciation grade — how strongly glacial carving
/// shapes the invention character. Kept as the grade-level statement of the
/// ramp [`glacial_weight`] interpolates; [`glacial_weight`] agrees with it at
/// each band's centre.
pub fn grade_glacial_weight(grade: GlaciationGrade) -> f64 {
match grade {
GlaciationGrade::None => 0.0,
GlaciationGrade::Light => 0.30,
GlaciationGrade::Moderate => 0.70,
GlaciationGrade::Heavy => 1.0,
// Under a permanent cap the fjord carving is there but partly buried.
GlaciationGrade::IceCap => 0.90,
}
}
/// `(temperature °C, weight)` knots of the continuous glacial ramp: the warm
/// edge of Light, then the centre of each grade's band in
/// `derive_glaciation_grade_from_climate` (Light −4..5, Moderate −14..−5,
/// Heavy −29..−15, IceCap ≤ −30). Linear between knots, flat beyond the ends.
const GLACIAL_KNOTS: [(f64, f64); 5] = [
(6.0, 0.0),
(0.5, 0.30),
(-9.5, 0.70),
(-22.0, 1.0),
(-34.0, 0.90),
];
/// Continuous glacial weight in `[0, 1]` from the driver climate (T-1213).
///
/// This replaced reading the CATEGORICAL grade, which made the character step:
/// the grade flips None → Light on an isotherm, `fjord` jumped 0 → 0.30, and
/// ridge, scatter floor and warp amplitude all jumped with it. On Ferrath's
/// descent-ladder Region that isotherm crossed the canvas as a straight seam in
/// the relief field, which the hillshade drew as a stair-stepped cliff — a
/// biome edge falling on a line, which D-258 (1) and the D-243 edge-fuzz rule
/// both forbid. The grade stays the classification; only the character's
/// dependence on it is made continuous.
///
/// The moisture gate (no ice below `moisture_q` 10) is ramped over 5..15 for
/// the same reason. Airless bodies carry no glacial weight (ice is geology).
pub fn glacial_weight(temperature_c: Option<f32>, moisture_q: i32) -> f64 {
let Some(t) = temperature_c else {
return 0.0;
};
let t = t as f64;
let first = GLACIAL_KNOTS[0];
let last = GLACIAL_KNOTS[GLACIAL_KNOTS.len() - 1];
let temp_weight = if t >= first.0 {
first.1
} else if t <= last.0 {
last.1
} else {
GLACIAL_KNOTS
.windows(2)
.find(|w| t <= w[0].0 && t >= w[1].0)
.map(|w| {
let f = (w[0].0 - t) / (w[0].0 - w[1].0);
w[0].1 + f * (w[1].1 - w[0].1)
})
.unwrap_or(0.0)
};
let moisture_gate = ((moisture_q as f64 - 5.0) / 10.0).clamp(0.0, 1.0);
temp_weight * moisture_gate
}
/// Resolve the invention character at `(wx, wy)` world metres.
///
/// `fjord` (0–1, see [`glacial_weight`]) and `driver_moisture_q` are the
/// **one-step-stale** climate drivers computed from the *unwarped* raw-bilinear
/// primitives (the T-1125 circularity ruling — see the call site in
/// `district_profile`).
pub fn coast_character_at(
env: &BodyCoastEnvelope,
seed: u64,
wx: f64,
wy: f64,
lat_deg: f64,
fjord: f64,
driver_moisture_q: i32,
) -> CoastCharacter {
// Seeded heterogeneity in [0,1]: same-coast stretches differ (Jeroen).
let hetero = character_field(seed, wx, wy);
let lat_frac = (lat_deg.abs() / 90.0).clamp(0.0, 1.0);
// Local wetness echoes the tier-1 erosion chain at position scale: the wet
// stretches of a body erode smoother than its dry stretches.
@@ -362,13 +422,71 @@ mod tests {
#[test]
fn character_fjord_raises_roughness_amplitude_and_ridge() {
let env = body_coast_envelope(&params("ice", "thin", "frozen"), TectonicClass::Stable);
let temperate = coast_character_at(&env, 42, 1e6, 2e6, 40.0, GlaciationGrade::None, 40);
let fjordy = coast_character_at(&env, 42, 1e6, 2e6, 70.0, GlaciationGrade::Heavy, 40);
let temperate = coast_character_at(
&env,
42,
1e6,
2e6,
40.0,
grade_glacial_weight(GlaciationGrade::None),
40,
);
let fjordy = coast_character_at(
&env,
42,
1e6,
2e6,
70.0,
grade_glacial_weight(GlaciationGrade::Heavy),
40,
);
assert!(fjordy.roughness > temperate.roughness);
assert!(fjordy.warp_amplitude_px > temperate.warp_amplitude_px);
assert!(fjordy.ridge > temperate.ridge);
}
/// The ramp says what the grades said, where the grades are unambiguous:
/// at each band's centre it returns that grade's weight.
#[test]
fn glacial_weight_agrees_with_the_grade_at_band_centres() {
use crate::atlas::district_profile::derive_glaciation_grade_from_climate;
for t in [20.0f32, 0.5, -9.5, -22.0, -40.0] {
let grade = derive_glaciation_grade_from_climate(Some(t), 60);
assert!(
(glacial_weight(Some(t), 60) - grade_glacial_weight(grade)).abs() < 1e-9,
"at {t} °C the ramp disagrees with {grade:?}"
);
}
}
/// The reason the ramp exists: no step anywhere. The categorical version
/// jumped 0.30 across one tenth of a degree at 6 °C.
#[test]
fn glacial_weight_never_steps() {
let mut prev = glacial_weight(Some(30.0), 60);
for i in 1..=800 {
let t = 30.0 - i as f32 * 0.1;
let w = glacial_weight(Some(t), 60);
assert!(
(w - prev).abs() < 0.01,
"glacial weight jumped {prev:.3} -> {w:.3} at {t} °C"
);
prev = w;
}
for m in 0..100 {
let a = glacial_weight(Some(-10.0), m);
let b = glacial_weight(Some(-10.0), m + 1);
// One moisture point is a tenth of the 5..15 ramp, at most 0.1 of weight.
assert!((a - b).abs() <= 0.1 + 1e-9, "moisture gate stepped at {m}");
}
}
#[test]
fn glacial_weight_is_zero_when_airless_or_dry() {
assert_eq!(glacial_weight(None, 80), 0.0);
assert_eq!(glacial_weight(Some(-25.0), 3), 0.0);
}
#[test]
fn character_heterogeneity_varies_along_a_coast() {
// Same body, same latitude, positions ~600 km apart: the seeded field must
@@ -385,7 +503,7 @@ mod tests {
i as f64 * 600_000.0,
500_000.0,
30.0,
GlaciationGrade::None,
grade_glacial_weight(GlaciationGrade::None),
50,
)
.warp_amplitude_px
@@ -406,7 +524,15 @@ mod tests {
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55);
let ch = coast_character_at(
&env,
42,
3e6,
1e6,
30.0,
grade_glacial_weight(GlaciationGrade::Light),
55,
);
let a = coast_warp_px(42, 3e6, 1e6, &ch, 0.0);
let b = coast_warp_px(42, 3e6, 1e6, &ch, 0.0);
assert_eq!(a, b, "warp must be deterministic");
@@ -433,7 +559,15 @@ mod tests {
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55);
let ch = coast_character_at(
&env,
42,
3e6,
1e6,
30.0,
grade_glacial_weight(GlaciationGrade::Light),
55,
);
for i in 0..100 {
let wx = i as f64 * 9_137.0;
let wy = i as f64 * -7_211.0;
@@ -454,7 +588,15 @@ mod tests {
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55);
let ch = coast_character_at(
&env,
42,
3e6,
1e6,
30.0,
grade_glacial_weight(GlaciationGrade::Light),
55,
);
let (dx, dy) = coast_warp_px(42, 1_000.0, 2_000.0, &ch, 1_000_000.0);
assert_eq!((dx, dy), (0.0, 0.0));
}
@@ -468,7 +610,15 @@ mod tests {
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 17, 1.2e6, 9e5, 25.0, GlaciationGrade::None, 60);
let ch = coast_character_at(
&env,
17,
1.2e6,
9e5,
25.0,
grade_glacial_weight(GlaciationGrade::None),
60,
);
let uncut = coast_warp_px(17, 1.2e6, 9e5, &ch, 0.0);
let cut = coast_warp_px(17, 1.2e6, 9e5, &ch, 16_385.0);
assert_ne!(
@@ -491,7 +641,15 @@ mod tests {
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 5, 2.5e6, 1.5e6, 15.0, GlaciationGrade::None, 45);
let ch = coast_character_at(
&env,
5,
2.5e6,
1.5e6,
15.0,
grade_glacial_weight(GlaciationGrade::None),
45,
);
let district = coast_warp_px(5, 2.5e6, 1.5e6, &ch, 4_096.0);
let quarter = coast_warp_px(5, 2.5e6, 1.5e6, &ch, 1_024.0);
assert_ne!(
@@ -508,7 +666,15 @@ mod tests {
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 42, 1e6, 1e6, 20.0, GlaciationGrade::None, 60);
let ch = coast_character_at(
&env,
42,
1e6,
1e6,
20.0,
grade_glacial_weight(GlaciationGrade::None),
60,
);
let (wdx, _) = coast_warp_px(42, 1e6, 1e6, &ch, 0.0);
let scatter = crate::atlas::detail_scatter::terrain_detail(42, 1e6, 1e6, 1.0, 0.5, 0.0);
assert_ne!(wdx, scatter);
+179 -77
View File
@@ -204,7 +204,14 @@ pub fn composition_offsets(
// rides on top: rare enough that the majority is never threatened (the
// conservation invariant is what bounds it), strong enough to cross a gate
// from well inside a class.
if is_inclusion(seed, wx, wy) {
//
// Gated by the caller's cutoff like every other invented octave. It was
// not, and at Region (379 m cells) the 192 m field was sampled below
// Nyquist: it did not vanish as the constant's doc intended, it ALIASED —
// a +25 slope push scattered at random through the canvas, tipping cells
// into slope-gated ValleyFloor as grid-locked speckle that the hillshade
// then embossed into stair-steps along the zone's frontier.
if INCLUSION_WAVELENGTH_M >= min_wavelength_m && is_inclusion(seed, wx, wy) {
let sign = if f >= 0.0 { 1 } else { -1 };
// The moisture half obeys the envelope rule: a world whose patchiness
// ceiling is zero (airless, bone-dry) grows no glades, because it has no
@@ -337,24 +344,105 @@ mod tests {
assert_eq!(v, 0.0, "every octave truncated must give exactly 0.0");
}
/// **D-258 invariant 3 — the binding acceptance gate, on real terrain.**
/// ...and the rare inclusions obey the same cutoff. They did not, and at
/// Region the 192 m inclusion field aliased into ValleyFloor speckle; the
/// test above only covered the smooth field, so it stayed green throughout.
#[test]
fn cutoff_above_the_inclusion_scale_invents_nothing() {
let mut inclusions_seen = 0;
for i in 0..4_000 {
let (wx, wy) = ((i % 64) as f64 * 97.0, (i / 64) as f64 * 97.0);
if is_inclusion(42, wx, wy) {
inclusions_seen += 1;
}
assert_eq!(
composition_offsets(42, wx, wy, 4_096.0, MOISTURE_COMPOSITION_CEILING_Q),
(0, 0),
"a rung that cannot resolve a 192 m inclusion must not be handed one"
);
}
assert!(inclusions_seen > 20, "sample never landed in an inclusion");
}
/// Tally the vegetation verdicts over a square patch of real ground.
///
/// `composed` switches the fine tier on or off through the one derive core
/// every production path uses, so the two tallies differ by composition and
/// by nothing else.
#[allow(clippy::too_many_arguments)]
fn tally_patch(
seed: crate::seed::SeedChain,
body: &str,
params: &crate::atlas::district_profile::BodyParams,
ta: &crate::atlas::features::TerrainAnalysis,
climate: &crate::atlas::district_profile::ClimateConstants,
(cx, cy): (f64, f64),
cells: usize,
step_m: f64,
composed: bool,
) -> std::collections::BTreeMap<u8, u32> {
let mut tally = std::collections::BTreeMap::new();
let half = cells as f64 / 2.0;
for iy in 0..cells {
for ix in 0..cells {
let prof = crate::atlas::district_profile::derive_at_metres_with_riparian(
seed,
body,
params,
ta,
cx + (ix as f64 - half) * step_m,
cy + (iy as f64 - half) * step_m,
climate,
0.0,
false,
None,
composed,
);
*tally.entry(prof.vegetation_class as u8).or_insert(0u32) += 1;
}
}
tally
}
/// `(class, share)` of the plurality in a tally.
fn majority(tally: &std::collections::BTreeMap<u8, u32>) -> (u8, f64) {
let total: u32 = tally.values().sum();
let (class, count) = tally
.iter()
.max_by_key(|(_, c)| **c)
.map(|(k, c)| (*k, *c))
.expect("non-empty tally");
(class, count as f64 / total as f64)
}
/// **D-258 invariants 2 and 3 — the binding acceptance gate, on real terrain.**
///
/// "Downsampling rung 0.5 must reproduce the rung-0 summary it came from: a
/// forest cell may gain marsh pockets but must still read as forest from
/// orbit." The zero-mean test above proves the FIELD averages out; this
/// proves the consequence that actually matters — that the CLASSIFICATION a
/// patch of ground reads as does not move when composition is applied and
/// then averaged back.
/// proves the consequence that matters — that the CLASSIFICATION a patch of
/// ground reads as does not move when composition is applied.
///
/// Runs the real derive over a District-sized patch twice, with and without
/// composition, and compares the MAJORITY vegetation verdict. Composition
/// may recolour a minority of cells — that is its entire purpose — but the
/// plurality must survive, or the map would disagree with the world it
/// zoomed out from.
/// Every patch is derived TWICE, with the fine tier off and on, through the
/// same core. The uncomposed plurality is the summary; the composed one must
/// equal it. An earlier version of this test derived once and only checked
/// that some class held a majority, which cannot tell a conserved summary
/// from a replaced one — a forest patch turned wholly to scrub passes it.
///
/// Patches whose own summary is marginal (below `DECISIVE_SHARE`) are a
/// real biome boundary, where composition may legitimately tip the vote, so
/// they are reported but not judged. The number that ARE judged is asserted
/// too, or a body change that made every patch marginal would pass by
/// judging nothing.
///
/// Runs in the ordinary `cargo test` path (it was `#[ignore]`d, so the
/// binding gate for this work ran only when someone typed it). ~3 s debug.
#[test]
#[ignore = "loads a real body's cascade; run explicitly"]
fn composition_conserves_the_summary_on_real_terrain() {
use std::collections::BTreeMap;
/// A summary this lopsided is a verdict, not a boundary.
const DECISIVE_SHARE: f64 = 0.7;
/// Fewer judged patches than this and the test is proving nothing.
const MIN_JUDGED: usize = 4;
let body = "GJ820Bc";
let Ok((snapshot, params)) =
@@ -369,84 +457,98 @@ mod tests {
);
let climate = crate::atlas::district_profile::ClimateConstants::default();
let seed = crate::seed::SeedChain::root(42).derive(crate::seed::SeedDomain::Body, 1);
let radius_m = params.body_radius_km.expect("Ferrath has a radius") * 1000.0;
let circumference_m = std::f64::consts::TAU * radius_m;
let meridian_m = std::f64::consts::PI * radius_m;
// A District-sized patch (2,048 m) at the descent ladder's own anchor,
// sampled at ~16 m so the fine band is fully resolved.
let (cx, cy) = (29_422_009.0f64, -5_675_959.0f64);
let mut tally: BTreeMap<u8, u32> = BTreeMap::new();
for iy in 0..128 {
for ix in 0..128 {
let wx = cx + (ix as f64 - 64.0) * 16.0;
let wy = cy + (iy as f64 - 64.0) * 16.0;
let prof = crate::atlas::district_profile::derive_at_metres(
seed,
body,
&params,
&ta,
wx,
wy,
&climate,
0.0,
&[],
);
*tally.entry(prof.vegetation_class as u8).or_insert(0) += 1;
// The descent ladder's own anchor, then a spread across the body: four
// longitudes by three latitudes (wy runs north-negative, equator at 0).
let mut anchors = vec![(29_422_009.0f64, -5_675_959.0f64)];
for lat_deg in [-40.0f64, 0.0, 40.0] {
for k in 0..4 {
anchors.push((
circumference_m * (k as f64 + 0.125) / 4.0,
-lat_deg / 180.0 * meridian_m,
));
}
}
let total: u32 = tally.values().sum();
let (majority, count) = tally
.iter()
.max_by_key(|(_, c)| **c)
.map(|(k, c)| (*k, *c))
.expect("non-empty tally");
// The plurality must still BE the plurality after composition — a
// downsample of this patch returns the class it started as.
let share = count as f64 / total as f64;
let mut judged = 0usize;
let mut changed_cells = 0u32;
for (i, &anchor) in anchors.iter().enumerate() {
// 64 x 64 at 32 m = one District (2,048 m); the fine band bottoms
// out at 128 m and inclusions are 192 m, so 32 m resolves both.
let base = tally_patch(seed, body, &params, &ta, &climate, anchor, 64, 32.0, false);
let comp = tally_patch(seed, body, &params, &ta, &climate, anchor, 64, 32.0, true);
let (base_class, base_share) = majority(&base);
let (comp_class, comp_share) = majority(&comp);
changed_cells += base
.iter()
.map(|(k, n)| n.abs_diff(*comp.get(k).unwrap_or(&0)))
.sum::<u32>()
/ 2;
eprintln!(
"anchor {i}: summary {base_class} at {:.1}% {base:?} -> composed {comp_class} at {:.1}% {comp:?}",
100.0 * base_share,
100.0 * comp_share,
);
if base_share < DECISIVE_SHARE {
continue;
}
judged += 1;
assert_eq!(
comp_class,
base_class,
"anchor {i} {anchor:?}: composition REPLACED the summary — class \
{base_class} at {:.1}% became class {comp_class} at {:.1}%. D-258 \
allows minority pockets, not a new majority; the map would \
disagree with the view it was zoomed in from.",
100.0 * base_share,
100.0 * comp_share,
);
assert!(
comp_share > 0.5,
"anchor {i}: the summary class survived only as a plurality \
({:.1}%) — composition invented more than it conserved",
100.0 * comp_share
);
}
assert!(
share > 0.5,
"composition broke the summary: majority class {majority} holds only \
{:.1}% of a District patch across {} classes {:?}. D-258 allows \
minority pockets, not a new majority — this would make the map \
disagree with the view it was zoomed in from.",
100.0 * share,
tally.len(),
tally
judged >= MIN_JUDGED,
"only {judged} of {} patches had a decisive summary to conserve — the \
gate judged too little to mean anything",
anchors.len()
);
// ...and it must not be a monoculture either, or nothing was composed.
//
// This assertion used to read `tally.len() > 1 || share == 1.0`, which is
// a TAUTOLOGY: a single-class tally has a 100% share by definition, so
// both branches were always satisfiable and the check could never fail —
// including in the exact case its own message describes, "nothing was
// composed". It was the only thing standing behind D-258's invariant 2
// (descending reveals COMPOSITION), and it was standing behind nothing.
//
// Both bounds are now asserted separately, because the invariant is
// two-sided: conservation caps how much may be invented (above), and
// composition sets a floor on how little (here). Measured at this anchor:
// 2 classes, minority 175/16384 = 1.07%.
assert!(
tally.len() > 1,
"tally {tally:?} — a District patch resolved to ONE vegetation class. \
D-258 invariant 2 says a summarised cell must be able to CONTAIN the \
minority it suppressed; a monoculture here means the fine tier did \
nothing at this scale, which is the flat map this work exists to fix."
changed_cells > 0,
"composition changed no verdict in any patch — the fine tier is off"
);
let minority = total - count;
let minority_share = minority as f64 / total as f64;
// Invariant 2's floor, at the ladder anchor where it was measured: a
// summarised cell must CONTAIN the minority it suppressed. Sampled at
// 16 m so the value stays comparable with the 1.07% on record.
//
// This assertion once read `tally.len() > 1 || share == 1.0`, a
// tautology — a single-class tally has a 100% share by definition, so it
// could never fail, including in the case its own message named.
let ladder = tally_patch(
seed, body, &params, &ta, &climate, anchors[0], 128, 16.0, true,
);
let (_, share) = majority(&ladder);
let minority_share = 1.0 - share;
assert!(
minority_share >= MIN_COMPOSED_MINORITY_SHARE,
"minority classes hold {:.2}% of the patch, below the {:.2}% floor — \
inclusions this sparse are indistinguishable from none at map scale. \
tally {tally:?}",
ladder.len() > 1 && minority_share >= MIN_COMPOSED_MINORITY_SHARE,
"ladder anchor tally {ladder:?}: minority holds {:.2}%, below the \
{:.2}% floor — descending reveals no composition at the rung this \
work exists to fix",
100.0 * minority_share,
100.0 * MIN_COMPOSED_MINORITY_SHARE,
);
eprintln!(
"conservation: majority class {majority} at {:.1}% across {} classes {:?}",
100.0 * share,
tally.len(),
tally
"conservation: {judged}/{} patches judged, {changed_cells} cells recomposed; \
ladder anchor minority {:.2}% {ladder:?}",
anchors.len(),
100.0 * minority_share,
);
}
}
+18 -6
View File
@@ -1198,7 +1198,8 @@ struct InventedPrimitives {
/// Steps:
/// 1. **Driver tier (one-step-stale, T-1125 circularity ruling):** the raw
/// *unwarped* bilinear envelope feeds a provisional moisture/temperature →
/// [`derive_glaciation_grade_from_climate`] — the coast character reads
/// [`coast_invention::glacial_weight`] (the continuous twin of
/// [`derive_glaciation_grade_from_climate`]) — the coast character reads
/// these stale drivers; it never reads the warped values it produces. The
/// raw inputs are the coarse ~40–160 km/pixel envelope anyway, so one step
/// of staleness is far below the signal's own resolution.
@@ -1213,7 +1214,7 @@ struct InventedPrimitives {
/// `derive_orbital_at_metres` called `derive_temperature_c`
/// UNCONDITIONALLY — wrong on every non-airless body (the entire coastline
/// population), silently producing a different `driver_temp` →
/// `driver_moisture` → `driver_glaciation` (a discrete band) →
/// `driver_moisture` → glaciation →
/// `coast_character_at` → warp displacement than District/Quarter at the
/// identical position, reintroducing the exact T-1160 defect class one
/// step upstream of the fix that closed it for the sampling position
@@ -1265,7 +1266,9 @@ fn invent_coastal_position(
None => derive_temperature_c(&driver_params, climate, seed.seed()),
};
let driver_moisture = derive_moisture_q(&driver_params, raw_elev_q, raw_ocean_q, climate);
let driver_glaciation = derive_glaciation_grade_from_climate(driver_temp, driver_moisture);
// Continuous, not the categorical grade: the character must not step where
// the grade flips on an isotherm (T-1213 — see `coast_invention::glacial_weight`).
let driver_glacial_weight = coast_invention::glacial_weight(driver_temp, driver_moisture);
// ── 2. Character: body personality envelope + position modulation. ──────
let tectonic = derive_tectonic_class(body_params);
@@ -1276,7 +1279,7 @@ fn invent_coastal_position(
world_x_m,
world_y_m,
body_params.latitude_deg,
driver_glaciation,
driver_glacial_weight,
driver_moisture,
);
@@ -1608,6 +1611,7 @@ pub fn derive_district_profile(
0.0, // batch path — no octave cutoff, matches derive_district's default
near_perennial_water,
Some(region_cache),
true, // batch path: composition applies, as on the window path (T-1213)
);
// D-256(c) binding requirement 1: basin_direction is inert to every other
@@ -1940,6 +1944,7 @@ pub fn derive_at_metres(
min_wavelength_m,
near_perennial_water,
None, // no pre-built region cache — on-demand on-the-fly derivation, exactly as before extraction
true, // metres-addressable path: composition applies (T-1213)
)
}
@@ -1963,8 +1968,14 @@ pub fn derive_at_metres(
/// field is inert to every other field's derivation, proven in the D-256
/// ruling, so an override after the fact is exactly equivalent to threading it
/// through).
///
/// `invent_composition` (T-1213) is `true` on both production paths. It exists
/// as a parameter so the D-258 conservation gate can derive the SAME ground
/// with and without the fine tier and compare the verdicts — the only way to
/// test that composition preserves a summary is to know the summary it
/// started from.
#[allow(clippy::too_many_arguments)]
fn derive_at_metres_with_riparian(
pub(crate) fn derive_at_metres_with_riparian(
seed: SeedChain,
body_id: &str,
body_params: &BodyParams,
@@ -1975,6 +1986,7 @@ fn derive_at_metres_with_riparian(
min_wavelength_m: f64,
near_perennial_water: bool,
region_cache: Option<&BTreeMap<RegionPos, RegionProfile>>,
invent_composition: bool,
) -> DistrictProfile {
// World metres -> fractional heightmap pixel + latitude. Mirrors
// `derive_district`'s former inline mapping exactly, just keyed on
@@ -2086,7 +2098,7 @@ fn derive_at_metres_with_riparian(
near_perennial_water,
lake_from_hydrology,
lake_margin_q,
true, // metres-addressable path: composition applies (T-1213)
invent_composition,
)
}
+2 -2
View File
@@ -1081,11 +1081,11 @@ mod tests {
use crate::atlas::coast_invention::{
body_coast_envelope, coast_character_at, coast_warp_px,
};
use crate::atlas::district_profile::{GlaciationGrade, TectonicClass};
use crate::atlas::district_profile::TectonicClass;
let params = test_params();
let env = body_coast_envelope(&params, TectonicClass::Stable);
let ch = coast_character_at(&env, 42, 0.0, 0.0, 20.0, GlaciationGrade::None, 50);
let ch = coast_character_at(&env, 42, 0.0, 0.0, 20.0, 0.0, 50);
let n = 200;
let mut course_vals = Vec::with_capacity(n);
+2 -2
View File
@@ -60,13 +60,13 @@
},
"slope_q": {
"min": 0,
"max": 34,
"max": 33,
"distinct": 22
},
"ocean_fraction_q": {
"min": 0,
"max": 100,
"distinct": 36
"distinct": 37
},
"morphology_zones": 7,
"vegetation_classes": 3,
@@ -515,11 +515,11 @@
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 4096,
"morphology": 12,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 27,
"slope_q": 2,
"elev_q": 76,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
@@ -536,7 +536,7 @@
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 0,
"slope_q": 1,
"elev_q": 79,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
@@ -773,12 +773,12 @@
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 1,
"slope_q": 28,
"precipitation": 2,
"slope_q": 3,
"elev_q": 24,
"ocean_fraction_q": 0,
"temperature_dc": 691,
"moisture_q": 19,
"moisture_q": 49,
"vegetation": 3
},
{
@@ -790,12 +790,12 @@
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 4,
"slope_q": 0,
"precipitation": 2,
"slope_q": 1,
"elev_q": 21,
"ocean_fraction_q": 0,
"temperature_dc": 707,
"moisture_q": 85,
"moisture_q": 55,
"vegetation": 3
},
{