Files
jpmschweitzerandClaude Opus 4.8 0263b68df9 feat(simulation): cache TerrainAnalysis + derive basin_direction from the D8 thalweg (T-1044, T-1047)
T-1044: run_layer1 now returns TerrainAnalysis (carried transiently on
CascadeSnapshot, dropped after the district + road-graph passes), eliminating the
redundant per-body drainage::analyze + TerrainAnalysis::analyze re-run flagged by
PERF/TODO(T-1044). Not persisted on the LRU-cached state (D-203/T-1048 size concern).

T-1047: basin_direction is now derived from the real D8 thalweg. run_layer1
aggregates a per-district dominant D8 direction from the live fdir grid (carried
transiently on DrainageResult), threaded via Layer1Output.district_basin_dirs ->
derive_all_districts -> DistrictProfile.basin_direction; derive_chunk_context reads
it directly. Removed the false derive_basin_direction (it branched on ocean_fraction_q
then read seed bits despite a doc comment claiming an elev_q/slope_q D8 proxy) +
corrected the module contract. D-239 §8 (D8 thalweg) now actually honoured.

1559 tests pass; golden byte-identical (district_basin_dirs is #[serde(skip)], transient).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 09:46:42 +02:00

106 lines
4.0 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Benchmark: per-body Layer-1 topography generation (#953).
//!
//! Times `run_layer1` (D8 drainage → terrain analysis → 7 feature tags →
//! sub-biome classification) on a synthetic canonical 512×256 heightmap — the
//! cost of "populating the top-level map for a body." Run with:
//!
//! cargo run --release --example bench_layer1
//!
//! The committed systems.db carries no heightmaps yet (the importer isn't in
//! regen-db), so this uses a multi-octave synthetic heightmap representative of
//! a continental body. Heightmap *content* affects timing (river/attractor
//! counts), so treat this as an order-of-magnitude figure against the D-208
//! ~50ms target, not a per-body guarantee.
use std::time::Instant;
use settled_reach_server::atlas::heightmap::{BodyHeightmap, GRID_H, GRID_W};
use settled_reach_server::atlas::layer1::run_layer1;
/// Deterministic multi-octave sine "noise" → continents, coasts, drainage.
fn synth_heightmap(w: u32, h: u32) -> Vec<f32> {
use std::f32::consts::{PI, TAU};
let mut data = vec![0f32; (w * h) as usize];
for r in 0..h {
for c in 0..w {
let x = c as f32 / w as f32 * TAU;
let y = r as f32 / h as f32 * PI;
let e = 0.5
+ 0.25 * (x * 3.0).sin() * (y * 2.0).sin()
+ 0.15 * (x * 7.0).cos() * (y * 5.0).sin()
+ 0.08 * (x * 13.0).sin() * (y * 11.0).cos()
+ 0.05 * (x * 23.0).cos() * (y * 19.0).sin();
data[(r * w + c) as usize] = e.clamp(0.0, 1.0);
}
}
data
}
fn main() {
let (w, h) = (GRID_W, GRID_H);
let hm = BodyHeightmap {
body_id: "Bench".into(),
width: w,
height: h,
data: synth_heightmap(w, h),
sea_level: 0.40,
};
// Warm-up + output characterization.
// run_layer1 now returns (Layer1Output, TerrainAnalysis) — destructure (T-1044).
let (o, _ta) = run_layer1(&hm);
let mut by_type: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
for a in &o.attractors {
*by_type
.entry(format!("{:?}", a.attractor_type))
.or_default() += 1;
}
println!("=== Layer-1 output for one 512×256 body ===");
println!(
" river cells: {} mouths: {} confluences: {} basins: {}",
o.river_network.river_cells.len(),
o.river_network.mouths.len(),
o.river_network.confluences.len(),
o.drainage_basins.len(),
);
println!(" attractors: {} {:?}", o.attractors.len(), by_type);
// Phase breakdown (median of N) to locate the cost.
use settled_reach_server::atlas::drainage;
use settled_reach_server::atlas::features::{extract_attractors, TerrainAnalysis};
let n = 30;
let med = |mut v: Vec<f64>| {
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v[v.len() / 2]
};
let mut t_drain = vec![];
let mut t_terr = vec![];
let mut t_feat = vec![];
let mut t_total = vec![];
for _ in 0..n {
let t = Instant::now();
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
t_drain.push(t.elapsed().as_secs_f64() * 1000.0);
let t = Instant::now();
let ta = TerrainAnalysis::analyze(&hm, &dr);
t_terr.push(t.elapsed().as_secs_f64() * 1000.0);
let t = Instant::now();
let raw = extract_attractors(&hm, &dr, &ta);
t_feat.push(t.elapsed().as_secs_f64() * 1000.0);
std::hint::black_box(&raw);
let t = Instant::now();
let out = run_layer1(&hm);
std::hint::black_box(&out.0);
t_total.push(t.elapsed().as_secs_f64() * 1000.0);
}
println!("\n=== phase breakdown (median of {n}, 512×256, release) ===");
println!(" D8 drainage (D-208): {:.2}ms", med(t_drain));
println!(" terrain analysis: {:.2}ms", med(t_terr));
println!(" feature extraction: {:.2}ms", med(t_feat));
println!(" run_layer1 (total): {:.2}ms", med(t_total));
println!(" D-208 target: ~50ms/body");
}