PoissonSamplerCache.java
/*
* 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.distribution;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.sampling.distribution.LargeMeanPoissonSampler.LargeMeanPoissonSamplerState;
/**
* Create a sampler for the
* <a href="http://mathworld.wolfram.com/PoissonDistribution.html">Poisson
* distribution</a> using a cache to minimise construction cost.
*
* <p>The cache will return a sampler equivalent to
* {@link PoissonSampler#PoissonSampler(UniformRandomProvider, double)}.</p>
*
* <p>The cache allows the {@link PoissonSampler} construction cost to be minimised
* for low size Poisson samples. The cache stores state for a range of integers where
* integer value {@code n} can be used to construct a sampler for the range
* {@code n <= mean < n+1}.</p>
*
* <p>The cache is advantageous under the following conditions:</p>
*
* <ul>
* <li>The mean of the Poisson distribution falls within a known range.
* <li>The sample size to be made with the <strong>same</strong> sampler is
* small.
* <li>The Poisson samples have different means with the same integer
* value(s) after rounding down.
* </ul>
*
* <p>If the sample size to be made with the <strong>same</strong> sampler is large
* then the construction cost is low compared to the sampling time and the cache
* has minimal benefit.</p>
*
* <p>Performance improvement is dependent on the speed of the
* {@link UniformRandomProvider}. A fast provider can obtain a two-fold speed
* improvement for a single-use Poisson sampler.</p>
*
* <p>The cache is thread safe. Note that concurrent threads using the cache
* must ensure a thread safe {@link UniformRandomProvider} is used when creating
* samplers, e.g. a unique sampler per thread.</p>
*
* <p>Sampling uses:</p>
*
* <ul>
* <li>{@link UniformRandomProvider#nextDouble()}
* <li>{@link UniformRandomProvider#nextLong()} (large means only)
* </ul>
*
* @since 1.2
*/
public class PoissonSamplerCache {
/**
* The minimum N covered by the cache where
* {@code N = (int)Math.floor(mean)}.
*/
private final int minN;
/**
* The maximum N covered by the cache where
* {@code N = (int)Math.floor(mean)}.
*/
private final int maxN;
/** The cache of states between {@link #minN} and {@link #maxN}. */
private final LargeMeanPoissonSamplerState[] values;
/**
* Create an instance.
*
* @param minMean The minimum mean covered by the cache.
* @param maxMean The maximum mean covered by the cache.
* @throws IllegalArgumentException if {@code maxMean < minMean}
*/
public PoissonSamplerCache(double minMean,
double maxMean) {
this(checkMeanRange(minMean, maxMean), maxMean, false);
}
/**
* @param minMean The minimum mean covered by the cache.
* @param maxMean The maximum mean covered by the cache.
* @param ignored Ignored value.
*/
private PoissonSamplerCache(double minMean,
double maxMean,
boolean ignored) {
// The cache can only be used for the LargeMeanPoissonSampler.
if (maxMean < PoissonSampler.PIVOT) {
// The upper limit is too small so no cache will be used.
// This class will just construct new samplers.
minN = 0;
maxN = 0;
values = null;
} else {
// Convert the mean into integers.
// Note the minimum is clipped to the algorithm switch point.
this.minN = (int) Math.floor(Math.max(minMean, PoissonSampler.PIVOT));
this.maxN = (int) Math.floor(Math.min(maxMean, Integer.MAX_VALUE));
values = new LargeMeanPoissonSamplerState[maxN - minN + 1];
}
}
/**
* @param minN The minimum N covered by the cache where {@code N = (int)Math.floor(mean)}.
* @param maxN The maximum N covered by the cache where {@code N = (int)Math.floor(mean)}.
* @param states The precomputed states.
*/
private PoissonSamplerCache(int minN,
int maxN,
LargeMeanPoissonSamplerState[] states) {
this.minN = minN;
this.maxN = maxN;
// Stored directly as the states were newly created within this class.
this.values = states;
}
/**
* Check the mean range.
*
* <p>This method exists to raise an exception before invocation of the
* private constructor; this mitigates Finalizer attacks
* (see SpotBugs CT_CONSTRUCTOR_THROW).
*
* @param minMean The minimum mean covered by the cache.
* @param maxMean The maximum mean covered by the cache.
* @return the minimum mean
* @throws IllegalArgumentException if {@code maxMean < minMean}
*/
private static double checkMeanRange(double minMean, double maxMean) {
// Note:
// Although a mean of 0 is invalid for a Poisson sampler this case
// is handled to make the cache user friendly. Any low means will
// be handled by the SmallMeanPoissonSampler and not cached.
// For this reason it is also OK if the means are negative.
// Allow minMean == maxMean so that the cache can be used
// to create samplers with distinct RNGs and the same mean.
if (maxMean < minMean) {
throw new IllegalArgumentException(
"Max mean: " + maxMean + " < " + minMean);
}
return minMean;
}
/**
* Creates a new Poisson sampler.
*
* <p>The returned sampler will function exactly the
* same as {@link PoissonSampler#of(UniformRandomProvider, double)}.
*
* @param rng Generator of uniformly distributed random numbers.
* @param mean Mean.
* @return A Poisson sampler
* @throws IllegalArgumentException if {@code mean <= 0} or
* {@code mean >} {@link Integer#MAX_VALUE}.
* @deprecated Use {@link #createSharedStateSampler(UniformRandomProvider, double)}.
*/
@Deprecated
public DiscreteSampler createPoissonSampler(UniformRandomProvider rng,
double mean) {
return createSharedStateSampler(rng, mean);
}
/**
* Creates a new Poisson sampler.
*
* <p>The returned sampler will function exactly the
* same as {@link PoissonSampler#of(UniformRandomProvider, double)}.
*
* @param rng Generator of uniformly distributed random numbers.
* @param mean Mean.
* @return A Poisson sampler
* @throws IllegalArgumentException if {@code mean <= 0} or
* {@code mean >} {@link Integer#MAX_VALUE}.
* @since 1.4
*/
public SharedStateDiscreteSampler createSharedStateSampler(UniformRandomProvider rng,
double mean) {
// Ensure the same functionality as the PoissonSampler by
// using a SmallMeanPoissonSampler under the switch point.
if (mean < PoissonSampler.PIVOT) {
return SmallMeanPoissonSampler.of(rng, mean);
}
if (mean > maxN) {
// Outside the range of the cache.
// This avoids extra parameter checks and handles the case when
// the cache is empty or if Math.floor(mean) is not an integer.
return LargeMeanPoissonSampler.of(rng, mean);
}
// Convert the mean into an integer.
final int n = (int) Math.floor(mean);
if (n < minN) {
// Outside the lower range of the cache.
return LargeMeanPoissonSampler.of(rng, mean);
}
// Look in the cache for a state that can be reused.
// Note: The cache is offset by minN.
final int index = n - minN;
final LargeMeanPoissonSamplerState state = values[index];
if (state == null) {
// Create a sampler and store the state for reuse.
// Do not worry about thread contention
// as the state is effectively immutable.
// If recomputed and replaced it will the same.
final LargeMeanPoissonSampler sampler = new LargeMeanPoissonSampler(rng, mean);
values[index] = sampler.getState();
return sampler;
}
// Compute the remaining fraction of the mean
final double lambdaFractional = mean - n;
return new LargeMeanPoissonSampler(rng, state, lambdaFractional);
}
/**
* Check if the mean is within the range where the cache can minimise the
* construction cost of the {@link PoissonSampler}.
*
* @param mean
* the mean
* @return true, if within the cache range
*/
public boolean withinRange(double mean) {
if (mean < PoissonSampler.PIVOT) {
// Construction is optimal
return true;
}
// Convert the mean into an integer.
final int n = (int) Math.floor(mean);
return n <= maxN && n >= minN;
}
/**
* Checks if the cache covers a valid range of mean values.
*
* <p>Note that the cache is only valid for one of the Poisson sampling
* algorithms. In the instance that a range was requested that was too
* low then there is nothing to cache and this functions returns
* {@code false}.
*
* <p>The cache can still be used to create a {@link PoissonSampler} using
* {@link #createSharedStateSampler(UniformRandomProvider, double)}.
*
* <p>This method can be used to determine if the cache has a potential
* performance benefit.
*
* @return true, if the cache covers a range of mean values
*/
public boolean isValidRange() {
return values != null;
}
/**
* Gets the minimum mean covered by the cache.
*
* <p>This value is the inclusive lower bound and is equal to
* the lowest integer-valued mean that is covered by the cache.
*
* <p>Note that this value may not match the value passed to the constructor
* due to the following reasons:
*
* <ul>
* <li>At small mean values a different algorithm is used for Poisson
* sampling and the cache is unnecessary.
* <li>The minimum is always an integer so may be below the constructor
* minimum mean.
* </ul>
*
* <p>If {@link #isValidRange()} returns {@code true} the cache will store
* state to reduce construction cost of samplers in
* the range {@link #getMinMean()} inclusive to {@link #getMaxMean()}
* inclusive. Otherwise this method returns 0;
*
* @return The minimum mean covered by the cache.
*/
public double getMinMean() {
return minN;
}
/**
* Gets the maximum mean covered by the cache.
*
* <p>This value is the inclusive upper bound and is equal to
* the double value below the first integer-valued mean that is
* above range covered by the cache.
*
* <p>Note that this value may not match the value passed to the constructor
* due to the following reasons:
* <ul>
* <li>At small mean values a different algorithm is used for Poisson
* sampling and the cache is unnecessary.
* <li>The maximum is always the double value below an integer so
* may be above the constructor maximum mean.
* </ul>
*
* <p>If {@link #isValidRange()} returns {@code true} the cache will store
* state to reduce construction cost of samplers in
* the range {@link #getMinMean()} inclusive to {@link #getMaxMean()}
* inclusive. Otherwise this method returns 0;
*
* @return The maximum mean covered by the cache.
*/
public double getMaxMean() {
if (isValidRange()) {
return Math.nextDown(maxN + 1.0);
}
return 0;
}
/**
* Gets the minimum mean value that can be cached.
*
* <p>Any {@link PoissonSampler} created with a mean below this level will not
* have any state that can be cached.
*
* @return the minimum cached mean
*/
public static double getMinimumCachedMean() {
return PoissonSampler.PIVOT;
}
/**
* Create a new {@link PoissonSamplerCache} with the given range
* reusing the current cache values.
*
* <p>This will create a new object even if the range is smaller or the
* same as the current cache.
*
* @param minMean The minimum mean covered by the cache.
* @param maxMean The maximum mean covered by the cache.
* @throws IllegalArgumentException if {@code maxMean < minMean}
* @return the poisson sampler cache
*/
public PoissonSamplerCache withRange(double minMean,
double maxMean) {
if (values == null) {
// Nothing to reuse
return new PoissonSamplerCache(minMean, maxMean);
}
checkMeanRange(minMean, maxMean);
// The cache can only be used for the LargeMeanPoissonSampler.
if (maxMean < PoissonSampler.PIVOT) {
return new PoissonSamplerCache(0, 0);
}
// Convert the mean into integers.
// Note the minimum is clipped to the algorithm switch point.
final int withMinN = (int) Math.floor(Math.max(minMean, PoissonSampler.PIVOT));
final int withMaxN = (int) Math.floor(maxMean);
final LargeMeanPoissonSamplerState[] states =
new LargeMeanPoissonSamplerState[withMaxN - withMinN + 1];
// Preserve values from the current array to the next
final int currentIndex;
final int nextIndex;
if (this.minN <= withMinN) {
// The current array starts before the new array
currentIndex = withMinN - this.minN;
nextIndex = 0;
} else {
// The new array starts before the current array
currentIndex = 0;
nextIndex = this.minN - withMinN;
}
final int length = Math.min(values.length - currentIndex, states.length - nextIndex);
if (length > 0) {
System.arraycopy(values, currentIndex, states, nextIndex, length);
}
return new PoissonSamplerCache(withMinN, withMaxN, states);
}
}