blob: e879b2b41703a11516a914906800456bf199817d [file] [log] [blame]
/*
* 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;
}
}
}