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>
This commit is contained in:
parent
53e371bb2f
commit
4aacfdabdd
3
BUILD.md
3
BUILD.md
@ -99,6 +99,9 @@ Phase 1 forms as before; tall mountains grow during Phase-2 drift.
|
|||||||
arcFactor 1.4 continental subduction-arc uplift (Andes)
|
arcFactor 1.4 continental subduction-arc uplift (Andes)
|
||||||
isostaticPersist 0.85 how strongly high crust resists relax (0..<1)
|
isostaticPersist 0.85 how strongly high crust resists relax (0..<1)
|
||||||
rootScale 2500 m above continentBase where persistence saturates
|
rootScale 2500 m above continentBase where persistence saturates
|
||||||
|
peakSoftCapStart 7000 m below this a tick's uplift always takes (P=1)
|
||||||
|
peakSoftCapEnd 12000 m at/above this uplift never takes (P=0); also the elev clamp
|
||||||
|
peakFailDrop 200 m max random drop when the grow roll loses (drift-only)
|
||||||
ridgeDepth -2500 m shallow elevation of brand-new crust at a ridge
|
ridgeDepth -2500 m shallow elevation of brand-new crust at a ridge
|
||||||
seafloorSubsidence 280 m per sqrt(My): seafloor deepens with crustal age
|
seafloorSubsidence 280 m per sqrt(My): seafloor deepens with crustal age
|
||||||
seafloorSeedAge 80 My initial oceanic age spread at generation
|
seafloorSeedAge 80 My initial oceanic age spread at generation
|
||||||
|
|||||||
21
CLAUDE.md
21
CLAUDE.md
@ -535,9 +535,24 @@ triangles (plates are fixed in phase 1).
|
|||||||
toward 1 = ranges barely erode and continents stay high; lower = more dynamic
|
toward 1 = ranges barely erode and continents stay high; lower = more dynamic
|
||||||
rise/erode) saturating at `rootScale` (2500 m above `continentBase`). These three
|
rise/erode) saturating at `rootScale` (2500 m above `continentBase`). These three
|
||||||
boosts are **drift-only** (gated on `Planet::drifting`); in Phase-2 drift the
|
boosts are **drift-only** (gated on `Planet::drifting`); in Phase-2 drift the
|
||||||
per-tick `erode()` is what limits their height, so they don't rail the clamp. The
|
per-tick `erode()` is what limits their height, so they don't rail the clamp.
|
||||||
hard clamp `[-11000, 9000]` in `step()` is the Everest-class cap; a few peak
|
- **Soft peak cap (drift-only) — no 9000 m plateau:** previously the hard
|
||||||
cells may sit there during drift (<2% — fine).
|
`[-11000, 9000]` clamp (in `step()`, `erode()` *and* `hydrology()`) flattened many
|
||||||
|
drift-time peaks into a 9000 m plateau. Now growth is **probabilistic** above
|
||||||
|
`peakSoftCapStart` (7000 m): the chance a tick's positive uplift "takes" falls
|
||||||
|
linearly to 0 at `peakSoftCapEnd` (12000 m), so peaks spread smoothly across a
|
||||||
|
height band instead of railing at one ceiling. A **lost** grow roll forfeits that
|
||||||
|
tick's uplift *and* shaves a random `0..peakFailDrop` (200 m) off, so a peak hovers
|
||||||
|
near its own height (height now tracks orogeny strength: strong seeds reach
|
||||||
|
10–11 km, most cluster lower). The hard clamp's **upper bound now tracks
|
||||||
|
`peakSoftCapEnd`** in all three places (just a safety rail; lower −11000 m
|
||||||
|
unchanged). The roll is a **pure hash of `(cellIndex, erodeIter, seed)`** — never
|
||||||
|
touches `rngState`, stays 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-version bump). **Drift-only** (gated on `drifting`) so Phase-1 forming
|
||||||
|
still auto-settles. Tune `peakSoftCapStart` / `peakSoftCapEnd` / `peakFailDrop` in
|
||||||
|
`planet.cfg`. Verified headless: smooth 8.5→10.5 km taper, 0 cells pinned at the
|
||||||
|
ceiling, determinism + exact resume intact.
|
||||||
- **Seafloor aging->depth (PlanetConfig, Phase 2 inc. 4):** `oceanBase` is now the
|
- **Seafloor aging->depth (PlanetConfig, Phase 2 inc. 4):** `oceanBase` is now the
|
||||||
**deep abyssal floor** (-6000 m, not a flat ocean base) and `ridgeDepth` the
|
**deep abyssal floor** (-6000 m, not a flat ocean base) and `ridgeDepth` the
|
||||||
shallow young value (-2500 m); `seafloorSubsidence` (280 m per sqrt(My)) sets how
|
shallow young value (-2500 m); `seafloorSubsidence` (280 m per sqrt(My)) sets how
|
||||||
|
|||||||
@ -30,9 +30,11 @@ void Planet::erode(double dtMy) {
|
|||||||
}
|
}
|
||||||
sErode[i] = delta;
|
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)
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i)
|
||||||
cells[i].elevation = std::clamp(cells[i].elevation + sErode[i], -11000.0, 9000.0);
|
cells[i].elevation = std::clamp(cells[i].elevation + sErode[i], -11000.0, cfg.peakSoftCapEnd);
|
||||||
|
|
||||||
if (++erodeIter % cfg.seaLevelEvery == 0) adjustSeaLevel();
|
if (++erodeIter % cfg.seaLevelEvery == 0) adjustSeaLevel();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,7 +98,8 @@ void Planet::hydrology(double dtMy) {
|
|||||||
}
|
}
|
||||||
load[d] += carried; // pass remaining sediment downstream
|
load[d] += carried; // pass remaining sediment downstream
|
||||||
}
|
}
|
||||||
|
// Upper clamp tracks peakSoftCapEnd (matches step()/erode()).
|
||||||
#pragma omp parallel for schedule(static) if(n > 20000)
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i)
|
||||||
cells[i].elevation = std::clamp(cells[i].elevation, -11000.0, 9000.0);
|
cells[i].elevation = std::clamp(cells[i].elevation, -11000.0, cfg.peakSoftCapEnd);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
#define CONFIG_FIELDS(D, I, U) \
|
#define CONFIG_FIELDS(D, I, U) \
|
||||||
D(radius) D(seaLevel) D(axialTilt) D(continentBase) D(oceanBase) D(upliftGain) D(relax) \
|
D(radius) D(seaLevel) D(axialTilt) D(continentBase) D(oceanBase) D(upliftGain) D(relax) \
|
||||||
D(collisionFactor) D(arcFactor) D(isostaticPersist) D(rootScale) \
|
D(collisionFactor) D(arcFactor) D(isostaticPersist) D(rootScale) \
|
||||||
|
D(peakSoftCapStart) D(peakSoftCapEnd) D(peakFailDrop) \
|
||||||
D(seafloorSubsidence) D(seafloorSeedAge) \
|
D(seafloorSubsidence) D(seafloorSeedAge) \
|
||||||
D(maxDriftSpeed) D(ridgeDepth) D(splitFraction) D(splitProbBase) D(splitProbSlope) \
|
D(maxDriftSpeed) D(ridgeDepth) D(splitFraction) D(splitProbBase) D(splitProbSlope) \
|
||||||
D(stalemateEps) D(stalemateBoost) D(babyPromoteFrac) D(volcanicLandFrac) \
|
D(stalemateEps) D(stalemateBoost) D(babyPromoteFrac) D(volcanicLandFrac) \
|
||||||
@ -114,6 +115,9 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
E(rng(cfg.arcFactor, 0.0, 20.0, "arcFactor"));
|
E(rng(cfg.arcFactor, 0.0, 20.0, "arcFactor"));
|
||||||
E(rng(cfg.isostaticPersist, 0.0, 0.95, "isostaticPersist"));
|
E(rng(cfg.isostaticPersist, 0.0, 0.95, "isostaticPersist"));
|
||||||
E(rng(cfg.rootScale, 100.0, 20000.0, "rootScale"));
|
E(rng(cfg.rootScale, 100.0, 20000.0, "rootScale"));
|
||||||
|
E(rng(cfg.peakSoftCapStart, 0.0, 20000.0, "peakSoftCapStart"));
|
||||||
|
E(rng(cfg.peakSoftCapEnd, 0.0, 20000.0, "peakSoftCapEnd"));
|
||||||
|
E(rng(cfg.peakFailDrop, 0.0, 5000.0, "peakFailDrop"));
|
||||||
E(rng(cfg.seafloorSubsidence, 0.0, 2000.0, "seafloorSubsidence"));
|
E(rng(cfg.seafloorSubsidence, 0.0, 2000.0, "seafloorSubsidence"));
|
||||||
E(rng(cfg.seafloorSeedAge, 0.0, 1000.0, "seafloorSeedAge"));
|
E(rng(cfg.seafloorSeedAge, 0.0, 1000.0, "seafloorSeedAge"));
|
||||||
E(rng(cfg.maxDriftSpeed, 0.1, 100.0, "maxDriftSpeed"));
|
E(rng(cfg.maxDriftSpeed, 0.1, 100.0, "maxDriftSpeed"));
|
||||||
@ -193,6 +197,8 @@ std::string validateConfig(const PlanetConfig& cfg) {
|
|||||||
|
|
||||||
if (cfg.oceanBase >= cfg.continentBase)
|
if (cfg.oceanBase >= cfg.continentBase)
|
||||||
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
bad.push_back("oceanBase >= continentBase (ocean floor must be below continents)");
|
||||||
|
if (cfg.peakSoftCapStart >= cfg.peakSoftCapEnd)
|
||||||
|
bad.push_back("peakSoftCapStart >= peakSoftCapEnd (grow probability must span a band)");
|
||||||
|
|
||||||
if (bad.empty()) return {};
|
if (bad.empty()) return {};
|
||||||
std::string msg = "Bad config:";
|
std::string msg = "Bad config:";
|
||||||
|
|||||||
@ -1,6 +1,22 @@
|
|||||||
#include "Planet.hpp"
|
#include "Planet.hpp"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
// Deterministic, thread-independent value in [0,1). Pure function of (cell, tick,
|
||||||
|
// seed) -- so the parallel step() loops stay bit-identical for any thread count
|
||||||
|
// and this never touches Planet::rngState. `salt` selects an independent stream
|
||||||
|
// (one for the grow roll, one for the drop magnitude). splitmix64 finalizer.
|
||||||
|
static inline double peakHash(int i, uint64_t tick, uint32_t seed, uint32_t salt) {
|
||||||
|
uint64_t x = (uint64_t)(uint32_t)i * 0x9E3779B97F4A7C15ull
|
||||||
|
+ (tick + 1) * 0xD1B54A32D192ED03ull
|
||||||
|
+ (uint64_t)seed * 0xBF58476D1CE4E5B9ull
|
||||||
|
+ (uint64_t)salt + 0x123456789ull;
|
||||||
|
x ^= x >> 30; x *= 0xBF58476D1CE4E5B9ull;
|
||||||
|
x ^= x >> 27; x *= 0x94D049BB133111EBull;
|
||||||
|
x ^= x >> 31;
|
||||||
|
return (double)(x >> 11) * (1.0 / 9007199254740992.0); // 53-bit -> [0,1)
|
||||||
|
}
|
||||||
|
|
||||||
// --- Phase 1/2 tectonics: one stress->uplift->relax tick ---------------------
|
// --- Phase 1/2 tectonics: one stress->uplift->relax tick ---------------------
|
||||||
// Data-parallel: every pass writes only its own cell index (double-buffered
|
// Data-parallel: every pass writes only its own cell index (double-buffered
|
||||||
@ -138,7 +154,23 @@ double Planet::step() {
|
|||||||
relaxEff = cfg.relax;
|
relaxEff = cfg.relax;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cells[i].elevation += delta[i];
|
double d = delta[i];
|
||||||
|
// Soft probabilistic peak cap (drift-only): above peakSoftCapStart the
|
||||||
|
// chance that uplift takes falls linearly to 0 at peakSoftCapEnd, so peaks
|
||||||
|
// spread across a height band instead of railing at the old hard ceiling.
|
||||||
|
// A lost roll forfeits this tick's uplift and shaves a random 0..peakFailDrop
|
||||||
|
// metres off (so a peak hovers near its own height). erodeIter is the tick
|
||||||
|
// counter (step/erode run 1:1 in drift; it's saved -> exact resume).
|
||||||
|
if (drifting && d > 0.0 && cells[i].elevation > cfg.peakSoftCapStart) {
|
||||||
|
double span = cfg.peakSoftCapEnd - cfg.peakSoftCapStart;
|
||||||
|
double p = span > 0.0 ? (cfg.peakSoftCapEnd - cells[i].elevation) / span : 0.0;
|
||||||
|
p = std::clamp(p, 0.0, 1.0);
|
||||||
|
if (peakHash(i, (uint64_t)erodeIter, cfg.seed, 0u) >= p) { // grow roll lost
|
||||||
|
d = 0.0;
|
||||||
|
cells[i].elevation -= peakHash(i, (uint64_t)erodeIter, cfg.seed, 1u) * cfg.peakFailDrop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cells[i].elevation += d;
|
||||||
cells[i].elevation += (base - cells[i].elevation) * relaxEff;
|
cells[i].elevation += (base - cells[i].elevation) * relaxEff;
|
||||||
// (geoAge is crust age in My, advanced by advect() in Phase 2.)
|
// (geoAge is crust age in My, advanced by advect() in Phase 2.)
|
||||||
}
|
}
|
||||||
@ -151,9 +183,13 @@ double Planet::step() {
|
|||||||
for (int nb : cells[i].neighbors) { sum += cells[nb].elevation; ++cnt; }
|
for (int nb : cells[i].neighbors) { sum += cells[nb].elevation; ++cnt; }
|
||||||
smoothed[i] = cells[i].elevation * 0.96 + (sum / cnt) * 0.04;
|
smoothed[i] = cells[i].elevation * 0.96 + (sum / cnt) * 0.04;
|
||||||
}
|
}
|
||||||
|
// Upper clamp tracks peakSoftCapEnd so the soft cap governs peak heights
|
||||||
|
// (a fixed 9000 m ceiling would re-flatten everything the soft cap allows);
|
||||||
|
// it's just a safety rail now. Lower clamp (-11000 m, trenches) is unchanged.
|
||||||
|
const double elevCeil = cfg.peakSoftCapEnd;
|
||||||
#pragma omp parallel for schedule(static) if(n > 20000)
|
#pragma omp parallel for schedule(static) if(n > 20000)
|
||||||
for (int i = 0; i < n; ++i)
|
for (int i = 0; i < n; ++i)
|
||||||
cells[i].elevation = std::clamp(smoothed[i], -11000.0, 9000.0);
|
cells[i].elevation = std::clamp(smoothed[i], -11000.0, elevCeil);
|
||||||
|
|
||||||
// Largest elevation change this tick -> 0 as the world reaches equilibrium.
|
// Largest elevation change this tick -> 0 as the world reaches equilibrium.
|
||||||
double maxChange = 0.0;
|
double maxChange = 0.0;
|
||||||
|
|||||||
@ -94,6 +94,16 @@ struct PlanetConfig {
|
|||||||
double isostaticPersist = 0.85; // how much high crust resists relax (0..<1)
|
double isostaticPersist = 0.85; // how much high crust resists relax (0..<1)
|
||||||
double rootScale = 2500.0;// m above continentBase where persistence saturates
|
double rootScale = 2500.0;// m above continentBase where persistence saturates
|
||||||
|
|
||||||
|
// --- Soft peak cap (drift-only): spread mountain heights, no hard plateau --
|
||||||
|
// Above peakSoftCapStart the chance that a tick's uplift "takes" falls linearly
|
||||||
|
// to 0 at peakSoftCapEnd, so peaks settle across a height band instead of all
|
||||||
|
// railing at one ceiling. A lost grow roll forfeits that tick's uplift and
|
||||||
|
// shaves a random 0..peakFailDrop metres off. The hard elevation clamp's upper
|
||||||
|
// bound tracks peakSoftCapEnd. Active only during drift (forming still settles).
|
||||||
|
double peakSoftCapStart = 7000.0; // m: below this, uplift always takes (P=1)
|
||||||
|
double peakSoftCapEnd = 12000.0; // m: at/above this, uplift never takes (P=0)
|
||||||
|
double peakFailDrop = 200.0; // m: max random drop when the grow roll loses
|
||||||
|
|
||||||
// --- Plate drift (Phase 2) ----------------------------------------------
|
// --- Plate drift (Phase 2) ----------------------------------------------
|
||||||
double maxDriftSpeed = 20.0; // cm/year; fastest plate (Earth is 1-10)
|
double maxDriftSpeed = 20.0; // cm/year; fastest plate (Earth is 1-10)
|
||||||
double ridgeDepth = -2500.0; // m, elevation of brand-new crust at a ridge
|
double ridgeDepth = -2500.0; // m, elevation of brand-new crust at a ridge
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user