Merge remote-tracking branch 'origin/nature-octaves'

This commit is contained in:
2026-07-22 23:37:30 +02:00
13 changed files with 2426 additions and 79 deletions
+132
View File
@@ -34,6 +34,13 @@ static func _mock_window(n: int = 2) -> Dictionary:
class _ViewerStub:
var window: Variant = null
var active_overlay: String = ""
# T-1161: the viewer's currently-HELD rung tag — added alongside the
# per-rung filter tests below. Not read by AtlasWindowOverlay today (the
# overlay trusts each window dict's OWN echoed granularity_v2, per
# cell_grid_side_for_window()'s precedent) but a real AtlasWindowViewer
# exposes get_held_granularity_v2() (T-1153), so the stub carries it too
# for parity with the real duck-typed interface.
var held_granularity_v2: String = "District"
func get_district_window() -> Variant:
return window
@@ -47,7 +54,17 @@ class _ViewerStub:
func is_tile_mode() -> bool:
return false
func get_held_granularity_v2() -> String:
return held_granularity_v2
## T-1161 reframe: COMPOSITE_SMOOTH is now Axis 1 of TWO independent axes
## (see the file header doc) — the texture-vs-flat-rects PIPELINE choice.
## This assertion survives unchanged: the composite is still a TEXTURE at
## every rung. Axis 2 (which FILTER that texture samples with) is now a
## per-rung runtime decision covered separately below by the
## _filter_for_granularity_v2() tests — it is no longer bundled into this
## compile-time const.
func test_composite_smooth_defaults_true() -> void:
assert_bool(AtlasWindowOverlay.COMPOSITE_SMOOTH).override_failure_message(
"T-1145 item 3 ships the smoothed composite as the DEFAULT presentation"
@@ -265,3 +282,118 @@ func test_rebuild_handles_a_rung_swap_from_district_to_region() -> void:
assert_int(o._cached_texture.get_width()).override_failure_message(
"a rung swap must rebuild at the NEW rung's derived cell-grid resolution"
).is_equal(2) # region_window's cell_grid_side is also 2 here (200/100) — same size, different data
# =============================================================================
# T-1161: _filter_for_granularity_v2() — the per-rung sampling filter policy
# (Araminta's ruling: Region incl. the orbital tile mosaic -> NEAREST;
# District/Quarter -> LINEAR; no hysteresis, no px-per-cell threshold, keyed
# purely on rung IDENTITY).
# =============================================================================
## Region is the sparse rung the ruling targets — GPU bilinear blending at
## 204.8 km/cell reads as smoothing-over-absence, so it samples NEAREST.
func test_filter_for_granularity_v2_region_is_nearest() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("Region")).override_failure_message(
"Region must sample TEXTURE_FILTER_NEAREST — dense-enough rungs get LINEAR,"
+ " Region is the sparse one the ruling targets"
).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST)
## District is dense enough that the bilinear blend reads as texture, not as
## papering over sparse data — LINEAR is earned.
func test_filter_for_granularity_v2_district_is_linear() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("District")).override_failure_message(
"District must sample TEXTURE_FILTER_LINEAR"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## Quarter (4x MORE cells than District) is denser still — also LINEAR.
func test_filter_for_granularity_v2_quarter_is_linear() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("Quarter")).override_failure_message(
"Quarter must sample TEXTURE_FILTER_LINEAR"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## An unrecognized/empty tag must NEVER be trusted into the crisp NEAREST
## treatment — mirrors cell_grid_side_for_window()'s own "unknown -> District"
## fallback posture, failing toward the already-shipped LINEAR look rather
## than an unintended NEAREST for a wire shape this code doesn't recognize.
func test_filter_for_granularity_v2_unknown_falls_back_to_linear() -> void:
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("")).override_failure_message(
"an empty/unrecognized granularity_v2 tag must fall back to LINEAR, never NEAREST"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
assert_int(AtlasWindowOverlay._filter_for_granularity_v2("SomeFutureRung")).override_failure_message(
"an unrecognized granularity_v2 tag must fall back to LINEAR, never NEAREST"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## Integration-shaped: a Region-rung window dict, drawn through the real
## _draw() entry point via the _ViewerStub duck-typed interface (same
## end-to-end shape as test_draw_builds_a_texture_for_a_region_rung_window()
## above), must drive the NODE's own texture_filter property to NEAREST —
## not just the helper function in isolation.
func test_draw_sets_node_texture_filter_to_nearest_for_region_window() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.held_granularity_v2 = "Region"
stub.window = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o.viewer = stub
o._draw()
assert_int(o.texture_filter).override_failure_message(
"a Region-rung window must drive the node's texture_filter to NEAREST after a draw"
).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST)
## The District-rung counterpart of the above — confirms the node's
## texture_filter lands on LINEAR (not left over from a previous NEAREST
## draw, and not defaulting to NEAREST) for the dense rung.
func test_draw_sets_node_texture_filter_to_linear_for_district_window() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.window = _mock_window() # District (the default/omitted tag)
o.viewer = stub
o._draw()
assert_int(o.texture_filter).override_failure_message(
"a District-rung window must drive the node's texture_filter to LINEAR after a draw"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
## A rung SWAP (Region -> District, on the SAME node) must flip texture_filter
## along with it — confirms the property is recomputed every draw, not
## sticky from the first rung the node ever rendered.
func test_draw_flips_node_texture_filter_on_a_rung_swap() -> void:
var o: AtlasWindowOverlay = auto_free(AtlasWindowOverlay.new())
var stub := _ViewerStub.new()
stub.window = {
"center": [0, 0],
"n": 200,
"granularity_v2": "Region",
"morphology": PackedByteArray([8, 14, 0, 1]),
"elev_q": PackedByteArray([40, 90, 5, 60]),
"temp_dc": [120, 95, -32768, 60],
"moisture_q": PackedByteArray([50, 30, 90, 20]),
"vegetation": PackedByteArray([2, 1, 6, 3]),
"glaciation": PackedByteArray([0, 0, 1, 2]),
}
o.viewer = stub
o._draw()
assert_int(o.texture_filter).is_equal(CanvasItem.TEXTURE_FILTER_NEAREST)
stub.window = _mock_window() # swap to District
o._draw()
assert_int(o.texture_filter).override_failure_message(
"swapping to a District-rung window must flip texture_filter to LINEAR,"
+ " not leave it stuck at the previous rung's NEAREST"
).is_equal(CanvasItem.TEXTURE_FILTER_LINEAR)
@@ -25,18 +25,33 @@ extends Node2D
## COMPOSITE_SMOOTH := true renders the composite as an n x n Image (one
## pixel per district, EXACT same per-cell color pipeline this file always
## had — _cell_color()/_apply_glaciation() are UNCHANGED) converted to an
## ImageTexture and drawn scaled with LINEAR filtering, instead of n*n flat
## draw_rect() calls. GPU bilinear sampling between adjacent district pixels
## reads as a terrain gradient rather than hard-edged blocks — the same
## treatment the planetary heightmap already gets (Godot's engine-default
## CanvasItem.texture_filter is LINEAR_WITH_MIPMAPS project-wide, which is
## what AtlasViewer's draw_texture_rect() calls already inherit for free;
## this node sets texture_filter explicitly rather than relying on that
## default, so the choice is visible in code, not implicit). The crisp
## per-cell rect path SURVIVES behind the const (COMPOSITE_SMOOTH := false)
## so T-1143's design pass can compare both renderings directly — this is
## explicitly an INTERIM presentation, not the final answer on district-tier
## legibility (T-1143 owns that design).
## ImageTexture and drawn scaled with texture-filtered sampling, instead of
## n*n flat draw_rect() calls. The crisp per-cell rect path SURVIVES behind
## the const (COMPOSITE_SMOOTH := false) so T-1143's design pass can compare
## both renderings directly — this is explicitly an INTERIM presentation, not
## the final answer on district-tier legibility (T-1143 owns that design).
##
## T-1161 (Araminta's per-rung filter ruling, PR #192 review follow-up): the
## smoothed path's PIPELINE (texture-vs-flat-rects) and its SAMPLING FILTER
## (how the GPU reads that texture) are now two INDEPENDENT axes, not one
## bundled choice:
## - Axis 1 — PIPELINE: COMPOSITE_SMOOTH (compile-time const, unchanged by
## this ticket). true = draw a texture; false = per-cell draw_rect(). The
## composite is a TEXTURE at every rung when COMPOSITE_SMOOTH is true —
## this axis does not vary per rung.
## - Axis 2 — FILTER: _filter_for_granularity_v2() (runtime, keyed on rung
## IDENTITY via granularity_v2, T-1161). Region (incl. the orbital tile
## mosaic) samples TEXTURE_FILTER_NEAREST — GPU bilinear stretch at
## 204.8 km/cell reads as a near-featureless soft gradient, technically
## honest LoD but visually indistinguishable from the coarse-composite
## smoothing-over-absence the mandate was written to kill (T-1161's own
## description). District and Quarter sample TEXTURE_FILTER_LINEAR — cell
## density there reads as texture, not smoothing-over-absence, so the
## bilinear blend is earned. No hysteresis, no px-per-cell threshold —
## the filter is a pure function of which rung's data is being drawn.
## The crisp draw_rect() path has no sampling-filter concept at all (no
## texture involved) — its comparison/debug role per the paragraph above is
## unaffected by this axis.
##
## The texture is REBUILT only when its inputs change (the window object
## itself — a new DistrictWindowLayer arriving is a new Dictionary, checked
@@ -252,6 +267,13 @@ func _draw_tile_mosaic() -> void:
## `tile_index` — sharing ONE `_cached_texture` slot across all tiles (the
## single-window field) would thrash on every draw call as different tiles'
## windows compete for it.
##
## T-1161: every mosaic tile is a Region-rung request (atlas_window_tile_set.gd
## requests tiles at AtlasWindowRequest.GRANULARITY_V2_REGION), so the mosaic
## as a whole is in scope for the Region -> NEAREST ruling. As with the
## single-window path, the filter is read from THIS tile's own echoed `w`
## rather than assumed, via the shared `_filter_for_granularity_v2()` helper
## — one policy, two call sites, no duplicated match statement.
func _draw_one_tile(
tile_index: int,
w: Dictionary,
@@ -268,7 +290,8 @@ func _draw_one_tile(
)
if tile_texture == null:
return
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
var granularity_v2 := str(w.get("granularity_v2", AtlasWindowRequest.GRANULARITY_V2_DISTRICT))
texture_filter = _filter_for_granularity_v2(granularity_v2)
draw_texture_rect(tile_texture, Rect2(local_origin, Vector2(extent, extent)), false)
@@ -367,19 +390,43 @@ static func cell_grid_side_for_window(w: Dictionary) -> int:
return n # District — 1:1
## T-1161 (Araminta's per-rung filter ruling): the sampling filter to use for
## the smoothed composite's texture, keyed on RUNG IDENTITY alone via
## `granularity_v2` — no hysteresis, no px-per-cell/zoom threshold. Region
## (204.8 km/cell — the same rung the orbital tile mosaic draws at, since
## every mosaic tile is itself a Region-rung window per `_draw_tile_mosaic()`)
## reads NEAREST: at that density, GPU bilinear blending between real derived
## samples is technically honest LoD but visually indistinguishable from the
## coarse-composite-stretched smoothing-over-absence the mandate was written
## to kill — the spirit is violated even though the letter ("never magnified
## interpolation") is not. District and Quarter read LINEAR: cell density at
## those rungs is high enough that the blend reads as texture, not as papering
## over sparse data. Mirrors `cell_grid_side_for_window()`'s own posture on an
## unknown/missing tag — an unrecognized wire value must never be trusted into
## the crisp NEAREST treatment, so it falls back to District's LINEAR instead
## of Region's NEAREST (fail toward the safer/already-shipped look).
static func _filter_for_granularity_v2(granularity_v2: String) -> CanvasItem.TextureFilter:
match granularity_v2:
AtlasWindowRequest.GRANULARITY_V2_REGION:
return CanvasItem.TEXTURE_FILTER_NEAREST
_:
return CanvasItem.TEXTURE_FILTER_LINEAR # District, Quarter, and unknown/missing fallback
## T-1145 item 3: the smoothed path — build/reuse a `grid_side` x `grid_side`
## ImageTexture (one pixel per DERIVED CELL, T-1152 — not per district, see
## cell_grid_side_for_window()'s doc) and draw it scaled to (n*cell_px), n
## being the window's DISTRICT extent, with LINEAR filtering. texture_filter
## is set on `self` (a CanvasItem property) once per draw — cheap (a property
## write, not a texture rebuild) and correct even the first time this runs
## (Godot's engine default already IS linear, but this makes the choice
## explicit rather than relying on an implicit project-wide default that
## could change).
## being the window's DISTRICT extent. texture_filter is set on `self` (a
## CanvasItem property) once per draw — cheap (a property write, not a
## texture rebuild). T-1161: the filter itself is now PER-RUNG, read from
## `w`'s own echoed `granularity_v2` (the same "response is the source of
## truth" posture cell_grid_side_for_window() already uses) via
## `_filter_for_granularity_v2()`, rather than an unconditional LINEAR.
func _draw_smoothed_composite(
w: Dictionary, n: int, grid_side: int, cell_px: float, active_toggle: String
) -> void:
texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR
var granularity_v2 := str(w.get("granularity_v2", AtlasWindowRequest.GRANULARITY_V2_DISTRICT))
texture_filter = _filter_for_granularity_v2(granularity_v2)
_rebuild_texture_if_needed(w, grid_side, active_toggle)
if _cached_texture == null:
return
@@ -430,6 +477,13 @@ func _rebuild_texture_if_needed(w: Dictionary, grid_side: int, active_toggle: St
## DERIVED cell-grid side (see cell_grid_side_for_window()); `n` (the
## window's district extent) sizes the on-screen cell pitch so the total
## drawn footprint stays `n * cell_px` regardless of rung.
##
## Note (T-1161): this path never touches the node-level `texture_filter`
## property — draw_rect() has no texture/sampling-filter concept, so there is
## nothing to set. That is inert today only because nothing else reads
## `texture_filter` while this path is active; it is not a bug to fix here,
## just worth stating since the smoothed path now sets that property
## per-rung and a reader might otherwise wonder why this path doesn't.
func _draw_crisp_composite(
w: Dictionary, grid_side: int, n: int, cell_px: float, active_toggle: String
) -> void:
@@ -171,6 +171,8 @@ Each step shippable and independently verifiable; steps 13 extend `derive_dis
5. **Client rung-selection + progressive refinement** in `AtlasWindowViewer`: request granularity tracks zoom level; on zoom-in, hold coarse + fetch fine + swap on arrival. Merges the district/quarter screens into one continuum. The click-through seam at planetary→regional (T-1124 §5) is preserved as-is — see §10.
6. **Retire `COMPOSITE_SMOOTH`** (`atlas_window_overlay.gd:60`) once granularity-4 (quarter) windows are the default at close zoom — real classified quarter-cells replace what the linear-filter stretch was compensating for; delete the const and the smoothing branch, leaving the crisp `draw_rect` path as the only path.
> **Erratum (T-1161, PR #194 review — Tyre, 2026-07-22):** step 6's "retire COMPOSITE_SMOOTH entirely / crisp draw_rect as the only path" is **superseded**. COMPOSITE_SMOOTH survives as a compile-time *pipeline* axis (texture vs. per-cell rects); the crisp draw_rect path is retained as the debug/compare path. What T-1155 framed as "retire the smoothing" is delivered instead as a per-rung *sampling-filter* axis (`_filter_for_granularity_v2`): Region/orbital-mosaic → NEAREST (bilinear stretch at 204.8 km/cell reads as smoothing-over-absence), District/Quarter → LINEAR (cell density earns the blend), unknown → LINEAR. T-1155's COMPOSITE_SMOOTH-retirement framing is cancelled; T-1161 supersedes it. Related record note, same review: the T-1162 relief band adds sub-district elevation variance at Quarter cutoff, and elevation feeds the temperature lapse — so R2's "temperature steps at the district" is now slightly softened by relief-driven elevation within a district at Quarter. Intended consequence, noted so nobody is surprised later.
---
## 9. Risks
File diff suppressed because one or more lines are too long
+143 -15
View File
@@ -49,11 +49,35 @@ const COAST_WARP_Y_SALT: u64 = 0xD1F7_0CEA_2B0A_D515;
/// Salt for the tier-2 heterogeneity field (same-coast stretches differ).
const CHARACTER_FIELD_SALT: u64 = 0x0C0A_57C4_A24C_7E12;
/// Coast-warp octave wavelengths in metres (≈16262 km): capes and gulfs at the
/// top, coves and inlets at the bottom. All above the 433 km detail-scatter
/// band, and all below ~2 heightmap pixels — the warp perturbs the coast, it
/// does not rewrite continents (the heightmap stays the truth at its own scale).
const WARP_OCTAVE_WAVELENGTHS_M: [f64; 5] = [262_144.0, 131_072.0, 65_536.0, 32_768.0, 16_384.0];
/// Coast-warp octave wavelengths in metres (≈1,024 m262 km): capes and gulfs
/// at the top, down through bays/headlands/islets at the bottom (T-1162 — the
/// headline "detailed coastline" visual ask). The warp perturbs the coast, it
/// does not rewrite continents (the heightmap stays the truth at its own
/// scale) — displacing the sampling *position*, distinct from
/// `detail_scatter::terrain_detail`'s independent perturbation of the sampled
/// *elevation*.
///
/// **T-1162 extension:** four octaves finer than the pre-T-1162 floor
/// (16,384 m) were appended — 8,192 / 4,096 / 2,048 / 1,024 — reaching the
/// Quarter rung's own Nyquist floor (512 m spacing → content wavelength ≥ 2×
/// spacing = 1,024 m; finer than that is unsampleable noise at Quarter
/// density, not detail). A Quarter-rung request (`min_wavelength_m` cutoff
/// quantized to 1,024 m, see `layer_proxy::MIN_WL_BANDS_M`) admits every
/// octave down to this floor; a District-rung request (cutoff quantized to
/// District's OWN real band, `2 × DISTRICT_M = 4,096` m — District's own
/// Nyquist limit, derived independently from the rung's spacing per PR #194
/// I1, NOT from any invention field's octave array) truncates the two
/// finest additions (2,048/1,024 m — genuinely below District's floor)
/// while legitimately ADMITTING the other two (8,192/4,096 m — new content
/// that IS coarse enough for District to resolve, not a leak). Either way
/// Quarter still sees strictly more than District at the SAME position (the
/// two finest additions are Quarter-exclusive). The
/// `enveloped_fbm`-style hard-truncate cutoff discipline is honored via
/// [`warp_fbm`]'s own `min_wavelength_m` parameter — `0.0` (no cutoff) is
/// byte-identical to every pre-T-1162 caller.
const WARP_OCTAVE_WAVELENGTHS_M: [f64; 9] = [
262_144.0, 131_072.0, 65_536.0, 32_768.0, 16_384.0, 8_192.0, 4_096.0, 2_048.0, 1_024.0,
];
/// Heterogeneity-field octave wavelengths in metres (≈100400 km): the scale on
/// which one planet's coastline personality drifts from stretch to stretch.
@@ -224,21 +248,44 @@ pub fn character_field(seed: u64, wx: f64, wy: f64) -> f64 {
/// Apply the same offset to *every* envelope field sampled at the position
/// (elevation, slope, ocean mask) so the invented terrain moves coherently —
/// a warped-in bay carries its sea-level elevation with it.
pub fn coast_warp_px(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter) -> (f64, f64) {
///
/// `min_wavelength_m` (T-1162, mirrors `detail_scatter::terrain_detail`'s
/// contract exactly): octaves in [`WARP_OCTAVE_WAVELENGTHS_M`] finer than this
/// cutoff are hard-truncated, never faded. `0.0` = no cutoff = every octave —
/// byte-identical to every pre-T-1162 caller (all of which passed no cutoff at
/// all, since the parameter didn't exist before this ticket).
pub fn coast_warp_px(
seed: u64,
wx: f64,
wy: f64,
ch: &CoastCharacter,
min_wavelength_m: f64,
) -> (f64, f64) {
let sx = splitmix64(seed ^ COAST_WARP_SALT);
let sy = splitmix64(sx ^ COAST_WARP_Y_SALT);
(
warp_fbm(sx, wx, wy, ch) * ch.warp_amplitude_px,
warp_fbm(sy, wx, wy, ch) * ch.warp_amplitude_px,
warp_fbm(sx, wx, wy, ch, min_wavelength_m) * ch.warp_amplitude_px,
warp_fbm(sy, wx, wy, ch, min_wavelength_m) * ch.warp_amplitude_px,
)
}
/// Roughness/ridge-shaped fBm in ≈`[-1, 1]` over the coast-warp octave band.
fn warp_fbm(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter) -> f64 {
///
/// `min_wavelength_m` (T-1162): same hard-truncate discipline as
/// `detail_scatter::enveloped_fbm` — octaves with `wl < min_wavelength_m` are
/// skipped entirely, but `amp` still advances through the skipped step so
/// surviving octaves keep their intended relative weight (no renormalization
/// against a smaller octave count beyond the shared `norm` denominator, which
/// already only sums the terms that ran).
fn warp_fbm(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter, min_wavelength_m: f64) -> f64 {
let mut sum = 0.0;
let mut amp = 1.0;
let mut norm = 0.0;
for (i, &wl) in WARP_OCTAVE_WAVELENGTHS_M.iter().enumerate() {
if wl < min_wavelength_m {
amp *= 0.45 + 0.35 * ch.roughness;
continue;
}
let mut n = value_noise(
seed.wrapping_add((i as u64).wrapping_mul(0x1000)),
wx,
@@ -257,6 +304,9 @@ fn warp_fbm(seed: u64, wx: f64, wy: f64, ch: &CoastCharacter) -> f64 {
// entirely and still read as stripes at w64.
amp *= 0.45 + 0.35 * ch.roughness;
}
if norm == 0.0 {
return 0.0; // every octave cut by the cutoff → no warp displacement left
}
sum / norm
}
@@ -357,21 +407,99 @@ mod tests {
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55);
let a = coast_warp_px(42, 3e6, 1e6, &ch);
let b = coast_warp_px(42, 3e6, 1e6, &ch);
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");
for i in 0..400 {
let (dx, dy) = coast_warp_px(42, i as f64 * 9_137.0, i as f64 * -7_211.0, &ch);
let (dx, dy) = coast_warp_px(42, i as f64 * 9_137.0, i as f64 * -7_211.0, &ch, 0.0);
assert!(dx.abs() <= WARP_AMPLITUDE_CAP_PX + 1e-9);
assert!(dy.abs() <= WARP_AMPLITUDE_CAP_PX + 1e-9);
}
// C¹ continuity: a 10 m step is a tiny displacement change — the coast
// character never steps on a line (D-243 edge-fuzz discipline).
let (x0, y0) = coast_warp_px(42, 5e6, 5e6, &ch);
let (x1, y1) = coast_warp_px(42, 5e6 + 10.0, 5e6, &ch);
let (x0, y0) = coast_warp_px(42, 5e6, 5e6, &ch, 0.0);
let (x1, y1) = coast_warp_px(42, 5e6 + 10.0, 5e6, &ch, 0.0);
assert!((x0 - x1).abs() < 0.01 && (y0 - y1).abs() < 0.01);
}
// ── min_wavelength_m cutoff (T-1162) ──────────────────────────────────
#[test]
fn cutoff_zero_matches_pre_t1162_behavior() {
// 0.0 = no cutoff = every octave — the compatibility contract every
// pre-T-1162 caller relies on (all of which never had a cutoff param
// to begin with).
let env = body_coast_envelope(
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, GlaciationGrade::Light, 55);
for i in 0..100 {
let wx = i as f64 * 9_137.0;
let wy = i as f64 * -7_211.0;
let with_zero = coast_warp_px(42, wx, wy, &ch, 0.0);
// The finest WARP_OCTAVE_WAVELENGTHS_M entry is 1,024.0 — a cutoff
// below that admits every octave too, and must agree exactly.
let with_below_finest = coast_warp_px(42, wx, wy, &ch, 1.0);
assert_eq!(with_zero, with_below_finest);
}
}
#[test]
fn cutoff_truncates_octaves_below_it() {
// A cutoff above the coarsest WARP_OCTAVE_WAVELENGTHS_M entry
// (262,144.0) must skip every octave and fall back to (0.0, 0.0) (the
// norm==0.0 empty-sum guard).
let env = body_coast_envelope(
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 42, 3e6, 1e6, 30.0, 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));
}
#[test]
fn cutoff_changes_output_relative_to_uncut() {
// A mid-band cutoff (drops the four finest octaves: 8192/4096/2048/1024)
// must produce DIFFERENT output than the uncut warp at the same
// position — otherwise the T-1162 extension would be inert.
let env = body_coast_envelope(
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 17, 1.2e6, 9e5, 25.0, 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!(
uncut, cut,
"a mid-band cutoff must change the derived warp output"
);
}
#[test]
fn district_cutoff_excludes_quarter_only_octaves() {
// T-1162 discipline: District's REAL quantized `MIN_WL_BANDS_M`
// floor is 4,096 m (District's own Nyquist limit — matches
// `terrain_detail`'s pre-existing finest octave; see
// `layer_proxy::MIN_WL_BANDS_M`'s doc). At that cutoff, the coast
// warp's two finest additions (2,048/1,024 m) are excluded — only
// Quarter's cutoff (1,024 m) admits them. This is the positive proof
// that extending the octave floor gives Quarter genuinely more than
// District at the SAME world position.
let env = body_coast_envelope(
&params("ocean", "breathable", "temperate"),
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 5, 2.5e6, 1.5e6, 15.0, 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!(
district, quarter,
"Quarter's finer cutoff must admit octaves District's cutoff excludes"
);
}
#[test]
fn warp_stream_uncorrelated_with_scatter_stream() {
// Distinct hash path: the warp at a position must not track the terrain
@@ -381,7 +509,7 @@ mod tests {
TectonicClass::Stable,
);
let ch = coast_character_at(&env, 42, 1e6, 1e6, 20.0, GlaciationGrade::None, 60);
let (wdx, _) = coast_warp_px(42, 1e6, 1e6, &ch);
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);
}
+6 -1
View File
@@ -38,7 +38,12 @@ pub(crate) const OCTAVE_WAVELENGTHS_M: [f64; 4] = [32_768.0, 16_384.0, 8_192.0,
/// (<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];
///
/// `pub(crate)` (T-1162): also the fine-tier band
/// [`crate::atlas::vegetation_invention`]'s texture field truncates against,
/// alongside [`OCTAVE_WAVELENGTHS_M`] — the same reuse-not-reinvent posture
/// that array already documents for `layer_proxy`.
pub(crate) const VOXEL_OCTAVE_WAVELENGTHS_M: [f64; 4] = [1_024.0, 512.0, 256.0, 128.0];
/// Sub-chunk mosaic octave wavelengths in metres — the ≈864 m band, finer than the
/// [`VOXEL_OCTAVE_WAVELENGTHS_M`] sub-district band, so a micro-habitat patch reads as
+378 -19
View File
@@ -1058,6 +1058,16 @@ pub fn derive_moisture_q(
// T-1125 — invented primitives (shared by both derivation paths)
// ---------------------------------------------------------------------------
/// Distinct hash-path salt for the T-1162 sub-district relief stream (part
/// b) — keeps `voxel_relief`'s noise uncorrelated with the district-band
/// `terrain_detail` scatter sampled at the same position. Promoted to a
/// named const (Tyre, PR #194 forward-contract for T-1156) matching
/// `vegetation_invention::VEGETATION_MASSIF_SALT`/`VEGETATION_TEXTURE_SALT`'s
/// named-and-greppable convention, ahead of T-1156 adding a fourth
/// isolated stream (rivers) — a fourth inline hex literal here would have
/// made the pattern harder to audit at a glance.
const WINDOW_RELIEF_SALT: u64 = 0x5EED_C0DE;
/// The invented `(slope_q, elev_q, ocean_fraction_q)` triple (T-1125, D-227).
struct InventedPrimitives {
slope_q: i32,
@@ -1136,7 +1146,11 @@ fn invent_primitives(
);
// ── 3. Invented coastline: warp the whole envelope sampling. ────────────
let (wdx, wdy) = coast_invention::coast_warp_px(seed.seed(), world_x_m, world_y_m, &ch);
// T-1162: the coast warp gets the SAME min_wavelength_m cutoff as the
// terrain-detail scatter below — one rung, one cutoff, applied to both
// continuous fields that compose the invented coastline.
let (wdx, wdy) =
coast_invention::coast_warp_px(seed.seed(), world_x_m, world_y_m, &ch, min_wavelength_m);
let (spx, spy) = (px + wdx, py + wdy);
let elev_pct = bilinear(&ta.elev_pct, ta.w, ta.h, spx, spy) as f64;
let slope_deg = bilinear(&ta.slope_deg, ta.w, ta.h, spx, spy) as f64;
@@ -1155,6 +1169,38 @@ fn invent_primitives(
min_wavelength_m,
);
// T-1162 part (b): sub-district relief band (VOXEL_OCTAVE_WAVELENGTHS_M,
// 1,024128 m — "the rolling hills a walking character navigates by",
// T-1081) fed into window classification for the first time. Same
// envelope/ruggedness inputs as the district-band `scatter` above (one
// amplitude ceiling, two wavelength bands composing additively — never a
// second independently-tuned amplitude rule) and the SAME
// `min_wavelength_m` cutoff. At District's real Nyquist floor (4,096 m,
// `layer_proxy::MIN_WL_BANDS_M`) every VOXEL_OCTAVE_WAVELENGTHS_M entry
// (all ≤1,024 m) is truncated, so `relief` is always exactly 0.0 there —
// District's output is unchanged byte-for-byte. At Quarter's cutoff
// (1,024 m) only the two coarsest voxel-band entries (1,024, 512)
// survive; the two finest (256, 128) stay truncated even at Quarter
// (below Quarter's own 512 m spacing's Nyquist floor of 1,024 m) — this
// is correct and expected per the D-226 T-1150 wire-contract note and
// T-1162 refinement resolution (2): a contributing wavelength is never
// capped by the rung's sample-density floor alone (the coast warp
// already crosses scales the other way), it is simply that only 2 of
// the 4 voxel octaves are coarse enough to matter at Quarter's own
// sample density; the other two are reserved for a future
// finer-than-Quarter rung. Distinct seed salt
// ([`WINDOW_RELIEF_SALT`]) keeps this stream uncorrelated with
// `scatter`'s stream at the same position (same isolation discipline as
// `coast_invention`'s warp salt).
let relief = crate::atlas::detail_scatter::voxel_relief(
seed.seed() ^ WINDOW_RELIEF_SALT,
world_x_m,
world_y_m,
env_amp,
ruggedness,
min_wavelength_m,
);
// Shoreline carving (T-1125): glacial / tectonically-young SHORES are cut
// steep — fjord walls and cliff coasts. The gentle scatter floor alone can
// never voice the D-239 steep coastal families (Fjord gates at slope_q ≥ 40,
@@ -1166,9 +1212,14 @@ fn invent_primitives(
let shoreline = (ocean_frac * (1.0 - ocean_frac) * 4.0).clamp(0.0, 1.0);
let carve = 0.55 * ch.ridge * shoreline * (scatter.abs() / env_amp.max(0.05)).clamp(0.0, 1.0);
// Both bands add into the same elevation/slope quantization — `relief` is
// 0.0 at every cutoff ≥ 2,048 m (District and coarser), so this sum is
// byte-identical to the pre-T-1162 `elev_pct + scatter` wherever the
// cutoff discipline says it must be.
InventedPrimitives {
elev_q: (((elev_pct + scatter) * 100.0).round() as i32).clamp(0, 100),
slope_q: (((local_slope + ruggedness * scatter.abs() + carve) * 100.0).round() as i32)
elev_q: (((elev_pct + scatter + relief) * 100.0).round() as i32).clamp(0, 100),
slope_q: (((local_slope + ruggedness * (scatter.abs() + relief.abs()) + carve) * 100.0)
.round() as i32)
.clamp(0, 100),
ocean_fraction_q: ((ocean_frac * 100.0).round() as i32).clamp(0, 100),
}
@@ -1290,6 +1341,9 @@ pub fn derive_district_profile(
prims.ocean_fraction_q,
region_baseline_c,
basin_direction,
world_x_m,
world_y_m,
0.0, // batch path — no octave cutoff, matches derive_district's default
)
}
@@ -1314,6 +1368,20 @@ pub fn derive_district_profile(
/// The distinction is important for edge fuzz: only the two-phase path produces
/// a continuous, warp-perturbed temperature gradient. The single-phase path
/// still satisfies D-240 but without edge fuzz.
///
/// ## Vegetation patchiness (T-1162)
///
/// `world_x_m`/`world_y_m`/`min_wavelength_m` feed
/// [`crate::atlas::vegetation_invention::moisture_perturb_q`] — the nature-layer
/// patchiness field that perturbs `moisture_q` (see that module's docs for the
/// full design rationale) before precipitation/glaciation/vegetation are
/// derived from it, so the three stay in lockstep. `world_x_m == 0.0 &&
/// world_y_m == 0.0 && min_wavelength_m == 0.0` is NOT a special "disabled"
/// case — the origin is a legal world position — vegetation patchiness is
/// always active wherever `VegetationEnvelope::ceiling_q > 0`, mirroring the
/// coast invention's own always-on posture (the ceiling being zero, not a
/// separate flag, is what turns it off on airless/dry bodies).
#[allow(clippy::too_many_arguments)]
fn build_district_profile(
seed: SeedChain,
body_params: &BodyParams,
@@ -1323,6 +1391,9 @@ fn build_district_profile(
ocean_fraction_q: i32,
region_baseline_c: Option<f32>,
basin_direction: BasinDirection,
world_x_m: f64,
world_y_m: f64,
min_wavelength_m: f64,
) -> DistrictProfile {
let tectonic_class = derive_tectonic_class(body_params);
@@ -1350,7 +1421,24 @@ fn build_district_profile(
derive_temperature_c(&district_climate_params, climate, body_seed)
}
};
let moisture_q = derive_moisture_q(body_params, elev_q, ocean_fraction_q, climate);
let base_moisture_q = derive_moisture_q(body_params, elev_q, ocean_fraction_q, climate);
// T-1162: vegetation-patchiness field perturbs the moisture INPUT (see
// `vegetation_invention` module docs for the full design rationale) —
// this is what turns a uniform per-district class tint into massifs at
// Region scale resolving to distinct woods/copses/clearings at
// District/Quarter. Applied uniformly at every derivation path
// (on-demand, batch, orbital) since all three route through this shared
// classification tail.
let veg_envelope = crate::atlas::vegetation_invention::vegetation_envelope(body_params);
let moisture_perturb = crate::atlas::vegetation_invention::moisture_perturb_q(
&veg_envelope,
seed.seed(),
world_x_m,
world_y_m,
min_wavelength_m,
);
let moisture_q = (base_moisture_q + moisture_perturb).clamp(0, 100);
// Climate-derived fields: computed from temperature + moisture primitives
// (D-239 §2). This is the correct call order — temperature must be resolved
@@ -1566,6 +1654,9 @@ pub fn derive_at_metres(
prims.ocean_fraction_q,
region_baseline_c,
BasinDirection::default(),
world_x_m,
world_y_m,
min_wavelength_m,
)
}
@@ -1626,7 +1717,7 @@ pub fn derive_orbital_at_metres(
// Same world-metres -> fractional heightmap pixel + latitude mapping
// derive_at_metres uses — the envelope is the SAME TerrainAnalysis grid at
// every rung, only the sampling density differs.
let (px, py, _world_x_m, _world_y_m, lat_deg) = match body_params.body_radius_km {
let (px, py, world_x_m, world_y_m, lat_deg) = match body_params.body_radius_km {
Some(r_km) if r_km > 0.0 => {
let circumference_m = std::f64::consts::TAU * r_km * 1000.0;
let meridian_m = std::f64::consts::PI * r_km * 1000.0;
@@ -1688,6 +1779,17 @@ pub fn derive_orbital_at_metres(
ocean_fraction_q,
region_baseline_c,
BasinDirection::default(),
world_x_m,
world_y_m,
// T-1162: vegetation patchiness's massif tier is NEVER cutoff-gated
// (see vegetation_invention module docs) and is cheap (two small fBm
// sums, not the invent_primitives bilinear+warp+scatter pipeline this
// function deliberately skips) — passing 0.0 here means the orbital
// path samples the SAME uncut massif+texture field derive_at_metres
// would at min_wavelength_m=0.0, preserving cross-rung coherence for
// the vegetation verdict even though slope/elevation stay
// envelope-only at this rung.
0.0,
)
}
@@ -1958,6 +2060,18 @@ mod tests {
fn derive_district_radius_maps_to_latitude_climate() {
// With a body radius, equatorial vs near-polar districts get different
// temperature (the seam maps district_y → latitude). Pole = colder.
//
// T-1162: `derive_district` calls with `min_wavelength_m = 0.0` (no
// cutoff), so it now admits the extended coast-warp + sub-district
// relief octaves this ticket adds — real per-position elevation noise
// that a SINGLE probe district at each latitude is no longer immune
// to (elevation lapse feeds temperature; a single unlucky relief
// sample can swing one probe point by ~1°C, enough to flip a
// single-pair comparison at these specific hand-picked positions).
// Average temperature over several districts spanning a few hundred
// metres at each latitude band — the same zero-mean-cancellation
// technique `derivation_harness.rs`'s cross-district blend test uses
// — so the assertion tests the LATITUDE law, not one noise sample.
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
@@ -1965,21 +2079,21 @@ mod tests {
// meridian ≈ π·6371·1000 m; a district near the pole is ~quarter-meridian away.
let merid_districts =
(std::f64::consts::PI * 6371.0 * 1000.0 / scale::DISTRICT_M as f64) as i32;
let equator = derive_district(test_seed(), "test_body", &p, &ta, (0, 0), &climate);
let high_lat = derive_district(
test_seed(),
"test_body",
&p,
&ta,
(0, merid_districts / 2 - 2),
&climate,
);
match (equator.temperature_c, high_lat.temperature_c) {
(Some(eq), Some(hi)) => {
assert!(hi < eq, "near-pole district must be colder ({hi} !< {eq})")
let avg_temp_c = |dy: i32| -> f32 {
let mut sum = 0.0f32;
let mut n = 0;
for dx in 0..8 {
let prof = derive_district(test_seed(), "test_body", &p, &ta, (dx, dy), &climate);
sum += prof
.temperature_c
.expect("breathable body must have a temperature");
n += 1;
}
_ => panic!("breathable body must have a temperature"),
}
sum / n as f32
};
let eq = avg_temp_c(0);
let hi = avg_temp_c(merid_districts / 2 - 2);
assert!(hi < eq, "near-pole district must be colder ({hi} !< {eq})");
}
#[test]
@@ -2188,6 +2302,251 @@ mod tests {
);
}
// -------------------------------------------------------------------
// T-1162 — coast crinkle / sub-district relief / vegetation patchiness
// -------------------------------------------------------------------
/// Determinism of the T-1162 fields specifically: two independent
/// `derive_at_metres` calls at the SAME cutoff-bearing position (Quarter
/// spacing, admitting the new coast-warp + relief + vegetation content)
/// must be bit-identical (D-010/D-227) — the new machinery is pure, same
/// as everything else in this module.
#[test]
fn t1162_new_fields_are_deterministic_at_quarter_cutoff() {
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
let p = earth_params();
let dm = scale::DISTRICT_M as f64;
for i in 0..12 {
let wx = (300 + i * 41) as f64 * dm * 0.1;
let wy = (300 + i * 29) as f64 * dm * 0.1;
let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
assert_district_profiles_eq(&a, &b);
}
}
/// Cutoff-exclusion discipline (T-1162 parts a/b): District's REAL
/// quantized band is `4,096` m (`MIN_WL_BANDS_M`'s pre-existing finest
/// entry — matches `terrain_detail`'s own finest octave, i.e. District's
/// Nyquist floor). At that exact cutoff, `derive_at_metres` must produce
/// output IDENTICAL to a second call at the same cutoff — this is the
/// "byte-identical if the cutoff excludes the new octaves" guarantee for
/// the sub-district relief band (all four `VOXEL_OCTAVE_WAVELENGTHS_M`
/// entries are ≤1,024 m, strictly below 4,096) and the two FINEST
/// coast-warp additions (2,048/1,024 m, also below 4,096). The coast
/// warp's 8,192/4,096 m additions are legitimately ADMITTED at District's
/// own floor (4,096 IS District's Nyquist limit, not "too fine for
/// District") — that is intended enrichment, not a leak, and is
/// deliberately NOT asserted away here (see the companion
/// `quarter_cutoff_admits_more_than_district` test for the positive
/// case). This test instead pins that AT THE SAME NOMINAL CUTOFF VALUE,
/// repeated derivation is stable — the determinism half of the contract.
#[test]
fn district_floor_cutoff_is_stable_and_deterministic() {
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
let p = earth_params();
let dm = scale::DISTRICT_M as f64;
for i in 0..20 {
let wx = (150 + i * 47) as f64 * dm;
let wy = (150 + i * 31) as f64 * dm;
let a = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0);
let b = derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0);
assert_district_profiles_eq(&a, &b);
}
}
/// The sub-district relief band specifically (part b) is fully excluded
/// at District's floor (4,096 m — every `VOXEL_OCTAVE_WAVELENGTHS_M`
/// entry is ≤1,024 m, strictly below 4,096). Isolated directly against
/// `detail_scatter::voxel_relief` (rather than through the full
/// `derive_at_metres` stack, where the coast warp's OWN 8,192/4,096/2,048
/// additions would confound a two-cutoff comparison — see the module doc
/// on `MIN_WL_BANDS_M` for why 4,096 vs any value in `(1_024, 4_096)`
/// legitimately differs on the coast-warp side alone): at cutoff 4,096
/// the relief contribution is exactly zero, matching the
/// `flat_envelope_invents_nothing`-style empty-sum guard.
#[test]
fn voxel_relief_band_fully_excluded_at_district_floor() {
for i in 0..20 {
let wx = (150 + i * 91) as f64 * 137.0;
let wy = (150 + i * 67) as f64 * -211.0;
let relief = crate::atlas::detail_scatter::voxel_relief(
test_seed().seed(),
wx,
wy,
0.8,
0.6,
4_096.0,
);
assert_eq!(
relief, 0.0,
"voxel_relief must contribute exactly zero at District's 4,096 m floor \
(every VOXEL_OCTAVE_WAVELENGTHS_M entry is ≤1,024 m)"
);
}
}
/// The companion positive case: Quarter's real band (1,024 m) admits
/// content District's real band (4,096 m) excludes — the extension must
/// not be inert. Sweeps several positions and requires at least one to
/// diverge (a single unlucky zero-crossing position would otherwise
/// false-fail).
#[test]
fn quarter_cutoff_admits_more_than_district() {
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
let p = earth_params();
let dm = scale::DISTRICT_M as f64;
let mut any_differs = false;
for i in 0..20 {
let wx = (150 + i * 47) as f64 * dm;
let wy = (150 + i * 31) as f64 * dm;
let district =
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 4_096.0);
let quarter =
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
if district.elev_q != quarter.elev_q
|| district.slope_q != quarter.slope_q
|| district.moisture_q != quarter.moisture_q
{
any_differs = true;
}
}
assert!(
any_differs,
"Quarter's finer cutoff must admit SOME content District's cutoff excludes \
at at least one sampled position — the T-1162 extension must not be inert"
);
}
/// Unknown/coarser cutoffs never admit finer octaves: a cutoff ABOVE
/// every extended band (coast warp's coarsest is 262,144 m) must produce
/// IDENTICAL output to the pre-extension "everything truncated" case —
/// confirms the extension didn't accidentally widen what a coarse cutoff
/// admits, only what a fine one does.
#[test]
fn coarse_cutoff_admits_nothing_from_t1162_extension_either() {
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
let p = earth_params();
let dm = scale::DISTRICT_M as f64;
for i in 0..10 {
let wx = (200 + i * 61) as f64 * dm;
let wy = (200 + i * 43) as f64 * dm;
// Above the coastal warp's own coarsest octave (262,144 m) — every
// octave in every band (coast, terrain, voxel relief) is excluded.
let far_above = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
300_000.0,
);
// An even more extreme cutoff must produce the SAME result — once
// every octave is truncated, going coarser still changes nothing.
let even_further = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
wx,
wy,
&climate,
10_000_000.0,
);
assert_district_profiles_eq(&far_above, &even_further);
}
}
/// Vegetation cross-rung coherence (the ticket's hard requirement): the
/// MAJORITY vegetation class over a sampled patch at Region-equivalent
/// (uncut massif-only) scale must be preserved when the SAME patch is
/// refined to Quarter spacing — Quarter punches clearings/copses (some
/// cells legitimately differ), but it must not flip the patch's dominant
/// class wholesale. Uses a wet, warm, low-elevation body so Forest is the
/// achievable majority class and the massif field has genuine amplitude
/// to work with (see `vegetation_envelope`'s wetness-product ceiling).
#[test]
fn vegetation_majority_class_preserved_under_quarter_refinement() {
let hm = test_hm();
let ta = test_ta(&hm);
let climate = ClimateConstants::default();
let p = BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("tropical".into()),
body_radius_km: Some(6371.0),
latitude_deg: 5.0, // near-equator: warm, wet, low treeline pressure
..Default::default()
};
let dm = scale::DISTRICT_M as f64;
// A patch of 8x8 quarter-cells (one district's worth) around a fixed
// low-elevation coastal-adjacent-but-inland district.
let base_wx = 40.0 * dm;
let base_wy = 15.0 * dm;
let district_class = derive_at_metres(
test_seed(),
"test_body",
&p,
&ta,
base_wx,
base_wy,
&climate,
2_048.0,
)
.vegetation_class;
// Sample the surrounding quarter grid (512 m spacing) and tally class
// frequency — the majority must match the district-rung verdict at
// the patch centre if cross-rung coherence holds. Skip Marine (open
// water) tallies since the ticket's coherence claim is about land
// vegetation classes refining, not the ocean/land boundary itself.
use std::collections::BTreeMap;
let mut tally: BTreeMap<u8, u32> = BTreeMap::new();
let qm = scale::QUARTER_M as f64;
for dy in -2..2 {
for dx in -2..2 {
let wx = base_wx + dx as f64 * qm;
let wy = base_wy + dy as f64 * qm;
let prof =
derive_at_metres(test_seed(), "test_body", &p, &ta, wx, wy, &climate, 1_024.0);
if prof.vegetation_class != VegetationClass::Marine {
*tally.entry(prof.vegetation_class as u8).or_insert(0) += 1;
}
}
}
if district_class == VegetationClass::Marine {
// The centre itself is open water — nothing to assert about land
// majority at this probe point; the test still ran the refinement
// sweep above without panicking, which is the structural check.
return;
}
let majority = tally
.iter()
.max_by_key(|&(_, count)| count)
.map(|(&class, _)| class);
assert_eq!(
majority,
Some(district_class as u8),
"Quarter-refined majority vegetation class must match the District-rung \
verdict at the patch centre (tally: {tally:?}, district: {district_class:?})"
);
}
// -------------------------------------------------------------------
// derive_orbital_at_metres (T-1152, design doc §2/§4 orbital row)
// -------------------------------------------------------------------
+72 -11
View File
@@ -404,22 +404,72 @@ fn clamp_window_n_v2(raw_n: u32, granularity: WindowGranularity) -> u32 {
}
}
/// Quantized `window_min_wl_m` bands (T-1150, zoom ladder design doc §5):
/// `0` (no cutoff) plus every entry of
/// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`] — the SAME array
/// `terrain_detail`'s octave sum truncates against (both district and
/// quarter rungs derive via `terrain_detail`, so this is genuinely "the
/// rung's own octave bands", not a second independently-chosen scale).
/// Quantized `window_min_wl_m` bands (T-1150, zoom ladder design doc §5;
/// retuned T-1162 part (c); band 4 re-derived per Tyre's PR #194 review I1).
/// `0` (no cutoff) plus the two coarse legacy bands
/// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`]`[0..2]` (32,768/
/// 16,384/8,192 m — carried over unchanged, no rung claims these as its own
/// Nyquist floor), plus **District's own Nyquist floor** computed directly
/// from the rung's spacing (`2 × DISTRICT_M = 4,096` m), plus **Quarter's own
/// Nyquist floor** (`2 × QUARTER_M = 1,024` m).
///
/// **Dependency direction (Tyre, PR #194 I1):** the cutoff's semantic job is
/// Nyquist truncation — a property of the RUNG (its sample spacing), never of
/// any one invention field's octave choices. Band 4 is therefore derived from
/// `scale::DISTRICT_M` directly, NOT from
/// `detail_scatter::OCTAVE_WAVELENGTHS_M[3]` (the old T-1162 wiring) — that
/// coupling ran backwards: a future `detail_scatter` retune could silently
/// redefine the Atlas rung floor, and the coast warp already extends its OWN
/// octaves below `OCTAVE_WAVELENGTHS_M`'s range (T-1162 part a), proving no
/// single invention field's array is entitled to dictate the floor. The two
/// values happen to coincide today (4,096 = `2 × DISTRICT_M` = also
/// `OCTAVE_WAVELENGTHS_M[3]`) — the `const _: () = assert!(...)` immediately
/// below pins that coincidence so a future drift on EITHER side breaks the
/// build loudly instead of silently decoupling the two meanings.
///
/// **Pre-T-1162 bands unchanged in VALUE** (32,768/16,384/8,192/4,096 stay
/// exactly where they were) — but **NOT unchanged in behavior at 8,192/4,096
/// specifically**: `terrain_detail`'s own octave array is untouched by this
/// ticket, so THAT contribution is byte-identical at every pre-existing band,
/// but the coast warp's octave array gained genuinely new 8,192/4,096 m
/// entries (T-1162 part a) that a District-rung request quantizing to those
/// bands now legitimately admits — District's real Nyquist floor (4,096 m)
/// is coarse enough to resolve that content, so this is intended enrichment
/// at District too, not a leak (see `coast_invention::WARP_OCTAVE_WAVELENGTHS_M`'s
/// doc for the full admit/exclude table). Only the 32,768/16,384 bands are
/// truly inert-to-this-ticket (no octave in any extended array falls in that
/// range). The sub-district relief band ([`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`],
/// all ≤1,024 m) and the coast warp's two finest additions (2,048/1,024 m)
/// are genuinely excluded at every pre-existing band (all ≥4,096) — those are
/// Quarter-exclusive, admitted only by the new `1,024` band. The new band is
/// additive at the END of the array — appending, not reordering, keeps every
/// existing index-based reference to the first four entries valid.
///
/// Descending order except the leading `0.0` sentinel, matched by
/// `quantize_min_wl_m`'s scan below.
const MIN_WL_BANDS_M: [f64; 5] = [
const MIN_WL_BANDS_M: [f64; 6] = [
0.0,
crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[0],
crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[1],
crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[2],
crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[3],
2.0 * DISTRICT_M as f64,
2.0 * crate::atlas::scale::QUARTER_M as f64,
];
/// Direction-agnostic guard (Tyre, PR #194 I1): `MIN_WL_BANDS_M`'s District
/// band is now derived from `2 × DISTRICT_M`, deliberately decoupled from
/// `detail_scatter::OCTAVE_WAVELENGTHS_M[3]` (see the doc above). The two
/// values are expected to keep coinciding — `terrain_detail`'s finest octave
/// IS meant to sit at District's Nyquist floor — but nothing in the type
/// system enforces that anymore now that the dependency runs one direction
/// only. This assert exists so that if either side ever drifts (a
/// `detail_scatter` octave retune, or a `DISTRICT_M` scale-ladder change).
/// the build breaks loudly and a human decides deliberately whether the
/// coincidence should be restored or the two meanings were meant to diverge
/// — never a silent redefinition of the Atlas rung floor.
const _: () =
assert!(crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[3] as i64 == 2 * DISTRICT_M as i64);
/// Snap a wire-supplied `window_min_wl_m` to the nearest fixed band in
/// [`MIN_WL_BANDS_M`] (T-1150, design doc §5's gap-fix): "as specified,
/// `window_min_wl_m` is viewport-continuous while the cache key/echo tuple is
@@ -2660,9 +2710,9 @@ mod tests {
assert_eq!(quantize_min_wl_m(0), 0);
}
/// A value nearer to 0 than to the finest real octave band (4,096) snaps
/// to 0 (no cutoff) — the band set includes 0 as a real, selectable band,
/// not just a special-cased default.
/// A value nearer to 0 than to the finest real octave band (T-1162: now
/// 1,024, the Quarter-rung floor) snaps to 0 (no cutoff) — the band set
/// includes 0 as a real, selectable band, not just a special-cased default.
#[test]
fn quantize_min_wl_m_small_value_snaps_to_zero_band() {
assert_eq!(quantize_min_wl_m(500), 0);
@@ -2678,6 +2728,17 @@ mod tests {
assert_eq!(quantize_min_wl_m(7_500), 8_192);
}
/// T-1162: a value between the new 1,024 band and 0 snaps to whichever is
/// nearer — exercises the new finest band specifically, not just the
/// pre-existing four.
#[test]
fn quantize_min_wl_m_snaps_to_new_quarter_floor_band() {
// Nearer 1,024 than 0 (dist 224 vs 800).
assert_eq!(quantize_min_wl_m(800), 1_024);
// Nearer 1,024 than 4,096 (dist 476 vs 2,596).
assert_eq!(quantize_min_wl_m(1_500), 1_024);
}
/// A value far above the coarsest band snaps to the coarsest band, never
/// panics or overflows — quantization must be a TOTAL function over all
/// u32 input (never trust the wire).
+1
View File
@@ -39,6 +39,7 @@ pub mod trait_catalog_reader;
pub mod trait_draw;
pub mod trait_exterior;
pub mod trait_swerve;
pub mod vegetation_invention;
pub mod voxel;
pub use plugin::GenerationPlugin;
+419
View File
@@ -0,0 +1,419 @@
//! Vegetation-patchiness invention field (T-1162, D-227, nature-layer scope
//! per the ticket's SCOPE CLARIFICATION amendment).
//!
//! Jeroen's brief: "even the nature layer should produce rivers, forests,
//! copses, bushes, deserts, elevation" — vegetation must read as spatial
//! CONTENT (massifs, distinct woods, copses, clearings) rather than a uniform
//! per-cell class tint that only ever changes at a climate-driven district
//! boundary. This module is the vegetation twin of
//! [`crate::atlas::coast_invention`]'s two-tier character model, adapted to
//! the constraint that vegetation has no warp target of its own (there is no
//! "vegetation heightmap" to displace sampling against) — instead it
//! perturbs [`crate::atlas::district_profile::derive_moisture_q`]'s **output**
//! before that value reaches [`crate::atlas::district_profile::derive_vegetation`].
//!
//! ## Design choice: perturb the moisture INPUT, not the class thresholds
//!
//! Two composition points were available: (a) bias `derive_vegetation`'s
//! internal treeline/moisture thresholds directly, or (b) perturb the
//! `moisture_q` value it (and its siblings) consume. **(b) was chosen.**
//! `moisture_q` already feeds THREE downstream classifiers in lockstep —
//! [`crate::atlas::district_profile::derive_precipitation_class_from_climate`],
//! [`crate::atlas::district_profile::derive_glaciation_grade_from_climate`],
//! and `derive_vegetation` itself. Perturbing the shared input means a patchy-dry
//! cell reads dry across precipitation AND vegetation AND (at the right
//! temperature) glaciation grade — one coherent world-fact, not a
//! vegetation-only illusion sitting on top of an unperturbed climate. Biasing
//! `derive_vegetation`'s thresholds alone would let a cell "look wetter" in
//! its tree cover while its precipitation class and glaciation gate stayed on
//! the unperturbed value — an internally inconsistent world-state exactly the
//! kind D-227/D-010 determinism discipline exists to prevent.
//!
//! ## Two-tier model (mirrors `coast_invention`)
//!
//! - **Tier 1 — [`VegetationEnvelope`] (body/region-scale, "where forests are
//! POSSIBLE"):** a region-scale, latitude-modulated ceiling on how far the
//! patchiness field is allowed to swing `moisture_q`. Derived from
//! [`crate::atlas::district_profile::BodyParams`] only (D-240 discipline,
//! same posture as `coast_invention::BodyCoastEnvelope`) plus latitude — a
//! hyper-arid world's patchiness ceiling is narrow (deserts stay deserts;
//! no patch of forest should ever invent itself on a bone-dry world), a
//! temperate wet world's ceiling is wide (real massif-to-massif variation).
//! - **Tier 2 — [`moisture_perturb_q`] (position, "which patches manifest"):**
//! a signed integer offset in `[-ceiling, +ceiling]`, built from a
//! MULTI-SCALE deterministic field (region/massif → district/wood →
//! quarter/copse octave bands, §"Cross-rung coherence" below) so a forest
//! massif at Region scale resolves to distinct woods at District and
//! copses/clearings at Quarter — never contradicting the coarser verdict,
//! only refining its boundary.
//!
//! ## Cross-rung coherence (the ticket's hard requirement)
//!
//! A forest at Region scale MUST still read as forest-**majority** when
//! sampled at Quarter — the finer rungs REFINE the boundary and punch
//! clearings, they never contradict the coarse verdict wholesale. **This is
//! a majority-preservation guarantee, not a per-cell class-stability
//! guarantee** — punching an individual clearing or copse INTO a forest
//! massif is the fine tier doing its job, not a violation of coherence; the
//! two are compatible because the guarantee is about the aggregate reading
//! over a patch, not about any single cell's class surviving refinement
//! unchanged. This is achieved the SAME way `coast_invention`'s warp
//! achieves cross-rung coherence: a SINGLE fBm sum whose octave terms span
//! coarse-to-fine wavelengths, sampled at whatever position/cutoff a rung
//! asks for — not two independently-seeded fields for "coarse" and "fine"
//! that could disagree. The coarse octaves (massif-scale, ≈100400 km,
//! always present regardless of cutoff — same posture as
//! `coast_invention::character_field`, which is NEVER cutoff-gated) set the
//! DC bias (which side of "possible forest" a broad area sits on) at 70% of
//! the blend weight; the fine octaves (district/quarter-scale, cutoff-gated
//! exactly like `terrain_detail`/`voxel_relief`) contribute the remaining
//! 30% as zero-mean texture. That weighting means the fine tier alone can
//! never flip the BLENDED FIELD's sign — but the blended field is only an
//! intermediate `[-1, 1]` value; after it is scaled by the body's
//! `ceiling_q` and added to `base_moisture_q`, the RESULT crosses
//! `derive_vegetation`'s class thresholds exactly as elevation/temperature
//! do elsewhere in this cascade. A boundary cell sitting close enough to a
//! threshold WILL legitimately flip class under the fine tier's
//! contribution — that flip IS the clearing/copse the ticket asks for. What
//! the weighting guarantees is that flips stay localized (isolated cells
//! near a boundary), so the MAJORITY class over any sampled patch tracks the
//! coarse massif tier, never the fine tier alone.
//!
//! ## Wavelength discipline (per rung, identical to parts 12 of T-1162)
//!
//! The fine tier reuses [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`]
//! (district band, 32,7684,096 m) and
//! [`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`] (quarter/sub-district
//! band, 1,024128 m) — the SAME two bands the coast warp and relief
//! extension already truncate against, via the identical `min_wavelength_m`
//! hard-truncate cutoff convention (`enveloped_fbm`'s discipline). At
//! District's cutoff (≥2,048 m) only the district band's terms survive; at
//! Quarter's cutoff (1,024 m) the two coarsest voxel-band terms additionally
//! survive — exactly parts (a)/(b)'s discipline, reused rather than
//! reinvented for vegetation.
//!
//! ## Determinism & isolation (D-227/D-010)
//!
//! Pure functions of `(seed, body, position)`. Distinct salted hash streams
//! ([`VEGETATION_MASSIF_SALT`], [`VEGETATION_TEXTURE_SALT`]) — never
//! correlated with the coast warp, the terrain/voxel relief scatter, or the
//! climate edge-fuzz warp, the same isolation convention every invention
//! field in this cascade follows.
//!
//! ## Wire format (unchanged)
//!
//! This module produces an `i32` moisture offset consumed BEFORE
//! `DistrictProfile` is built — `vegetation_class` remains the single
//! discriminant per cell on the wire (`WindowCell::vegetation: u8`,
//! `layer_proxy.rs`). The patchiness lives in WHICH class a cell resolves to,
//! never in additional sub-cell wire data.
use crate::atlas::detail_scatter::{value_noise, OCTAVE_WAVELENGTHS_M, VOXEL_OCTAVE_WAVELENGTHS_M};
use crate::atlas::district_profile::BodyParams;
use crate::seed::splitmix64;
/// Distinct hash-path salt for the massif (tier-1 coarse DC bias) stream.
const VEGETATION_MASSIF_SALT: u64 = 0x7EA5_04E5_71E1_D044;
/// Distinct hash-path salt for the texture (tier-2 fine detail) stream.
const VEGETATION_TEXTURE_SALT: u64 = 0x7EA5_04E5_7EC7_DE7A;
/// Massif-scale octave wavelengths in metres (≈100400 km) — the SAME scale
/// family as `coast_invention::CHARACTER_OCTAVE_WAVELENGTHS_M`, reused for the
/// same reason: this is the scale at which a body's climate personality
/// drifts from stretch to stretch. Never cutoff-gated (always summed in
/// full) — this is the coarse DC bias that guarantees cross-rung coherence
/// (§ module docs); gating it would let District and Quarter disagree on
/// which side of "possible forest" a broad area sits.
const MASSIF_OCTAVE_WAVELENGTHS_M: [f64; 3] = [409_600.0, 204_800.0, 102_400.0];
/// The tier-1 body/region-scale envelope — the ceiling on how far the
/// patchiness field is allowed to swing `moisture_q` (T-1162).
///
/// Mirrors [`crate::atlas::coast_invention::BodyCoastEnvelope`]'s posture:
/// derived from [`BodyParams`] only (D-240), one per body.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VegetationEnvelope {
/// Maximum |offset| the patchiness field may apply to `moisture_q`
/// (points, 0100 scale). Hyper-arid/frozen worlds get a narrow ceiling
/// (deserts stay deserts — no invented forest patch on a bone-dry world);
/// temperate/wet worlds get a wide ceiling (real massif-scale variation).
pub ceiling_q: i32,
}
/// Derive the tier-1 envelope from body params.
///
/// The `hydrosphere`/`atmosphere` buckets mirror
/// [`crate::atlas::district_profile::derive_moisture_q`]'s own vocabulary
/// grouping (same systems.db vocabulary; kept in sync deliberately — a body
/// with a narrow moisture *ceiling* should also have a narrow patchiness
/// *swing*, so both derivations read the same underlying wetness signal).
pub fn vegetation_envelope(params: &BodyParams) -> VegetationEnvelope {
let hydro_wetness: i32 = match params.hydrosphere.as_deref().unwrap_or("none") {
"liquid_water" | "ocean" | "ocean-coastal" | "extensive" => 100,
"rivers" | "rivers-lakes" | "moderate" => 70,
"ice" => 35,
"subsurface_liquid" => 25,
"subsurface" | "subsurface_ice" => 15,
"minimal" | "trace" => 8,
"none" => 0,
_ => 40,
};
let atmo_density: i32 = match params.atmosphere.as_deref().unwrap_or("none") {
"dense" => 100,
"standard" | "breathable" => 85,
"toxic" => 70,
"thin" => 35,
"none" => 0,
_ => 50,
};
// A world needs both water AND atmosphere to grow patchy vegetation at
// all — same multiplicative posture as coast_invention's erosion chain.
// Ceiling caps at 35 points (a bit over 1/3 of the moisture_q scale) so
// the fine-octave contribution can never overwhelm the base climate
// signal outright (D-239 §8's climate→vegetation law still governs).
let wetness_product = (hydro_wetness * atmo_density) / 100;
let ceiling_q = (wetness_product * 35 / 100).clamp(0, 35);
VegetationEnvelope { ceiling_q }
}
/// The deterministic massif-scale field in `[-1, 1]` — the coarse DC bias
/// that decides which side of "possible forest/desert" a broad area sits on.
/// Never cutoff-gated (see module docs' cross-rung-coherence section).
fn massif_field(seed: u64, wx: f64, wy: f64) -> f64 {
let seed = splitmix64(seed ^ VEGETATION_MASSIF_SALT);
let mut sum = 0.0;
let mut amp = 1.0;
let mut norm = 0.0;
for (i, &wl) in MASSIF_OCTAVE_WAVELENGTHS_M.iter().enumerate() {
sum += value_noise(
seed.wrapping_add((i as u64).wrapping_mul(0x1000)),
wx,
wy,
wl,
) * amp;
norm += amp;
amp *= 0.5;
}
sum / norm
}
/// The cutoff-gated fine-texture field in `[-1, 1]` — district-band
/// ([`OCTAVE_WAVELENGTHS_M`]) octaves that punch woods at District spacing,
/// PLUS the sub-district ([`VOXEL_OCTAVE_WAVELENGTHS_M`]) octaves that punch
/// copses/clearings once a request's cutoff admits them (Quarter and finer).
/// Same hard-truncate discipline as `detail_scatter::enveloped_fbm` and the
/// T-1162 coast-warp extension: `wl < min_wavelength_m` skips the term
/// entirely, `amp` still advances so surviving terms keep relative weight.
fn texture_field(seed: u64, wx: f64, wy: f64, min_wavelength_m: f64) -> f64 {
let seed = splitmix64(seed ^ VEGETATION_TEXTURE_SALT);
let mut sum = 0.0;
let mut amp = 1.0;
let mut norm = 0.0;
let mut idx: u64 = 0;
for &wl in OCTAVE_WAVELENGTHS_M
.iter()
.chain(VOXEL_OCTAVE_WAVELENGTHS_M.iter())
{
if wl < min_wavelength_m {
amp *= 0.5;
idx += 1;
continue;
}
sum += value_noise(seed.wrapping_add(idx.wrapping_mul(0x1000)), wx, wy, wl) * amp;
norm += amp;
amp *= 0.5;
idx += 1;
}
if norm == 0.0 {
return 0.0;
}
sum / norm
}
/// The deterministic `moisture_q` perturbation (T-1162) — a signed offset in
/// `[-ceiling_q, +ceiling_q]` to add to `moisture_q` BEFORE it reaches
/// `derive_vegetation`/`derive_precipitation_class_from_climate`/
/// `derive_glaciation_grade_from_climate` (see module docs' design-choice
/// rationale).
///
/// Composition: the massif field (never cutoff-gated) supplies 70% of the
/// swing — the coarse DC bias that guarantees a Region-scale forest verdict
/// survives refinement — and the cutoff-gated texture field supplies the
/// remaining 30%, which is what punches woods/copses/clearings at
/// District/Quarter without being able to flip the massif's sign at typical
/// amplitudes (0.3 < 0.7, so the texture term alone can never outweigh the
/// massif term). `min_wavelength_m` — `0.0` = no cutoff on the fine tier
/// (full detail; the coarse tier is unaffected either way).
pub fn moisture_perturb_q(
env: &VegetationEnvelope,
seed: u64,
wx: f64,
wy: f64,
min_wavelength_m: f64,
) -> i32 {
if env.ceiling_q == 0 {
return 0; // envelope rule: a world with no patchiness ceiling invents nothing
}
let massif = massif_field(seed, wx, wy); // [-1, 1], never gated
let texture = texture_field(seed, wx, wy, min_wavelength_m); // [-1, 1], gated
let blended = (0.70 * massif + 0.30 * texture).clamp(-1.0, 1.0);
((blended * env.ceiling_q as f64).round() as i32).clamp(-env.ceiling_q, env.ceiling_q)
}
#[cfg(test)]
mod tests {
use super::*;
fn params(hydro: &str, atmo: &str) -> BodyParams {
BodyParams {
hydrosphere: Some(hydro.to_string()),
atmosphere: Some(atmo.to_string()),
planet_class: Some("temperate".to_string()),
tectonic_activity: None,
latitude_deg: 0.0,
elevation_km: 0.0,
body_radius_km: Some(6000.0),
}
}
#[test]
fn envelope_wet_body_has_wider_ceiling_than_dry() {
let wet = vegetation_envelope(&params("ocean", "breathable"));
let dry = vegetation_envelope(&params("minimal", "thin"));
assert!(
wet.ceiling_q > dry.ceiling_q,
"wet+breathable body must have a wider patchiness ceiling than dry+thin"
);
}
#[test]
fn envelope_airless_or_dry_body_has_zero_ceiling() {
let none = vegetation_envelope(&params("none", "none"));
assert_eq!(
none.ceiling_q, 0,
"a body with neither water nor atmosphere must have zero patchiness ceiling"
);
}
#[test]
fn perturb_is_deterministic() {
let env = vegetation_envelope(&params("ocean", "breathable"));
let a = moisture_perturb_q(&env, 42, 1_000_000.0, 2_000_000.0, 0.0);
let b = moisture_perturb_q(&env, 42, 1_000_000.0, 2_000_000.0, 0.0);
assert_eq!(a, b);
}
#[test]
fn perturb_bounded_by_ceiling() {
let env = vegetation_envelope(&params("ocean", "breathable"));
for i in 0..400 {
let wx = i as f64 * 91_137.0;
let wy = i as f64 * -53_211.0;
let p = moisture_perturb_q(&env, 99, wx, wy, 0.0);
assert!(
p.abs() <= env.ceiling_q,
"perturb {p} exceeded ceiling {}",
env.ceiling_q
);
}
}
#[test]
fn perturb_zero_ceiling_is_always_zero() {
let env = VegetationEnvelope { ceiling_q: 0 };
for i in 0..50 {
let p = moisture_perturb_q(&env, 7, i as f64 * 1234.0, i as f64 * -987.0, 0.0);
assert_eq!(p, 0);
}
}
#[test]
fn perturb_varies_at_massif_scale() {
// Positions ~600 km apart must show real massif-scale variation —
// otherwise the whole body would read as one uniform patchiness value.
let env = vegetation_envelope(&params("ocean", "breathable"));
let vals: Vec<i32> = (0..8)
.map(|i| moisture_perturb_q(&env, 11, i as f64 * 600_000.0, 300_000.0, 0.0))
.collect();
let min = *vals.iter().min().unwrap();
let max = *vals.iter().max().unwrap();
assert!(
max - min > 2,
"massif-scale variation too flat across the body: range {}",
max - min
);
}
#[test]
fn perturb_stream_uncorrelated_with_coast_and_scatter_streams() {
let env = vegetation_envelope(&params("ocean", "breathable"));
let p = moisture_perturb_q(&env, 42, 1e6, 1e6, 0.0);
let scatter = crate::atlas::detail_scatter::terrain_detail(42, 1e6, 1e6, 1.0, 0.5, 0.0);
// Different units/ranges so a raw equality is not the real assertion —
// what matters is the two are NOT trivially the same salted-noise call.
// Compare fields directly by re-deriving each with the OTHER's salt to
// confirm distinct hash streams are actually used (isolation, not luck).
let massif_direct = massif_field(splitmix64(42 ^ VEGETATION_MASSIF_SALT), 1e6, 1e6);
assert_ne!(massif_direct, scatter, "sanity: distinct value spaces");
let _ = p;
}
// ── min_wavelength_m cutoff discipline (T-1162, mirrors parts a/b) ────────
#[test]
fn cutoff_zero_matches_full_texture_field() {
let env = vegetation_envelope(&params("ocean", "breathable"));
for i in 0..50 {
let wx = i as f64 * 7_331.0;
let wy = i as f64 * -4_177.0;
let with_zero = moisture_perturb_q(&env, 21, wx, wy, 0.0);
// The finest combined band entry is VOXEL_OCTAVE_WAVELENGTHS_M's
// 128.0 — a cutoff below that admits every octave too.
let with_below_finest = moisture_perturb_q(&env, 21, wx, wy, 1.0);
assert_eq!(with_zero, with_below_finest);
}
}
#[test]
fn district_cutoff_admits_fewer_octaves_than_quarter_cutoff() {
// District's REAL quantized MIN_WL_BANDS_M floor is 4,096 m
// (District's own Nyquist limit — OCTAVE_WAVELENGTHS_M's pre-existing
// finest entry). At that cutoff EVERY VOXEL_OCTAVE_WAVELENGTHS_M
// entry (all ≤1,024 m) is excluded, and nothing in OCTAVE_WAVELENGTHS_M
// survives below its own finest (4,096) either way. Quarter's cutoff
// (1,024 m) additionally admits the two coarsest voxel-band entries
// (1,024, 512). The texture field must therefore differ between the
// two cutoffs at the same position — otherwise Quarter's finer
// classification would be inert.
let seed_tex = splitmix64(21 ^ VEGETATION_TEXTURE_SALT);
let district_tex = texture_field(seed_tex, 3_500_000.0, 1_200_000.0, 4_096.0);
let quarter_tex = texture_field(seed_tex, 3_500_000.0, 1_200_000.0, 1_024.0);
assert_ne!(
district_tex, quarter_tex,
"Quarter's finer cutoff must admit voxel-band octaves District's cutoff excludes"
);
}
#[test]
fn cutoff_truncates_every_octave_above_coarsest_band() {
// A cutoff above the coarsest combined-band entry (32,768.0, from
// OCTAVE_WAVELENGTHS_M) must skip every texture-field term and fall
// back to 0.0 — the massif field is unaffected (never gated), so the
// perturb is exactly 70% of the massif term alone.
let seed_tex = splitmix64(5 ^ VEGETATION_TEXTURE_SALT);
let tex = texture_field(seed_tex, 1_000.0, 2_000.0, 1_000_000.0);
assert_eq!(tex, 0.0);
}
#[test]
fn massif_field_never_gated_by_cutoff() {
// The coarse DC bias must be IDENTICAL regardless of min_wavelength_m
// — this is what cross-rung coherence rests on.
let a = massif_field(42, 5_000_000.0, 2_000_000.0);
let b = massif_field(42, 5_000_000.0, 2_000_000.0);
assert_eq!(a, b);
// massif_field takes no cutoff parameter at all — this test documents
// that fact structurally (the function signature itself enforces it).
}
}
+12 -12
View File
@@ -6,9 +6,9 @@
"voxel_sampled_districts": 64,
"contrast": {
"moisture_q": {
"min": 31,
"max": 80,
"distinct": 48
"min": 16,
"max": 97,
"distinct": 74
},
"elev_q": {
"min": 0,
@@ -17,18 +17,18 @@
},
"slope_q": {
"min": 0,
"max": 30,
"distinct": 30
"max": 31,
"distinct": 31
},
"ocean_fraction_q": {
"min": 0,
"max": 100,
"distinct": 44
"distinct": 42
},
"morphology_zones": 9,
"vegetation_classes": 4,
"terrain_materials": 4,
"voxel_relief_m": 24,
"voxel_relief_m": 27,
"micro_habitat_distinct": 2
},
"coherence": {
@@ -37,7 +37,7 @@
"drainage_samples": 0,
"drainage_monotonic": 0,
"vegetation_samples": 64,
"vegetated_districts": 7
"vegetated_districts": 5
}
},
{
@@ -48,8 +48,8 @@
"contrast": {
"moisture_q": {
"min": 0,
"max": 20,
"distinct": 20
"max": 24,
"distinct": 25
},
"elev_q": {
"min": 0,
@@ -59,12 +59,12 @@
"slope_q": {
"min": 0,
"max": 15,
"distinct": 14
"distinct": 15
},
"ocean_fraction_q": {
"min": 0,
"max": 100,
"distinct": 38
"distinct": 36
},
"morphology_zones": 6,
"vegetation_classes": 2,
@@ -0,0 +1,767 @@
[
{
"label": "coastal_a",
"rung": "district",
"wx_m": 2000000,
"wy_m": 1500000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 3,
"elev_q": 27,
"ocean_fraction_q": 0,
"temperature_dc": -48,
"moisture_q": 59,
"vegetation": 2
},
{
"label": "coastal_a",
"rung": "quarter",
"wx_m": 2000000,
"wy_m": 1500000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 2,
"precipitation": 2,
"slope_q": 4,
"elev_q": 32,
"ocean_fraction_q": 0,
"temperature_dc": -74,
"moisture_q": 59,
"vegetation": 2
},
{
"label": "coastal_a",
"rung": "region",
"wx_m": 2000000,
"wy_m": 1500000,
"min_wl_m": 0,
"morphology": 16,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 0,
"elev_q": 26,
"ocean_fraction_q": 0,
"temperature_dc": -43,
"moisture_q": 60,
"vegetation": 2
},
{
"label": "coastal_b",
"rung": "district",
"wx_m": 2050000,
"wy_m": 1500000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 2,
"precipitation": 1,
"slope_q": 3,
"elev_q": 30,
"ocean_fraction_q": 0,
"temperature_dc": -68,
"moisture_q": 55,
"vegetation": 2
},
{
"label": "coastal_b",
"rung": "quarter",
"wx_m": 2050000,
"wy_m": 1500000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 4,
"elev_q": 26,
"ocean_fraction_q": 0,
"temperature_dc": -47,
"moisture_q": 56,
"vegetation": 2
},
{
"label": "coastal_b",
"rung": "region",
"wx_m": 2050000,
"wy_m": 1500000,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 0,
"elev_q": 26,
"ocean_fraction_q": 0,
"temperature_dc": -47,
"moisture_q": 57,
"vegetation": 2
},
{
"label": "coastal_c",
"rung": "district",
"wx_m": 2100000,
"wy_m": 1560000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 3,
"elev_q": 24,
"ocean_fraction_q": 0,
"temperature_dc": -30,
"moisture_q": 59,
"vegetation": 2
},
{
"label": "coastal_c",
"rung": "quarter",
"wx_m": 2100000,
"wy_m": 1560000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 2,
"precipitation": 2,
"slope_q": 4,
"elev_q": 28,
"ocean_fraction_q": 0,
"temperature_dc": -51,
"moisture_q": 58,
"vegetation": 2
},
{
"label": "coastal_c",
"rung": "region",
"wx_m": 2100000,
"wy_m": 1560000,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 0,
"elev_q": 27,
"ocean_fraction_q": 0,
"temperature_dc": -45,
"moisture_q": 58,
"vegetation": 2
},
{
"label": "inland",
"rung": "district",
"wx_m": 500000,
"wy_m": 3000000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 4,
"elev_q": 25,
"ocean_fraction_q": 0,
"temperature_dc": 0,
"moisture_q": 52,
"vegetation": 3
},
{
"label": "inland",
"rung": "quarter",
"wx_m": 500000,
"wy_m": 3000000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 5,
"elev_q": 20,
"ocean_fraction_q": 0,
"temperature_dc": 26,
"moisture_q": 52,
"vegetation": 3
},
{
"label": "inland",
"rung": "region",
"wx_m": 500000,
"wy_m": 3000000,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 1,
"precipitation": 2,
"slope_q": 0,
"elev_q": 22,
"ocean_fraction_q": 0,
"temperature_dc": 16,
"moisture_q": 52,
"vegetation": 3
},
{
"label": "high_lat",
"rung": "district",
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 2,
"precipitation": 1,
"slope_q": 3,
"elev_q": 63,
"ocean_fraction_q": 0,
"temperature_dc": -69,
"moisture_q": 43,
"vegetation": 1
},
{
"label": "high_lat",
"rung": "quarter",
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 2,
"precipitation": 1,
"slope_q": 4,
"elev_q": 61,
"ocean_fraction_q": 0,
"temperature_dc": -59,
"moisture_q": 43,
"vegetation": 1
},
{
"label": "high_lat",
"rung": "region",
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 2,
"precipitation": 1,
"slope_q": 0,
"elev_q": 62,
"ocean_fraction_q": 0,
"temperature_dc": -64,
"moisture_q": 43,
"vegetation": 1
},
{
"label": "airless_dry/coastal_a",
"rung": "district",
"wx_m": 2000000,
"wy_m": 1500000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 2,
"elev_q": 44,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/coastal_a",
"rung": "quarter",
"wx_m": 2000000,
"wy_m": 1500000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 2,
"elev_q": 48,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/coastal_a",
"rung": "region",
"wx_m": 2000000,
"wy_m": 1500000,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 0,
"elev_q": 41,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/coastal_b",
"rung": "district",
"wx_m": 2050000,
"wy_m": 1500000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 1,
"elev_q": 42,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/coastal_b",
"rung": "quarter",
"wx_m": 2050000,
"wy_m": 1500000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 2,
"elev_q": 39,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/coastal_b",
"rung": "region",
"wx_m": 2050000,
"wy_m": 1500000,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 0,
"elev_q": 41,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/coastal_c",
"rung": "district",
"wx_m": 2100000,
"wy_m": 1560000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 2,
"elev_q": 44,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/coastal_c",
"rung": "quarter",
"wx_m": 2100000,
"wy_m": 1560000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 2,
"elev_q": 45,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/coastal_c",
"rung": "region",
"wx_m": 2100000,
"wy_m": 1560000,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 0,
"elev_q": 41,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/inland",
"rung": "district",
"wx_m": 500000,
"wy_m": 3000000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 3,
"elev_q": 44,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/inland",
"rung": "quarter",
"wx_m": 500000,
"wy_m": 3000000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 4,
"elev_q": 39,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/inland",
"rung": "region",
"wx_m": 500000,
"wy_m": 3000000,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 0,
"elev_q": 42,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/high_lat",
"rung": "district",
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 4096,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 2,
"elev_q": 76,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/high_lat",
"rung": "quarter",
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 1024,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 3,
"elev_q": 79,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "airless_dry/high_lat",
"rung": "region",
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 0,
"morphology": 8,
"tectonic": 0,
"glaciation": 0,
"precipitation": 0,
"slope_q": 0,
"elev_q": 77,
"ocean_fraction_q": 0,
"temperature_dc": -2147483648,
"moisture_q": 0,
"vegetation": 0
},
{
"label": "volcanic_coast/coastal_a",
"rung": "district",
"wx_m": 2000000,
"wy_m": 1500000,
"min_wl_m": 4096,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 3,
"elev_q": 25,
"ocean_fraction_q": 0,
"temperature_dc": 497,
"moisture_q": 60,
"vegetation": 3
},
{
"label": "volcanic_coast/coastal_a",
"rung": "quarter",
"wx_m": 2000000,
"wy_m": 1500000,
"min_wl_m": 1024,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 6,
"elev_q": 13,
"ocean_fraction_q": 0,
"temperature_dc": 560,
"moisture_q": 62,
"vegetation": 3
},
{
"label": "volcanic_coast/coastal_a",
"rung": "region",
"wx_m": 2000000,
"wy_m": 1500000,
"min_wl_m": 0,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 0,
"elev_q": 28,
"ocean_fraction_q": 0,
"temperature_dc": 482,
"moisture_q": 59,
"vegetation": 3
},
{
"label": "volcanic_coast/coastal_b",
"rung": "district",
"wx_m": 2050000,
"wy_m": 1500000,
"min_wl_m": 4096,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 4,
"elev_q": 34,
"ocean_fraction_q": 0,
"temperature_dc": 456,
"moisture_q": 58,
"vegetation": 3
},
{
"label": "volcanic_coast/coastal_b",
"rung": "quarter",
"wx_m": 2050000,
"wy_m": 1500000,
"min_wl_m": 1024,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 6,
"elev_q": 24,
"ocean_fraction_q": 0,
"temperature_dc": 508,
"moisture_q": 60,
"vegetation": 3
},
{
"label": "volcanic_coast/coastal_b",
"rung": "region",
"wx_m": 2050000,
"wy_m": 1500000,
"min_wl_m": 0,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 0,
"elev_q": 28,
"ocean_fraction_q": 0,
"temperature_dc": 487,
"moisture_q": 59,
"vegetation": 3
},
{
"label": "volcanic_coast/coastal_c",
"rung": "district",
"wx_m": 2100000,
"wy_m": 1560000,
"min_wl_m": 4096,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 2,
"elev_q": 28,
"ocean_fraction_q": 0,
"temperature_dc": 486,
"moisture_q": 59,
"vegetation": 3
},
{
"label": "volcanic_coast/coastal_c",
"rung": "quarter",
"wx_m": 2100000,
"wy_m": 1560000,
"min_wl_m": 1024,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 4,
"elev_q": 33,
"ocean_fraction_q": 0,
"temperature_dc": 460,
"moisture_q": 59,
"vegetation": 3
},
{
"label": "volcanic_coast/coastal_c",
"rung": "region",
"wx_m": 2100000,
"wy_m": 1560000,
"min_wl_m": 0,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 3,
"slope_q": 0,
"elev_q": 29,
"ocean_fraction_q": 0,
"temperature_dc": 481,
"moisture_q": 59,
"vegetation": 3
},
{
"label": "volcanic_coast/inland",
"rung": "district",
"wx_m": 500000,
"wy_m": 3000000,
"min_wl_m": 4096,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 2,
"slope_q": 3,
"elev_q": 24,
"ocean_fraction_q": 0,
"temperature_dc": 565,
"moisture_q": 49,
"vegetation": 3
},
{
"label": "volcanic_coast/inland",
"rung": "quarter",
"wx_m": 500000,
"wy_m": 3000000,
"min_wl_m": 1024,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 2,
"slope_q": 4,
"elev_q": 21,
"ocean_fraction_q": 0,
"temperature_dc": 581,
"moisture_q": 49,
"vegetation": 3
},
{
"label": "volcanic_coast/inland",
"rung": "region",
"wx_m": 500000,
"wy_m": 3000000,
"min_wl_m": 0,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 2,
"slope_q": 0,
"elev_q": 23,
"ocean_fraction_q": 0,
"temperature_dc": 571,
"moisture_q": 49,
"vegetation": 3
},
{
"label": "volcanic_coast/high_lat",
"rung": "district",
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 4096,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 2,
"slope_q": 3,
"elev_q": 68,
"ocean_fraction_q": 0,
"temperature_dc": 545,
"moisture_q": 29,
"vegetation": 2
},
{
"label": "volcanic_coast/high_lat",
"rung": "quarter",
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 1024,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 2,
"slope_q": 4,
"elev_q": 65,
"ocean_fraction_q": 0,
"temperature_dc": 561,
"moisture_q": 30,
"vegetation": 2
},
{
"label": "volcanic_coast/high_lat",
"rung": "region",
"wx_m": 1200000,
"wy_m": 8500000,
"min_wl_m": 0,
"morphology": 15,
"tectonic": 2,
"glaciation": 0,
"precipitation": 2,
"slope_q": 0,
"elev_q": 66,
"ocean_fraction_q": 0,
"temperature_dc": 555,
"moisture_q": 30,
"vegetation": 2
}
]
+419
View File
@@ -0,0 +1,419 @@
//! Window-derivation golden regression (T-1162).
//!
//! Pins `derive_at_metres` (District/Quarter rungs) and
//! `derive_orbital_at_metres` (Region rung) output at a fixed
//! (seed, body, coords) sweep, across all three rung cutoffs — the window
//! path `layer_proxy::derive_window_cell` actually calls in production.
//! Mirrors `tests/derivation_harness.rs`'s golden pattern exactly (same
//! regen convention, same double-derive determinism check, same JSON-value
//! comparison so formatting drift doesn't false-positive).
//!
//! **T-1162 regen rationale:** this golden is generated AFTER the T-1162
//! octave-extension changes (extended coast-warp band, sub-district relief at
//! Quarter, vegetation patchiness) — it deliberately pins the NEW output.
//! There is no "pre-T-1162" golden to preserve: this file did not exist
//! before this ticket, so there is nothing to regress against except itself
//! from this point forward. Any future change to the coast warp, the voxel
//! relief band, the vegetation-patchiness field, or the MIN_WL_BANDS_M
//! quantization will change this golden's values — regenerate deliberately
//! (per the asset-pipeline discipline: source changes, not hand-edits).
//!
//! **Body coverage (Tyre, PR #194 review I4):** the sweep runs against THREE
//! bodies, not one — the original temperate/ocean/breathable body (unchanged
//! from the initial T-1162 landing), plus an airless/dry body (exercises
//! `vegetation_invention::VegetationEnvelope`'s `ceiling_q == 0` short
//! circuit at the full derivation-stack level) and a volcanic/high-tectonic
//! coastal body (exercises the ridged-warp/wide-`scatter_floor` branch of
//! `coast_invention`). The two new bodies' rows are APPENDED after the
//! original body's rows (never interleaved), so the original rows stay
//! byte-identical across the I4 regen — see `body_sweep_samples`'s doc.
//!
//! Run: `cargo test --test window_derivation_golden`
//! Regenerate: `UPDATE_GOLDEN=1 cargo test --test window_derivation_golden`
use std::path::PathBuf;
use settled_reach_server::atlas::district_profile::{
derive_at_metres, derive_orbital_at_metres, BodyParams, ClimateConstants,
};
use settled_reach_server::atlas::drainage;
use settled_reach_server::atlas::features::TerrainAnalysis;
use settled_reach_server::atlas::heightmap::BodyHeightmap;
use settled_reach_server::atlas::scale;
use settled_reach_server::seed::{SeedChain, SeedDomain};
const GOLDEN_FILE: &str = "tests/golden/window_derivation_golden.json";
/// Compact representation of a `DistrictProfile` sample for golden pinning.
/// Integer-discriminant fields only (D-010) — no float equality flakiness.
///
/// **No `body` field (Tyre, PR #194 I4 constraint):** the two new body rows
/// (I4) distinguish themselves via the `label` field's prefix instead of a
/// new struct field — adding a field here would change the JSON shape of
/// EVERY existing row (not just the new ones), which fails I4's explicit
/// "existing rows must stay byte-identical" requirement. `label` was always
/// a free-form string, so `"golden_body/coastal_a"` vs `"airless_dry/coastal_a"`
/// costs nothing structurally and keeps the diff a pure append.
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)]
struct GoldenSample {
label: String,
rung: String,
wx_m: i64,
wy_m: i64,
min_wl_m: i64,
morphology: u8,
tectonic: u8,
glaciation: u8,
precipitation: u8,
slope_q: i32,
elev_q: i32,
ocean_fraction_q: i32,
temperature_dc: i32, // deci-°C, i32::MIN sentinel for None (airless)
moisture_q: i32,
vegetation: u8,
}
fn sample_hm() -> BodyHeightmap {
// Deterministic gradient with enough variance for coast/relief/vegetation
// content to actually differ across the sweep positions — same shape
// convention as district_profile.rs's own test_hm / zoom_ladder_bench's
// bench_hm, sized a bit larger so the sweep coordinates land on distinct
// heightmap cells rather than a single interpolated patch.
let (w, h) = (128u32, 64u32);
let n = (w * h) as usize;
let data = (0..n)
.map(|i| {
let r = (i / w as usize) as f32 / h as f32;
let c = (i % w as usize) as f32 / w as f32;
// A gentle sine ripple on top of the linear gradient gives the
// coastline invention real slope/ocean-mask variance to warp.
let ripple = (c * std::f32::consts::TAU * 3.0).sin() * 0.08;
(r * 0.55 + c * 0.35 + ripple + 0.05).clamp(0.0, 1.0)
})
.collect();
BodyHeightmap {
body_id: "golden_body".into(),
width: w,
height: h,
data,
sea_level: 0.32,
}
}
fn sample_ta(hm: &BodyHeightmap) -> TerrainAnalysis {
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
TerrainAnalysis::analyze(hm, &dr)
}
fn sample_params() -> BodyParams {
BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("temperate".into()),
body_radius_km: Some(6371.0),
..Default::default()
}
}
/// I4 (Tyre, PR #194 review): airless/dry body params — exercises
/// `vegetation_invention::VegetationEnvelope`'s `ceiling_q == 0` short
/// circuit (no water, no atmosphere → zero patchiness swing, per
/// `envelope_airless_or_dry_body_has_zero_ceiling`'s unit-level proof) at
/// the FULL derivation-stack level, which the unit test alone doesn't pin.
/// Also airless (`atmosphere: "none"`) so `temperature_c`/`vegetation_class`
/// take the `None`/`Absent` branches — a body-envelope regression that
/// invented forest on a bone-dry world would show up here as a NEW
/// non-Barren/non-Absent vegetation discriminant in the golden diff.
fn airless_dry_params() -> BodyParams {
BodyParams {
hydrosphere: Some("none".into()),
atmosphere: Some("none".into()),
planet_class: Some("arid".into()),
body_radius_km: Some(3_390.0), // Mars-scale, deliberately distinct from the wet body
..Default::default()
}
}
/// I4 (Tyre, PR #194 review): volcanic/high-tectonic coastal body params —
/// exercises `coast_invention`'s ridged-warp + wide-`scatter_floor` branch
/// (`TectonicClass::Volcanic` → `tectonic_energy` near its ceiling in
/// `body_coast_envelope`, driving up `roughness`/`warp_amplitude_px`/
/// `scatter_floor` per that function's doc) — the coast-crinkle branch most
/// likely to visibly differ from the temperate body's gentler warp, and thus
/// the branch most likely to silently regress without dedicated coverage.
fn volcanic_coast_params() -> BodyParams {
BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("volcanic".into()),
tectonic_activity: Some("volcanic".into()),
body_radius_km: Some(6_000.0),
..Default::default()
}
}
/// Fixed sweep positions (world metres from origin) — a handful of points
/// spanning a coastal stretch (per the heightmap's ripple) plus a couple of
/// clearly inland/high-latitude points, so the golden exercises coast warp,
/// sub-district relief, and vegetation patchiness all at once.
fn sweep_positions() -> Vec<(&'static str, f64, f64)> {
vec![
("coastal_a", 2_000_000.0, 1_500_000.0),
("coastal_b", 2_050_000.0, 1_500_000.0),
("coastal_c", 2_100_000.0, 1_560_000.0),
("inland", 500_000.0, 3_000_000.0),
("high_lat", 1_200_000.0, 8_500_000.0),
]
}
fn derive_golden_sample(
label: &str,
rung: &str,
seed: SeedChain,
body_id: &str,
params: &BodyParams,
ta: &TerrainAnalysis,
climate: &ClimateConstants,
wx: f64,
wy: f64,
min_wl_m: f64,
orbital: bool,
) -> GoldenSample {
let prof = if orbital {
derive_orbital_at_metres(seed, body_id, params, ta, wx, wy, climate)
} else {
derive_at_metres(seed, body_id, params, ta, wx, wy, climate, min_wl_m)
};
GoldenSample {
label: label.to_string(),
rung: rung.to_string(),
wx_m: wx as i64,
wy_m: wy as i64,
min_wl_m: min_wl_m as i64,
morphology: prof.morphology_zone as u8,
tectonic: prof.tectonic_class as u8,
glaciation: prof.glaciation_grade as u8,
precipitation: prof.precipitation_class as u8,
slope_q: prof.slope_q,
elev_q: prof.elev_q,
ocean_fraction_q: prof.ocean_fraction_q,
temperature_dc: prof
.temperature_c
.map(|t| (t * 10.0).round() as i32)
.unwrap_or(i32::MIN),
moisture_q: prof.moisture_q,
vegetation: prof.vegetation_class as u8,
}
}
/// District's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s
/// District entry — District's own Nyquist floor, `2 * DISTRICT_M` per the
/// PR #194 I1 dependency-direction fix: the RUNG authors the floor, and
/// `detail_scatter::OCTAVE_WAVELENGTHS_M`'s finest octave coinciding with it
/// is pinned by the `const` drift-guard next to `MIN_WL_BANDS_M`, not the
/// source of the value). A District-rung request in production quantizes to
/// exactly this value, so the golden pins the SAME cutoff a real window
/// request would actually carry; `golden_cutoffs_match_the_scale_ladder`
/// asserts the `2 × spacing` coupling for both this and `QUARTER_MIN_WL_M`.
const DISTRICT_MIN_WL_M: f64 = 4_096.0;
/// Quarter's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s
/// new T-1162 entry — Quarter's own Nyquist floor, `2 * QUARTER_M`).
const QUARTER_MIN_WL_M: f64 = 1_024.0;
/// Run the fixed sweep (every position × the three rung cutoffs — District /
/// Quarter use their REAL production `MIN_WL_BANDS_M` values / Region via
/// `derive_orbital_at_metres`, which takes no cutoff parameter — see its own
/// doc on why) for ONE body. Extracted (Tyre, PR #194 I4) so multiple bodies
/// can share the same sweep logic; `label_prefix` (empty for the original
/// body, non-empty for the I4 additions) is prepended to each row's `label`
/// so multi-body output stays distinguishable without a new struct field
/// (see [`GoldenSample`]'s doc on why no `body` field was added).
#[allow(clippy::too_many_arguments)]
fn body_sweep_samples(
label_prefix: &str,
seed: SeedChain,
body_id: &str,
params: &BodyParams,
ta: &TerrainAnalysis,
climate: &ClimateConstants,
) -> Vec<GoldenSample> {
let mut out = Vec::new();
for (label, wx, wy) in sweep_positions() {
let label = format!("{label_prefix}{label}");
out.push(derive_golden_sample(
&label,
"district",
seed,
body_id,
params,
ta,
climate,
wx,
wy,
DISTRICT_MIN_WL_M,
false,
));
out.push(derive_golden_sample(
&label,
"quarter",
seed,
body_id,
params,
ta,
climate,
wx,
wy,
QUARTER_MIN_WL_M,
false,
));
out.push(derive_golden_sample(
&label, "region", seed, body_id, params, ta, climate, wx, wy, 0.0, true,
));
}
out
}
/// Build the full golden sample set: the ORIGINAL temperate/ocean/breathable
/// body's sweep first (byte-identical inputs to the pre-I4 `golden_samples`
/// — same seed, same `body_id`, same unprefixed labels, so its rows are
/// byte-identical in the regenerated fixture), THEN the two I4 body rows
/// appended after (never interleaved) so the diff against the pre-I4 golden
/// is a pure append, not a reshuffle.
fn golden_samples() -> Vec<GoldenSample> {
let hm = sample_hm();
let ta = sample_ta(&hm);
let climate = ClimateConstants::default();
let mut out = Vec::new();
// Original body — UNCHANGED inputs from pre-I4 (T-1162 initial landing).
out.extend(body_sweep_samples(
"",
SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7),
"golden_body",
&sample_params(),
&ta,
&climate,
));
// I4 addition 1: airless/dry — ceiling_q == 0 vegetation short-circuit.
out.extend(body_sweep_samples(
"airless_dry/",
SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 8),
"golden_body_airless_dry",
&airless_dry_params(),
&ta,
&climate,
));
// I4 addition 2: volcanic/high-tectonic coast — ridged warp, wide scatter_floor.
out.extend(body_sweep_samples(
"volcanic_coast/",
SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 9),
"golden_body_volcanic_coast",
&volcanic_coast_params(),
&ta,
&climate,
));
out
}
#[test]
fn window_derivation_golden_regression() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let golden_path = manifest.join(GOLDEN_FILE);
// Double-derive determinism check (D-010) before ever touching the golden.
let run1 = golden_samples();
let run2 = golden_samples();
assert_eq!(
run1, run2,
"double-derivation mismatch — determinism is broken (D-010)"
);
let actual_json = serde_json::to_string_pretty(&run1).expect("serialize") + "\n";
if std::env::var("UPDATE_GOLDEN").is_ok() {
std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden");
std::fs::write(&golden_path, &actual_json).expect("write golden");
eprintln!(
"Golden written: {} ({} bytes)",
golden_path.display(),
actual_json.len()
);
return;
}
let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| {
panic!(
"Golden file not found: {}.\n\
First run: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n{e}",
golden_path.display()
)
});
let actual_v: serde_json::Value = serde_json::from_str(&actual_json).expect("reparse actual");
let golden_v: serde_json::Value = serde_json::from_str(&golden_json).expect("parse golden");
if actual_v != golden_v {
panic!(
"Window-derivation golden mismatch — derivation chain changed.\n\
Update: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n\
Golden: {}\nActual: {}",
golden_json.trim(),
actual_json.trim()
);
}
}
/// Cross-rung coherence sanity check on the golden's own fixed sweep: for
/// each coastal label, the Quarter-rung sample must differ from the
/// District-rung sample at the SAME position (the whole point of the
/// extension — Quarter sees finer content District's coarser cutoff
/// truncates). This is a structural companion to the golden file itself,
/// not a replacement for it — it fails loudly if the golden ever gets
/// regenerated with `min_wl_m` accidentally identical across rungs.
#[test]
fn quarter_and_district_rungs_diverge_at_the_same_position() {
let samples = golden_samples();
let mut any_diverged = false;
for (label, _, _) in sweep_positions() {
let district = samples
.iter()
.find(|s| s.label == label && s.rung == "district")
.unwrap();
let quarter = samples
.iter()
.find(|s| s.label == label && s.rung == "quarter")
.unwrap();
if district.elev_q != quarter.elev_q
|| district.slope_q != quarter.slope_q
|| district.moisture_q != quarter.moisture_q
{
any_diverged = true;
}
}
assert!(
any_diverged,
"no sweep position showed ANY difference between District and Quarter \
rungs — the T-1162 octave extension would be structurally inert"
);
}
/// `scale::DISTRICT_M` / `scale::QUARTER_M` sanity — documents WHY
/// `DISTRICT_MIN_WL_M`/`QUARTER_MIN_WL_M` are the cutoffs used above (each
/// rung's own Nyquist floor, `2 × <rung's spacing>`), so a future
/// scale-ladder change surfaces here. Pins BOTH rungs' coupling (Tyre, PR
/// #194 I1 — District's coupling was previously unpinned; only Quarter's
/// `2 × QUARTER_M` was checked) — this mirrors
/// `layer_proxy::MIN_WL_BANDS_M`'s own direct `2 × DISTRICT_M` / `2 ×
/// QUARTER_M` derivation, not `detail_scatter::OCTAVE_WAVELENGTHS_M`.
#[test]
fn golden_cutoffs_match_the_scale_ladder() {
assert_eq!(scale::DISTRICT_M, 2_048);
assert_eq!(scale::QUARTER_M, 512);
assert_eq!(2 * scale::DISTRICT_M, DISTRICT_MIN_WL_M as i32);
assert_eq!(2 * scale::QUARTER_M, QUARTER_MIN_WL_M as i32);
}