@@ -0,0 +1,412 @@
//! Sub-cell **composition** — the fine tier that lets a summarised cell contain
//! the minority it suppressed (T-1213, [D-258](../../governance) invariant 2).
//!
//! # Why a third tier exists
//!
//! [`crate::atlas::vegetation_invention`] already perturbs `moisture_q` with two
//! bands: a massif field (100– 410 km, never gated) and a texture field
//! (32 km– 128 m, gated). That pair was built for **cross-rung coherence** and is
//! explicitly weighted so it cannot change a verdict — 70% massif against 30%
//! texture, so "the texture term alone can never outweigh the massif term".
//!
//! D-258 asks for the opposite thing. Its invariant (2) says descending the
//! ladder must reveal COMPOSITION: *a cell reading "forest" globally must be
//! able to contain clearings, marsh, rock and scrub the vote suppressed*. That
//! requires the fine tier to flip a classification, which the existing pair is
//! designed to prevent.
//!
//! The two are reconciled by D-258's own third invariant rather than by picking
//! a winner: **flips are allowed, and conservation is the bound.** Downsampling
//! the composed field must reproduce the summary it came from. A forest cell may
//! gain marsh pockets; it must still read as forest from orbit.
//!
//! # Why the existing tiers could not simply be turned up
//!
//! Measured on Ferrath, the texture field's amplitude is dominated by its
//! coarsest octaves — 50.2% sits in the 32,768 m octave alone, and everything at
//! or below 2,048 m accounts for **5.9%** of the total. Across a District window
//! (2,048 m of ground) only that 5.9% varies at all, which after the 30% texture
//! weight and a ~29-point ceiling swings `moisture_q` by **±0.51 points**.
//! Vegetation thresholds are 5– 15 points apart, so nothing can ever cross one.
//! That is not a tuning shortfall, it is the geometric series: raising the
//! ceiling enough to matter at District would make the field violent at Region.
//!
//! So this tier carries the fine band ALONE, normalized to its own full swing.
//! It is quiet where the coarse tiers are loud and loud where they have nothing
//! left to say.
//!
//! # What it feeds
//!
//! Both classification inputs that gate everything downstream:
//!
//! - `moisture_q` → vegetation (clearings, marsh, scrub inside a forest)
//! - `slope_q` → morphology (CliffCoast, Fjord, DuneStrand, Delta)
//!
//! The second is not incidental. Measured down the ladder on Ferrath, morphology
//! carries **nine** zones at Global and exactly **one** (AlluvialPlain) at every
//! rung below it: every zone that gates on a slope threshold becomes unreachable,
//! so fjord walls and cliff coasts exist only on the whole-body view. Vegetation
//! and coastal character flatten for the same reason, one field apart.
use crate ::atlas ::detail_scatter ::{ value_noise , VOXEL_OCTAVE_WAVELENGTHS_M } ;
use crate ::seed ::splitmix64 ;
/// Salt separating this tier's noise stream from the massif and texture fields
/// sampled at the same position. Without it the three would correlate and the
/// composition would merely deepen the patches the texture field already made,
/// instead of cutting across them.
const COMPOSITION_SALT : u64 = 0x0C0F_9051_71ED_5EED ;
/// Separate stream again for the rare-inclusion test, so a glade does not
/// preferentially land where the smooth field already peaks.
const INCLUSION_SALT : u64 = 0x1_C1EA_5124_60DE ;
/// Size of a rare inclusion — a glade, a blowdown, a rocky outcrop. Chosen to
/// read as a FEATURE at the rungs that can see it: 192 m is roughly 50 gridunits
/// across at District and half a gridunit at Region, so a clearing is a shape
/// when you are close and correctly invisible from orbit.
const INCLUSION_WAVELENGTH_M : f64 = 192.0 ;
/// Noise value above which a position is inside an inclusion. `value_noise`
/// returns `[-1, 1]`, and 0.72 puts roughly 4– 6% of ground inside one — sparse
/// enough that the majority class is never in danger (the conservation
/// invariant is the real bound), common enough that a walk through deep forest
/// crosses one.
const INCLUSION_THRESHOLD : f64 = 0.72 ;
/// How hard an inclusion pushes `moisture_q`. Deliberately LARGE — larger than
/// [`MOISTURE_COMPOSITION_CEILING_Q`] — because its whole job is to cross a
/// class gate from well INSIDE a class, which the smooth term by design cannot.
const INCLUSION_MOISTURE_SWING_Q : i32 = 30 ;
/// How hard an inclusion pushes `slope_q` — a rocky outcrop is genuinely steep
/// ground, so this one is allowed to reach where the smooth slope term is not.
const INCLUSION_SLOPE_SWING_Q : i32 = 25 ;
/// Maximum swing this tier may apply to `moisture_q`, in points.
///
/// Sized against the vegetation ladder's own spacing (`derive_vegetation`'s
/// gates sit 5– 15 points apart): large enough that ground sitting NEAR a
/// threshold crosses it in places, small enough that ground sitting well inside
/// a class never leaves it. That asymmetry is what makes the invention
/// conservative — a marginal forest grows clearings, a deep one does not.
pub const MOISTURE_COMPOSITION_CEILING_Q : i32 = 12 ;
/// Maximum swing this tier may apply to `slope_q`, in points.
///
/// SMALL, and deliberately smaller than the moisture ceiling — the opposite of
/// the first attempt, which set it to 15 on the reasoning that the morphology
/// gates it feeds are far apart (Fjord at 40, CliffCoast at 55). That reasoning
/// was backwards. Measured against the window-derivation golden's fixtures,
/// base `slope_q` on ordinary ground is 2– 6, so a ±15 swing does not VARY the
/// signal, it REPLACES it: one fixture moved 6 → 19, and two coastal samples
/// flipped to Wetland on the strength of invented slope alone.
///
/// The gates are far apart for a reason: a cliff coast is a real landform, not
/// a dice roll. Composition must let ground that genuinely sits near a
/// threshold fall on both sides of it — never manufacture a fjord on a flood
/// plain. Where the terrain is flat, flat is the honest answer, and the
/// morphology variety visible at Global comes from places that actually have
/// slope.
pub const SLOPE_COMPOSITION_CEILING_Q : i32 = 6 ;
/// The fine-band composition field at a world position, in `[-1, 1]`.
///
/// Only [`VOXEL_OCTAVE_WAVELENGTHS_M`] (1,024– 128 m) — the band that actually
/// varies inside a District window — normalized across its own octaves so the
/// tier uses its full swing rather than the 5.9% tail it holds inside the
/// texture field's series.
///
/// `min_wavelength_m` gates octaves finer than the caller's sample density, the
/// same discipline every other invented field follows; `0.0` = no cutoff.
fn composition_field ( seed : u64 , wx : f64 , wy : f64 , min_wavelength_m : f64 ) -> f64 {
let seed = splitmix64 ( seed ^ COMPOSITION_SALT ) ;
let mut sum = 0.0 ;
let mut amp = 1.0 ;
let mut norm = 0.0 ;
for ( i , & wl ) in VOXEL_OCTAVE_WAVELENGTHS_M . iter ( ) . enumerate ( ) {
if wl < min_wavelength_m {
amp * = 0.5 ;
continue ;
}
sum + = value_noise (
seed . wrapping_add ( ( i as u64 ) . wrapping_mul ( 0x1000 ) ) ,
wx ,
wy ,
wl ,
) * amp ;
norm + = amp ;
amp * = 0.5 ;
}
if norm < = 0.0 {
return 0.0 ;
}
( sum / norm ) . clamp ( - 1.0 , 1.0 )
}
/// Composition offsets for one position: `(moisture_q, slope_q)`, in points.
///
/// **Zero-mean by construction**, which is what satisfies D-258's conservation
/// invariant: `value_noise` is symmetric about zero, so averaging the offsets
/// over any area large enough to contain whole features returns to the
/// unperturbed summary. Composition adds variety within a cell without moving
/// what the cell reads as from orbit — the property
/// `composition_conserves_the_summary` pins on real terrain.
///
/// The two offsets are drawn from the SAME field rather than two independent
/// ones, and deliberately: on real ground steep places drain, so slope and
/// moisture are correlated, and a rocky outcrop should tend to be the drier
/// patch rather than an unrelated one. The moisture offset is negated for that
/// reason — where this tier lifts slope it drops moisture.
pub fn composition_offsets (
seed : u64 ,
wx : f64 ,
wy : f64 ,
min_wavelength_m : f64 ,
moisture_ceiling_q : i32 ,
) -> ( i32 , i32 ) {
let f = composition_field ( seed , wx , wy , min_wavelength_m ) ;
// Ceiling is caller-supplied for moisture so a bone-dry or airless world,
// whose vegetation envelope is already zero, invents no damp pockets — the
// same envelope rule `vegetation_invention` applies one tier up.
let mut moisture = ( ( - f * moisture_ceiling_q as f64 ) . round ( ) as i32 )
. clamp ( - moisture_ceiling_q , moisture_ceiling_q ) ;
let mut slope = ( ( f * SLOPE_COMPOSITION_CEILING_Q as f64 ) . round ( ) as i32 )
. clamp ( - SLOPE_COMPOSITION_CEILING_Q , SLOPE_COMPOSITION_CEILING_Q ) ;
// RARE INCLUSIONS — the clearing in the deep wood, the rocky outcrop on the
// plain (Jeroen, 2026-08-08: "maybe a dense forest should still sometimes
// produce a clearing or a rocky outcropping").
//
// The smooth offsets above are a gentle sway around the base value, so they
// can only change a verdict where the ground already sits near a gate. That
// made deep-in-class ground immune, and the conservation test duly measured
// a District patch that was 100% Forest — a monoculture, which is the flat
// map this ticket exists to fix, one scale down.
//
// D-258 says a cell reading forest must be able to CONTAIN clearings, marsh,
// rock and scrub. Contain, not "border on". So a sparse, high-contrast term
// 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 ) {
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
// vegetation to clear. Caught by `zero_moisture_ceiling_invents_no_moisture`,
// which the first version of this failed — the inclusion was applied
// unconditionally and put damp pockets on airless rock.
if moisture_ceiling_q > 0 {
moisture = ( moisture - sign * INCLUSION_MOISTURE_SWING_Q ) . clamp ( - 100 , 100 ) ;
}
// The slope half is NOT gated. An outcrop is geology, not biology — a
// dead world is exactly where bare rock should break the surface.
slope = ( slope + sign * INCLUSION_SLOPE_SWING_Q ) . clamp ( - 100 , 100 ) ;
}
( moisture , slope )
}
/// Does this position fall inside a rare inclusion — a clearing, a blowdown, a
/// rocky outcrop?
///
/// Thresholded value noise rather than a per-cell dice roll, so inclusions come
/// out as connected BLOBS a few hundred metres across. A per-cell test would
/// scatter single stray cells through the forest, which reads as speckle
/// (exactly the failure the client-side stipple's first attempt had) rather
/// than as a glade.
fn is_inclusion ( seed : u64 , wx : f64 , wy : f64 ) -> bool {
let s = splitmix64 ( seed ^ INCLUSION_SALT ) ;
value_noise ( s , wx , wy , INCLUSION_WAVELENGTH_M ) > INCLUSION_THRESHOLD
}
#[ cfg(test) ]
mod tests {
use super ::* ;
#[ test ]
fn field_is_deterministic ( ) {
let a = composition_field ( 42 , 1_000.0 , 2_000.0 , 0.0 ) ;
let b = composition_field ( 42 , 1_000.0 , 2_000.0 , 0.0 ) ;
assert_eq! ( a , b , " same seed and position must give the same field " ) ;
}
#[ test ]
fn field_stays_in_unit_range ( ) {
for i in 0 .. 500 {
let v = composition_field ( 7 , i as f64 * 37.0 , i as f64 * 91.0 , 0.0 ) ;
assert! ( ( - 1. 0 ..= 1.0 ) . contains ( & v ) , " field out of range: {v} " ) ;
}
}
/// The whole reason this tier exists: it must VARY across a District window
/// (2,048 m), where the texture field it supplements is effectively
/// constant.
#[ test ]
fn field_varies_across_a_district_window ( ) {
let mut lo = f64 ::MAX ;
let mut hi = f64 ::MIN ;
for i in 0 .. 64 {
let wx = i as f64 * 32.0 ; // 64 samples across 2,048 m
let v = composition_field ( 11 , wx , 0.0 , 0.0 ) ;
lo = lo . min ( v ) ;
hi = hi . max ( v ) ;
}
assert! (
hi - lo > 0.5 ,
" swing across a District window was only {:.3} — this tier exists \
precisely because the coarse bands are flat at this scale " ,
hi - lo
) ;
}
/// D-258 invariant 3, at the field level: the offsets average back to
/// nothing, so a downsample reproduces the summary they were added to.
#[ test ]
fn offsets_are_zero_mean_over_area ( ) {
let mut m_sum = 0 i64 ;
let mut s_sum = 0 i64 ;
let n = 4_000 ;
for i in 0 .. n {
let wx = ( i % 64 ) as f64 * 128.0 ;
let wy = ( i / 64 ) as f64 * 128.0 ;
let ( m , s ) = composition_offsets ( 99 , wx , wy , 0.0 , MOISTURE_COMPOSITION_CEILING_Q ) ;
m_sum + = m as i64 ;
s_sum + = s as i64 ;
}
let m_mean = m_sum as f64 / n as f64 ;
let s_mean = s_sum as f64 / n as f64 ;
assert! (
m_mean . abs ( ) < 1.0 ,
" moisture offset mean {m_mean:.3} — a biased tier would shift the \
summary it is supposed to preserve "
) ;
assert! (
s_mean . abs ( ) < 1.5 ,
" slope offset mean {s_mean:.3} is biased "
) ;
}
/// Slope and moisture must move OPPOSITE ways: steep drains.
#[ test ]
fn steep_places_are_the_drier_places ( ) {
let mut checked = 0 ;
for i in 0 .. 200 {
let wx = i as f64 * 71.0 ;
let ( m , s ) = composition_offsets ( 5 , wx , 0.0 , 0.0 , MOISTURE_COMPOSITION_CEILING_Q ) ;
if s . abs ( ) > = 3 & & m . abs ( ) > = 3 {
assert! (
( s > 0 ) ! = ( m > 0 ) ,
" slope {s} and moisture {m} moved the same way — a rocky \
outcrop should be the drier patch, not a wetter one "
) ;
checked + = 1 ;
}
}
assert! ( checked > 10 , " only {checked} samples exercised the check " ) ;
}
/// A zero ceiling invents nothing — the envelope rule, so a bone-dry world
/// grows no damp pockets.
#[ test ]
fn zero_moisture_ceiling_invents_no_moisture ( ) {
for i in 0 .. 100 {
let ( m , _ ) = composition_offsets ( 3 , i as f64 * 55.0 , 0.0 , 0.0 , 0 ) ;
assert_eq! ( m , 0 ) ;
}
}
/// Gating above the whole band silences the tier rather than aliasing it.
#[ test ]
fn cutoff_above_the_band_yields_no_offset ( ) {
let v = composition_field ( 42 , 500.0 , 500.0 , 4_096.0 ) ;
assert_eq! ( v , 0.0 , " every octave truncated must give exactly 0.0 " ) ;
}
/// **D-258 invariant 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.
///
/// 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.
#[ test ]
#[ ignore = " loads a real body's cascade; run explicitly " ]
fn composition_conserves_the_summary_on_real_terrain ( ) {
use std ::collections ::BTreeMap ;
let body = " GJ820Bc " ;
let Ok ( ( snapshot , params ) ) =
crate ::atlas ::believability ::cascade_snapshot_for_body ( 42 , body )
else {
eprintln! ( " skip: {body} not loadable " ) ;
return ;
} ;
let ( _l1 , ta ) = crate ::atlas ::layer1 ::run_layer1_with_moisture (
& snapshot . heightmap ,
crate ::atlas ::district_profile ::derive_moisture_ceiling_q ( & params ) ,
) ;
let climate = crate ::atlas ::district_profile ::ClimateConstants ::default ( ) ;
let seed = crate ::seed ::SeedChain ::root ( 42 ) . derive ( crate ::seed ::SeedDomain ::Body , 1 ) ;
// 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.0 f64 , - 5_675_959.0 f64 ) ;
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 ;
}
}
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 ;
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
) ;
// ...and it must not be a monoculture either, or nothing was composed.
assert! (
tally . len ( ) > 1 | | share = = 1.0 ,
" tally {tally:?} — expected either composition or an honestly uniform patch "
) ;
eprintln! (
" conservation: majority class {majority} at {:.1} % across {} classes {:?} " ,
100.0 * share ,
tally . len ( ) ,
tally
) ;
}
}