fix(simulation): address PR #172 review — C1 over-vegetation + H1 biosphere gate (T-1084/T-1085)
Hoshe + Tyre review of the micro-habitat-mosaic branch requested changes on two data-confirmed correctness bugs plus follow-through. All addressed. Blocking: - C1 (voxel.rs): the mosaic re-granted vegetation the climate withheld — a frozen ice world (Edict) read 61% vegetated tundra. The Grassland apply-gate now fires only where the climate already grants cover (!Barren); a climatically-barren district (frozen < -50C = surface ice/geology D-239 §2, or hyper-arid moisture<5, both resolved to VegetationClass::Barren upstream) is skipped. Genuine tundra (cold + moisture>=15) is Scrub upstream and still reaches the tundra palette via the same !Barren path. believability.json regenerated: Edict vegetated_districts 39->0, Arbour 20->8 (its real cold-pole/arid districts revert to barren); intra-class variety preserved (Arbour vegetation_classes 3, micro_habitat_distinct 2). - H1 (bodies.py): biosphere Gate 0 keyed on `inhabited`, conflating settlement with biosphere and force-Airless'ing uninhabited-but-alive worlds (D-247: worlds were alive before humans). Gate 0 is now settlement-independent — keyed on atmosphere/hydrosphere/planet_class. 24 uninhabited-alive worlds now classified (was 0); GJ0g-1 (dense atm + rivers) -> NativeMirror. Split 122 Compatible : 121 Mirror; all 9 canon exemplars hold. Follow-through: - C2 (bodies.py + D-247): Gate 0 now reads planet_class (barren -> Airless) as D-247's "+ planet_class" phrasing names. D-247 amended to record the actual two-gate signal set (atm/hydro/planet_class habitability; economic_base_primary chirality; settlement-independence). - C3 (believability.rs): micro_habitat_distinct criterion relaxed to `== 0 || >= 2` — the min-over-patches estimator makes K=3 unreachable against a dominant-entry palette; K=2 provisional, Q-123 calibrates. - C4 + H2 (D-246): amended to record the v1 scope cut — only the Soil-derived vegetated/wet classes take the mosaic; material-driven families are `_ => false` so Hardpan/Scree are authored but unreachable in v1; no elevation change in v1; the mosaic OWNS the §8 climate->vegetation law for the classes it touches. - H3 (voxel.rs): three dedicated mosaic-pass regression tests — C1 barren-respect, §8 material-untouched + forest-textured, T-1040 no-water-without-channel. - H4 (voxel.rs): relief_signal comment corrected to ~[-1,+1]. - H5 (bodies.py): frontmatter override now strips quotes + validates against the 4-value enum (warn-and-skip on typo). systems.db regenerated + stamped (H1). believability golden regenerated (C1). Full cargo test green; clippy -D warnings + ruff clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -113,10 +113,16 @@ pub struct ContrastMetrics {
|
||||
/// traverses — not the single 64 m sample chunk, which is too narrow to register it.
|
||||
pub voxel_relief_m: i32,
|
||||
/// Minimum distinct micro-habitat axis-triples (terrain × vegetation × water) found
|
||||
/// in any single sampled VEGETATED-land district (T-1084, D-246). The D-245 intra-class
|
||||
/// variety screen: a vegetated patch must show ≥K distinct micro-features, never 100%
|
||||
/// one — near 1 on the pre-mosaic uniform world ("Wetland 100%"), ≥3 once the mosaic
|
||||
/// textures the class. Barren / water patches are excluded (legitimately uniform).
|
||||
/// in any single sampled VEGETATED-land district (T-1084, D-246); `0` = no vegetated
|
||||
/// patch was sampled (barren / ocean world). The D-245 intra-class variety screen: a
|
||||
/// vegetated patch must show ≥K distinct micro-features, never 100% one — near 1 on the
|
||||
/// pre-mosaic uniform world ("Wetland 100%"). Barren / water patches are excluded
|
||||
/// (legitimately uniform). NOTE (PR #172, C3): this is a *minimum over patches* against
|
||||
/// palettes with a dominant entry (e.g. dense stand 40/90), so a single 64 m chunk
|
||||
/// often sits in the dominant bucket → the provisional floor is K=2 ("no vegetated
|
||||
/// patch is 100% one micro-habitat"), the achievable, meaningful bar. The estimator
|
||||
/// (min vs mean/median), the final K, and same-class-only pooling are Q-123's to
|
||||
/// calibrate against the enforcer's baselines.
|
||||
pub micro_habitat_distinct: usize,
|
||||
}
|
||||
|
||||
@@ -375,9 +381,13 @@ pub fn evaluate_criteria(r: &BelievabilityReport) -> Vec<Criterion> {
|
||||
},
|
||||
Criterion {
|
||||
name: "intra-class variety",
|
||||
pass: c.micro_habitat_distinct >= 3,
|
||||
// 0 = no vegetated patch sampled (barren/ocean world — legitimately nothing to
|
||||
// vary). Otherwise the provisional floor is K=2 = "no vegetated patch is 100%
|
||||
// one micro-habitat" — the achievable bar for a min-over-patches estimator;
|
||||
// ≥3 + the estimator are Q-123's to calibrate (PR #172, C3).
|
||||
pass: c.micro_habitat_distinct == 0 || c.micro_habitat_distinct >= 2,
|
||||
detail: format!(
|
||||
"min {} distinct micro-habitats per vegetated patch",
|
||||
"min {} distinct micro-habitats per vegetated patch (K≥2 provisional; Q-123 calibrates)",
|
||||
c.micro_habitat_distinct
|
||||
),
|
||||
},
|
||||
|
||||
+111
-17
@@ -509,7 +509,7 @@ pub fn derive_voxel_column(
|
||||
);
|
||||
let relief_m = (relief * VOXEL_RELIEF_SPAN_M as f64) as i32; // truncate (D-010)
|
||||
column.elevation_m = (column.elevation_m + relief_m).max(0);
|
||||
relief_signal = relief; // ~[-0.5, +0.5]; negative = mid-scale valley (wetter)
|
||||
relief_signal = relief; // ~[-1, +1] (envelope clamps to 1); negative = valley (wetter)
|
||||
}
|
||||
|
||||
// ── 3c. Intra-class micro-habitat mosaic (T-1084, D-246) ──────────────
|
||||
@@ -520,12 +520,15 @@ pub fn derive_voxel_column(
|
||||
// (mid-scale relief thalweg + district moisture) so wet habitats settle in the
|
||||
// lows. This is the motivating fix for the probe's uniform "Wetland 100%".
|
||||
//
|
||||
// v1 scope: terrain MATERIAL is left exactly as the family generator set it, so
|
||||
// the D-239 §8 lithology law holds untouched. The material-variant micro-habitats
|
||||
// (hardpan / scree / dry-wash gravel) and the extreme classes (Rock / Sand /
|
||||
// Gravel / Lava) need shape-aware §8 reconciliation and are a follow-up. A
|
||||
// climatically-barren district stays barren (D-245 §5 climate-appropriateness).
|
||||
// Within these classes this supersedes the per-voxel `scatter_vegetation` scatter.
|
||||
// v1 scope: terrain MATERIAL is left exactly as the family generator set it, so the
|
||||
// D-239 §8 *lithology* law holds untouched; the material-variant micro-habitats
|
||||
// (hardpan / scree / dry-wash gravel) and the extreme classes (Rock / Sand / Gravel /
|
||||
// Lava) need shape-aware §8 reconciliation and are a follow-up (D-246 amendment). The
|
||||
// mosaic now OWNS the §8 *climate→vegetation* law for these classes: the no-skip half
|
||||
// holds (no palette emits Barren in a Forest zone), and a climatically-barren district
|
||||
// stays barren (D-245 §5 — the mosaic never re-grants cover the climate withheld). The
|
||||
// family's guaranteed riparian band is softened to a probabilistic wet-habitat near
|
||||
// channels — acceptable at v1. Within these classes this supersedes `scatter_vegetation`.
|
||||
if !matches!(family, MorphologyFamily::WaterBody) {
|
||||
use crate::atlas::mosaic::{self, SurfaceClass};
|
||||
let dominant_veg = Vegetation::from_vegetation_class(district_eff.vegetation_class);
|
||||
@@ -536,16 +539,14 @@ pub fn derive_voxel_column(
|
||||
mosaic::ClimateBiome::from_climate(district_eff.temperature_c, district_eff.moisture_q);
|
||||
let apply = match class {
|
||||
SurfaceClass::Wetland | SurfaceClass::Forest => true,
|
||||
// A vegetated grassland always; a barren-dominant one only when the biome is
|
||||
// cold (tundra / boreal), where the sparse lichen/scrub mosaic is climate-
|
||||
// appropriate — a hot barren district (desert on Soil) stays bare.
|
||||
SurfaceClass::Grassland => {
|
||||
!matches!(dominant_veg, Vegetation::Barren)
|
||||
|| matches!(
|
||||
biome,
|
||||
mosaic::ClimateBiome::Tundra | mosaic::ClimateBiome::Boreal
|
||||
)
|
||||
}
|
||||
// Only where the climate already grants cover (C1 fix). A Barren-dominant
|
||||
// district is Barren for a reason `derive_vegetation` already resolved —
|
||||
// frozen (< -50 °C = surface ice/geology, D-239 §2), hyper-arid (moisture < 5),
|
||||
// or cold-dry marginal ground — so the mosaic must NOT re-grant it (else it
|
||||
// paints tundra scrub onto frozen ice worlds like Edict). Genuine tundra
|
||||
// (cold + moisture ≥ 15) is already Scrub upstream, so it reaches the biome's
|
||||
// tundra palette through this same !Barren path — no special cold clause needed.
|
||||
SurfaceClass::Grassland => !matches!(dominant_veg, Vegetation::Barren),
|
||||
// Rock / Sand / Gravel / Lava are material-driven — a §8-aware follow-up.
|
||||
_ => false,
|
||||
};
|
||||
@@ -2535,6 +2536,99 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Intra-class micro-habitat mosaic — dedicated integration surface (T-1084,
|
||||
// D-246, PR #172 H3). Locks the three load-bearing invariants of the §3c pass
|
||||
// that no other test targets directly: the C1 climate-authority gate, the §8
|
||||
// lithology hold, and the T-1040 hydrology guard.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/// C1 regression: the mosaic must NOT re-grant vegetation the climate withheld.
|
||||
/// A climatically-barren district — a frozen ice world (like Edict) or a
|
||||
/// hyper-arid margin, both resolved to `VegetationClass::Barren` upstream by
|
||||
/// `derive_vegetation` — reaches the Grassland surface-class (AlluvialPlain →
|
||||
/// Soil, not temperature-gated), but the apply-gate is `!Barren`, so the mosaic
|
||||
/// is skipped and every voxel stays Barren. Before the fix,
|
||||
/// `ClimateBiome::from_climate` read the sub-zero temp as Tundra and painted
|
||||
/// tundra scrub onto the ice — Edict read 61 % vegetated (D-245 §5 violation).
|
||||
#[test]
|
||||
fn mosaic_pass_does_not_revegetate_barren_climate() {
|
||||
let frozen_barren = DistrictProfile {
|
||||
vegetation_class: VegetationClass::Barren,
|
||||
temperature_c: Some(-60.0), // frozen: surface ice = geology (D-239 §2)
|
||||
moisture_q: 5, // also hyper-arid; keeps terrain Soil (< 60)
|
||||
..alluvial_district()
|
||||
};
|
||||
let chunk = derive_chunk_context(42, "GJ244Ad", &frozen_barren, (10, 20), None);
|
||||
for tx in [100, 137, 200, 263, 331, 404] {
|
||||
let col = derive_voxel_column(42, "GJ244Ad", &frozen_barren, &chunk, tx, 90);
|
||||
assert_eq!(
|
||||
col.vegetation,
|
||||
Vegetation::Barren,
|
||||
"frozen/hyper-arid barren district must stay Barren (C1), got {:?} at x={tx}",
|
||||
col.vegetation,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Positive path + §8 lithology hold: a vegetated Forest district IS textured
|
||||
/// by the mosaic (≥2 distinct vegetation micro-habitats, never Barren per the
|
||||
/// §8 no-skip law), while the terrain MATERIAL is left exactly as the family
|
||||
/// generator set it (Soil). The mosaic owns the climate→vegetation half of §8
|
||||
/// but never the lithology half (D-246 v1 scope).
|
||||
#[test]
|
||||
fn mosaic_pass_textures_forest_without_touching_material() {
|
||||
let district = alluvial_district(); // Forest, temperate, moisture 55 → Soil
|
||||
let chunk = alluvial_chunk(&district);
|
||||
let mut veg_seen = std::collections::BTreeSet::new();
|
||||
for tx in 100..160 {
|
||||
let col = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, 100);
|
||||
assert_eq!(
|
||||
col.terrain,
|
||||
TerrainMaterial::Soil,
|
||||
"mosaic must not change terrain material (§8 lithology), got {:?} at x={tx}",
|
||||
col.terrain,
|
||||
);
|
||||
assert_ne!(
|
||||
col.vegetation,
|
||||
Vegetation::Barren,
|
||||
"Forest zone never renders Barren (§8 no-skip), at x={tx}",
|
||||
);
|
||||
veg_seen.insert(format!("{:?}", col.vegetation));
|
||||
}
|
||||
assert!(
|
||||
veg_seen.len() >= 2,
|
||||
"mosaic must texture a Forest district into ≥2 micro-habitats, saw {veg_seen:?}",
|
||||
);
|
||||
}
|
||||
|
||||
/// T-1040 hydrology guard: the mosaic adds standing water ONLY where the
|
||||
/// chunk's hydrology gate allows it. In a channel-free chunk
|
||||
/// (`ocean_fraction_q < 10` ⇒ `has_active_channel == false`) the wet-habitat
|
||||
/// water is suppressed — every voxel stays Dry — even though the Forest palette
|
||||
/// contains wet habitats.
|
||||
#[test]
|
||||
fn mosaic_pass_adds_no_water_without_active_channel() {
|
||||
let dry_forest = DistrictProfile {
|
||||
ocean_fraction_q: 0, // < 10 ⇒ has_active_channel == false
|
||||
..alluvial_district()
|
||||
};
|
||||
let chunk = derive_chunk_context(42, "GJ1c", &dry_forest, (10, 20), None);
|
||||
assert!(
|
||||
!chunk.has_active_channel,
|
||||
"test precondition: chunk must have no active channel",
|
||||
);
|
||||
for tx in 100..160 {
|
||||
let col = derive_voxel_column(42, "GJ1c", &dry_forest, &chunk, tx, 100);
|
||||
assert_eq!(
|
||||
col.water,
|
||||
Water::Dry,
|
||||
"no standing water may appear in a channel-free chunk (T-1040), got {:?} at x={tx}",
|
||||
col.water,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elevation_is_non_negative() {
|
||||
let district = alluvial_district();
|
||||
|
||||
Reference in New Issue
Block a user