blob: 85511736d81bb70c5105098211f048d759162422 [file] [log] [blame]
/*
* 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.shape;
import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.sampling.SharedStateSampler;
import org.apache.commons.rng.sampling.distribution.NormalizedGaussianSampler;
import org.apache.commons.rng.sampling.distribution.ZigguratNormalizedGaussianSampler;
/**
* Generate coordinates <a href="http://mathworld.wolfram.com/BallPointPicking.html">
* uniformly distributed within the unit n-ball</a>.
*
* <p>Sampling uses:</p>
*
* <ul>
* <li>{@link UniformRandomProvider#nextLong()}
* <li>{@link UniformRandomProvider#nextDouble()} (only for dimensions above 2)
* </ul>
*
* @since 1.4
*/
public abstract class UnitBallSampler implements SharedStateSampler<UnitBallSampler> {
/** The dimension for 1D sampling. */
private static final int ONE_D = 1;
/** The dimension for 2D sampling. */
private static final int TWO_D = 2;
/** The dimension for 3D sampling. */
private static final int THREE_D = 3;
/** The mask to extract the lower 53-bits from a long. */
private static final long LOWER_53_BITS = -1L >>> 11;
/**
* Sample uniformly from a 1D unit line.
*/
private static class UnitBallSampler1D extends UnitBallSampler {
/** The source of randomness. */
private final UniformRandomProvider rng;
/**
* @param rng Source of randomness.
*/
UnitBallSampler1D(UniformRandomProvider rng) {
this.rng = rng;
}
@Override
public double[] sample() {
return new double[] {makeSignedDouble(rng.nextLong())};
}
@Override
public UnitBallSampler withUniformRandomProvider(UniformRandomProvider rng) {
return new UnitBallSampler1D(rng);
}
}
/**
* Sample uniformly from a 2D unit disk.
*/
private static class UnitBallSampler2D extends UnitBallSampler {
/** The source of randomness. */
private final UniformRandomProvider rng;
/**
* @param rng Source of randomness.
*/
UnitBallSampler2D(UniformRandomProvider rng) {
this.rng = rng;
}
@Override
public double[] sample() {
// Generate via rejection method of a circle inside a square of edge length 2.
// This should compute approximately 2^2 / pi = 1.27 square positions per sample.
double x;
double y;
do {
x = makeSignedDouble(rng.nextLong());
y = makeSignedDouble(rng.nextLong());
} while (x * x + y * y > 1.0);
return new double[] {x, y};
}
@Override
public UnitBallSampler withUniformRandomProvider(UniformRandomProvider rng) {
return new UnitBallSampler2D(rng);
}
}
/**
* Sample uniformly from a 3D unit ball. This is an non-array based specialisation of
* {@link UnitBallSamplerND} for performance.
*/
private static class UnitBallSampler3D extends UnitBallSampler {
/** The normal distribution. */
private final NormalizedGaussianSampler normal;
/**
* @param rng Source of randomness.
*/
UnitBallSampler3D(UniformRandomProvider rng) {
normal = new ZigguratNormalizedGaussianSampler(rng);
}
@Override
public double[] sample() {
// Discard 2 samples from the coordinate but include in the sum
final double x0 = normal.sample();
final double x1 = normal.sample();
final double x = normal.sample();
final double y = normal.sample();
final double z = normal.sample();
final double sum = x0 * x0 + x1 * x1 + x * x + y * y + z * z;
// Note: Handle the possibility of a zero sum and invalid inverse
if (sum == 0) {
return sample();
}
final double f = 1.0 / Math.sqrt(sum);
return new double[] {x * f, y * f, z * f};
}
@Override
public UnitBallSampler withUniformRandomProvider(UniformRandomProvider rng) {
return new UnitBallSampler3D(rng);
}
}
/**
* Sample uniformly from a unit n-ball.
* Take a random point on the (n+1)-dimensional hypersphere and drop two coordinates.
* Remember that the (n+1)-hypersphere is the unit sphere of R^(n+2), i.e. the surface
* of the (n+2)-dimensional ball.
* @see <a href="https://mathoverflow.net/questions/309567/sampling-a-uniformly-distributed-point-inside-a-hypersphere">
* Sampling a uniformly distributed point INSIDE a hypersphere?</a>
*/
private static class UnitBallSamplerND extends UnitBallSampler {
/** The dimension. */
private final int dimension;
/** The normal distribution. */
private final NormalizedGaussianSampler normal;
/**
* @param dimension Space dimension.
* @param rng Source of randomness.
*/
UnitBallSamplerND(int dimension, UniformRandomProvider rng) {
this.dimension = dimension;
normal = new ZigguratNormalizedGaussianSampler(rng);
}
@Override
public double[] sample() {
final double[] sample = new double[dimension];
// Discard 2 samples from the coordinate but include in the sum
final double x0 = normal.sample();
final double x1 = normal.sample();
double sum = x0 * x0 + x1 * x1;
for (int i = 0; i < dimension; i++) {
final double x = normal.sample();
sum += x * x;
sample[i] = x;
}
// Note: Handle the possibility of a zero sum and invalid inverse
if (sum == 0) {
return sample();
}
final double f = 1.0 / Math.sqrt(sum);
for (int i = 0; i < dimension; i++) {
sample[i] *= f;
}
return sample;
}
@Override
public UnitBallSampler withUniformRandomProvider(UniformRandomProvider rng) {
return new UnitBallSamplerND(dimension, rng);
}
}
/**
* @return a random Cartesian coordinate within the unit n-ball.
*/
public abstract double[] sample();
/**
* Create a unit n-ball sampler for the given dimension.
* Sampled points are uniformly distributed within the unit n-ball.
*
* <p>Sampling is supported in dimensions of 1 or above.
*
* @param dimension Space dimension.
* @param rng Source of randomness.
* @return the sampler
* @throws IllegalArgumentException If {@code dimension <= 0}
*/
public static UnitBallSampler of(int dimension,
UniformRandomProvider rng) {
if (dimension <= 0) {
throw new IllegalArgumentException("Dimension must be strictly positive");
} else if (dimension == ONE_D) {
return new UnitBallSampler1D(rng);
} else if (dimension == TWO_D) {
return new UnitBallSampler2D(rng);
} else if (dimension == THREE_D) {
return new UnitBallSampler3D(rng);
}
return new UnitBallSamplerND(dimension, rng);
}
/**
* Creates a signed double in the range {@code [-1, 1)}. The magnitude is sampled evenly
* from the 2<sup>54</sup> dyadic rationals in the range.
*
* <p>Note: This method will not return samples for both -0.0 and 0.0.
*
* @param bits the bits
* @return the double
*/
private static double makeSignedDouble(long bits) {
// Use the upper 54 bits on the assumption they are more random.
// The sign bit generates a value of 0 or 1 for subtraction.
// The next 53 bits generates a positive number in the range [0, 1).
// [0, 1) - (0 or 1) => [-1, 1)
return (((bits >>> 10) & LOWER_53_BITS) * 0x1.0p-53d) - (bits >>> 63);
}
}