1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
28
29 class UnitSphereSamplerTest {
30
31 private static final double TWO_PI = 2 * Math.PI;
32
33
34
35
36 @Test
37 void testInvalidDimensionThrows() {
38
39 Assertions.assertThrows(IllegalArgumentException.class,
40 () -> new UnitSphereSampler(0, null));
41 }
42
43
44
45
46 @Test
47 void testInvalidDimensionThrowsWithFactoryConstructor() {
48 Assertions.assertThrows(IllegalArgumentException.class,
49 () -> UnitSphereSampler.of(null, 0));
50 }
51
52
53
54
55 @Test
56 void testDistribution1D() {
57 testDistribution1D(false);
58 }
59
60
61
62
63 @Test
64 void testDistribution1DWithFactoryConstructor() {
65 testDistribution1D(true);
66 }
67
68
69
70
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
77 int count = 0;
78 for (int i = 0; i < samples; i++) {
79
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
88 Assertions.fail("Invalid unit length: " + d);
89 }
90 }
91
92 assertMonobit(count, samples);
93 }
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109 private static void assertMonobit(int bitCount, int numberOfBits) {
110
111 final double sum = 2.0 * bitCount - numberOfBits;
112
113
114
115
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
125
126 @Test
127 void testDistribution2D() {
128 testDistribution2D(false);
129 }
130
131
132
133
134 @Test
135 void testDistribution2DWithFactoryConstructor() {
136 testDistribution2D(true);
137 }
138
139
140
141
142
143 private static void testDistribution2D(boolean factoryConstructor) {
144 final UniformRandomProvider rng = RandomAssert.createRNG();
145 final UnitSphereSampler generator = createUnitSphereSampler(2, rng, factoryConstructor);
146
147
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
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
168
169 @Test
170 void testDistribution3D() {
171 testDistribution3D(false);
172 }
173
174
175
176
177 @Test
178 void testDistribution3DWithFactoryConstructor() {
179 testDistribution3D(true);
180 }
181
182
183
184
185
186 private static void testDistribution3D(boolean factoryConstructor) {
187 final UniformRandomProvider rng = RandomAssert.createRNG();
188 final UnitSphereSampler generator = createUnitSphereSampler(3, rng, factoryConstructor);
189
190
191
192
193
194
195
196
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
206 final int angleBin = angleBin(angleBins, v[0], v[1]);
207
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
220
221 @Test
222 void testDistribution4D() {
223 testDistribution4D(false);
224 }
225
226
227
228
229 @Test
230 void testDistribution4DWithFactoryConstructor() {
231 testDistribution4D(true);
232 }
233
234
235
236
237
238 private static void testDistribution4D(boolean factoryConstructor) {
239 final UniformRandomProvider rng = RandomAssert.createRNG();
240 final UnitSphereSampler generator = createUnitSphereSampler(4, rng, factoryConstructor);
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255 final int layers = 10;
256 final int angleBins = 20;
257
258
259
260
261
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
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
277 observed1[circleBin(angleBins, r, v[0], v[1])]++;
278
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
293
294
295
296
297
298
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
308
309
310
311
312
313
314 private static int angleBin(int angleBins, double x, double y) {
315 final double angle = Math.atan2(y, x);
316
317 return (int) (angleBins * (angle + Math.PI) / TWO_PI);
318 }
319
320
321
322
323
324
325
326
327
328
329 private static int radiusBin(double[] r, double x, double y) {
330 final double length = Math.sqrt(x * x + y * y);
331
332
333
334
335
336
337 for (int layer = 0; layer < r.length; layer++) {
338 if (length <= r[layer]) {
339 return layer;
340 }
341 }
342
343 throw new AssertionError("Invalid sample length: " + length);
344 }
345
346
347
348
349 @Test
350 void testBadProvider2D() {
351 Assertions.assertThrows(StackOverflowError.class,
352 () -> testBadProvider(2));
353 }
354
355
356
357
358 @Test
359 void testBadProvider3D() {
360 Assertions.assertThrows(StackOverflowError.class,
361 () -> testBadProvider(3));
362 }
363
364
365
366
367 @Test
368 void testBadProvider4D() {
369 Assertions.assertThrows(StackOverflowError.class,
370 () -> testBadProvider(4));
371 }
372
373
374
375
376
377
378
379
380
381 private static void testBadProvider(final int dimension) {
382
383
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
396
397 @Test
398 void testInvalidInverseNormalisation2D() {
399 testInvalidInverseNormalisationND(2);
400 }
401
402
403
404
405 @Test
406 void testInvalidInverseNormalisation3D() {
407 testInvalidInverseNormalisationND(3);
408 }
409
410
411
412
413 @Test
414 void testInvalidInverseNormalisation4D() {
415 testInvalidInverseNormalisationND(4);
416 }
417
418
419
420
421
422
423 private static void testInvalidInverseNormalisationND(final int dimension) {
424
425
426 final UniformRandomProvider bad = new SplitMix64(0x1a2b3cL) {
427 private int count = -2 * dimension;
428
429 @Override
430 public long nextLong() {
431
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
443
444
445 @Test
446 void testNextNormSquaredAfterZeroIsValid() {
447
448
449 final double normSq = Math.nextUp(0.0);
450
451 final double f = 1 / Math.sqrt(normSq);
452
453 Assertions.assertTrue(f > 0 && f <= Double.MAX_VALUE);
454 }
455
456
457
458
459 @Test
460 void testSharedStateSampler1D() {
461 testSharedStateSampler(1, false);
462 }
463
464
465
466
467 @Test
468 void testSharedStateSampler2D() {
469 testSharedStateSampler(2, false);
470 }
471
472
473
474
475 @Test
476 void testSharedStateSampler3D() {
477 testSharedStateSampler(3, false);
478 }
479
480
481
482
483 @Test
484 void testSharedStateSampler4D() {
485 testSharedStateSampler(4, false);
486 }
487
488
489
490
491 @Test
492 void testSharedStateSampler1DWithFactoryConstructor() {
493 testSharedStateSampler(1, true);
494 }
495
496
497
498
499 @Test
500 void testSharedStateSampler2DWithFactoryConstructor() {
501 testSharedStateSampler(2, true);
502 }
503
504
505
506
507 @Test
508 void testSharedStateSampler3DWithFactoryConstructor() {
509 testSharedStateSampler(3, true);
510 }
511
512
513
514
515 @Test
516 void testSharedStateSampler4DWithFactoryConstructor() {
517 testSharedStateSampler(4, true);
518 }
519
520
521
522
523
524
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
536
537
538
539
540
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
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 }