From the Hoshe/Tyre review: - drainage: assert flow_accumulation/max_accumulation determinism + clamp ≥ 1 (the D-209 strength denominator); isolated-basin merge path (no panic). - subbiome: each derivable variant reachable + Volcanic never emitted. - planet_simulation: new test_sim_determinism.py — same body simulates to a bit-identical elevation array at 1024×512 (the 271-body bake can't be cheaply re-run, so a silent drift = full re-bake). Verified PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -683,4 +683,37 @@ mod tests {
|
||||
"Basin count must be deterministic"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flow_accumulation_deterministic_and_clamped() {
|
||||
// flow_accumulation/max_accumulation are the D-209 strength denominator —
|
||||
// a silent drift corrupts every attractor strength. Lock them down.
|
||||
let elev = slope_grid(64, 32);
|
||||
let r1 = analyze(&elev, 64, 32, 0.3);
|
||||
let r2 = analyze(&elev, 64, 32, 0.3);
|
||||
assert_eq!(r1.flow_accumulation, r2.flow_accumulation);
|
||||
assert_eq!(r1.max_accumulation, r2.max_accumulation);
|
||||
assert!(r1.max_accumulation >= 1, "max_accumulation must be clamped ≥ 1");
|
||||
// Flat / all-ocean world: still well-defined (no division by zero).
|
||||
let flat = flat_grid(16, 8, 0.5);
|
||||
let rf = analyze(&flat, 16, 8, 0.9); // sea_level above all terrain
|
||||
assert!(rf.max_accumulation >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_basins_no_panic() {
|
||||
// Two land patches split by an ocean band (rows 3-4 below sea level):
|
||||
// exercises basin labeling/merge on a disconnected world.
|
||||
let (w, h) = (32usize, 8usize);
|
||||
let mut elev = vec![0.1f32; w * h]; // ocean everywhere
|
||||
for r in [0, 1, 2, 5, 6, 7] {
|
||||
for c in 0..w {
|
||||
// two raised land bands, sloped so they drain internally
|
||||
elev[r * w + c] = 0.5 + (c as f32 / w as f32) * 0.3;
|
||||
}
|
||||
}
|
||||
let res = analyze(&elev, w as u32, h as u32, 0.3);
|
||||
let n = res.drainage_basins.len();
|
||||
assert!(n >= 1 && n <= 12, "basin count {n} out of range");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,4 +156,29 @@ mod tests {
|
||||
assert_eq!(c1.to_bits(), c2.to_bits());
|
||||
assert!(c1 >= 1.0, "cost is at least the grassland baseline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_variant_reachable_and_volcanic_never_emitted() {
|
||||
use SubBiomeVariant::*;
|
||||
// (elev_pct, slope, water_dist, temp) → expected variant, per the
|
||||
// classify_variant branch order. Covers all 10 derivable variants.
|
||||
let cases: &[(f32, f32, u16, f32, SubBiomeVariant)] = &[
|
||||
(0.95, 0.0, 100, 0.5, Alpine), // high elevation dominates
|
||||
(0.10, 0.0, 1, 0.5, Wetland), // saturated low ground
|
||||
(0.35, 0.0, 4, 0.5, CoastalLowland), // near coast, low
|
||||
(0.50, 0.0, 100, 0.10, Tundra), // cold pole
|
||||
(0.50, 0.0, 100, 0.30, BorealForest), // cool
|
||||
(0.50, 0.0, 10, 0.90, TropicalWet), // hot + moist
|
||||
(0.50, 0.0, 30, 0.90, Savanna), // hot + mid-dry
|
||||
(0.50, 0.0, 50, 0.90, Desert), // hot + dry
|
||||
(0.50, 0.0, 10, 0.50, TemperateForest), // temperate + moist
|
||||
(0.50, 0.0, 40, 0.50, TemperateGrassland),// temperate + mid
|
||||
(0.50, 0.0, 70, 0.50, Desert), // temperate + arid
|
||||
];
|
||||
for &(e, s, w, t, expected) in cases {
|
||||
let got = classify_variant(e, s, w, t);
|
||||
assert_eq!(got, expected, "classify_variant({e},{s},{w},{t})");
|
||||
assert_ne!(got, Volcanic, "Volcanic must never be emitted (no L1 signal)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Determinism guard for planet_simulation.simulate() (#963).
|
||||
|
||||
The 1024×512 elevation bump (D-202 amended) claims to be deterministic — the
|
||||
271-body heightmap bake cannot be re-run cheaply, so a silent non-determinism
|
||||
means a full regeneration. This asserts that simulating the same body twice
|
||||
yields a bit-identical elevation array.
|
||||
|
||||
Run: uv run python tooling/planet-gen/test_sim_determinism.py
|
||||
Exit: 0 = deterministic, 1 = drift detected, 2 = could not find a test body.
|
||||
"""
|
||||
import hashlib
|
||||
import sqlite3
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent.parent
|
||||
sys.path.insert(0, str(REPO / "tooling" / "planet-gen"))
|
||||
|
||||
import numpy as np # noqa: E402
|
||||
|
||||
from body_definition_parser import parse_system # noqa: E402
|
||||
from planet_simulation import GRID_H, GRID_W, simulate # noqa: E402
|
||||
|
||||
|
||||
def _elev_hash(terrain) -> str:
|
||||
arr = np.ascontiguousarray(terrain["elevation"], dtype=np.float32)
|
||||
return hashlib.sha256(arr.tobytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
conn = sqlite3.connect(str(REPO / "server" / "data" / "systems.db"))
|
||||
rows = conn.execute(
|
||||
"SELECT body_id, terrain_reference FROM bodies "
|
||||
"WHERE inhabited=1 AND terrain_reference IS NOT NULL AND population>0 "
|
||||
"AND system_id != 'GJ 0' ORDER BY body_id LIMIT 30"
|
||||
).fetchall()
|
||||
|
||||
checked = 0
|
||||
for body_id, tref in rows:
|
||||
if checked >= 3:
|
||||
break
|
||||
sys_index = tref.split("/bodies/")[0] + "/index.md"
|
||||
try:
|
||||
defs = parse_system(sys_index)
|
||||
except Exception:
|
||||
continue
|
||||
bd = next((d for d in defs if d.get("id") == body_id), None)
|
||||
if bd is None:
|
||||
continue
|
||||
t1 = simulate(bd)
|
||||
t2 = simulate(bd)
|
||||
if not t1 or "elevation" not in t1:
|
||||
continue
|
||||
h1, h2 = _elev_hash(t1), _elev_hash(t2)
|
||||
ok = h1 == h2 and t1["elevation"].shape == (GRID_H, GRID_W)
|
||||
print(f" {body_id}: {'OK' if ok else 'DRIFT'} {h1[:16]} shape={t1['elevation'].shape}")
|
||||
if not ok:
|
||||
print(f"FAIL: {body_id} elevation is non-deterministic ({h1[:16]} != {h2[:16]})")
|
||||
return 1
|
||||
checked += 1
|
||||
|
||||
if checked == 0:
|
||||
print("SKIP: no testable body found")
|
||||
return 2
|
||||
print(f"PASS: {checked} bodies simulate deterministically at {GRID_W}×{GRID_H}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user