View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.rng.sampling;
18  
19  import java.util.Arrays;
20  import org.apache.commons.math3.stat.inference.ChiSquareTest;
21  import org.apache.commons.rng.UniformRandomProvider;
22  import org.apache.commons.rng.core.source64.SplitMix64;
23  import org.junit.jupiter.api.Assertions;
24  import org.junit.jupiter.api.Test;
25  
26  /**
27   * Test for {@link UnitSphereSampler}.
28   */
29  class UnitSphereSamplerTest {
30      /** 2 pi */
31      private static final double TWO_PI = 2 * Math.PI;
32  
33      /**
34       * Test a non-positive dimension.
35       */
36      @Test
37      void testInvalidDimensionThrows() {
38          // Use instance constructor not factory constructor to exercise 1.X public API
39          Assertions.assertThrows(IllegalArgumentException.class,
40              () -> new UnitSphereSampler(0, null));
41      }
42  
43      /**
44       * Test a non-positive dimension.
45       */
46      @Test
47      void testInvalidDimensionThrowsWithFactoryConstructor() {
48          Assertions.assertThrows(IllegalArgumentException.class,
49              () -> UnitSphereSampler.of(null, 0));
50      }
51  
52      /**
53       * Test the distribution of points in one dimension.
54       */
55      @Test
56      void testDistribution1D() {
57          testDistribution1D(false);
58      }
59  
60      /**
61       * Test the distribution of points in one dimension with the factory constructor.
62       */
63      @Test
64      void testDistribution1DWithFactoryConstructor() {
65          testDistribution1D(true);
66      }
67  
68      /**
69       * Test the distribution of points in one dimension.
70       * RNG-130: All samples should be 1 or -1.
71       */
72      private static void testDistribution1D(boolean factoryConstructor) {
73          final UniformRandomProvider rng = RandomAssert.createRNG();
74          final UnitSphereSampler generator = createUnitSphereSampler(1, rng, factoryConstructor);
75          final int samples = 10000;
76          // Count the negatives.
77          int count = 0;
78          for (int i = 0; i < samples; i++) {
79              // Test the deprecated method once in the test suite.
80              @SuppressWarnings("deprecation")
81              final double[] v = generator.nextVector();
82              Assertions.assertEquals(1, v.length);
83              final double d = v[0];
84              if (d == -1.0) {
85                  count++;
86              } else if (d != 1.0) {
87                  // RNG-130: All samples should be 1 or -1.
88                  Assertions.fail("Invalid unit length: " + d);
89              }
90          }
91          // Test the number of negatives is approximately 50%
92          assertMonobit(count, samples);
93      }
94  
95      /**
96       * Assert that the number of 1 bits is approximately 50%. This is based upon a fixed-step "random
97       * walk" of +1/-1 from zero.
98       *
99       * <p>The test is equivalent to the NIST Monobit test with a fixed p-value of 0.01. The number of
100      * bits is recommended to be above 100.</p>
101      *
102      * @see <a href="https://csrc.nist.gov/publications/detail/sp/800-22/rev-1a/final">Bassham, et al
103      *      (2010) NIST SP 800-22: A Statistical Test Suite for Random and Pseudorandom Number
104      *      Generators for Cryptographic Applications. Section 2.1.</a>
105      *
106      * @param bitCount The bit count.
107      * @param numberOfBits Number of bits.
108      */
109     private static void assertMonobit(int bitCount, int numberOfBits) {
110         // Convert the bit count into a number of +1/-1 steps.
111         final double sum = 2.0 * bitCount - numberOfBits;
112         // The reference distribution is Normal with a standard deviation of sqrt(n).
113         // Check the absolute position is not too far from the mean of 0 with a fixed
114         // p-value of 0.01 taken from a 2-tailed Normal distribution. Computation of
115         // the p-value requires the complimentary error function.
116         final double absSum = Math.abs(sum);
117         final double max = Math.sqrt(numberOfBits) * 2.576;
118         Assertions.assertTrue(absSum <= max,
119             () -> "Walked too far astray: " + absSum + " > " + max +
120                   " (test will fail randomly about 1 in 100 times)");
121     }
122 
123     /**
124      * Test the distribution of points in two dimensions.
125      */
126     @Test
127     void testDistribution2D() {
128         testDistribution2D(false);
129     }
130 
131     /**
132      * Test the distribution of points in two dimensions with the factory constructor.
133      */
134     @Test
135     void testDistribution2DWithFactoryConstructor() {
136         testDistribution2D(true);
137     }
138 
139     /**
140      * Test the distribution of points in two dimensions.
141      * Obtains polar coordinates and checks the angle distribution is uniform.
142      */
143     private static void testDistribution2D(boolean factoryConstructor) {
144         final UniformRandomProvider rng = RandomAssert.createRNG();
145         final UnitSphereSampler generator = createUnitSphereSampler(2, rng, factoryConstructor);
146 
147         // In 2D, angles with a given vector should be uniformly distributed.
148         final int angleBins = 200;
149         final long[] observed = new long[angleBins];
150         final int steps = 100000;
151         for (int i = 0; i < steps; ++i) {
152             final double[] v = generator.sample();
153             Assertions.assertEquals(2, v.length);
154             Assertions.assertEquals(1.0, length(v), 1e-10);
155             // Get the polar angle bin from xy
156             final int angleBin = angleBin(angleBins, v[0], v[1]);
157             observed[angleBin]++;
158         }
159 
160         final double[] expected = new double[observed.length];
161         Arrays.fill(expected, (double) steps / observed.length);
162         final double p = new ChiSquareTest().chiSquareTest(expected, observed);
163         Assertions.assertFalse(p < 0.01, () -> "p-value too small: " + p);
164     }
165 
166     /**
167      * Test the distribution of points in three dimensions.
168      */
169     @Test
170     void testDistribution3D() {
171         testDistribution3D(false);
172     }
173 
174     /**
175      * Test the distribution of points in three dimensions with the factory constructor.
176      */
177     @Test
178     void testDistribution3DWithFactoryConstructor() {
179         testDistribution3D(true);
180     }
181 
182     /**
183      * Test the distribution of points in three dimensions.
184      * Obtains spherical coordinates and checks the distribution is uniform.
185      */
186     private static void testDistribution3D(boolean factoryConstructor) {
187         final UniformRandomProvider rng = RandomAssert.createRNG();
188         final UnitSphereSampler generator = createUnitSphereSampler(3, rng, factoryConstructor);
189 
190         // Get 3D spherical coordinates. Assign to a bin.
191         //
192         // polar angle (theta) in [0, 2pi)
193         // azimuthal angle (phi) in [0, pi)
194         //
195         // theta = arctan(y/x) and is uniformly distributed
196         // phi = arccos(z); and cos(phi) is uniformly distributed
197         final int angleBins = 20;
198         final int depthBins = 10;
199         final long[] observed = new long[angleBins * depthBins];
200         final int steps = 1000000;
201         for (int i = 0; i < steps; ++i) {
202             final double[] v = generator.sample();
203             Assertions.assertEquals(3, v.length);
204             Assertions.assertEquals(1.0, length(v), 1e-10);
205             // Get the polar angle bin from xy
206             final int angleBin = angleBin(angleBins, v[0], v[1]);
207             // Map cos(phi) = z from [-1, 1) to [0, 1) then assign a bin
208             final int depthBin = (int) (depthBins * (v[2] + 1) / 2);
209             observed[depthBin * angleBins + angleBin]++;
210         }
211 
212         final double[] expected = new double[observed.length];
213         Arrays.fill(expected, (double) steps / observed.length);
214         final double p = new ChiSquareTest().chiSquareTest(expected, observed);
215         Assertions.assertFalse(p < 0.01, () -> "p-value too small: " + p);
216     }
217 
218     /**
219      * Test the distribution of points in four dimensions.
220      */
221     @Test
222     void testDistribution4D() {
223         testDistribution4D(false);
224     }
225 
226     /**
227      * Test the distribution of points in four dimensions with the factory constructor.
228      */
229     @Test
230     void testDistribution4DWithFactoryConstructor() {
231         testDistribution4D(true);
232     }
233 
234     /**
235      * Test the distribution of points in four dimensions.
236      * Checks the surface of the 3-sphere can be used to generate uniform samples within a circle.
237      */
238     private static void testDistribution4D(boolean factoryConstructor) {
239         final UniformRandomProvider rng = RandomAssert.createRNG();
240         final UnitSphereSampler generator = createUnitSphereSampler(4, rng, factoryConstructor);
241 
242         // No uniform distribution of spherical coordinates for a 3-sphere.
243         // https://en.wikipedia.org/wiki/N-sphere#Spherical_coordinates
244         // Here we exploit the fact that the uniform distribution of a (n+1)-sphere
245         // when discarding two coordinates is uniform within a n-ball.
246         // Thus any two coordinates of the 4 are uniform within a circle.
247         // Here we separately test pairs (0, 1) and (2, 3).
248         // Note: We cannot create a single bin from the two bins in each circle as they are
249         // not independent. A point close to the edge in one circle requires a point close to
250         // the centre in the other circle to create a unit radius from all 4 coordinates.
251         // This test exercises the N-dimension sampler and demonstrates the vectors obey
252         // properties of the (n+1)-sphere.
253 
254         // Divide the circle into layers of concentric rings and an angle.
255         final int layers = 10;
256         final int angleBins = 20;
257 
258         // Compute the radius for each layer to have the same area
259         // (i.e. incrementally larger concentric circles must increase area by a constant).
260         // r = sqrt(fraction * maxArea / pi)
261         // Unit circle has area pi so we just use sqrt(fraction).
262         final double[] r = new double[layers];
263         for (int i = 1; i < layers; i++) {
264             r[i - 1] = Math.sqrt((double) i / layers);
265         }
266         // The final radius should be 1.0.
267         r[layers - 1] = 1.0;
268 
269         final long[] observed1 = new long[layers * angleBins];
270         final long[] observed2 = new long[observed1.length];
271         final int steps = 1000000;
272         for (int i = 0; i < steps; ++i) {
273             final double[] v = generator.sample();
274             Assertions.assertEquals(4, v.length);
275             Assertions.assertEquals(1.0, length(v), 1e-10);
276             // Circle 1
277             observed1[circleBin(angleBins, r, v[0], v[1])]++;
278             // Circle 2
279             observed2[circleBin(angleBins, r, v[2], v[3])]++;
280         }
281 
282         final double[] expected = new double[observed1.length];
283         Arrays.fill(expected, (double) steps / observed1.length);
284         final ChiSquareTest chi = new ChiSquareTest();
285         final double p1 = chi.chiSquareTest(expected, observed1);
286         Assertions.assertFalse(p1 < 0.01, () -> "Circle 1 p-value too small: " + p1);
287         final double p2 = chi.chiSquareTest(expected, observed2);
288         Assertions.assertFalse(p2 < 0.01, () -> "Circle 2 p-value too small: " + p2);
289     }
290 
291     /**
292      * Compute a bin inside the circle using the polar angle theta and the radius thresholds.
293      *
294      * @param angleBins the number of angle bins
295      * @param r the radius bin thresholds (ascending order)
296      * @param x the x coordinate
297      * @param y the y coordinate
298      * @return the bin
299      */
300     private static int circleBin(int angleBins, double[] r, double x, double y) {
301         final int angleBin = angleBin(angleBins, x, y);
302         final int radiusBin = radiusBin(r, x, y);
303         return radiusBin * angleBins + angleBin;
304     }
305 
306     /**
307      * Compute an angle bin from the xy vector. The bin will represent the range [0, 2pi).
308      *
309      * @param angleBins the number of angle bins
310      * @param x the x coordinate
311      * @param y the y coordinate
312      * @return the bin
313      */
314     private static int angleBin(int angleBins, double x, double y) {
315         final double angle = Math.atan2(y, x);
316         // Map [-pi, pi) to [0, 1) then assign a bin
317         return (int) (angleBins * (angle + Math.PI) / TWO_PI);
318     }
319 
320     /**
321      * Compute a radius bin from the xy vector. The bin is assigned if the length of the vector
322      * is above the threshold of the bin.
323      *
324      * @param r the radius bin thresholds (ascending order)
325      * @param x the x coordinate
326      * @param y the y coordinate
327      * @return the bin
328      */
329     private static int radiusBin(double[] r, double x, double y) {
330         final double length = Math.sqrt(x * x + y * y);
331 
332         // Note: The bin should be uniformly distributed.
333         // A test using a custom binary search (avoiding double NaN checks)
334         // shows the simple loop over a small number of bins is comparable in speed.
335         // The loop is preferred for simplicity. A binary search may be better
336         // if the number of bins is increased.
337         for (int layer = 0; layer < r.length; layer++) {
338             if (length <= r[layer]) {
339                 return layer;
340             }
341         }
342         // Unreachable if the xy component is from a vector of length <= 1
343         throw new AssertionError("Invalid sample length: " + length);
344     }
345 
346     /**
347      * Test infinite recursion occurs with a bad provider in 2D.
348      */
349     @Test
350     void testBadProvider2D() {
351         Assertions.assertThrows(StackOverflowError.class,
352             () -> testBadProvider(2));
353     }
354 
355     /**
356      * Test infinite recursion occurs with a bad provider in 3D.
357      */
358     @Test
359     void testBadProvider3D() {
360         Assertions.assertThrows(StackOverflowError.class,
361             () -> testBadProvider(3));
362     }
363 
364     /**
365      * Test infinite recursion occurs with a bad provider in 4D.
366      */
367     @Test
368     void testBadProvider4D() {
369         Assertions.assertThrows(StackOverflowError.class,
370             () -> testBadProvider(4));
371     }
372 
373     /**
374      * Test the edge case where the normalisation sum to divide by is always zero.
375      * This test requires generation of Gaussian samples with the value 0.
376      * The sample eventually fails due to infinite recursion.
377      * See RNG-55.
378      *
379      * @param dimension the dimension
380      */
381     private static void testBadProvider(final int dimension) {
382         // A provider that will create zero valued Gaussian samples
383         // from the ZigguratNormalizedGaussianSampler.
384         final UniformRandomProvider bad = new SplitMix64(0L) {
385             @Override
386             public long nextLong() {
387                 return 0;
388             }
389         };
390 
391         UnitSphereSampler.of(bad, dimension).sample();
392     }
393 
394     /**
395      * Test the edge case where the normalisation sum to divide by is zero for 2D.
396      */
397     @Test
398     void testInvalidInverseNormalisation2D() {
399         testInvalidInverseNormalisationND(2);
400     }
401 
402     /**
403      * Test the edge case where the normalisation sum to divide by is zero for 3D.
404      */
405     @Test
406     void testInvalidInverseNormalisation3D() {
407         testInvalidInverseNormalisationND(3);
408     }
409 
410     /**
411      * Test the edge case where the normalisation sum to divide by is zero for 4D.
412      */
413     @Test
414     void testInvalidInverseNormalisation4D() {
415         testInvalidInverseNormalisationND(4);
416     }
417 
418     /**
419      * Test the edge case where the normalisation sum to divide by is zero.
420      * This test requires generation of Gaussian samples with the value 0.
421      * See RNG-55.
422      */
423     private static void testInvalidInverseNormalisationND(final int dimension) {
424         // Create a provider that will create a bad first sample but then recover.
425         // This checks recursion will return a good value.
426         final UniformRandomProvider bad = new SplitMix64(0x1a2b3cL) {
427             private int count = -2 * dimension;
428 
429             @Override
430             public long nextLong() {
431                 // Return enough zeros to create Gaussian samples of zero for all coordinates.
432                 return count++ < 0 ? 0 : super.nextLong();
433             }
434         };
435 
436         final double[] vector = UnitSphereSampler.of(bad, dimension).sample();
437         Assertions.assertEquals(dimension, vector.length);
438         Assertions.assertEquals(1.0, length(vector), 1e-10);
439     }
440 
441     /**
442      * Test to demonstrate that using floating-point equality of the norm squared with
443      * zero is valid. Any norm squared after zero should produce a valid scaling factor.
444      */
445     @Test
446     void testNextNormSquaredAfterZeroIsValid() {
447         // The sampler explicitly handles length == 0 using recursion.
448         // Anything above zero should be valid.
449         final double normSq = Math.nextUp(0.0);
450         // Map to the scaling factor
451         final double f = 1 / Math.sqrt(normSq);
452         // As long as this is finite positive then the sampler is valid
453         Assertions.assertTrue(f > 0 && f <= Double.MAX_VALUE);
454     }
455 
456     /**
457      * Test the SharedStateSampler implementation for 1D.
458      */
459     @Test
460     void testSharedStateSampler1D() {
461         testSharedStateSampler(1, false);
462     }
463 
464     /**
465      * Test the SharedStateSampler implementation for 2D.
466      */
467     @Test
468     void testSharedStateSampler2D() {
469         testSharedStateSampler(2, false);
470     }
471 
472     /**
473      * Test the SharedStateSampler implementation for 3D.
474      */
475     @Test
476     void testSharedStateSampler3D() {
477         testSharedStateSampler(3, false);
478     }
479 
480     /**
481      * Test the SharedStateSampler implementation for 4D.
482      */
483     @Test
484     void testSharedStateSampler4D() {
485         testSharedStateSampler(4, false);
486     }
487 
488     /**
489      * Test the SharedStateSampler implementation for 1D using the factory constructor.
490      */
491     @Test
492     void testSharedStateSampler1DWithFactoryConstructor() {
493         testSharedStateSampler(1, true);
494     }
495 
496     /**
497      * Test the SharedStateSampler implementation for 2D using the factory constructor.
498      */
499     @Test
500     void testSharedStateSampler2DWithFactoryConstructor() {
501         testSharedStateSampler(2, true);
502     }
503 
504     /**
505      * Test the SharedStateSampler implementation for 3D using the factory constructor.
506      */
507     @Test
508     void testSharedStateSampler3DWithFactoryConstructor() {
509         testSharedStateSampler(3, true);
510     }
511 
512     /**
513      * Test the SharedStateSampler implementation for 4D using the factory constructor.
514      */
515     @Test
516     void testSharedStateSampler4DWithFactoryConstructor() {
517         testSharedStateSampler(4, true);
518     }
519 
520     /**
521      * Test the SharedStateSampler implementation for the given dimension.
522      *
523      * @param dimension the dimension
524      * @param factoryConstructor true to use the factory constructor
525      */
526     private static void testSharedStateSampler(int dimension, boolean factoryConstructor) {
527         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
528         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
529         final UnitSphereSampler sampler1 = createUnitSphereSampler(dimension, rng1, factoryConstructor);
530         final UnitSphereSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
531         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
532     }
533 
534     /**
535      * Creates a UnitSphereSampler.
536      *
537      * @param dimension the dimension
538      * @param rng the source of randomness
539      * @param factoryConstructor true to use the factory constructor
540      * @return the sampler
541      */
542     private static UnitSphereSampler createUnitSphereSampler(int dimension, UniformRandomProvider rng,
543             boolean factoryConstructor) {
544         return factoryConstructor ?
545                 UnitSphereSampler.of(rng, dimension) : new UnitSphereSampler(dimension, rng);
546     }
547 
548     /**
549      * @return the length (L2-norm) of given vector.
550      */
551     private static double length(double[] vector) {
552         double total = 0;
553         for (final double d : vector) {
554             total += d * d;
555         }
556         return Math.sqrt(total);
557     }
558 }