Tooling
This commit is contained in:
@@ -64,6 +64,11 @@ type Params struct {
|
||||
CriticalSlope float64
|
||||
SlopeCap float64 // where the flux stops stiffening, as a fraction of Sc
|
||||
MaxHillslopeSub int // the sub-step budget that bound buys
|
||||
|
||||
// MFDExponent selects multiple-flow-direction drainage area over D8's single receiver, and is the
|
||||
// exponent on the partition. 0 keeps the old Accumulate, which is what every bake before this ran and
|
||||
// what the A/B comparison needs. See mfd.go for why one is the right default.
|
||||
MFDExponent float64
|
||||
}
|
||||
|
||||
// Grid holds the flow topology and the scratch it is built from. Allocated once and reused across every
|
||||
@@ -85,7 +90,11 @@ type Grid struct {
|
||||
Stack []int32 // every node after its receiver
|
||||
Area []float32 // drainage area, m²
|
||||
|
||||
seed uint64 // the jitter's seed; see jitter.go and SetSeed
|
||||
cancel <-chan struct{} // closed to abandon a run mid-solve; see SetCancel
|
||||
seed uint64 // the jitter's seed; see jitter.go and SetSeed
|
||||
originX int32 // where this grid sits on the planet; see SetFrame. Zero is "this grid is the world"
|
||||
originY int32
|
||||
planetW int32 // the cylinder's width, or 0 when there is no cylinder
|
||||
donorOff []int32
|
||||
donorList []int32
|
||||
cursor []int32
|
||||
@@ -93,6 +102,13 @@ type Grid struct {
|
||||
pq *bucketPQ
|
||||
fifo []int32
|
||||
scratch []float32
|
||||
|
||||
// Multiple-flow accumulation. mfdPending is how many strictly higher neighbours a cell still owes before
|
||||
// it may be released; a byte, because a cell has eight neighbours and cannot owe more. See mfd.go.
|
||||
mfdPending []uint8
|
||||
mfdQueue []int32
|
||||
mfdMode mfdPow
|
||||
mfdExp float64
|
||||
}
|
||||
|
||||
// SetElevationRange sizes the flood's bucket queue. Called once, with the manifest's elevation range plus a
|
||||
@@ -116,6 +132,7 @@ func NewGrid(w, h int, cellM float64, base []bool) *Grid {
|
||||
Receiver: make([]int32, n), Length: make([]float32, n), Stack: make([]int32, 0, n),
|
||||
Area: make([]float32, n), donorOff: make([]int32, n+1), donorList: make([]int32, n),
|
||||
closed: make([]bool, n), fifo: make([]int32, 0, n), scratch: make([]float32, n),
|
||||
mfdPending: make([]uint8, n),
|
||||
}
|
||||
g.fixed = make([]bool, n)
|
||||
for i := range g.fixed {
|
||||
@@ -178,7 +195,7 @@ func (g *Grid) FillDepressions(h []float32, epsilon float32) {
|
||||
}
|
||||
g.closed[ni] = true
|
||||
if h[ni] <= celev {
|
||||
h[ni] = celev + epsilon*(0.5+hash01(g.seed, ni))
|
||||
h[ni] = celev + epsilon*(0.5+hashXY(g.seed, g.worldX(nx), g.worldY(ny), jitterFloodEpsilon))
|
||||
g.fifo = append(g.fifo, ni)
|
||||
} else {
|
||||
g.pq.push(h[ni], ni)
|
||||
@@ -239,7 +256,7 @@ func (g *Grid) ComputeReceivers(h []float32) {
|
||||
}
|
||||
// The tie-break, not a change of gradient: the comparison is jittered, the slope that
|
||||
// is kept is not, so Length and the stream-power update see the true geometry.
|
||||
sj := s * (1 + 1e-3*(hash01(g.seed, i*8+int32(k))-0.5))
|
||||
sj := s * (1 + 1e-3*(hashXY(g.seed, g.worldX(x), g.worldY(y), int32(k)+jitterReceiverTie)-0.5))
|
||||
if sj > bestJitter {
|
||||
bestJitter, best, bestLen = sj, ni, l
|
||||
}
|
||||
@@ -298,6 +315,11 @@ func (g *Grid) BuildStack() {
|
||||
}
|
||||
}
|
||||
|
||||
// Scratch hands out the grid's spare float32 buffer, which is the width of the grid and is dead between
|
||||
// steps. It is here so a pass that runs once after the solve - the edge-preserving smooth - does not allocate
|
||||
// a second copy of the height field at planet scale just to have somewhere to write.
|
||||
func (g *Grid) Scratch() []float32 { return g.scratch }
|
||||
|
||||
// scratchInt32 reuses the float32 scratch as int32 storage; same width, and it saves a 12 MB allocation per
|
||||
// step at the geology grid.
|
||||
func (g *Grid) scratchInt32() []int32 {
|
||||
@@ -458,6 +480,14 @@ func clampAt(a []float32, w, h, x, y int) float32 {
|
||||
return a[y*w+x]
|
||||
}
|
||||
|
||||
// SetCancel gives the solve a way to be abandoned part way through.
|
||||
//
|
||||
// It is checked once a step rather than inside one, which is the right granularity: a step is milliseconds on
|
||||
// a small region and a couple of seconds on a big one, so the longest a caller waits is one step, and nothing
|
||||
// inside a step is safe to leave half done. The height field is left wherever the solve had got to, which is
|
||||
// what a cancelled run means - it is not a checkpoint and nothing downstream should read it as one.
|
||||
func (g *Grid) SetCancel(ch <-chan struct{}) { g.cancel = ch }
|
||||
|
||||
// Run is the whole solve. Progress is reported through log, which is what a five-minute budget needs to be
|
||||
// steerable: a run that is going wrong should say so at step 500, not at the end.
|
||||
func (g *Grid) Run(h []float32, uplift, k []float32, p Params, log func(step int, total int, elapsedPct float64)) {
|
||||
@@ -466,12 +496,23 @@ func (g *Grid) Run(h []float32, uplift, k []float32, p Params, log func(step int
|
||||
fill = 1
|
||||
}
|
||||
for step := 0; step < p.Steps; step++ {
|
||||
if g.cancel != nil {
|
||||
select {
|
||||
case <-g.cancel:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
if step%fill == 0 {
|
||||
g.FillDepressions(h, 1e-3)
|
||||
}
|
||||
g.ComputeReceivers(h)
|
||||
g.BuildStack()
|
||||
g.Accumulate()
|
||||
if p.MFDExponent > 0 {
|
||||
g.AccumulateMFD(h, p.MFDExponent)
|
||||
} else {
|
||||
g.Accumulate()
|
||||
}
|
||||
g.StreamPower(h, uplift, k, p)
|
||||
if p.CriticalSlope > 0 {
|
||||
// The clamp still runs, and it still has to: a belt rising at millimetres a year asks for slopes
|
||||
|
||||
Reference in New Issue
Block a user