First foundation slice of the Atlas-to-tile derivation model (epic T-974), building the carrier layer ahead of its T-1027+ consumers. T-1026 — Anti-squaring domain warp (D-239 §4): stateless pure fn(seed,body_id,pos)->(f64,f64), ±8m, f64 to the final voxel then as-i32 truncation for IEEE-754 cross-target determinism. New domain_warp.rs, SeedDomain::DomainWarp; golden-vector + cross-thread tests. Position math only — D-010 integer discipline preserved downstream. Marked dead_code until the T-1028 VoxelColumn pipeline consumes it. T-1023 — RegionProfile carrier (D-239 §1,§10): new RegionProfile + TectonicClass/GlaciationGrade/PrecipitationClass enums + BodyParams; derived per-region river_threshold replacing the global 200 for tile consumers. regions: BTreeMap on BodyWorldState, populated via the cascade's new RegionProfile layer (runs when body_params is Some, else falls back to Settlement). D-010 integer discipline, BTree ordering. T-1024 — District climate primitives (D-239 §2): nullable temperature_c + moisture on RegionProfile, mean-annual scalar (no clock dep; dynamic branch deferred to Q-105). Hybrid inputs — new bodies.axial_tilt_deg column imported from planet-gen body-defs (populate_axial_tilt_deg, 2611 bodies), luminosity and orbital distance derived at runtime; greenhouse + diurnal-swing tables in source-canonical climate_constants.toml. D-239 implementation note added. cargo test: 1498 passed, 0 failed. clippy clean (pre-existing large_enum_variant only). make check-systems-db: stamp fresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
5.1 KiB
Rust
113 lines
5.1 KiB
Rust
//! Golden-seed regression for the Layer 0→1 generation cascade (#952, D-200, D-224).
|
||
//!
|
||
//! Pins two artifacts for one fixed input heightmap, in a single diffable JSON
|
||
//! golden (matching the `golden_suite.rs` convention):
|
||
//! - **Layer 0** — SHA-256 of the source `heightmap.png` bytes. Flips if the
|
||
//! Python heightmap generator (or the committed file) changes.
|
||
//! - **Layer 1** — the serialized `Layer1Output` of the cascade run on a
|
||
//! downsampled copy. Flips if the Rust drainage / feature / sub-biome code
|
||
//! changes.
|
||
//!
|
||
//! The input is a real committed body heightmap (the #963 bake output), so this
|
||
//! is an end-to-end determinism guard. The cascade is downsampled to keep the
|
||
//! golden small while still exercising the full Layer-1 pipeline.
|
||
//!
|
||
//! Regenerate after an intended change:
|
||
//! UPDATE_GOLDEN=1 cargo test --test cascade_golden
|
||
//!
|
||
//! Golden captured on x86_64. The downsample and sub-biome cost use f32, so a
|
||
//! different architecture could in principle round differently — regenerate
|
||
//! per-arch if CI ever moves off x86_64.
|
||
|
||
use std::path::PathBuf;
|
||
|
||
use serde_json::{json, Value};
|
||
use settled_reach_server::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer};
|
||
use settled_reach_server::atlas::heightmap::load_heightmap_png;
|
||
use settled_reach_server::seed::SeedChain;
|
||
use sha2::{Digest, Sha256};
|
||
|
||
/// Source heightmap — a real committed body, relative to the server manifest dir.
|
||
const SOURCE_HEIGHTMAP: &str = "../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png";
|
||
/// Downsample target: large enough that GJ1c's drainage produces a real river
|
||
/// network (not just attractors), small enough for a compact golden.
|
||
const DOWNSAMPLE: (u32, u32) = (256, 128);
|
||
/// World seed for the run. Cosmetic here — Layers 0–1 are RNG-free, so changing
|
||
/// it does not change the golden; it is carried only to exercise the SeedChain
|
||
/// contract end-to-end (D-224). Seed-sensitivity gets pinned once Layer 3+ lands.
|
||
const WORLD_SEED: u64 = 42;
|
||
const GOLDEN: &str = "tests/golden/cascade_layer1.json";
|
||
|
||
#[test]
|
||
fn cascade_layer0_to_1_matches_golden() {
|
||
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||
let src = manifest.join(SOURCE_HEIGHTMAP);
|
||
|
||
// ── Layer 0 — load + hash the source heightmap.png. ─────────────────────
|
||
let png_bytes = std::fs::read(&src)
|
||
.unwrap_or_else(|e| panic!("read source heightmap {}: {e}", src.display()));
|
||
let mut hasher = Sha256::new();
|
||
hasher.update(&png_bytes);
|
||
let sha = format!("{:x}", hasher.finalize());
|
||
|
||
let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode source heightmap");
|
||
|
||
// ── Layer 1 — downsample, then run the cascade to topography. ───────────
|
||
let small = heightmap.downsample(DOWNSAMPLE.0, DOWNSAMPLE.1);
|
||
let body_seed = SeedChain::for_body(WORLD_SEED, "GJ1c");
|
||
let snapshot =
|
||
run_cascade_from_heightmap(body_seed, small, &[], None, None, CascadeLayer::Topography);
|
||
let layer1 = snapshot.layer1.expect("Layer 1 ran");
|
||
|
||
let actual = json!({
|
||
"source_heightmap": SOURCE_HEIGHTMAP,
|
||
"source_sha256": sha,
|
||
"downsample": [DOWNSAMPLE.0, DOWNSAMPLE.1],
|
||
"layer1": serde_json::to_value(&layer1).expect("serialize Layer1Output"),
|
||
});
|
||
let actual_json = serde_json::to_string_pretty(&actual).expect("format JSON") + "\n";
|
||
|
||
let golden_path = manifest.join(GOLDEN);
|
||
|
||
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 not found: {}. Run: UPDATE_GOLDEN=1 cargo test --test cascade_golden\n{e}",
|
||
golden_path.display()
|
||
)
|
||
});
|
||
|
||
// Compare as parsed JSON so whitespace/formatting never causes spurious diffs.
|
||
let actual_v: Value = serde_json::from_str(&actual_json).expect("reparse actual JSON");
|
||
let golden_v: Value = serde_json::from_str(&golden_json).expect("parse golden JSON");
|
||
|
||
if actual_v != golden_v {
|
||
let count =
|
||
|v: &Value, ptr: &str| v.pointer(ptr).and_then(Value::as_array).map_or(0, Vec::len);
|
||
panic!(
|
||
"Cascade golden mismatch.\n \
|
||
source_sha256: golden={} actual={}\n \
|
||
river_cells: golden={} actual={}\n \
|
||
attractors: golden={} actual={}\n\
|
||
To update: UPDATE_GOLDEN=1 cargo test --test cascade_golden\n Golden: {}",
|
||
golden_v["source_sha256"],
|
||
actual_v["source_sha256"],
|
||
count(&golden_v, "/layer1/river_network/river_cells"),
|
||
count(&actual_v, "/layer1/river_network/river_cells"),
|
||
count(&golden_v, "/layer1/attractors"),
|
||
count(&actual_v, "/layer1/attractors"),
|
||
golden_path.display(),
|
||
);
|
||
}
|
||
}
|