Tooling
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
package planet
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"salty/terrain/internal/coast"
|
||||
"salty/terrain/internal/field"
|
||||
"salty/terrain/internal/fluvial"
|
||||
"salty/terrain/internal/manifest"
|
||||
"salty/terrain/internal/region"
|
||||
"salty/terrain/internal/stats"
|
||||
"salty/terrain/internal/template"
|
||||
"salty/terrain/internal/thermal"
|
||||
"salty/terrain/internal/uplift"
|
||||
)
|
||||
|
||||
// Bake solves the geology of every region and composites the result into one planet.
|
||||
//
|
||||
// The order is the whole design: each landmass is solved in a box of its own with water all round it, which
|
||||
// is exactly the same answer as solving the planet whole because no flow path crosses open water; then the
|
||||
// land is written back; then the sea floor is laid once, over the finished cylinder.
|
||||
|
||||
// Result is a baked planet at geology resolution.
|
||||
type Result struct {
|
||||
In *Inputs
|
||||
Height *field.Field // metres, over the whole planet including the polar pad
|
||||
Flow []float32 // drainage area in m2, for the rivers on the preview
|
||||
Sea []bool // after the bake: below sea level
|
||||
|
||||
Regions []RegionResult
|
||||
Craters []CraterStats
|
||||
Elapsed time.Duration
|
||||
|
||||
// Coast is the shelf, the surf and the sediment budget, run once over the whole cylinder after every
|
||||
// region is composited. Nil only when there is no planet to run it on.
|
||||
Coast *coast.Result
|
||||
|
||||
// Stats is the whole planet's, pooled from the regions. Nil until Write computes it.
|
||||
//
|
||||
// Pooled rather than averaged, which is the one thing that makes it a planet statistic at all: a region
|
||||
// holds a histogram and histograms add, so merging them and taking a quantile of the sum gives exactly
|
||||
// what one pass over the whole world would have. See internal/stats.
|
||||
Stats *stats.Report
|
||||
|
||||
cancelled bool
|
||||
}
|
||||
|
||||
// RegionResult is what one region's solve cost and produced.
|
||||
type RegionResult struct {
|
||||
ID int
|
||||
Cells int
|
||||
LandCells int
|
||||
Seconds float64
|
||||
MinM float64
|
||||
MaxM float64
|
||||
ClipFrac float64
|
||||
|
||||
// FaultClamped is how many cells the fault pass pushed past the angle of repose and had to bound. Not an
|
||||
// error - it is the set saying the throws are large for the class they sit in - but worth seeing in the
|
||||
// summary rather than discovering later in a hillshade full of polygonal facets.
|
||||
FaultClamped int
|
||||
|
||||
// stats is this region's land statistics, unexported because it is scaffolding: it exists to be merged
|
||||
// into the planet's and is not part of what a region result means.
|
||||
stats *stats.Accumulator
|
||||
}
|
||||
|
||||
// statsOptions is what a world is judged against, in one place so a region and the planet it belongs to
|
||||
// cannot disagree about it - two accumulators built on different bounds do not merge.
|
||||
func statsOptions(m *manifest.Manifest) stats.Options {
|
||||
return stats.Options{
|
||||
ElevMin: m.ElevationM.Min, ElevMax: m.ElevationM.Max,
|
||||
TalusDeg: m.Pipeline.Thermal.TalusDeg, ReliefWindowM: reliefWindowM,
|
||||
ChannelM2: channelKm2 * 1e6,
|
||||
K: m.Pipeline.Fluvial.K, M: m.Pipeline.Fluvial.M, N: m.Pipeline.Fluvial.N,
|
||||
}
|
||||
}
|
||||
|
||||
// The two constants the statistics are taken at. They are flags on `generate` and fixed here, because a bake
|
||||
// is compared against other bakes and a window that moved between them would make the relief column
|
||||
// meaningless: 500 m is the usual choice, and 1 km2 is the incoming spec's channel definition.
|
||||
const (
|
||||
reliefWindowM = 500.0
|
||||
channelKm2 = 1.0
|
||||
)
|
||||
|
||||
// BakeOptions steer a run without editing the manifest.
|
||||
type BakeOptions struct {
|
||||
Only []int // region ids; empty means all
|
||||
Steps int // override the fluvial step count
|
||||
Jobs int // regions solved at once; 0 is the default
|
||||
Log func(string, ...any)
|
||||
|
||||
// OnRegion is called each time a region's land has been written back, with the planet as it stands.
|
||||
//
|
||||
// It runs holding the composite lock, so Height and Flow can be read without racing the workers still
|
||||
// running - and every one of them is stopped while it does, so it has to be quick and it must not keep a
|
||||
// reference to either past the call. It exists so a caller can *watch* a bake: two hours is a long time
|
||||
// to find out at the end that the numbers were wrong, and the world filling in one landmass at a time
|
||||
// answers that at the first one.
|
||||
OnRegion func(*Result, RegionResult)
|
||||
|
||||
// Cancel abandons the run when it is closed. Regions in flight stop at the end of their current step and
|
||||
// their land is composited in whatever state the solve had reached, so a cancelled Result is for looking
|
||||
// at and never for writing out as a bake.
|
||||
Cancel <-chan struct{}
|
||||
}
|
||||
|
||||
// Cancelled reports whether a run was abandoned before it finished.
|
||||
func (r *Result) Cancelled() bool { return r.cancelled }
|
||||
|
||||
// Bake runs the geology solve over a prepared planet.
|
||||
func Bake(in *Inputs, opt BakeOptions) (*Result, error) {
|
||||
log := opt.Log
|
||||
if log == nil {
|
||||
log = func(string, ...any) {}
|
||||
}
|
||||
m := in.M
|
||||
p := in.P
|
||||
n := p.W * p.H
|
||||
started := time.Now()
|
||||
|
||||
params := solveParams(m)
|
||||
if opt.Steps > 0 {
|
||||
params.Steps = opt.Steps
|
||||
}
|
||||
log("fluvial %d steps of %.0f yr (%.2f Myr), K %.1e, m %.2f, n %.2f, fill every %d",
|
||||
params.Steps, params.DtYr, float64(params.Steps)*params.DtYr/1e6,
|
||||
params.K, params.M, params.N, params.FillEvery)
|
||||
|
||||
res := &Result{In: in, Height: field.New(p.W, p.H, p.CellM), Flow: make([]float32, n)}
|
||||
for i := range res.Height.Data {
|
||||
res.Height.Data[i] = float32(m.SeaLevelM)
|
||||
}
|
||||
|
||||
rates := in.Legend.Rates()
|
||||
ks := in.Legend.Erodibilities()
|
||||
plainM, plainFloor := in.Legend.CoastalPlains()
|
||||
massifFloor, massifFraction := in.Legend.Massifs()
|
||||
wanted := wantedRegions(in.Part.Regions, opt.Only)
|
||||
|
||||
// Regions are solved a few at a time. They are independent - each writes only the land it owns, and no
|
||||
// flow path crosses the water between them - so this changes nothing about the result; what it buys is
|
||||
// the parts of the solve that are sequential *within* a region. Terrain.md's profile says most of the
|
||||
// runtime is the stack walk and the flood's cursor, neither of which parallelises inside one grid, so
|
||||
// overlapping regions is where the cores actually go.
|
||||
//
|
||||
// Results land in indexed slots and are read back in region order afterwards, never drained from a
|
||||
// channel: cross-cutting rule 12 means the output must not depend on which goroutine finished first.
|
||||
tables := paint{rates: rates, ks: ks, plainM: plainM, plainFloor: plainFloor,
|
||||
massifFloor: massifFloor, massifFraction: massifFraction, massifCells: m.Planet.MassifCells(),
|
||||
rockCells: m.Planet.LithologyCells(), rockMult: m.Pipeline.Lithology.KMultipliers,
|
||||
lithMix: in.Legend.LithologyMixes(), faults: in.Faults,
|
||||
// The *manifest's* step count, not the overridden one. `throw_m` is a total displacement over the
|
||||
// run and it becomes a rate by dividing by the run's length, so taking the override would make a
|
||||
// short run raise the *rate* to build the same scarp in less time - which at `--steps 200` is five
|
||||
// times the uplift and past the repose ceiling, so a tuning run would show every fault pinned
|
||||
// against the clamp and tell an author nothing about the world they are tuning. `--steps` means
|
||||
// "run less time"; everything else in the solve is under-done by it and faults should be too.
|
||||
runYears: float64(m.Pipeline.Fluvial.Steps) * params.DtYr,
|
||||
clampCeilM: faultCeilingMYr(m),
|
||||
}
|
||||
|
||||
jobs := opt.Jobs
|
||||
if jobs <= 0 {
|
||||
jobs = 3
|
||||
}
|
||||
if jobs > len(wanted) {
|
||||
jobs = len(wanted)
|
||||
}
|
||||
// Biggest first, so the long poles start early and the short ones fill the tail.
|
||||
order := append([]region.Region(nil), wanted...)
|
||||
sort.Slice(order, func(a, b int) bool { return order[a].Cells() > order[b].Cells() })
|
||||
|
||||
out := make([]RegionResult, len(order))
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
// Guards the composite - the one place workers touch shared state - so that OnRegion can read the whole
|
||||
// planet without racing them.
|
||||
var composite sync.Mutex
|
||||
safeLog := func(format string, a ...any) {
|
||||
mu.Lock()
|
||||
log(format, a...)
|
||||
mu.Unlock()
|
||||
}
|
||||
next := make(chan int)
|
||||
go func() {
|
||||
for i := range order {
|
||||
next <- i
|
||||
}
|
||||
close(next)
|
||||
}()
|
||||
for w := 0; w < jobs; w++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := range next {
|
||||
if cancelled(opt.Cancel) {
|
||||
continue // drain the queue; the workers already running stop at their next step
|
||||
}
|
||||
out[i] = solveRegion(in, order[i], params, tables, res, safeLog, opt, &composite)
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
res.Regions = out
|
||||
sort.Slice(res.Regions, func(a, b int) bool { return res.Regions[a].ID < res.Regions[b].ID })
|
||||
|
||||
if cancelled(opt.Cancel) {
|
||||
res.cancelled = true
|
||||
res.Elapsed = time.Since(started)
|
||||
log("cancelled after %s", res.Elapsed.Round(time.Second))
|
||||
return res, nil
|
||||
}
|
||||
|
||||
res.Craters = stampCraters(in, res, log)
|
||||
runCoast(in, res, log)
|
||||
res.Elapsed = time.Since(started)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func wantedRegions(all []region.Region, only []int) []region.Region {
|
||||
if len(only) == 0 {
|
||||
return all
|
||||
}
|
||||
keep := make(map[int]bool, len(only))
|
||||
for _, id := range only {
|
||||
keep[id] = true
|
||||
}
|
||||
var out []region.Region
|
||||
for _, r := range all {
|
||||
if keep[r.ID] {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// solveRegion is one landmass: cut it out, build its painted geology, run the solve, write the land back.
|
||||
// progressCells is where a region becomes big enough to be worth reporting on: below a couple of million it
|
||||
// is over in a minute or two and the lines are noise.
|
||||
const progressCells = 2_000_000
|
||||
|
||||
// paint is the per-class tables a region's geology is built from, bundled so the worker signature does not
|
||||
// grow a column every time the legend learns a new word.
|
||||
type paint struct {
|
||||
rates, ks []float32
|
||||
plainM []float64
|
||||
plainFloor []float32
|
||||
massifFloor []float32
|
||||
// massifFraction is per class, zero for a class that is one rate all over. massifCells is the planet's
|
||||
// upland fabric wavelength in lattice cells, which is a property of the planet rather than of a class.
|
||||
massifFraction []float64
|
||||
massifCells int
|
||||
|
||||
// The rock field and the fault set: both are properties of the planet that every region reads the same
|
||||
// way, which is the whole point of computing them once above rather than per region.
|
||||
rockCells int
|
||||
rockMult []float64
|
||||
lithMix []float64
|
||||
faults []uplift.FaultTrace
|
||||
runYears float64
|
||||
clampCeilM float64
|
||||
}
|
||||
|
||||
func cancelled(ch <-chan struct{}) bool {
|
||||
if ch == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-ch:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func solveRegion(in *Inputs, rg region.Region, params fluvial.Params, t paint,
|
||||
res *Result, log func(string, ...any), opt BakeOptions, composite *sync.Mutex) RegionResult {
|
||||
|
||||
m := in.M
|
||||
start := time.Now()
|
||||
class, land := in.Part.Cut(in.Map, rg)
|
||||
|
||||
up := uplift.FromTemplate(uplift.Paint{
|
||||
Frame: rg.Frame, Class: class, Land: land,
|
||||
Rates: t.rates, Ks: t.ks, PlainM: t.plainM, PlainFloor: t.plainFloor,
|
||||
MassifFloor: t.massifFloor, MassifFraction: t.massifFraction, MassifCells: t.massifCells,
|
||||
RockCells: t.rockCells, RockMult: t.rockMult, LithMix: t.lithMix,
|
||||
Faults: t.faults, RunYears: t.runYears, ClampCeilM: t.clampCeilM,
|
||||
Variation: m.Planet.UpliftVariation,
|
||||
}, m)
|
||||
|
||||
h := up.Height.Clone()
|
||||
grid := fluvial.NewGrid(rg.Frame.W, rg.Frame.H, rg.Frame.P.CellM, up.Base)
|
||||
grid.SetSeed(m.Source.Seed)
|
||||
grid.SetCancel(opt.Cancel)
|
||||
grid.SetFrame(rg.Frame) // the jitter is a hash of world position, not of grid index
|
||||
grid.SetElevationRange(m.ElevationM.Min-200, m.ElevationM.Max+500)
|
||||
|
||||
log("region %2d start %.1f x %.1f km, %d cells (%d land)%s",
|
||||
rg.ID, float64(rg.Frame.W)*rg.Frame.P.CellM/1000, float64(rg.Frame.H)*rg.Frame.P.CellM/1000,
|
||||
rg.Cells(), rg.LandCells, seamNote(rg.Seam))
|
||||
|
||||
// Progress, because a big region is an hour on its own and silence for an hour is indistinguishable from
|
||||
// a hang. Every tenth, and only for regions big enough to be worth waiting on: with several in flight the
|
||||
// lines interleave, so each one carries its region id and none of them is per-step.
|
||||
var progress func(step, total int, pct float64)
|
||||
if rg.Cells() > progressCells {
|
||||
next := 10.0
|
||||
progress = func(step, total int, pct float64) {
|
||||
if pct < next || step == 0 {
|
||||
return
|
||||
}
|
||||
next = pct + 10
|
||||
lo, hi := landExtent(h.Data, land)
|
||||
elapsed := time.Since(start)
|
||||
eta := time.Duration(float64(elapsed) / (pct / 100) * (1 - pct/100))
|
||||
log("region %2d %3.0f%% step %d/%d, land %.0f..%.0f m, eta %s",
|
||||
rg.ID, pct, step, total, lo, hi, eta.Round(time.Second))
|
||||
}
|
||||
}
|
||||
grid.Run(h.Data, up.Rate.Data, up.K.Data, params, progress)
|
||||
|
||||
// The edge-preserving pass, if the manifest asked for one. Here and not after the write: the region's
|
||||
// statistics are gathered a few lines below while the grid is alive, and a smooth applied after them
|
||||
// would put a surface on disk that meta.json does not describe - which is the bake-against-tiles drift
|
||||
// trap in a new costume.
|
||||
//
|
||||
// It reads a cell's eight neighbours, and that is safe against the decomposition for a reason particular
|
||||
// to this pass: a region's frame edges are open ocean by construction and every landmass in it sits at
|
||||
// least margin_cells from them, so a land cell's neighbours are always inside its own region. The rule
|
||||
// this would otherwise break - no neighbourhood operation near a region edge - still stands for anything
|
||||
// that reaches across the waterline.
|
||||
if sm := m.Pipeline.Smooth; sm.Passes > 0 {
|
||||
field.SmoothEdgePreserving(h.Data, h.W, h.H, h.CellM, land, sm.Passes, sm.SlopeRef, grid.Scratch())
|
||||
}
|
||||
|
||||
lo, hi := landExtent(h.Data, land)
|
||||
peakRate := 0.0
|
||||
for i, isLand := range land {
|
||||
if isLand && float64(up.Rate.Data[i]) > peakRate {
|
||||
peakRate = float64(up.Rate.Data[i])
|
||||
}
|
||||
}
|
||||
clip := clipFraction(h.Data, land, m.ElevationM.Min, m.ElevationM.Max)
|
||||
secs := time.Since(start).Seconds()
|
||||
clampNote := ""
|
||||
if up.FaultClamped > 0 {
|
||||
clampNote = fmt.Sprintf(", %.2f%% of it bounded at repose by faults",
|
||||
100*float64(up.FaultClamped)/float64(rg.LandCells))
|
||||
}
|
||||
log("region %2d done %.0f..%.0f m, %.3f%% clipped, %.2f mm/yr peak%s [%.0f s]",
|
||||
rg.ID, lo, hi, clip*100, peakRate*1000, clampNote, secs)
|
||||
|
||||
// The region's own land statistics, gathered here while its grid is still alive - it is thrown away a
|
||||
// few lines below, and the composited planet has no uplift field or flow topology to recover them from.
|
||||
// Only the land this region *owns*: `Cut` marks nothing else, so the pieces are disjoint and the sum is
|
||||
// the planet.
|
||||
acc := stats.New(statsOptions(m))
|
||||
acc.Add(stats.Input{
|
||||
H: h, Land: land, UpliftMYr: up.Rate.Data, KLocal: up.K.Data,
|
||||
Area: grid.Area, Receiver: grid.Receiver, Length: grid.Length,
|
||||
})
|
||||
|
||||
rr := RegionResult{ID: rg.ID, Cells: rg.Cells(), LandCells: rg.LandCells,
|
||||
Seconds: secs, MinM: lo, MaxM: hi, ClipFrac: clip, FaultClamped: up.FaultClamped, stats: acc}
|
||||
|
||||
// The write-back and the hook under one lock. Composite is the only place a worker touches shared state,
|
||||
// so holding it here is what lets OnRegion read the whole planet without racing the others.
|
||||
composite.Lock()
|
||||
in.Part.Composite(res.Height.Data, in.Map, rg, h.Data)
|
||||
compositeFlow(in.Part, in.Map, rg, res.Flow, grid.Area)
|
||||
if opt.OnRegion != nil {
|
||||
opt.OnRegion(res, rr)
|
||||
}
|
||||
composite.Unlock()
|
||||
|
||||
return rr
|
||||
}
|
||||
|
||||
func seamNote(seam bool) string {
|
||||
if seam {
|
||||
return ", across the seam"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// compositeFlow writes a region's drainage area back for the preview's rivers. It follows Composite's rule:
|
||||
// only the land this region owns.
|
||||
func compositeFlow(part *region.Partition, mp *template.Map, rg region.Region, dst, area []float32) {
|
||||
for y := 0; y < rg.Frame.H; y++ {
|
||||
for x := 0; x < rg.Frame.W; x++ {
|
||||
pi := rg.Frame.PlanetIdx(x, y)
|
||||
if part.Owner[pi] != int32(rg.ID) || mp.Sea[pi] {
|
||||
continue
|
||||
}
|
||||
dst[pi] = area[y*rg.Frame.W+x]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func solveParams(m *manifest.Manifest) fluvial.Params {
|
||||
f := m.Pipeline.Fluvial
|
||||
return fluvial.Params{
|
||||
K: f.K, M: f.M, N: f.N, DtYr: f.DtYr, Steps: f.Steps,
|
||||
Diffusion: f.DiffusionM2Yr, FillEvery: f.FillEvery,
|
||||
TalusSlope: thermal.TalusFromDegrees(m.Pipeline.Thermal.TalusDeg),
|
||||
ThermalEvery: m.Pipeline.Thermal.Every,
|
||||
ThermalPasses: m.Pipeline.Thermal.CoarsePasses,
|
||||
CriticalAreaM2: f.CriticalAreaM2,
|
||||
ChannelTaper: f.ChannelTaper,
|
||||
CriticalSlope: thermal.TalusFromDegrees(f.CriticalSlopeDeg),
|
||||
SlopeCap: f.SlopeCap,
|
||||
MaxHillslopeSub: f.MaxHillslopeSub,
|
||||
MFDExponent: f.MFDExponent,
|
||||
}
|
||||
}
|
||||
|
||||
// runCoast lays the sea floor and works the shoreline, once, over the whole cylinder.
|
||||
//
|
||||
// Once and whole rather than per region, which is D-53's rule and is not a convenience: the pass costs tens
|
||||
// of nanoseconds a cell against tens of nanoseconds a cell *per step* for the solve, and cutting it up would
|
||||
// truncate the fetch across every strait, split the sediment budget whose conservation is the one thing in it
|
||||
// not derived from something already measured, and leave the shoreline length and the exposure percentiles as
|
||||
// statistics that do not pool. It runs after every region is composited, because two of its three processes
|
||||
// read the finished land: the shelf width comes off the relief standing behind each shore, and the surf cuts
|
||||
// into whatever the solve built.
|
||||
//
|
||||
// The painted depths go in as the *abyss*, one value per cell. That is what makes the derived margin meet the
|
||||
// painted ocean instead of stepping to it: an author who painted `shelf` at 120 m, `deep` at 512 and `surf`
|
||||
// at 20 gets a continental slope that runs down to each of those where each of them is, and a strait painted
|
||||
// shallower than the shelf break comes out as shelf all the way across rather than as a trench.
|
||||
func runCoast(in *Inputs, res *Result, log func(string, ...any)) {
|
||||
m := in.M
|
||||
depths := in.Legend.Depths()
|
||||
abyss := make([]float32, len(res.Height.Data))
|
||||
for i := range abyss {
|
||||
if in.Map.Sea[i] {
|
||||
abyss[i] = depths[in.Map.Class[i]]
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
cs := coast.Build(coast.Input{
|
||||
Height: res.Height, Sea: in.Map.Sea, SeaLevelM: m.SeaLevelM,
|
||||
BreakM: m.ShelfBreakM(),
|
||||
// AbyssM is the square canvas's key and is never read on this path: `Abyss` below is per-cell and
|
||||
// always allocated, so `abyssAt` takes the painting every time. Left as the fallback it is rather
|
||||
// than removed, and named here because it is the same shape as the read D-64 had to fix one line up.
|
||||
AbyssM: -m.Pipeline.Continent.SeaFloorM.Lo(),
|
||||
Abyss: abyss,
|
||||
// The cylinder, which is the whole of what D-59's successor had to add: every march, every ray and
|
||||
// every running sum in the pass crosses the seam now, and a planet measured on a flat grid would have
|
||||
// had its shelf, its fetch and its sediment budget all stop dead at one meridian.
|
||||
WrapX: true,
|
||||
NoisePeriodM: in.P.NoisePeriodM,
|
||||
Flow: res.Flow,
|
||||
Seed: m.Source.Seed,
|
||||
Cfg: m.Pipeline.Coast,
|
||||
})
|
||||
res.Coast = cs
|
||||
res.Sea = cs.Sea
|
||||
|
||||
if !m.Pipeline.Coast.Enabled {
|
||||
log("ocean the painted depths, flat: the coastal pass is switched off in the manifest")
|
||||
return
|
||||
}
|
||||
// The margin's own numbers, said out loud, because they decide how much of the painted ocean survives
|
||||
// and they were a square-canvas inheritance nobody could see until D-64. A shelf and a slope together
|
||||
// reach `ShelfKm.Hi() + SlopeKm` from every shore; where the sea is narrower than twice that, the
|
||||
// painting's depth is never reached anywhere in it and the author's ocean is whatever `break_m` says.
|
||||
cfg := m.Pipeline.Coast
|
||||
reach := cfg.ShelfKm.Hi() + cfg.SlopeKm
|
||||
log(" sea floor: shelf %.1f..%.1f km to a break at %.0f m, then %.1f km of slope to the painted "+
|
||||
"depth, so the painting owns the water past %.1f km offshore and nothing nearer",
|
||||
cfg.ShelfKm.Lo(), cfg.ShelfKm.Hi(), m.ShelfBreakM(), cfg.SlopeKm, reach)
|
||||
log("coast %s", cs.Stats.Summary())
|
||||
log(" over the whole cylinder in %s, once: the pass is tens of nanoseconds a cell and cutting it",
|
||||
time.Since(start).Round(time.Millisecond))
|
||||
log(" up would truncate the fetch across every strait and split the sediment budget")
|
||||
}
|
||||
|
||||
func landExtent(h []float32, land []bool) (lo, hi float64) {
|
||||
lo, hi = 1e30, -1e30
|
||||
any := false
|
||||
for i, v := range h {
|
||||
if !land[i] {
|
||||
continue
|
||||
}
|
||||
any = true
|
||||
if float64(v) < lo {
|
||||
lo = float64(v)
|
||||
}
|
||||
if float64(v) > hi {
|
||||
hi = float64(v)
|
||||
}
|
||||
}
|
||||
if !any {
|
||||
return 0, 0
|
||||
}
|
||||
return lo, hi
|
||||
}
|
||||
|
||||
// clipFraction is how much of the land the 16-bit encoding would cut off. Above a fraction of a per cent it
|
||||
// is a failed run rather than a rounded one, and painted uplift makes it easier to hit: an author can ask
|
||||
// for more relief than the elevation range holds.
|
||||
func clipFraction(h []float32, land []bool, minM, maxM float64) float64 {
|
||||
n, clipped := 0, 0
|
||||
for i, v := range h {
|
||||
if !land[i] {
|
||||
continue
|
||||
}
|
||||
n++
|
||||
if float64(v) < minM || float64(v) > maxM {
|
||||
clipped++
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(clipped) / float64(n)
|
||||
}
|
||||
|
||||
// faultCeilingMYr is the uplift rate at which a divide stands at the angle of repose, in metres a year at
|
||||
// K x1. It is the number `terrain plan` already prints as the clamp ceiling, in the unit the solve works in;
|
||||
// here it bounds what a *fault* may add on top of what an author painted, and nothing else. Zero when the
|
||||
// repose clamp is switched off, which switches the bound off with it.
|
||||
func faultCeilingMYr(m *manifest.Manifest) float64 {
|
||||
if m.Pipeline.Thermal.TalusDeg <= 0 || m.Pipeline.Thermal.TalusDeg >= 90 {
|
||||
return 0
|
||||
}
|
||||
return clampCeiling(m.Pipeline.Fluvial.K, m.GeologyCellM(), m.Pipeline.Fluvial.M,
|
||||
m.Pipeline.Thermal.TalusDeg) / 1000
|
||||
}
|
||||
Reference in New Issue
Block a user