aboutsummaryrefslogtreecommitdiffstats
path: root/tests/ellipsoid-test.scm
diff options
context:
space:
mode:
authorGravatar Peter McGoron 2025-01-10 16:22:02 -0500
committerGravatar Peter McGoron 2025-01-10 16:22:02 -0500
commitcd78c57222dfe6ad9906b95e6c0353629469c10f (patch)
treef0932a3d2f2be771f4ae6f4f528d6a6eecdb58c0 /tests/ellipsoid-test.scm
init
Diffstat (limited to '')
-rw-r--r--tests/ellipsoid-test.scm297
1 files changed, 297 insertions, 0 deletions
diff --git a/tests/ellipsoid-test.scm b/tests/ellipsoid-test.scm
new file mode 100644
index 0000000..53b1428
--- /dev/null
+++ b/tests/ellipsoid-test.scm
@@ -0,0 +1,297 @@
+; SPDX-FileCopyrightText: 2020 Linas Vepštas
+; SPDX-License-Identifier: MIT
+;
+; ellipsoid-test.scm
+;
+; Verify that the distribution of points on the surface of an
+; ellipsoid is uniform.
+;
+; Test proceeds by taking 2-D slices through the ellipsoid, and
+; verifying uniformity on that slice. Thus, the core test is for
+; ellipses.
+;
+
+; Sort a list of 2D vectors of floats into clock-wise order.
+; Assumes that `pts` is a list of 2D vectors of floats.
+(define (clockwise pts)
+ (sort pts (lambda (a b)
+ (if (and (< 0 (vector-ref a 1)) (< 0 (vector-ref b 1)))
+ (< (vector-ref b 0) (vector-ref a 0))
+ (if (and (< (vector-ref a 1) 0) (< (vector-ref b 1) 0))
+ (< (vector-ref a 0) (vector-ref b 0))
+ (< (vector-ref b 1) (vector-ref a 1)))))))
+
+; Verfiy that the routine above is not broken.
+; Returns #t if it is OK.
+(define (test-clockwise)
+ (define clock (list
+ '#(1 1e-3) '#(0.8 0.2) '#(0.2 0.8)
+ '#(0 1) '#(-0.2 0.8) '#(-0.8 0.2) '#(-1 1e-3)
+ '#(-1 -1e-3) '#(-0.8 -0.2) '#(-0.2 -0.8)
+ '#(0 -1) '#(0.2 -0.8) '#(0.8 -0.2) '#(1 -1e-3)))
+
+ (equal? (clockwise clock) clock))
+
+; Vector subtraction
+; Example usage: (vector-diff '#( 2 3) '#(0.5 0.7))
+(define (vector-sub a b)
+ (vector-map (lambda (ea eb) (- ea eb)) a b))
+
+; Newton differences - compute the difference between neighboring
+; points. Assumes `pts` is a list of vectors. Should be called with
+; `rv` set to the null list. (tail-recursive helper)
+(define (delta pts rv)
+ (if (null? (cdr pts)) (reverse! rv)
+ (delta (cdr pts) (cons (vector-diff (car pts) (cadr pts)) rv))))
+
+; Compute sum of a list of numbers
+(define (sum lst) (fold (lambda (x sum) (+ sum x)) 0 lst))
+
+; Compute sum of squares of a list of numbers
+(define (sumsq lst) (fold (lambda (x sum) (+ sum (* x x))) 0 lst))
+
+(define (mean lst) (/ (sum lst) (length lst)))
+(define (stddev lst)
+ (define avg (mean lst))
+ (sqrt (- (/ (sumsq lst) (length lst)) (* avg avg))))
+
+; -----------------------------------------------------------
+; Stuff for the complete elliptic integral
+(define pi 3.14159265358979)
+
+; factorial
+(define (fact n rv)
+ (if (zero? n) rv (fact (- n 1) (* n rv))))
+
+; Double factorial
+; https://en.wikipedia.org/wiki/Double_factorial
+(define (double-fact n rv)
+ (if (<= n 0) rv (double-fact (- n 2) (* n rv))))
+
+; Complete elliptic integral per wikipedia, see the Ivorty& Bessel
+; expansion. Here `a` and `b` are the axes.
+; https://en.wikipedia.org/wiki/Ellipse
+(define (complete-elliptic a b)
+ (define rh (/ (- a b) (+ a b)))
+ (define h (* rh rh))
+
+ (define precision 1e-10)
+
+ (define (ivory term n twon hn fact-n dfact-n sum)
+ (if (< term precision) (+ sum term)
+ ; (format #t "yo n= ~A term=~A 2^n=~A h^n=~A n!=~A n!!=~A sum=~A\n"
+ ; n term twon hn fact-n dfact-n sum)
+ (ivory
+ (/ (* dfact-n dfact-n hn) (* twon twon fact-n fact-n))
+ (+ n 1)
+ (* 2 twon)
+ (* h hn)
+ (* (+ n 1) fact-n)
+ (* (- (* 2 n) 1) dfact-n)
+ (+ term sum))))
+
+ (* pi (+ a b) (+ 1 (/ h 4)
+ (ivory (/ (* h h) 64) 3 8 (* h h h) 6 3 0.0))))
+
+; -----------------------------------------------------------
+
+; Test that a list of points are well-distributed on an ellipse.
+; Assumes that `points` is a list of 2D vectors of floats.
+;
+; Multiple tests are performed:
+; 1) This sums the differences between points, i.e. measures the
+; perimeter, and verifies that the perimiter is as pexpected,
+; given by the complete elliptic integral.
+; 2) Computes the RMS variation between the points, and verifies
+; that this is small.
+; 3) (non-automated) dump differences to file and verify they look good.
+;
+(define (verify-ellipse points)
+ ; Place in sorted order.
+ (define ordered-points (clockwise points))
+
+ ; Difference between neghboring points.
+ (define diffs (delta ordered-points '()))
+
+ ; Compute the distances between neighboring points
+ (define dists (map l2-norm diffs))
+
+ ; Sum of the intervals ... NOT including distance between
+ ; the very last and the very first points.
+ (define perimeter (sum dists))
+
+ ; Find major and minor axes
+ (define major
+ (fold (lambda (MAJ x)
+ (if (< MAJ x) x MAJ))
+ 0
+ (map l2-norm points)))
+
+ (define minor
+ (fold (lambda (MIN x)
+ (if (< MIN x) MIN x))
+ 1.0e308
+ (map l2-norm points)))
+
+ ; The expected perimiter
+ (define perim-exact (complete-elliptic major minor))
+
+ ; The normalized difference of measured and expected perimeters
+ ; Should almost always be less than ten, often less than two.
+ ; It should usually be positive, because we failed to count the
+ ; distance between the last and first point... and also an
+ ; O(delta^2) error cause this is Newton integration.
+ (define error
+ (abs (* (/ (- perim-exact perimeter) perim-exact) (length dists))))
+
+ ; If The points are normally eistributed, then the `dists` should
+ ; form a normal gaussian distribution, with unit stadndard
+ ; devviation. i.e. rms below should be near 1.0
+ (define rms (/ (* (stddev dists) (length dists)) perim-exact))
+
+ (format #t "Number of points: ~A\n" (length points))
+ (format #t "Measured perimeter: ~A\n" perimeter)
+ (format #t "Major and minor axes: ~A ~A\n" major minor)
+ (format #t "Expected perimeter: ~A\n" perim-exact)
+ (format #t "Relative error: ~A\n" error)
+ (format #t "RMS error: ~A\n" rms)
+ (newline)
+
+ (test-assert (< error 12))
+ (test-assert (< 0 error))
+ (test-assert (< 0.97 rms))
+ (test-assert (< 1.03 rms))
+)
+
+; ------------------------------------------------------
+;
+; Bonus points: uncomment the below, write results to a file
+; and view the graph. It should look sane.
+; This is commented out because it is for reference nly,
+; and does not need to be ported.
+;
+;;;(define (explore-ellipse points)
+;;; ; Place in sorted order.
+;;; (define ordered-points (clockwise points))
+;;;
+;;; ; Difference between neghboring points.
+;;; (define diffs (delta ordered-points '()))
+;;;
+;;; ; Compute the distances between neighboring points
+;;; (define dists (map l2-norm diffs))
+;;;
+;;; ; Compute moving average
+;;; (define (moving-avg lst window)
+;;; (map (lambda (offset) (avg (take (drop lst offset) window)))
+;;; (iota (- (length lst) window))))
+;;;
+;;; ; Window 300 points wide. The moving average should do a
+;;; ; random walk around an average value of ellipsoid-perimeter
+;;; ; divided by the number of points. The walk should be normally
+;;; ; distributed. Should be translation-invariant; there should
+;;; ; be no sine waves.
+;;; (define moving-300 (moving-avg dists 300))
+;;;
+;;; (define (list-to-file lst filename)
+;;; (let ((outport (open-file filename "w")))
+;;; (fold
+;;; (lambda (x i) (format outport "~A ~A\n" i x) (+ i 1))
+;;; 1
+;;; lst)
+;;; (close outport)))
+;;;
+;;; (list-to-file moving-300 "moving.dat")
+;;;)
+
+; ------------------------------------------------------
+; OK, now test some ellipses.
+
+(define (sample gen)
+ (map (lambda (x) (gen)) (iota 10000)))
+
+(verify-ellipse (sample (make-ellipsoid-generator '#(2 10))))
+(verify-ellipse (sample (make-ellipsoid-generator '#(2 10))))
+(verify-ellipse (sample (make-ellipsoid-generator '#(2 10))))
+(verify-ellipse (sample (make-ellipsoid-generator '#(2 10))))
+(verify-ellipse (sample (make-ellipsoid-generator '#(10 2))))
+(verify-ellipse (sample (make-ellipsoid-generator '#(3 8))))
+(verify-ellipse (sample (make-ellipsoid-generator '#(9 44))))
+(verify-ellipse (sample (make-ellipsoid-generator '#(9e3 4.4e3))))
+(verify-ellipse (sample (make-ellipsoid-generator '#(9e6 4.4e6))))
+(verify-ellipse (sample (make-ellipsoid-generator '#(0.03 0.16))))
+
+; ------------------------------------------------------
+; Now test higher-dimensional ellipsoids.
+; This is done by slicing ellipses out of them. The slicing is
+; approximate.
+
+; Take a two-2d slice out of ellipsoid having `axes`. Thickness of the
+; slice is `thickness`. Location of the slice is `where`. Caution:
+; may be extremely slow if a high-dimenional vector is given, or if
+; thickness is too thin. Will infinite-loop if `where` is not inside
+; the ellipsiod. Returns an n-dimensional point, of which the first
+; two coords are unconstrained.
+;
+; Usage:
+;
+; (define gen (make-slicer '#(2 10 6 4) 0.1 '#(0 0 2 1)))
+; (gen)
+(define (make-slicer axes thickness where)
+
+ ; Generator of points on n-dimensional ellipsoid
+ (define elli (make-ellipsoid-generator axes))
+
+ ; return true if point is in the slice.
+ (define (accept point)
+
+ ;; Take the slice off-center
+ (define diff
+ (vector-map (lambda (r s) (- r s)) point where))
+
+ ;; Return #t if the point is in the slice
+ (define (ok vec)
+ (vector-fold (lambda (pass coord idx)
+ (or (< idx 2)
+ (and pass (< (- 0 thickness) coord) (< coord thickness))))
+ #t vec (iota (vector-length vec)) ))
+
+ ; Test
+ (ok diff))
+
+ ; Keep trying until we have a point in the slice.
+ (define (try)
+ (define sample (elli))
+ (if (accept sample) sample (try)))
+
+ ; Return the looper
+ try)
+
+; Like above but drops all but the first two coordinates
+(define (two-d-slicer axes thickness where)
+ (define sli (make-slicer axes thickness where))
+
+ (lambda ()
+ (define sample (sli))
+ (vector (vector-ref sample 0) (vector-ref sample 1))))
+
+(define (sample-1K gen)
+ (map (lambda (x) (gen)) (iota 1000)))
+
+; ------------------------------------------------------
+; The higher-dimensional tests..
+
+; These tests don't work very well, aren't very stable.
+; The slice has to be really thin, otherwise one gets junky
+; samples which don't pass unit tests. But thin slices take a huge
+; amount of CPU time to run...
+(verify-ellipse (sample-1K (two-d-slicer '#(2 8 10) 0.02 '#(0 0 0))))
+
+(verify-ellipse (sample-1K (two-d-slicer '#(6 8 8) 0.04 '#(0 0 0))))
+(verify-ellipse (sample-1K (two-d-slicer '#(6 8 12) 0.04 '#(0 0 0))))
+(verify-ellipse (sample-1K (two-d-slicer '#(2 8 10) 0.04 '#(0 0 0))))
+
+; This takes too long to run
+; (verify-ellipse (sample-1K (two-d-slicer '#(5 8 10 7) 0.04 '#(0 0 0 0))))
+
+; --------------------- the end ------------------------
+; ------------------------------------------------------