Tooling
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
package fluvial
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"salty/terrain/internal/field"
|
||||
)
|
||||
|
||||
// Multiple-flow-direction drainage area: Freeman, Quinn and Holmgren's partition, and the answer to the one
|
||||
// thing D8 cannot do.
|
||||
//
|
||||
// A planar hillslope is where D8 fails, and it fails in closed form. The specific catchment area on a plane
|
||||
// is the distance from the divide and it is the same at every point along a contour, because nothing about a
|
||||
// plane tells one flow line from its neighbour. D8 has to disagree: every cell picks the same steepest
|
||||
// neighbour, so the flow lines run exactly parallel and never converge, and a cell either sits on a line and
|
||||
// carries the whole tube or sits off one and carries a single cell for ever. Measured on a ramp at an aspect
|
||||
// of 22.5 degrees, the most-drained cell in a contour band carries 769 times the median and 30 % of the grid
|
||||
// drains nothing at all (flow_test.go). Stream power then reads A^m off that and cuts each line in, which is
|
||||
// what a bake's mountain flanks were: a comb of ruler-straight parallel grooves, one per surviving line,
|
||||
// spaced by the mean distance between the merges the router's tie-break jitter happened to allow - a spacing
|
||||
// in cells, which is why it measured the same eighteen cells at an 8 m cell and at a 32 m one.
|
||||
//
|
||||
// The partition below splits a cell's area among every downslope neighbour by (dh_k * q_k)^p, where q_k is
|
||||
// the share of the cell's perimeter facing direction k divided by the centre-to-centre distance: 0.5 for a
|
||||
// cardinal neighbour, 0.25 for a diagonal. The cell size cancels out of the ratio, so the weights are height
|
||||
// differences times a constant - no division and no transcendental in the inner loop at p = 1.
|
||||
//
|
||||
// It replaces Accumulate and it replaces only that. Receiver, Length and Stack stay D8, because
|
||||
// Braun-Willett's implicit update walks one receiver chain and there is no multi-receiver form of it that is
|
||||
// still unconditionally stable. Stream power therefore incises along the steepest path using the area that
|
||||
// actually converges there. That pairing is deliberate and it is the standard one; it is not an oversight.
|
||||
|
||||
// mfdPow selects how the partition quantity is raised to p, once per call rather than once per cell. p is
|
||||
// almost always 1, where the whole thing is a multiply.
|
||||
type mfdPow uint8
|
||||
|
||||
const (
|
||||
mfdP1 mfdPow = iota
|
||||
mfdP2
|
||||
mfdP3
|
||||
mfdP4
|
||||
mfdGeneral
|
||||
)
|
||||
|
||||
func mfdModeFor(p float64) (mfdPow, float64) {
|
||||
switch {
|
||||
case math.Abs(p-1) < 1e-9:
|
||||
return mfdP1, 1
|
||||
case math.Abs(p-2) < 1e-9:
|
||||
return mfdP2, 2
|
||||
case math.Abs(p-3) < 1e-9:
|
||||
return mfdP3, 3
|
||||
case math.Abs(p-4) < 1e-9:
|
||||
return mfdP4, 4
|
||||
default:
|
||||
return mfdGeneral, p
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Grid) mfdRaise(v float32) float32 {
|
||||
switch g.mfdMode {
|
||||
case mfdP1:
|
||||
return v
|
||||
case mfdP2:
|
||||
return v * v
|
||||
case mfdP3:
|
||||
return v * v * v
|
||||
case mfdP4:
|
||||
v2 := v * v
|
||||
return v2 * v2
|
||||
default:
|
||||
return float32(math.Pow(float64(v), g.mfdExp))
|
||||
}
|
||||
}
|
||||
|
||||
// mfdQ is the perimeter share facing each D8 direction divided by the distance to it, in the order of dx8
|
||||
// and dy8: NW N NE W E SW S SE. Cardinal 0.5, diagonal 0.25.
|
||||
var mfdQ = [8]float32{0.25, 0.5, 0.25, 0.5, 0.5, 0.25, 0.5, 0.25}
|
||||
|
||||
// AccumulateMFD fills Area with multiple-flow drainage area, in m².
|
||||
//
|
||||
// The order is Kahn's algorithm over the flow graph rather than a sort by elevation, and neither of the two
|
||||
// obvious alternatives works. The D8 stack cannot be reused: BuildStack is a depth-first walk of the donor
|
||||
// tree, so a deep node of one subtree precedes a shallow node of the next and the order is not descending in
|
||||
// elevation - a cell would send area to a neighbour that had already been processed, and the loss would fall
|
||||
// on the flanks, which is exactly where it cannot be afforded. A bucket sort cannot either: the queue
|
||||
// quantises to a centimetre while the flood's epsilon ladder across a filled flat is a millimetre a cell, so
|
||||
// ten cells of one descending chain share a bucket and a lake bed would leak its area.
|
||||
//
|
||||
// Kahn needs no elevation comparison at all. mfdPending[i] is how many strictly higher neighbours i still
|
||||
// owes; a cell is ready when the count reaches zero. Because "strictly lower" is a strict order the graph is
|
||||
// acyclic, so every cell is released exactly once - which is asserted, because the alternative is a drainage
|
||||
// area that is quietly too small in a two-hour bake.
|
||||
//
|
||||
// The counting pass is inside this function and not folded into ComputeReceivers, which already reads all
|
||||
// eight neighbours and could have produced it for nothing. It was, and it was wrong: the walk *consumes* the
|
||||
// counts, so a second call without an intervening ComputeReceivers seeded its whole queue at once and
|
||||
// returned a drainage area that was silently wrong rather than panicking. Run happens to call the two in
|
||||
// lockstep, so nothing would have caught it there. A pass that owns its own preconditions cannot be misused
|
||||
// that way, and this one is a pure gather, so it parallelises and costs almost nothing in wall clock.
|
||||
func (g *Grid) AccumulateMFD(h []float32, p float64) {
|
||||
n := g.W * g.H
|
||||
g.mfdMode, g.mfdExp = mfdModeFor(p)
|
||||
|
||||
field.Rows(g.H, func(y0, y1 int) {
|
||||
for y := y0; y < y1; y++ {
|
||||
for x := 0; x < g.W; x++ {
|
||||
i := y*g.W + x
|
||||
pend := uint8(0)
|
||||
for k := 0; k < 8; k++ {
|
||||
nx, ny := x+dx8[k], y+dy8[k]
|
||||
if nx < 0 || ny < 0 || nx >= g.W || ny >= g.H {
|
||||
continue
|
||||
}
|
||||
// How many neighbours will hand this cell a share: the ones strictly above it. The
|
||||
// weights below skip a neighbour when hn >= hc, so c sends to n exactly when
|
||||
// h[c] > h[n] - the same predicate, and it has to stay the same one or the walk ends
|
||||
// short.
|
||||
if h[ny*g.W+nx] > h[i] {
|
||||
pend++
|
||||
}
|
||||
}
|
||||
g.mfdPending[i] = pend
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
cell := float32(g.CellM * g.CellM)
|
||||
for i := range g.Area {
|
||||
g.Area[i] = cell
|
||||
}
|
||||
|
||||
if cap(g.mfdQueue) < n {
|
||||
g.mfdQueue = make([]int32, 0, n)
|
||||
}
|
||||
q := g.mfdQueue[:0]
|
||||
for i := 0; i < n; i++ {
|
||||
if g.mfdPending[i] == 0 {
|
||||
q = append(q, int32(i))
|
||||
}
|
||||
}
|
||||
|
||||
// p = 1 is the default and it is a multiply; hoisting the mode test out of the cell loop saves a call
|
||||
// and a switch on every one of the eight faces of every cell of every step.
|
||||
linear := g.mfdMode == mfdP1
|
||||
|
||||
var wgt [8]float32
|
||||
for read := 0; read < len(q); read++ {
|
||||
c := q[read]
|
||||
cx, cy := int(c)%g.W, int(c)/g.W
|
||||
hc := h[c]
|
||||
var total float32
|
||||
for k := 0; k < 8; k++ {
|
||||
nx, ny := cx+dx8[k], cy+dy8[k]
|
||||
if nx < 0 || ny < 0 || nx >= g.W || ny >= g.H {
|
||||
wgt[k] = 0
|
||||
continue
|
||||
}
|
||||
// The same predicate the counting pass above used, written the same way round, because the
|
||||
// counts and these weights have to agree cell for cell or the walk ends short.
|
||||
hn := h[ny*g.W+nx]
|
||||
if hn >= hc {
|
||||
wgt[k] = 0
|
||||
continue
|
||||
}
|
||||
dh := hc - hn
|
||||
w := dh * mfdQ[k]
|
||||
if !linear {
|
||||
w = g.mfdRaise(w)
|
||||
}
|
||||
wgt[k] = w
|
||||
total += w
|
||||
}
|
||||
// A fixed cell is base level: it absorbs what arrives and sends nothing on. It still has to release
|
||||
// the cells below it, or their counts would never reach zero and the walk would end short - which is
|
||||
// why the release
|
||||
// loop below is not inside the `total > 0` branch.
|
||||
share := float32(0)
|
||||
if total > 0 && !g.fixed[c] {
|
||||
share = g.Area[c] / total
|
||||
}
|
||||
for k := 0; k < 8; k++ {
|
||||
if wgt[k] == 0 {
|
||||
continue
|
||||
}
|
||||
ni := int32((cy+dy8[k])*g.W + cx + dx8[k])
|
||||
if share > 0 {
|
||||
g.Area[ni] += share * wgt[k]
|
||||
}
|
||||
g.mfdPending[ni]--
|
||||
if g.mfdPending[ni] == 0 {
|
||||
q = append(q, ni)
|
||||
}
|
||||
}
|
||||
}
|
||||
g.mfdQueue = q
|
||||
|
||||
if len(q) != n {
|
||||
// Unreachable unless the pending counts and the weights disagree about which neighbours are lower,
|
||||
// which would mean area silently going missing. Loud is the only useful behaviour here.
|
||||
panic("fluvial: MFD released " + itoa(len(q)) + " of " + itoa(n) + " cells; the pending counts and " +
|
||||
"the downslope test disagree")
|
||||
}
|
||||
}
|
||||
|
||||
func itoa(v int) string {
|
||||
if v == 0 {
|
||||
return "0"
|
||||
}
|
||||
var b [20]byte
|
||||
i := len(b)
|
||||
for v > 0 {
|
||||
i--
|
||||
b[i] = byte('0' + v%10)
|
||||
v /= 10
|
||||
}
|
||||
return string(b[i:])
|
||||
}
|
||||
Reference in New Issue
Block a user