You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ah...@apache.org on 2021/07/25 09:11:42 UTC
[commons-rng] branch master updated (e21c78d -> a5d6134)
This is an automated email from the ASF dual-hosted git repository.
aherbert pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/commons-rng.git.
from e21c78d Pass RNG as first argument to UnitSphereSampler factory constructor
new b930e7b RNG-156: Add DirichletSampler
new a5d6134 RNG-156: Track changes
The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../sampling/distribution/DirichletSampler.java | 262 +++++++++++++++++++
.../distribution/DirichletSamplerTest.java | 276 +++++++++++++++++++++
src/changes/changes.xml | 3 +
src/main/resources/pmd/pmd-ruleset.xml | 3 +-
4 files changed, 543 insertions(+), 1 deletion(-)
create mode 100644 commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/DirichletSampler.java
create mode 100644 commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/DirichletSamplerTest.java
[commons-rng] 01/02: RNG-156: Add DirichletSampler
Posted by ah...@apache.org.
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
commit b930e7ba3e6c5c623d2c04c3100b8dcdd2bba239
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Fri Jul 23 13:50:03 2021 +0100
RNG-156: Add DirichletSampler
---
.../sampling/distribution/DirichletSampler.java | 262 +++++++++++++++++++
.../distribution/DirichletSamplerTest.java | 276 +++++++++++++++++++++
src/main/resources/pmd/pmd-ruleset.xml | 3 +-
3 files changed, 540 insertions(+), 1 deletion(-)
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
new file mode 100644
index 0000000..86e6358
--- /dev/null
+++ b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/DirichletSampler.java
@@ -0,0 +1,262 @@
+/*
+ * 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.SharedStateObjectSampler;
+
+/**
+ * Sampling from a <a href="https://en.wikipedia.org/wiki/Dirichlet_distribution">Dirichlet
+ * distribution</a>.
+ *
+ * <p>Sampling uses:</p>
+ *
+ * <ul>
+ * <li>{@link UniformRandomProvider#nextLong()}
+ * <li>{@link UniformRandomProvider#nextDouble()}
+ * </ul>
+ *
+ * @since 1.4
+ */
+public abstract class DirichletSampler implements SharedStateObjectSampler<double[]> {
+ /** The minimum number of categories. */
+ private static final int MIN_CATGEORIES = 2;
+
+ /** RNG (used for the toString() method). */
+ private final UniformRandomProvider rng;
+
+ /**
+ * Sample from a Dirichlet distribution with different concentration parameters
+ * for each category.
+ */
+ private static final class GeneralDirichletSampler extends DirichletSampler {
+ /** Samplers for each category. */
+ private final SharedStateContinuousSampler[] samplers;
+
+ /**
+ * @param rng Generator of uniformly distributed random numbers.
+ * @param samplers Samplers for each category.
+ */
+ GeneralDirichletSampler(UniformRandomProvider rng,
+ SharedStateContinuousSampler[] samplers) {
+ super(rng);
+ // Array is stored directly as it is generated within the DirichletSampler class
+ this.samplers = samplers;
+ }
+
+ @Override
+ protected int getK() {
+ return samplers.length;
+ }
+
+ @Override
+ protected double nextGamma(int i) {
+ return samplers[i].sample();
+ }
+
+ @Override
+ public GeneralDirichletSampler withUniformRandomProvider(UniformRandomProvider rng) {
+ final SharedStateContinuousSampler[] newSamplers = new SharedStateContinuousSampler[samplers.length];
+ for (int i = 0; i < newSamplers.length; i++) {
+ newSamplers[i] = samplers[i].withUniformRandomProvider(rng);
+ }
+ return new GeneralDirichletSampler(rng, newSamplers);
+ }
+ }
+
+ /**
+ * Sample from a symmetric Dirichlet distribution with the same concentration parameter
+ * for each category.
+ */
+ private static final class SymmetricDirichletSampler extends DirichletSampler {
+ /** Number of categories. */
+ private final int k;
+ /** Sampler for the categories. */
+ private final SharedStateContinuousSampler sampler;
+
+ /**
+ * @param rng Generator of uniformly distributed random numbers.
+ * @param k Number of categories.
+ * @param sampler Sampler for the categories.
+ */
+ SymmetricDirichletSampler(UniformRandomProvider rng,
+ int k,
+ SharedStateContinuousSampler sampler) {
+ super(rng);
+ this.k = k;
+ this.sampler = sampler;
+ }
+
+ @Override
+ protected int getK() {
+ return k;
+ }
+
+ @Override
+ protected double nextGamma(int i) {
+ return sampler.sample();
+ }
+
+ @Override
+ public SymmetricDirichletSampler withUniformRandomProvider(UniformRandomProvider rng) {
+ return new SymmetricDirichletSampler(rng, k, sampler.withUniformRandomProvider(rng));
+ }
+ }
+
+ /**
+ * @param rng Generator of uniformly distributed random numbers.
+ */
+ private DirichletSampler(UniformRandomProvider rng) {
+ this.rng = rng;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return "Dirichlet deviate [" + rng.toString() + "]";
+ }
+
+ @Override
+ 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;
+ }
+ // 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)) {
+ // 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;
+ }
+
+ /**
+ * Gets the number of categories.
+ *
+ * @return k
+ */
+ protected abstract int getK();
+
+ /**
+ * Create a gamma sample for the given category.
+ *
+ * @param category Category.
+ * @return the sample
+ */
+ protected abstract double nextGamma(int category);
+
+ /** {@inheritDoc} */
+ // Redeclare the signature to return a DirichletSampler not a SharedStateObjectSampler<double[]>
+ @Override
+ public abstract DirichletSampler withUniformRandomProvider(UniformRandomProvider rng);
+
+ /**
+ * Creates a new Dirichlet distribution sampler.
+ *
+ * @param rng Generator of uniformly distributed random numbers.
+ * @param alpha Concentration parameters.
+ * @return the sampler
+ * @throws IllegalArgumentException if the number of concentration parameters
+ * is less than 2; or if any concentration parameter is not strictly positive.
+ */
+ public static DirichletSampler of(UniformRandomProvider rng,
+ double... alpha) {
+ validateNumberOfCategories(alpha.length);
+ final SharedStateContinuousSampler[] samplers = new SharedStateContinuousSampler[alpha.length];
+ for (int i = 0; i < samplers.length; i++) {
+ samplers[i] = createSampler(rng, alpha[i]);
+ }
+ return new GeneralDirichletSampler(rng, samplers);
+ }
+
+ /**
+ * Creates a new symmetric Dirichlet distribution sampler using the same concentration
+ * parameter for each category.
+ *
+ * @param rng Generator of uniformly distributed random numbers.
+ * @param k Number of categories.
+ * @param alpha Concentration parameter.
+ * @return the sampler
+ * @throws IllegalArgumentException if the number of categories is
+ * less than 2; or if the concentration parameter is not strictly positive.
+ */
+ public static DirichletSampler symmetric(UniformRandomProvider rng,
+ int k,
+ double alpha) {
+ validateNumberOfCategories(k);
+ final SharedStateContinuousSampler sampler = createSampler(rng, alpha);
+ return new SymmetricDirichletSampler(rng, k, sampler);
+ }
+
+ /**
+ * Validate the number of categories.
+ *
+ * @param k Number of categories.
+ * @throws IllegalArgumentException if the number of categories is
+ * less than 2.
+ */
+ private static void validateNumberOfCategories(int k) {
+ if (k < MIN_CATGEORIES) {
+ throw new IllegalArgumentException("Invalid number of categories: " + k);
+ }
+ }
+
+ /**
+ * Creates a gamma sampler for a category with the given concentration parameter.
+ *
+ * @param rng Generator of uniformly distributed random numbers.
+ * @param alpha Concentration parameter.
+ * @return the sampler
+ * @throws IllegalArgumentException if the concentration parameter is not strictly positive.
+ */
+ private static SharedStateContinuousSampler createSampler(UniformRandomProvider rng,
+ double alpha) {
+ // Negation of logic will detect NaN
+ if (!isNonZeroPositiveFinite(alpha)) {
+ throw new IllegalArgumentException("Invalid concentration: " + alpha);
+ }
+ // Create a Gamma(shape=alpha, scale=1) sampler.
+ if (alpha == 1) {
+ // Special case
+ // Gamma(shape=1, scale=1) == Exponential(mean=1)
+ return ZigguratSampler.Exponential.of(rng);
+ }
+ return AhrensDieterMarsagliaTsangGammaSampler.of(rng, alpha, 1);
+ }
+
+ /**
+ * Return true if the value is non-zero, positive and finite.
+ *
+ * @param x Value.
+ * @return true if non-zero positive finite
+ */
+ private static boolean isNonZeroPositiveFinite(double x) {
+ return x > 0 && x < Double.POSITIVE_INFINITY;
+ }
+}
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
new file mode 100644
index 0000000..53b440b
--- /dev/null
+++ b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/DirichletSamplerTest.java
@@ -0,0 +1,276 @@
+/*
+ * 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.junit.Assert;
+import org.junit.Test;
+import java.util.Arrays;
+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.rng.UniformRandomProvider;
+import org.apache.commons.rng.core.source64.SplitMix64;
+import org.apache.commons.rng.sampling.RandomAssert;
+import org.apache.commons.rng.simple.RandomSource;
+
+/**
+ * Test for {@link DirichletSampler}.
+ */
+public class DirichletSamplerTest {
+ @Test(expected = IllegalArgumentException.class)
+ public void testDistributionThrowsWithInvalidNumberOfCategories() {
+ final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+ DirichletSampler.of(rng, 1.0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testDistributionThrowsWithZeroConcentration() {
+ final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+ DirichletSampler.of(rng, 1.0, 0.0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testDistributionThrowsWithNaNConcentration() {
+ final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+ DirichletSampler.of(rng, 1.0, Double.NaN);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testDistributionThrowsWithInfiniteConcentration() {
+ final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+ DirichletSampler.of(rng, 1.0, Double.POSITIVE_INFINITY);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSymmetricDistributionThrowsWithInvalidNumberOfCategories() {
+ final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+ DirichletSampler.symmetric(rng, 1, 1.0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSymmetricDistributionThrowsWithZeroConcentration() {
+ final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+ DirichletSampler.symmetric(rng, 2, 0.0);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSymmetricDistributionThrowsWithNaNConcentration() {
+ final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+ DirichletSampler.symmetric(rng, 2, Double.NaN);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSymmetricDistributionThrowsWithInfiniteConcentration() {
+ final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+ DirichletSampler.symmetric(rng, 2, Double.POSITIVE_INFINITY);
+ }
+
+ /**
+ * Create condition so that all samples are zero and it is impossible to normalise the
+ * samples to sum to 1. These should be ignored and the sample is repeated until
+ * normalisation is possible.
+ */
+ @Test
+ public void testInvalidSampleIsIgnored() {
+ // An RNG implementation which should create zero samples from the underlying
+ // exponential sampler for an initial sequence.
+ final UniformRandomProvider rng = new SplitMix64(0L) {
+ private int i;
+
+ @Override
+ public long next() {
+ return i++ < 10 ? 0L : super.next();
+ }
+ };
+
+ // Alpha=1 will use an exponential sampler
+ final DirichletSampler sampler = DirichletSampler.symmetric(rng, 2, 1.0);
+ assertSample(2, sampler.sample());
+ }
+
+ @Test
+ public void testSharedStateSampler() {
+ 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 sampler2 = sampler1.withUniformRandomProvider(rng2);
+ RandomAssert.assertProduceSameSequence(sampler1, sampler2);
+ }
+
+ @Test
+ public void testSharedStateSamplerForSymmetricCase() {
+ 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 sampler2 = sampler1.withUniformRandomProvider(rng2);
+ RandomAssert.assertProduceSameSequence(sampler1, sampler2);
+ }
+
+ @Test
+ public void testSymmetricCaseMatchesGeneralCase() {
+ 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);
+ }
+ }
+
+ /**
+ * Test the toString method. This is added to ensure coverage.
+ */
+ @Test
+ public void testToString() {
+ final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+ final DirichletSampler sampler1 = DirichletSampler.symmetric(rng, 2, 1.0);
+ final DirichletSampler sampler2 = DirichletSampler.of(rng, 0.5, 1, 1.5);
+ Assert.assertTrue(sampler1.toString().toLowerCase().contains("dirichlet"));
+ Assert.assertTrue(sampler2.toString().toLowerCase().contains("dirichlet"));
+ }
+
+ @Test
+ public void testSampling1() {
+ assertSamples(1, 2, 3);
+ }
+
+ @Test
+ public void testSampling2() {
+ assertSamples(1, 1, 1);
+ }
+
+ @Test
+ public void testSampling3() {
+ assertSamples(0.5, 1, 1.5);
+ }
+
+ @Test
+ public void testSampling4() {
+ assertSamples(1, 3);
+ }
+
+ @Test
+ public void testSampling5() {
+ assertSamples(1, 2, 3, 4);
+ }
+
+ /**
+ * Assert samples from the distribution. The variates are tested against the expected
+ * mean and covariance for the given concentration parameters.
+ *
+ * @param alpha Concentration parameters.
+ */
+ private static void assertSamples(double... alpha) {
+ // No fixed seed. Failed tests will be repeated by the JUnit test runner.
+ final UniformRandomProvider rng = RandomSource.XO_RO_SHI_RO_128_PP.create();
+ final DirichletSampler sampler = DirichletSampler.of(rng, alpha);
+ final int k = alpha.length;
+ final double[][] samples = new double[100000][];
+ for (int i = 0; i < samples.length; i++) {
+ final double[] x = sampler.sample();
+ assertSample(k, x);
+ samples[i] = x;
+ }
+
+ // Computation of moments:
+ // https://en.wikipedia.org/wiki/Dirichlet_distribution#Moments
+
+ // Compute the sum of the concentration parameters: alpha0
+ double alpha0 = 0;
+ for (int i = 0; i < k; i++) {
+ alpha0 += alpha[i];
+ }
+
+ // Use a moderate tolerance.
+ // Differences are usually observed in the 3rd significant figure.
+ final double relativeTolerance = 5e-2;
+
+ // Mean = alpha[i] / alpha0
+ final double[] means = getColumnMeans(samples);
+ for (int i = 0; i < k; i++) {
+ final double mean = alpha[i] / alpha0;
+ Assert.assertEquals("Mean", mean, means[i], mean * relativeTolerance);
+ }
+
+ // 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);
+ for (int i = 0; i < k; i++) {
+ final double var = alpha[i] * (alpha0 - alpha[i]) / denom;
+ Assert.assertEquals("Variance", var, covars[i][i], var * relativeTolerance);
+ for (int j = i + 1; j < k; j++) {
+ final double covar = -alpha[i] * alpha[j] / denom;
+ Assert.assertEquals("Covariance", covar, covars[i][j], Math.abs(covar) * relativeTolerance);
+ }
+ }
+ }
+
+ /**
+ * Assert the sample has the correct length and sums to 1.
+ *
+ * @param k Expected number of categories.
+ * @param x Sample.
+ */
+ private static void assertSample(int k, double[] x) {
+ Assert.assertEquals("Number of categories", k, x.length);
+ // There are always at least 2 categories
+ double sum = x[0] + x[1];
+ // Sum the rest
+ for (int i = 2; i < x.length; i++) {
+ sum += x[i];
+ }
+ Assert.assertEquals("Invalid sum", 1.0, sum, 1e-10);
+ }
+
+ /**
+ * Gets the column means. This is done using the same method as the means in the
+ * Apache Commons Math Covariance class by using the Mean class.
+ *
+ * @param data the data
+ * @return the column means
+ */
+ private static double[] getColumnMeans(double[][] data) {
+ final Array2DRowRealMatrix m = new Array2DRowRealMatrix(data, false);
+ final Mean mean = new Mean();
+ final double[] means = new double[m.getColumnDimension()];
+ for (int i = 0; i < means.length; i++) {
+ means[i] = mean.evaluate(m.getColumn(i));
+ }
+ return means;
+ }
+
+ /**
+ * Gets the covariance.
+ *
+ * @param data the data
+ * @return the covariance
+ */
+ private static double[][] getCovariance(double[][] data) {
+ final Array2DRowRealMatrix m = new Array2DRowRealMatrix(data, false);
+ return new Covariance(m).getCovarianceMatrix().getData();
+ }
+}
diff --git a/src/main/resources/pmd/pmd-ruleset.xml b/src/main/resources/pmd/pmd-ruleset.xml
index c022ac1..9d95e9f 100644
--- a/src/main/resources/pmd/pmd-ruleset.xml
+++ b/src/main/resources/pmd/pmd-ruleset.xml
@@ -75,7 +75,8 @@
<!-- Array is generated internally in this case. -->
<property name="violationSuppressXPath"
value="//ClassOrInterfaceDeclaration[@SimpleName='PoissonSamplerCache' or @SimpleName='AliasMethodDiscreteSampler'
- or @SimpleName='GuideTableDiscreteSampler' or @SimpleName='SharedStateDiscreteProbabilitySampler']"/>
+ or @SimpleName='GuideTableDiscreteSampler' or @SimpleName='SharedStateDiscreteProbabilitySampler'
+ or @SimpleName='DirichletSampler']"/>
</properties>
</rule>
<rule ref="category/java/bestpractices.xml/SystemPrintln">
[commons-rng] 02/02: RNG-156: Track changes
Posted by ah...@apache.org.
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
commit a5d61345d0ee00c1a99e1e9427945fbb83ff6f58
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Sun Jul 25 10:11:38 2021 +0100
RNG-156: Track changes
---
src/changes/changes.xml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 9781dfb..c94a0a4 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -77,6 +77,9 @@ re-run tests that fail, and pass the build if they succeed
within the allotted number of reruns (the test will be marked
as 'flaky' in the report).
">
+ <action dev="aherbert" type="add" issue="156">
+ New "DirichletSampler" class to sample from a Dirichlet distribution.
+ </action>
<action dev="aherbert" type="update" issue="157">
"UnitSphereSampler": Deprecate public constructor. Use the factory constructor to create
an optimal sampler.