fix(simulation): lake_margin_q normalizes per basin — full-range depth gradient (T-1188)

The PR #206 eyeball caught what every numeric gate passed: the depth
signal was visually flat on both test lakes (GJ1c lmq=0 across the whole
basin; GJ338Bd 0-13 of 100). Two compounding causes: a fixed absolute
ceiling (one body's p90 cell depth) compressing skewed depth
distributions into single digits, and heightmap-pitch depth variation
being sub-texel-tiny within most basins. lake_margin_q is now
depth / the basin's own maximum settled depth: HydrologyResult grows
basin_max_depth_scaled (computed in solve() from existing basin_cells
membership, broadcast per basin), threaded through
HydrologySample.basin_max_depth, normalized in lake_from_hydrology_at
with a degenerate-basin epsilon guard (a genuinely uniform pond shades
flat — honest, not forced). Lake EXISTENCE (filled > original) is
untouched — only tone changes. Measured at district spacing:
GJ1c min=0 p50=33 max=84; GJ338Bd min=8 p50=38 max=70 — full-range
shore-to-deep ramps on both. No perceptual curve added: the linear
per-basin ramp is already well-quartiled. project.yaml 0.4.1 -> 0.4.2
(0.4.1-tagged canvases carrying flat-lmq semantics reached real disk
caches during eyeball runs and must miss). Acceptance gates green;
zero golden churn (lake_margin_q not captured by either golden shape).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 14:23:11 +02:00
co-authored by Claude Fable 5
parent b39dd49950
commit 39f0fd8c51
5 changed files with 124 additions and 42 deletions
+14
View File
@@ -134,6 +134,14 @@ pub struct HydrologySample {
/// water-surface height at every working-grid cell (equal to
/// `elevation` wherever no lake exists).
pub filled: Vec<f32>,
/// `HydrologyResult.basin_max_depth_scaled`, rescaled back to `[0.0,
/// 1.0]` fraction units (T-1188): the MAXIMUM settled depth anywhere in
/// this cell's basin, broadcast to every cell in that basin, `0.0` for
/// non-lake cells. Used to normalize `lake_margin_q` per-basin instead
/// of against a single fixed absolute ceiling — see
/// `district_profile::lake_from_hydrology_at`'s doc for the full
/// rationale (the PR #206 eyeball finding that motivated this field).
pub basin_max_depth: Vec<f32>,
}
const WATER_DIST_CAP: u16 = 255;
@@ -215,9 +223,15 @@ impl TerrainAnalysis {
.iter()
.map(|&s| crate::atlas::hydrology_equilibrium::scaled_to_fraction(s))
.collect();
let basin_max_depth: Vec<f32> = result
.basin_max_depth_scaled
.iter()
.map(|&s| crate::atlas::hydrology_equilibrium::scaled_to_fraction(s))
.collect();
self.hydrology = Some(HydrologySample {
elevation: elevation.to_vec(),
filled,
basin_max_depth,
});
self
}