| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.commons.rng.sampling.shape; |
| |
| import java.util.Arrays; |
| |
| import org.junit.Assert; |
| import org.junit.Test; |
| |
| import org.apache.commons.math3.stat.inference.ChiSquareTest; |
| |
| import org.apache.commons.rng.UniformRandomProvider; |
| import org.apache.commons.rng.sampling.RandomAssert; |
| import org.apache.commons.rng.simple.RandomSource; |
| |
| /** |
| * Test for {@link TetrahedronSampler}. |
| */ |
| public class TetrahedronSamplerTest { |
| /** |
| * Test invalid vertex dimensions (i.e. not 3D coordinates). |
| */ |
| @Test |
| public void testInvalidDimensionThrows() { |
| final UniformRandomProvider rng = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| final double[] ok = new double[3]; |
| final double[] bad = new double[2]; |
| final double[][] c = {ok, ok, ok, ok}; |
| for (int i = 0; i < c.length; i++) { |
| c[i] = bad; |
| try { |
| TetrahedronSampler.of(c[0], c[1], c[2], c[3], rng); |
| Assert.fail(String.format("Did not detect invalid dimension for vertex: %d", i)); |
| } catch (IllegalArgumentException ex) { |
| // Expected |
| } |
| c[i] = ok; |
| } |
| } |
| |
| /** |
| * Test non-finite vertices. |
| */ |
| @Test |
| public void testNonFiniteVertexCoordinates() { |
| final UniformRandomProvider rng = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| // A valid tetrahedron |
| final double[][] c = new double[][] { |
| {1, 1, 1}, {1, -1, 1}, {-1, 1, 1}, {1, 1, -1} |
| }; |
| Assert.assertNotNull(TetrahedronSampler.of(c[0], c[1], c[2], c[3], rng)); |
| final double[] bad = {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN}; |
| for (int i = 0; i < c.length; i++) { |
| for (int j = 0; j < c[0].length; j++) { |
| for (final double d : bad) { |
| final double value = c[i][j]; |
| c[i][j] = d; |
| try { |
| TetrahedronSampler.of(c[0], c[1], c[2], c[3], rng); |
| Assert.fail(String.format("Did not detect non-finite coordinate: %d,%d = %s", i, j, d)); |
| } catch (IllegalArgumentException ex) { |
| // Expected |
| } |
| c[i][j] = value; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Test a tetrahedron with coordinates that are separated by more than |
| * {@link Double#MAX_VALUE}. |
| */ |
| @Test |
| public void testExtremeValueCoordinates() { |
| // Object seed so use Long not long |
| final Long seed = 876543L; |
| // Create a valid tetrahedron that can be scaled |
| final double[][] c1 = new double[][] { |
| {1, 1, 1}, {1, -1, -1}, {-1, -1, 1}, {-1, 1, -1} |
| }; |
| final double[][] c2 = new double[4][3]; |
| // Extremely large value for scaling. Use a power of 2 for exact scaling. |
| final double scale = 0x1.0p1023; |
| for (int i = 0; i < c1.length; i++) { |
| // Scale the second tetrahedron |
| for (int j = 0; j < 3; j++) { |
| c2[i][j] = c1[i][j] * scale; |
| } |
| } |
| // Show the tetrahedron is too big to compute vectors between points. |
| Assert.assertEquals("Expect vector c - a to be infinite in the x dimension", |
| Double.NEGATIVE_INFINITY, c2[2][0] - c2[0][0], 0.0); |
| Assert.assertEquals("Expect vector c - d to be infinite in the y dimension", |
| Double.NEGATIVE_INFINITY, c2[2][1] - c2[3][1], 0.0); |
| Assert.assertEquals("Expect vector c - b to be infinite in the z dimension", |
| Double.POSITIVE_INFINITY, c2[2][2] - c2[1][2], 0.0); |
| |
| final TetrahedronSampler sampler1 = TetrahedronSampler.of(c1[0], c1[1], c1[2], c1[3], |
| RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP, seed)); |
| final TetrahedronSampler sampler2 = TetrahedronSampler.of(c2[0], c2[1], c2[2], c2[3], |
| RandomSource.create(RandomSource.XO_RO_SHI_RO_128_PP, seed)); |
| |
| for (int n = 0; n < 10; n++) { |
| final double[] a = sampler1.sample(); |
| final double[] b = sampler2.sample(); |
| for (int i = 0; i < a.length; i++) { |
| a[i] *= scale; |
| } |
| Assert.assertArrayEquals(a, b, 0.0); |
| } |
| } |
| |
| /** |
| * Test the distribution of points in three dimensions. 6 tetrahedra are used to create |
| * a box. The distribution should be uniform inside the box. |
| */ |
| @Test |
| public void testDistribution() { |
| // Create the lower and upper limits of the box |
| final double lx = -1; |
| final double ly = -2; |
| final double lz = 1; |
| final double ux = 3; |
| final double uy = 4; |
| final double uz = 5; |
| // Create vertices abcd and efgh for the lower and upper rectangles |
| // (in the XY plane) of the box |
| final double[] a = {lx, ly, lz}; |
| final double[] b = {ux, ly, lz}; |
| final double[] c = {ux, uy, lz}; |
| final double[] d = {lx, uy, lz}; |
| final double[] e = {lx, ly, uz}; |
| final double[] f = {ux, ly, uz}; |
| final double[] g = {ux, uy, uz}; |
| final double[] h = {lx, uy, uz}; |
| |
| // Assign bins |
| final int bins = 10; |
| // Samples should be a multiple of 6 (due to combining 6 equal volume tetrahedra) |
| final int samplesPerBin = 12; |
| // Scale factors to assign x,y,z to a bin |
| final double sx = bins / (ux - lx); |
| final double sy = bins / (uy - ly); |
| final double sz = bins / (uz - lz); |
| |
| // Compute factor to allocate bin index: |
| // index = x + y * binsX + z * binsX * binsY |
| final int binsXy = bins * bins; |
| |
| // Expect a uniform distribution (this is rescaled by the ChiSquareTest) |
| final double[] expected = new double[binsXy * bins]; |
| Arrays.fill(expected, 1); |
| |
| // Increase the loops and use a null seed (i.e. randomly generated) to verify robustness |
| final UniformRandomProvider rng = RandomSource.create(RandomSource.XO_SHI_RO_512_PP, 0xaabbccddeeffL); |
| |
| // Cut the box into 6 equal volume tetrahedra by cutting the box in half three times, |
| // cutting diagonally through each of the three pairs of opposing faces. In this way, |
| // the tetrahedra all share one of the main diagonals of the box (d-f). |
| // See the cuts used for the marching tetrahedra algorithm: |
| // https://en.wikipedia.org/wiki/Marching_tetrahedra |
| final TetrahedronSampler[] samplers = {TetrahedronSampler.of(d, f, b, c, rng), |
| TetrahedronSampler.of(d, f, c, g, rng), |
| TetrahedronSampler.of(d, f, g, h, rng), |
| TetrahedronSampler.of(d, f, h, e, rng), |
| TetrahedronSampler.of(d, f, e, a, rng), |
| TetrahedronSampler.of(d, f, a, b, rng)}; |
| // To determine the sample is inside the correct tetrahedron it is projected to the |
| // 4 faces of the tetrahedron along the face normals. The distance should be negative |
| // when the face normals are orientated outwards. |
| final InsideTetrahedron[] insides = {new InsideTetrahedron(d, f, b, c), |
| new InsideTetrahedron(d, f, c, g), |
| new InsideTetrahedron(d, f, g, h), |
| new InsideTetrahedron(d, f, h, e), |
| new InsideTetrahedron(d, f, e, a), |
| new InsideTetrahedron(d, f, a, b)}; |
| |
| final int samples = expected.length * samplesPerBin; |
| for (int n = 0; n < 1; n++) { |
| // Assign each coordinate to a region inside the combined box |
| final long[] observed = new long[expected.length]; |
| // Equal volume tetrahedra so sample from each one |
| for (int i = 0; i < samples; i += 6) { |
| for (int j = 0; j < 6; j++) { |
| addObservation(samplers[j].sample(), observed, bins, binsXy, |
| lx, ly, lz, sx, sy, sz, insides[j]); |
| } |
| } |
| final double p = new ChiSquareTest().chiSquareTest(expected, observed); |
| Assert.assertFalse("p-value too small: " + p, p < 0.001); |
| } |
| } |
| |
| /** |
| * Adds the observation. Coordinates are mapped using the offsets, scaled and |
| * then cast to an integer bin. |
| * |
| * <pre> |
| * binx = (int) ((x - lx) * sx) |
| * </pre> |
| * |
| * @param v the sample (3D coordinate xyz) |
| * @param observed the observations |
| * @param binsX the numbers of bins in the x dimension |
| * @param binsXy the numbers of bins in the combined x and y dimensions |
| * @param lx the lower limit to convert the x coordinate to the x bin |
| * @param ly the lower limit to convert the y coordinate to the y bin |
| * @param lz the lower limit to convert the z coordinate to the z bin |
| * @param sx the scale to convert the x coordinate to the x bin |
| * @param sy the scale to convert the y coordinate to the y bin |
| * @param sz the scale to convert the z coordinate to the z bin |
| * @param inside the inside tetrahedron test |
| */ |
| // CHECKSTYLE: stop ParameterNumberCheck |
| private static void addObservation(double[] v, long[] observed, |
| int binsX, int binsXy, |
| double lx, double ly, double lz, |
| double sx, double sy, double sz, |
| InsideTetrahedron inside) { |
| Assert.assertEquals(3, v.length); |
| // Test the point is inside the correct tetrahedron |
| Assert.assertTrue("Not inside the tetrahedron", inside.test(v)); |
| final double x = v[0]; |
| final double y = v[1]; |
| final double z = v[2]; |
| // Add to the correct bin after using the offset |
| final int binx = (int) ((x - lx) * sx); |
| final int biny = (int) ((y - ly) * sy); |
| final int binz = (int) ((z - lz) * sz); |
| observed[binz * binsXy + biny * binsX + binx]++; |
| } |
| // CHECKSTYLE: resume ParameterNumberCheck |
| |
| /** |
| * Test the SharedStateSampler implementation. |
| */ |
| @Test |
| public void testSharedStateSampler() { |
| final UniformRandomProvider rng1 = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| final UniformRandomProvider rng2 = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| final double[] c1 = createCoordinate(-1); |
| final double[] c2 = createCoordinate(2); |
| final double[] c3 = createCoordinate(-3); |
| final double[] c4 = createCoordinate(4); |
| final TetrahedronSampler sampler1 = TetrahedronSampler.of(c1, c2, c3, c4, rng1); |
| final TetrahedronSampler sampler2 = sampler1.withUniformRandomProvider(rng2); |
| RandomAssert.assertProduceSameSequence( |
| new RandomAssert.Sampler<double[]>() { |
| @Override |
| public double[] sample() { |
| return sampler1.sample(); |
| } |
| }, |
| new RandomAssert.Sampler<double[]>() { |
| @Override |
| public double[] sample() { |
| return sampler2.sample(); |
| } |
| }); |
| } |
| |
| /** |
| * Test the input vectors are copied and not used by reference. |
| */ |
| @Test |
| public void testChangedInputCoordinates() { |
| final UniformRandomProvider rng1 = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| final UniformRandomProvider rng2 = RandomSource.create(RandomSource.SPLIT_MIX_64, 0L); |
| final double[] c1 = createCoordinate(1); |
| final double[] c2 = createCoordinate(2); |
| final double[] c3 = createCoordinate(-3); |
| final double[] c4 = createCoordinate(-4); |
| final TetrahedronSampler sampler1 = TetrahedronSampler.of(c1, c2, c3, c4, rng1); |
| // Check the input vectors are copied and not used by reference. |
| // Change them in place and create a new sampler. It should have different output |
| // translated by the offset. |
| final double offset = 10; |
| for (int i = 0; i < 3; i++) { |
| c1[i] += offset; |
| c2[i] += offset; |
| c3[i] += offset; |
| c4[i] += offset; |
| } |
| final TetrahedronSampler sampler2 = TetrahedronSampler.of(c1, c2, c3, c4, rng2); |
| for (int n = 0; n < 5; n++) { |
| final double[] s1 = sampler1.sample(); |
| final double[] s2 = sampler2.sample(); |
| Assert.assertEquals(3, s1.length); |
| Assert.assertEquals(3, s2.length); |
| for (int i = 0; i < 3; i++) { |
| Assert.assertEquals(s1[i] + offset, s2[i], 1e-10); |
| } |
| } |
| } |
| |
| /** |
| * Test the inside tetrahedron predicate. |
| */ |
| @Test |
| public void testInsideTetrahedron() { |
| final double[][] c1 = new double[][] { |
| {1, 1, 1}, {1, -1, -1}, {-1, -1, 1}, {-1, 1, -1} |
| }; |
| final InsideTetrahedron inside = new InsideTetrahedron(c1[0], c1[1], c1[2], c1[3]); |
| // Testing points on the vertices, edges or faces are subject to floating point error |
| final double epsilon = 1e-14; |
| // Vertices |
| for (int i = 0; i < 4; i++) { |
| Assert.assertTrue(inside.test(c1[i], epsilon)); |
| } |
| // Edge |
| Assert.assertTrue(inside.test(new double[] {1, 0, 0}, epsilon)); |
| Assert.assertTrue(inside.test(new double[] {0.5, 0.5, 1}, epsilon)); |
| // Just inside the edge |
| Assert.assertTrue(inside.test(new double[] {1 - 1e-10, 0, 0})); |
| Assert.assertTrue(inside.test(new double[] {0.5, 0.5, 1 - 1e-10})); |
| // Just outside the edge |
| Assert.assertFalse(inside.test(new double[] {1, 0, 1e-10}, epsilon)); |
| Assert.assertFalse(inside.test(new double[] {0.5, 0.5 + 1e-10, 1}, epsilon)); |
| // Face |
| double x = 1.0 / 3; |
| Assert.assertTrue(inside.test(new double[] {x, -x, x}, epsilon)); |
| Assert.assertTrue(inside.test(new double[] {-x, -x, -x}, epsilon)); |
| Assert.assertTrue(inside.test(new double[] {x, x, -x}, epsilon)); |
| Assert.assertTrue(inside.test(new double[] {-x, x, x}, epsilon)); |
| // Just outside the face |
| x += 1e-10; |
| Assert.assertFalse(inside.test(new double[] {x, -x, x}, epsilon)); |
| Assert.assertFalse(inside.test(new double[] {-x, -x, -x}, epsilon)); |
| Assert.assertFalse(inside.test(new double[] {x, x, -x}, epsilon)); |
| Assert.assertFalse(inside.test(new double[] {-x, x, x}, epsilon)); |
| // Inside |
| Assert.assertTrue(inside.test(new double[] {0, 0, 0})); |
| Assert.assertTrue(inside.test(new double[] {0.5, 0.25, -0.1})); |
| // Outside |
| Assert.assertFalse(inside.test(new double[] {0, 20, 0})); |
| Assert.assertFalse(inside.test(new double[] {-20, 0, 0})); |
| Assert.assertFalse(inside.test(new double[] {6, 6, 4})); |
| } |
| |
| /** |
| * Creates the coordinate of length 3 filled with |
| * the given value and the dimension index: x + i. |
| * |
| * @param x the value for index 0 |
| * @return the coordinate |
| */ |
| private static double[] createCoordinate(double x) { |
| final double[] coord = new double[3]; |
| for (int i = 0; i < 3; i++) { |
| coord[0] = x + i; |
| } |
| return coord; |
| } |
| |
| /** |
| * Class to test if a point is inside the tetrahedron. |
| * |
| * <p>Computes the outer pointing face normals for the tetrahedron. A point is inside |
| * if the point lies below each of the face planes of the shape. |
| * |
| * @see <a href="https://mathworld.wolfram.com/Point-PlaneDistance.html">Point-Plane distance</a> |
| */ |
| private static class InsideTetrahedron { |
| /** The face normals. */ |
| private final double[][] n; |
| /** The distance of each face from the origin. */ |
| private final double[] d; |
| |
| /** |
| * Create an instance. |
| * |
| * @param v1 The first vertex. |
| * @param v2 The second vertex. |
| * @param v3 The third vertex. |
| * @param v4 The fourth vertex. |
| */ |
| InsideTetrahedron(double[] v1, double[] v2, double[] v3, double[] v4) { |
| // Compute the centre of each face |
| final double[][] x = new double[][] { |
| centre(v1, v2, v3), |
| centre(v2, v3, v4), |
| centre(v3, v4, v1), |
| centre(v4, v1, v2) |
| }; |
| |
| // Compute the normal for each face |
| n = new double[][] { |
| normal(v1, v2, v3), |
| normal(v2, v3, v4), |
| normal(v3, v4, v1), |
| normal(v4, v1, v2) |
| }; |
| |
| // Given the plane: |
| // 0 = ax + by + cz + d |
| // Where abc is the face normal and d is the distance of the plane from the origin. |
| // Compute d: |
| // d = -(ax + by + cz) |
| d = new double[] { |
| -dot(n[0], x[0]), |
| -dot(n[1], x[1]), |
| -dot(n[2], x[2]), |
| -dot(n[3], x[3]), |
| }; |
| |
| // Compute the distance of the other vertex from each face plane. |
| // When below the distance should be negative. Orient each normal so this is true. |
| // |
| // This distance D of a point xyz to the plane is: |
| // D = ax + by + cz + d |
| // Above plane: |
| // ax + by + cz + d > 0 |
| // ax + by + cz > -d |
| final double[][] other = {v4, v1, v2, v3}; |
| for (int i = 0; i < 4; i++) { |
| if (dot(n[i], other[i]) > -d[i]) { |
| // Swap orientation |
| n[i][0] = -n[i][0]; |
| n[i][1] = -n[i][1]; |
| n[i][2] = -n[i][2]; |
| d[i] = -d[i]; |
| } |
| } |
| } |
| |
| /** |
| * Compute the centre of the triangle face. |
| * |
| * @param a The first vertex. |
| * @param b The second vertex. |
| * @param c The third vertex. |
| * @return the centre |
| */ |
| private static double[] centre(double[] a, double[] b, double[] c) { |
| return new double[] { |
| (a[0] + b[0] + c[0]) / 3, |
| (a[1] + b[1] + c[1]) / 3, |
| (a[2] + b[2] + c[2]) / 3 |
| }; |
| } |
| |
| /** |
| * Compute the normal of the triangle face. |
| * |
| * @param a The first vertex. |
| * @param b The second vertex. |
| * @param c The third vertex. |
| * @return the normal |
| */ |
| private static double[] normal(double[] a, double[] b, double[] c) { |
| final double[] v1 = subtract(b, a); |
| final double[] v2 = subtract(c, a); |
| // Cross product |
| final double[] normal = { |
| v1[1] * v2[2] - v1[2] * v2[1], |
| v1[2] * v2[0] - v1[0] * v2[2], |
| v1[0] * v2[1] - v1[1] * v2[0] |
| }; |
| // Normalise |
| final double scale = 1.0 / Math.sqrt(dot(normal, normal)); |
| normal[0] *= scale; |
| normal[1] *= scale; |
| normal[2] *= scale; |
| return normal; |
| } |
| |
| /** |
| * Compute the dot product of vector {@code a} and {@code b}. |
| * |
| * <pre> |
| * a.b = a.x * b.x + a.y * b.y + a.z * b.z |
| * </pre> |
| * |
| * @param a the first vector |
| * @param b the second vector |
| * @return the dot product |
| */ |
| private static double dot(double[] a, double[] b) { |
| return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; |
| } |
| |
| /** |
| * Subtract the second term from the first: {@code a - b}. |
| * |
| * @param a The first term. |
| * @param b The second term. |
| * @return the vector {@code a - b} |
| */ |
| private static double[] subtract(double[] a, double[] b) { |
| return new double[] { |
| a[0] - b[0], |
| a[1] - b[1], |
| a[2] - b[2] |
| }; |
| } |
| |
| /** |
| * Check the point is inside the tetrahedron. |
| * |
| * @param x the coordinate |
| * @return true if inside the tetrahedron |
| */ |
| boolean test(double[] x) { |
| // Must be below all the face planes |
| for (int i = 0; i < 4; i++) { |
| // This distance D of a point xyz to the plane is: |
| // D = ax + by + cz + d |
| // Above plane: |
| // ax + by + cz + d > 0 |
| // ax + by + cz > -d |
| if (dot(n[i], x) > -d[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Check the point is inside the tetrahedron within the given absolute epsilon. |
| * |
| * @param x the coordinate |
| * @param epsilon the epsilon |
| * @return true if inside the tetrahedron |
| */ |
| boolean test(double[] x, double epsilon) { |
| for (int i = 0; i < 4; i++) { |
| // As above but with an epsilon above zero |
| if (dot(n[i], x) > epsilon - d[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| } |