From befdb689c1f2e85b8dad3755c53bf73315b2e4c8 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Sat, 25 Jul 2026 16:09:46 +0200 Subject: [PATCH] =?UTF-8?q?fix(simulation):=20orbital=20rung=20applies=20c?= =?UTF-8?q?oast=5Fwarp=5Fpx=20=E2=80=94=20one=20coastline=20at=20every=20r?= =?UTF-8?q?ung=20(T-1160)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Global/Region rungs sampled the heightmap ocean mask raw while every finer rung samples it at the coast-warped position — a structurally different coastline at the orbital-to-district seam. Audited on all 267 real body heightmaps at region spacing, coastal band only: 5.01% land/ocean classification disagreement (2,911 of 58,073 cells), mean displacement ~6.0 km, max ~18.9 km. Fork taken: APPLY the warp — derive_orbital_at_metres now runs invent_primitives' steps 1-3 (driver climate -> coast character -> coast_warp_px) before sampling, still skipping detail-scatter (its octave ceiling, 32.8 km, is below region spacing; the WARP's octaves reach 262 km, which is why skipping it was wrong). Cost measured: ~784 ns/cell added (~15 ms on an Earth-class Global canvas; low hundreds of ms parallel at the 8.3M-cell ceiling). The ignored audit test preserves the pre-fix baseline for the record. Golden re-pinned — region rows only, district/quarter byte-identical. project.yaml 0.4.2 -> 0.4.3: orbital canvas bytes changed, client disk caches must miss. Awaiting Araminta's review-seat sign-off per ticket. Co-Authored-By: Claude Fable 5 --- project.yaml | 6 +- server/src/atlas/district_profile.rs | 369 ++++++++++++++++-- .../golden/window_derivation_golden.json | 24 +- 3 files changed, 355 insertions(+), 44 deletions(-) diff --git a/project.yaml b/project.yaml index f962665a7..3ae2906f3 100644 --- a/project.yaml +++ b/project.yaml @@ -4,7 +4,11 @@ name: The Settled Reach # 0.4.1-tagged step canvases carried lake_margin_q's flat absolute-ceiling # semantics (pre-per-basin-normalization) and were written to real disk # caches during T-1188 eyeball runs — 0.4.2 forces those entries to miss. -version: 0.4.2 +# 0.4.2-tagged Global/Region-rung step canvases sampled the coastline +# position raw (no coast_warp_px) — T-1160 applies the warp at orbital +# sampling, changing orbital-rung canvas bytes (elev_q/temperature_dc/ +# moisture_q shift near coasts) — 0.4.3 forces those cached entries to miss. +version: 0.4.3 repository: settled-reach diff --git a/server/src/atlas/district_profile.rs b/server/src/atlas/district_profile.rs index 15f0cda1a..e9d6c05b9 100644 --- a/server/src/atlas/district_profile.rs +++ b/server/src/atlas/district_profile.rs @@ -1928,26 +1928,43 @@ fn derive_at_metres_with_riparian( } /// The orbital-rung derivation (T-1152, zoom ladder design doc §2/§4): the -/// coarse-granularity twin of [`derive_at_metres`] that skips [`invent_primitives`] -/// entirely — **no coastline warp, no detail-scatter octave sum, no classification -/// noise call of any kind**. Per the design doc's orbital row: "`region_baseline_at_district` -/// only — bilinear blend of 4 region baselines, no `invent_primitives`, no -/// classification [driver]." Orbital sample spacing (≥205 km, D-243's region rung -/// and coarser) sits below `detail_scatter`'s own octave floor -/// (`OCTAVE_WAVELENGTHS_M`'s coarsest entry is 32,768 m ≈ 32.8 km — an order of -/// magnitude finer than a region), so the invented terrain has nothing left to -/// contribute at this spacing; calling it would burn cycles synthesizing detail -/// no orbital pixel can resolve. What DOES vary at orbital spacing is the -/// **envelope** the heightmap itself carries (the `TerrainAnalysis` continental -/// shape) and the **region climate baseline** — this function samples exactly -/// those two, nothing else. +/// coarse-granularity twin of [`derive_at_metres`] that skips MOST of +/// [`invent_primitives`] — **no detail-scatter octave sum, no classification +/// noise call of any kind** — but DOES apply the coast-warp position shift +/// (T-1160). Per the design doc's orbital row: "`region_baseline_at_district` +/// only — bilinear blend of 4 region baselines, no [detail-scatter] +/// invention, no classification [driver]." Orbital sample spacing (≥205 km, +/// D-243's region rung and coarser) sits below `detail_scatter`'s own octave +/// floor (`OCTAVE_WAVELENGTHS_M`'s coarsest entry is 32,768 m ≈ 32.8 km — an +/// order of magnitude finer than a region), so the invented RELIEF texture has +/// nothing left to contribute at this spacing; running it would burn cycles +/// synthesizing detail no orbital pixel can resolve. The coastline WARP is +/// different: `WARP_OCTAVE_WAVELENGTHS_M`'s coarsest entries run up to 262,144 m +/// (262 km) — coarser than a region — so the warp is NOT below this rung's +/// resolution floor, and (T-1160 audit) skipping it produced a structurally +/// different coastline position than every finer rung (5.0% land/ocean +/// classification disagreement at coastal-band cells, 267 real bodies; mean +/// displacement ~6.0 km / max ~18.9 km at region spacing — see +/// `derive_orbital_coastline_divergence_audit`). What varies at orbital +/// spacing is therefore the **envelope** the heightmap carries (the +/// `TerrainAnalysis` continental shape), sampled at the **coast-warped +/// position**, plus the **region climate baseline** — this function samples +/// exactly those, nothing else. /// -/// **Cost model (design doc R1 — measure first):** one `bilinear` (elevation), -/// one `bilinear_bool` (ocean mask), one `region_baseline_at_district` call (its -/// own cost is 4×`derive_region_baseline_c` on a cache miss, O(1) on a cache hit) -/// — no octave sum, no coast-warp trig, no character/envelope computation. See -/// `server/tests/zoom_ladder_bench.rs`'s `bench_derive_orbital_at_metres` for the -/// measured per-cell figure this claim rests on. +/// **Cost model (T-1160 audit, measured on 267 real bodies, release build):** +/// the added driver-tier climate + `coast_character_at` + `coast_warp_px` work +/// costs ~784 ns/cell (`coast_warp_px` alone: ~528 ns/cell) — roughly half of +/// what `invent_primitives`' FULL pipeline costs on top of this function's own +/// ~903 ns/cell baseline (`bench_derive_orbital_at_metres_region_spacing`: +/// orbital 902.6 ns/cell vs. full `derive_at_metres` 1940.6 ns/cell at the same +/// spacing — a 2.15x gap, of which detail-scatter/classification-noise is the +/// remainder this function still skips). At the D-255(a) fixed-rung ceiling +/// (3,840×2,160 = 8.3M cells) the added cost is ~6.5 s single-threaded / low +/// hundreds of ms on the row-chunked parallel serving path (T-1151); at a +/// realistic Global-rung Earth-sized-body canvas (195×97 ≈ 18.9K cells) it is +/// ~15 ms. Accepted: coastline correctness at the orbital/Region seam is worth +/// a sub-millisecond-to-low-second cost that scales with (and is bounded by) +/// the same canvas-size ceiling every other rung's cost already respects. /// /// Produces the SAME six-field tail every other rung produces (`morphology_zone`, /// `elev_q`, `temperature_c`, `moisture_q`, `vegetation_class`, `glaciation_grade`) @@ -2011,12 +2028,56 @@ pub fn derive_orbital_at_metres( ..body_params.clone() }; - // The envelope only — no coast-warp, no detail-scatter. This is exactly - // `invent_primitives`' step-1 "driver tier" raw bilinear reads, promoted to - // be the FINAL primitives instead of a one-step-stale input to invention. - let elev_q = + // T-1160: coast-warp the sampling POSITION (steps 1-3 of + // `invent_primitives`), but skip step 4 (slope-independent detail-scatter) + // — the envelope carries no per-cell slope signal at orbital spacing (see + // the function doc's note on `slope_q`, unchanged). Before this fix, this + // function sampled `ta.elev_pct`/`ta.ocean_mask` RAW while District/Quarter + // sampled the SAME arrays at the coast-warped position — a structurally + // different coastline at the orbital-to-district seam (audit: 5.0% + // land/ocean classification disagreement at coastal-band cells across 267 + // real bodies, mean displacement ~6.0 km / max ~18.9 km at region + // spacing). Applying the warp here makes the orbital rung sample the SAME + // invented coastline every finer rung already draws — coarser density, + // same line, not a different line (T-1160). + // + // Driver tier: raw-bilinear reads feed the coast character exactly as + // `invent_primitives` step 1 does (one-step-stale climate driving the + // coast personality, never circular on its own warped output). + let raw_elev_q = ((bilinear(&ta.elev_pct, ta.w, ta.h, px, py) as f64 * 100.0).round() as i32).clamp(0, 100); - let ocean_fraction_q = ((bilinear_bool(&ta.ocean_mask, ta.w, ta.h, px, py) as f64 * 100.0) + let raw_ocean_q = ((bilinear_bool(&ta.ocean_mask, ta.w, ta.h, px, py) as f64 * 100.0).round() + as i32) + .clamp(0, 100); + let driver_params = BodyParams { + elevation_km: (raw_elev_q as f64 / 100.0) * MAX_REGION_ELEVATION_KM, + ..params.clone() + }; + let driver_temp = derive_temperature_c(&driver_params, climate, seed.seed()); + let driver_moisture = derive_moisture_q(&driver_params, raw_elev_q, raw_ocean_q, climate); + let driver_glaciation = derive_glaciation_grade_from_climate(driver_temp, driver_moisture); + + // Character + invented coastline (steps 2-3): same body-personality + // envelope + position character + warp displacement District/Quarter use, + // applied to the SAME `(px, py)` this function already resolved. + let tectonic = derive_tectonic_class(body_params); + let envelope = coast_invention::body_coast_envelope(body_params, tectonic); + let ch = coast_invention::coast_character_at( + &envelope, + seed.seed(), + world_x_m, + world_y_m, + lat_deg, + driver_glaciation, + driver_moisture, + ); + let (wdx, wdy) = coast_invention::coast_warp_px(seed.seed(), world_x_m, world_y_m, &ch, 0.0); + let (spx, spy) = (px + wdx, py + wdy); + + let elev_q = + ((bilinear(&ta.elev_pct, ta.w, ta.h, spx, spy) as f64 * 100.0).round() as i32) + .clamp(0, 100); + let ocean_fraction_q = ((bilinear_bool(&ta.ocean_mask, ta.w, ta.h, spx, spy) as f64 * 100.0) .round() as i32) .clamp(0, 100); // No invented ruggedness at orbital spacing (see the function doc's note @@ -3384,13 +3445,14 @@ mod tests { assert_district_profiles_eq(&a, &b); } - /// The orbital path must NOT run `invent_primitives` — the design doc's - /// central constraint (§2: "no invent_primitives at orbital wavelengths"). - /// Direct proof: `slope_q` is always exactly 0 (invention is the only - /// source of nonzero slope_q at this call depth — see - /// `derive_orbital_at_metres`'s doc on why slope_q is fixed), sampled - /// across enough distinct positions that a nonzero value appearing even - /// once would falsify the claim. + /// The orbital path must NOT run `invent_primitives`'s detail-scatter/ + /// relief step (step 4) — the design doc's central constraint (§2: "no + /// [relief] invention at orbital wavelengths"), even though it DOES now + /// run steps 1-3 (coast-warp, T-1160). Direct proof: `slope_q` is always + /// exactly 0 (detail-scatter is the only source of nonzero slope_q at + /// this call depth — see `derive_orbital_at_metres`'s doc on why slope_q + /// is fixed), sampled across enough distinct positions that a nonzero + /// value appearing even once would falsify the claim. #[test] fn derive_orbital_at_metres_never_invents_slope() { let hm = test_hm(); @@ -3505,6 +3567,251 @@ mod tests { let _ = prof.glaciation_grade; } + /// T-1160 audit (historical record — the fix landed; this test documents + /// the numbers that justified it and stays as a standing measurement, not + /// a regression gate with a hardcoded threshold). Quantifies the + /// orbital-vs-district coastline positional divergence that existed + /// BEFORE this ticket: `derive_orbital_at_metres` used to sample + /// `ta.ocean_mask` raw (no `coast_warp_px`) while District/Quarter warped + /// the same mask first (`invent_primitives` step 3). This test + /// independently reimplements both the pre-fix raw read (a) and the + /// District-style warped read (b) — deliberately NOT calling + /// `derive_orbital_at_metres` itself, so the measurement stays valid as a + /// historical baseline regardless of that function's current + /// implementation — loads every real `heightmap.png` under + /// `wiki/star-systems/`, and for a grid of region-spacing sample points on + /// each body reports how often (a) and (b) disagree on land/ocean + /// classification, the warp displacement converted to real metres, and + /// the per-cell cost of computing (b). These are the numbers T-1160's + /// fix (applying the warp at orbital sampling, see + /// `derive_orbital_at_metres`'s doc) is based on. `#[ignore]`d — reads + /// ~267 real wiki heightmap files, run explicitly: + /// `cargo test --release -p settled-reach-server derive_orbital_coastline_divergence_audit -- --ignored --nocapture` + #[test] + #[ignore] + fn derive_orbital_coastline_divergence_audit() { + use std::path::Path; + use std::time::Instant; + + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../wiki/star-systems"); + let mut heightmaps: Vec = Vec::new(); + collect_heightmaps(&root, &mut heightmaps); + heightmaps.sort(); + assert!( + !heightmaps.is_empty(), + "expected to find real heightmap.png files under {:?}", + root + ); + + let climate = ClimateConstants::default(); + let seed = test_seed(); + let region_m = scale::REGION_M as f64; + + let mut bodies_sampled = 0usize; + let mut total_cells = 0u64; + let mut disagreements = 0u64; + let mut max_displacement_m = 0.0f64; + let mut sum_displacement_m = 0.0f64; + let mut displacement_samples = 0u64; + + let mut warp_evals = 0u64; + let mut warp_only_nanos = 0u64; + let mut warp_plus_character_nanos = 0u64; + + for hm_path in &heightmaps { + let Ok(file) = std::fs::File::open(hm_path) else { + continue; + }; + let Ok(hm) = crate::atlas::heightmap::load_heightmap_reader(file, "audit", 0.4) else { + continue; + }; + if hm.width == 0 || hm.height == 0 { + continue; + } + let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level); + let ta = TerrainAnalysis::analyze(&hm, &dr); + + let body_params = BodyParams { + hydrosphere: Some("ocean".into()), + atmosphere: Some("breathable".into()), + planet_class: Some("temperate".into()), + body_radius_km: Some(6371.0), + ..Default::default() + }; + let circumference_m = std::f64::consts::TAU * 6371.0 * 1000.0; + let meridian_m = std::f64::consts::PI * 6371.0 * 1000.0; + + let tectonic = derive_tectonic_class(&body_params); + let envelope = coast_invention::body_coast_envelope(&body_params, tectonic); + + // Sample on a region-spacing grid across the whole body — same + // spacing the orbital/Region rung actually renders at. + let cols = (circumference_m / region_m).floor() as i64; + let rows = (meridian_m / region_m).floor() as i64; + if cols < 1 || rows < 1 { + continue; + } + + bodies_sampled += 1; + + for row in 0..rows { + for col in 0..cols { + let wx = col as f64 * region_m; + let wy = row as f64 * region_m; + + let px = (wx / circumference_m).rem_euclid(1.0) * ta.w as f64; + let lat_frac = (wy / meridian_m).clamp(-0.5, 0.5); + let py = (0.5 + lat_frac) * ta.h.saturating_sub(1) as f64; + let lat_deg = -lat_frac * 180.0; + + // (a) raw orbital read — today's derive_orbital_at_metres. + let raw_ocean_q = ((bilinear_bool(&ta.ocean_mask, ta.w, ta.h, px, py) as f64 + * 100.0) + .round() as i32) + .clamp(0, 100); + + // Only worth measuring displacement near a coastline — a + // cell deep in open ocean or deep inland can't flip + // classification no matter the (sub-pixel) warp, so + // restrict the expensive per-cell warp eval + metre + // conversion to the coastal band (0 < raw_ocean_q < 100 + // OR any 4-neighbor disagrees) — this is also exactly + // the "coastal cell" population the ticket's cost + // estimate ("~one warp eval per coastal cell") means. + let is_coastal_band = { + let n = bilinear_bool(&ta.ocean_mask, ta.w, ta.h, px, py - 1.0) > 0.5; + let s = bilinear_bool(&ta.ocean_mask, ta.w, ta.h, px, py + 1.0) > 0.5; + let e = bilinear_bool(&ta.ocean_mask, ta.w, ta.h, px + 1.0, py) > 0.5; + let w = bilinear_bool(&ta.ocean_mask, ta.w, ta.h, px - 1.0, py) > 0.5; + let here = raw_ocean_q > 50; + here != n || here != s || here != e || here != w + }; + if !is_coastal_band { + continue; + } + + total_cells += 1; + + // (b) District-style warped read at the SAME position — + // mirrors invent_primitives' driver-tier + character + + // warp steps, using the real per-body envelope. Timed + // separately from the file-I/O/drainage/TerrainAnalysis + // setup above — this is the actual per-cell cost T-1160 + // would add at orbital sampling, not body-load overhead. + let cell_start = Instant::now(); + let raw_elev_q = ((bilinear(&ta.elev_pct, ta.w, ta.h, px, py) as f64 * 100.0) + .round() as i32) + .clamp(0, 100); + let driver_params = BodyParams { + elevation_km: (raw_elev_q as f64 / 100.0) * MAX_REGION_ELEVATION_KM, + latitude_deg: lat_deg, + ..body_params.clone() + }; + let driver_temp = + derive_temperature_c(&driver_params, &climate, seed.seed()); + let driver_moisture = + derive_moisture_q(&driver_params, raw_elev_q, raw_ocean_q, &climate); + let driver_glaciation = + derive_glaciation_grade_from_climate(driver_temp, driver_moisture); + + let ch = coast_invention::coast_character_at( + &envelope, + seed.seed(), + wx, + wy, + lat_deg, + driver_glaciation, + driver_moisture, + ); + warp_plus_character_nanos += cell_start.elapsed().as_nanos() as u64; + + warp_evals += 1; + let warp_call_start = Instant::now(); + let (wdx, wdy) = coast_invention::coast_warp_px(seed.seed(), wx, wy, &ch, 0.0); + warp_only_nanos += warp_call_start.elapsed().as_nanos() as u64; + warp_plus_character_nanos += warp_call_start.elapsed().as_nanos() as u64; + let (spx, spy) = (px + wdx, py + wdy); + let warped_ocean_q = ((bilinear_bool(&ta.ocean_mask, ta.w, ta.h, spx, spy) + as f64 + * 100.0) + .round() as i32) + .clamp(0, 100); + + if (raw_ocean_q > 50) != (warped_ocean_q > 50) { + disagreements += 1; + } + + // Displacement converted to real metres at this body's + // equator-circumference-derived px scale (constant across + // latitude for an equirectangular map's column spacing; + // using the equatorial scale is the conservative / + // largest-metres-per-pixel case for row spacing too since + // meridian_m/ta.h <= circumference_m/ta.w for ta.h<=ta.w/2). + let m_per_px = circumference_m / ta.w as f64; + let disp_px = (wdx * wdx + wdy * wdy).sqrt(); + let disp_m = disp_px * m_per_px; + sum_displacement_m += disp_m; + displacement_samples += 1; + if disp_m > max_displacement_m { + max_displacement_m = disp_m; + } + } + } + } + let mean_displacement_m = if displacement_samples > 0 { + sum_displacement_m / displacement_samples as f64 + } else { + 0.0 + }; + let disagreement_pct = if total_cells > 0 { + 100.0 * disagreements as f64 / total_cells as f64 + } else { + 0.0 + }; + let ns_per_warp_call = if warp_evals > 0 { + warp_only_nanos as f64 / warp_evals as f64 + } else { + 0.0 + }; + let ns_per_full_cell_cost = if warp_evals > 0 { + warp_plus_character_nanos as f64 / warp_evals as f64 + } else { + 0.0 + }; + + eprintln!("=== T-1160 orbital coastline divergence audit ==="); + eprintln!("bodies sampled: {bodies_sampled}"); + eprintln!("coastal-band cells sampled: {total_cells}"); + eprintln!("land/ocean disagreements: {disagreements} ({disagreement_pct:.2}%)"); + eprintln!("mean warp displacement: {mean_displacement_m:.1} m"); + eprintln!("max warp displacement: {max_displacement_m:.1} m"); + eprintln!( + "coast_warp_px call cost: {ns_per_warp_call:.1} ns/call ({warp_evals} calls, {warp_only_nanos} ns total)" + ); + eprintln!( + "full added cost/cell: {ns_per_full_cell_cost:.1} ns/cell (driver temp/moisture/glaciation + coast_character_at + coast_warp_px)" + ); + + // Sanity floor — this audit is meaningless if it silently sampled + // zero real bodies or zero coastal cells (e.g. a bad path). + assert!(bodies_sampled > 10, "too few bodies sampled — check path"); + assert!(total_cells > 0, "no coastal-band cells found — check masks"); + } + + fn collect_heightmaps(dir: &std::path::Path, out: &mut Vec) { + let Ok(entries) = std::fs::read_dir(dir) else { + return; + }; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + collect_heightmaps(&path, out); + } else if path.file_name().and_then(|n| n.to_str()) == Some("heightmap.png") { + out.push(path); + } + } + } + #[test] fn derive_district_profile_is_deterministic() { let hm = test_hm(); diff --git a/server/tests/golden/window_derivation_golden.json b/server/tests/golden/window_derivation_golden.json index fa1678e81..f53c9f583 100644 --- a/server/tests/golden/window_derivation_golden.json +++ b/server/tests/golden/window_derivation_golden.json @@ -95,10 +95,10 @@ "glaciation": 0, "precipitation": 3, "slope_q": 0, - "elev_q": 26, + "elev_q": 27, "ocean_fraction_q": 0, - "temperature_dc": 127, - "moisture_q": 57, + "temperature_dc": 122, + "moisture_q": 56, "vegetation": 3 }, { @@ -248,7 +248,7 @@ "glaciation": 2, "precipitation": 1, "slope_q": 0, - "elev_q": 62, + "elev_q": 63, "ocean_fraction_q": 0, "temperature_dc": -120, "moisture_q": 43, @@ -452,7 +452,7 @@ "glaciation": 0, "precipitation": 0, "slope_q": 0, - "elev_q": 41, + "elev_q": 42, "ocean_fraction_q": 0, "temperature_dc": -2147483648, "moisture_q": 0, @@ -503,7 +503,7 @@ "glaciation": 0, "precipitation": 0, "slope_q": 0, - "elev_q": 42, + "elev_q": 43, "ocean_fraction_q": 0, "temperature_dc": -2147483648, "moisture_q": 0, @@ -707,9 +707,9 @@ "glaciation": 0, "precipitation": 3, "slope_q": 0, - "elev_q": 28, + "elev_q": 29, "ocean_fraction_q": 0, - "temperature_dc": 728, + "temperature_dc": 723, "moisture_q": 59, "vegetation": 3 }, @@ -809,9 +809,9 @@ "glaciation": 0, "precipitation": 2, "slope_q": 0, - "elev_q": 23, + "elev_q": 24, "ocean_fraction_q": 0, - "temperature_dc": 697, + "temperature_dc": 691, "moisture_q": 49, "vegetation": 3 }, @@ -860,10 +860,10 @@ "glaciation": 0, "precipitation": 2, "slope_q": 0, - "elev_q": 66, + "elev_q": 67, "ocean_fraction_q": 0, "temperature_dc": 300, - "moisture_q": 30, + "moisture_q": 29, "vegetation": 2 }, {