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 TetrahedronSampler}.
28   */
29  class TetrahedronSamplerTest {
30      /**
31       * Test invalid vertex dimensions (i.e. not 3D coordinates).
32       */
33      @Test
34      void testInvalidDimensionThrows() {
35          final UniformRandomProvider rng = RandomAssert.seededRNG();
36          final double[] ok = new double[3];
37          final double[] bad = new double[2];
38          final double[][] c = {ok, ok, ok, ok};
39          for (int i = 0; i < c.length; i++) {
40              final int ii = i;
41              c[i] = bad;
42              Assertions.assertThrows(IllegalArgumentException.class,
43                  () -> TetrahedronSampler.of(rng, c[0], c[1], c[2], c[3]),
44                  () -> String.format("Did not detect invalid dimension for vertex: %d", ii));
45              c[i] = ok;
46          }
47      }
48  
49      /**
50       * Test non-finite vertices.
51       */
52      @Test
53      void testNonFiniteVertexCoordinates() {
54          final UniformRandomProvider rng = RandomAssert.seededRNG();
55          // A valid tetrahedron
56          final double[][] c = new double[][] {
57              {1, 1, 1}, {1, -1, 1}, {-1, 1, 1}, {1, 1, -1}
58          };
59          Assertions.assertNotNull(TetrahedronSampler.of(rng, c[0], c[1], c[2], c[3]));
60          final double[] bad = {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN};
61          for (int i = 0; i < c.length; i++) {
62              final int ii = i;
63              for (int j = 0; j < c[0].length; j++) {
64                  final int jj = j;
65                  for (final double d : bad) {
66                      final double value = c[i][j];
67                      c[i][j] = d;
68                      Assertions.assertThrows(IllegalArgumentException.class,
69                          () -> TetrahedronSampler.of(rng, c[0], c[1], c[2], c[3]),
70                          () -> String.format("Did not detect non-finite coordinate: %d,%d = %s", ii, jj, d));
71                      c[i][j] = value;
72                  }
73              }
74          }
75      }
76  
77      /**
78       * Test a tetrahedron with coordinates that are separated by more than
79       * {@link Double#MAX_VALUE}.
80       */
81      @Test
82      void testExtremeValueCoordinates() {
83          // Create a valid tetrahedron that can be scaled
84          final double[][] c1 = new double[][] {
85              {1, 1, 1}, {1, -1, -1}, {-1, -1, 1}, {-1, 1, -1}
86          };
87          final double[][] c2 = new double[4][3];
88          // Extremely large value for scaling. Use a power of 2 for exact scaling.
89          final double scale = 0x1.0p1023;
90          for (int i = 0; i < c1.length; i++) {
91              // Scale the second tetrahedron
92              for (int j = 0; j < 3; j++) {
93                  c2[i][j] = c1[i][j] * scale;
94              }
95          }
96          // Show the tetrahedron is too big to compute vectors between points.
97          Assertions.assertEquals(Double.NEGATIVE_INFINITY, c2[2][0] - c2[0][0],
98              "Expect vector c - a to be infinite in the x dimension");
99          Assertions.assertEquals(Double.NEGATIVE_INFINITY, c2[2][1] - c2[3][1],
100             "Expect vector c - d to be infinite in the y dimension");
101         Assertions.assertEquals(Double.POSITIVE_INFINITY, c2[2][2] - c2[1][2],
102             "Expect vector c - b to be infinite in the z dimension");
103 
104         final TetrahedronSampler sampler1 = TetrahedronSampler.of(
105             RandomAssert.seededRNG(), c1[0], c1[1], c1[2], c1[3]);
106         final TetrahedronSampler sampler2 = TetrahedronSampler.of(
107             RandomAssert.seededRNG(), c2[0], c2[1], c2[2], c2[3]);
108 
109         for (int n = 0; n < 10; n++) {
110             final double[] a = sampler1.sample();
111             final double[] b = sampler2.sample();
112             for (int i = 0; i < a.length; i++) {
113                 a[i] *= scale;
114             }
115             Assertions.assertArrayEquals(a, b);
116         }
117     }
118 
119     /**
120      * Test the distribution of points in three dimensions. 6 tetrahedra are used to create
121      * a box. The distribution should be uniform inside the box.
122      */
123     @Test
124     void testDistribution() {
125         // Create the lower and upper limits of the box
126         final double lx = -1;
127         final double ly = -2;
128         final double lz = 1;
129         final double ux = 3;
130         final double uy = 4;
131         final double uz = 5;
132         // Create vertices abcd and efgh for the lower and upper rectangles
133         // (in the XY plane) of the box
134         final double[] a = {lx, ly, lz};
135         final double[] b = {ux, ly, lz};
136         final double[] c = {ux, uy, lz};
137         final double[] d = {lx, uy, lz};
138         final double[] e = {lx, ly, uz};
139         final double[] f = {ux, ly, uz};
140         final double[] g = {ux, uy, uz};
141         final double[] h = {lx, uy, uz};
142 
143         // Assign bins
144         final int bins = 10;
145         // Samples should be a multiple of 6 (due to combining 6 equal volume tetrahedra)
146         final int samplesPerBin = 12;
147         // Scale factors to assign x,y,z to a bin
148         final double sx = bins / (ux - lx);
149         final double sy = bins / (uy - ly);
150         final double sz = bins / (uz - lz);
151 
152         // Compute factor to allocate bin index:
153         // index = x + y * binsX + z * binsX * binsY
154         final int binsXy = bins * bins;
155 
156         // Expect a uniform distribution (this is rescaled by the ChiSquareTest)
157         final double[] expected = new double[binsXy * bins];
158         Arrays.fill(expected, 1);
159 
160         // Increase the loops to verify robustness
161         final UniformRandomProvider rng = RandomAssert.createRNG();
162 
163         // Cut the box into 6 equal volume tetrahedra by cutting the box in half three times,
164         // cutting diagonally through each of the three pairs of opposing faces. In this way,
165         // the tetrahedra all share one of the main diagonals of the box (d-f).
166         // See the cuts used for the marching tetrahedra algorithm:
167         // https://en.wikipedia.org/wiki/Marching_tetrahedra
168         final TetrahedronSampler[] samplers = {TetrahedronSampler.of(rng, d, f, b, c),
169                                                TetrahedronSampler.of(rng, d, f, c, g),
170                                                TetrahedronSampler.of(rng, d, f, g, h),
171                                                TetrahedronSampler.of(rng, d, f, h, e),
172                                                TetrahedronSampler.of(rng, d, f, e, a),
173                                                TetrahedronSampler.of(rng, d, f, a, b)};
174         // To determine the sample is inside the correct tetrahedron it is projected to the
175         // 4 faces of the tetrahedron along the face normals. The distance should be negative
176         // when the face normals are orientated outwards.
177         final Tetrahedron[] tetrahedrons = {new Tetrahedron(d, f, b, c),
178                                             new Tetrahedron(d, f, c, g),
179                                             new Tetrahedron(d, f, g, h),
180                                             new Tetrahedron(d, f, h, e),
181                                             new Tetrahedron(d, f, e, a),
182                                             new Tetrahedron(d, f, a, b)};
183 
184         final int samples = expected.length * samplesPerBin;
185         for (int n = 0; n < 1; n++) {
186             // Assign each coordinate to a region inside the combined box
187             final long[] observed = new long[expected.length];
188             // Equal volume tetrahedra so sample from each one
189             for (int i = 0; i < samples; i += 6) {
190                 for (int j = 0; j < 6; j++) {
191                     addObservation(samplers[j].sample(), observed, bins, binsXy,
192                                    lx, ly, lz, sx, sy, sz, tetrahedrons[j]);
193                 }
194             }
195             final double p = new ChiSquareTest().chiSquareTest(expected, observed);
196             Assertions.assertFalse(p < 0.001, () -> "p-value too small: " + p);
197         }
198     }
199 
200     /**
201      * Adds the observation. Coordinates are mapped using the offsets, scaled and
202      * then cast to an integer bin.
203      *
204      * <pre>
205      * binx = (int) ((x - lx) * sx)
206      * </pre>
207      *
208      * @param v the sample (3D coordinate xyz)
209      * @param observed the observations
210      * @param binsX the numbers of bins in the x dimension
211      * @param binsXy the numbers of bins in the combined x and y dimensions
212      * @param lx the lower limit to convert the x coordinate to the x bin
213      * @param ly the lower limit to convert the y coordinate to the y bin
214      * @param lz the lower limit to convert the z coordinate to the z bin
215      * @param sx the scale to convert the x coordinate to the x bin
216      * @param sy the scale to convert the y coordinate to the y bin
217      * @param sz the scale to convert the z coordinate to the z bin
218      * @param tetrahedron the tetrahedron the sample should be within
219      */
220     // CHECKSTYLE: stop ParameterNumberCheck
221     private static void addObservation(double[] v, long[] observed,
222                                        int binsX, int binsXy,
223                                        double lx, double ly, double lz,
224                                        double sx, double sy, double sz,
225                                        Tetrahedron tetrahedron) {
226         Assertions.assertEquals(3, v.length);
227         // Test the point is inside the correct tetrahedron
228         Assertions.assertTrue(tetrahedron.contains(v), "Not inside the tetrahedron");
229         final double x = v[0];
230         final double y = v[1];
231         final double z = v[2];
232         // Add to the correct bin after using the offset
233         final int binx = (int) ((x - lx) * sx);
234         final int biny = (int) ((y - ly) * sy);
235         final int binz = (int) ((z - lz) * sz);
236         observed[binz * binsXy + biny * binsX + binx]++;
237     }
238     // CHECKSTYLE: resume ParameterNumberCheck
239 
240     /**
241      * Test the SharedStateSampler implementation.
242      */
243     @Test
244     void testSharedStateSampler() {
245         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
246         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
247         final double[] c1 = createCoordinate(-1);
248         final double[] c2 = createCoordinate(2);
249         final double[] c3 = createCoordinate(-3);
250         final double[] c4 = createCoordinate(4);
251         final TetrahedronSampler sampler1 = TetrahedronSampler.of(rng1, c1, c2, c3, c4);
252         final TetrahedronSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
253         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
254     }
255 
256     /**
257      * Test the input vectors are copied and not used by reference.
258      */
259     @Test
260     void testChangedInputCoordinates() {
261         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
262         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
263         final double[] c1 = createCoordinate(1);
264         final double[] c2 = createCoordinate(2);
265         final double[] c3 = createCoordinate(-3);
266         final double[] c4 = createCoordinate(-4);
267         final TetrahedronSampler sampler1 = TetrahedronSampler.of(rng1, c1, c2, c3, c4);
268         // Check the input vectors are copied and not used by reference.
269         // Change them in place and create a new sampler. It should have different output
270         // translated by the offset.
271         final double offset = 10;
272         for (int i = 0; i < 3; i++) {
273             c1[i] += offset;
274             c2[i] += offset;
275             c3[i] += offset;
276             c4[i] += offset;
277         }
278         final TetrahedronSampler sampler2 = TetrahedronSampler.of(rng2, c1, c2, c3, c4);
279         for (int n = 0; n < 5; n++) {
280             final double[] s1 = sampler1.sample();
281             final double[] s2 = sampler2.sample();
282             Assertions.assertEquals(3, s1.length);
283             Assertions.assertEquals(3, s2.length);
284             for (int i = 0; i < 3; i++) {
285                 Assertions.assertEquals(s1[i] + offset, s2[i], 1e-10);
286             }
287         }
288     }
289 
290     /**
291      * Test the tetrahedron contains predicate.
292      */
293     @Test
294     void testTetrahedronContains() {
295         final double[][] c1 = new double[][] {
296             {1, 1, 1}, {1, -1, -1}, {-1, -1, 1}, {-1, 1, -1}
297         };
298         final Tetrahedron tetrahedron = new Tetrahedron(c1[0], c1[1], c1[2], c1[3]);
299         // Testing points on the vertices, edges or faces are subject to floating point error
300         final double epsilon = 1e-14;
301         // Vertices
302         for (int i = 0; i < 4; i++) {
303             Assertions.assertTrue(tetrahedron.contains(c1[i], epsilon));
304         }
305         // Edge
306         Assertions.assertTrue(tetrahedron.contains(new double[] {1, 0, 0}, epsilon));
307         Assertions.assertTrue(tetrahedron.contains(new double[] {0.5, 0.5, 1}, epsilon));
308         // Just inside the edge
309         Assertions.assertTrue(tetrahedron.contains(new double[] {1 - 1e-10, 0, 0}));
310         Assertions.assertTrue(tetrahedron.contains(new double[] {0.5, 0.5, 1 - 1e-10}));
311         // Just outside the edge
312         Assertions.assertFalse(tetrahedron.contains(new double[] {1, 0, 1e-10}, epsilon));
313         Assertions.assertFalse(tetrahedron.contains(new double[] {0.5, 0.5 + 1e-10, 1}, epsilon));
314         // Face
315         double x = 1.0 / 3;
316         Assertions.assertTrue(tetrahedron.contains(new double[] {x, -x, x}, epsilon));
317         Assertions.assertTrue(tetrahedron.contains(new double[] {-x, -x, -x}, epsilon));
318         Assertions.assertTrue(tetrahedron.contains(new double[] {x, x, -x}, epsilon));
319         Assertions.assertTrue(tetrahedron.contains(new double[] {-x, x, x}, epsilon));
320         // Just outside the face
321         x += 1e-10;
322         Assertions.assertFalse(tetrahedron.contains(new double[] {x, -x, x}, epsilon));
323         Assertions.assertFalse(tetrahedron.contains(new double[] {-x, -x, -x}, epsilon));
324         Assertions.assertFalse(tetrahedron.contains(new double[] {x, x, -x}, epsilon));
325         Assertions.assertFalse(tetrahedron.contains(new double[] {-x, x, x}, epsilon));
326         // Inside
327         Assertions.assertTrue(tetrahedron.contains(new double[] {0, 0, 0}));
328         Assertions.assertTrue(tetrahedron.contains(new double[] {0.5, 0.25, -0.1}));
329         // Outside
330         Assertions.assertFalse(tetrahedron.contains(new double[] {0, 20, 0}));
331         Assertions.assertFalse(tetrahedron.contains(new double[] {-20, 0, 0}));
332         Assertions.assertFalse(tetrahedron.contains(new double[] {6, 6, 4}));
333     }
334 
335     /**
336      * Creates the coordinate of length 3 filled with
337      * the given value and the dimension index: x + i.
338      *
339      * @param x the value for index 0
340      * @return the coordinate
341      */
342     private static double[] createCoordinate(double x) {
343         final double[] coord = new double[3];
344         for (int i = 0; i < 3; i++) {
345             coord[0] = x + i;
346         }
347         return coord;
348     }
349 
350     /**
351      * Class to test if a point is inside the tetrahedron.
352      *
353      * <p>Computes the outer pointing face normals for the tetrahedron. A point is inside
354      * if the point lies below each of the face planes of the shape.
355      *
356      * @see <a href="https://mathworld.wolfram.com/Point-PlaneDistance.html">Point-Plane distance</a>
357      */
358     private static class Tetrahedron {
359         /** The face normals. */
360         private final double[][] n;
361         /** The distance of each face from the origin. */
362         private final double[] d;
363 
364         /**
365          * Create an instance.
366          *
367          * @param v1 The first vertex.
368          * @param v2 The second vertex.
369          * @param v3 The third vertex.
370          * @param v4 The fourth vertex.
371          */
372         Tetrahedron(double[] v1, double[] v2, double[] v3, double[] v4) {
373             // Compute the centre of each face
374             final double[][] x = new double[][] {
375                 centre(v1, v2, v3),
376                 centre(v2, v3, v4),
377                 centre(v3, v4, v1),
378                 centre(v4, v1, v2)
379             };
380 
381             // Compute the normal for each face
382             n = new double[][] {
383                 normal(v1, v2, v3),
384                 normal(v2, v3, v4),
385                 normal(v3, v4, v1),
386                 normal(v4, v1, v2)
387             };
388 
389             // Given the plane:
390             // 0 = ax + by + cz + d
391             // Where abc is the face normal and d is the distance of the plane from the origin.
392             // Compute d:
393             // d = -(ax + by + cz)
394             d = new double[] {
395                 -dot(n[0], x[0]),
396                 -dot(n[1], x[1]),
397                 -dot(n[2], x[2]),
398                 -dot(n[3], x[3]),
399             };
400 
401             // Compute the distance of the other vertex from each face plane.
402             // When below the distance should be negative. Orient each normal so this is true.
403             //
404             // This distance D of a point xyz to the plane is:
405             // D = ax + by + cz + d
406             // Above plane:
407             // ax + by + cz + d > 0
408             // ax + by + cz > -d
409             final double[][] other = {v4, v1, v2, v3};
410             for (int i = 0; i < 4; i++) {
411                 if (dot(n[i], other[i]) > -d[i]) {
412                     // Swap orientation
413                     n[i][0] = -n[i][0];
414                     n[i][1] = -n[i][1];
415                     n[i][2] = -n[i][2];
416                     d[i] = -d[i];
417                 }
418             }
419         }
420 
421         /**
422          * Compute the centre of the triangle face.
423          *
424          * @param a The first vertex.
425          * @param b The second vertex.
426          * @param c The third vertex.
427          * @return the centre
428          */
429         private static double[] centre(double[] a, double[] b, double[] c) {
430             return new double[] {
431                 (a[0] + b[0] + c[0]) / 3,
432                 (a[1] + b[1] + c[1]) / 3,
433                 (a[2] + b[2] + c[2]) / 3
434             };
435         }
436 
437         /**
438          * Compute the normal of the triangle face.
439          *
440          * @param a The first vertex.
441          * @param b The second vertex.
442          * @param c The third vertex.
443          * @return the normal
444          */
445         private static double[] normal(double[] a, double[] b, double[] c) {
446             final double[] v1 = subtract(b, a);
447             final double[] v2 = subtract(c, a);
448             // Cross product
449             final double[] normal = {
450                 v1[1] * v2[2] - v1[2] * v2[1],
451                 v1[2] * v2[0] - v1[0] * v2[2],
452                 v1[0] * v2[1] - v1[1] * v2[0]
453             };
454             // Normalise
455             final double scale = 1.0 / Math.sqrt(dot(normal, normal));
456             normal[0] *= scale;
457             normal[1] *= scale;
458             normal[2] *= scale;
459             return normal;
460         }
461 
462         /**
463          * Compute the dot product of vector {@code a} and {@code b}.
464          *
465          * <pre>
466          * a.b = a.x * b.x + a.y * b.y + a.z * b.z
467          * </pre>
468          *
469          * @param a the first vector
470          * @param b the second vector
471          * @return the dot product
472          */
473         private static double dot(double[] a, double[] b) {
474             return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
475         }
476 
477         /**
478          * Subtract the second term from the first: {@code a - b}.
479          *
480          * @param a The first term.
481          * @param b The second term.
482          * @return the vector {@code a - b}
483          */
484         private static double[] subtract(double[] a, double[] b) {
485             return new double[] {
486                 a[0] - b[0],
487                 a[1] - b[1],
488                 a[2] - b[2]
489             };
490         }
491 
492         /**
493          * Check whether or not the tetrahedron contains the given point.
494          *
495          * @param x the coordinate
496          * @return true if inside the tetrahedron
497          */
498         boolean contains(double[] x) {
499             // Must be below all the face planes
500             for (int i = 0; i < 4; i++) {
501                 // This distance D of a point xyz to the plane is:
502                 // D = ax + by + cz + d
503                 // Above plane:
504                 // ax + by + cz + d > 0
505                 // ax + by + cz > -d
506                 if (dot(n[i], x) > -d[i]) {
507                     return false;
508                 }
509             }
510             return true;
511         }
512 
513         /**
514          * Check whether or not the tetrahedron contains the given point
515          * within the given absolute epsilon.
516          *
517          * @param x the coordinate
518          * @param epsilon the epsilon
519          * @return true if inside the tetrahedron
520          */
521         boolean contains(double[] x, double epsilon) {
522             for (int i = 0; i < 4; i++) {
523                 // As above but with an epsilon above zero
524                 if (dot(n[i], x) > epsilon - d[i]) {
525                     return false;
526                 }
527             }
528             return true;
529         }
530     }
531 }