aboutsummaryrefslogtreecommitdiffstats
path: root/tests/sphere-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/sphere-test.scm
init
Diffstat (limited to 'tests/sphere-test.scm')
-rw-r--r--tests/sphere-test.scm301
1 files changed, 301 insertions, 0 deletions
diff --git a/tests/sphere-test.scm b/tests/sphere-test.scm
new file mode 100644
index 0000000..3ccb67f
--- /dev/null
+++ b/tests/sphere-test.scm
@@ -0,0 +1,301 @@
+;;; SPDX-FileCopyrightText: 2020 Arvydas Silanskas
+;;; SPDX-FileCopyrightText: 2024 Bradley J Lucier
+;;; SPDX-License-Identifier: MIT
+;;;
+;;; Take REPS samples from unit sphere, verify random distribution.
+;;;
+;;; This test checks that:
+;;; * Every sample has unit length, within numerical tolerance.
+;;; * The REPS samples are uniformly distributed.
+;;; * Rotations of the REPS samples are uniformly distributed.
+(define (test-sphere sphereg dim-sizes REPS rotate?)
+ (define random-int (random-source-make-integers (current-random-source)))
+ (define random-real (random-source-make-reals (current-random-source)))
+ (define N (- (vector-length dim-sizes) 1))
+
+ ;; Fix list of samples
+ (define samples
+ (generator->list (gtake sphereg REPS)))
+
+ (define (l2-norm VEC)
+ (sqrt (vector-fold
+ (lambda (sum x l) (+ sum (/ (* x x)
+ (* l l))))
+ 0
+ VEC
+ dim-sizes)))
+
+ ;; Rotate the j'th amnd k'th coordinates of a vector VEC
+ ;; by cosine co and sine si
+ (define (pair-rot VEC j k co si)
+ (define oj (vector-ref VEC j))
+ (define ok (vector-ref VEC k))
+ (define nj (+ (* co oj) (* si ok)))
+ (define nk (+ (* (- si) oj) (* co ok)))
+ (list->vector
+ (map (lambda (index)
+ (cond
+ ((= index j) nj)
+ ((= index k) nk)
+ (else (vector-ref VEC index))))
+ (iota (vector-length VEC)))))
+
+ ;; Apply a random rotation to a collection of vectors
+ (define how-many-rots
+ (if (< 10 N) 10 N))
+
+ (define (arb-rot VEC-LIST)
+ (define j (random-int N))
+ (define k (+ j 1 (random-int (- N j))))
+ (define theta (* 3.14 (random-real)))
+ (define co (cos theta))
+ (define si (sin theta))
+ (define rvl
+ (map (lambda (vec)
+ (pair-rot vec j k co si))
+ VEC-LIST))
+ (if (not (= 0 (random-int how-many-rots)))
+ (arb-rot rvl)
+ rvl))
+
+ ;; Expect a vector approaching zero. That is, each individual
+ ;; coordinate should be uniformly randomly distributed in the
+ ;; interval [-1,1]. The sum of REPS samples of these should
+ ;; converge to zero. The standard deviation of a uniform
+ ;; distribution is sqrt(REPS/12).
+ ;; https://en.wikipedia.org/wiki/Continuous_uniform_distribution
+ ;; So setting max bound of 9 stddev should allow it to usually
+ ;; pass.
+ (define (converge-to-zero samples)
+ (fold (lambda (acc sample) (vector-map + sample acc))
+ (make-vector REPS 0.0)
+ samples))
+
+ (define (should-be-zero samples)
+ (l2-norm (converge-to-zero samples)))
+
+ (define (norm-should-be-zero samples)
+ (/ (should-be-zero samples) (sqrt (/ REPS 12.0))))
+
+ (define (check-zero samples)
+ (define num-stddev 9.0)
+ (define zz (norm-should-be-zero samples))
+ (test-assert (< zz num-stddev)))
+
+ ;; maximum allowed tolerance for radius deviation
+ (define EPS (* 2e-15 (sqrt N)))
+
+ ;; Each individual sphere radius should be 1.0 to within float
+ ;; tolerance.
+ (for-each
+ (lambda (SAMP)
+ (test-approximate 1.0 (l2-norm SAMP) EPS))
+ samples)
+
+ ;; The distribution should be zero
+ (check-zero samples)
+
+ ;; Rotate wildly. Should still be uniform.
+ (when rotate?
+ (for-each
+ (lambda (junk) (check-zero (arb-rot samples)))
+ (make-list 12))))
+
+(define (test-ball ballg dim-sizes)
+ (define (l2-norm VEC)
+ (sqrt (vector-fold
+ (lambda (sum x l) (+ sum (/ (* x x)
+ (* l l))))
+ 0
+ VEC
+ dim-sizes)))
+
+ (define (test-ball-generates-on-radius radius err)
+ (test-assert
+ (generator-any
+ (lambda (vec)
+ (define n (l2-norm vec))
+ (and (> n (- radius err))
+ (< n (+ radius err))))
+ (gtake ballg 10000))))
+
+ (define (test-ball-avg-zero N)
+ (define vec-sum
+ (generator-fold
+ (lambda (vec acc)
+ (vector-map + vec acc))
+ (make-vector (vector-length dim-sizes) 0.0)
+ (gtake ballg N)))
+ (define avg-vec
+ (vector-map
+ (lambda (e)
+ (/ e N))
+ vec-sum))
+ (define n (l2-norm avg-vec))
+ (test-assert (< n 1)))
+
+ (test-ball-generates-on-radius 0.0 0.1)
+ (test-ball-generates-on-radius 0.5 0.1)
+ (test-ball-generates-on-radius 1.0 0.1)
+
+ (test-ball-avg-zero 5000))
+
+ ;; Number of standard deviation difference from the expected value
+ ;; before we say a test failed. If set to 2, then one out of
+ ;; 20 tests will fail even if the code is correct. Setting it to
+ ;; 3 means that only one out of a 1000 tests will fail even if the
+ ;; code is correct.
+
+ (define STDs 3)
+
+ (define (test-ellipsoid a b N)
+
+ (define epsilon 1e-10)
+
+ (define pi (* 4 (atan 1)))
+
+ (define g (make-ellipsoid-generator (vector a b)))
+
+ (define points (generator->list (gtake g N)))
+
+ ;; The points on the "top" of the ellipse, with
+ ;; x between -a/2 and a/2
+
+ (define top
+ (filter (lambda (v)
+ (and (< (- (/ a 2)) (vector-ref v 0) (/ a 2))
+ (< 0 (vector-ref v 1))))
+ points))
+
+ ;; The points on the "right" of the ellipse, with
+ ;; y between -b/2 and b/2
+
+ (define right
+ (filter (lambda (v)
+ (and (< (- (/ b 2)) (vector-ref v 1) (/ b 2))
+ (< 0 (vector-ref v 0))))
+ points))
+
+ (define (arc-length a b)
+ ;; parametrization: (a\cos t, b\sin t)
+ ;; this is the norm of the derivative
+ (lambda (t)
+ (sqrt (+ (square (* a (sin t)))
+ (square (* b (cos t)))))))
+
+ (define (simpsons-rule f t0 t1 N)
+ ;; O(Delta^4) numerical integration
+ ;; integrate f between t0 and t1 with N intervals
+ (let* ((Delta (/ (- t1 t0) N))
+ (sum1 (do ((i 0 (+ i 1))
+ (sum 0. (+ sum
+ (f (+ t0 (* i Delta)))
+ (f (+ t0 (* (+ i 1) Delta))))))
+ ((= i N) sum)))
+ (sum2 (do ((i 0 (+ i 1))
+ (sum 0. (+ sum (f (+ t0 (* (+ i 0.5) Delta))))))
+ ((= i N) sum))))
+ (* Delta (/ (+ (* 4 sum2) sum1) 6))))
+
+ (define p-right
+ (/ (simpsons-rule (arc-length a b) (- (* pi 1/6)) (* pi 1/6) 100)
+ (simpsons-rule (arc-length a b) 0 (* pi 2) 100)))
+
+ (define p-top
+ (/ (simpsons-rule (arc-length a b) (* pi 1/3) (* pi 2/3) 100)
+ (simpsons-rule (arc-length a b) 0 (* pi 2) 100)))
+
+ ;; test that they're all more-or-less on the ellipse
+
+ (test-assert (every (lambda (p)
+ (< (abs (- (sqrt (+ (square (/ (vector-ref p 0) a))
+ (square (/ (vector-ref p 1) b))))
+ 1))
+ epsilon))
+ points))
+
+ (for-each (lambda (p m)
+ ;; p = probability of landing in arc, m = measured number
+ (test-assert (< (abs (- (* p N) m))
+ (* STDs (sqrt (* N p (- 1 p)))))))
+ (list p-right p-top)
+ (map length (list right top)))
+ )
+
+ (define (test-ellipse a b N)
+
+ ;; This test with two-dimensional ellipses stands in for all
+ ;; dimensions, as the code to generate points is independent
+ ;; of dimension
+
+ (define pi (* 4 (atan 1)))
+
+ (define interior-points
+ (generator->list (gtake (make-ball-generator (vector a b)) N)))
+
+ (define (in-ellipse? point)
+ (< (+ (square (/ (vector-ref point 0) a))
+ (square (/ (vector-ref point 1) b)))
+ 1))
+
+ ;; Find points inside rectangles inside various parts
+ ;; of the ellipse
+
+ (define center
+ (filter (lambda (v)
+ (and (< (- (/ a 4))
+ (vector-ref v 0)
+ (/ a 4))
+ (< (- (/ b 4))
+ (vector-ref v 1)
+ (/ b 4))))
+ interior-points))
+
+ (define right-x
+ ;; (right-x b/4) is on the ellipse
+ (* a (sqrt 15/16)))
+
+ (define right
+ (filter (lambda (v)
+ (and (< (- right-x (/ a 2))
+ (vector-ref v 0)
+ right-x)
+ (< (- (/ b 4))
+ (vector-ref v 1)
+ (/ b 4))))
+ interior-points))
+
+ (define top-y
+ ;; (a/4 top-y) is on the ellipse
+ (* b (sqrt 15/16)))
+
+ (define top
+ (filter (lambda (v)
+ (and (< (- (/ a 4))
+ (vector-ref v 0)
+ (/ a 4))
+ (< (- top-y (/ b 2))
+ (vector-ref v 1)
+ top-y)))
+ interior-points))
+
+ ;; p is he fraction of the area in the ellipse
+ ;; contained in the rectangles (it's all the same).
+
+ #;(define p
+ (/ (* (/ a 2) (/ b 2))
+ (* pi a b)))
+
+ (define p (/ (* 4 pi)))
+
+ ;; check that all the points are truly inside the ellipse.
+
+ (test-assert (every in-ellipse? interior-points))
+
+ (for-each (lambda (p m)
+ ;; p = probability of landing in rectangle,
+ ;; m = measured number of events
+ (test-assert (< (abs (- (* p N) m))
+ (* STDs (sqrt (* N p (- 1 p)))))))
+ (list p p p)
+ (map length (list center right top))))