Tooling
This commit is contained in:
@@ -18,7 +18,16 @@ import (
|
||||
// bucket, so what it leaves is pyramids with faces aligned to the grid — the blocky, ruler-cut facets that
|
||||
// are visible in any preview of a mountain belt here. Nothing about that is geology; it is the D8 stencil
|
||||
// printed onto the landscape. Nonlinear diffusion approaches the same limiting angle *asymptotically* and
|
||||
// through a symmetric five-point stencil, so there is no cut, no facet and no preferred direction.
|
||||
// through a symmetric stencil, so there is no cut, no facet and no preferred direction.
|
||||
//
|
||||
// The stencil is nine-point, and it has to be. Run's design is that the clamp cuts and this rounds off what
|
||||
// it cut before the next step sees it - but the clamp cuts along all eight neighbour directions and a
|
||||
// five-point stencil transports across four, so it cannot touch a diagonally-cut facet at all. That was not a
|
||||
// refinement, it was a hole in the stated design. The weights are 4/6 cardinal and 1/6 diagonal, which is the
|
||||
// isotropic nine-point Laplacian: on h = (a/2)(x^2+y^2) the cardinal faces sum to 2ad^2 and the diagonals to
|
||||
// 4ad^2, so (1/6)(8ad^2 + 4ad^2) = 2ad^2 = dx^2 * grad2(h), exactly what the five-point gave. coeff is
|
||||
// therefore unchanged. A diagonal face is sqrt(2) further away, so it carries its own critical height
|
||||
// difference; leaving that out would make every diagonal read as 1.41 times its true S/Sc.
|
||||
//
|
||||
// It is also mass-conserving, which the clamp is not: the flux out of one cell is the flux into its
|
||||
// neighbour by construction, so material shed from a divide arrives at the foot of the slope rather than
|
||||
@@ -57,6 +66,14 @@ func (g *Grid) DiffuseNonlinear(h []float32, d, sc, slopeCap, dt float64, maxSub
|
||||
dx := g.CellM
|
||||
dx2 := dx * dx
|
||||
|
||||
// The Courant number the sub-stepping aims for. The worst mode is the checkerboard: on the five-point
|
||||
// stencil its cardinal faces sum to -8*amp and the amplification is 1 - 8*coeff, stable to coeff 0.25;
|
||||
// on the nine-point the diagonals cancel and (4/6)*(-8*amp) leaves 1 - 5.333*coeff, stable to 0.375. Both
|
||||
// targets keep the same 1.25x margin under their own limit, and the extra room is most of what pays for
|
||||
// the four extra faces. Raising the target without the 4/6 and 1/6 weights, or adding the faces without
|
||||
// raising the target, is a scheme that checkerboards a few hundred steps in - which is the failure the
|
||||
// budget note below is about, and it does not announce itself.
|
||||
|
||||
// The steepest ground on the grid bounds D_eff for the whole call. Uplift is not applied in here and
|
||||
// diffusion only relaxes slopes, so nothing can get steeper part-way through and invalidate the bound.
|
||||
u := math.Min(g.maxSlopeRatio(h, sc), slopeCap)
|
||||
@@ -64,7 +81,7 @@ func (g *Grid) DiffuseNonlinear(h []float32, d, sc, slopeCap, dt float64, maxSub
|
||||
// What the sub-step budget can pay for. Lowering the cap rather than truncating the sub-step count is
|
||||
// what keeps this stable: a truncated count leaves alpha above 0.25 and the surface checkerboards a few
|
||||
// hundred steps later, which is precisely the sort of failure that does not announce itself.
|
||||
if budget := float64(maxSub) * 0.2 * dx2 / (d * dt); f > budget {
|
||||
if budget := float64(maxSub) * subTargetNine * dx2 / (d * dt); f > budget {
|
||||
f = budget
|
||||
u = invStiffness(f)
|
||||
}
|
||||
@@ -75,7 +92,7 @@ func (g *Grid) DiffuseNonlinear(h []float32, d, sc, slopeCap, dt float64, maxSub
|
||||
f = 1
|
||||
u = 0
|
||||
}
|
||||
sub := int(math.Ceil(d * f * dt / dx2 / 0.2))
|
||||
sub := int(math.Ceil(d * f * dt / dx2 / subTargetNine))
|
||||
if sub < 1 {
|
||||
sub = 1
|
||||
}
|
||||
@@ -87,6 +104,7 @@ func (g *Grid) DiffuseNonlinear(h []float32, d, sc, slopeCap, dt float64, maxSub
|
||||
// factor of dx too large, which pins every face against the cap and quietly turns the whole law into
|
||||
// linear diffusion with a constant multiplier.
|
||||
dhCrit := float32(sc * dx)
|
||||
dhCritDiag := float32(sc * dx * math.Sqrt2)
|
||||
|
||||
src := h
|
||||
tmp := g.scratch[:len(h)]
|
||||
@@ -100,13 +118,17 @@ func (g *Grid) DiffuseNonlinear(h []float32, d, sc, slopeCap, dt float64, maxSub
|
||||
continue
|
||||
}
|
||||
c := src[i]
|
||||
// The net inflow over the four faces. Each face is evaluated from both of its cells,
|
||||
// The net inflow over all eight faces. Each face is evaluated from both of its cells,
|
||||
// which costs twice and buys a gather: no two goroutines ever write the same cell.
|
||||
net := flux(clampAt(src, g.W, g.H, x-1, y)-c, dhCrit, uCap) +
|
||||
card := flux(clampAt(src, g.W, g.H, x-1, y)-c, dhCrit, uCap) +
|
||||
flux(clampAt(src, g.W, g.H, x+1, y)-c, dhCrit, uCap) +
|
||||
flux(clampAt(src, g.W, g.H, x, y-1)-c, dhCrit, uCap) +
|
||||
flux(clampAt(src, g.W, g.H, x, y+1)-c, dhCrit, uCap)
|
||||
tmp[i] = c + coeff*net
|
||||
diag := flux(clampAt(src, g.W, g.H, x-1, y-1)-c, dhCritDiag, uCap) +
|
||||
flux(clampAt(src, g.W, g.H, x+1, y-1)-c, dhCritDiag, uCap) +
|
||||
flux(clampAt(src, g.W, g.H, x-1, y+1)-c, dhCritDiag, uCap) +
|
||||
flux(clampAt(src, g.W, g.H, x+1, y+1)-c, dhCritDiag, uCap)
|
||||
tmp[i] = c + coeff*(nineCard*card+nineDiag*diag)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -114,6 +136,14 @@ func (g *Grid) DiffuseNonlinear(h []float32, d, sc, slopeCap, dt float64, maxSub
|
||||
}
|
||||
}
|
||||
|
||||
// The isotropic nine-point Laplacian's weights, and the Courant target its stability allows. See
|
||||
// DiffuseNonlinear.
|
||||
const (
|
||||
nineCard = float32(4.0 / 6.0)
|
||||
nineDiag = float32(1.0 / 6.0)
|
||||
subTargetNine = 0.3
|
||||
)
|
||||
|
||||
// flux is q/D for one face, in height differences rather than slopes: one factor of the cell spacing cancels
|
||||
// against the divergence and is carried in coeff instead. dhCrit is the height difference that corresponds to
|
||||
// Sc across one cell, so dh/dhCrit is exactly S/Sc. u is capped so the denominator cannot reach zero.
|
||||
@@ -152,9 +182,19 @@ func invStiffness(f float64) float64 {
|
||||
return lo
|
||||
}
|
||||
|
||||
// maxSlopeRatio is the steepest face on the grid as a fraction of Sc. Cardinal neighbours only, because those
|
||||
// are the faces the five-point stencil actually transports across.
|
||||
// maxSlopeRatio is the steepest face on the grid as a fraction of Sc, over every face the stencil transports
|
||||
// across - which since the stencil went to nine points means the diagonals too. A diagonal face is compared
|
||||
// against its own critical height difference, sqrt(2) larger, so what comes back is a slope ratio either way.
|
||||
//
|
||||
// What this number is for is worth being exact about, because it looks like physics and is not. It bounds the
|
||||
// stiffening for the whole call, and the flux law only caps a face when that face exceeds the bound - so on a
|
||||
// grid whose steepest face is the bound, no face is capped and the value has no effect on any cell. Its one
|
||||
// real job is to decide how many sub-steps the call pays for, which is a cost question. Where it does reach
|
||||
// the physics is when the sub-step budget cannot buy the grid's own maximum; the cap is then lowered to what
|
||||
// the budget affords, and that value is the manifest's - D, dt, the cell and MaxHillslopeSub - and not the
|
||||
// grid's, so a planet decomposed two ways still answers the same.
|
||||
func maxSlopeRatio(h []float32, w, hgt int, sc, cellM float64) float64 {
|
||||
invDiag := float32(1 / math.Sqrt2)
|
||||
var maxDiff float32
|
||||
for y := 0; y < hgt; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
@@ -164,6 +204,16 @@ func maxSlopeRatio(h []float32, w, hgt int, sc, cellM float64) float64 {
|
||||
if dv := abs32(h[i+1] - c); dv > maxDiff {
|
||||
maxDiff = dv
|
||||
}
|
||||
if y+1 < hgt {
|
||||
if dv := abs32(h[i+w+1]-c) * invDiag; dv > maxDiff {
|
||||
maxDiff = dv
|
||||
}
|
||||
}
|
||||
if y > 0 {
|
||||
if dv := abs32(h[i-w+1]-c) * invDiag; dv > maxDiff {
|
||||
maxDiff = dv
|
||||
}
|
||||
}
|
||||
}
|
||||
if y+1 < hgt {
|
||||
if dv := abs32(h[i+w] - c); dv > maxDiff {
|
||||
|
||||
Reference in New Issue
Block a user