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.distribution;
18  
19  import org.apache.commons.rng.RestorableUniformRandomProvider;
20  import org.apache.commons.rng.UniformRandomProvider;
21  import org.apache.commons.rng.sampling.RandomAssert;
22  import org.apache.commons.rng.simple.RandomSource;
23  import org.junit.jupiter.api.Assertions;
24  import org.junit.jupiter.api.Test;
25  
26  /**
27   * This test checks the {@link PoissonSamplerCache} functions exactly like the
28   * constructor of the {@link PoissonSampler}, irrespective of the range
29   * covered by the cache.
30   */
31  class PoissonSamplerCacheTest {
32  
33      // Set a range so that the SmallMeanPoissonSampler is also required.
34  
35      /** The minimum of the range of the mean */
36      private final int minRange = (int) Math.floor(PoissonSampler.PIVOT - 2);
37      /** The maximum of the range of the mean */
38      private final int maxRange = (int) Math.floor(PoissonSampler.PIVOT + 6);
39      /** The mid-point of the range of the mean */
40      private final int midRange = (minRange + maxRange) / 2;
41  
42      /**
43       * Test the cache reports the minimum mean that uses an algorithm that supports caching.
44       * This mean is the same level as the algorithm switch point in the PoissonSampler.
45       */
46      @Test
47      void testMinimumCachedMean() {
48          Assertions.assertEquals(PoissonSampler.PIVOT, PoissonSamplerCache.getMinimumCachedMean());
49      }
50  
51      // Edge cases for construction
52  
53      /**
54       * Test the cache can be created without a range that requires a cache.
55       * In this case the cache will be a pass through to the constructor
56       * of the SmallMeanPoissonSampler.
57       */
58      @Test
59      void testConstructorWithNoCache() {
60          final double min = 0;
61          final double max = PoissonSampler.PIVOT - 2;
62          final PoissonSamplerCache cache = createPoissonSamplerCache(min, max);
63          Assertions.assertFalse(cache.isValidRange());
64          Assertions.assertEquals(0, cache.getMinMean());
65          Assertions.assertEquals(0, cache.getMaxMean());
66      }
67  
68      /**
69       * Test the cache can be created with a range of 1.
70       * In this case the cache will be valid for all mean values
71       * in the range {@code n <= mean < n+1}.
72       */
73      @Test
74      void testConstructorWhenMaxEqualsMin() {
75          final double min = PoissonSampler.PIVOT + 2;
76          final double max = min;
77          final PoissonSamplerCache cache = createPoissonSamplerCache(min, max);
78          Assertions.assertTrue(cache.isValidRange());
79          Assertions.assertEquals(min, cache.getMinMean());
80          Assertions.assertEquals(Math.nextDown(Math.floor(max) + 1),
81                                  cache.getMaxMean());
82      }
83  
84      /**
85       * Test the cache can be created with a range of 1.
86       * In this case the cache will be valid for all mean values
87       * in the range {@code n <= mean < n+1}.
88       */
89      @Test
90      void testConstructorWhenMaxAboveMin() {
91          final double min = PoissonSampler.PIVOT + 2;
92          final double max = min + 10;
93          final PoissonSamplerCache cache = createPoissonSamplerCache(min, max);
94          Assertions.assertTrue(cache.isValidRange());
95          Assertions.assertEquals(min, cache.getMinMean());
96          Assertions.assertEquals(Math.nextDown(Math.floor(max) + 1),
97                                  cache.getMaxMean());
98      }
99  
100     /**
101      * Test the cache requires a range with {@code max >= min}.
102      */
103     @Test
104     void testConstructorThrowsWhenMaxIsLessThanMin() {
105         final double min = PoissonSampler.PIVOT;
106         final double max = Math.nextDown(min);
107         Assertions.assertThrows(IllegalArgumentException.class,
108             () -> createPoissonSamplerCache(min, max));
109     }
110 
111     /**
112      * Test the cache can be created with a min range below 0.
113      * In this case the range is truncated to 0.
114      */
115     @Test
116     void testConstructorWhenMinBelow0() {
117         final double min = -1;
118         final double max = PoissonSampler.PIVOT + 2;
119         final PoissonSamplerCache cache = createPoissonSamplerCache(min, max);
120         Assertions.assertTrue(cache.isValidRange());
121         Assertions.assertEquals(PoissonSampler.PIVOT, cache.getMinMean());
122         Assertions.assertEquals(Math.nextDown(Math.floor(max) + 1),
123                                 cache.getMaxMean());
124     }
125 
126     /**
127      * Test the cache can be created with a max range below 0.
128      * In this case the range is truncated to 0, i.e. no cache.
129      */
130     @Test
131     void testConstructorWhenMaxBelow0() {
132         final double min = -10;
133         final double max = -1;
134         final PoissonSamplerCache cache = createPoissonSamplerCache(min, max);
135         Assertions.assertFalse(cache.isValidRange());
136         Assertions.assertEquals(0, cache.getMinMean());
137         Assertions.assertEquals(0, cache.getMaxMean());
138     }
139 
140     /**
141      * Test the cache can be created without a range that requires a cache.
142      * In this case the cache will be a pass through to the constructor
143      * of the SmallMeanPoissonSampler.
144      */
145     @Test
146     void testWithRangeConstructorWithNoCache() {
147         final double min = 0;
148         final double max = PoissonSampler.PIVOT - 2;
149         final PoissonSamplerCache cache = createPoissonSamplerCache().withRange(min, max);
150         Assertions.assertFalse(cache.isValidRange());
151         Assertions.assertEquals(0, cache.getMinMean());
152         Assertions.assertEquals(0, cache.getMaxMean());
153     }
154 
155     /**
156      * Test the cache can be created with a range of 1.
157      * In this case the cache will be valid for all mean values
158      * in the range {@code n <= mean < n+1}.
159      */
160     @Test
161     void testWithRangeConstructorWhenMaxEqualsMin() {
162         final double min = PoissonSampler.PIVOT + 2;
163         final double max = min;
164         final PoissonSamplerCache cache = createPoissonSamplerCache().withRange(min, max);
165         Assertions.assertTrue(cache.isValidRange());
166         Assertions.assertEquals(min, cache.getMinMean());
167         Assertions.assertEquals(Math.nextDown(Math.floor(max) + 1),
168                                 cache.getMaxMean());
169     }
170 
171     /**
172      * Test the cache can be created with a range of 1.
173      * In this case the cache will be valid for all mean values
174      * in the range {@code n <= mean < n+1}.
175      */
176     @Test
177     void testWithRangeConstructorWhenMaxAboveMin() {
178         final double min = PoissonSampler.PIVOT + 2;
179         final double max = min + 10;
180         final PoissonSamplerCache cache = createPoissonSamplerCache().withRange(min, max);
181         Assertions.assertTrue(cache.isValidRange());
182         Assertions.assertEquals(min, cache.getMinMean());
183         Assertions.assertEquals(Math.nextDown(Math.floor(max) + 1),
184                                 cache.getMaxMean());
185     }
186 
187     /**
188      * Test the cache requires a range with {@code max >= min}.
189      */
190     @Test
191     void testWithRangeConstructorThrowsWhenMaxIsLessThanMin() {
192         final double min = PoissonSampler.PIVOT;
193         final double max = Math.nextDown(min);
194         final PoissonSamplerCache cache = createPoissonSamplerCache();
195         Assertions.assertThrows(IllegalArgumentException.class,
196             () -> cache.withRange(min, max));
197     }
198 
199     /**
200      * Test the cache can be created with a min range below 0.
201      * In this case the range is truncated to 0.
202      */
203     @Test
204     void testWithRangeConstructorWhenMinBelow0() {
205         final double min = -1;
206         final double max = PoissonSampler.PIVOT + 2;
207         final PoissonSamplerCache cache = createPoissonSamplerCache().withRange(min, max);
208         Assertions.assertTrue(cache.isValidRange());
209         Assertions.assertEquals(PoissonSampler.PIVOT, cache.getMinMean());
210         Assertions.assertEquals(Math.nextDown(Math.floor(max) + 1),
211                                 cache.getMaxMean());
212     }
213 
214     /**
215      * Test the cache can be created from a cache with no capacity.
216      */
217     @Test
218     void testWithRangeConstructorWhenCacheHasNoCapcity() {
219         final double min = PoissonSampler.PIVOT + 2;
220         final double max = min + 10;
221         final PoissonSamplerCache cache = createPoissonSamplerCache(0, 0).withRange(min, max);
222         Assertions.assertTrue(cache.isValidRange());
223         Assertions.assertEquals(min, cache.getMinMean());
224         Assertions.assertEquals(Math.nextDown(Math.floor(max) + 1),
225                                 cache.getMaxMean());
226     }
227 
228     /**
229      * Test the withinRange function of the cache signals when construction
230      * cost is minimal.
231      */
232     @Test
233     void testWithinRange() {
234         final double min = PoissonSampler.PIVOT + 10;
235         final double max = PoissonSampler.PIVOT + 20;
236         final PoissonSamplerCache cache = createPoissonSamplerCache(min, max);
237         // Under the pivot point is always within range
238         Assertions.assertTrue(cache.withinRange(PoissonSampler.PIVOT - 1));
239         Assertions.assertFalse(cache.withinRange(min - 1));
240         Assertions.assertTrue(cache.withinRange(min));
241         Assertions.assertTrue(cache.withinRange(max));
242         Assertions.assertFalse(cache.withinRange(max + 10));
243     }
244 
245     // Edge cases for creating a Poisson sampler
246 
247     /**
248      * Test createSharedStateSampler() with a bad mean.
249      *
250      * <p>Note this test actually tests the SmallMeanPoissonSampler throws.
251      */
252     @Test
253     void testCreateSharedStateSamplerThrowsWithZeroMean() {
254         final UniformRandomProvider rng = RandomAssert.seededRNG();
255         final PoissonSamplerCache cache = createPoissonSamplerCache();
256         Assertions.assertThrows(IllegalArgumentException.class,
257             () -> cache.createSharedStateSampler(rng, 0));
258     }
259 
260     /**
261      * Test createSharedStateSampler() with a mean that is too large.
262      */
263     @Test
264     void testCreateSharedStateSamplerThrowsWithNonIntegerMean() {
265         final UniformRandomProvider rng = RandomAssert.seededRNG();
266         final PoissonSamplerCache cache = createPoissonSamplerCache();
267         final double mean = Integer.MAX_VALUE + 1.0;
268         Assertions.assertThrows(IllegalArgumentException.class,
269             () -> cache.createSharedStateSampler(rng, mean));
270     }
271 
272     // Sampling tests
273 
274     /**
275      * Test the cache returns the same samples as the PoissonSampler when it
276      * covers the entire range.
277      */
278     @Test
279     void testCanComputeSameSamplesAsPoissonSamplerWithFullRangeCache() {
280         checkComputeSameSamplesAsPoissonSampler(minRange,
281                                                 maxRange);
282     }
283 
284     /**
285      * Test the cache returns the same samples as the PoissonSampler
286      * with no cache.
287      */
288     @Test
289     void testCanComputeSameSamplesAsPoissonSamplerWithNoCache() {
290         checkComputeSameSamplesAsPoissonSampler(0,
291                                                 minRange - 2);
292     }
293 
294     /**
295      * Test the cache returns the same samples as the PoissonSampler with
296      * partial cache covering the lower range.
297      */
298     @Test
299     void testCanComputeSameSamplesAsPoissonSamplerWithPartialCacheCoveringLowerRange() {
300         checkComputeSameSamplesAsPoissonSampler(minRange,
301                                                 midRange);
302     }
303 
304     /**
305      * Test the cache returns the same samples as the PoissonSampler with
306      * partial cache covering the upper range.
307      */
308     @Test
309     void testCanComputeSameSamplesAsPoissonSamplerWithPartialCacheCoveringUpperRange() {
310         checkComputeSameSamplesAsPoissonSampler(midRange,
311                                                 maxRange);
312     }
313 
314     /**
315      * Test the cache returns the same samples as the PoissonSampler with
316      * cache above the upper range.
317      */
318     @Test
319     void testCanComputeSameSamplesAsPoissonSamplerWithCacheAboveTheUpperRange() {
320         checkComputeSameSamplesAsPoissonSampler(maxRange + 10,
321                                                 maxRange + 20);
322     }
323 
324     /**
325      * Check poisson samples are the same from the {@link PoissonSampler}
326      * and {@link PoissonSamplerCache}.
327      *
328      * @param minMean the min mean of the cache
329      * @param maxMean the max mean of the cache
330      */
331     private void checkComputeSameSamplesAsPoissonSampler(int minMean,
332                                                          int maxMean) {
333         // Two identical RNGs
334         final RandomSource source = RandomSource.SPLIT_MIX_64;
335         final long seed = RandomSource.createLong();
336         final RestorableUniformRandomProvider rng1 = source.create(seed);
337         final RestorableUniformRandomProvider rng2 = source.create(seed);
338 
339         // Create the cache with the given range
340         final PoissonSamplerCache cache =
341                 createPoissonSamplerCache(minMean, maxMean);
342         // Test all means in the test range (which may be different
343         // from the cache range).
344         for (int i = minRange; i <= maxRange; i++) {
345             // Test integer mean (no SmallMeanPoissonSampler required)
346             testPoissonSamples(rng1, rng2, cache, i);
347             // Test non-integer mean (SmallMeanPoissonSampler required)
348             testPoissonSamples(rng1, rng2, cache, i + 0.5);
349         }
350     }
351 
352     /**
353      * Creates the poisson sampler cache with the given range.
354      *
355      * @param minMean the min mean
356      * @param maxMean the max mean
357      * @return the poisson sampler cache
358      */
359     private static PoissonSamplerCache createPoissonSamplerCache(double minMean,
360                                                           double maxMean) {
361         return new PoissonSamplerCache(minMean, maxMean);
362     }
363 
364     /**
365      * Creates a poisson sampler cache that will have a valid range for the cache.
366      *
367      * @return the poisson sampler cache
368      */
369     private static PoissonSamplerCache createPoissonSamplerCache() {
370         return new PoissonSamplerCache(PoissonSampler.PIVOT,
371                                        PoissonSampler.PIVOT + 10);
372     }
373 
374     /**
375      * Test poisson samples are the same from the {@link PoissonSampler}
376      * and {@link PoissonSamplerCache}. The random providers must be
377      * identical (including state).
378      *
379      * @param rng1  the first random provider
380      * @param rng2  the second random provider
381      * @param cache the cache
382      * @param mean  the mean
383      */
384     private static void testPoissonSamples(
385             final RestorableUniformRandomProvider rng1,
386             final RestorableUniformRandomProvider rng2,
387             PoissonSamplerCache cache,
388             double mean) {
389         final DiscreteSampler s1 = PoissonSampler.of(rng1, mean);
390         final DiscreteSampler s2 = cache.createSharedStateSampler(rng2, mean);
391         for (int j = 0; j < 10; j++) {
392             Assertions.assertEquals(s1.sample(), s2.sample());
393         }
394     }
395 
396     /**
397      * Test the cache returns the same samples as the PoissonSampler with
398      * a new cache reusing the entire range.
399      */
400     @Test
401     void testCanComputeSameSamplesAsPoissonSamplerReusingCacheEntireRange() {
402         checkComputeSameSamplesAsPoissonSamplerReusingCache(midRange,
403                                                             maxRange,
404                                                             midRange,
405                                                             maxRange);
406     }
407 
408     /**
409      * Test the cache returns the same samples as the PoissonSampler with
410      * a new cache reusing none of the range.
411      */
412     @Test
413     void testCanComputeSameSamplesAsPoissonSamplerReusingCacheNoRange() {
414         checkComputeSameSamplesAsPoissonSamplerReusingCache(midRange,
415                                                             maxRange,
416                                                             maxRange + 10,
417                                                             maxRange + 20);
418     }
419 
420     /**
421      * Test the cache returns the same samples as the PoissonSampler with
422      * a new cache reusing some of the lower range.
423      */
424     @Test
425     void testCanComputeSameSamplesAsPoissonSamplerReusingCacheLowerRange() {
426         checkComputeSameSamplesAsPoissonSamplerReusingCache(midRange,
427                                                             maxRange,
428                                                             minRange,
429                                                             midRange + 1);
430     }
431 
432     /**
433      * Test the cache returns the same samples as the PoissonSampler with
434      * a new cache reusing some of the upper range.
435      */
436     @Test
437     void testCanComputeSameSamplesAsPoissonSamplerReusingCacheUpperRange() {
438         checkComputeSameSamplesAsPoissonSamplerReusingCache(midRange,
439                                                             maxRange,
440                                                             maxRange - 1,
441                                                             maxRange + 5);
442     }
443 
444     /**
445      * Check poisson samples are the same from the {@link PoissonSampler}
446      * and a {@link PoissonSamplerCache} created reusing values.
447      *
448      * <p>Note: This cannot check the cache values were reused but ensures
449      * that a new cache created with a range functions correctly.
450      *
451      * @param minMean  the min mean of the cache
452      * @param maxMean  the max mean of the cache
453      * @param minMean2 the min mean of the second cache
454      * @param maxMean2 the max mean of the second cache
455      */
456     private void checkComputeSameSamplesAsPoissonSamplerReusingCache(int minMean,
457                                                                      int maxMean,
458                                                                      int minMean2,
459                                                                      int maxMean2) {
460         // Two identical RNGs
461         final RandomSource source = RandomSource.SPLIT_MIX_64;
462         final long seed = RandomSource.createLong();
463         final RestorableUniformRandomProvider rng1 = source.create(seed);
464         final RestorableUniformRandomProvider rng2 = source.create(seed);
465 
466         // Create the cache with the given range and fill it
467         final PoissonSamplerCache cache =
468                 createPoissonSamplerCache(minMean, maxMean);
469         // Test all means in the test range (which may be different
470         // from the cache range).
471         for (int i = minMean; i <= maxMean; i++) {
472             cache.createSharedStateSampler(rng1, i);
473         }
474 
475         final PoissonSamplerCache cache2 = cache.withRange(minMean2, maxMean2);
476         Assertions.assertNotSame(cache, cache2, "WithRange cache is the same object");
477 
478         // Test all means in the test range (which may be different
479         // from the cache range).
480         for (int i = minRange; i <= maxRange; i++) {
481             // Test integer mean (no SmallMeanPoissonSampler required)
482             testPoissonSamples(rng1, rng2, cache2, i);
483             // Test non-integer mean (SmallMeanPoissonSampler required)
484             testPoissonSamples(rng1, rng2, cache2, i + 0.5);
485         }
486     }
487 
488     /**
489      * Explicit test for the deprecated method createPoissonSampler().
490      * All other uses of this method have been removed.
491      */
492     @SuppressWarnings("deprecation")
493     @Test
494     void testCreatePoissonSampler() {
495         final PoissonSamplerCache cache = createPoissonSamplerCache(0, 100);
496         final DiscreteSampler s2 = cache.createPoissonSampler(null, 42);
497         Assertions.assertTrue(s2 instanceof LargeMeanPoissonSampler);
498     }
499 }