Reported after a real long-running world (elapsedMy ~1340) showed seaLevel drifted to -2900m, with "land" at -2000m next to "ocean" at -4000m. Root cause: adjustSeaLevel()'s geographic land target (elevation above seaLevel, fixed 30%) and advect()'s crust-type land conservation (targetLand/landBand, held near whatever Phase-1 forming settled at for that seed) are two independent notions of "land" that aren't guaranteed to agree -- this seed's continental crust settled at only ~26%. With genuine land structurally short of the target, the controller had no lower bound and kept sinking seaLevel to misclassify progressively older, deeper oceanic crust as land -- and since that crust keeps ageing and deepening even at a fixed seaLevel, it was chasing a moving target with no way to ever settle. Added seaLevelMin/seaLevelMax config fields (default +-3000m) that clamp adjustSeaLevel()'s candidate nudge, so a world that can't reach the target land fraction settles at a plausible offset instead of an unbounded one. New test_sealevel.cpp reproduces the pathology with a continuous elevation distribution (not a synthetic cliff) and confirms the controller still engages but never crosses either bound. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
104 lines
5.5 KiB
C++
104 lines
5.5 KiB
C++
// Headless test for the sea-level controller's runaway guard (no display needed).
|
|
//
|
|
// g++ -std=c++17 -O2 -Isrc/sim test_sealevel.cpp src/sim/IcoSphere.cpp
|
|
// src/sim/Planet.cpp src/sim/PlanetTectonics.cpp src/sim/PlanetDrift.cpp
|
|
// src/sim/PlanetErosion.cpp src/sim/PlanetHydrology.cpp
|
|
// src/sim/PlanetBiomes.cpp src/sim/PlanetClimate.cpp src/sim/PlanetLive.cpp
|
|
// src/sim/PlanetOcean.cpp src/sim/PlanetBiota.cpp src/sim/PlanetFloraGen.cpp
|
|
// src/sim/PlanetFaunaGen.cpp src/sim/PlanetFungiGen.cpp src/sim/PlanetIO.cpp
|
|
// -o /tmp/ts && /tmp/ts
|
|
//
|
|
// Reproduces the bug reported after a long real run: on a world whose buoyant
|
|
// (continental) crust area permanently falls short of landFractionTarget (crust
|
|
// generation is independent of that geographic target -- nothing guarantees they
|
|
// match), the old unbounded adjustSeaLevel() had no way to reach the target using
|
|
// real land, so it kept sinking seaLevel to "poach" ageing oceanic crust as fake
|
|
// land -- and since that crust keeps deepening with age, the target kept slipping
|
|
// away, sinking seaLevel indefinitely (observed: -2900 m after ~1340 My, still
|
|
// falling). Verifies the seaLevelMin/seaLevelMax clamp stops the runaway in both
|
|
// directions, that it actually engages (proving the mechanism still adjusts, not
|
|
// just trivially idle), and that a normal, non-pathological world never comes
|
|
// close to the clamp.
|
|
|
|
#include "Planet.hpp"
|
|
#include <cstdio>
|
|
#include <cmath>
|
|
|
|
static int failures = 0;
|
|
static void check(bool cond, const char* what) {
|
|
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
|
if (!cond) ++failures;
|
|
}
|
|
|
|
int main() {
|
|
std::printf("Sea level: runaway guard\n");
|
|
|
|
// --- Scenario 1: continental crust structurally short of the target -------
|
|
// 25% of cells continental near continentBase, 75% oceanic spread smoothly
|
|
// across a wide, continuous depth range (mimicking a real mix of young and
|
|
// long-ageing seafloor, not a cliff) -- deliberately short of the 30% default
|
|
// landFractionTarget. With a continuous distribution, each single seaLevelStep
|
|
// nudge always captures more cells and strictly reduces the error, so the
|
|
// pre-fix controller would keep taking that nudge forever, sinking without end.
|
|
{
|
|
PlanetConfig cfg; cfg.subdivisions = 3; cfg.seed = 99;
|
|
Planet planet; planet.generate(cfg);
|
|
const int n = (int)planet.cells.size();
|
|
for (int i = 0; i < n; ++i) {
|
|
bool continental = (i % 4) == 0; // 25% < landFractionTarget (30%)
|
|
planet.setCrust(i, continental);
|
|
if (continental) planet.setElevation(i, planet.cfg.continentBase + (i % 7) * 10.0);
|
|
else planet.setElevation(i, -10.0 - 8000.0 * (double)i / n); // continuous -10..-8010 m
|
|
}
|
|
double startSeaLevel = planet.cfg.seaLevel;
|
|
// erode()'s transport is dtMy-scaled; dt=0 isolates the periodic
|
|
// adjustSeaLevel() call (still triggered by the iteration counter) from
|
|
// any elevation change, so the crafted distribution above stays intact.
|
|
for (int i = 0; i < planet.cfg.seaLevelEvery * 400; ++i) planet.erode(0.0);
|
|
|
|
check(planet.cfg.seaLevel < startSeaLevel - 100.0,
|
|
"short-of-target land engages the controller (seaLevel moves down)");
|
|
check(planet.cfg.seaLevel >= planet.cfg.seaLevelMin,
|
|
"seaLevel never sinks past seaLevelMin (the pre-fix bug: it had no floor)");
|
|
check(planet.cfg.seaLevel <= planet.cfg.seaLevelMax, "seaLevel stays within the upper bound too");
|
|
}
|
|
|
|
// --- Scenario 2: mirror case, land far in excess of the target ------------
|
|
{
|
|
PlanetConfig cfg; cfg.subdivisions = 3; cfg.seed = 100;
|
|
Planet planet; planet.generate(cfg);
|
|
const int n = (int)planet.cells.size();
|
|
for (int i = 0; i < n; ++i) {
|
|
bool continental = (i % 4) != 0; // 75% >> landFractionTarget (30%)
|
|
planet.setCrust(i, continental);
|
|
if (continental) planet.setElevation(i, 10.0 + 8000.0 * (double)i / n); // continuous 10..8010 m
|
|
else planet.setElevation(i, planet.cfg.oceanBase + (i % 7) * 10.0);
|
|
}
|
|
double startSeaLevel = planet.cfg.seaLevel;
|
|
for (int i = 0; i < planet.cfg.seaLevelEvery * 400; ++i) planet.erode(0.0);
|
|
|
|
check(planet.cfg.seaLevel > startSeaLevel + 100.0,
|
|
"excess land engages the controller (seaLevel moves up)");
|
|
check(planet.cfg.seaLevel <= planet.cfg.seaLevelMax,
|
|
"seaLevel never climbs past seaLevelMax (mirror of the sinking bug)");
|
|
check(planet.cfg.seaLevel >= planet.cfg.seaLevelMin, "seaLevel stays within the lower bound too");
|
|
}
|
|
|
|
// --- Scenario 3: a normal, freshly-generated world never approaches the ---
|
|
// clamp -- the guard is a safety rail for the pathological case, not
|
|
// something that interferes with ordinary sea-level settling.
|
|
{
|
|
PlanetConfig cfg; cfg.subdivisions = 4; cfg.seed = 42;
|
|
Planet planet; planet.generate(cfg);
|
|
for (int i = 0; i < 40; ++i) planet.step(); // settle Phase-1 relief first
|
|
planet.drifting = true;
|
|
for (int i = 0; i < planet.cfg.seaLevelEvery * 20; ++i) planet.erode(planet.cflDtMy());
|
|
|
|
check(planet.cfg.seaLevel > planet.cfg.seaLevelMin * 0.5 && planet.cfg.seaLevel < planet.cfg.seaLevelMax * 0.5,
|
|
"a normal world's sea level settles well clear of the clamp bounds");
|
|
}
|
|
|
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL SEA LEVEL CHECKS PASSED\n", failures);
|
|
return failures ? 1 : 0;
|
|
}
|