163 lines
5.6 KiB
Go
163 lines
5.6 KiB
Go
package fluvial
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
// TestClampToReposeCutsACone is the smallest possible check on the constraint: a cone far steeper than the
|
|
// repose angle must come back at or under it.
|
|
func TestClampToReposeCutsACone(t *testing.T) {
|
|
const (
|
|
w, h = 81, 81
|
|
cellM = 10.0
|
|
talus = 0.4 // about 22 degrees
|
|
)
|
|
base := make([]bool, w*h)
|
|
field := make([]float32, w*h)
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
d := math.Hypot(float64(x-w/2), float64(y-h/2)) * cellM
|
|
field[y*w+x] = float32(math.Max(0, 800-2.0*d)) // slope 2.0, five times repose
|
|
}
|
|
}
|
|
g := NewGrid(w, h, cellM, base)
|
|
g.SetElevationRange(-100, 2000)
|
|
removed := g.ClampToRepose(field, talus)
|
|
|
|
worst := 0.0
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
for k := 0; k < 8; k++ {
|
|
nx, ny := x+dx8[k], y+dy8[k]
|
|
if nx < 0 || ny < 0 || nx >= w || ny >= h {
|
|
continue
|
|
}
|
|
d := cellM
|
|
if dx8[k] != 0 && dy8[k] != 0 {
|
|
d = cellM * math.Sqrt2
|
|
}
|
|
if s := float64(field[y*w+x]-field[ny*w+nx]) / d; s > worst {
|
|
worst = s
|
|
}
|
|
}
|
|
}
|
|
}
|
|
t.Logf("removed %.2f m mean; steepest slope now %.3f (repose %.3f)", removed, worst, talus)
|
|
if worst > talus*1.02 {
|
|
t.Errorf("steepest slope %.3f exceeds repose %.3f", worst, talus)
|
|
}
|
|
}
|
|
|
|
// TestClampToReposeIsIsotropic asks what shape is left, which the test above cannot: a four-sided pyramid
|
|
// satisfies "no slope exceeds repose" exactly, so the constraint check says nothing about whether the clamp
|
|
// cut a cone or cut a pyramid.
|
|
//
|
|
// Clamp a cone far above repose, then for each of 360 azimuths find by bisection the radius at which the
|
|
// surface falls through a fixed height. A cone gives a constant radius; the amplitudes of the four-fold and
|
|
// eight-fold Fourier components of that radius, as a fraction of its mean, say how far from one it is.
|
|
//
|
|
// What the numbers turn out to be, and what they are not. Measured 0.97 % four-fold and 2.39 % eight-fold -
|
|
// and *identical* with the pop-order jitter, with the allowance jitter, with both and with neither. On a cone
|
|
// no two cells share a bucket, because the surface falls twenty metres a cell against a one-centimetre
|
|
// bucket, so the ordering bias this file's jitter removes has nothing to bite on here. The residual is
|
|
// geometry: a path to a point at 22.5 degrees has to be built of cardinal and diagonal steps, and the octile
|
|
// distance it accumulates exceeds the straight line by up to 8 %, so an eight-connected clamp cuts an
|
|
// octagon out of a cone whatever order it works in. That is irreducible without a wider neighbourhood, and
|
|
// the thresholds below sit above it: this test guards against a regression to something far worse, and
|
|
// TestBucketPQDoesNotPreferRasterOrder is what actually holds the ordering honest.
|
|
func TestClampToReposeIsIsotropic(t *testing.T) {
|
|
const (
|
|
w, h = 201, 201
|
|
cellM = 10.0
|
|
talus = 0.4
|
|
level = 300.0
|
|
)
|
|
field := make([]float32, w*h)
|
|
for y := 0; y < h; y++ {
|
|
for x := 0; x < w; x++ {
|
|
d := math.Hypot(float64(x-w/2), float64(y-h/2)) * cellM
|
|
field[y*w+x] = float32(math.Max(0, 2000-2.0*d))
|
|
}
|
|
}
|
|
g := NewGrid(w, h, cellM, make([]bool, w*h))
|
|
g.SetSeed(37125)
|
|
g.SetElevationRange(-100, 4000)
|
|
g.ClampToRepose(field, talus)
|
|
|
|
at := func(fx, fy float64) float64 { // bilinear, in cells
|
|
x0, y0 := int(fx), int(fy)
|
|
if x0 < 0 || y0 < 0 || x0 >= w-1 || y0 >= h-1 {
|
|
return 0
|
|
}
|
|
tx, ty := fx-float64(x0), fy-float64(y0)
|
|
return (1-ty)*((1-tx)*float64(field[y0*w+x0])+tx*float64(field[y0*w+x0+1])) +
|
|
ty*((1-tx)*float64(field[(y0+1)*w+x0])+tx*float64(field[(y0+1)*w+x0+1]))
|
|
}
|
|
|
|
const rays = 360
|
|
var sum, c4r, c4i, c8r, c8i float64
|
|
for i := 0; i < rays; i++ {
|
|
th := 2 * math.Pi * float64(i) / rays
|
|
cs, sn := math.Cos(th), math.Sin(th)
|
|
lo, hi := 0.0, float64(w/2-2)
|
|
for n := 0; n < 40; n++ { // bisect on the radius where the surface crosses `level`
|
|
mid := (lo + hi) / 2
|
|
if at(float64(w/2)+mid*cs, float64(h/2)+mid*sn) > level {
|
|
lo = mid
|
|
} else {
|
|
hi = mid
|
|
}
|
|
}
|
|
r := (lo + hi) / 2
|
|
sum += r
|
|
c4r += r * math.Cos(4*th)
|
|
c4i += r * math.Sin(4*th)
|
|
c8r += r * math.Cos(8*th)
|
|
c8i += r * math.Sin(8*th)
|
|
}
|
|
a4 := 2 * math.Hypot(c4r, c4i) / sum
|
|
a8 := 2 * math.Hypot(c8r, c8i) / sum
|
|
t.Logf("clamped cone: mean radius %.2f cells, four-fold %.2f%%, eight-fold %.2f%%",
|
|
sum/rays, a4*100, a8*100)
|
|
if a4 > 0.02 || a8 > 0.04 {
|
|
t.Errorf("the clamped cone is %.2f%% four-fold and %.2f%% eight-fold against 0.97 and 2.39 measured: "+
|
|
"it is a pyramid, not an octagon", a4*100, a8*100)
|
|
}
|
|
}
|
|
|
|
// TestBucketPQDoesNotPreferRasterOrder is the unit underneath it. Pushed plain, cells at one elevation come
|
|
// back in exactly reverse insertion order, which is a Spearman correlation of -1.
|
|
func TestBucketPQDoesNotPreferRasterOrder(t *testing.T) {
|
|
const n = 4096
|
|
order := func(jitter bool) float64 {
|
|
q := newBucketPQ(0, 100)
|
|
for i := 0; i < n; i++ {
|
|
if jitter {
|
|
q.pushJittered(50, int32(i), (hashXY(1, int32(i%64), int32(i/64), jitterReposeOrder)-0.5)*2*reposeOrderBuckets)
|
|
} else {
|
|
q.push(50, int32(i))
|
|
}
|
|
}
|
|
var sum float64
|
|
for pos := 0; pos < n; pos++ {
|
|
idx := float64(q.pop())
|
|
sum += (float64(pos) - float64(n-1)/2) * (idx - float64(n-1)/2)
|
|
}
|
|
var varr float64
|
|
for i := 0; i < n; i++ {
|
|
d := float64(i) - float64(n-1)/2
|
|
varr += d * d
|
|
}
|
|
return sum / varr
|
|
}
|
|
plain, jittered := order(false), order(true)
|
|
t.Logf("pop order against flat index: plain %.3f, jittered %.3f", plain, jittered)
|
|
if plain > -0.99 {
|
|
t.Errorf("plain push no longer pops in reverse insertion order (%.3f); this test's premise is gone", plain)
|
|
}
|
|
if math.Abs(jittered) > 0.05 {
|
|
t.Errorf("jittered push still correlates with flat index at %.3f", jittered)
|
|
}
|
|
}
|