This commit is contained in:
Rainer Leit
2026-09-25 17:02:24 +03:00
parent cc43ed8dc8
commit 9597629951
2149 changed files with 460234 additions and 1770 deletions
@@ -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)
}
}