feat(simulation): believability test protocol — D-245 enforcer (T-1083)

Promote the throwaway aliveness probe into a committed, repeatable believability
protocol — the instrument that found T-1080/T-1081/T-1082.

- atlas/believability.rs (new): BelievabilityReport (serde) + analyze() computing
  CONTRAST (per-field min/max/distinct for moisture/elev/slope/ocean over all
  districts; distinct morphology zones / vegetation classes / terrain materials)
  and COHERENCE (water-renders-wet, drainage-monotonic, vegetation-present) — never
  marginal per-tile counts (the lesson: that called a broken uniform world ALIVE).
  evaluate_criteria() = advisory D-245 checks; cascade_for_body()/seed_to_u64()
  loader shared by the bin and the harness. Unit tests prove the metric tells a
  uniform world (fails) from a varied one (passes) + determinism.
- bin/aliveness_probe.rs: refactored to a thin CLI over the module — prints the
  body-level report + advisory D-245 criteria, drops the naive per-tile verdict.
- tests/believability_harness.rs (+ golden): runs the real cascade for Arbour
  (temperate/ocean) + Edict (frozen/ice), asserts determinism, golden-snapshots
  the reports (regression — updates when T-1080/T-1082 land), advisory criteria
  with BELIEVABILITY_STRICT=1 to fail on unmet D-245 criteria. Skips if committed
  data absent. x86_64 golden (cascade has f32 warp paths).

Baseline @ believability-v1: Arbour 3/7, Edict 3/7 criteria pass — moisture
gradient, water-renders-wet, drainage all FAIL (T-1080/T-1082); Edict vegetation
0/64 (blank tundra — the frozen 'reads dead' case D-245 targets).

PNG layer maps (ticket item 3) deferred — explicitly optional; the contrast +
coherence metrics are the core enforcer.

clippy --all-targets -D warnings clean; 1575 lib tests + the harness pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 14:55:08 +02:00
co-authored by Claude Opus 4.8
parent 528cd5d5f5
commit 91a079718b
7 changed files with 905 additions and 218 deletions
+86 -218
View File
@@ -1,96 +1,52 @@
//! Aliveness probe — sample what the generated world *yields* at locations on a
//! body, to judge whether nature reads as a living, caused place (not just
//! law-correct). Exploratory tooling, not a test.
//!
//! It runs the real deterministic cascade (`run_cascade_from_heightmap`, up to
//! the road graph) for one body + seed, then probes a set of locations and
//! prints, for each, the authoritative per-area nature summary (`DistrictProfile`
//! — real elevation/slope/drainage/morphology/vegetation/climate derived from the
//! committed heightmap) plus a derived `VoxelColumn` sample (terrain / vegetation /
//! water / seasonal-cover / elevation) and a one-line "does it read alive" verdict.
//! Aliveness probe — the interactive front-end of the D-245 believability enforcer
//! (T-1083). It runs the real deterministic cascade for one body+seed and prints the
//! body-level [`BelievabilityReport`](settled_reach_server::atlas::believability) —
//! contrast + coherence, the verdict on whether nature reads alive — followed by a few
//! per-location samples for colour. The load+cascade+analyze logic lives in
//! `atlas::believability` so this binary and `tests/believability_harness` measure the
//! exact same thing.
//!
//! ## Why the *district* tier
//!
//! As of this writing the voxel layer (T-1028/T-1029) is a walking skeleton: no
//! production code maps a world chunk → its covering `DistrictProfile` yet (every
//! `derive_voxel_column` caller is a test). The `DistrictProfile` (2 km cell) is
//! therefore the finest *authoritative* nature unit the cascade actually produces,
//! so probes are addressed in district space; the voxel sample inside each is
//! illustrative ground-truth of what that district's 1 m tiles derive to.
//! No production code maps a world chunk → its covering `DistrictProfile` yet (the voxel
//! layer is a walking skeleton), so analysis is addressed in district space; the voxel
//! sample inside each district is illustrative ground-truth of what its 1 m tiles derive to.
//!
//! ```sh
//! cargo run --bin aliveness_probe -- --body GJ338Bd --seed yolo --probes 5
//! ```
use std::collections::BTreeMap;
use std::path::PathBuf;
use settled_reach_server::atlas::attractor_matching::CityRecord;
use settled_reach_server::atlas::body_params_reader::BodyParamsReader;
use settled_reach_server::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
use settled_reach_server::atlas::believability::{
analyze, cascade_for_body, evaluate_criteria, seed_to_u64, BelievabilityReport,
};
use settled_reach_server::atlas::chunk_context::derive_chunk_context;
use settled_reach_server::atlas::district_profile::DistrictProfile;
use settled_reach_server::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
use settled_reach_server::atlas::scale::{
self, ChunkPos, DistrictPos, CHUNKS_PER_DISTRICT, CHUNK_M,
};
use settled_reach_server::atlas::voxel::derive_voxel_column;
use settled_reach_server::seed::SeedChain;
use settled_reach_server::simulation::generator::SettlementClass;
const DEFAULT_SEA_LEVEL: f32 = 0.3;
/// 150 chunks (the original question's offset) in metres = 9 600 m.
const ANCHOR_OFFSET_CHUNKS: i32 = 150;
fn main() {
let args = Args::parse(std::env::args().skip(1));
let world_seed = fnv1a64(args.seed.as_bytes());
let world_seed = seed_to_u64(&args.seed);
eprintln!(
"aliveness probe — body={} seed=\"{}\" (u64={world_seed}) probes={}",
args.body, args.seed, args.probes
);
// ── Inputs (DB-free cascade; we resolve its inputs here) ────────────────
let db_path = first_existing(&["server/data/systems.db", "data/systems.db"])
.expect("systems.db not found (run from repo root)");
let hm_path = find_heightmap(&args.body).unwrap_or_else(|| {
panic!(
"no heightmap.png under wiki/star-systems/*/bodies/{}/",
args.body
)
});
let params = BodyParamsReader::open(&db_path)
.expect("open systems.db")
.read_body_params(&args.body)
.expect("read body params");
let cities = read_cities(&db_path, &args.body);
eprintln!(
"loaded heightmap {} | planet_class={:?} hydrosphere={:?} | {} settlements",
hm_path.display(),
params.planet_class,
params.hydrosphere,
cities.len()
);
// ── Run the real cascade through the road graph ─────────────────────────
let hm = load_heightmap_png(&hm_path, &args.body, DEFAULT_SEA_LEVEL).expect("load heightmap");
let working = if hm.width > GRID_W || hm.height > GRID_H {
hm.downsample(GRID_W, GRID_H)
} else {
hm
let bws = match cascade_for_body(world_seed, &args.body) {
Ok(b) => b,
Err(e) => {
eprintln!("cannot run cascade for {}: {e}", args.body);
std::process::exit(1);
}
};
let body_seed = SeedChain::for_body(world_seed, &args.body);
let snapshot = run_cascade_from_heightmap(
body_seed,
working,
&cities,
None,
Some(&params),
CascadeLayer::RoadGraph,
);
let bws = snapshot.into_body_world_state();
let districts = &bws.districts;
if districts.is_empty() {
@@ -104,37 +60,27 @@ fn main() {
bws.placements.len()
);
// ── Principal city (ranked by placement score; per-city pop is 0 in DB) ──
let name_of: BTreeMap<u64, &str> = cities
.iter()
.map(|c| (c.city_id, c.name.as_str()))
.collect();
let principal = bws.placements.iter().max_by_key(|p| p.score);
println!(
"\n================ {} (seed \"{}\") ================",
args.body, args.seed
);
let anchor_district = match principal {
Some(p) => {
let dp = scale::heightmap_pixel_to_district(p.position);
println!(
"principal settlement: {} @ pixel {:?} → district {:?} (ranked by placement score {}, since per-city population is 0 in the DB)",
name_of.get(&p.city_id).copied().unwrap_or("<unnamed>"),
p.position,
dp,
p.score
);
// "≈150 chunks west" = 9 600 m west = ~4.7 districts. West = x.
let west_districts = (ANCHOR_OFFSET_CHUNKS * CHUNK_M) / scale::DISTRICT_M; // = 4
Some((dp.0 - west_districts.max(1) - 1, dp.1)) // round 4.7 → 5 west
}
None => {
println!("no settlement placements — skipping the anchored probe.");
None
}
};
// ── The anchored probe (the location originally asked about) ────────────
// ── Body-level believability report — the D-245 enforcer verdict ────────
let report = analyze(world_seed, &args.body, districts);
print_report(&report);
// ── Anchored probe: ≈150 chunks west of the principal settlement ────────
// (highest placement score; per-city population is 0 in the DB).
let anchor_district = bws.placements.iter().max_by_key(|p| p.score).map(|p| {
let dp = scale::heightmap_pixel_to_district(p.position);
println!(
"\nprincipal settlement: city {} @ pixel {:?} → district {:?} (placement score {})",
p.city_id, p.position, dp, p.score
);
let west_districts = (ANCHOR_OFFSET_CHUNKS * CHUNK_M) / scale::DISTRICT_M;
(dp.0 - west_districts.max(1) - 1, dp.1)
});
if let Some(adp) = anchor_district {
println!(
"\n---- ANCHORED PROBE: ≈{ANCHOR_OFFSET_CHUNKS} chunks (9.6 km) west of the principal city ----"
@@ -148,7 +94,7 @@ fn main() {
if dp == adp {
""
} else {
"(nearest land district to the target point — target itself is off-map / open ocean)"
"(nearest land district — target is off-map / open ocean)"
},
),
None => println!(" target {adp:?} and neighbours are off-map; nothing to sample."),
@@ -158,15 +104,58 @@ fn main() {
// ── N random probes (deterministic from the seed) ───────────────────────
println!("\n---- {} RANDOM PROBES ----", args.probes);
for i in 0..args.probes {
let pick = (fnv1a64(format!("{}/{i}", args.seed).as_bytes()) % keys.len() as u64) as usize;
let pick = (seed_to_u64(&format!("{}/{i}", args.seed)) % keys.len() as u64) as usize;
let dp = keys[pick];
probe(&args.body, world_seed, dp, &districts[&dp], "");
}
println!("\n(district tier — voxel addressing is not production-wired yet; voxel rows are illustrative ground-truth derived for a representative chunk of each district.)");
println!("\n(district tier — voxel addressing is not production-wired yet; voxel rows are illustrative ground-truth for a representative chunk of each district.)");
}
/// Probe one district: print its profile + a derived voxel-column sample + verdict.
/// Print the body-level believability report + the advisory D-245 criteria — the
/// enforcer verdict (T-1083, D-245). Contrast and coherence, never per-tile counts.
fn print_report(r: &BelievabilityReport) {
let c = &r.contrast;
let h = &r.coherence;
println!(
"\n-- BELIEVABILITY (D-245 enforcer) -- {} districts, {} voxel-sampled --",
r.district_count, r.voxel_sampled_districts
);
println!(
" contrast: moisture_q[{}..{} ×{}] elev_q[{}..{} ×{}] slope_q[{}..{} ×{}] ocean%q[{}..{} ×{}]",
c.moisture_q.min, c.moisture_q.max, c.moisture_q.distinct,
c.elev_q.min, c.elev_q.max, c.elev_q.distinct,
c.slope_q.min, c.slope_q.max, c.slope_q.distinct,
c.ocean_fraction_q.min, c.ocean_fraction_q.max, c.ocean_fraction_q.distinct,
);
println!(
" morphology zones={} vegetation classes={} terrain materials={}",
c.morphology_zones, c.vegetation_classes, c.terrain_materials
);
println!(
" coherence: water wet {}/{} drainage monotonic {}/{} vegetated {}/{}",
h.water_districts_wet,
h.water_districts,
h.drainage_monotonic,
h.drainage_samples,
h.vegetated_districts,
h.vegetation_samples,
);
let crit = evaluate_criteria(r);
let passes = crit.iter().filter(|c| c.pass).count();
println!(" D-245 criteria (advisory — Q-123 calibrates thresholds):");
for cr in &crit {
println!(
" [{}] {}{}",
if cr.pass { "PASS" } else { "FAIL" },
cr.name,
cr.detail
);
}
println!("{}/{} criteria pass", passes, crit.len());
}
/// Probe one district: print its profile + a derived voxel-column sample for colour.
fn probe(body: &str, world_seed: u64, dp: DistrictPos, prof: &DistrictProfile, note: &str) {
// Representative chunk at the district centre (32 chunks / district).
let chunk: ChunkPos = (
@@ -230,67 +219,8 @@ fn probe(body: &str, world_seed: u64, dp: DistrictPos, prof: &DistrictProfile, n
fmt_hist(&water, n),
fmt_hist(&cover, n)
);
println!(
"{}",
verdict(&ctx_summary(&veg, &water, &cover, n), elev_max - elev_min)
);
}
struct Summary {
veg_alive_pct: u32,
water_pct: u32,
frozen_pct: u32,
}
fn ctx_summary(
veg: &BTreeMap<String, u32>,
water: &BTreeMap<String, u32>,
cover: &BTreeMap<String, u32>,
n: i64,
) -> Summary {
let n = n.max(1) as u32;
let barren = veg.get("Barren").copied().unwrap_or(0);
let dry = water.get("Dry").copied().unwrap_or(0);
let none_cover = cover.get("None").copied().unwrap_or(0);
Summary {
veg_alive_pct: 100 * (n - barren) / n,
water_pct: 100 * (n - dry) / n,
frozen_pct: 100 * (n - none_cover) / n,
}
}
/// A blunt heuristic "does this read as living nature" line. Not a metric of
/// record — a prompt for the eye.
fn verdict(s: &Summary, relief_m: i32) -> String {
let mut signals: Vec<String> = Vec::new();
if s.veg_alive_pct >= 50 {
signals.push(format!("vegetated ({}%)", s.veg_alive_pct));
} else if s.veg_alive_pct > 0 {
signals.push(format!("sparse life ({}%)", s.veg_alive_pct));
} else {
signals.push("barren".into());
}
if s.water_pct > 0 {
signals.push(format!("water present ({}%)", s.water_pct));
}
if s.frozen_pct >= 50 {
signals.push(format!("snow/ice {}%", s.frozen_pct));
}
if relief_m >= 20 {
signals.push(format!("relief {relief_m} m (hills/landmarks)"));
} else if relief_m <= 2 {
signals.push("flat".into());
}
let alive = s.veg_alive_pct >= 30 || s.water_pct >= 20 || relief_m >= 20;
format!(
"{}{}",
if alive {
"reads ALIVE"
} else {
"reads thin/dead"
},
signals.join(", ")
)
// No per-location "reads ALIVE" line: a marginal per-tile count called a broken
// uniform world alive (the T-1083 lesson). The verdict is the body-level report above.
}
fn fmt_hist(h: &BTreeMap<String, u32>, n: i64) -> String {
@@ -325,58 +255,7 @@ fn nearest_present(
}
// ---------------------------------------------------------------------------
// Inputs
// ---------------------------------------------------------------------------
fn read_cities(db: &PathBuf, body: &str) -> Vec<CityRecord> {
let conn = rusqlite::Connection::open(db).expect("open db for cities");
let mut stmt = conn
.prepare(
"SELECT id, name, COALESCE(economic_role,'service_mixed'), COALESCE(population,0)
FROM atlas_city_names WHERE body_id = ?1 ORDER BY id",
)
.expect("prepare city query");
let rows = stmt
.query_map([body], |r| {
Ok(CityRecord {
city_id: r.get::<_, i64>(0)? as u64,
name: r.get(1)?,
// Per-city settlement_class is unset in the DB pool; the budget
// class is the default the read-set assumes (city_context_reader).
settlement_class: SettlementClass::PopulationBudget,
economic_role: r.get(2)?,
population: r.get(3)?,
})
})
.expect("city query")
.filter_map(Result::ok)
.collect();
rows
}
/// Glob `wiki/star-systems/*/bodies/<body>/heightmap.png`.
fn find_heightmap(body: &str) -> Option<PathBuf> {
for base in ["wiki/star-systems", "../wiki/star-systems"] {
let root = PathBuf::from(base);
let Ok(systems) = std::fs::read_dir(&root) else {
continue;
};
for sys in systems.flatten() {
let cand = sys.path().join("bodies").join(body).join("heightmap.png");
if cand.is_file() {
return Some(cand);
}
}
}
None
}
fn first_existing(paths: &[&str]) -> Option<PathBuf> {
paths.iter().map(PathBuf::from).find(|p| p.is_file())
}
// ---------------------------------------------------------------------------
// Args + hashing
// Args
// ---------------------------------------------------------------------------
struct Args {
@@ -402,14 +281,3 @@ impl Args {
Args { body, seed, probes }
}
}
/// FNV-1a 64-bit — a stable, dependency-free string→u64 so `--seed yolo` maps to
/// a deterministic world seed (reproducible across runs, unlike std's RandomState).
fn fnv1a64(bytes: &[u8]) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}