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  
18  package org.apache.commons.rng.sampling;
19  
20  import java.util.Arrays;
21  import java.util.concurrent.atomic.AtomicInteger;
22  import org.apache.commons.math3.stat.inference.ChiSquareTest;
23  import org.apache.commons.rng.UniformRandomProvider;
24  import org.apache.commons.rng.sampling.CompositeSamplers.Builder;
25  import org.apache.commons.rng.sampling.CompositeSamplers.DiscreteProbabilitySampler;
26  import org.apache.commons.rng.sampling.CompositeSamplers.DiscreteProbabilitySamplerFactory;
27  import org.apache.commons.rng.sampling.distribution.AliasMethodDiscreteSampler;
28  import org.apache.commons.rng.sampling.distribution.ContinuousSampler;
29  import org.apache.commons.rng.sampling.distribution.DiscreteSampler;
30  import org.apache.commons.rng.sampling.distribution.GuideTableDiscreteSampler;
31  import org.apache.commons.rng.sampling.distribution.LongSampler;
32  import org.apache.commons.rng.sampling.distribution.SharedStateContinuousSampler;
33  import org.apache.commons.rng.sampling.distribution.SharedStateDiscreteSampler;
34  import org.apache.commons.rng.sampling.distribution.SharedStateLongSampler;
35  import org.junit.jupiter.api.Assertions;
36  import org.junit.jupiter.api.Test;
37  
38  /**
39   * Test class for {@link CompositeSamplers}.
40   */
41  class CompositeSamplersTest {
42      /**
43       * Test the default implementations of the discrete probability sampler factory.
44       */
45      @Test
46      void testDiscreteProbabilitySampler() {
47          final UniformRandomProvider rng = RandomAssert.createRNG();
48          final double[] probabilities = {0.1, 0.2, 0.3, 0.4};
49  
50          final ChiSquareTest chisq = new ChiSquareTest();
51  
52          final int n = 1000000;
53          for (final DiscreteProbabilitySampler item : DiscreteProbabilitySampler.values()) {
54              final DiscreteSampler sampler = item.create(rng, probabilities.clone());
55              final long[] observed = new long[probabilities.length];
56              for (int i = 0; i < n; i++) {
57                  observed[sampler.sample()]++;
58              }
59              final double p = chisq.chiSquareTest(probabilities, observed);
60              Assertions.assertFalse(p < 0.001, () -> item + " p-value too small: " + p);
61          }
62      }
63  
64      /**
65       * Test an empty builder cannot build a sampler.
66       */
67      @Test
68      void testEmptyBuilderThrows() {
69          final UniformRandomProvider rng = RandomAssert.seededRNG();
70          final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
71                  .newSharedStateObjectSamplerBuilder();
72          Assertions.assertEquals(0, builder.size());
73          Assertions.assertThrows(IllegalStateException.class,
74              () -> builder.build(rng));
75      }
76  
77      /**
78       * Test adding null sampler to a builder.
79       */
80      @Test
81      void testNullSharedStateObjectSamplerThrows() {
82          final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
83                  .newSharedStateObjectSamplerBuilder();
84          Assertions.assertThrows(NullPointerException.class,
85              () -> builder.add(null, 1.0));
86      }
87  
88      /**
89       * Test invalid weights (zero, negative, NaN, infinte).
90       */
91      @Test
92      void testInvalidWeights() {
93          final UniformRandomProvider rng = RandomAssert.seededRNG();
94          final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
95                  .newSharedStateObjectSamplerBuilder();
96          final RangeSampler sampler = new RangeSampler(45, 63, rng);
97          // Zero weight is ignored
98          Assertions.assertEquals(0, builder.size());
99          builder.add(sampler, 0.0);
100         Assertions.assertEquals(0, builder.size());
101 
102         final double[] bad = {-1, Double.NaN, Double.POSITIVE_INFINITY};
103         for (final double weight : bad) {
104             Assertions.assertThrows(IllegalArgumentException.class,
105                 () -> builder.add(sampler, weight),
106                 () -> "Did not detect invalid weight: " + weight);
107         }
108     }
109 
110     /**
111      * Test a single sampler added to the builder is returned without a composite.
112      */
113     @Test
114     void testSingleSharedStateObjectSampler() {
115         final UniformRandomProvider rng = RandomAssert.seededRNG();
116         final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
117                 .newSharedStateObjectSamplerBuilder();
118         final RangeSampler sampler = new RangeSampler(45, 63, rng);
119         builder.add(sampler, 1.0);
120         Assertions.assertEquals(1, builder.size());
121         final SharedStateObjectSampler<Integer> composite = builder.build(rng);
122         Assertions.assertSame(sampler, composite);
123     }
124 
125     /**
126      * Test sampling is uniform across several ObjectSampler samplers.
127      */
128     @Test
129     void testObjectSamplerSamples() {
130         final Builder<ObjectSampler<Integer>> builder = CompositeSamplers.newObjectSamplerBuilder();
131         final UniformRandomProvider rng = RandomAssert.createRNG();
132         final int n = 15;
133         final int min = -134;
134         final int max = 2097;
135         addObjectSamplers(builder, n, min, max, rng);
136         assertObjectSamplerSamples(builder.build(rng), min, max);
137     }
138 
139     /**
140      * Test sampling is uniform across several SharedStateObjectSampler samplers.
141      */
142     @Test
143     void testSharedStateObjectSamplerSamples() {
144         final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
145                 .newSharedStateObjectSamplerBuilder();
146         final UniformRandomProvider rng = RandomAssert.createRNG();
147         final int n = 11;
148         final int min = 42;
149         final int max = 678;
150         addObjectSamplers(builder, n, min, max, rng);
151         // Exercise the shared state interface
152         final UniformRandomProvider rng1 = RandomAssert.createRNG();
153         assertObjectSamplerSamples(builder.build(rng).withUniformRandomProvider(rng1), min, max);
154     }
155 
156     /**
157      * Test sampling is uniform across several SharedStateObjectSampler samplers
158      * using a custom factory that implements SharedStateDiscreteSampler.
159      */
160     @Test
161     void testSharedStateObjectSamplerSamplesWithCustomSharedStateDiscreteSamplerFactory() {
162         final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
163                 .newSharedStateObjectSamplerBuilder();
164         final AtomicInteger factoryCount = new AtomicInteger();
165         builder.setFactory(new DiscreteProbabilitySamplerFactory() {
166             @Override
167             public DiscreteSampler create(UniformRandomProvider rng, double[] probabilities) {
168                 factoryCount.incrementAndGet();
169                 // Use an expanded table with a non-default alpha
170                 return AliasMethodDiscreteSampler.of(rng, probabilities, 2);
171             }
172         });
173         final UniformRandomProvider rng = RandomAssert.createRNG();
174         final int n = 7;
175         final int min = -610;
176         final int max = 745;
177         addObjectSamplers(builder, n, min, max, rng);
178 
179         // Exercise the shared state interface
180         final UniformRandomProvider rng1 = RandomAssert.createRNG();
181         assertObjectSamplerSamples(builder.build(rng).withUniformRandomProvider(rng1), min, max);
182 
183         Assertions.assertEquals(1, factoryCount.get(), "Factory should not be used to create the shared state sampler");
184     }
185 
186     /**
187      * Test sampling is uniform across several SharedStateObjectSampler samplers
188      * using a custom factory that implements DiscreteSampler (so must be wrapped).
189      */
190     @Test
191     void testSharedStateObjectSamplerSamplesWithCustomDiscreteSamplerFactory() {
192         final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
193                 .newSharedStateObjectSamplerBuilder();
194         final AtomicInteger factoryCount = new AtomicInteger();
195         builder.setFactory(new DiscreteProbabilitySamplerFactory() {
196             @Override
197             public DiscreteSampler create(UniformRandomProvider rng, double[] probabilities) {
198                 factoryCount.incrementAndGet();
199                 // Wrap so it is not a SharedStateSamplerInstance.
200                 final DiscreteSampler sampler = GuideTableDiscreteSampler.of(rng, probabilities, 2);
201                 // Destroy the probabilities to check that custom factories are not trusted.
202                 Arrays.fill(probabilities, Double.NaN);
203                 return new DiscreteSampler() {
204                     @Override
205                     public int sample() {
206                         return sampler.sample();
207                     }
208                 };
209             }
210         });
211         final UniformRandomProvider rng = RandomAssert.createRNG();
212         final int n = 14;
213         final int min = 56;
214         final int max = 2033;
215         addObjectSamplers(builder, n, min, max, rng);
216 
217         // Exercise the shared state interface.
218         // This tests the custom factory is used twice.
219         final UniformRandomProvider rng1 = RandomAssert.createRNG();
220         assertObjectSamplerSamples(builder.build(rng).withUniformRandomProvider(rng1), min, max);
221 
222         Assertions.assertEquals(2, factoryCount.get(), "Factory should be used to create the shared state sampler");
223     }
224 
225     /**
226      * Test sampling is uniform across several ObjectSampler samplers with a uniform
227      * weighting. This tests an edge case where there is no requirement for a
228      * sampler from a discrete probability distribution as the distribution is
229      * uniform.
230      */
231     @Test
232     void testObjectSamplerSamplesWithUniformWeights() {
233         final Builder<ObjectSampler<Integer>> builder = CompositeSamplers.newObjectSamplerBuilder();
234         final UniformRandomProvider rng = RandomAssert.createRNG();
235         final int max = 60;
236         final int interval = 10;
237         for (int min = 0; min < max; min += interval) {
238             builder.add(new RangeSampler(min, min + interval, rng), 1.0);
239         }
240         assertObjectSamplerSamples(builder.build(rng), 0, max);
241     }
242 
243     /**
244      * Test sampling is uniform across several ObjectSampler samplers with very
245      * large weights. This tests an edge case where the weights with sum to
246      * infinity.
247      */
248     @Test
249     void testObjectSamplerSamplesWithVeryLargeWeights() {
250         final Builder<ObjectSampler<Integer>> builder = CompositeSamplers.newObjectSamplerBuilder();
251         final UniformRandomProvider rng = RandomAssert.createRNG();
252         // Ratio 4:4:2:1
253         // The weights will sum to infinity as they are more than 2^1024.
254         final double w4 = 0x1.0p1023;
255         final double w2 = 0x1.0p1022;
256         final double w1 = 0x1.0p1021;
257         Assertions.assertEquals(Double.POSITIVE_INFINITY, w4 + w4 + w2 + w1);
258         builder.add(new RangeSampler(0, 40, rng), w4);
259         builder.add(new RangeSampler(40, 80, rng), w4);
260         builder.add(new RangeSampler(80, 100, rng), w2);
261         builder.add(new RangeSampler(100, 110, rng), w1);
262         assertObjectSamplerSamples(builder.build(rng), 0, 110);
263     }
264 
265     /**
266      * Test sampling is uniform across several ObjectSampler samplers with very
267      * small weights. This tests an edge case where the weights divided by their sum
268      * are valid (due to accurate floating-point division) but cannot be multiplied
269      * by the reciprocal of the sum.
270      */
271     @Test
272     void testObjectSamplerSamplesWithSubNormalWeights() {
273         final Builder<ObjectSampler<Integer>> builder = CompositeSamplers.newObjectSamplerBuilder();
274         final UniformRandomProvider rng = RandomAssert.createRNG();
275         // Ratio 4:4:2:1
276         // The weights are very small sub-normal numbers
277         final double w4 = Double.MIN_VALUE * 4;
278         final double w2 = Double.MIN_VALUE * 2;
279         final double w1 = Double.MIN_VALUE;
280         final double sum = w4 + w4 + w2 + w1;
281         // Cannot do a divide by multiplying by the reciprocal
282         Assertions.assertEquals(Double.POSITIVE_INFINITY, 1.0 / sum);
283         // A divide works so the sampler should work
284         Assertions.assertEquals(4.0 / 11, w4 / sum);
285         Assertions.assertEquals(2.0 / 11, w2 / sum);
286         Assertions.assertEquals(1.0 / 11, w1 / sum);
287         builder.add(new RangeSampler(0, 40, rng), w4);
288         builder.add(new RangeSampler(40, 80, rng), w4);
289         builder.add(new RangeSampler(80, 100, rng), w2);
290         builder.add(new RangeSampler(100, 110, rng), w1);
291         assertObjectSamplerSamples(builder.build(rng), 0, 110);
292     }
293 
294     /**
295      * Add samplers to the builder that sample from contiguous ranges between the
296      * minimum and maximum. Note: {@code max - min >= n}
297      *
298      * @param builder the builder
299      * @param n the number of samplers (must be {@code >= 2})
300      * @param min the minimum (inclusive)
301      * @param max the maximum (exclusive)
302      * @param rng the source of randomness
303      */
304     private static void addObjectSamplers(Builder<? super SharedStateObjectSampler<Integer>> builder, int n, int min,
305             int max, UniformRandomProvider rng) {
306         // Create the ranges using n-1 random ticks in the range (min, max),
307         // adding the limits and then sorting in ascending order.
308         // The samplers are then constructed:
309         //
310         // min-------A-----B----max
311         // Sampler 1 = [min, A)
312         // Sampler 2 = [A, B)
313         // Sampler 3 = [B, max)
314 
315         // Use a combination sampler to ensure the ticks are unique in the range.
316         // This will throw if the range is negative.
317         final int range = max - min - 1;
318         int[] ticks = new CombinationSampler(rng, range, n - 1).sample();
319         // Shift the ticks into the range
320         for (int i = 0; i < ticks.length; i++) {
321             ticks[i] += min + 1;
322         }
323         // Add the min and max
324         ticks = Arrays.copyOf(ticks, n + 1);
325         ticks[n - 1] = min;
326         ticks[n] = max;
327         Arrays.sort(ticks);
328 
329         // Sample within the ranges between the ticks
330         final int before = builder.size();
331         for (int i = 1; i < ticks.length; i++) {
332             final RangeSampler sampler = new RangeSampler(ticks[i - 1], ticks[i], rng);
333             // Weight using the range
334             builder.add(sampler, sampler.range);
335         }
336 
337         Assertions.assertEquals(n, builder.size() - before, "Failed to add the correct number of samplers");
338     }
339 
340     /**
341      * Assert sampling is uniform between the minimum and maximum.
342      *
343      * @param sampler the sampler
344      * @param min the minimum (inclusive)
345      * @param max the maximum (exclusive)
346      */
347     private static void assertObjectSamplerSamples(ObjectSampler<Integer> sampler, int min, int max) {
348         final int n = 100000;
349         final long[] observed = new long[max - min];
350         for (int i = 0; i < n; i++) {
351             observed[sampler.sample() - min]++;
352         }
353 
354         final double[] expected = new double[observed.length];
355         Arrays.fill(expected, (double) n / expected.length);
356         final double p = new ChiSquareTest().chiSquareTest(expected, observed);
357         Assertions.assertFalse(p < 0.001, () -> "p-value too small: " + p);
358     }
359 
360     /**
361      * Test sampling is uniform across several DiscreteSampler samplers.
362      */
363     @Test
364     void testDiscreteSamplerSamples() {
365         final Builder<DiscreteSampler> builder = CompositeSamplers.newDiscreteSamplerBuilder();
366         final UniformRandomProvider rng = RandomAssert.createRNG();
367         final int n = 15;
368         final int min = -134;
369         final int max = 2097;
370         addDiscreteSamplers(builder, n, min, max, rng);
371         assertDiscreteSamplerSamples(builder.build(rng), min, max);
372     }
373 
374     /**
375      * Test sampling is uniform across several SharedStateDiscreteSampler samplers.
376      */
377     @Test
378     void testSharedStateDiscreteSamplerSamples() {
379         final Builder<SharedStateDiscreteSampler> builder = CompositeSamplers.newSharedStateDiscreteSamplerBuilder();
380         final UniformRandomProvider rng = RandomAssert.createRNG();
381         final int n = 11;
382         final int min = 42;
383         final int max = 678;
384         addDiscreteSamplers(builder, n, min, max, rng);
385         assertDiscreteSamplerSamples(builder.build(rng), min, max);
386     }
387 
388     /**
389      * Add samplers to the builder that sample from contiguous ranges between the
390      * minimum and maximum. Note: {@code max - min >= n}
391      *
392      * @param builder the builder
393      * @param n the number of samplers (must be {@code >= 2})
394      * @param min the minimum (inclusive)
395      * @param max the maximum (exclusive)
396      * @param rng the source of randomness
397      */
398     private static void addDiscreteSamplers(Builder<? super SharedStateDiscreteSampler> builder, int n, int min,
399             int max, UniformRandomProvider rng) {
400         // Create the ranges using n-1 random ticks in the range (min, max),
401         // adding the limits and then sorting in ascending order.
402         // The samplers are then constructed:
403         //
404         // min-------A-----B----max
405         // Sampler 1 = [min, A)
406         // Sampler 2 = [A, B)
407         // Sampler 3 = [B, max)
408 
409         // Use a combination sampler to ensure the ticks are unique in the range.
410         // This will throw if the range is negative.
411         final int range = max - min - 1;
412         int[] ticks = new CombinationSampler(rng, range, n - 1).sample();
413         // Shift the ticks into the range
414         for (int i = 0; i < ticks.length; i++) {
415             ticks[i] += min + 1;
416         }
417         // Add the min and max
418         ticks = Arrays.copyOf(ticks, n + 1);
419         ticks[n - 1] = min;
420         ticks[n] = max;
421         Arrays.sort(ticks);
422 
423         // Sample within the ranges between the ticks
424         final int before = builder.size();
425         for (int i = 1; i < ticks.length; i++) {
426             final IntRangeSampler sampler = new IntRangeSampler(rng, ticks[i - 1], ticks[i]);
427             // Weight using the range
428             builder.add(sampler, sampler.range);
429         }
430 
431         Assertions.assertEquals(n, builder.size() - before, "Failed to add the correct number of samplers");
432     }
433 
434     /**
435      * Assert sampling is uniform between the minimum and maximum.
436      *
437      * @param sampler the sampler
438      * @param min the minimum (inclusive)
439      * @param max the maximum (exclusive)
440      */
441     private static void assertDiscreteSamplerSamples(DiscreteSampler sampler, int min, int max) {
442         final int n = 100000;
443         final long[] observed = new long[max - min];
444         for (int i = 0; i < n; i++) {
445             observed[sampler.sample() - min]++;
446         }
447 
448         final double[] expected = new double[observed.length];
449         Arrays.fill(expected, (double) n / expected.length);
450         final double p = new ChiSquareTest().chiSquareTest(expected, observed);
451         Assertions.assertFalse(p < 0.001, () -> "p-value too small: " + p);
452     }
453 
454     /**
455      * Test sampling is uniform across several ContinuousSampler samplers.
456      */
457     @Test
458     void testContinuousSamplerSamples() {
459         final Builder<ContinuousSampler> builder = CompositeSamplers.newContinuousSamplerBuilder();
460         final UniformRandomProvider rng = RandomAssert.createRNG();
461         final int n = 15;
462         final double min = 67.2;
463         final double max = 2033.8;
464         addContinuousSamplers(builder, n, min, max, rng);
465         assertContinuousSamplerSamples(builder.build(rng), min, max);
466     }
467 
468     /**
469      * Test sampling is uniform across several SharedStateContinuousSampler samplers.
470      */
471     @Test
472     void testSharedStateContinuousSamplerSamples() {
473         final Builder<SharedStateContinuousSampler> builder = CompositeSamplers
474                 .newSharedStateContinuousSamplerBuilder();
475         final UniformRandomProvider rng = RandomAssert.createRNG();
476         final int n = 11;
477         final double min = -15.7;
478         final double max = 123.4;
479         addContinuousSamplers(builder, n, min, max, rng);
480         assertContinuousSamplerSamples(builder.build(rng), min, max);
481     }
482 
483     /**
484      * Add samplers to the builder that sample from contiguous ranges between the
485      * minimum and maximum. Note: {@code max - min >= n}
486      *
487      * @param builder the builder
488      * @param n the number of samplers (must be {@code >= 2})
489      * @param min the minimum (inclusive)
490      * @param max the maximum (exclusive)
491      * @param rng the source of randomness
492      */
493     private static void addContinuousSamplers(Builder<? super SharedStateContinuousSampler> builder, int n, double min,
494             double max, UniformRandomProvider rng) {
495         // Create the ranges using n-1 random ticks in the range (min, max),
496         // adding the limits and then sorting in ascending order.
497         // The samplers are then constructed:
498         //
499         // min-------A-----B----max
500         // Sampler 1 = [min, A)
501         // Sampler 2 = [A, B)
502         // Sampler 3 = [B, max)
503 
504         // For double values it is extremely unlikely the same value will be generated.
505         // An assertion is performed to ensure we create the correct number of samplers.
506         DoubleRangeSampler sampler = new DoubleRangeSampler(rng, min, max);
507         final double[] ticks = new double[n + 1];
508         ticks[0] = min;
509         ticks[1] = max;
510         // Shift the ticks into the range
511         for (int i = 2; i < ticks.length; i++) {
512             ticks[i] = sampler.sample();
513         }
514         Arrays.sort(ticks);
515 
516         // Sample within the ranges between the ticks
517         final int before = builder.size();
518         for (int i = 1; i < ticks.length; i++) {
519             sampler = new DoubleRangeSampler(rng, ticks[i - 1], ticks[i]);
520             // Weight using the range
521             builder.add(sampler, sampler.range());
522         }
523 
524         Assertions.assertEquals(n, builder.size() - before, "Failed to add the correct number of samplers");
525     }
526 
527     /**
528      * Assert sampling is uniform between the minimum and maximum.
529      *
530      * @param sampler the sampler
531      * @param min the minimum (inclusive)
532      * @param max the maximum (exclusive)
533      */
534     private static void assertContinuousSamplerSamples(ContinuousSampler sampler, double min, double max) {
535         final int n = 100000;
536         final int bins = 200;
537         final long[] observed = new long[bins];
538         final double scale = bins / (max - min);
539         for (int i = 0; i < n; i++) {
540             // scale the sample into a bin within the range:
541             // bin = bins * (x - min) / (max - min)
542             observed[(int) (scale * (sampler.sample() - min))]++;
543         }
544 
545         final double[] expected = new double[observed.length];
546         Arrays.fill(expected, (double) n / expected.length);
547         final double p = new ChiSquareTest().chiSquareTest(expected, observed);
548         Assertions.assertFalse(p < 0.001, () -> "p-value too small: " + p);
549     }
550 
551     /**
552      * Test sampling is uniform across several LongSampler samplers.
553      */
554     @Test
555     void testLongSamplerSamples() {
556         final Builder<LongSampler> builder = CompositeSamplers.newLongSamplerBuilder();
557         final UniformRandomProvider rng = RandomAssert.createRNG();
558         final int n = 15;
559         final long min = -134;
560         final long max = 1L << 54;
561         addLongSamplers(builder, n, min, max, rng);
562         assertLongSamplerSamples(builder.build(rng), min, max);
563     }
564 
565     /**
566      * Test sampling is uniform across several SharedStateLongSampler samplers.
567      */
568     @Test
569     void testSharedStateLongSamplerSamples() {
570         final Builder<SharedStateLongSampler> builder = CompositeSamplers.newSharedStateLongSamplerBuilder();
571         final UniformRandomProvider rng = RandomAssert.createRNG();
572         final int n = 11;
573         final long min = 42;
574         final long max = 1L << 53;
575         addLongSamplers(builder, n, min, max, rng);
576         assertLongSamplerSamples(builder.build(rng), min, max);
577     }
578 
579     /**
580      * Add samplers to the builder that sample from contiguous ranges between the
581      * minimum and maximum. Note: {@code max - min >= n}
582      *
583      * @param builder the builder
584      * @param n the number of samplers (must be {@code >= 2})
585      * @param min the minimum (inclusive)
586      * @param max the maximum (exclusive)
587      * @param rng the source of randomness
588      */
589     private static void addLongSamplers(Builder<? super SharedStateLongSampler> builder, int n, long min,
590             long max, UniformRandomProvider rng) {
591         // Create the ranges using n-1 random ticks in the range (min, max),
592         // adding the limits and then sorting in ascending order.
593         // The samplers are then constructed:
594         //
595         // min-------A-----B----max
596         // Sampler 1 = [min, A)
597         // Sampler 2 = [A, B)
598         // Sampler 3 = [B, max)
599 
600         // For long values it is extremely unlikely the same value will be generated.
601         // An assertion is performed to ensure we create the correct number of samplers.
602         LongRangeSampler sampler = new LongRangeSampler(rng, min, max);
603         final long[] ticks = new long[n + 1];
604         ticks[0] = min;
605         ticks[1] = max;
606         // Shift the ticks into the range
607         for (int i = 2; i < ticks.length; i++) {
608             ticks[i] = sampler.sample();
609         }
610         Arrays.sort(ticks);
611 
612 
613         // Sample within the ranges between the ticks
614         final int before = builder.size();
615         for (int i = 1; i < ticks.length; i++) {
616             sampler = new LongRangeSampler(rng, ticks[i - 1], ticks[i]);
617             // Weight using the range
618             builder.add(sampler, sampler.range);
619         }
620 
621         Assertions.assertEquals(n, builder.size() - before, "Failed to add the correct number of samplers");
622     }
623 
624     /**
625      * Assert sampling is uniform between the minimum and maximum.
626      *
627      * @param sampler the sampler
628      * @param min the minimum (inclusive)
629      * @param max the maximum (exclusive)
630      */
631     private static void assertLongSamplerSamples(LongSampler sampler, long min, long max) {
632         final int n = 100000;
633         final int bins = 200;
634         final long[] observed = new long[bins];
635         final long range = max - min;
636         for (int i = 0; i < n; i++) {
637             // scale the sample into a bin within the range:
638             observed[(int) (bins * (sampler.sample() - min) / range)]++;
639         }
640 
641         final double[] expected = new double[observed.length];
642         Arrays.fill(expected, (double) n / expected.length);
643         final double p = new ChiSquareTest().chiSquareTest(expected, observed);
644         Assertions.assertFalse(p < 0.001, () -> "p-value too small: " + p);
645     }
646 
647     /**
648      * Test the SharedStateSampler implementation for the composite
649      * SharedStateObjectSampler.
650      */
651     @Test
652     void testSharedStateObjectSampler() {
653         testSharedStateObjectSampler(false);
654     }
655 
656     /**
657      * Test the SharedStateSampler implementation for the composite
658      * SharedStateObjectSampler with a factory that does not support a shared state sampler.
659      */
660     @Test
661     void testSharedStateObjectSamplerWithCustomFactory() {
662         testSharedStateObjectSampler(true);
663     }
664 
665     /**
666      * Test the SharedStateSampler implementation for the composite
667      * SharedStateObjectSampler.
668      *
669      * @param customFactory Set to true to use a custom discrete sampler factory that does not
670      * support a shared stated sampler.
671      */
672     private static void testSharedStateObjectSampler(boolean customFactory) {
673         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
674         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
675 
676         final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
677                 .newSharedStateObjectSamplerBuilder();
678 
679         if (customFactory) {
680             addFactoryWithNoSharedStateSupport(builder);
681         }
682 
683         // Sample within the ranges between the ticks
684         final int[] ticks = {6, 13, 42, 99};
685         for (int i = 1; i < ticks.length; i++) {
686             final RangeSampler sampler = new RangeSampler(ticks[i - 1], ticks[i], rng1);
687             // Weight using the range
688             builder.add(sampler, sampler.range);
689         }
690 
691         final SharedStateObjectSampler<Integer> sampler1 = builder.build(rng1);
692         final SharedStateObjectSampler<Integer> sampler2 = sampler1.withUniformRandomProvider(rng2);
693         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
694     }
695 
696     /**
697      * Test the SharedStateSampler implementation for the composite
698      * SharedStateDiscreteSampler.
699      */
700     @Test
701     void testSharedStateDiscreteSampler() {
702         testSharedStateDiscreteSampler(false);
703     }
704 
705     /**
706      * Test the SharedStateSampler implementation for the composite
707      * SharedStateDiscreteSampler with a factory that does not support a shared state sampler.
708      */
709     @Test
710     void testSharedStateDiscreteSamplerWithCustomFactory() {
711         testSharedStateDiscreteSampler(true);
712     }
713 
714     /**
715      * Test the SharedStateSampler implementation for the composite
716      * SharedStateDiscreteSampler.
717      *
718      * @param customFactory Set to true to use a custom discrete sampler factory that does not
719      * support a shared stated sampler.
720      */
721     private static void testSharedStateDiscreteSampler(boolean customFactory) {
722         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
723         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
724 
725         final Builder<SharedStateDiscreteSampler> builder = CompositeSamplers.newSharedStateDiscreteSamplerBuilder();
726 
727         if (customFactory) {
728             addFactoryWithNoSharedStateSupport(builder);
729         }
730 
731         // Sample within the ranges between the ticks
732         final int[] ticks = {-3, 5, 14, 22};
733         for (int i = 1; i < ticks.length; i++) {
734             final IntRangeSampler sampler = new IntRangeSampler(rng1, ticks[i - 1], ticks[i]);
735             // Weight using the range
736             builder.add(sampler, sampler.range);
737         }
738 
739         final SharedStateDiscreteSampler sampler1 = builder.build(rng1);
740         final SharedStateDiscreteSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
741         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
742     }
743 
744     /**
745      * Test the SharedStateSampler implementation for the composite
746      * SharedStateContinuousSampler.
747      */
748     @Test
749     void testSharedStateContinuousSampler() {
750         testSharedStateContinuousSampler(false);
751     }
752 
753     /**
754      * Test the SharedStateSampler implementation for the composite
755      * SharedStateContinuousSampler with a factory that does not support a shared state sampler.
756      */
757     @Test
758     void testSharedStateContinuousSamplerWithCustomFactory() {
759         testSharedStateContinuousSampler(true);
760     }
761 
762     /**
763      * Test the SharedStateSampler implementation for the composite
764      * SharedStateContinuousSampler.
765      *
766      * @param customFactory Set to true to use a custom discrete sampler factory that does not
767      * support a shared stated sampler.
768      */
769     private static void testSharedStateContinuousSampler(boolean customFactory) {
770         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
771         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
772 
773         final Builder<SharedStateContinuousSampler> builder = CompositeSamplers
774                 .newSharedStateContinuousSamplerBuilder();
775 
776         if (customFactory) {
777             addFactoryWithNoSharedStateSupport(builder);
778         }
779 
780         // Sample within the ranges between the ticks
781         final double[] ticks = {7.89, 13.99, 21.7, 35.6, 45.5};
782         for (int i = 1; i < ticks.length; i++) {
783             final DoubleRangeSampler sampler = new DoubleRangeSampler(rng1, ticks[i - 1], ticks[i]);
784             // Weight using the range
785             builder.add(sampler, sampler.range());
786         }
787 
788         final SharedStateContinuousSampler sampler1 = builder.build(rng1);
789         final SharedStateContinuousSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
790         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
791     }
792 
793     /**
794      * Adds a DiscreteSamplerFactory to the builder that creates samplers that do not share state.
795      *
796      * @param builder the builder
797      */
798     private static void addFactoryWithNoSharedStateSupport(Builder<?> builder) {
799         builder.setFactory(new DiscreteProbabilitySamplerFactory() {
800             @Override
801             public DiscreteSampler create(UniformRandomProvider rng, double[] probabilities) {
802                 // Wrap so it is not a SharedStateSamplerInstance.
803                 final DiscreteSampler sampler = GuideTableDiscreteSampler.of(rng, probabilities, 2);
804                 // Destroy the probabilities to check that custom factories are not trusted.
805                 Arrays.fill(probabilities, Double.NaN);
806                 return new DiscreteSampler() {
807                     @Override
808                     public int sample() {
809                         return sampler.sample();
810                     }
811                 };
812             }
813         });
814     }
815 
816     /**
817      * Test the SharedStateSampler implementation for the composite
818      * SharedStateLongSampler.
819      */
820     @Test
821     void testSharedStateLongSampler() {
822         testSharedStateLongSampler(false);
823     }
824 
825     /**
826      * Test the SharedStateSampler implementation for the composite
827      * SharedStateLongSampler with a factory that does not support a shared state sampler.
828      */
829     @Test
830     void testSharedStateLongSamplerWithCustomFactory() {
831         testSharedStateLongSampler(true);
832     }
833 
834     /**
835      * Test the SharedStateSampler implementation for the composite
836      * SharedStateLongSampler.
837      *
838      * @param customFactory Set to true to use a custom discrete sampler factory that does not
839      * support a shared stated sampler.
840      */
841     private static void testSharedStateLongSampler(boolean customFactory) {
842         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
843         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
844 
845         final Builder<SharedStateLongSampler> builder = CompositeSamplers.newSharedStateLongSamplerBuilder();
846 
847         if (customFactory) {
848             addFactoryWithNoSharedStateSupport(builder);
849         }
850 
851         // Sample within the ranges between the ticks
852         final long[] ticks = {-32634628368L, 516234712, 1472839427384234L, 72364572187368423L};
853         for (int i = 1; i < ticks.length; i++) {
854             final LongRangeSampler sampler = new LongRangeSampler(rng1, ticks[i - 1], ticks[i]);
855             // Weight using the range
856             builder.add(sampler, sampler.range);
857         }
858 
859         final SharedStateLongSampler sampler1 = builder.build(rng1);
860         final SharedStateLongSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
861         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
862     }
863 
864     /**
865      * Sample an object {@code Integer} from a range.
866      */
867     private static class RangeSampler implements SharedStateObjectSampler<Integer> {
868         private final int min;
869         private final int range;
870         private final UniformRandomProvider rng;
871 
872         /**
873          * @param min the minimum (inclusive)
874          * @param max the maximum (exclusive)
875          * @param rng the source of randomness
876          */
877         RangeSampler(int min, int max, UniformRandomProvider rng) {
878             this.min = min;
879             this.range = max - min;
880             this.rng = rng;
881         }
882 
883         @Override
884         public Integer sample() {
885             return min + rng.nextInt(range);
886         }
887 
888         @Override
889         public SharedStateObjectSampler<Integer> withUniformRandomProvider(UniformRandomProvider generator) {
890             return new RangeSampler(min, min + range, generator);
891         }
892     }
893 
894     /**
895      * Sample a primitive {@code integer} from a range.
896      */
897     private static class IntRangeSampler implements SharedStateDiscreteSampler {
898         private final int min;
899         private final int range;
900         private final UniformRandomProvider rng;
901 
902         /**
903          * @param rng the source of randomness
904          * @param min the minimum (inclusive)
905          * @param max the maximum (exclusive)
906          */
907         IntRangeSampler(UniformRandomProvider rng, int min, int max) {
908             this.min = min;
909             this.range = max - min;
910             this.rng = rng;
911         }
912 
913         @Override
914         public int sample() {
915             return min + rng.nextInt(range);
916         }
917 
918         @Override
919         public SharedStateDiscreteSampler withUniformRandomProvider(UniformRandomProvider generator) {
920             return new IntRangeSampler(generator, min, min + range);
921         }
922     }
923 
924     /**
925      * Sample a primitive {@code double} from a range between a and b.
926      */
927     private static class DoubleRangeSampler implements SharedStateContinuousSampler {
928         private final double a;
929         private final double b;
930         private final UniformRandomProvider rng;
931 
932         /**
933          * @param rng the source of randomness
934          * @param a bound a
935          * @param b bound b
936          */
937         DoubleRangeSampler(UniformRandomProvider rng, double a, double b) {
938             this.a = a;
939             this.b = b;
940             this.rng = rng;
941         }
942 
943         /**
944          * Get the range from a to b.
945          *
946          * @return the range
947          */
948         double range() {
949             return Math.abs(b - a);
950         }
951 
952         @Override
953         public double sample() {
954             // a + u * (b - a) == u * b + (1 - u) * a
955             final double u = rng.nextDouble();
956             return u * b + (1 - u) * a;
957         }
958 
959         @Override
960         public SharedStateContinuousSampler withUniformRandomProvider(UniformRandomProvider generator) {
961             return new DoubleRangeSampler(generator, a, b);
962         }
963     }
964 
965     /**
966      * Sample a primitive {@code long} from a range.
967      */
968     private static class LongRangeSampler implements SharedStateLongSampler {
969         private final long min;
970         private final long range;
971         private final UniformRandomProvider rng;
972 
973         /**
974          * @param rng the source of randomness
975          * @param min the minimum (inclusive)
976          * @param max the maximum (exclusive)
977          */
978         LongRangeSampler(UniformRandomProvider rng, long min, long max) {
979             this.min = min;
980             this.range = max - min;
981             this.rng = rng;
982         }
983 
984         @Override
985         public long sample() {
986             return min + rng.nextLong(range);
987         }
988 
989         @Override
990         public SharedStateLongSampler withUniformRandomProvider(UniformRandomProvider generator) {
991             return new LongRangeSampler(generator, min, min + range);
992         }
993     }
994 }