Tooling
This commit is contained in:
@@ -48,3 +48,115 @@ func TestClampToReposeCutsACone(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user