GeometricSampler.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;
/**
* Sampling from a <a href="https://en.wikipedia.org/wiki/Geometric_distribution">geometric
* distribution</a>.
*
* <p>This distribution samples the number of failures before the first success taking values in the
* set {@code [0, 1, 2, ...]}.</p>
*
* <p>The sample is computed using a related exponential distribution. If \( X \) is an
* exponentially distributed random variable with parameter \( \lambda \), then
* \( Y = \left \lfloor X \right \rfloor \) is a geometrically distributed random variable with
* parameter \( p = 1 − e^\lambda \), with \( p \) the probability of success.</p>
*
* <p>This sampler outperforms using the {@link InverseTransformDiscreteSampler} with an appropriate
* geometric inverse cumulative probability function.</p>
*
* <p>Usage note: As the probability of success (\( p \)) tends towards zero the mean of the
* distribution (\( \frac{1-p}{p} \)) tends towards infinity and due to the use of {@code int}
* for the sample this can result in truncation of the distribution.</p>
*
* <p>Sampling uses {@link UniformRandomProvider#nextDouble()}.</p>
*
* @see <a
* href="https://en.wikipedia.org/wiki/Geometric_distribution#Related_distributions">Geometric
* distribution - related distributions</a>
* @since 1.3
*/
public final class GeometricSampler {
/**
* Sample from the geometric distribution when the probability of success is 1.
*/
private static final class GeometricP1Sampler
implements SharedStateDiscreteSampler {
/** The single instance. */
static final GeometricP1Sampler INSTANCE = new GeometricP1Sampler();
@Override
public int sample() {
// When probability of success is 1 the sample is always zero
return 0;
}
@Override
public String toString() {
return "Geometric(p=1) deviate";
}
@Override
public SharedStateDiscreteSampler withUniformRandomProvider(UniformRandomProvider rng) {
// No requirement for a new instance
return this;
}
}
/**
* Sample from the geometric distribution by using a related exponential distribution.
*/
private static final class GeometricExponentialSampler
implements SharedStateDiscreteSampler {
/** Underlying source of randomness. Used only for the {@link #toString()} method. */
private final UniformRandomProvider rng;
/** The related exponential sampler for the geometric distribution. */
private final SharedStateContinuousSampler exponentialSampler;
/**
* @param rng Generator of uniformly distributed random numbers
* @param probabilityOfSuccess The probability of success (must be in the range
* {@code [0 < probabilityOfSuccess < 1]})
*/
GeometricExponentialSampler(UniformRandomProvider rng, double probabilityOfSuccess) {
this.rng = rng;
// Use a related exponential distribution:
// λ = −ln(1 − probabilityOfSuccess)
// exponential mean = 1 / λ
// --
// Note on validation:
// If probabilityOfSuccess == Math.nextDown(1.0) the exponential mean is >0 (valid).
// If probabilityOfSuccess == Double.MIN_VALUE the exponential mean is +Infinity
// and the sample will always be Integer.MAX_VALUE (the distribution is truncated). It
// is noted in the class Javadoc that the use of a small p leads to truncation so
// no checks are made for this case.
final double exponentialMean = 1.0 / (-Math.log1p(-probabilityOfSuccess));
exponentialSampler = ZigguratSampler.Exponential.of(rng, exponentialMean);
}
/**
* @param rng Generator of uniformly distributed random numbers
* @param source Source to copy.
*/
GeometricExponentialSampler(UniformRandomProvider rng, GeometricExponentialSampler source) {
this.rng = rng;
exponentialSampler = source.exponentialSampler.withUniformRandomProvider(rng);
}
@Override
public int sample() {
// Return the floor of the exponential sample
return (int) Math.floor(exponentialSampler.sample());
}
@Override
public String toString() {
return "Geometric deviate [" + rng.toString() + "]";
}
@Override
public SharedStateDiscreteSampler withUniformRandomProvider(UniformRandomProvider rng) {
return new GeometricExponentialSampler(rng, this);
}
}
/** Class contains only static methods. */
private GeometricSampler() {}
/**
* Creates a new geometric distribution sampler. The samples will be provided in the set
* {@code k=[0, 1, 2, ...]} where {@code k} indicates the number of failures before the first
* success.
*
* @param rng Generator of uniformly distributed random numbers.
* @param probabilityOfSuccess The probability of success.
* @return the sampler
* @throws IllegalArgumentException if {@code probabilityOfSuccess} is not in the range
* {@code [0 < probabilityOfSuccess <= 1]})
*/
public static SharedStateDiscreteSampler of(UniformRandomProvider rng,
double probabilityOfSuccess) {
if (probabilityOfSuccess <= 0 || probabilityOfSuccess > 1) {
throw new IllegalArgumentException(
"Probability of success (p) must be in the range [0 < p <= 1]: " +
probabilityOfSuccess);
}
return probabilityOfSuccess == 1 ?
GeometricP1Sampler.INSTANCE :
new GeometricExponentialSampler(rng, probabilityOfSuccess);
}
}