diff --git a/BUILD.md b/BUILD.md index 895f071..e025e85 100644 --- a/BUILD.md +++ b/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) isostaticPersist 0.85 how strongly high crust resists relax (0..<1) 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 seafloorSubsidence 280 m per sqrt(My): seafloor deepens with crustal age seafloorSeedAge 80 My initial oceanic age spread at generation diff --git a/CLAUDE.md b/CLAUDE.md index 3ed65ad..c0c1ba6 100644 --- a/CLAUDE.md +++ b/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 rise/erode) saturating at `rootScale` (2500 m above `continentBase`). These three 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 - hard clamp `[-11000, 9000]` in `step()` is the Everest-class cap; a few peak - cells may sit there during drift (<2% — fine). + per-tick `erode()` is what limits their height, so they don't rail the clamp. +- **Soft peak cap (drift-only) — no 9000 m plateau:** previously the hard + `[-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 **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 diff --git a/src/sim/PlanetErosion.cpp b/src/sim/PlanetErosion.cpp index b8dd6de..6ab0ddb 100644 --- a/src/sim/PlanetErosion.cpp +++ b/src/sim/PlanetErosion.cpp @@ -30,9 +30,11 @@ void Planet::erode(double dtMy) { } 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, 9000.0); + cells[i].elevation = std::clamp(cells[i].elevation + sErode[i], -11000.0, cfg.peakSoftCapEnd); if (++erodeIter % cfg.seaLevelEvery == 0) adjustSeaLevel(); } diff --git a/src/sim/PlanetHydrology.cpp b/src/sim/PlanetHydrology.cpp index 5ef4487..0383ac2 100644 --- a/src/sim/PlanetHydrology.cpp +++ b/src/sim/PlanetHydrology.cpp @@ -98,7 +98,8 @@ void Planet::hydrology(double dtMy) { } load[d] += carried; // pass remaining sediment downstream } + // Upper clamp tracks peakSoftCapEnd (matches step()/erode()). #pragma omp parallel for schedule(static) if(n > 20000) 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); } diff --git a/src/sim/PlanetIO.cpp b/src/sim/PlanetIO.cpp index 122205f..82a7ca6 100644 --- a/src/sim/PlanetIO.cpp +++ b/src/sim/PlanetIO.cpp @@ -15,6 +15,7 @@ #define CONFIG_FIELDS(D, I, U) \ D(radius) D(seaLevel) D(axialTilt) D(continentBase) D(oceanBase) D(upliftGain) D(relax) \ D(collisionFactor) D(arcFactor) D(isostaticPersist) D(rootScale) \ + D(peakSoftCapStart) D(peakSoftCapEnd) D(peakFailDrop) \ D(seafloorSubsidence) D(seafloorSeedAge) \ D(maxDriftSpeed) D(ridgeDepth) D(splitFraction) D(splitProbBase) D(splitProbSlope) \ 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.isostaticPersist, 0.0, 0.95, "isostaticPersist")); 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.seafloorSeedAge, 0.0, 1000.0, "seafloorSeedAge")); E(rng(cfg.maxDriftSpeed, 0.1, 100.0, "maxDriftSpeed")); @@ -193,6 +197,8 @@ std::string validateConfig(const PlanetConfig& cfg) { if (cfg.oceanBase >= cfg.continentBase) 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 {}; std::string msg = "Bad config:"; diff --git a/src/sim/PlanetTectonics.cpp b/src/sim/PlanetTectonics.cpp index 86fc1e6..9911e0e 100644 --- a/src/sim/PlanetTectonics.cpp +++ b/src/sim/PlanetTectonics.cpp @@ -1,6 +1,22 @@ #include "Planet.hpp" #include #include +#include + +// 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 --------------------- // Data-parallel: every pass writes only its own cell index (double-buffered @@ -138,7 +154,23 @@ double Planet::step() { 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; // (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; } 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) 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. double maxChange = 0.0; diff --git a/src/sim/PlanetTypes.hpp b/src/sim/PlanetTypes.hpp index 54669e3..5dd430d 100644 --- a/src/sim/PlanetTypes.hpp +++ b/src/sim/PlanetTypes.hpp @@ -94,6 +94,16 @@ struct PlanetConfig { double isostaticPersist = 0.85; // how much high crust resists relax (0..<1) 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) ---------------------------------------------- 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