Merge remote-tracking branch 'origin/district-window-render'

This commit is contained in:
2026-07-16 22:36:40 +02:00
2 changed files with 440 additions and 10 deletions
+22 -5
View File
@@ -45,9 +45,9 @@ use serde::{Deserialize, Serialize};
use crate::atlas::attractor_matching::CityRecord;
use crate::atlas::body_params_reader::BodyParamsReader;
use crate::atlas::body_world_state::BodyWorldState;
use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
use crate::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer, CascadeSnapshot};
use crate::atlas::chunk_context::derive_chunk_context;
use crate::atlas::district_profile::DistrictProfile;
use crate::atlas::district_profile::{BodyParams, DistrictProfile};
use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W};
use crate::atlas::scale::{ChunkPos, DistrictPos, CHUNKS_PER_DISTRICT, CHUNK_M};
use crate::atlas::voxel::{derive_voxel_column, Vegetation, Water};
@@ -508,9 +508,26 @@ pub fn seed_to_u64(seed: &str) -> u64 {
///
/// `Err` if `systems.db` or the body's `heightmap.png` cannot be found, or the body
/// has no params — callers (the regression harness) may *skip* on that rather than
/// fail, so this is the one believability entry point that does I/O. Tries both the
/// repo-root and `server/`-relative paths so it works from either CWD.
/// fail, so the `*_for_body` loaders are the believability entry points that do I/O.
/// Tries both the repo-root and `server/`-relative paths so it works from either CWD.
pub fn cascade_for_body(world_seed: u64, body_id: &str) -> Result<BodyWorldState, String> {
cascade_snapshot_for_body(world_seed, body_id)
.map(|(snapshot, _params)| snapshot.into_body_world_state())
}
/// Like [`cascade_for_body`], but returns the raw [`CascadeSnapshot`] — which still
/// carries the transient `TerrainAnalysis` — together with the body's [`BodyParams`].
///
/// This is the entry point for callers that need the *on-demand* district-derivation
/// inputs (`district_profile::derive_district` wants `&TerrainAnalysis` +
/// `&BodyParams`), e.g. the aliveness probe's `--render` district-window mode
/// (T-1123). [`cascade_for_body`] is this plus the `BodyWorldState` conversion,
/// which drops the TerrainAnalysis per the D-203/T-1048 size budget — take it off
/// the snapshot *before* converting.
pub fn cascade_snapshot_for_body(
world_seed: u64,
body_id: &str,
) -> Result<(CascadeSnapshot, BodyParams), String> {
let db = first_existing(&["server/data/systems.db", "data/systems.db"])
.ok_or_else(|| "systems.db not found".to_string())?;
let hm_path =
@@ -538,7 +555,7 @@ pub fn cascade_for_body(world_seed: u64, body_id: &str) -> Result<BodyWorldState
Some(&params),
CascadeLayer::Region,
);
Ok(snapshot.into_body_world_state())
Ok((snapshot, params))
}
/// Read a body's settlements from `atlas_city_names`, mirroring
+418 -5
View File
@@ -15,18 +15,37 @@
//! ```sh
//! cargo run --bin aliveness_probe -- --body GJ338Bd --seed yolo --probes 5
//! ```
//!
//! ## `--render <out_dir>` — district-window maps (T-1123)
//!
//! Renders an N×N window (`--window`, default 64) of **true 2 km districts**
//! centred on the body's principal settlement, one PNG per attribute panel
//! (morphology / elev / temp / moisture / veg), via the pure on-demand
//! [`derive_district`] path. The orbit→jet-plane bridge demonstrator: nobody
//! had ever *seen* the district layer before this.
//!
//! ```sh
//! cargo run --bin aliveness_probe -- --body GJ380c --seed 42 \
//! --render /tmp/district-windows --window 64
//! ```
use std::collections::BTreeMap;
use settled_reach_server::atlas::believability::{
analyze, cascade_for_body, evaluate_criteria, seed_to_u64, BelievabilityReport,
analyze, cascade_snapshot_for_body, evaluate_criteria, seed_to_u64, BelievabilityReport,
};
use settled_reach_server::atlas::body_world_state::BodyWorldState;
use settled_reach_server::atlas::chunk_context::derive_chunk_context;
use settled_reach_server::atlas::district_profile::DistrictProfile;
use settled_reach_server::atlas::district_profile::{
derive_district, BodyParams, ClimateConstants, DistrictProfile, VegetationClass,
};
use settled_reach_server::atlas::features::TerrainAnalysis;
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::MorphologyZone;
/// 150 chunks (the original question's offset) in metres = 9 600 m.
const ANCHOR_OFFSET_CHUNKS: i32 = 150;
@@ -40,13 +59,29 @@ fn main() {
args.body, args.seed, args.probes
);
let bws = match cascade_for_body(world_seed, &args.body) {
Ok(b) => b,
let (mut snapshot, body_params) = match cascade_snapshot_for_body(world_seed, &args.body) {
Ok(x) => x,
Err(e) => {
eprintln!("cannot run cascade for {}: {e}", args.body);
std::process::exit(1);
}
};
// The transient TerrainAnalysis is the bilinear-envelope input the on-demand
// derive_district path (--render) needs for true 2 km districts. The cascade
// frees it once its own consumers (DistrictProfile + RoadGraph) have run
// (D-203/T-1048 size budget), so at Region depth `take()` yields None and we
// re-derive from the working heightmap — the path CascadeSnapshot documents
// ("callers that need it after the cascade must re-derive from run_layer1").
// Must happen BEFORE into_body_world_state() moves the heightmap out.
let terrain = match snapshot.terrain_analysis.take() {
Some(ta) => Some(ta),
None if args.render.is_some() => {
Some(settled_reach_server::atlas::layer1::run_layer1(&snapshot.heightmap).1)
}
None => None,
};
let body_seed = snapshot.seed;
let bws = snapshot.into_body_world_state();
let districts = &bws.districts;
if districts.is_empty() {
@@ -110,6 +145,16 @@ fn main() {
}
println!("\n(district tier — voxel addressing is not production-wired yet; voxel rows are illustrative ground-truth for a representative chunk of each district.)");
// ── --render: district-window PNG maps (T-1123) ──────────────────────────
if let Some(out_dir) = &args.render {
match &terrain {
Some(ta) => render_district_windows(out_dir, &args, body_seed, &body_params, ta, &bws),
None => eprintln!(
"--render: cascade retained no TerrainAnalysis — cannot render district windows."
),
}
}
}
/// Print the body-level believability report + the advisory D-245 criteria — the
@@ -254,6 +299,312 @@ fn nearest_present(
None
}
// ---------------------------------------------------------------------------
// --render: district-window PNG maps (T-1123)
// ---------------------------------------------------------------------------
//
// N×N windows of TRUE 2 km districts around the principal settlement, one PNG
// per attribute panel, straight from the pure on-demand `derive_district`
// path. Deliberately NOT `bws.districts`: that eager map is the coarse
// working-grid pseudo-grid (gcpr = 8 heightmap cells per "district", tens of
// km per cell) — the whole point here is true 2 km fidelity.
/// `MorphologyZone` discriminant → RGB. Transliterated from the client overlay
/// palette — `client/ui/implant/apps/atlas/atlas_marker_overlay.gd`,
/// `MORPHOLOGY_COLORS` (D-239 §6 discriminant order, T-1046) — which is the
/// source of truth; **keep the two in sync** (same order, same 17 entries).
/// The client's 0.55 alpha is dropped: these PNGs are opaque, no heightmap
/// underlay to blend with.
const MORPHOLOGY_RGB: [[u8; 3]; 17] = [
[26, 51, 115], // 0 OpenOcean
[51, 102, 166], // 1 Lake
[115, 140, 128], // 2 TidalFlat
[217, 199, 115], // 3 DuneStrand
[128, 128, 140], // 4 CliffCoast
[77, 102, 128], // 5 Fjord
[102, 166, 153], // 6 Delta
[77, 140, 140], // 7 Estuarine
[77, 153, 77], // 8 AlluvialPlain
[115, 179, 102], // 9 RiverBank
[89, 153, 128], // 10 MeanderReach
[140, 153, 115], // 11 BraidedPlain
[128, 140, 77], // 12 ValleyFloor
[140, 115, 77], // 13 MountainPass
[204, 209, 217], // 14 Alpine
[115, 38, 31], // 15 Volcanic
[64, 115, 102], // 16 Wetland
];
/// The five attribute panels, in output order.
const PANEL_NAMES: [&str; 5] = ["morphology", "elev", "temp", "moisture", "veg"];
/// Render the district window around the principal settlement and write one PNG
/// per panel to `out_dir`, plus a one-line manifest per image and a determinism
/// spot-proof (two full derive+encode passes must be byte-identical — D-010 in
/// miniature).
fn render_district_windows(
out_dir: &str,
args: &Args,
body_seed: SeedChain,
params: &BodyParams,
ta: &TerrainAnalysis,
bws: &BodyWorldState,
) {
// Anchor: `--anchor <substr>` (case-insensitive name match) when given,
// else the capital placement when one exists, else the most populous,
// else the best-scoring.
let named = args.anchor.as_ref().and_then(|pat| {
let pat_lower = pat.to_lowercase();
let found = bws
.placements
.iter()
.find(|p| p.name.to_lowercase().contains(&pat_lower));
if found.is_none() {
eprintln!(
"--anchor \"{pat}\": no placement name matches — using the principal settlement"
);
}
found
});
let Some(anchor) = named.or_else(|| {
bws.placements
.iter()
.max_by_key(|p| (p.is_capital, p.population, p.score))
}) else {
eprintln!("--render: body has no settlement placements — nothing to anchor a window on.");
return;
};
let centre = true_district_of_pixel(anchor.position, ta.w, ta.h, params.body_radius_km);
if let Err(e) = std::fs::create_dir_all(out_dir) {
eprintln!("--render: cannot create {out_dir}: {e}");
return;
}
let n = args.window.max(2);
let extent_km = n as f64 * scale::DISTRICT_M as f64 / 1000.0;
let climate = ClimateConstants::default();
println!("\n---- DISTRICT WINDOW RENDER (T-1123) ----");
println!(
"anchor: {} (capital={}, pop={}) @ working pixel {:?} → true district ({}, {})",
anchor.name, anchor.is_capital, anchor.population, anchor.position, centre.0, centre.1
);
println!(
"window: {n}×{n} districts = {extent_km:.1}×{extent_km:.1} km @ 2.048 km/px (seed \"{}\")",
args.seed
);
// Determinism spot-proof: two independent full passes, byte-compared.
let t0 = std::time::Instant::now();
let first = render_window_panels(body_seed, &bws.body_id, params, ta, centre, n, &climate);
let pass1_ms = t0.elapsed().as_millis();
let t1 = std::time::Instant::now();
let second = render_window_panels(body_seed, &bws.body_id, params, ta, centre, n, &climate);
let pass2_ms = t1.elapsed().as_millis();
let identical = first
.iter()
.zip(second.iter())
.filter(|((_, a), (_, b))| a == b)
.count();
let body_lower = args.body.to_lowercase();
for (panel, png_bytes) in &first {
let file = format!("{out_dir}/{body_lower}_w{n}_{panel}.png");
if let Err(e) = std::fs::write(&file, png_bytes) {
eprintln!("--render: write {file}: {e}");
continue;
}
println!(
"RENDER body={} seed=\"{}\" window={n} extent_km={extent_km:.1} \
anchor_city=\"{}\" anchor_district=({},{}) panel={panel} file={file}",
args.body, args.seed, anchor.name, centre.0, centre.1
);
}
println!(
"DETERMINISM: {identical}/{} panels byte-identical across two full derive+encode passes \
[{}] — pass1 {pass1_ms} ms, pass2 {pass2_ms} ms ({} district derivations/pass)",
first.len(),
if identical == first.len() {
"OK"
} else {
"MISMATCH"
},
(n as u64) * (n as u64),
);
}
/// Derive every district in the window and encode the five attribute panels
/// as PNG byte vectors (encode included so the determinism proof covers the
/// full derive→encode pipeline).
fn render_window_panels(
seed: SeedChain,
body_id: &str,
params: &BodyParams,
ta: &TerrainAnalysis,
centre: DistrictPos,
n: u32,
climate: &ClimateConstants,
) -> Vec<(&'static str, Vec<u8>)> {
let n_i = n as i32;
let half = n_i / 2;
let mut bufs: [Vec<u8>; 5] = std::array::from_fn(|_| vec![0u8; (n * n) as usize * 3]);
for row in 0..n_i {
for col in 0..n_i {
// Row 0 = northmost: smaller dy = further north (derive_district maps
// negative wy to negative lat_frac = northern latitudes).
let dp = (centre.0 - half + col, centre.1 - half + row);
let prof = derive_district(seed, body_id, params, ta, dp, climate);
let i = ((row * n_i + col) * 3) as usize;
set_rgb(&mut bufs[0], i, morphology_rgb(prof.morphology_zone));
set_rgb(&mut bufs[1], i, gray_rgb(prof.elev_q));
set_rgb(&mut bufs[2], i, temperature_rgb(prof.temperature_c));
set_rgb(&mut bufs[3], i, moisture_rgb(prof.moisture_q));
set_rgb(&mut bufs[4], i, vegetation_rgb(prof.vegetation_class));
}
}
for buf in &mut bufs {
draw_anchor_crosshair(buf, n_i, half, half);
}
PANEL_NAMES
.iter()
.zip(bufs)
.map(|(name, buf)| (*name, encode_rgb_png(n, n, &buf)))
.collect()
}
/// Map a working-grid settlement pixel `(row, col)` to the TRUE metric 2 km
/// district containing it — the inverse of `derive_district`'s forward mapping
/// (district → fractional working-grid position via the body radius, the D-204
/// elastic seam). NOT `scale::heightmap_pixel_to_district`: that addresses the
/// eager gcpr = 8 pseudo-grid, whose cells span tens of km.
fn true_district_of_pixel(
pixel: (u16, u16),
ta_w: usize,
ta_h: usize,
radius_km: Option<f64>,
) -> DistrictPos {
let (row, col) = (pixel.0 as f64, pixel.1 as f64);
match radius_km {
Some(r) if r > 0.0 => {
let circumference_m = std::f64::consts::TAU * r * 1000.0;
let meridian_m = std::f64::consts::PI * r * 1000.0;
let wx = col / ta_w.max(1) as f64 * circumference_m;
let lat_frac = if ta_h > 1 {
row / (ta_h - 1) as f64 - 0.5
} else {
0.0
};
let wy = lat_frac * meridian_m;
(
(wx / scale::DISTRICT_M as f64).round() as i32,
(wy / scale::DISTRICT_M as f64).round() as i32,
)
}
// No radius: the district grid IS the working grid (derive_district's
// tiny-test-body fallback) — the pixel is the district.
_ => (col as i32, row as i32),
}
}
#[inline]
fn set_rgb(buf: &mut [u8], i: usize, c: [u8; 3]) {
buf[i..i + 3].copy_from_slice(&c);
}
fn morphology_rgb(zone: MorphologyZone) -> [u8; 3] {
// Out-of-palette discriminant (enum grew, palette didn't) → magenta sentinel,
// and the unit test below fails until both palettes are extended.
*MORPHOLOGY_RGB
.get(zone as u8 as usize)
.unwrap_or(&[255, 0, 255])
}
/// `elev_q` 0100 → 8-bit grayscale.
fn gray_rgb(elev_q: i32) -> [u8; 3] {
let g = (elev_q.clamp(0, 100) * 255 / 100) as u8;
[g, g, g]
}
/// Heat ramp over 60…+40 °C (clamped; piecewise blue→cyan→yellow→red).
/// Airless bodies (`temperature_c == None`, D-239 §2) render as a magenta
/// sentinel — "no atmosphere" must never be readable as a temperature.
fn temperature_rgb(t: Option<f32>) -> [u8; 3] {
const STOPS: [(f32, [f32; 3]); 4] = [
(-60.0, [0.0, 0.0, 255.0]),
(-20.0, [0.0, 255.0, 255.0]),
(10.0, [255.0, 255.0, 0.0]),
(40.0, [255.0, 0.0, 0.0]),
];
let Some(t) = t else { return [255, 0, 255] };
let t = t.clamp(STOPS[0].0, STOPS[STOPS.len() - 1].0);
for w in STOPS.windows(2) {
let (t0, c0) = w[0];
let (t1, c1) = w[1];
if t <= t1 {
return lerp_rgb(c0, c1, ((t - t0) / (t1 - t0)).clamp(0.0, 1.0));
}
}
[255, 0, 0]
}
/// Dry→wet ramp: parched tan → grass green → deep water blue (`moisture_q` 0100).
fn moisture_rgb(q: i32) -> [u8; 3] {
let q = q.clamp(0, 100) as f32;
if q <= 50.0 {
lerp_rgb([150.0, 110.0, 70.0], [90.0, 160.0, 90.0], q / 50.0)
} else {
lerp_rgb([90.0, 160.0, 90.0], [30.0, 90.0, 200.0], (q - 50.0) / 50.0)
}
}
/// Categorical vegetation palette ([`VegetationClass`], D-239 §8 ladder).
fn vegetation_rgb(v: VegetationClass) -> [u8; 3] {
match v {
VegetationClass::Absent => [40, 40, 40], // airless — near-black
VegetationClass::Barren => [160, 140, 110], // bare-ground tan
VegetationClass::Scrub => [140, 160, 80], // olive
VegetationClass::Forest => [30, 110, 40], // closed canopy
VegetationClass::RiparianScrub => [70, 170, 120], // waterway band (open)
VegetationClass::RiparianThicket => [10, 130, 90], // waterway band (dense)
}
}
fn lerp_rgb(a: [f32; 3], b: [f32; 3], t: f32) -> [u8; 3] {
let mix = |i: usize| (a[i] + (b[i] - a[i]) * t).round().clamp(0.0, 255.0) as u8;
[mix(0), mix(1), mix(2)]
}
/// White crosshair (±3 px arms) with darkened diagonal shoulders at the anchor
/// pixel, so the marker stays visible on dark (ocean) and light (alpine) panels
/// alike and the window orients against the planetary Atlas shots.
fn draw_anchor_crosshair(buf: &mut [u8], n: i32, cx: i32, cy: i32) {
let mut put = |x: i32, y: i32, c: [u8; 3]| {
if x >= 0 && x < n && y >= 0 && y < n {
set_rgb(buf, ((y * n + x) * 3) as usize, c);
}
};
for (dx, dy) in [(-1, -1), (1, -1), (-1, 1), (1, 1)] {
put(cx + dx, cy + dy, [0, 0, 0]);
}
for d in -3i32..=3 {
put(cx + d, cy, [255, 255, 255]);
put(cx, cy + d, [255, 255, 255]);
}
}
/// Encode an 8-bit RGB buffer as PNG bytes (pattern from `heightmap.rs`).
fn encode_rgb_png(w: u32, h: u32, rgb: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
{
let mut enc = png::Encoder::new(&mut out, w, h);
enc.set_color(png::ColorType::Rgb);
enc.set_depth(png::BitDepth::Eight);
let mut writer = enc.write_header().expect("png header");
writer.write_image_data(rgb).expect("png data");
}
out
}
// ---------------------------------------------------------------------------
// Args
// ---------------------------------------------------------------------------
@@ -262,6 +613,13 @@ struct Args {
body: String,
seed: String,
probes: u32,
/// `--render <out_dir>` — write district-window PNG panels (T-1123).
render: Option<String>,
/// `--window <N>` — window side in districts (default 64 ≈ 131 km).
window: u32,
/// `--anchor <substr>` — anchor the window on the placement whose name
/// matches (case-insensitive); default: capital, else most populous.
anchor: Option<String>,
}
impl Args {
@@ -269,15 +627,70 @@ impl Args {
let mut body = "GJ338Bd".to_string();
let mut seed = "yolo".to_string();
let mut probes = 5u32;
let mut render = None;
let mut window = 64u32;
let mut anchor = None;
let mut it = args.peekable();
while let Some(a) = it.next() {
match a.as_str() {
"--body" => body = it.next().unwrap_or(body),
"--seed" => seed = it.next().unwrap_or(seed),
"--probes" => probes = it.next().and_then(|s| s.parse().ok()).unwrap_or(probes),
"--render" => render = it.next(),
"--window" => window = it.next().and_then(|s| s.parse().ok()).unwrap_or(window),
"--anchor" => anchor = it.next(),
_ => {}
}
}
Args { body, seed, probes }
Args {
body,
seed,
probes,
render,
window,
anchor,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
/// The palette is indexed by `MorphologyZone` discriminant (repr(u8),
/// append-only). Pinning the LAST variant's discriminant against the
/// palette length forces a palette extension — here AND in the client
/// source of truth (`atlas_marker_overlay.gd::MORPHOLOGY_COLORS`) — when
/// a zone is appended.
#[test]
fn morphology_palette_covers_every_zone() {
assert_eq!(MorphologyZone::Wetland as usize + 1, MORPHOLOGY_RGB.len());
}
/// `true_district_of_pixel` must invert `derive_district`'s forward mapping
/// (district → fractional working-grid position via the body radius) to
/// within the half-district rounding error — ≲0.01 working pixel at
/// planetary radii.
#[test]
fn true_district_inverts_forward_mapping() {
let (w, h) = (128usize, 64usize);
let r_km = 6371.0;
let circ_m = std::f64::consts::TAU * r_km * 1000.0;
let mer_m = std::f64::consts::PI * r_km * 1000.0;
for (row, col) in [(10u16, 100u16), (32, 0), (5, 127), (63, 64)] {
let (dx, dy) = true_district_of_pixel((row, col), w, h, Some(r_km));
let px = ((dx as f64 * scale::DISTRICT_M as f64) / circ_m).rem_euclid(1.0) * w as f64;
let py = (0.5 + ((dy as f64 * scale::DISTRICT_M as f64) / mer_m).clamp(-0.5, 0.5))
* (h - 1) as f64;
assert!((px - col as f64).abs() < 0.05, "px {px} vs col {col}");
assert!((py - row as f64).abs() < 0.05, "py {py} vs row {row}");
}
}
/// Without a radius the fallback maps pixel → district 1:1 (mirrors
/// `derive_district`'s tiny-test-body branch).
#[test]
fn true_district_fallback_is_identity() {
assert_eq!(true_district_of_pixel((7, 3), 128, 64, None), (3, 7));
}
}