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 java.util.stream.DoubleStream;
21  import org.apache.commons.rng.UniformRandomProvider;
22  import org.apache.commons.rng.sampling.RandomAssert;
23  import org.apache.commons.rng.sampling.UnitSphereSampler;
24  import org.junit.jupiter.api.Assertions;
25  import org.junit.jupiter.api.Test;
26  
27  /**
28   * Test for {@link LineSampler}.
29   */
30  class LineSamplerTest {
31      /**
32       * Test an unsupported dimension.
33       */
34      @Test
35      void testInvalidDimensionThrows() {
36          final UniformRandomProvider rng = RandomAssert.seededRNG();
37          Assertions.assertThrows(IllegalArgumentException.class,
38              () -> LineSampler.of(rng, new double[0], new double[0]));
39      }
40  
41      /**
42       * Test a dimension mismatch between vertices.
43       */
44      @Test
45      void testDimensionMismatchThrows() {
46          final UniformRandomProvider rng = RandomAssert.seededRNG();
47          final double[] c2 = new double[2];
48          final double[] c3 = new double[3];
49          for (double[][] c : new double[][][] {
50              {c2, c3},
51              {c3, c2},
52          }) {
53              Assertions.assertThrows(IllegalArgumentException.class,
54                  () -> LineSampler.of(rng, c[0], c[1]),
55                  () -> String.format("Did not detect dimension mismatch: %d,%d",
56                      c[0].length, c[1].length));
57          }
58      }
59  
60      /**
61       * Test non-finite vertices.
62       */
63      @Test
64      void testNonFiniteVertexCoordinates() {
65          final UniformRandomProvider rng = RandomAssert.seededRNG();
66          // A valid line
67          final double[][] c = new double[][] {
68              {0, 1, 2}, {-1, 2, 3}
69          };
70          Assertions.assertNotNull(LineSampler.of(rng, c[0],  c[1]));
71          final double[] bad = {Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NaN};
72          for (int i = 0; i < c.length; i++) {
73              final int ii = i;
74              for (int j = 0; j < c[0].length; j++) {
75                  final int jj = j;
76                  for (final double d : bad) {
77                      final double value = c[i][j];
78                      c[i][j] = d;
79                      Assertions.assertThrows(IllegalArgumentException.class,
80                          () -> LineSampler.of(rng, c[0], c[1]),
81                          () -> String.format("Did not detect non-finite coordinate: %d,%d = %s",
82                              ii, jj, d));
83                      c[i][j] = value;
84                  }
85              }
86          }
87      }
88  
89      /**
90       * Test a line with coordinates that are separated by more than
91       * {@link Double#MAX_VALUE} in 1D.
92       */
93      @Test
94      void testExtremeValueCoordinates1D() {
95          testExtremeValueCoordinates(1);
96      }
97  
98      /**
99       * Test a line with coordinates that are separated by more than
100      * {@link Double#MAX_VALUE} in 2D.
101      */
102     @Test
103     void testExtremeValueCoordinates2D() {
104         testExtremeValueCoordinates(2);
105     }
106 
107     /**
108      * Test a line with coordinates that are separated by more than
109      * {@link Double#MAX_VALUE} in 3D.
110      */
111     @Test
112     void testExtremeValueCoordinates3D() {
113         testExtremeValueCoordinates(3);
114     }
115 
116     /**
117      * Test a line with coordinates that are separated by more than
118      * {@link Double#MAX_VALUE} in 4D.
119      */
120     @Test
121     void testExtremeValueCoordinates4D() {
122         testExtremeValueCoordinates(4);
123     }
124 
125     /**
126      * Test a line with coordinates that are separated by more than
127      * {@link Double#MAX_VALUE}.
128      *
129      * @param dimension the dimension
130      */
131     private static void testExtremeValueCoordinates(int dimension) {
132         final double[][] c1 = new double[2][dimension];
133         final double[][] c2 = new double[2][dimension];
134         // Create a valid line that can be scaled
135         Arrays.fill(c1[0], -1);
136         Arrays.fill(c1[1], 1);
137         // Extremely large value for scaling. Use a power of 2 for exact scaling.
138         final double scale = 0x1.0p1023;
139         for (int i = 0; i < c1.length; i++) {
140             // Scale the second line
141             for (int j = 0; j < dimension; j++) {
142                 c2[i][j] = c1[i][j] * scale;
143             }
144         }
145         // Show the line is too big to compute vectors between points.
146         Assertions.assertEquals(Double.POSITIVE_INFINITY, c2[1][0] - c2[0][0],
147             "Expect vector b - a to be infinite in the x dimension");
148 
149         final LineSampler sampler1 = LineSampler.of(
150             RandomAssert.seededRNG(), c1[0], c1[1]);
151         final LineSampler sampler2 = LineSampler.of(
152             RandomAssert.seededRNG(), c2[0], c2[1]);
153 
154         for (int n = 0; n < 10; n++) {
155             final double[] a = sampler1.sample();
156             final double[] b = sampler2.sample();
157             for (int i = 0; i < a.length; i++) {
158                 a[i] *= scale;
159             }
160             Assertions.assertArrayEquals(a, b);
161         }
162     }
163 
164     /**
165      * Test the distribution of points in 1D.
166      */
167     @Test
168     void testDistribution1D() {
169         testDistributionND(1);
170     }
171 
172     /**
173      * Test the distribution of points in 2D.
174      */
175     @Test
176     void testDistribution2D() {
177         testDistributionND(2);
178     }
179 
180     /**
181      * Test the distribution of points in 3D.
182      */
183     @Test
184     void testDistribution3D() {
185         testDistributionND(3);
186     }
187 
188     /**
189      * Test the distribution of points in 4D.
190      */
191     @Test
192     void testDistribution4D() {
193         testDistributionND(4);
194     }
195 
196     /**
197      * Test the distribution of points in N dimensions. The output coordinates
198      * should be uniform in the line.
199      *
200      * @param dimension the dimension
201      */
202     private static void testDistributionND(int dimension) {
203         final UniformRandomProvider rng = RandomAssert.createRNG();
204 
205         double[] a;
206         double[] b;
207         if (dimension == 1) {
208             a = new double[] {rng.nextDouble()};
209             b = new double[] {-rng.nextDouble()};
210         } else {
211             final UnitSphereSampler sphere = UnitSphereSampler.of(rng, dimension);
212             a = sphere.sample();
213             b = sphere.sample();
214         }
215 
216         // To test uniformity on the line all fractional lengths along each dimension
217         // should be the same constant:
218         // x - a
219         // ----- = C
220         // b - a
221         // This should be uniformly distributed in the range [0, 1].
222         // Pre-compute scaling:
223         final double[] scale = new double[dimension];
224         for (int i = 0; i < dimension; i++) {
225             scale[i] = 1.0 / (b[i] - a[i]);
226         }
227 
228         // Use two RNGs with the same output. The line sampler only uses a single double
229         // per sample so we can assert that the point is approximately located along the
230         // line vector is each dimension by the constant C. Floating-point tolerance
231         // is required to validate the equality.
232         final UniformRandomProvider[] rngs = RandomAssert.createRNG(2);
233         final UniformRandomProvider rng1 = rngs[0];
234         final UniformRandomProvider rng2 = rngs[1];
235         final LineSampler sampler = LineSampler.of(rng1, a, b);
236         final double relEps = 1e-10;
237         final int n = 1000;
238         final DoubleStream.Builder errors = DoubleStream.builder();
239         for (int i = 0; i < n; i++) {
240             final double[] x = sampler.sample();
241             Assertions.assertEquals(dimension, x.length);
242             final double c = rng2.nextDouble();
243             for (int j = 0; j < dimension; j++) {
244                 final double u = (x[j] - a[j]) * scale[j];
245                 Assertions.assertTrue(u >= 0.0 && u <= 1.0, "Not within the line");
246                 final double e = Math.abs(c - u) / u;
247                 errors.add(e);
248                 Assertions.assertEquals(c, u, c * relEps,
249                     () -> "Not on the expected line: rel.error = " + e);
250             }
251         }
252         final double averageRelError = errors.build().sum() / n;
253         Assertions.assertTrue(averageRelError < 1e-12,
254             () -> "Average relative error is too large = " + averageRelError);
255     }
256 
257     /**
258      * Test the SharedStateSampler implementation for 1D.
259      */
260     @Test
261     void testSharedStateSampler1D() {
262         testSharedStateSampler(1);
263     }
264 
265     /**
266      * Test the SharedStateSampler implementation for 2D.
267      */
268     @Test
269     void testSharedStateSampler2D() {
270         testSharedStateSampler(2);
271     }
272 
273     /**
274      * Test the SharedStateSampler implementation for 3D.
275      */
276     @Test
277     void testSharedStateSampler3D() {
278         testSharedStateSampler(3);
279     }
280 
281     /**
282      * Test the SharedStateSampler implementation for 4D.
283      */
284     @Test
285     void testSharedStateSampler4D() {
286         testSharedStateSampler(4);
287     }
288 
289     /**
290      * Test the SharedStateSampler implementation for the given dimension.
291      */
292     private static void testSharedStateSampler(int dimension) {
293         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
294         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
295         final double[] c1 = createCoordinate(1, dimension);
296         final double[] c2 = createCoordinate(2, dimension);
297         final LineSampler sampler1 = LineSampler.of(rng1, c1, c2);
298         final LineSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
299         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
300     }
301 
302     /**
303      * Test the input vectors are copied and not used by reference for 1D.
304      */
305     @Test
306     void testChangedInputCoordinates1D() {
307         testChangedInputCoordinates(1);
308     }
309 
310     /**
311      * Test the input vectors are copied and not used by reference for 2D.
312      */
313     @Test
314     void testChangedInputCoordinates2D() {
315         testChangedInputCoordinates(2);
316     }
317 
318     /**
319      * Test the input vectors are copied and not used by reference for 3D.
320      */
321     @Test
322     void testChangedInputCoordinates3D() {
323         testChangedInputCoordinates(3);
324     }
325 
326     /**
327      * Test the input vectors are copied and not used by reference for 4D.
328      */
329     @Test
330     void testChangedInputCoordinates4D() {
331         testChangedInputCoordinates(4);
332     }
333 
334     /**
335      * Test the input vectors are copied and not used by reference for the given
336      * dimension.
337      *
338      * @param dimension the dimension
339      */
340     private static void testChangedInputCoordinates(int dimension) {
341         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
342         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
343         final double[] c1 = createCoordinate(1, dimension);
344         final double[] c2 = createCoordinate(2, dimension);
345         final LineSampler sampler1 = LineSampler.of(rng1, c1, c2);
346         // Check the input vectors are copied and not used by reference.
347         // Change them in place and create a new sampler. It should have different output
348         // translated by the offset.
349         final double offset = 10;
350         for (int i = 0; i < dimension; i++) {
351             c1[i] += offset;
352             c2[i] += offset;
353         }
354         final LineSampler sampler2 = LineSampler.of(rng2, c1, c2);
355         for (int n = 0; n < 3; n++) {
356             final double[] s1 = sampler1.sample();
357             final double[] s2 = sampler2.sample();
358             Assertions.assertEquals(s1.length, s2.length);
359             Assertions.assertFalse(Arrays.equals(s1, s2),
360                 "First sampler has used the vertices by reference");
361             for (int i = 0; i < dimension; i++) {
362                 Assertions.assertEquals(s1[i] + offset, s2[i], 1e-10);
363             }
364         }
365     }
366 
367     /**
368      * Creates the coordinate of length specified by the dimension filled with
369      * the given value and the dimension index: x + i.
370      *
371      * @param x the value for index 0
372      * @param dimension the dimension
373      * @return the coordinate
374      */
375     private static double[] createCoordinate(double x, int dimension) {
376         final double[] coord = new double[dimension];
377         for (int i = 0; i < dimension; i++) {
378             coord[0] = x + i;
379         }
380         return coord;
381     }
382 }