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.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   * Test for {@link TriangleSampler}.
28   */
29  class TriangleSamplerTest {
30      // Precomputed 3D and 4D rotation matrices for forward and backward transforms
31      // created using the matlab function by jodag:
32      // https://stackoverflow.com/questions/50337642/how-to-calculate-a-rotation-matrix-in-n-dimensions-given-the-point-to-rotate-an
33  
34      /** 3D rotation around the vector [1; 2; 3] by pi/4. */
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      /** 3D rotation around the vector [1; 2; 3] by -pi/4. */
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      /** 4D rotation around the orthogonal vectors [1 0; 0 1; 1 0; 0 1] by pi/4. */
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      /** 4D rotation around the orthogonal vectors [1 0; 0 1; 1 0; 0 1] by -pi/4. */
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       * Test the sampling assumptions used to transform coordinates outside the triangle
59       * back inside the triangle.
60       */
61      @Test
62      void testSamplingAssumptions() {
63          // The separation between the 2^53 dyadic rationals in the interval [0, 1)
64          final double delta = 0x1.0p-53;
65          double s = 0.5;
66          double t = 0.5 + delta;
67          // This value cannot be exactly represented and is rounded
68          final double spt = s + t;
69          // Test that (1 - (1-s) - (1-t)) is not equal to (s + t - 1).
70          // This is due to the rounding to store s + t as a double.
71          final double expected = 1 - (1 - s) - (1 - t);
72          Assertions.assertNotEquals(expected, spt - 1);
73          Assertions.assertNotEquals(expected, s + t - 1);
74          // For any uniform deviate u in [0, 1], u - 1 is exact, thus s - 1 is exact
75          // and s - 1 + t is exact.
76          Assertions.assertEquals(expected, s - 1 + t);
77  
78          // Test that a(1 - s - t) + sb + tc does not overflow is s+t = 1
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       * Test an unsupported dimension.
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      * Test a dimension mismatch between vertices.
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      * Test non-finite vertices.
124      */
125     @Test
126     void testNonFiniteVertexCoordinates() {
127         final UniformRandomProvider rng = RandomAssert.seededRNG();
128         // A valid triangle
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      * Test a triangle with coordinates that are separated by more than
152      * {@link Double#MAX_VALUE} in two dimensions.
153      */
154     @Test
155     void testExtremeValueCoordinates2D() {
156         testExtremeValueCoordinates(2);
157     }
158 
159     /**
160      * Test a triangle with coordinates that are separated by more than
161      * {@link Double#MAX_VALUE} in three dimensions.
162      */
163     @Test
164     void testExtremeValueCoordinates3D() {
165         testExtremeValueCoordinates(3);
166     }
167 
168     /**
169      * Test a triangle with coordinates that are separated by more than
170      * {@link Double#MAX_VALUE} in four dimensions.
171      */
172     @Test
173     void testExtremeValueCoordinates4D() {
174         testExtremeValueCoordinates(4);
175     }
176 
177     /**
178      * Test a triangle with coordinates that are separated by more than
179      * {@link Double#MAX_VALUE}.
180      *
181      * @param dimension the dimension
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         // Create a valid triangle that can be scaled
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         // Extremely large value for scaling. Use a power of 2 for exact scaling.
194         final double scale = 0x1.0p1023;
195         for (int i = 0; i < 3; i++) {
196             // Fill the remaining dimensions with 1 or -1
197             for (int j = 2; j < dimension; j++) {
198                 c1[i][j] = 1 - (j & 0x2);
199             }
200             // Scale the second triangle
201             for (int j = 0; j < dimension; j++) {
202                 c2[i][j] = c1[i][j] * scale;
203             }
204         }
205         // Show the triangle is too big to compute vectors between points.
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      * Test the distribution of points in two dimensions.
228      */
229     @Test
230     void testDistribution2D() {
231         testDistributionND(2);
232     }
233 
234     /**
235      * Test the distribution of points in three dimensions.
236      */
237     @Test
238     void testDistribution3D() {
239         testDistributionND(3);
240     }
241 
242     /**
243      * Test the distribution of points in four dimensions.
244      */
245     @Test
246     void testDistribution4D() {
247         testDistributionND(4);
248     }
249 
250     /**
251      * Test the distribution of points in N dimensions. The output coordinates
252      * should be uniform in the triangle.
253      *
254      * <p>ND is supported by transforming 2D triangles to ND, sampling in ND then
255      * transforming back to 2D. The transformed results should have a zero
256      * coordinate for indices above 1. Supports 2 to 4 dimensions.
257      *
258      * @param dimension the dimension in the range [2, 4]
259      */
260     private static void testDistributionND(int dimension) {
261         // Create 3 triangles that form a 3x4 rectangle:
262         // b-----c
263         // |\   /|
264         // | \ / |
265         // |  e  |
266         // |   \ |
267         // |    \|
268         // a-----d
269         //
270         // Sample from them proportional to the area:
271         // adb = 4 * 3 / 2 = 6
272         // bce = 3 * 2 / 2 = 3
273         // cde = 4 * 1.5 / 2 = 3
274         // The samples in the ratio 2:1:1 should be uniform within the rectangle.
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         // Assign bins
282         final int bins = 20;
283         final int samplesPerBin = 10;
284         // Scale factors to assign x,y to a bin
285         final double sx = bins / 3.0;
286         final double sy = bins / 4.0;
287 
288         // Expect a uniform distribution (this is rescaled by the ChiSquareTest)
289         final double[] expected = new double[bins * bins];
290         Arrays.fill(expected, 1);
291 
292         // Support ND by applying a rotation transform to the 2D triangles to generate
293         // n-coordinates. The samples are transformed back and should be uniform in 2D.
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         // Increase the loops to verify robustness
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             // Assign each coordinate to a region inside the combined rectangle
319             final long[] observed = new long[expected.length];
320             // Sample according to the area of each triangle (ratio 2:1:1)
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      * Adds the observation. Coordinates are mapped using the offsets, scaled and
334      * then cast to an integer bin.
335      *
336      * <pre>
337      * binx = (int) (x * sx)
338      * </pre>
339      *
340      * @param v the sample (2D coordinate xy)
341      * @param observed the observations
342      * @param bins the numbers of bins in the x dimension
343      * @param sx the scale to convert the x coordinate to the x bin
344      * @param sy the scale to convert the y coordinate to the y bin
345      * @param triangle the triangle the sample should be within
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         // Test the point is triangle the triangle
352         Assertions.assertTrue(triangle.contains(x, y));
353         // Add to the correct bin after using the offset
354         final int binx = (int) (x * sx);
355         final int biny = (int) (y * sy);
356         observed[biny * bins + binx]++;
357     }
358 
359     /**
360      * Test the SharedStateSampler implementation for 2D.
361      */
362     @Test
363     void testSharedStateSampler2D() {
364         testSharedStateSampler(2);
365     }
366 
367     /**
368      * Test the SharedStateSampler implementation for 3D.
369      */
370     @Test
371     void testSharedStateSampler3D() {
372         testSharedStateSampler(3);
373     }
374 
375     /**
376      * Test the SharedStateSampler implementation for 4D.
377      */
378     @Test
379     void testSharedStateSampler4D() {
380         testSharedStateSampler(4);
381     }
382 
383     /**
384      * Test the SharedStateSampler implementation for the given dimension.
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      * Test the input vectors are copied and not used by reference for 2D.
399      */
400     @Test
401     void testChangedInputCoordinates2D() {
402         testChangedInputCoordinates(2);
403     }
404 
405     /**
406      * Test the input vectors are copied and not used by reference for 3D.
407      */
408     @Test
409     void testChangedInputCoordinates3D() {
410         testChangedInputCoordinates(3);
411     }
412 
413     /**
414      * Test the input vectors are copied and not used by reference for 4D.
415      */
416     @Test
417     void testChangedInputCoordinates4D() {
418         testChangedInputCoordinates(4);
419     }
420 
421     /**
422      * Test the input vectors are copied and not used by reference for the given
423      * dimension.
424      *
425      * @param dimension the dimension
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         // Check the input vectors are copied and not used by reference.
435         // Change them in place and create a new sampler. It should have different output
436         // translated by the offset.
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      * Creates the coordinate of length specified by the dimension filled with
458      * the given value and the dimension index: x + i.
459      *
460      * @param x the value for index 0
461      * @param dimension the dimension
462      * @return the coordinate
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      * Test the transform generates 3D coordinates and can reverse them.
474      */
475     @Test
476     void testTransform3D() {
477         testTransformND(3);
478     }
479 
480     /**
481      * Test the transform generates 4D coordinates and can reverse them.
482      */
483     @Test
484     void testTransform4D() {
485         testTransformND(4);
486     }
487 
488     /**
489      * Test the transform generates ND coordinates and can reverse them.
490      *
491      * @param dimension the dimension
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         // Check that higher dimension coordinates are generated.
521         // Note: Initially higher dimensions are zero.
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      * Test 3D rotations forward and reverse.
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      * Test 4D rotations forward and reverse.
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      * Matrix multiplication. It is assumed the matrix is square and matches (or exceeds)
557      * the vector length.
558      *
559      * @param m the matrix
560      * @param v the vector
561      * @return the result
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      * @return the length (L2-norm) of given vector.
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      * Test the triangle contains predicate.
590      */
591     @Test
592     void testTriangleContains() {
593         final Triangle triangle = new Triangle(1, 2, 3, 1, 0.5, 6);
594         // Vertices
595         Assertions.assertTrue(triangle.contains(1, 2));
596         Assertions.assertTrue(triangle.contains(3, 1));
597         Assertions.assertTrue(triangle.contains(0.5, 6));
598         // Edge
599         Assertions.assertTrue(triangle.contains(0.75, 4));
600         // Inside
601         Assertions.assertTrue(triangle.contains(1.5, 3));
602         // Outside
603         Assertions.assertFalse(triangle.contains(0, 20));
604         Assertions.assertFalse(triangle.contains(-20, 0));
605         Assertions.assertFalse(triangle.contains(6, 6));
606         // Just outside
607         Assertions.assertFalse(triangle.contains(0.75, 4 - 1e-10));
608 
609         // Note:
610         // Touching triangles can both have the point triangle.
611         // This predicate is not suitable for assigning points uniquely to
612         // non-overlapping triangles that share an edge.
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      * Define a transform on coordinates.
620      */
621     private interface Transform {
622         /**
623          * Apply the transform.
624          *
625          * @param coord the coordinates
626          * @return the new coordinates
627          */
628         double[] apply(double[] coord);
629     }
630 
631     /**
632      * Transform coordinates from 2D to a higher dimension using the rotation matrix.
633      */
634     private static class ForwardTransform implements Transform {
635         private final double[][] r;
636 
637         /**
638          * @param r the rotation matrix
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      * Transform coordinates from a higher dimension to 2D using the rotation matrix.
652      * The result should be in the 2D plane (i.e. higher dimensions of the transformed vector
653      * are asserted to be zero).
654      */
655     private static class ReverseTransform implements Transform {
656         private final double[][] r;
657         private final int n;
658 
659         /**
660          * @param r the rotation matrix
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             // This should reverse the 2D transform and return to the XY plane.
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      * No-operation transform on 2D input. Asserts the input coordinates are length 2.
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      * Class to test if a point is inside the triangle.
692      *
693      * <p>This function has been adapted from a StackOverflow post by Cédric Dufour. It converts the
694      * point to unscaled barycentric coordinates (s,t) and tests they are within the triangle.
695      * (Scaling would be done by dividing by twice the area of the triangle.)
696      *
697      * <h2>Warning</h2>
698      *
699      * <p>This assigns points on the edges as inside irrespective of edge orientation. Thus
700      * back-to-back touching triangles can both have the point inside them. A predicate for geometry
701      * applications where the point must be within a unique non-overlapping triangle should use
702      * a different solution, e.g. assigning new points to the result of a triangulation.
703      * For testing sampling within the triangle this predicate is acceptable.
704      *
705      * @see <a
706      * href="https://stackoverflow.com/questions/2049582/how-to-determine-if-a-point-is-in-a-2d-triangle?lq=1">
707      * Point in a triangle</a>
708      * @see <a href="https://stackoverflow.com/a/34093754">Point inside triangle by Cédric Dufour</a>
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          * Create an instance.
721          *
722          * @param p0 triangle vertex 0
723          * @param p1 triangle vertex 1
724          * @param p2 triangle vertex 2
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          * Create an instance.
731          *
732          * @param p0x triangle vertex 0 x coordinate
733          * @param p0y triangle vertex 0 y coordinate
734          * @param p1x triangle vertex 1 x coordinate
735          * @param p1y triangle vertex 1 y coordinate
736          * @param p2x triangle vertex 2 x coordinate
737          * @param p2y triangle vertex 2 y coordinate
738          */
739         Triangle(double p0x, double p0y, double p1x, double p1y, double p2x, double p2y) {
740             this.p2x = p2x;
741             this.p2y = p2y;
742             // Precompute factors
743             dX21 = p2x - p1x;
744             dY12 = p1y - p2y;
745             dY20 = p2y - p0y;
746             dX02 = p0x - p2x;
747             // d = twice the signed area of the triangle
748             d = dY12 * (p0x - p2x) + dX21 * (p0y - p2y);
749         }
750 
751         /**
752          * Check whether or not the triangle contains the given point.
753          *
754          * @param px the point x coordinate
755          * @param py the point y coordinate
756          * @return true if inside the triangle
757          */
758         boolean contains(double px, double py) {
759             // Barycentric coordinates:
760             // p = p0 + (p1 - p0) * s + (p2 - p0) * t
761             // The point p is inside the triangle if 0 <= s <= 1 and 0 <= t <= 1 and s + t <= 1
762             //
763             // The following solves s and t.
764             // Some factors are precomputed.
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 }