219 lines
7.8 KiB
Go
219 lines
7.8 KiB
Go
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:])
|
|
}
|