This is an automated email from the ASF dual-hosted git repository.

aherbert pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-rng.git


The following commit(s) were added to refs/heads/master by this push:
     new dd8ca0e5 RNG-195: Extend support for alpha parameters in 
DirichletSampler
dd8ca0e5 is described below

commit dd8ca0e56574111d4fc069b5a59a4607cd8acb44
Author: Alex Herbert <[email protected]>
AuthorDate: Sat Oct 10 19:04:12 2026 +0100

    RNG-195: Extend support for alpha parameters in DirichletSampler
    
    Changes have been made to output log samples from the gamma sampler. The
    log samples can be scaled by a constant before normalising by their sum.
    
    This supports Dirichlet samples where the gamma samples are all zero or
    overflow due to tiny (<1e-19) or large (>1e33) concentration parameters.
---
 .../AhrensDieterMarsagliaTsangGammaSampler.java    | 105 ++++-
 .../sampling/distribution/DirichletSampler.java    | 250 ++++++++++--
 ...AhrensDieterMarsagliaTsangGammaSamplerTest.java |  81 +++-
 .../distribution/DirichletSamplerTest.java         | 433 ++++++++++++++++++++-
 src/changes/changes.xml                            |   4 +
 5 files changed, 811 insertions(+), 62 deletions(-)

diff --git 
a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/AhrensDieterMarsagliaTsangGammaSampler.java
 
b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/AhrensDieterMarsagliaTsangGammaSampler.java
index 42687f3b..e05c96cd 100644
--- 
a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/AhrensDieterMarsagliaTsangGammaSampler.java
+++ 
b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/AhrensDieterMarsagliaTsangGammaSampler.java
@@ -16,6 +16,8 @@
  */
 package org.apache.commons.rng.sampling.distribution;
 
+import java.util.function.DoubleBinaryOperator;
+import java.util.function.DoubleUnaryOperator;
 import org.apache.commons.rng.UniformRandomProvider;
 
 /**
@@ -138,6 +140,10 @@ public class AhrensDieterMarsagliaTsangGammaSampler
         private final double oneOverAlpha;
         /** Optimization (see code). */
         private final double bGSOptim;
+        /** Output mapping function for step 2. Accepts u and x. */
+        private final DoubleBinaryOperator out2;
+        /** Output mapping function for step 3. Accepts x. */
+        private final DoubleUnaryOperator out3;
 
         /**
          * Constructs a new instance.
@@ -145,15 +151,32 @@ public class AhrensDieterMarsagliaTsangGammaSampler
          * @param rng Generator of uniformly distributed random numbers.
          * @param alpha Alpha parameter of the distribution.
          * @param theta Theta parameter of the distribution.
+         * @param logSamples Return the natural logarithm of the sample.
          * @throws IllegalArgumentException if {@code alpha <= 0} or {@code 
theta <= 0},
          * or if {@code alpha} or {@code theta} are not finite
          */
         AhrensDieterGammaSampler(UniformRandomProvider rng,
                                  double alpha,
-                                 double theta) {
+                                 double theta,
+                                 boolean logSamples) {
             super(rng, alpha, theta);
             oneOverAlpha = 1 / alpha;
             bGSOptim = 1 + alpha / Math.E;
+            if (logSamples) {
+                final double logTheta = Math.log(theta);
+                // RNG-195: Support log gamma samples for the DirichletSampler
+                // x      = ((1 + alpha / Math.E) * u)^1/alpha
+                // log(x) = log((1 + alpha / Math.E) * u) * 1/alpha
+                //        = (log(1 + alpha / Math.E) + log(u)) * 1/alpha
+                // alpha < 2^-106 the log1p term (~2^-107) cannot be added to 
log(u).
+                final double logBSGOptim = Math.log1p(alpha / Math.E);
+                out2 = (u, x) -> logTheta + (logBSGOptim + Math.log(u)) * 
oneOverAlpha;
+                out3 = x -> logTheta + Math.log(x);
+            } else {
+                // RNG-195: Support for the existing gamma sampler behaviour 
is unchanged
+                out2 = (u, x) -> theta * x;
+                out3 = x -> theta * x;
+            }
         }
 
         /**
@@ -167,6 +190,8 @@ public class AhrensDieterMarsagliaTsangGammaSampler
             super(rng, source);
             oneOverAlpha = source.oneOverAlpha;
             bGSOptim = source.bGSOptim;
+            out2 = source.out2;
+            out3 = source.out3;
         }
 
         @Override
@@ -180,6 +205,11 @@ public class AhrensDieterMarsagliaTsangGammaSampler
 
                 if (p <= 1) {
                     // Step 2:
+                    // Always taken when alpha < 2^-52 since 1 + alpha / E = 1.
+                    // RNG-195: Support for the existing gamma sampler 
behaviour is unchanged.
+                    // This does not use exp(log(p) * 1/alpha) or log1p 
variants to recycle
+                    // the log term. Output can differ by a large relative 
error.
+                    // In the main use case for real samples using pow is 
faster than exp+log.
                     final double x = Math.pow(p, oneOverAlpha);
                     final double u2 = rng.nextDouble();
 
@@ -187,15 +217,24 @@ public class AhrensDieterMarsagliaTsangGammaSampler
                         // Reject.
                         continue;
                     }
-                    return theta * x;
+                    return out2.applyAsDouble(u, x);
                 }
 
                 // Step 3:
+                // -log(((1+alpha/E) - p) / alpha)
+                // -log((1+alpha/E) - p) + log(alpha)
+                // At limit alpha ~ 1
+                // max(1 + alpha / E) = 1.368, p > 1
+                // (1 + alpha / Math.E) - p < 0.368 and -> 0 as alpha reduces.
+                // No use for log1p.
+                // alpha ~ 1 sample range:
+                // -log(2^-52) = 36.04
+                // -log(1 / E) = 1
                 final double x = -Math.log((bGSOptim - p) * oneOverAlpha);
                 final double u2 = rng.nextDouble();
 
                 if (u2 <= Math.pow(x, alpha - 1)) {
-                    return theta * x;
+                    return out3.applyAsDouble(x);
                 }
                 // Reject and continue.
             }
@@ -228,6 +267,10 @@ public class AhrensDieterMarsagliaTsangGammaSampler
         private final double cOptim;
         /** Gaussian sampling. */
         private final NormalizedGaussianSampler gaussian;
+        /** Output mapping function for step 1. Accepts v and Gaussian sample 
x. */
+        private final DoubleBinaryOperator out1;
+        /** Output mapping function for step 2. Accepts v and log(v). */
+        private final DoubleBinaryOperator out2;
 
         /**
          * Constructs a new instance.
@@ -235,16 +278,32 @@ public class AhrensDieterMarsagliaTsangGammaSampler
          * @param rng Generator of uniformly distributed random numbers.
          * @param alpha Alpha parameter of the distribution.
          * @param theta Theta parameter of the distribution.
+         * @param logSamples Return the natural logarithm of the sample.
          * @throws IllegalArgumentException if {@code alpha <= 0} or {@code 
theta <= 0},
          * or if {@code alpha} or {@code theta} are not finite
          */
         MarsagliaTsangGammaSampler(UniformRandomProvider rng,
                                    double alpha,
-                                   double theta) {
+                                   double theta,
+                                   boolean logSamples) {
             super(rng, alpha, theta);
             gaussian = ZigguratSampler.NormalizedGaussian.of(rng);
+            // Note: As alpha -> large:
+            // dOptim -> alpha
+            // cOptim -> 0
+            // samples -> alpha
             dOptim = alpha - ONE_THIRD;
             cOptim = ONE_THIRD / Math.sqrt(dOptim);
+            if (logSamples) {
+                final double c = Math.log(theta) + Math.log(dOptim);
+                out1 = (v, x) -> c + 3 * Math.log1p(cOptim * x);
+                out2 = (v, lv) -> c + lv;
+            } else {
+                // RNG-195: Support for the existing gamma sampler behaviour 
is unchanged
+                final double c = theta * dOptim;
+                out1 = (v, x) -> c * v;
+                out2 = (v, lv) -> c * v;
+            }
         }
 
         /**
@@ -259,6 +318,8 @@ public class AhrensDieterMarsagliaTsangGammaSampler
             gaussian = ZigguratSampler.NormalizedGaussian.of(rng);
             dOptim = source.dOptim;
             cOptim = source.cOptim;
+            out1 = source.out1;
+            out2 = source.out2;
         }
 
         @Override
@@ -277,11 +338,15 @@ public class AhrensDieterMarsagliaTsangGammaSampler
 
                 // Squeeze.
                 if (u < 1 - 0.0331 * x2 * x2) {
-                    return theta * dOptim * v;
+                    return out1.applyAsDouble(v, x);
                 }
 
-                if (Math.log(u) < 0.5 * x2 + dOptim * (1 - v + Math.log(v))) {
-                    return theta * dOptim * v;
+                // Support for large alpha where 1 + cOptim * x is 1.
+                // log(v) = log((1 + cOptim * x)^3)
+                // Ensures log output is consistent on both output pathways.
+                final double lv = 3 * Math.log1p(cOptim * x);
+                if (Math.log(u) < 0.5 * x2 + dOptim * (1 - v + lv)) {
+                    return out2.applyAsDouble(v, lv);
                 }
             }
         }
@@ -346,9 +411,31 @@ public class AhrensDieterMarsagliaTsangGammaSampler
     public static SharedStateContinuousSampler of(UniformRandomProvider rng,
                                                   double alpha,
                                                   double theta) {
+        return of(rng, alpha, theta, false);
+    }
+
+    /**
+     * Creates a new gamma distribution sampler.
+     *
+     * <p><Strong>This method is intentionally not part of the public 
API</strong>.
+     *
+     * @param rng Generator of uniformly distributed random numbers.
+     * @param alpha Alpha parameter of the distribution (this is a shape 
parameter).
+     * @param theta Theta parameter of the distribution (this is a scale 
parameter).
+     * @param logSamples Return samples using the natural logarithm of the 
sample.
+     * @return the sampler
+     * @throws IllegalArgumentException if {@code alpha <= 0} or {@code theta 
<= 0},
+     * or if {@code alpha} or {@code theta} are not finite
+     * @since 1.8
+     */
+    // package-private for use in the DirichletSampler
+    static SharedStateContinuousSampler of(UniformRandomProvider rng,
+                                           double alpha,
+                                           double theta,
+                                           boolean logSamples) {
         // Each sampler should check the input arguments.
         return alpha < 1 ?
-                new AhrensDieterGammaSampler(rng, alpha, theta) :
-                new MarsagliaTsangGammaSampler(rng, alpha, theta);
+                new AhrensDieterGammaSampler(rng, alpha, theta, logSamples) :
+                new MarsagliaTsangGammaSampler(rng, alpha, theta, logSamples);
     }
 }
diff --git 
a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/DirichletSampler.java
 
b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/DirichletSampler.java
index 0776ca93..a3781156 100644
--- 
a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/DirichletSampler.java
+++ 
b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/DirichletSampler.java
@@ -35,10 +35,143 @@ import 
org.apache.commons.rng.sampling.SharedStateObjectSampler;
 public abstract class DirichletSampler implements 
SharedStateObjectSampler<double[]> {
     /** The minimum number of categories. */
     private static final int MIN_CATGEORIES = 2;
+    /** The minimum alpha before using log samples. See RNG-195. */
+    private static final double MIN_ALPHA = 0x1p-5;
+    /** The maximum alpha before using log samples. See RNG-195. */
+    private static final double MAX_ALPHA = 0x1p53;
+    /** The threshold for tiny alpha to use scaling.
+     * A tiny alpha is any value where log1p(alpha/E) + log(u) == log(u) for u 
in (0, 1).
+     * The smallest magnitude for log(u) is 2^-53 so a tiny alpha is 2^-53 * 
2^-53 = 2^-106.
+     * However this must be lowered by the amount of scaling
+     * applied to alpha. Set using a conservatively low threshold so that
+     * TINY_ALPHA * ALPHA_SCALE is below 2^-106. */
+    private static final double TINY_ALPHA = 0x1p-163;
+    /** Scaling to apply to tiny alpha where all log gamma samplers
+     * will return samples as log(u) / alpha with u in [0, 1).
+     * Any scaled alpha must be below 2^-106 after scaling so the log(u) / 
alpha
+     * pathway is always used, and log1p(alpha/E) cannot be added to log(u).
+     * Set so that log(2^-53) / (scale * MIN_VALUE) is finite allowing support
+     * for alpha down to the lowest non-zero double value when the uniform 
deviate
+     * is the smallest non-zero value. */
+    private static final double ALPHA_SCALE = 0x1p56;
 
+    /** Normalizer for the gamma samples. */
+    private final Normalizer normalizer;
     /** RNG (used for the toString() method). */
     private final UniformRandomProvider rng;
 
+    /**
+     * Normalize an array of samples to unit length.
+     */
+    // package-private for testing
+    enum Normalizer {
+        /** Normalise real-valued samples.
+         * The sum of the samples is expected to be a finite normal double.
+         * A sub-normal sum cannot be normalised as this uses multiplication 
by the
+         * reciprocal of the sum (which overflows when sum is tiny). */
+        REAL {
+            @Override
+            boolean normalize(double[] x) {
+                // Samples are never length zero
+                double norm = x[0];
+                for (int i = 1; i < x.length; i++) {
+                    norm += x[i];
+                }
+                // Normalize by dividing by the sum of the samples
+                norm = 1.0 / norm;
+                // Detect an invalid normalization,
+                // e.g. cases of all zero samples, or one or more infinite 
samples.
+                if (isNonZeroPositiveFinite(norm)) {
+                    // Normalise
+                    for (int i = 0; i < x.length; i++) {
+                        x[i] *= norm;
+                    }
+                    return true;
+                }
+                return false;
+            }
+        },
+        /** Normalise log-valued samples.
+         * This will normalize any sample even including infinity (or NaN). */
+        LOG {
+            @Override
+            boolean normalize(double[] x) {
+                return normalize(x, 1.0);
+            }
+        },
+        /** Normalise scaled log-valued samples.
+         * This will normalize any sample even including infinity (or NaN). */
+        SCALED_LOG {
+            @Override
+            boolean normalize(double[] x) {
+                return normalize(x, ALPHA_SCALE);
+            }
+        };
+
+        /**
+         * Normalise the sample to unit length using the sum.
+         *
+         * <p>Returns false if the sample could not be normalized. This occurs 
when
+         * all terms underflow and the sum is zero, or one or more terms
+         * overflow and the sum is infinity.
+         *
+         * @param x Sample.
+         * @return true if the sample was normalized
+         */
+        abstract boolean normalize(double[] x);
+
+        /**
+         * Normalise the log sample to unit length using the sum.
+         * Log samples are multiplied by the provided {@code scale}. Used
+         * when log samples originate from a scaled alpha parameter passed
+         * to the gamma sampler.
+         *
+         * @param x Sample.
+         * @param scale Scale.
+         * @return true
+         */
+        static boolean normalize(double[] x, double scale) {
+            // Scale log numbers so we can sum them.
+            // Subtracting the maximum log value stabilizes the sum.
+            // The maximum exp term will be 1, all other terms <=1 and the sum 
is finite.
+            double max = x[0];
+            for (int i = 1; i < x.length; i++) {
+                max = Math.max(max, x[i]);
+            }
+            double norm = 0;
+            if (Double.isFinite(max)) {
+                // A finite max will normalize any sample including -infinity.
+                for (int i = 0; i < x.length; i++) {
+                    // If scale > 1 then log samples are moved further apart.
+                    // It is likely only the max will contribute to the sum
+                    // and other sample values are 0.
+                    x[i] = Math.exp((x[i] - max) * scale);
+                    norm += x[i];
+                }
+            } else {
+                // Any value matching the max is normalised to length 1.
+                // A side effect is that any NaNs will normalize to 1
+                // as Math.max puts NaN above all other numbers.
+                // NaNs or positive infinity are not expected from the log 
gamma samples.
+                final long b = Double.doubleToLongBits(max);
+                for (int i = 0; i < x.length; i++) {
+                    x[i] = Double.doubleToLongBits(x[i]) == b ? 1 : 0;
+                    norm += x[i];
+                }
+            }
+            // Note: In contrast to the real case where samples
+            // are expected across all categories, log samples
+            // for tiny alpha are typically biased to 1 category.
+            // Using divide ensures extreme samples for tiny
+            // alpha have 1.0 in the chosen category and not 1-2^53
+            // which can happen when multiplying by the reciprocal.
+            for (int i = 0; i < x.length; i++) {
+                x[i] /= norm;
+            }
+            return true;
+        }
+    }
+
     /**
      * Sample from a Dirichlet distribution with different concentration 
parameters
      * for each category.
@@ -52,10 +185,12 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
          *
          * @param rng Generator of uniformly distributed random numbers.
          * @param samplers Samplers for each category.
+         * @param normalizer Normalizer for the samples.
          */
         GeneralDirichletSampler(UniformRandomProvider rng,
-                                SharedStateContinuousSampler[] samplers) {
-            super(rng);
+                                SharedStateContinuousSampler[] samplers,
+                                Normalizer normalizer) {
+            super(rng, normalizer);
             // Array is stored directly as it is generated within the 
DirichletSampler class
             this.samplers = samplers;
         }
@@ -76,7 +211,7 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
             for (int i = 0; i < newSamplers.length; i++) {
                 newSamplers[i] = samplers[i].withUniformRandomProvider(rng);
             }
-            return new GeneralDirichletSampler(rng, newSamplers);
+            return new GeneralDirichletSampler(rng, newSamplers, 
getNormalizer());
         }
     }
 
@@ -96,11 +231,13 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
          * @param rng Generator of uniformly distributed random numbers.
          * @param k Number of categories.
          * @param sampler Sampler for the categories.
+         * @param normalizer Normalizer for the samples.
          */
         SymmetricDirichletSampler(UniformRandomProvider rng,
                                   int k,
-                                  SharedStateContinuousSampler sampler) {
-            super(rng);
+                                  SharedStateContinuousSampler sampler,
+                                  Normalizer normalizer) {
+            super(rng, normalizer);
             this.k = k;
             this.sampler = sampler;
         }
@@ -117,7 +254,7 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
 
         @Override
         public SymmetricDirichletSampler 
withUniformRandomProvider(UniformRandomProvider rng) {
-            return new SymmetricDirichletSampler(rng, k, 
sampler.withUniformRandomProvider(rng));
+            return new SymmetricDirichletSampler(rng, k, 
sampler.withUniformRandomProvider(rng), getNormalizer());
         }
     }
 
@@ -125,9 +262,20 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
      * Constructs a new instance.
      *
      * @param rng Generator of uniformly distributed random numbers.
+     * @param normalizer Normalizer for the samples.
      */
-    DirichletSampler(UniformRandomProvider rng) {
+    DirichletSampler(UniformRandomProvider rng, Normalizer normalizer) {
         this.rng = rng;
+        this.normalizer = normalizer;
+    }
+
+    /**
+     * Gets the normalizer.
+     *
+     * @return the normalizer
+     */
+    Normalizer getNormalizer() {
+        return normalizer;
     }
 
     /** {@inheritDoc} */
@@ -141,25 +289,15 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
     public double[] sample() {
         // Create Gamma(alpha_i, 1) deviates for all alpha
         final double[] y = new double[getK()];
-        double norm = 0;
         for (int i = 0; i < y.length; i++) {
-            final double yi = nextGamma(i);
-            norm += yi;
-            y[i] = yi;
+            y[i] = nextGamma(i);
         }
-        // Normalize by dividing by the sum of the samples
-        norm = 1.0 / norm;
-        // Detect an invalid normalization, e.g. cases of all zero samples
-        if (!isNonZeroPositiveFinite(norm)) {
+        if (!normalizer.normalize(y)) {
             // Sample again using recursion.
             // A stack overflow due to a broken RNG will eventually occur
             // rather than the alternative which is an infinite loop.
             return sample();
         }
-        // Normalise
-        for (int i = 0; i < y.length; i++) {
-            y[i] *= norm;
-        }
         return y;
     }
 
@@ -195,11 +333,22 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
     public static DirichletSampler of(UniformRandomProvider rng,
                                       double... alpha) {
         validateNumberOfCategories(alpha.length);
+        // Check for the maximum alpha.
+        // The mean of each gamma sample is the concentration parameter alpha.
+        // All samples must be normalised to unit length using the sum.
+        // Use log samples when samples will likely over/underflow on the 
largest
+        // sample (create a zero or infinite sample). These can be scaled by 
the
+        // largest sample before summing.
+        double maxAlpha = alpha[0];
+        for (int i = 1; i < alpha.length; i++) {
+            maxAlpha = Math.max(maxAlpha, alpha[i]);
+        }
+        final boolean logSamples = requireLogSamples(maxAlpha);
         final SharedStateContinuousSampler[] samplers = new 
SharedStateContinuousSampler[alpha.length];
         for (int i = 0; i < samplers.length; i++) {
-            samplers[i] = createSampler(rng, alpha[i]);
+            samplers[i] = createSampler(rng, alpha[i], logSamples, maxAlpha);
         }
-        return new GeneralDirichletSampler(rng, samplers);
+        return new GeneralDirichletSampler(rng, samplers, 
createNormalizer(logSamples, maxAlpha));
     }
 
     /**
@@ -217,8 +366,9 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
                                              int k,
                                              double alpha) {
         validateNumberOfCategories(k);
-        final SharedStateContinuousSampler sampler = createSampler(rng, alpha);
-        return new SymmetricDirichletSampler(rng, k, sampler);
+        final boolean logSamples = requireLogSamples(alpha);
+        final SharedStateContinuousSampler sampler = createSampler(rng, alpha, 
logSamples, alpha);
+        return new SymmetricDirichletSampler(rng, k, sampler, 
createNormalizer(logSamples, alpha));
     }
 
     /**
@@ -239,19 +389,41 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
      *
      * @param rng Generator of uniformly distributed random numbers.
      * @param alpha Concentration parameter.
+     * @param logSamples Return samples using the natural logarithm of the 
sample.
+     * @param maxAlpha Maximum alpha.
      * @return the sampler
      * @throws IllegalArgumentException if the concentration parameter is not 
strictly positive.
      */
     private static SharedStateContinuousSampler 
createSampler(UniformRandomProvider rng,
-                                                              double alpha) {
+                                                              double alpha,
+                                                              boolean 
logSamples,
+                                                              double maxAlpha) 
{
         InternalUtils.requireStrictlyPositiveFinite(alpha, "alpha 
concentration");
         // Create a Gamma(shape=alpha, scale=1) sampler.
-        if (alpha == 1) {
+        if (alpha == 1 && !logSamples) {
             // Special case
             // Gamma(shape=1, scale=1) == Exponential(mean=1)
             return ZigguratSampler.Exponential.of(rng);
         }
-        return AhrensDieterMarsagliaTsangGammaSampler.of(rng, alpha, 1);
+
+        // -----
+        // Alpha scaling
+        //
+        // When alpha is very small 1/alpha will overflow.
+        // This invalidates the gamma sampler which creates samples using:
+        //   u in [0, 1) : p = u * (1 + alpha / E) ~ u
+        //   x = p^(1/alpha)
+        // For any p < 1 the result is zero. Log samples use:
+        //   log(p) / alpha
+        // This is stable until alpha approaches MIN_NORMAL where it can 
overflow.
+        // Since this sampler normalizes log samples using scaling so they can 
be
+        // summed the solution to tiny alpha is to perform some scaling on 
alpha
+        // when all samplers use the same gamma sampler method. Log samples
+        // must be rescaled before normalisation:
+        // log(p) / alpha = scale * log(p) / (scale * alpha)
+        // -----
+        final double a = maxAlpha < TINY_ALPHA ? alpha * ALPHA_SCALE : alpha;
+        return AhrensDieterMarsagliaTsangGammaSampler.of(rng, a, 1, 
logSamples);
     }
 
     /**
@@ -263,4 +435,30 @@ public abstract class DirichletSampler implements 
SharedStateObjectSampler<doubl
     private static boolean isNonZeroPositiveFinite(double x) {
         return x > 0 && x < Double.POSITIVE_INFINITY;
     }
+
+    /**
+     * Check if the gamma sampler should use log samples. This extends support 
for
+     * small and large concentration parameters where the samples cannot be 
normalised
+     * to unit length without underflow or overflow.
+     *
+     * @param alpha Maximum alpha parameter.
+     * @return true if log samples are required
+     */
+    private static boolean requireLogSamples(double alpha) {
+        return alpha < MIN_ALPHA || alpha > MAX_ALPHA;
+    }
+
+    /**
+     * Creates the normalizer for the samples.
+     *
+     * @param logSamples Return samples using the natural logarithm of the 
sample.
+     * @param alpha Maximum alpha parameter.
+     * @return the normalizer
+     */
+    private static Normalizer createNormalizer(boolean logSamples, double 
alpha) {
+        if (logSamples) {
+            return alpha < TINY_ALPHA ? Normalizer.SCALED_LOG : Normalizer.LOG;
+        }
+        return Normalizer.REAL;
+    }
 }
diff --git 
a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/AhrensDieterMarsagliaTsangGammaSamplerTest.java
 
b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/AhrensDieterMarsagliaTsangGammaSamplerTest.java
index 42e57a28..ed872e3a 100644
--- 
a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/AhrensDieterMarsagliaTsangGammaSamplerTest.java
+++ 
b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/AhrensDieterMarsagliaTsangGammaSamplerTest.java
@@ -50,19 +50,21 @@ class AhrensDieterMarsagliaTsangGammaSamplerTest {
     }
 
     /**
-     * Test the SharedStateSampler implementation.
+     * Test the SharedStateSampler implementation with {@code alpha < 1}.
      */
     @Test
     void testSharedStateSamplerWithAlphaBelowOne() {
-        testSharedStateSampler(0.5, 3.456);
+        testSharedStateSampler(0.5, 3.456, false);
+        testSharedStateSampler(0.5, 3.456, true);
     }
 
     /**
-     * Test the SharedStateSampler implementation.
+     * Test the SharedStateSampler implementation with {@code alpha > 1}.
      */
     @Test
     void testSharedStateSamplerWithAlphaAboveOne() {
-        testSharedStateSampler(3.5, 3.456);
+        testSharedStateSampler(3.5, 3.456, false);
+        testSharedStateSampler(3.5, 3.456, true);
     }
 
     /**
@@ -70,17 +72,82 @@ class AhrensDieterMarsagliaTsangGammaSamplerTest {
      *
      * @param alpha Alpha.
      * @param theta Theta.
+     * @param logSamples Use log samples.
      */
-    private static void testSharedStateSampler(double alpha, double theta) {
+    private static void testSharedStateSampler(double alpha, double theta, 
boolean logSamples) {
         final UniformRandomProvider rng1 = RandomAssert.seededRNG();
         final UniformRandomProvider rng2 = RandomAssert.seededRNG();
-        // Use instance constructor not factory constructor to exercise 1.X 
public API
-        final AhrensDieterMarsagliaTsangGammaSampler sampler1 =
+        final SharedStateContinuousSampler sampler1 = logSamples ?
+            AhrensDieterMarsagliaTsangGammaSampler.of(rng1, alpha, theta, 
true) :
+            // Use instance constructor not factory constructor to exercise 
1.X public API
             new AhrensDieterMarsagliaTsangGammaSampler(rng1, alpha, theta);
         final SharedStateContinuousSampler sampler2 = 
sampler1.withUniformRandomProvider(rng2);
         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
     }
 
+    /**
+     * Test the log samples implementation with {@code alpha < 1}.
+     */
+    @Test
+    void testLogSamplesWithAlphaBelowOne() {
+        // Note: ulp passes with N=1,000,000. N has been lowered for time 
savings.
+        final int n = 1000;
+        testLogSamples(0.5, 3.456, n, 25);
+        // alpha = 2^-52 is where (1 + alpha/E) == 1 (See RNG-195)
+        testLogSamples(0x1p-50, 1.0, n, 50);
+        testLogSamples(0x1p-52, 1.0, n, 50);
+        testLogSamples(0x1p-54, 1.0, n, 50);
+    }
+
+    /**
+     * Test the log samples implementation with {@code alpha > 1}.
+     */
+    @Test
+    void testLogSamplesWithAlphaAboveOne() {
+        // Note: ulp passes with N=1,000,000. N has been lowered for time 
savings.
+        final int n = 1000;
+        testLogSamples(3.5, 3.456, n, 25);
+        // As alpha -> large then samples -> alpha
+        // Occurs consistently > 3e32 (See RNG-195)
+        testLogSamples(1e30, 1.0, n, 50);
+        testLogSamples(1e32, 1.0, n, 50);
+        testLogSamples(1e34, 1.0, n, 50);
+    }
+
+    /**
+     * Test the log samples implementation matches the real samples output.
+     *
+     * @param alpha Alpha.
+     * @param theta Theta.
+     * @param ulps Units-in-the-last place for matching output.
+     */
+    private static void testLogSamples(double alpha, double theta, int n, int 
ulps) {
+        // May spuriously fail. Allow reruns to use a different RNG.
+        final UniformRandomProvider[] rngs = RandomAssert.createRNG(2);
+        final UniformRandomProvider rng1 = rngs[0];
+        final UniformRandomProvider rng2 = rngs[1];
+        final SharedStateContinuousSampler sampler1 =
+            AhrensDieterMarsagliaTsangGammaSampler.of(rng1, alpha, theta, 
false);
+        final SharedStateContinuousSampler sampler2 =
+            AhrensDieterMarsagliaTsangGammaSampler.of(rng2, alpha, theta, 
true);
+        for (int i = 0; i < n; i++) {
+            // The sequence may not be exactly the same.
+            final double x1 = sampler1.sample();
+            final double x2 = sampler2.sample();
+            // Use exp on the log sample.
+            final double s2 = Math.exp(x2);
+            Assertions.assertEquals(x1, s2, ulps * Math.ulp(x1),
+                () -> String.format("exp %s %s ulp=%d", alpha, theta, (long) 
((s2 - x1) / Math.ulp(x1))));
+            // Using log on the real sample has issues mapping
+            // the domains and relative error can be large when x1 -> 1 
(log(x1) -> 0)
+            if (x1 < 0.95 && x1 > 1.05) {
+                final double s1 = Math.log(x1);
+                Assertions.assertEquals(x2, s1, ulps * Math.ulp(x2),
+                    () -> String.format("log %s %s ulp=%d", alpha, theta, 
(long) ((s1 - x2) / Math.ulp(x2))));
+            }
+        }
+    }
+
     /**
      * Test the toString method. This is added to ensure coverage as the 
factory constructor
      * used in other tests does not create an instance of the wrapper class.
diff --git 
a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/DirichletSamplerTest.java
 
b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/DirichletSamplerTest.java
index 1dc6c6c0..222390ea 100644
--- 
a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/DirichletSamplerTest.java
+++ 
b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/DirichletSamplerTest.java
@@ -16,13 +16,19 @@
  */
 package org.apache.commons.rng.sampling.distribution;
 
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.time.Duration;
 import java.util.Arrays;
+import java.util.function.Supplier;
 import org.apache.commons.math3.linear.Array2DRowRealMatrix;
 import org.apache.commons.math3.stat.correlation.Covariance;
 import org.apache.commons.math3.stat.descriptive.moment.Mean;
+import org.apache.commons.math3.stat.inference.ChiSquareTest;
 import org.apache.commons.rng.UniformRandomProvider;
 import org.apache.commons.rng.core.source64.SplitMix64;
 import org.apache.commons.rng.sampling.RandomAssert;
+import 
org.apache.commons.rng.sampling.distribution.DirichletSampler.Normalizer;
 import org.apache.commons.rng.simple.RandomSource;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -31,6 +37,126 @@ import org.junit.jupiter.api.Test;
  * Test for {@link DirichletSampler}.
  */
 class DirichletSamplerTest {
+    /** Threshold for tiny alpha.
+     * Set where log1p(alpha/Math.E) + log(u) == log(u) for u in [0, 1). */
+    private static final double TINY_ALPHA = 0x1p-106;
+
+    @Test
+    void testRealNormalizer() {
+        final Normalizer normalizer = Normalizer.REAL;
+        // Fail on all zeros
+        assertNormalizer(normalizer, new double[] {0, 0, 0}, null);
+        // Fail when sum is infinite
+        assertNormalizer(normalizer, new double[] {Double.POSITIVE_INFINITY, 
1, 1}, null);
+        assertNormalizer(normalizer, new double[] {Double.MAX_VALUE, 
Double.MAX_VALUE}, null);
+        // A sub-normal sum cannot divide using multiplication of the 
reciprocal
+        assertNormalizer(normalizer, new double[] {Double.MIN_VALUE, 
Double.MIN_VALUE}, null);
+        assertNormalizer(normalizer, new double[] {0, Double.MIN_VALUE}, null);
+        assertNormalizer(normalizer, new double[] {0x1p-1025, 0x1p-1025}, 
null);
+        // Normalize to unit length
+        assertNormalizer(normalizer, new double[] {1, 1}, new double[] {0.5, 
0.5});
+        assertNormalizer(normalizer, new double[] {1, 2, 3, 4}, new double[] 
{1.0 / 10, 2.0 / 10, 3.0 / 10, 4.0 / 10});
+        assertNormalizer(normalizer, new double[] {0, Double.MAX_VALUE}, new 
double[] {0, 1});
+        assertNormalizer(normalizer, new double[] {0, Double.MIN_NORMAL}, new 
double[] {0, 1});
+    }
+
+    @Test
+    void testLogNormalizer() {
+        final Normalizer normalizer = Normalizer.LOG;
+        final double inf = Double.POSITIVE_INFINITY;
+        // Infinite sample
+        assertNormalizer(normalizer, new double[] {inf, 1, 1}, new double[] 
{1, 0, 0});
+        assertNormalizer(normalizer, new double[] {inf, 1, inf}, new double[] 
{0.5, 0, 0.5});
+        // Finite max normalises even with -infinity
+        assertNormalizer(normalizer, new double[] {-inf, 
Math.log(Double.MIN_VALUE)}, new double[] {0, 1});
+        // Sum of exp(x_i) overflows
+        assertNormalizer(normalizer, new double[] {Double.MAX_VALUE, 
Double.MAX_VALUE}, new double[] {0.5, 0.5});
+        // Sum of exp(x_i) underflows
+        assertNormalizer(normalizer, new double[] {-Double.MAX_VALUE, 
-Double.MAX_VALUE}, new double[] {0.5, 0.5});
+        // Normalize to unit length
+        // Equal log values
+        final double[] e1 = {1.0 / 3, 1.0 / 3, 1.0 / 3};
+        assertNormalizer(normalizer, new double[] {0, 0, 0}, e1);
+        assertNormalizer(normalizer, new double[] {42, 42, 42}, e1);
+        assertNormalizer(normalizer, new double[] {-13, -13, -13}, e1);
+        // Scale independent normalisation of finite real values
+        final double[] e2 = {Math.exp(-1), Math.exp(-2), Math.exp(-3)};
+        Normalizer.REAL.normalize(e2);
+        assertNormalizer(normalizer, new double[] {-1, -2, -3}, e2);
+        assertNormalizer(normalizer, new double[] {-11, -12, -13}, e2);
+        assertNormalizer(normalizer, new double[] {-10001, -10002, -10003}, 
e2);
+        assertNormalizer(normalizer, new double[] {10003, 10002, 10001}, e2);
+        // Finite max normalises even with -infinity
+        final double[] e3 = {Math.exp(-1), Math.exp(-2), Math.exp(-inf)};
+        Normalizer.REAL.normalize(e3);
+        assertNormalizer(normalizer, new double[] {-1, -2, -inf}, e3);
+
+        // Side effect: NaN is considered the maximum sample.
+        final double nan = Double.NaN;
+        assertNormalizer(normalizer, new double[] {nan, 1, 1}, new double[] 
{1, 0, 0});
+        assertNormalizer(normalizer, new double[] {nan, 1, nan}, new double[] 
{0.5, 0, 0.5});
+        assertNormalizer(normalizer, new double[] {inf, nan}, new double[] {0, 
1});
+    }
+
+    @Test
+    void testScaledLogNormalizer() {
+        final Normalizer normalizer = Normalizer.SCALED_LOG;
+        final double inf = Double.POSITIVE_INFINITY;
+        // Infinite sample
+        assertNormalizer(normalizer, new double[] {inf, 1, 1}, new double[] 
{1, 0, 0});
+        assertNormalizer(normalizer, new double[] {inf, 1, inf}, new double[] 
{0.5, 0, 0.5});
+        // Finite max normalises even with -infinity
+        assertNormalizer(normalizer, new double[] {-inf, 
Math.log(Double.MIN_VALUE)}, new double[] {0, 1});
+        // Sum of exp(x_i) overflows
+        assertNormalizer(normalizer, new double[] {Double.MAX_VALUE, 
Double.MAX_VALUE}, new double[] {0.5, 0.5});
+        // Sum of exp(x_i) underflows
+        assertNormalizer(normalizer, new double[] {-Double.MAX_VALUE, 
-Double.MAX_VALUE}, new double[] {0.5, 0.5});
+        // Normalize to unit length
+        // Equal log values
+        final double[] e1 = {1.0 / 3, 1.0 / 3, 1.0 / 3};
+        assertNormalizer(normalizer, new double[] {0, 0, 0}, e1);
+        assertNormalizer(normalizer, new double[] {42, 42, 42}, e1);
+        assertNormalizer(normalizer, new double[] {-13, -13, -13}, e1);
+        // Scale independent normalisation of finite real values.
+        // ---
+        // Log values are scaled by 2^56 so we have to downscale the provided 
sample.
+        final double downscale = 0x1p-56;
+        // ---
+        final double[] e2 = {Math.exp(-1), Math.exp(-2), Math.exp(-3)};
+        Normalizer.REAL.normalize(e2);
+        assertNormalizer(normalizer, new double[] {-1 * downscale, -2 * 
downscale, -3 * downscale}, e2);
+        assertNormalizer(normalizer, new double[] {-4 * downscale, -5 * 
downscale, -6 * downscale}, e2);
+        assertNormalizer(normalizer, new double[] {-400 * downscale, -401 * 
downscale, -402 * downscale}, e2);
+        // Finite max normalises even with -infinity
+        final double[] e3 = {Math.exp(-1), Math.exp(-2), Math.exp(-inf)};
+        Normalizer.REAL.normalize(e3);
+        assertNormalizer(normalizer, new double[] {-1 * downscale, -2 * 
downscale, -inf}, e3);
+
+        // Side effect: NaN is considered the maximum sample.
+        final double nan = Double.NaN;
+        assertNormalizer(normalizer, new double[] {nan, 1, 1}, new double[] 
{1, 0, 0});
+        assertNormalizer(normalizer, new double[] {nan, 1, nan}, new double[] 
{0.5, 0, 0.5});
+        assertNormalizer(normalizer, new double[] {inf, nan}, new double[] {0, 
1});
+    }
+
+    private static void assertNormalizer(Normalizer normalizer, double[] x, 
double[] expected) {
+        final double[] y = x.clone();
+        final boolean result = normalizer.normalize(x);
+        final Supplier<String> msg = () -> Arrays.toString(y) + " -> " + 
Arrays.toString(x);
+        if (expected == null) {
+            Assertions.assertFalse(result, msg);
+            Assertions.assertArrayEquals(y, x, msg);
+        } else {
+            Assertions.assertTrue(result, msg);
+            final double sum = Arrays.stream(expected).sum();
+            Assertions.assertEquals(1, sum, 0x1p-52, "Expected sum should be 
1");
+            // Normalization may not be exact
+            for (int i = 0; i < x.length; i++) {
+                Assertions.assertEquals(expected[i], x[i], 
Math.ulp(expected[i]), msg);
+            }
+        }
+    }
+
     @Test
     void testDistributionThrowsWithInvalidNumberOfCategories() {
         final UniformRandomProvider rng = RandomAssert.seededRNG();
@@ -110,42 +236,267 @@ class DirichletSamplerTest {
         assertSample(2, sampler.sample());
     }
 
+    /**
+     * Test the assumption that a scaled alpha parameter to a log gamma 
sampler will
+     * return the scaled samples. This works when alpha is very small as the 
sampler
+     * creates samples using log(p) / alpha == scale * log(p) / (scale * 
alpha).
+     */
+    @Test
+    void testScalingAssumptions() {
+        // The threshold where log1p(alpha/E) + log(u) == log(u)
+        // The smallest magnitude for log(u) in [0, 1) is log(1-2^-53) = 
-1.1102230246251565E-16.
+        final double l = Math.log(Math.nextDown(1.0));
+        Assertions.assertEquals(l, Math.log1p(TINY_ALPHA / Math.E) + l);
+
+        final RandomSource randomSource = RandomSource.XO_RO_SHI_RO_128_PP;
+        final byte[] seed = randomSource.createSeed();
+        final UniformRandomProvider rng = randomSource.create(seed);
+        final double scale = 0x1p53;
+        final double tiny = TINY_ALPHA / scale;
+        for (int i = 0; i < 10; i++) {
+            final double alpha1 = rng.nextDouble() * tiny;
+            final double alpha2 = alpha1 * scale;
+            final UniformRandomProvider rng1 = randomSource.create(seed);
+            final UniformRandomProvider rng2 = randomSource.create(seed);
+            // log samples
+            final SharedStateContinuousSampler sampler1 = 
AhrensDieterMarsagliaTsangGammaSampler.of(rng1, alpha1, 1, true);
+            final SharedStateContinuousSampler sampler2 = 
AhrensDieterMarsagliaTsangGammaSampler.of(rng2, alpha2, 1, true);
+            RandomAssert.assertProduceSameSequence(sampler1, () -> 
sampler2.sample() * scale);
+        }
+    }
+
+    /**
+     * Test extreme concentration parameters.
+     * This tests concentration parameters that failed in a previous version
+     * due to a sum of gamma samples that was either zero or infinity. This 
could
+     * not be used to normalise the sum to 1.
+     * Critical thresholds were 1e-19 and 2^1023 for unbounded recursion.
+     * See RNG-195.
+     */
+    @Test
+    void testExtremeConcentration() {
+        // Thresholds to use log gamma samples are 2^-5 and 2^53.
+        // The assertion checks the sum of the sample is 1.
+        assertExtremeConcentration(false, 0, 1e-5, 1e-6, 1e-7);
+        assertExtremeConcentration(false, 0, 1e18, 1e19, 1e20);
+        assertExtremeConcentration(true, 3, 1e50);
+        assertExtremeConcentration(true, 3, 1e-8);
+
+        // Tiny alpha ~ 2^-106
+        assertExtremeConcentration(false, 0, TINY_ALPHA / 1.5, TINY_ALPHA / 2, 
TINY_ALPHA, 2.5);
+        assertExtremeConcentration(true, 2, Double.MIN_VALUE);
+        // One dominant concentration
+        assertExtremeConcentration(false, 0, TINY_ALPHA / 1e200, TINY_ALPHA / 
1.5, TINY_ALPHA / 1e100);
+        // Two dominant concentrations
+        assertExtremeConcentration(false, 0, TINY_ALPHA / 1.5, TINY_ALPHA / 
1.5, TINY_ALPHA / 1e100);
+
+        // Large alpha
+        assertExtremeConcentration(false, 0, 0x1p1022, 0x1p1023, 0x1.5p1023);
+        assertExtremeConcentration(true, 2, Double.MAX_VALUE);
+
+        // A large alpha and tiny alpha will create some log samples as 
-infinity.
+        // Normalisation should handle this and this category will never be 
observed.
+        assertExtremeConcentration(false, 0, 0x1p1023, TINY_ALPHA / 2);
+        assertExtremeConcentration(false, 0, Double.MIN_VALUE, 
Double.MAX_VALUE);
+    }
+
+    private static void assertExtremeConcentration(boolean symmetric, int k, 
double... alpha) {
+        final RandomSource randomSource = RandomSource.XO_RO_SHI_RO_128_PP;
+        final byte[] seed = randomSource.createSeed();
+        final UniformRandomProvider rng1 = randomSource.create(seed);
+        final DirichletSampler sampler1;
+        final double[] alphas;
+        if (symmetric) {
+            sampler1 = DirichletSampler.symmetric(rng1, k, alpha[0]);
+            alphas = new double[k];
+            Arrays.fill(alphas, alpha[0]);
+        } else {
+            sampler1 = DirichletSampler.of(rng1, alpha);
+            alphas = alpha.clone();
+        }
+
+        // Test sample generation does not infinitely recurse.
+        // 10 milliseconds is fine on a fast machine; add some headroom
+        final int n = 100;
+        final Duration timeout = Duration.ofMillis(100);
+        final double[][][] samples = new double[1][][];
+
+        Assertions.assertTimeoutPreemptively(timeout,
+            () -> samples[0] = sampler1.samples(n).toArray(double[][]::new));
+        final double[][] x = samples[0];
+        Assertions.assertNotNull(x);
+
+        if (symmetric) {
+            // Check the general case matches
+            final UniformRandomProvider rng2 = randomSource.create(seed);
+            final DirichletSampler sampler2 = DirichletSampler.of(rng2, 
alphas);
+
+            Assertions.assertTimeoutPreemptively(timeout,
+                () -> samples[0] = 
sampler2.samples(n).toArray(double[][]::new));
+            final double[][] y = samples[0];
+            // Check nested arrays
+            Assertions.assertArrayEquals(x, y);
+        }
+
+        // Checks on the sample. Correct length and sum to 1.
+        for (int i = 0; i < n; i++) {
+            assertSample(alphas.length, x[i]);
+        }
+
+        // Extreme concentrations have predictable output
+        final double minAlpha = Arrays.stream(alpha).min().getAsDouble();
+        final double maxAlpha = Arrays.stream(alpha).max().getAsDouble();
+
+        // If alpha is above 1e33 then the probability of a gamma sample 
p(x=alpha) = 1.
+        // Check if the samples are all the same. The samples should be the 
normalised
+        // log alpha values.
+        if (minAlpha > 1e33) {
+            final double[] expected = 
Arrays.stream(alphas).map(Math::log).toArray();
+            Normalizer.LOG.normalize(expected);
+            for (int i = 0; i < n; i++) {
+                Assertions.assertArrayEquals(expected, x[i], "All samples 
should be the same");
+            }
+        }
+
+        // If alpha is below 2^-53 then the log gamma samples are log(u) / 
alpha.
+        // If alpha is all the same then the samples are expected to be 1.0 in 
a single
+        // category and 0.0 elsewhere (assuming u output from the RNG is not 
identical
+        // within a sample).
+        // If the alpha magnitude difference is very large across categories 
some categories
+        // will never be observed.
+        if (maxAlpha < TINY_ALPHA) {
+            // Count observed categories
+            final long[] observed = new long[alphas.length];
+            if (symmetric) {
+                // A symmetric sample for a tiny concentration is to pick one 
of the
+                // categories as 1.0 and all others 0.0.
+                // Note a previous check ensured the sum was 1.0. Here we test 
for
+                // categories being 1.0 and all others zero, or allow a rare 
case of 0.5
+                // in two categories (chance of 2^-53 for two p output from 
the RNG to match).
+                for (int i = 0; i < n; i++) {
+                    final double[] s = x[i];
+                    int cOne = 0;
+                    int cHalf = 0;
+                    int cZero = 0;
+                    for (int j = 0; j < s.length; j++) {
+                        if (Math.abs(s[j] - 1.0) < 1e15) {
+                            cOne++;
+                            observed[j]++;
+                        } else if (Math.abs(s[j] - 0.5) < 1e15) {
+                            cHalf++;
+                            observed[j]++;
+                        } else if (Math.abs(s[j]) < 1e15) {
+                            cZero++;
+                            // not observed
+                        }
+                    }
+                    // Expected 1.0, 0.5 or 0.0. Fail if anything else was 
present.
+                    Assertions.assertEquals(s.length, cOne + cHalf + cZero,
+                        () -> Arrays.toString(s));
+                }
+                // The observed should follow a uniform distribution across 
categories
+                final ChiSquareTest chiSquareTest = new ChiSquareTest();
+                final double[] expected = new double[alphas.length];
+                Arrays.fill(expected, 1.0 / alphas.length);
+                Assertions.assertFalse(chiSquareTest.chiSquareTest(expected, 
observed, 0.001),
+                    () -> "Symetric case was not uniform: " + 
Arrays.toString(observed));
+            } else {
+                // Assume all alphas are different as the symmetric case was 
not used.
+                // Flag any alpha not expected by comparison to max alpha.
+                // Identify the case where for all p in [0, 1):
+                //   log(p1) / alpha1 > log(p2) / alpha2
+                //   log(p1) / log(p2) > alpha1 / alpha2
+                // Extreme finite sample when p1 = 2^-53 and p2 = 1-2^-53:
+                // -36.74 / -1.11e-16 = 3.31e17
+                // If an alpha is much smaller than the max then it should not 
be observed
+                // assuming p != 0 for the dominant concentration.
+                // This test can fail by chance so check the allowed category 
across all samples.
+                final int[] allowed = Arrays.stream(alphas).mapToInt(a -> 
maxAlpha > a * 4e17 ? 0 : 1).toArray();
+                for (int i = 0; i < n; i++) {
+                    final double[] s = x[i];
+                    for (int j = 0; j < s.length; j++) {
+                        if (s[j] != 0) {
+                            observed[j]++;
+                        }
+                    }
+                }
+                // Check the allowed categories
+                for (int j = 0; j < alphas.length; j++) {
+                    // The disallowed category can occur by chance (frequency 
2^-53 when the max
+                    // alpha sample had p=0 from the RNG). This is very 
unlikely multiple times.
+                    if (allowed[j] == 0 && observed[j] > 1) {
+                        Assertions.fail(String.format("%s : Unexpected 
observation for tiny alpha %s",
+                            Arrays.toString(alphas), 
Arrays.toString(observed)));
+                    }
+                }
+            }
+        }
+    }
+
     @Test
     void testSharedStateSampler() {
+        assertSharedStateSampler(1, 2, 3);
+        // Tiny alpha
+        assertSharedStateSampler(TINY_ALPHA / 1.5, TINY_ALPHA / 2, TINY_ALPHA 
/ 2.5);
+        // Large alpha
+        assertSharedStateSampler(1e100, 2e100, 3e100);
+        // Small alpha (requires log samples) with a category with alpha=1
+        // that can use an exponential sampler
+        assertSharedStateSampler(1, 1e-5, 1e-3);
+    }
+
+    private static void assertSharedStateSampler(double... alpha) {
         final RandomSource randomSource = RandomSource.XO_RO_SHI_RO_128_PP;
         final byte[] seed = randomSource.createSeed();
         final UniformRandomProvider rng1 = randomSource.create(seed);
         final UniformRandomProvider rng2 = randomSource.create(seed);
-        final DirichletSampler sampler1 = DirichletSampler.of(rng1, 1, 2, 3);
+        final DirichletSampler sampler1 = DirichletSampler.of(rng1, alpha);
         final DirichletSampler sampler2 = 
sampler1.withUniformRandomProvider(rng2);
         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
     }
 
     @Test
     void testSharedStateSamplerForSymmetricCase() {
+        assertSharedStateSamplerForSymmetricCase(2, 1.5);
+        // Tiny alpha
+        assertSharedStateSamplerForSymmetricCase(3, TINY_ALPHA / 1.5);
+        // Large alpha
+        assertSharedStateSamplerForSymmetricCase(2, 2.5e50);
+        // Exponential sampler
+        assertSharedStateSamplerForSymmetricCase(4, 1);
+    }
+
+    private static void assertSharedStateSamplerForSymmetricCase(int k, double 
alpha) {
         final RandomSource randomSource = RandomSource.XO_RO_SHI_RO_128_PP;
         final byte[] seed = randomSource.createSeed();
         final UniformRandomProvider rng1 = randomSource.create(seed);
         final UniformRandomProvider rng2 = randomSource.create(seed);
-        final DirichletSampler sampler1 = DirichletSampler.symmetric(rng1, 2, 
1.5);
+        final DirichletSampler sampler1 = DirichletSampler.symmetric(rng1, k, 
alpha);
         final DirichletSampler sampler2 = 
sampler1.withUniformRandomProvider(rng2);
         RandomAssert.assertProduceSameSequence(sampler1, sampler2);
     }
 
     @Test
     void testSymmetricCaseMatchesGeneralCase() {
+        assertSymmetricCaseMatchesGeneralCase(3, 0.5);
+        assertSymmetricCaseMatchesGeneralCase(3, 1.0);
+        assertSymmetricCaseMatchesGeneralCase(3, 1.5);
+        // Tiny alpha
+        assertSymmetricCaseMatchesGeneralCase(4, TINY_ALPHA / 1.5);
+        // Large alpha
+        assertSymmetricCaseMatchesGeneralCase(2, 42e50);
+    }
+
+    private static void assertSymmetricCaseMatchesGeneralCase(int k, double 
alpha) {
         final RandomSource randomSource = RandomSource.XO_RO_SHI_RO_128_PP;
         final byte[] seed = randomSource.createSeed();
         final UniformRandomProvider rng1 = randomSource.create(seed);
         final UniformRandomProvider rng2 = randomSource.create(seed);
-        final int k = 3;
         final double[] alphas = new double[k];
-        for (final double alpha : new double[] {0.5, 1.0, 1.5}) {
-            Arrays.fill(alphas, alpha);
-            final DirichletSampler sampler1 = DirichletSampler.symmetric(rng1, 
k, alpha);
-            final DirichletSampler sampler2 = DirichletSampler.of(rng2, 
alphas);
-            RandomAssert.assertProduceSameSequence(sampler1, sampler2);
-        }
+        Arrays.fill(alphas, alpha);
+        final DirichletSampler sampler1 = DirichletSampler.symmetric(rng1, k, 
alpha);
+        final DirichletSampler sampler2 = DirichletSampler.of(rng2, alphas);
+        RandomAssert.assertProduceSameSequence(sampler1, sampler2);
     }
 
     /**
@@ -185,6 +536,30 @@ class DirichletSamplerTest {
         assertSamples(1, 2, 3, 4);
     }
 
+    @Test
+    void testSampling6() {
+        assertSamples(1000, 2000, 3000);
+    }
+
+    @Test
+    void testSampling7() {
+        // Log samples are used when max alpha < 2^-5 or > 2^53
+        assertSamples(0x1p-6, 0x2p-6, 0x3p-6);
+    }
+
+    @Test
+    void testSampling8() {
+        // Log samples are used when max alpha < 2^-5 or > 2^53
+        assertSamples(0x1p53, 0x2p53, 0x3p53);
+    }
+
+    @Test
+    void testSampling9() {
+        // tiny alpha.
+        // BigDecimal handles the small values to compute the mean and 
variance.
+        assertSamples(TINY_ALPHA / 1.5, TINY_ALPHA / 2.5, TINY_ALPHA / 2);
+    }
+
     /**
      * Assert samples from the distribution. The variates are tested against 
the expected
      * mean and covariance for the given concentration parameters.
@@ -205,38 +580,56 @@ class DirichletSamplerTest {
 
         // Computation of moments:
         // https://en.wikipedia.org/wiki/Dirichlet_distribution#Moments
+        // Perform in extended precision to handle extreme concentration 
parameters.
+        // 20 digits of precision when dividing is enough for a double result.
+        final MathContext mc = new MathContext(20);
 
         // Compute the sum of the concentration parameters: alpha0
-        double alpha0 = 0;
+        BigDecimal alpha0 = BigDecimal.ZERO;
         for (int i = 0; i < k; i++) {
-            alpha0 += alpha[i];
+            alpha0 = alpha0.add(bd(alpha[i]));
         }
 
         // Use a moderate tolerance.
         // Differences are usually observed in the 3rd significant figure.
-        final double relativeTolerance = 5e-2;
+        // When the alpha is large the covariance is very low and computation 
of
+        // actual may be inaccurate. Here differences are in the 1st or
+        // 2nd significant figure.
+        final double meanRelativeTolerance = 5e-2;
+        final double varRelativeTolerance = alpha0.doubleValue() > 0x1p53 ? 
0.1 : 5e-2;
 
         // Mean = alpha[i] / alpha0
         final double[] means = getColumnMeans(samples);
         for (int i = 0; i < k; i++) {
-            final double mean = alpha[i] / alpha0;
-            Assertions.assertEquals(mean, means[i], mean * relativeTolerance, 
"Mean");
+            final double mean = bd(alpha[i]).divide(alpha0, mc).doubleValue();
+            Assertions.assertEquals(mean, means[i], mean * 
meanRelativeTolerance, "Mean");
         }
 
         // Variance = alpha_i (alpha_0 - alpha_i) / alpha_0^2 (alpha_0 + 1)
         // Covariance = -alpha_i * alpha_j / alpha_0^2 (alpha_0 + 1)
         final double[][] covars = getCovariance(samples);
-        final double denom = alpha0 * alpha0 * (alpha0 + 1);
+        final BigDecimal denom = 
alpha0.pow(2).multiply(alpha0.add(BigDecimal.ONE));
         for (int i = 0; i < k; i++) {
-            final double variance = alpha[i] * (alpha0 - alpha[i]) / denom;
-            Assertions.assertEquals(variance, covars[i][i], variance * 
relativeTolerance, "Variance");
+            final double variance = 
bd(alpha[i]).multiply(alpha0.subtract(bd(alpha[i])))
+                .divide(denom, mc).doubleValue();
+            Assertions.assertEquals(variance, covars[i][i], variance * 
varRelativeTolerance, "Variance");
             for (int j = i + 1; j < k; j++) {
-                final double covar = -alpha[i] * alpha[j] / denom;
-                Assertions.assertEquals(covar, covars[i][j], Math.abs(covar) * 
relativeTolerance, "Covariance");
+                final double covar = 
bd(-alpha[i]).multiply(bd(alpha[j])).divide(denom, mc).doubleValue();
+                Assertions.assertEquals(covar, covars[i][j], Math.abs(covar) * 
varRelativeTolerance, "Covariance");
             }
         }
     }
 
+    /**
+     * Create a BigDecimal from the double value.
+     *
+     * @param x Value.
+     * @return the BigDecimal
+     */
+    private static BigDecimal bd(double x) {
+        return new BigDecimal(x);
+    }
+
     /**
      * Assert the sample has the correct length and sums to 1.
      *
@@ -251,7 +644,7 @@ class DirichletSamplerTest {
         for (int i = 2; i < x.length; i++) {
             sum += x[i];
         }
-        Assertions.assertEquals(1.0, sum, 1e-10, "Invalid sum");
+        Assertions.assertEquals(1.0, sum, 2 * Math.ulp(1.0), "Invalid sum");
     }
 
     /**
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 606b6f4b..040bb0c6 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -56,6 +56,10 @@ If the output is not quite correct, check for invisible 
trailing spaces!
     <release version="1.8" date="TBD" description="
 New features, updates and bug fixes (requires Java 8).
 ">
+      <action dev="aherbert" type="fix" due-to="Security scan, Alex Herbert" 
issue="RNG-195">
+        "DirichletSampler": Avoid unbouded recursion for extremely small or 
large
+        concentration parameters which created stack overflow errors.
+      </action>
       <action dev="aherbert" type="add" due-to="Alex Herbert" issue="RNG-203">
         "ZetaSampler": Add a sampler for the zeta distribution.
       </action>

Reply via email to