planetsim/src/sim/PlanetErosion.cpp
Jonas Reith 4aacfdabdd Soft probabilistic peak cap: spread mountain heights, drop the 9000 m plateau
Drift-time peaks used to rail into the hard elevation clamp and flatten into a
9000 m plateau (the clamp lived in step(), erode() AND hydrology(), so erosion
re-flattened them every tick). Replace the hard ceiling with a probabilistic
soft cap: above peakSoftCapStart (7000 m) the chance a tick's uplift "takes"
falls linearly to 0 at peakSoftCapEnd (12000 m); a lost grow roll forfeits the
uplift and shaves a random 0..peakFailDrop (200 m) off. Peaks now spread smoothly
across a height band (strong orogeny reaches ~10-11 km, most cluster lower) with
zero cells pinned at the ceiling.

- The roll is a pure hash of (cellIndex, erodeIter, seed): never touches
  rngState, bit-identical across OpenMP thread counts, and since erodeIter is
  saved (step/erode run 1:1 in drift) F5/F9 resumes bit-identical -- no save bump.
- Drift-only (gated on Planet::drifting) so Phase-1 forming still auto-settles.
- The hard clamp's upper bound now tracks peakSoftCapEnd in all three places
  (step/erode/hydrology); lower -11000 m unchanged.
- New planet.cfg knobs peakSoftCapStart / peakSoftCapEnd / peakFailDrop with
  validation (+ start < end cross-rule). Docs updated (CLAUDE.md, BUILD.md).

Verified headless: smooth 8.5->10.5 km taper, 0 pinned, determinism + exact
resume intact, test_logic + test_biota pass, full app builds clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 22:26:38 +02:00

61 lines
3.0 KiB
C++

#include "Planet.hpp"
#include <algorithm>
#include <cmath>
// --- Phase 2: erosion + sea level -------------------------------------------
// Mass-conserving downhill sediment transport: along each edge the higher cell
// gives material to the lower one, faster above sea level (subaerial weathering)
// than below (slow submarine). Highs wear down toward an uplift<->erosion
// equilibrium; the sediment piles in basins and below sea level, building shelves
// and deltas and slowly filling trenches. One double-buffered gather pass (each
// cell writes only its own index -> bit-identical for any thread count).
void Planet::erode(double dtMy) {
const int n = (int)cells.size();
sErode.resize(n);
const double sea = cfg.seaLevel;
#pragma omp parallel for schedule(static) if(n > 20000)
for (int i = 0; i < n; ++i) {
double ei = cells[i].elevation, delta = 0.0;
for (int j : cells[i].neighbors) {
double ej = cells[j].elevation;
if (ei > ej) { // i is higher: i -> j outflow
double rate = (ei > sea) ? cfg.erosionLandRate : cfg.erosionSeaRate;
delta -= std::min(rate * dtMy, 0.5) * (ei - ej);
} else if (ej > ei) { // j is higher: j -> i inflow
double rate = (ej > sea) ? cfg.erosionLandRate : cfg.erosionSeaRate;
delta += std::min(rate * dtMy, 0.5) * (ej - ei);
}
}
sErode[i] = delta;
}
// Upper clamp tracks peakSoftCapEnd (matches step()); the soft peak cap, not a
// fixed 9000 m wall here, governs how high mountains stand.
#pragma omp parallel for schedule(static) if(n > 20000)
for (int i = 0; i < n; ++i)
cells[i].elevation = std::clamp(cells[i].elevation + sErode[i], -11000.0, cfg.peakSoftCapEnd);
if (++erodeIter % cfg.seaLevelEvery == 0) adjustSeaLevel();
}
// Gradual eustatic controller: if the geographic land fraction is outside a
// deadband around the target, nudge sea level by a small fixed step (raising it
// floods land, lowering exposes it). Called infrequently (seaLevelEvery) so the
// coastline drifts slowly, not in a jump. A nudge is only taken if it actually
// reduces the error -- otherwise (e.g. a big mass of cells at one elevation, where
// a full step would overshoot) it rests at the closest a fixed step allows instead
// of oscillating back and forth across that "cliff".
void Planet::adjustSeaLevel() {
const int n = (int)cells.size();
if (n == 0) return;
auto landFracAt = [&](double sl) {
int a = 0; for (const auto& c : cells) if (c.elevation > sl) ++a; return (double)a / n;
};
double err = landFracAt(cfg.seaLevel) - cfg.landFractionTarget;
if (std::fabs(err) <= cfg.seaLevelTol) return; // within deadband: rest
double cand = cfg.seaLevel + (err > 0 ? cfg.seaLevelStep : -cfg.seaLevelStep);
double candErr = landFracAt(cand) - cfg.landFractionTarget;
if (std::fabs(candErr) < std::fabs(err)) cfg.seaLevel = cand; // nudge only if it helps
}