The coarse and fine sampling loops both touched h=0.10 and the two doubles round to the same float32, so the strict check compared the identity value against itself and threw on every curve-on run. Only strictly increasing float samples are compared now. The curve itself was never non-monotonic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
4.4 KiB
C#
112 lines
4.4 KiB
C#
using Godot;
|
|
|
|
/// <summary>
|
|
/// The height-redistribution curve (terrain-water task 05, graduation M-7) — a pure,
|
|
/// static, monotonic piecewise map over raw blueprint heights (D-035: numbers in,
|
|
/// numbers out, no lifecycle).
|
|
///
|
|
/// OUTPUT bands are fixed by design (the storm ladder; developer-approved
|
|
/// 75 % orange coverage / plateau 50 m above sea / peaks 220 m above sea).
|
|
/// INPUT knots were calibrated ONCE from measured data — the pooled CDF of
|
|
/// above-sea land heights across batch 04's ten flat-sea seeds (340,618,126
|
|
/// samples, 2026-08-07): P75 / P90 / P93 / P96 / max. The same knots apply to
|
|
/// every seed; per-seed band proportions vary a few points by design.
|
|
///
|
|
/// Shape, monotonic by construction (every segment's normalized slope is bounded
|
|
/// below by a positive constant) and asserted numerically at startup:
|
|
/// h ≤ sea (0.15) identity — water and the below-sea world untouched
|
|
/// sea → t1 smooth toe, ease-out blend (gentle rolling, never flat)
|
|
/// t1 → t2 linear rise into the red band
|
|
/// t2 → t3 smooth shoulder up to the plateau shelf
|
|
/// t3 → t4 near-flat plateau step (small positive slope)
|
|
/// t4 → hmaxCal accelerating spike to the peak cap
|
|
/// h > hmaxCal linear tail (keeps strict monotonicity, no clamp)
|
|
/// </summary>
|
|
public static class HeightCurve
|
|
{
|
|
public const ushort VERSION = 1;
|
|
|
|
// Input knots — calibrated from batch 04 B-flat pooled land CDF (see report).
|
|
public const float T1 = 0.628736f; // P75 — orange coverage boundary
|
|
public const float T2 = 0.819152f; // P90
|
|
public const float T3 = 0.879340f; // P93
|
|
public const float T4 = 0.962922f; // P96
|
|
public const float HMAX_CAL = 1.452219f; // pooled max
|
|
|
|
// Output bands — the storm ladder.
|
|
public const float SEA = 0.15f;
|
|
public const float ORANGE_CEIL = 0.206f; // 1000-yr storm ceiling
|
|
public const float RED_CEIL = 0.27f; // biblical ceiling
|
|
public const float PLATEAU_LO = SEA + 50f / 251f; // ≈ 0.34924 (50 m above sea)
|
|
public const float PLATEAU_HI = PLATEAU_LO + 0.02f; // ≈ 0.36924 (~5 m step relief)
|
|
public const float PEAK_CAP = SEA + 220f / 251f; // ≈ 1.02649 (220 m above sea)
|
|
public const float TAIL_SLOPE = 0.25f; // above HMAX_CAL
|
|
|
|
public static float Apply(float h)
|
|
{
|
|
if (h <= SEA) return h;
|
|
|
|
float u, s;
|
|
if (h < T1)
|
|
{
|
|
u = (h - SEA) / (T1 - SEA);
|
|
s = 0.3f * u + 0.7f * (u * (2f - u)); // ease-out, slope ≥ 0.3
|
|
return SEA + s * (ORANGE_CEIL - SEA);
|
|
}
|
|
if (h < T2)
|
|
{
|
|
u = (h - T1) / (T2 - T1);
|
|
return ORANGE_CEIL + u * (RED_CEIL - ORANGE_CEIL); // linear
|
|
}
|
|
if (h < T3)
|
|
{
|
|
u = (h - T2) / (T3 - T2);
|
|
s = 0.2f * u + 0.8f * (u * u * (3f - 2f * u)); // smoothstep blend, slope ≥ 0.2
|
|
return RED_CEIL + s * (PLATEAU_LO - RED_CEIL);
|
|
}
|
|
if (h < T4)
|
|
{
|
|
u = (h - T3) / (T4 - T3);
|
|
return PLATEAU_LO + u * (PLATEAU_HI - PLATEAU_LO); // near-flat, small positive slope
|
|
}
|
|
if (h < HMAX_CAL)
|
|
{
|
|
u = (h - T4) / (HMAX_CAL - T4);
|
|
s = 0.2f * u + 0.8f * (u * u * u); // ease-in spike, slope ≥ 0.2
|
|
return PLATEAU_HI + s * (PEAK_CAP - PLATEAU_HI);
|
|
}
|
|
return PEAK_CAP + (h - HMAX_CAL) * TAIL_SLOPE;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Numeric strict-monotonicity check across the whole plausible domain.
|
|
/// Cheap (runs once at generator start); a violation is a build bug, not a
|
|
/// data condition — fail loudly and refuse to generate.
|
|
/// </summary>
|
|
public static void AssertMonotonic()
|
|
{
|
|
float prevH = -7f;
|
|
float prev = Apply(prevH);
|
|
|
|
// Successive double samples can round to the SAME float32 (the two loops
|
|
// meeting at 0.10 did exactly that and tripped the strict check against
|
|
// itself) — so only strictly increasing float samples are compared.
|
|
void Check(double hd)
|
|
{
|
|
float h = (float)hd;
|
|
if (h <= prevH) return;
|
|
float v = Apply(h);
|
|
if (v <= prev)
|
|
throw new System.InvalidOperationException(
|
|
$"[HeightCurve] MONOTONICITY VIOLATION at h={h}: {v} <= {prev}. Refusing to generate.");
|
|
prev = v;
|
|
prevH = h;
|
|
}
|
|
|
|
// Coarse below the identity region, fine through every knot, out past the tail.
|
|
for (double h = -7.0 + 0.01; h < 0.10; h += 0.01) Check(h);
|
|
for (double h = 0.10; h <= 2.0; h += 0.0001) Check(h);
|
|
for (double h = 2.05; h <= 8.0; h += 0.05) Check(h);
|
|
GD.Print("[HeightCurve] Monotonicity assertion passed (v" + VERSION + ").");
|
|
}
|
|
}
|