1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.rng.sampling.shape;
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.sampling.RandomAssert;
23 import org.junit.jupiter.api.Assertions;
24 import org.junit.jupiter.api.Test;
25
26
27
28
29 class TriangleSamplerTest {
30
31
32
33
34
35 private static final double[][] F3 = {
36 {0.728027725387508, -0.525104821111919, 0.440727305612110},
37 {0.608788597915763, 0.790790557990391, -0.063456571298848},
38 {-0.315201640406345, 0.314507901710379, 0.895395278995195}};
39
40 private static final double[][] R3 = {
41 {0.728027725387508, 0.608788597915762, -0.315201640406344},
42 {-0.525104821111919, 0.790790557990391, 0.314507901710379},
43 {0.440727305612110, -0.063456571298848, 0.895395278995195}};
44
45 private static final double[][] F4 = {
46 {0.853553390593274, -0.353553390593274, 0.146446609406726, 0.353553390593274},
47 {0.353553390593274, 0.853553390593274, -0.353553390593274, 0.146446609406726},
48 {0.146446609406726, 0.353553390593274, 0.853553390593274, -0.353553390593274},
49 {-0.353553390593274, 0.146446609406726, 0.353553390593274, 0.853553390593274}};
50
51 private static final double[][] R4 = {
52 {0.853553390593274, 0.353553390593274, 0.146446609406726, -0.353553390593274},
53 {-0.353553390593274, 0.853553390593274, 0.353553390593274, 0.146446609406726},
54 {0.146446609406726, -0.353553390593274, 0.853553390593274, 0.353553390593274},
55 {0.353553390593274, 0.146446609406726, -0.353553390593274, 0.853553390593274}};
56
57
58
59
60
61 @Test
62 void testSamplingAssumptions() {
63
64 final double delta = 0x1.0p-53;
65 double s = 0.5;
66 double t = 0.5 + delta;
67
68 final double spt = s + t;
69
70
71 final double expected = 1 - (1 - s) - (1 - t);
72 Assertions.assertNotEquals(expected, spt - 1);
73 Assertions.assertNotEquals(expected, s + t - 1);
74
75
76 Assertions.assertEquals(expected, s - 1 + t);
77
78
79 final double max = Double.MAX_VALUE;
80 s -= delta;
81 final UniformRandomProvider rng = RandomAssert.createRNG();
82 for (int n = 0; n < 100; n++) {
83 Assertions.assertNotEquals(Double.POSITIVE_INFINITY, (1 - s - t) * max + s * max + t * max);
84 s = rng.nextDouble();
85 t = 1.0 - s;
86 }
87 }
88
89
90
91
92 @Test
93 void testInvalidDimensionThrows() {
94 final UniformRandomProvider rng = RandomAssert.seededRNG();
95 Assertions.assertThrows(IllegalArgumentException.class,
96 () -> TriangleSampler.of(rng, new double[1], new double[1], new double[1]));
97 }
98
99
100
101
102 @Test
103 void testDimensionMismatchThrows() {
104 final UniformRandomProvider rng = RandomAssert.seededRNG();
105 final double[] c2 = new double[2];
106 final double[] c3 = new double[3];
107 for (double[][] c : new double[][][] {
108 {c2, c2, c3},
109 {c2, c3, c2},
110 {c3, c2, c2},
111 {c2, c3, c3},
112 {c3, c3, c2},
113 {c3, c2, c3},
114 }) {
115 Assertions.assertThrows(IllegalArgumentException.class,
116 () -> TriangleSampler.of(rng, c[0], c[1], c[2]),
117 () -> String.format("Did not detect dimension mismatch: %d,%d,%d",
118 c[0].length, c[1].length, c[2].length));
119 }
120 }
121
122
123
124
125 @Test
126 void testNonFiniteVertexCoordinates() {
127 final UniformRandomProvider rng = RandomAssert.seededRNG();
128
129 final double[][] c = new double[][] {
130 {0, 0, 1}, {2, 1, 0}, {-1, 2, 3}
131 };
132 Assertions.assertNotNull(TriangleSampler.of(rng, c[0], c[1], c[2]));
133 final double[] bad = {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN};
134 for (int i = 0; i < c.length; i++) {
135 final int ii = i;
136 for (int j = 0; j < c[0].length; j++) {
137 final int jj = j;
138 for (final double d : bad) {
139 final double value = c[i][j];
140 c[i][j] = d;
141 Assertions.assertThrows(IllegalArgumentException.class,
142 () -> TriangleSampler.of(rng, c[0], c[1], c[2]),
143 () -> String.format("Did not detect non-finite coordinate: %d,%d = %s", ii, jj, d));
144 c[i][j] = value;
145 }
146 }
147 }
148 }
149
150
151
152
153
154 @Test
155 void testExtremeValueCoordinates2D() {
156 testExtremeValueCoordinates(2);
157 }
158
159
160
161
162
163 @Test
164 void testExtremeValueCoordinates3D() {
165 testExtremeValueCoordinates(3);
166 }
167
168
169
170
171
172 @Test
173 void testExtremeValueCoordinates4D() {
174 testExtremeValueCoordinates(4);
175 }
176
177
178
179
180
181
182
183 private static void testExtremeValueCoordinates(int dimension) {
184 final double[][] c1 = new double[3][dimension];
185 final double[][] c2 = new double[3][dimension];
186
187 c1[0][0] = -1;
188 c1[0][1] = -1;
189 c1[1][0] = 1;
190 c1[1][1] = 1;
191 c1[2][0] = 1;
192 c1[2][1] = -1;
193
194 final double scale = 0x1.0p1023;
195 for (int i = 0; i < 3; i++) {
196
197 for (int j = 2; j < dimension; j++) {
198 c1[i][j] = 1 - (j & 0x2);
199 }
200
201 for (int j = 0; j < dimension; j++) {
202 c2[i][j] = c1[i][j] * scale;
203 }
204 }
205
206 Assertions.assertEquals(Double.POSITIVE_INFINITY, c2[2][0] - c2[0][0],
207 "Expect vector c - a to be infinite in the x dimension");
208 Assertions.assertEquals(Double.NEGATIVE_INFINITY, c2[2][1] - c2[1][1],
209 "Expect vector c - b to be infinite in the y dimension");
210
211 final TriangleSampler sampler1 = TriangleSampler.of(
212 RandomAssert.seededRNG(), c1[0], c1[1], c1[2]);
213 final TriangleSampler sampler2 = TriangleSampler.of(
214 RandomAssert.seededRNG(), c2[0], c2[1], c2[2]);
215
216 for (int n = 0; n < 10; n++) {
217 final double[] a = sampler1.sample();
218 final double[] b = sampler2.sample();
219 for (int i = 0; i < a.length; i++) {
220 a[i] *= scale;
221 }
222 Assertions.assertArrayEquals(a, b);
223 }
224 }
225
226
227
228
229 @Test
230 void testDistribution2D() {
231 testDistributionND(2);
232 }
233
234
235
236
237 @Test
238 void testDistribution3D() {
239 testDistributionND(3);
240 }
241
242
243
244
245 @Test
246 void testDistribution4D() {
247 testDistributionND(4);
248 }
249
250
251
252
253
254
255
256
257
258
259
260 private static void testDistributionND(int dimension) {
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275 final double[] a = {0, 0};
276 final double[] b = {0, 4};
277 final double[] c = {3, 4};
278 final double[] d = {3, 0};
279 final double[] e = {1.5, 2};
280
281
282 final int bins = 20;
283 final int samplesPerBin = 10;
284
285 final double sx = bins / 3.0;
286 final double sy = bins / 4.0;
287
288
289 final double[] expected = new double[bins * bins];
290 Arrays.fill(expected, 1);
291
292
293
294 Transform forward;
295 Transform reverse;
296 if (dimension == 4) {
297 forward = new ForwardTransform(F4);
298 reverse = new ReverseTransform(R4);
299 } else if (dimension == 3) {
300 forward = new ForwardTransform(F3);
301 reverse = new ReverseTransform(R3);
302 } else if (dimension == 2) {
303 forward = reverse = new Transform2Dto2D();
304 } else {
305 throw new AssertionError("Unsupported dimension: " + dimension);
306 }
307
308
309 final UniformRandomProvider rng = RandomAssert.createRNG();
310 final TriangleSampler sampler1 = TriangleSampler.of(rng, forward.apply(a), forward.apply(d), forward.apply(b));
311 final TriangleSampler sampler2 = TriangleSampler.of(rng, forward.apply(b), forward.apply(c), forward.apply(e));
312 final TriangleSampler sampler3 = TriangleSampler.of(rng, forward.apply(c), forward.apply(d), forward.apply(e));
313 final Triangle triangle1 = new Triangle(a, d, b);
314 final Triangle triangle2 = new Triangle(b, c, e);
315 final Triangle triangle3 = new Triangle(c, d, e);
316 final int samples = expected.length * samplesPerBin;
317 for (int n = 0; n < 1; n++) {
318
319 final long[] observed = new long[expected.length];
320
321 for (int i = 0; i < samples; i += 4) {
322 addObservation(reverse.apply(sampler1.sample()), observed, bins, sx, sy, triangle1);
323 addObservation(reverse.apply(sampler1.sample()), observed, bins, sx, sy, triangle1);
324 addObservation(reverse.apply(sampler2.sample()), observed, bins, sx, sy, triangle2);
325 addObservation(reverse.apply(sampler3.sample()), observed, bins, sx, sy, triangle3);
326 }
327 final double p = new ChiSquareTest().chiSquareTest(expected, observed);
328 Assertions.assertFalse(p < 0.001, () -> "p-value too small: " + p);
329 }
330 }
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347 private static void addObservation(double[] v, long[] observed,
348 int bins, double sx, double sy, Triangle triangle) {
349 final double x = v[0];
350 final double y = v[1];
351
352 Assertions.assertTrue(triangle.contains(x, y));
353
354 final int binx = (int) (x * sx);
355 final int biny = (int) (y * sy);
356 observed[biny * bins + binx]++;
357 }
358
359
360
361
362 @Test
363 void testSharedStateSampler2D() {
364 testSharedStateSampler(2);
365 }
366
367
368
369
370 @Test
371 void testSharedStateSampler3D() {
372 testSharedStateSampler(3);
373 }
374
375
376
377
378 @Test
379 void testSharedStateSampler4D() {
380 testSharedStateSampler(4);
381 }
382
383
384
385
386 private static void testSharedStateSampler(int dimension) {
387 final UniformRandomProvider rng1 = RandomAssert.seededRNG();
388 final UniformRandomProvider rng2 = RandomAssert.seededRNG();
389 final double[] c1 = createCoordinate(1, dimension);
390 final double[] c2 = createCoordinate(2, dimension);
391 final double[] c3 = createCoordinate(-3, dimension);
392 final TriangleSampler sampler1 = TriangleSampler.of(rng1, c1, c2, c3);
393 final TriangleSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
394 RandomAssert.assertProduceSameSequence(sampler1, sampler2);
395 }
396
397
398
399
400 @Test
401 void testChangedInputCoordinates2D() {
402 testChangedInputCoordinates(2);
403 }
404
405
406
407
408 @Test
409 void testChangedInputCoordinates3D() {
410 testChangedInputCoordinates(3);
411 }
412
413
414
415
416 @Test
417 void testChangedInputCoordinates4D() {
418 testChangedInputCoordinates(4);
419 }
420
421
422
423
424
425
426
427 private static void testChangedInputCoordinates(int dimension) {
428 final UniformRandomProvider rng1 = RandomAssert.seededRNG();
429 final UniformRandomProvider rng2 = RandomAssert.seededRNG();
430 final double[] c1 = createCoordinate(1, dimension);
431 final double[] c2 = createCoordinate(2, dimension);
432 final double[] c3 = createCoordinate(-3, dimension);
433 final TriangleSampler sampler1 = TriangleSampler.of(rng1, c1, c2, c3);
434
435
436
437 final double offset = 10;
438 for (int i = 0; i < dimension; i++) {
439 c1[i] += offset;
440 c2[i] += offset;
441 c3[i] += offset;
442 }
443 final TriangleSampler sampler2 = TriangleSampler.of(rng2, c1, c2, c3);
444 for (int n = 0; n < 3; n++) {
445 final double[] s1 = sampler1.sample();
446 final double[] s2 = sampler2.sample();
447 Assertions.assertEquals(s1.length, s2.length);
448 Assertions.assertFalse(Arrays.equals(s1, s2),
449 "First sampler has used the vertices by reference");
450 for (int i = 0; i < dimension; i++) {
451 Assertions.assertEquals(s1[i] + offset, s2[i], 1e-10);
452 }
453 }
454 }
455
456
457
458
459
460
461
462
463
464 private static double[] createCoordinate(double x, int dimension) {
465 final double[] coord = new double[dimension];
466 for (int i = 0; i < dimension; i++) {
467 coord[0] = x + i;
468 }
469 return coord;
470 }
471
472
473
474
475 @Test
476 void testTransform3D() {
477 testTransformND(3);
478 }
479
480
481
482
483 @Test
484 void testTransform4D() {
485 testTransformND(4);
486 }
487
488
489
490
491
492
493 private static void testTransformND(int dimension) {
494 Transform forward;
495 Transform reverse;
496 if (dimension == 4) {
497 forward = new ForwardTransform(F4);
498 reverse = new ReverseTransform(R4);
499 } else if (dimension == 3) {
500 forward = new ForwardTransform(F3);
501 reverse = new ReverseTransform(R3);
502 } else if (dimension == 2) {
503 forward = reverse = new Transform2Dto2D();
504 } else {
505 throw new AssertionError("Unsupported dimension: " + dimension);
506 }
507
508 final UniformRandomProvider rng = RandomAssert.seededRNG();
509 double sum = 0;
510 for (int n = 0; n < 10; n++) {
511 final double[] a = new double[] {rng.nextDouble(), rng.nextDouble()};
512 final double[] b = forward.apply(a);
513 Assertions.assertEquals(dimension, b.length);
514 for (int i = 2; i < dimension; i++) {
515 sum += Math.abs(b[i]);
516 }
517 final double[] c = reverse.apply(b);
518 Assertions.assertArrayEquals(a, c, 1e-10);
519 }
520
521
522 final double actual = sum;
523 final double nonZeroThreshold = 0.01;
524 Assertions.assertTrue(actual > nonZeroThreshold,
525 () -> "No non-zero higher dimension coordinates: " + actual + " <= " + nonZeroThreshold);
526 }
527
528
529
530
531 @Test
532 void testRotations3D() {
533 final double[] x = {1, 0.5, 0};
534 final double[] y = multiply(F3, x);
535 Assertions.assertArrayEquals(new double[] {0.465475314831549, 1.004183876910958, -0.157947689551155}, y, 1e-10);
536 Assertions.assertEquals(length(x), length(y), 1e-10);
537 final double[] x2 = multiply(R3, y);
538 Assertions.assertArrayEquals(x, x2, 1e-10);
539 }
540
541
542
543
544 @Test
545 void testRotations4D() {
546 final double[] x = {1, 0.5, 0, 0};
547 final double[] y = multiply(F4, x);
548 Assertions.assertArrayEquals(
549 new double[] {0.676776695296637, 0.780330085889911, 0.323223304703363, -0.280330085889911}, y, 1e-10);
550 Assertions.assertEquals(length(x), length(y), 1e-10);
551 final double[] x2 = multiply(R4, y);
552 Assertions.assertArrayEquals(x, x2, 1e-10);
553 }
554
555
556
557
558
559
560
561
562
563 private static double[] multiply(double[][] matrix, double[] v) {
564 final int n = matrix.length;
565 final int m = v.length;
566 final double[] r = new double[n];
567 for (int i = 0; i < n; i++) {
568 double sum = 0;
569 for (int j = 0; j < m; j++) {
570 sum += matrix[i][j] * v[j];
571 }
572 r[i] = sum;
573 }
574 return r;
575 }
576
577
578
579
580 private static double length(double[] vector) {
581 double total = 0;
582 for (final double d : vector) {
583 total += d * d;
584 }
585 return Math.sqrt(total);
586 }
587
588
589
590
591 @Test
592 void testTriangleContains() {
593 final Triangle triangle = new Triangle(1, 2, 3, 1, 0.5, 6);
594
595 Assertions.assertTrue(triangle.contains(1, 2));
596 Assertions.assertTrue(triangle.contains(3, 1));
597 Assertions.assertTrue(triangle.contains(0.5, 6));
598
599 Assertions.assertTrue(triangle.contains(0.75, 4));
600
601 Assertions.assertTrue(triangle.contains(1.5, 3));
602
603 Assertions.assertFalse(triangle.contains(0, 20));
604 Assertions.assertFalse(triangle.contains(-20, 0));
605 Assertions.assertFalse(triangle.contains(6, 6));
606
607 Assertions.assertFalse(triangle.contains(0.75, 4 - 1e-10));
608
609
610
611
612
613 final Triangle triangle2 = new Triangle(1, 2, 3, 1, 0, -2);
614 Assertions.assertTrue(triangle.contains(2, 1.5));
615 Assertions.assertTrue(triangle2.contains(2, 1.5));
616 }
617
618
619
620
621 private interface Transform {
622
623
624
625
626
627
628 double[] apply(double[] coord);
629 }
630
631
632
633
634 private static class ForwardTransform implements Transform {
635 private final double[][] r;
636
637
638
639
640 ForwardTransform(double[][] r) {
641 this.r = r;
642 }
643
644 @Override
645 public double[] apply(double[] coord) {
646 return multiply(r, coord);
647 }
648 }
649
650
651
652
653
654
655 private static class ReverseTransform implements Transform {
656 private final double[][] r;
657 private final int n;
658
659
660
661
662 ReverseTransform(double[][] r) {
663 this.r = r;
664 n = r.length;
665 }
666
667 @Override
668 public double[] apply(double[] coord) {
669 Assertions.assertEquals(n, coord.length);
670 final double[] x = multiply(r, coord);
671
672 for (int i = 2; i < x.length; i++) {
673 Assertions.assertEquals(0.0, x[i], 1e-14);
674 }
675 return new double[] {x[0], x[1]};
676 }
677 }
678
679
680
681
682 private static class Transform2Dto2D implements Transform {
683 @Override
684 public double[] apply(double[] coord) {
685 Assertions.assertEquals(2, coord.length);
686 return coord;
687 }
688 }
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710 private static class Triangle {
711 private final double p2x;
712 private final double p2y;
713 private final double dX21;
714 private final double dY12;
715 private final double dY20;
716 private final double dX02;
717 private final double d;
718
719
720
721
722
723
724
725
726 Triangle(double[] p0, double[] p1, double[] p2) {
727 this(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]);
728 }
729
730
731
732
733
734
735
736
737
738
739 Triangle(double p0x, double p0y, double p1x, double p1y, double p2x, double p2y) {
740 this.p2x = p2x;
741 this.p2y = p2y;
742
743 dX21 = p2x - p1x;
744 dY12 = p1y - p2y;
745 dY20 = p2y - p0y;
746 dX02 = p0x - p2x;
747
748 d = dY12 * (p0x - p2x) + dX21 * (p0y - p2y);
749 }
750
751
752
753
754
755
756
757
758 boolean contains(double px, double py) {
759
760
761
762
763
764
765 final double dX = px - p2x;
766 final double dY = py - p2y;
767 final double s = dY12 * dX + dX21 * dY;
768 final double t = dY20 * dX + dX02 * dY;
769 if (d < 0) {
770 return s <= 0 && t <= 0 && s + t >= d;
771 }
772 return s >= 0 && t >= 0 && s + t <= d;
773 }
774 }
775 }