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/20 11:10:23 UTC

[commons-rng] branch master updated (11b8d25 -> 066e43e)

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 11b8d25  Update DiscreteUniformSampler to match UniformLongSampler
     new 211f351  RNG-138: CompositeSamplers to sample from a weighted combination of samplers
     new 066e43e  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:
 .../commons/rng/sampling/CompositeSamplers.java    | 1093 ++++++++++++++++++++
 .../rng/sampling/CompositeSamplersTest.java        | 1004 ++++++++++++++++++
 src/changes/changes.xml                            |    3 +
 src/main/resources/pmd/pmd-ruleset.xml             |   23 +-
 4 files changed, 2121 insertions(+), 2 deletions(-)
 create mode 100644 commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/CompositeSamplers.java
 create mode 100644 commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/CompositeSamplersTest.java

[commons-rng] 02/02: 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 066e43e35a2d5bc1e63f781455c3b660ca436341
Author: aherbert <ah...@apache.org>
AuthorDate: Tue Jul 20 12:10:18 2021 +0100

    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 2dc66ae..81a2512 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="138">
+        New "CompositeSamplers" class to sample from a weighted combination of samplers.
+      </action>
       <action dev="aherbert" type="add" issue="140">
         New "LongSampler" interface for sampling a long. New "UniformLongSampler" to sample
         from a range.

[commons-rng] 01/02: RNG-138: CompositeSamplers to sample from a weighted combination of samplers

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 211f351968f98c923e5634e59127fe945780db09
Author: Alex Herbert <ah...@apache.org>
AuthorDate: Fri May 14 20:40:25 2021 +0100

    RNG-138: CompositeSamplers to sample from a weighted combination of
    samplers
---
 .../commons/rng/sampling/CompositeSamplers.java    | 1093 ++++++++++++++++++++
 .../rng/sampling/CompositeSamplersTest.java        | 1004 ++++++++++++++++++
 src/main/resources/pmd/pmd-ruleset.xml             |   23 +-
 3 files changed, 2118 insertions(+), 2 deletions(-)

diff --git a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/CompositeSamplers.java b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/CompositeSamplers.java
new file mode 100644
index 0000000..a269338
--- /dev/null
+++ b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/CompositeSamplers.java
@@ -0,0 +1,1093 @@
+/*
+ * 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;
+
+import java.util.List;
+import java.util.ArrayList;
+
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.sampling.distribution.AliasMethodDiscreteSampler;
+import org.apache.commons.rng.sampling.distribution.ContinuousSampler;
+import org.apache.commons.rng.sampling.distribution.DiscreteSampler;
+import org.apache.commons.rng.sampling.distribution.DiscreteUniformSampler;
+import org.apache.commons.rng.sampling.distribution.GuideTableDiscreteSampler;
+import org.apache.commons.rng.sampling.distribution.LongSampler;
+import org.apache.commons.rng.sampling.distribution.MarsagliaTsangWangDiscreteSampler;
+import org.apache.commons.rng.sampling.distribution.SharedStateContinuousSampler;
+import org.apache.commons.rng.sampling.distribution.SharedStateDiscreteSampler;
+import org.apache.commons.rng.sampling.distribution.SharedStateLongSampler;
+
+/**
+ * Factory class to create a sampler that combines sampling from multiple samplers.
+ *
+ * <p>The composite sampler is constructed using a {@link Builder builder} for the type of samplers
+ * that will form the composite. Each sampler has a weight in the composition.
+ * Samples are returned using a 2 step algorithm:
+ *
+ * <ol>
+ *  <li>Select a sampler based on its weighting
+ *  <li>Return a sample from the selected sampler
+ * </ol>
+ *
+ * <p>The weights used for each sampler create a discrete probability distribution. This is
+ * sampled using a discrete probability distribution sampler. The builder provides methods
+ * to change the default implementation.
+ *
+ * <p>The following example will create a sampler to uniformly sample the border of a triangle
+ * using the line segment lengths as weights:
+ *
+ * <pre>
+ * UniformRandomProvider rng = RandomSource.KISS.create();
+ * double[] a = {1.23, 4.56};
+ * double[] b = {6.78, 9.01};
+ * double[] c = {3.45, 2.34};
+ * ObjectSampler&lt;double[]&gt; sampler =
+ *     CompositeSamplers.&lt;double[]&gt;newObjectSamplerBuilder()
+ *         .add(LineSampler.of(a, b, rng), Math.hypot(a[0] - b[0], a[1] - b[1]))
+ *         .add(LineSampler.of(b, c, rng), Math.hypot(b[0] - c[0], b[1] - c[1]))
+ *         .add(LineSampler.of(c, a, rng), Math.hypot(c[0] - a[0], c[1] - a[1]))
+ *         .build(rng);
+ * </pre>
+ *
+ * @since 1.4
+ */
+public final class CompositeSamplers {
+    /**
+     * A factory for creating a sampler of a user-defined
+     * <a href="http://en.wikipedia.org/wiki/Probability_distribution#Discrete_probability_distribution">
+     * discrete probability distribution</a>.
+     */
+    public interface DiscreteProbabilitySamplerFactory {
+        /**
+         * Creates the sampler.
+         *
+         * @param rng Source of randomness.
+         * @param probabilities Discrete probability distribution.
+         * @return the sampler
+         */
+        DiscreteSampler create(UniformRandomProvider rng,
+                               double[] probabilities);
+    }
+
+    /**
+     * The DiscreteProbabilitySampler class defines implementations that sample from a user-defined
+     * <a href="http://en.wikipedia.org/wiki/Probability_distribution#Discrete_probability_distribution">
+     * discrete probability distribution</a>.
+     *
+     * <p>All implementations support the {@link SharedStateDiscreteSampler} interface.
+     */
+    public enum DiscreteProbabilitySampler implements DiscreteProbabilitySamplerFactory {
+        /** Sample using a guide table (see {@link GuideTableDiscreteSampler}). */
+        GUIDE_TABLE {
+            @Override
+            public SharedStateDiscreteSampler create(UniformRandomProvider rng, double[] probabilities) {
+                return GuideTableDiscreteSampler.of(rng, probabilities);
+            }
+        },
+        /** Sample using the alias method (see {@link AliasMethodDiscreteSampler}). */
+        ALIAS_METHOD {
+            @Override
+            public SharedStateDiscreteSampler create(UniformRandomProvider rng, double[] probabilities) {
+                return AliasMethodDiscreteSampler.of(rng, probabilities);
+            }
+        },
+        /**
+         * Sample using an optimised look-up table (see
+         * {@link org.apache.commons.rng.sampling.distribution.MarsagliaTsangWangDiscreteSampler.Enumerated
+         * MarsagliaTsangWangDiscreteSampler.Enumerated}).
+         */
+        LOOKUP_TABLE {
+            @Override
+            public SharedStateDiscreteSampler create(UniformRandomProvider rng, double[] probabilities) {
+                return MarsagliaTsangWangDiscreteSampler.Enumerated.of(rng, probabilities);
+            }
+        };
+    }
+
+    /**
+     * A class to implement the SharedStateDiscreteSampler interface for a discrete probability
+     * sampler given a factory and the probability distribution. Each new instance will recreate
+     * the distribution sampler using the factory.
+     */
+    private static class SharedStateDiscreteProbabilitySampler implements SharedStateDiscreteSampler {
+        /** The sampler. */
+        private final DiscreteSampler sampler;
+        /** The factory to create a new discrete sampler. */
+        private final DiscreteProbabilitySamplerFactory factory;
+        /** The probabilities. */
+        private final double[] probabilities;
+
+        /**
+         * @param sampler Sampler of the discrete distribution.
+         * @param factory Factory to create a new discrete sampler.
+         * @param probabilities Probabilities of the discrete distribution.
+         * @throws NullPointerException if the {@code sampler} is null
+         */
+        SharedStateDiscreteProbabilitySampler(DiscreteSampler sampler,
+                                              DiscreteProbabilitySamplerFactory factory,
+                                              double[] probabilities) {
+            this.sampler = requireNonNull(sampler, "discrete sampler");
+            // Assume the factory and probabilities are not null
+            this.factory = factory;
+            this.probabilities = probabilities;
+        }
+
+        @Override
+        public int sample() {
+            // Delegate
+            return sampler.sample();
+        }
+
+        @Override
+        public SharedStateDiscreteSampler withUniformRandomProvider(UniformRandomProvider rng) {
+            // The factory may destructively modify the probabilities
+            return new SharedStateDiscreteProbabilitySampler(factory.create(rng, probabilities.clone()),
+                                                             factory, probabilities);
+        }
+    }
+
+    /**
+     * Builds a composite sampler.
+     *
+     * <p>A composite sampler is a combination of multiple samplers
+     * that all return the same sample type. Each sampler has a weighting in the composition.
+     * Samples are returned using a 2 step algorithm:
+     *
+     * <ol>
+     *  <li>Select a sampler based on its weighting
+     *  <li>Return a sample from the selected sampler
+     * </ol>
+     *
+     * <p>Step 1 requires a discrete sampler constructed from a discrete probability distribution.
+     * The probability for each sampler is the sampler weight divided by the sum of the weights:
+     * <pre>
+     * p(i) = w(i) / sum(w)
+     * </pre>
+     *
+     * <p>The builder provides a method to set the factory used to generate the discrete sampler.
+     *
+     * @param <S> Type of sampler
+     */
+    public interface Builder<S> {
+        /**
+         * Return the number of samplers in the composite. The size must be non-zero before
+         * the {@link #build(UniformRandomProvider) build} method can create a sampler.
+         *
+         * @return the size
+         */
+        int size();
+
+        /**
+         * Adds the sampler to the composite. A sampler with a zero weight is ignored.
+         *
+         * @param sampler Sampler.
+         * @param weight Weight for the composition.
+         * @return a reference to this builder
+         * @throws IllegalArgumentException if {@code weight} is negative, infinite or {@code NaN}.
+         * @throws NullPointerException if {@code sampler} is null.
+         */
+        Builder<S> add(S sampler, double weight);
+
+        /**
+         * Sets the factory to use to generate the composite's discrete sampler from the sampler
+         * weights.
+         *
+         * <p>Note: If the factory is not explicitly set then a default will be used.
+         *
+         * @param factory Factory.
+         * @return a reference to this builder
+         * @throws NullPointerException if {@code factory} is null.
+         */
+        Builder<S> setFactory(DiscreteProbabilitySamplerFactory factory);
+
+        /**
+         * Builds the composite sampler. The {@code rng} is the source of randomness for selecting
+         * which sampler to use for each sample.
+         *
+         * <p>Note: When the sampler is created the builder is reset to an empty state.
+         * This prevents building multiple composite samplers with the same samplers and
+         * their identical underlying source of randomness.
+         *
+         * @param rng Generator of uniformly distributed random numbers.
+         * @return the sampler
+         * @throws IllegalStateException if no samplers have been added to create a composite.
+         * @see #size()
+         */
+        S build(UniformRandomProvider rng);
+    }
+
+    /**
+     * Builds a composite sampler.
+     *
+     * <p>A single builder can be used to create composites of different implementing classes
+     * which support different sampler interfaces. The type of sampler is generic. The individual
+     * samplers and their weights can be collected by the builder. The build method creates
+     * the discrete probability distribution from the weights. The final composite is created
+     * using a factory to create the class.
+     *
+     * @param <S> Type of sampler
+     */
+    private static class SamplerBuilder<S> implements Builder<S> {
+        /** The specialisation of the sampler. */
+        private final Specialisation specialisation;
+        /** The weighted samplers. */
+        private final List<WeightedSampler<S>> weightedSamplers;
+        /** The factory to create the discrete probability sampler from the weights. */
+        private DiscreteProbabilitySamplerFactory factory;
+        /** The factory to create the composite sampler. */
+        private final SamplerFactory<S> compositeFactory;
+
+        /**
+         * The specialisation of composite sampler to build.
+         * This is used to determine if specialised interfaces from the sampler
+         * type must be supported, e.g. {@link SharedStateSampler}.
+         */
+        enum Specialisation {
+            /** Instance of {@link SharedStateSampler}. */
+            SHARED_STATE_SAMPLER,
+            /** No specialisation. */
+            NONE;
+        }
+
+        /**
+         * A factory for creating composite samplers.
+         *
+         * <p>This interface is used to build concrete implementations
+         * of different sampler interfaces.
+         *
+         * @param <S> Type of sampler
+         */
+        interface SamplerFactory<S> {
+            /**
+             * Creates a new composite sampler.
+             *
+             * <p>If the composite specialisation is a
+             * {@link Specialisation#SHARED_STATE_SAMPLER shared state sampler}
+             * the discrete sampler passed to this method will be an instance of
+             * {@link SharedStateDiscreteSampler}.
+             *
+             * @param discreteSampler Discrete sampler.
+             * @param samplers Samplers.
+             * @return the sampler
+             */
+            S createSampler(DiscreteSampler discreteSampler,
+                            List<S> samplers);
+        }
+
+        /**
+         * Contains a weighted sampler.
+         *
+         * @param <S> Sampler type
+         */
+        private static class WeightedSampler<S> {
+            /** The weight. */
+            private final double weight;
+            /** The sampler. */
+            private final S sampler;
+
+            /**
+             * @param weight the weight
+             * @param sampler the sampler
+             * @throws IllegalArgumentException if {@code weight} is negative, infinite or {@code NaN}.
+             * @throws NullPointerException if {@code sampler} is null.
+             */
+            WeightedSampler(double weight, S sampler) {
+                this.weight = requirePositiveFinite(weight, "weight");
+                this.sampler = requireNonNull(sampler, "sampler");
+            }
+
+            /**
+             * Gets the weight.
+             *
+             * @return the weight
+             */
+            double getWeight() {
+                return weight;
+            }
+
+            /**
+             * Gets the sampler.
+             *
+             * @return the sampler
+             */
+            S getSampler() {
+                return sampler;
+            }
+
+            /**
+             * Checks that the specified value is positive finite and throws a customized
+             * {@link IllegalArgumentException} if it is not.
+             *
+             * @param value the value
+             * @param message detail message to be used in the event that a {@code
+             *                IllegalArgumentException} is thrown
+             * @return {@code value} if positive finite
+             * @throws IllegalArgumentException if {@code weight} is negative, infinite or {@code NaN}.
+             */
+            private static double requirePositiveFinite(double value, String message) {
+                // Must be positive finite
+                if (!(value >= 0 && value < Double.POSITIVE_INFINITY)) {
+                    throw new IllegalArgumentException(message + " is not positive finite: " + value);
+                }
+                return value;
+            }
+        }
+
+        /**
+         * @param specialisation Specialisation of the sampler.
+         * @param compositeFactory Factory to create the final composite sampler.
+         */
+        SamplerBuilder(Specialisation specialisation,
+                       SamplerFactory<S> compositeFactory) {
+            this.specialisation = specialisation;
+            this.compositeFactory = compositeFactory;
+            weightedSamplers = new ArrayList<WeightedSampler<S>>();
+            factory = DiscreteProbabilitySampler.GUIDE_TABLE;
+        }
+
+        @Override
+        public int size() {
+            return weightedSamplers.size();
+        }
+
+        @Override
+        public Builder<S> add(S sampler, double weight) {
+            // Ignore zero weights. The sampler and weight are validated by the WeightedSampler.
+            if (weight != 0) {
+                weightedSamplers.add(new WeightedSampler<S>(weight, sampler));
+            }
+            return this;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * <p>If the weights are uniform the factory is ignored and composite's discrete sampler
+         * is a {@link DiscreteUniformSampler uniform distribution sampler}.
+         */
+        @Override
+        public Builder<S> setFactory(DiscreteProbabilitySamplerFactory samplerFactory) {
+            this.factory = requireNonNull(samplerFactory, "factory");
+            return this;
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * <p>If only one sampler has been added to the builder then the sampler is returned
+         * and the builder is reset.
+         *
+         * @throws IllegalStateException if no samplers have been added to create a composite.
+         */
+        @Override
+        public S build(UniformRandomProvider rng) {
+            final List<WeightedSampler<S>> list = this.weightedSamplers;
+            final int n = list.size();
+            if (n == 0) {
+                throw new IllegalStateException("No samplers to build the composite");
+            }
+            if (n == 1) {
+                // No composite
+                final S sampler = list.get(0).sampler;
+                reset();
+                return sampler;
+            }
+
+            // Extract the weights and samplers.
+            final double[] weights = new double[n];
+            final ArrayList<S> samplers = new ArrayList<S>(n);
+            for (int i = 0; i < n; i++) {
+                final WeightedSampler<S> weightedItem = list.get(i);
+                weights[i] = weightedItem.getWeight();
+                samplers.add(weightedItem.getSampler());
+            }
+
+            reset();
+
+            final DiscreteSampler discreteSampler = createDiscreteSampler(rng, weights);
+
+            return compositeFactory.createSampler(discreteSampler, samplers);
+        }
+
+        /**
+         * Reset the builder.
+         */
+        private void reset() {
+            weightedSamplers.clear();
+        }
+
+        /**
+         * Creates the discrete sampler of the enumerated probability distribution.
+         *
+         * <p>If the specialisation is a {@link Specialisation#SHARED_STATE_SAMPLER shared state sampler}
+         * the discrete sampler will be an instance of {@link SharedStateDiscreteSampler}.
+         *
+         * @param rng Generator of uniformly distributed random numbers.
+         * @param weights Weight associated to each item.
+         * @return the sampler
+         */
+        private DiscreteSampler createDiscreteSampler(UniformRandomProvider rng,
+                                                      double[] weights) {
+            // Edge case. Detect uniform weights.
+            final int n = weights.length;
+            if (uniform(weights)) {
+                // Uniformly sample from the size.
+                // Note: Upper bound is inclusive.
+                return DiscreteUniformSampler.of(rng, 0, n - 1);
+            }
+
+            // If possible normalise with a simple sum.
+            final double sum = sum(weights);
+            if (sum < Double.POSITIVE_INFINITY) {
+                // Do not use f = 1.0 / sum and multiplication by f.
+                // Use of divide handles a sub-normal sum.
+                for (int i = 0; i < n; i++) {
+                    weights[i] /= sum;
+                }
+            } else {
+                // The sum is not finite. We know the weights are all positive finite.
+                // Compute the mean without overflow and divide by the mean and number of items.
+                final double mean = mean(weights);
+                for (int i = 0; i < n; i++) {
+                    // Two step division avoids using the denominator (mean * n)
+                    weights[i] = weights[i] / mean / n;
+                }
+            }
+
+            // Create the sampler from the factory.
+            // Check if a SharedStateSampler is required.
+            // If a default factory then the result is a SharedStateDiscreteSampler,
+            // otherwise the sampler must be checked.
+            if (specialisation == Specialisation.SHARED_STATE_SAMPLER &&
+                !(factory instanceof DiscreteProbabilitySampler)) {
+                // If the factory was user defined then clone the weights as they may be required
+                // to create a SharedStateDiscreteProbabilitySampler.
+                final DiscreteSampler sampler = factory.create(rng, weights.clone());
+                return sampler instanceof SharedStateDiscreteSampler ?
+                     sampler :
+                     new SharedStateDiscreteProbabilitySampler(sampler, factory, weights);
+            }
+
+            return factory.create(rng, weights);
+        }
+
+        /**
+         * Check if all the values are the same.
+         *
+         * <p>Warning: This method assumes there are input values. If the length is zero an
+         * {@link ArrayIndexOutOfBoundsException} will be thrown.
+         *
+         * @param values the values
+         * @return true if all values are the same
+         */
+        private static boolean uniform(double[] values) {
+            final double value = values[0];
+            for (int i = 1; i < values.length; i++) {
+                if (value != values[i]) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Compute the sum of the values.
+         *
+         * @param values the values
+         * @return the sum
+         */
+        private static double sum(double[] values) {
+            double sum = 0;
+            for (final double value : values) {
+                sum += value;
+            }
+            return sum;
+        }
+
+        /**
+         * Compute the mean of the values. Uses a rolling algorithm to avoid overflow of a simple sum.
+         * This method can be used to compute the mean of observed counts for normalisation to a
+         * probability:
+         *
+         * <pre>
+         * double[] values = ...;
+         * int n = values.length;
+         * double mean = mean(values);
+         * for (int i = 0; i &lt; n; i++) {
+         *     // Two step division avoids using the denominator (mean * n)
+         *     values[i] = values[i] / mean / n;
+         * }
+         * </pre>
+         *
+         * <p>Warning: This method assumes there are input values. If the length is zero an
+         * {@link ArrayIndexOutOfBoundsException} will be thrown.
+         *
+         * @param values the values
+         * @return the mean
+         */
+        private static double mean(double[] values) {
+            double mean = values[0];
+            int i = 1;
+            while (i < values.length) {
+                // Deviation from the mean
+                final double dev = values[i] - mean;
+                i++;
+                mean += dev / i;
+            }
+            return mean;
+        }
+    }
+
+    /**
+     * A composite sampler.
+     *
+     * <p>The source sampler for each sampler is chosen based on a user-defined continuous
+     * probability distribution.
+     *
+     * @param <S> Type of sampler
+     */
+    private static class CompositeSampler<S> {
+        /** Continuous sampler to choose the individual sampler to sample. */
+        protected final DiscreteSampler discreteSampler;
+        /** Collection of samplers to be sampled from. */
+        protected final List<S> samplers;
+
+        /**
+         * @param discreteSampler Continuous sampler to choose the individual sampler to sample.
+         * @param samplers Collection of samplers to be sampled from.
+         */
+        CompositeSampler(DiscreteSampler discreteSampler,
+                         List<S> samplers) {
+            this.discreteSampler = discreteSampler;
+            this.samplers = samplers;
+        }
+
+        /**
+         * Gets the next sampler to use to create a sample.
+         *
+         * @return the sampler
+         */
+        S nextSampler() {
+            return samplers.get(discreteSampler.sample());
+        }
+    }
+
+    /**
+     * A factory for creating a composite ObjectSampler.
+     *
+     * @param <T> Type of sample
+     */
+    private static class ObjectSamplerFactory<T> implements
+            SamplerBuilder.SamplerFactory<ObjectSampler<T>> {
+        /** The instance. */
+        @SuppressWarnings("rawtypes")
+        private static final ObjectSamplerFactory INSTANCE = new ObjectSamplerFactory();
+
+        /**
+         * Get an instance.
+         *
+         * @param <T> Type of sample
+         * @return the factory
+         */
+        static <T> ObjectSamplerFactory<T> instance() {
+            return INSTANCE;
+        }
+
+        @Override
+        public ObjectSampler<T> createSampler(DiscreteSampler discreteSampler,
+                                              List<ObjectSampler<T>> samplers) {
+            return new CompositeObjectSampler<T>(discreteSampler, samplers);
+        }
+
+        /**
+         * A composite object sampler.
+         *
+         * @param <T> Type of sample
+         */
+        private static class CompositeObjectSampler<T>
+                extends CompositeSampler<ObjectSampler<T>>
+                implements ObjectSampler<T> {
+            /**
+             * @param discreteSampler Discrete sampler to choose the individual sampler to sample.
+             * @param samplers Collection of samplers to be sampled from.
+             */
+            CompositeObjectSampler(DiscreteSampler discreteSampler,
+                                   List<ObjectSampler<T>> samplers) {
+                super(discreteSampler, samplers);
+            }
+
+            @Override
+            public T sample() {
+                return nextSampler().sample();
+            }
+        }
+    }
+
+    /**
+     * A factory for creating a composite SharedStateObjectSampler.
+     *
+     * @param <T> Type of sample
+     */
+    private static class SharedStateObjectSamplerFactory<T> implements
+            SamplerBuilder.SamplerFactory<SharedStateObjectSampler<T>> {
+        /** The instance. */
+        @SuppressWarnings("rawtypes")
+        private static final SharedStateObjectSamplerFactory INSTANCE = new SharedStateObjectSamplerFactory();
+
+        /**
+         * Get an instance.
+         *
+         * @param <T> Type of sample
+         * @return the factory
+         */
+        static <T> SharedStateObjectSamplerFactory<T> instance() {
+            return INSTANCE;
+        }
+
+        @Override
+        public SharedStateObjectSampler<T> createSampler(DiscreteSampler discreteSampler,
+                                                         List<SharedStateObjectSampler<T>> samplers) {
+            // The input discrete sampler is assumed to be a SharedStateDiscreteSampler
+            return new CompositeSharedStateObjectSampler<T>(
+                (SharedStateDiscreteSampler) discreteSampler, samplers);
+        }
+
+        /**
+         * A composite object sampler with shared state support.
+         *
+         * <p>The source sampler for each sampler is chosen based on a user-defined
+         * discrete probability distribution.
+         *
+         * @param <T> Type of sample
+         */
+        private static class CompositeSharedStateObjectSampler<T>
+                extends CompositeSampler<SharedStateObjectSampler<T>>
+                implements SharedStateObjectSampler<T> {
+            /**
+             * @param discreteSampler Discrete sampler to choose the individual sampler to sample.
+             * @param samplers Collection of samplers to be sampled from.
+             */
+            CompositeSharedStateObjectSampler(SharedStateDiscreteSampler discreteSampler,
+                                              List<SharedStateObjectSampler<T>> samplers) {
+                super(discreteSampler, samplers);
+            }
+
+            @Override
+            public T sample() {
+                return nextSampler().sample();
+            }
+
+            @Override
+            public CompositeSharedStateObjectSampler<T> withUniformRandomProvider(UniformRandomProvider rng) {
+                // Duplicate each sampler with the same source of randomness
+                return new CompositeSharedStateObjectSampler<T>(
+                    ((SharedStateDiscreteSampler) this.discreteSampler).withUniformRandomProvider(rng),
+                    copy(samplers, rng));
+            }
+        }
+    }
+
+    /**
+     * A factory for creating a composite DiscreteSampler.
+     */
+    private static class DiscreteSamplerFactory implements
+            SamplerBuilder.SamplerFactory<DiscreteSampler> {
+        /** The instance. */
+        static final DiscreteSamplerFactory INSTANCE = new DiscreteSamplerFactory();
+
+        @Override
+        public DiscreteSampler createSampler(DiscreteSampler discreteSampler,
+                                             List<DiscreteSampler> samplers) {
+            return new CompositeDiscreteSampler(discreteSampler, samplers);
+        }
+
+        /**
+         * A composite discrete sampler.
+         */
+        private static class CompositeDiscreteSampler
+                extends CompositeSampler<DiscreteSampler>
+                implements DiscreteSampler {
+            /**
+             * @param discreteSampler Discrete sampler to choose the individual sampler to sample.
+             * @param samplers Collection of samplers to be sampled from.
+             */
+            CompositeDiscreteSampler(DiscreteSampler discreteSampler,
+                                     List<DiscreteSampler> samplers) {
+                super(discreteSampler, samplers);
+            }
+
+            @Override
+            public int sample() {
+                return nextSampler().sample();
+            }
+        }
+    }
+
+    /**
+     * A factory for creating a composite SharedStateDiscreteSampler.
+     */
+    private static class SharedStateDiscreteSamplerFactory implements
+            SamplerBuilder.SamplerFactory<SharedStateDiscreteSampler> {
+        /** The instance. */
+        static final SharedStateDiscreteSamplerFactory INSTANCE = new SharedStateDiscreteSamplerFactory();
+
+        @Override
+        public SharedStateDiscreteSampler createSampler(DiscreteSampler discreteSampler,
+                                                        List<SharedStateDiscreteSampler> samplers) {
+            // The input discrete sampler is assumed to be a SharedStateDiscreteSampler
+            return new CompositeSharedStateDiscreteSampler(
+                (SharedStateDiscreteSampler) discreteSampler, samplers);
+        }
+
+        /**
+         * A composite discrete sampler with shared state support.
+         */
+        private static class CompositeSharedStateDiscreteSampler
+                extends CompositeSampler<SharedStateDiscreteSampler>
+                implements SharedStateDiscreteSampler {
+            /**
+             * @param discreteSampler Discrete sampler to choose the individual sampler to sample.
+             * @param samplers Collection of samplers to be sampled from.
+             */
+            CompositeSharedStateDiscreteSampler(SharedStateDiscreteSampler discreteSampler,
+                                                List<SharedStateDiscreteSampler> samplers) {
+                super(discreteSampler, samplers);
+            }
+
+            @Override
+            public int sample() {
+                return nextSampler().sample();
+            }
+
+            @Override
+            public CompositeSharedStateDiscreteSampler withUniformRandomProvider(UniformRandomProvider rng) {
+                // Duplicate each sampler with the same source of randomness
+                return new CompositeSharedStateDiscreteSampler(
+                    ((SharedStateDiscreteSampler) this.discreteSampler).withUniformRandomProvider(rng),
+                    copy(samplers, rng));
+            }
+        }
+    }
+
+    /**
+     * A factory for creating a composite ContinuousSampler.
+     */
+    private static class ContinuousSamplerFactory implements
+            SamplerBuilder.SamplerFactory<ContinuousSampler> {
+        /** The instance. */
+        static final ContinuousSamplerFactory INSTANCE = new ContinuousSamplerFactory();
+
+        @Override
+        public ContinuousSampler createSampler(DiscreteSampler discreteSampler,
+                                               List<ContinuousSampler> samplers) {
+            return new CompositeContinuousSampler(discreteSampler, samplers);
+        }
+
+        /**
+         * A composite continuous sampler.
+         */
+        private static class CompositeContinuousSampler
+                extends CompositeSampler<ContinuousSampler>
+                implements ContinuousSampler {
+            /**
+             * @param discreteSampler Continuous sampler to choose the individual sampler to sample.
+             * @param samplers Collection of samplers to be sampled from.
+             */
+            CompositeContinuousSampler(DiscreteSampler discreteSampler,
+                                       List<ContinuousSampler> samplers) {
+                super(discreteSampler, samplers);
+            }
+
+            @Override
+            public double sample() {
+                return nextSampler().sample();
+            }
+        }
+    }
+
+    /**
+     * A factory for creating a composite SharedStateContinuousSampler.
+     */
+    private static class SharedStateContinuousSamplerFactory implements
+            SamplerBuilder.SamplerFactory<SharedStateContinuousSampler> {
+        /** The instance. */
+        static final SharedStateContinuousSamplerFactory INSTANCE = new SharedStateContinuousSamplerFactory();
+
+        @Override
+        public SharedStateContinuousSampler createSampler(DiscreteSampler discreteSampler,
+                                                          List<SharedStateContinuousSampler> samplers) {
+            // The sampler is assumed to be a SharedStateContinuousSampler
+            return new CompositeSharedStateContinuousSampler(
+                (SharedStateDiscreteSampler) discreteSampler, samplers);
+        }
+
+        /**
+         * A composite continuous sampler with shared state support.
+         */
+        private static class CompositeSharedStateContinuousSampler
+                extends CompositeSampler<SharedStateContinuousSampler>
+                implements SharedStateContinuousSampler {
+            /**
+             * @param discreteSampler Continuous sampler to choose the individual sampler to sample.
+             * @param samplers Collection of samplers to be sampled from.
+             */
+            CompositeSharedStateContinuousSampler(SharedStateDiscreteSampler discreteSampler,
+                                                  List<SharedStateContinuousSampler> samplers) {
+                super(discreteSampler, samplers);
+            }
+
+            @Override
+            public double sample() {
+                return nextSampler().sample();
+            }
+
+            @Override
+            public CompositeSharedStateContinuousSampler withUniformRandomProvider(UniformRandomProvider rng) {
+                // Duplicate each sampler with the same source of randomness
+                return new CompositeSharedStateContinuousSampler(
+                    ((SharedStateDiscreteSampler) this.discreteSampler).withUniformRandomProvider(rng),
+                    copy(samplers, rng));
+            }
+        }
+    }
+
+    /**
+     * A factory for creating a composite LongSampler.
+     */
+    private static class LongSamplerFactory implements
+            SamplerBuilder.SamplerFactory<LongSampler> {
+        /** The instance. */
+        static final LongSamplerFactory INSTANCE = new LongSamplerFactory();
+
+        @Override
+        public LongSampler createSampler(DiscreteSampler discreteSampler,
+                                         List<LongSampler> samplers) {
+            return new CompositeLongSampler(discreteSampler, samplers);
+        }
+
+        /**
+         * A composite long sampler.
+         */
+        private static class CompositeLongSampler
+                extends CompositeSampler<LongSampler>
+                implements LongSampler {
+            /**
+             * @param discreteSampler Long sampler to choose the individual sampler to sample.
+             * @param samplers Collection of samplers to be sampled from.
+             */
+            CompositeLongSampler(DiscreteSampler discreteSampler,
+                                 List<LongSampler> samplers) {
+                super(discreteSampler, samplers);
+            }
+
+            @Override
+            public long sample() {
+                return nextSampler().sample();
+            }
+        }
+    }
+
+    /**
+     * A factory for creating a composite SharedStateLongSampler.
+     */
+    private static class SharedStateLongSamplerFactory implements
+            SamplerBuilder.SamplerFactory<SharedStateLongSampler> {
+        /** The instance. */
+        static final SharedStateLongSamplerFactory INSTANCE = new SharedStateLongSamplerFactory();
+
+        @Override
+        public SharedStateLongSampler createSampler(DiscreteSampler discreteSampler,
+                                                    List<SharedStateLongSampler> samplers) {
+            // The input discrete sampler is assumed to be a SharedStateLongSampler
+            return new CompositeSharedStateLongSampler(
+                (SharedStateDiscreteSampler) discreteSampler, samplers);
+        }
+
+        /**
+         * A composite long sampler with shared state support.
+         */
+        private static class CompositeSharedStateLongSampler
+                extends CompositeSampler<SharedStateLongSampler>
+                implements SharedStateLongSampler {
+            /**
+             * @param discreteSampler Long sampler to choose the individual sampler to sample.
+             * @param samplers Collection of samplers to be sampled from.
+             */
+            CompositeSharedStateLongSampler(SharedStateDiscreteSampler discreteSampler,
+                                            List<SharedStateLongSampler> samplers) {
+                super(discreteSampler, samplers);
+            }
+
+            @Override
+            public long sample() {
+                return nextSampler().sample();
+            }
+
+            @Override
+            public CompositeSharedStateLongSampler withUniformRandomProvider(UniformRandomProvider rng) {
+                // Duplicate each sampler with the same source of randomness
+                return new CompositeSharedStateLongSampler(
+                    ((SharedStateDiscreteSampler) this.discreteSampler).withUniformRandomProvider(rng),
+                    copy(samplers, rng));
+            }
+        }
+    }
+
+    /** No public instances. */
+    private CompositeSamplers() {}
+
+    /**
+     * Create a new builder for a composite {@link ObjectSampler}.
+     *
+     * <p>Note: If the compiler cannot infer the type parameter of the sampler it can be specified
+     * within the diamond operator {@code <T>} preceding the call to
+     * {@code newObjectSamplerBuilder()}, for example:
+     *
+     * <pre>{@code
+     * CompositeSamplers.<double[]>newObjectSamplerBuilder()
+     * }</pre>
+     *
+     * @param <T> Type of the sample.
+     * @return the builder
+     */
+    public static <T> Builder<ObjectSampler<T>> newObjectSamplerBuilder() {
+        final SamplerBuilder.SamplerFactory<ObjectSampler<T>> factory = ObjectSamplerFactory.instance();
+        return new SamplerBuilder<ObjectSampler<T>>(
+            SamplerBuilder.Specialisation.NONE, factory);
+    }
+
+    /**
+     * Create a new builder for a composite {@link SharedStateObjectSampler}.
+     *
+     * <p>Note: If the compiler cannot infer the type parameter of the sampler it can be specified
+     * within the diamond operator {@code <T>} preceding the call to
+     * {@code newSharedStateObjectSamplerBuilder()}, for example:
+     *
+     * <pre>{@code
+     * CompositeSamplers.<double[]>newSharedStateObjectSamplerBuilder()
+     * }</pre>
+     *
+     * @param <T> Type of the sample.
+     * @return the builder
+     */
+    public static <T> Builder<SharedStateObjectSampler<T>> newSharedStateObjectSamplerBuilder() {
+        final SamplerBuilder.SamplerFactory<SharedStateObjectSampler<T>> factory =
+            SharedStateObjectSamplerFactory.instance();
+        return new SamplerBuilder<SharedStateObjectSampler<T>>(
+            SamplerBuilder.Specialisation.SHARED_STATE_SAMPLER, factory);
+    }
+
+    /**
+     * Create a new builder for a composite {@link DiscreteSampler}.
+     *
+     * @return the builder
+     */
+    public static Builder<DiscreteSampler> newDiscreteSamplerBuilder() {
+        return new SamplerBuilder<DiscreteSampler>(
+            SamplerBuilder.Specialisation.NONE, DiscreteSamplerFactory.INSTANCE);
+    }
+
+    /**
+     * Create a new builder for a composite {@link SharedStateDiscreteSampler}.
+     *
+     * @return the builder
+     */
+    public static Builder<SharedStateDiscreteSampler> newSharedStateDiscreteSamplerBuilder() {
+        return new SamplerBuilder<SharedStateDiscreteSampler>(
+            SamplerBuilder.Specialisation.SHARED_STATE_SAMPLER, SharedStateDiscreteSamplerFactory.INSTANCE);
+    }
+
+    /**
+     * Create a new builder for a composite {@link ContinuousSampler}.
+     *
+     * @return the builder
+     */
+    public static Builder<ContinuousSampler> newContinuousSamplerBuilder() {
+        return new SamplerBuilder<ContinuousSampler>(
+            SamplerBuilder.Specialisation.NONE, ContinuousSamplerFactory.INSTANCE);
+    }
+
+    /**
+     * Create a new builder for a composite {@link SharedStateContinuousSampler}.
+     *
+     * @return the builder
+     */
+    public static Builder<SharedStateContinuousSampler> newSharedStateContinuousSamplerBuilder() {
+        return new SamplerBuilder<SharedStateContinuousSampler>(
+            SamplerBuilder.Specialisation.SHARED_STATE_SAMPLER, SharedStateContinuousSamplerFactory.INSTANCE);
+    }
+
+    /**
+     * Create a new builder for a composite {@link LongSampler}.
+     *
+     * @return the builder
+     */
+    public static Builder<LongSampler> newLongSamplerBuilder() {
+        return new SamplerBuilder<LongSampler>(
+            SamplerBuilder.Specialisation.NONE, LongSamplerFactory.INSTANCE);
+    }
+
+    /**
+     * Create a new builder for a composite {@link SharedStateLongSampler}.
+     *
+     * @return the builder
+     */
+    public static Builder<SharedStateLongSampler> newSharedStateLongSamplerBuilder() {
+        return new SamplerBuilder<SharedStateLongSampler>(
+            SamplerBuilder.Specialisation.SHARED_STATE_SAMPLER, SharedStateLongSamplerFactory.INSTANCE);
+    }
+
+    /**
+     * Checks that the specified object reference is not {@code null} and throws a
+     * customized {@link NullPointerException} if it is.
+     *
+     * <P>Note: This method is to be replaced with
+     * {@code java.util.Objects.requireNonNull} when the source requires Java 8.
+     *
+     * @param obj the object reference to check for nullity
+     * @param message detail message to be used in the event that a {@code
+     *                NullPointerException} is thrown
+     * @param <T> the type of the reference
+     * @return {@code obj} if not {@code null}
+     * @throws NullPointerException if {@code obj} is {@code null}
+     */
+    private static <T> T requireNonNull(T obj, String message) {
+        if (obj == null) {
+            throw new NullPointerException(message);
+        }
+        return obj;
+    }
+
+    /**
+     * Create a copy instance of each sampler in the list of samplers using the given
+     * uniform random provider as the source of randomness.
+     *
+     * @param <T> the type of sampler
+     * @param samplers Source to copy.
+     * @param rng Generator of uniformly distributed random numbers.
+     * @return the copy
+     */
+    private static <T extends SharedStateSampler<T>> List<T> copy(List<T> samplers,
+                                                                  UniformRandomProvider rng) {
+        final ArrayList<T> newSamplers = new ArrayList<T>(samplers.size());
+        for (final T s : samplers) {
+            newSamplers.add(s.withUniformRandomProvider(rng));
+        }
+        return newSamplers;
+    }
+}
diff --git a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/CompositeSamplersTest.java b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/CompositeSamplersTest.java
new file mode 100644
index 0000000..2081590
--- /dev/null
+++ b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/CompositeSamplersTest.java
@@ -0,0 +1,1004 @@
+/*
+ * 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;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.apache.commons.math3.stat.inference.ChiSquareTest;
+import org.apache.commons.rng.UniformRandomProvider;
+import org.apache.commons.rng.sampling.CompositeSamplers.Builder;
+import org.apache.commons.rng.sampling.CompositeSamplers.DiscreteProbabilitySampler;
+import org.apache.commons.rng.sampling.CompositeSamplers.DiscreteProbabilitySamplerFactory;
+import org.apache.commons.rng.sampling.distribution.AliasMethodDiscreteSampler;
+import org.apache.commons.rng.sampling.distribution.ContinuousSampler;
+import org.apache.commons.rng.sampling.distribution.DiscreteSampler;
+import org.apache.commons.rng.sampling.distribution.GuideTableDiscreteSampler;
+import org.apache.commons.rng.sampling.distribution.LongSampler;
+import org.apache.commons.rng.sampling.distribution.SharedStateContinuousSampler;
+import org.apache.commons.rng.sampling.distribution.SharedStateDiscreteSampler;
+import org.apache.commons.rng.sampling.distribution.SharedStateLongSampler;
+import org.apache.commons.rng.simple.RandomSource;
+
+/**
+ * Test class for {@link CompositeSamplers}.
+ */
+public class CompositeSamplersTest {
+    /**
+     * Test the default implementations of the discrete probability sampler factory.
+     */
+    @Test
+    public void testDiscreteProbabilitySampler() {
+        final UniformRandomProvider rng = RandomSource.MWC_256.create(78979L);
+        final double[] probabilities = {0.1, 0.2, 0.3, 0.4};
+        final double mean = 0.2 + 2 * 0.3 + 3 * 0.4;
+        final int n = 1000000;
+        for (final DiscreteProbabilitySampler item : DiscreteProbabilitySampler.values()) {
+            final DiscreteSampler sampler = item.create(rng, probabilities.clone());
+            long sum = 0;
+            for (int i = 0; i < n; i++) {
+                sum += sampler.sample();
+            }
+            Assert.assertEquals(item.name(), mean, (double) sum / n, 1e-3);
+        }
+    }
+
+    /**
+     * Test an empty builder cannot build a sampler.
+     */
+    @Test(expected = IllegalStateException.class)
+    public void testEmptyBuilderThrows() {
+        final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+        final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
+                .newSharedStateObjectSamplerBuilder();
+        Assert.assertEquals(0, builder.size());
+        builder.build(rng);
+    }
+
+    /**
+     * Test adding null sampler to a builder.
+     */
+    @Test(expected = NullPointerException.class)
+    public void testNullSharedStateObjectSamplerThrows() {
+        final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
+                .newSharedStateObjectSamplerBuilder();
+        builder.add(null, 1.0);
+    }
+
+    /**
+     * Test invalid weights (zero, negative, NaN, infinte).
+     */
+    @Test
+    public void testInvalidWeights() {
+        final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+        final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
+                .newSharedStateObjectSamplerBuilder();
+        final RangeSampler sampler = new RangeSampler(45, 63, rng);
+        // Zero weight is ignored
+        Assert.assertEquals(0, builder.size());
+        builder.add(sampler, 0.0);
+        Assert.assertEquals(0, builder.size());
+
+        final double[] bad = {-1, Double.NaN, Double.POSITIVE_INFINITY};
+        for (final double weight : bad) {
+            try {
+                builder.add(sampler, weight);
+                Assert.fail("Did not detect invalid weight: " + weight);
+            } catch (final IllegalArgumentException ex) {
+                // Expected
+            }
+        }
+    }
+
+    /**
+     * Test a single sampler added to the builder is returned without a composite.
+     */
+    @Test
+    public void testSingleSharedStateObjectSampler() {
+        final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L);
+        final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
+                .newSharedStateObjectSamplerBuilder();
+        final RangeSampler sampler = new RangeSampler(45, 63, rng);
+        builder.add(sampler, 1.0);
+        Assert.assertEquals(1, builder.size());
+        final SharedStateObjectSampler<Integer> composite = builder.build(rng);
+        Assert.assertSame(sampler, composite);
+    }
+
+    /**
+     * Test sampling is uniform across several ObjectSampler samplers.
+     */
+    @Test
+    public void testObjectSamplerSamples() {
+        final Builder<ObjectSampler<Integer>> builder = CompositeSamplers.newObjectSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.PCG_XSH_RR_32_OS.create(345);
+        final int n = 15;
+        final int min = -134;
+        final int max = 2097;
+        addObjectSamplers(builder, n, min, max, rng);
+        assertObjectSamplerSamples(builder.build(rng), min, max);
+    }
+
+    /**
+     * Test sampling is uniform across several SharedStateObjectSampler samplers.
+     */
+    @Test
+    public void testSharedStateObjectSamplerSamples() {
+        final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
+                .newSharedStateObjectSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.PCG_XSH_RS_32_OS.create(299);
+        final int n = 11;
+        final int min = 42;
+        final int max = 678;
+        addObjectSamplers(builder, n, min, max, rng);
+        // Exercise the shared state interface
+        final UniformRandomProvider rng1 = RandomSource.XO_SHI_RO_256_PLUS.create(0x9a8c6f5e);
+        assertObjectSamplerSamples(builder.build(rng).withUniformRandomProvider(rng1), min, max);
+    }
+
+    /**
+     * Test sampling is uniform across several SharedStateObjectSampler samplers
+     * using a custom factory that implements SharedStateDiscreteSampler.
+     */
+    @Test
+    public void testSharedStateObjectSamplerSamplesWithCustomSharedStateDiscreteSamplerFactory() {
+        final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
+                .newSharedStateObjectSamplerBuilder();
+        final AtomicInteger factoryCount = new AtomicInteger();
+        builder.setFactory(new DiscreteProbabilitySamplerFactory() {
+            @Override
+            public DiscreteSampler create(UniformRandomProvider rng, double[] probabilities) {
+                factoryCount.incrementAndGet();
+                // Use an expanded table with a non-default alpha
+                return AliasMethodDiscreteSampler.of(rng, probabilities, 2);
+            }
+        });
+        final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(0xa6b7c9);
+        final int n = 7;
+        final int min = -610;
+        final int max = 745;
+        addObjectSamplers(builder, n, min, max, rng);
+
+        // Exercise the shared state interface
+        final UniformRandomProvider rng1 = RandomSource.XO_SHI_RO_256_PLUS.create(0x1f2e3d);
+        assertObjectSamplerSamples(builder.build(rng).withUniformRandomProvider(rng1), min, max);
+
+        Assert.assertEquals("Factory should not be used to create the shared state sampler", 1, factoryCount.get());
+    }
+
+    /**
+     * Test sampling is uniform across several SharedStateObjectSampler samplers
+     * using a custom factory that implements DiscreteSampler (so must be wrapped).
+     */
+    @Test
+    public void testSharedStateObjectSamplerSamplesWithCustomDiscreteSamplerFactory() {
+        final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
+                .newSharedStateObjectSamplerBuilder();
+        final AtomicInteger factoryCount = new AtomicInteger();
+        builder.setFactory(new DiscreteProbabilitySamplerFactory() {
+            @Override
+            public DiscreteSampler create(UniformRandomProvider rng, double[] probabilities) {
+                factoryCount.incrementAndGet();
+                // Wrap so it is not a SharedStateSamplerInstance.
+                final DiscreteSampler sampler = GuideTableDiscreteSampler.of(rng, probabilities, 2);
+                // Destroy the probabilities to check that custom factories are not trusted.
+                Arrays.fill(probabilities, Double.NaN);
+                return new DiscreteSampler() {
+                    @Override
+                    public int sample() {
+                        return sampler.sample();
+                    }
+                };
+            }
+        });
+        final UniformRandomProvider rng = RandomSource.XO_SHI_RO_128_PP.create(0x263478628L);
+        final int n = 14;
+        final int min = 56;
+        final int max = 2033;
+        addObjectSamplers(builder, n, min, max, rng);
+
+        // Exercise the shared state interface.
+        // This tests the custom factory is used twice.
+        final UniformRandomProvider rng1 = RandomSource.XO_SHI_RO_256_PLUS.create(0x8c7b6a);
+        assertObjectSamplerSamples(builder.build(rng).withUniformRandomProvider(rng1), min, max);
+
+        Assert.assertEquals("Factory should be used to create the shared state sampler", 2, factoryCount.get());
+    }
+
+    /**
+     * Test sampling is uniform across several ObjectSampler samplers with a uniform
+     * weighting. This tests an edge case where there is no requirement for a
+     * sampler from a discrete probability distribution as the distribution is
+     * uniform.
+     */
+    @Test
+    public void testObjectSamplerSamplesWithUniformWeights() {
+        final Builder<ObjectSampler<Integer>> builder = CompositeSamplers.newObjectSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.JSF_64.create(678345);
+        final int max = 60;
+        final int interval = 10;
+        for (int min = 0; min < max; min += interval) {
+            builder.add(new RangeSampler(min, min + interval, rng), 1.0);
+        }
+        assertObjectSamplerSamples(builder.build(rng), 0, max);
+    }
+
+    /**
+     * Test sampling is uniform across several ObjectSampler samplers with very
+     * large weights. This tests an edge case where the weights with sum to
+     * infinity.
+     */
+    @Test
+    public void testObjectSamplerSamplesWithVeryLargeWeights() {
+        final Builder<ObjectSampler<Integer>> builder = CompositeSamplers.newObjectSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.SFC_64.create(267934293);
+        // Ratio 4:4:2:1
+        // The weights will sum to infinity as they are more than 2^1024.
+        final double w4 = 0x1.0p1023;
+        final double w2 = 0x1.0p1022;
+        final double w1 = 0x1.0p1021;
+        Assert.assertEquals(Double.POSITIVE_INFINITY, w4 + w4 + w2 + w1, 0.0);
+        builder.add(new RangeSampler(0, 40, rng), w4);
+        builder.add(new RangeSampler(40, 80, rng), w4);
+        builder.add(new RangeSampler(80, 100, rng), w2);
+        builder.add(new RangeSampler(100, 110, rng), w1);
+        assertObjectSamplerSamples(builder.build(rng), 0, 110);
+    }
+
+    /**
+     * Test sampling is uniform across several ObjectSampler samplers with very
+     * small weights. This tests an edge case where the weights divided by their sum
+     * are valid (due to accurate floating-point division) but cannot be multiplied
+     * by the reciprocal of the sum.
+     */
+    @Test
+    public void testObjectSamplerSamplesWithSubNormalWeights() {
+        final Builder<ObjectSampler<Integer>> builder = CompositeSamplers.newObjectSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.MSWS.create(6786);
+        // Ratio 4:4:2:1
+        // The weights are very small sub-normal numbers
+        final double w4 = Double.MIN_VALUE * 4;
+        final double w2 = Double.MIN_VALUE * 2;
+        final double w1 = Double.MIN_VALUE;
+        final double sum = w4 + w4 + w2 + w1;
+        // Cannot do a divide by multiplying by the reciprocal
+        Assert.assertEquals(Double.POSITIVE_INFINITY, 1.0 / sum, 0.0);
+        // A divide works so the sampler should work
+        Assert.assertEquals(4.0 / 11, w4 / sum, 0.0);
+        Assert.assertEquals(2.0 / 11, w2 / sum, 0.0);
+        Assert.assertEquals(1.0 / 11, w1 / sum, 0.0);
+        builder.add(new RangeSampler(0, 40, rng), w4);
+        builder.add(new RangeSampler(40, 80, rng), w4);
+        builder.add(new RangeSampler(80, 100, rng), w2);
+        builder.add(new RangeSampler(100, 110, rng), w1);
+        assertObjectSamplerSamples(builder.build(rng), 0, 110);
+    }
+
+    /**
+     * Add samplers to the builder that sample from contiguous ranges between the
+     * minimum and maximum. Note: {@code max - min >= n}
+     *
+     * @param builder the builder
+     * @param n the number of samplers (must be {@code >= 2})
+     * @param min the minimum (inclusive)
+     * @param max the maximum (exclusive)
+     * @param rng the source of randomness
+     */
+    private static void addObjectSamplers(Builder<? super SharedStateObjectSampler<Integer>> builder, int n, int min,
+            int max, UniformRandomProvider rng) {
+        // Create the ranges using n-1 random ticks in the range (min, max),
+        // adding the limits and then sorting in ascending order.
+        // The samplers are then constructed:
+        //
+        // min-------A-----B----max
+        // Sampler 1 = [min, A)
+        // Sampler 2 = [A, B)
+        // Sampler 3 = [B, max)
+
+        // Use a combination sampler to ensure the ticks are unique in the range.
+        // This will throw if the range is negative.
+        final int range = max - min - 1;
+        int[] ticks = new CombinationSampler(rng, range, n - 1).sample();
+        // Shift the ticks into the range
+        for (int i = 0; i < ticks.length; i++) {
+            ticks[i] += min + 1;
+        }
+        // Add the min and max
+        ticks = Arrays.copyOf(ticks, n + 1);
+        ticks[n - 1] = min;
+        ticks[n] = max;
+        Arrays.sort(ticks);
+
+        // Sample within the ranges between the ticks
+        final int before = builder.size();
+        for (int i = 1; i < ticks.length; i++) {
+            final RangeSampler sampler = new RangeSampler(ticks[i - 1], ticks[i], rng);
+            // Weight using the range
+            builder.add(sampler, sampler.range);
+        }
+
+        Assert.assertEquals("Failed to add the correct number of samplers", n, builder.size() - before);
+    }
+
+    /**
+     * Assert sampling is uniform between the minimum and maximum.
+     *
+     * @param sampler the sampler
+     * @param min the minimum (inclusive)
+     * @param max the maximum (exclusive)
+     */
+    private static void assertObjectSamplerSamples(ObjectSampler<Integer> sampler, int min, int max) {
+        final int n = 100000;
+        final long[] observed = new long[max - min];
+        for (int i = 0; i < n; i++) {
+            observed[sampler.sample() - min]++;
+        }
+
+        final double[] expected = new double[observed.length];
+        Arrays.fill(expected, (double) n / expected.length);
+        final double p = new ChiSquareTest().chiSquareTest(expected, observed);
+        Assert.assertFalse("p-value too small: " + p, p < 0.001);
+    }
+
+    /**
+     * Test sampling is uniform across several DiscreteSampler samplers.
+     */
+    @Test
+    public void testDiscreteSamplerSamples() {
+        final Builder<DiscreteSampler> builder = CompositeSamplers.newDiscreteSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.PCG_XSH_RR_32_OS.create(345);
+        final int n = 15;
+        final int min = -134;
+        final int max = 2097;
+        addDiscreteSamplers(builder, n, min, max, rng);
+        assertDiscreteSamplerSamples(builder.build(rng), min, max);
+    }
+
+    /**
+     * Test sampling is uniform across several SharedStateDiscreteSampler samplers.
+     */
+    @Test
+    public void testSharedStateDiscreteSamplerSamples() {
+        final Builder<SharedStateDiscreteSampler> builder = CompositeSamplers.newSharedStateDiscreteSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.PCG_XSH_RS_32_OS.create(299);
+        final int n = 11;
+        final int min = 42;
+        final int max = 678;
+        addDiscreteSamplers(builder, n, min, max, rng);
+        assertDiscreteSamplerSamples(builder.build(rng), min, max);
+    }
+
+    /**
+     * Add samplers to the builder that sample from contiguous ranges between the
+     * minimum and maximum. Note: {@code max - min >= n}
+     *
+     * @param builder the builder
+     * @param n the number of samplers (must be {@code >= 2})
+     * @param min the minimum (inclusive)
+     * @param max the maximum (exclusive)
+     * @param rng the source of randomness
+     */
+    private static void addDiscreteSamplers(Builder<? super SharedStateDiscreteSampler> builder, int n, int min,
+            int max, UniformRandomProvider rng) {
+        // Create the ranges using n-1 random ticks in the range (min, max),
+        // adding the limits and then sorting in ascending order.
+        // The samplers are then constructed:
+        //
+        // min-------A-----B----max
+        // Sampler 1 = [min, A)
+        // Sampler 2 = [A, B)
+        // Sampler 3 = [B, max)
+
+        // Use a combination sampler to ensure the ticks are unique in the range.
+        // This will throw if the range is negative.
+        final int range = max - min - 1;
+        int[] ticks = new CombinationSampler(rng, range, n - 1).sample();
+        // Shift the ticks into the range
+        for (int i = 0; i < ticks.length; i++) {
+            ticks[i] += min + 1;
+        }
+        // Add the min and max
+        ticks = Arrays.copyOf(ticks, n + 1);
+        ticks[n - 1] = min;
+        ticks[n] = max;
+        Arrays.sort(ticks);
+
+        // Sample within the ranges between the ticks
+        final int before = builder.size();
+        for (int i = 1; i < ticks.length; i++) {
+            final IntRangeSampler sampler = new IntRangeSampler(ticks[i - 1], ticks[i], rng);
+            // Weight using the range
+            builder.add(sampler, sampler.range);
+        }
+
+        Assert.assertEquals("Failed to add the correct number of samplers", n, builder.size() - before);
+    }
+
+    /**
+     * Assert sampling is uniform between the minimum and maximum.
+     *
+     * @param sampler the sampler
+     * @param min the minimum (inclusive)
+     * @param max the maximum (exclusive)
+     */
+    private static void assertDiscreteSamplerSamples(DiscreteSampler sampler, int min, int max) {
+        final int n = 100000;
+        final long[] observed = new long[max - min];
+        for (int i = 0; i < n; i++) {
+            observed[sampler.sample() - min]++;
+        }
+
+        final double[] expected = new double[observed.length];
+        Arrays.fill(expected, (double) n / expected.length);
+        final double p = new ChiSquareTest().chiSquareTest(expected, observed);
+        Assert.assertFalse("p-value too small: " + p, p < 0.001);
+    }
+
+    /**
+     * Test sampling is uniform across several ContinuousSampler samplers.
+     */
+    @Test
+    public void testContinuousSamplerSamples() {
+        final Builder<ContinuousSampler> builder = CompositeSamplers.newContinuousSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.XO_SHI_RO_256_PP.create(9283756);
+        final int n = 15;
+        final double min = 67.2;
+        final double max = 2033.8;
+        addContinuousSamplers(builder, n, min, max, rng);
+        assertContinuousSamplerSamples(builder.build(rng), min, max);
+    }
+
+    /**
+     * Test sampling is uniform across several SharedStateContinuousSampler samplers.
+     */
+    @Test
+    public void testSharedStateContinuousSamplerSamples() {
+        final Builder<SharedStateContinuousSampler> builder = CompositeSamplers
+                .newSharedStateContinuousSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.PCG_RXS_M_XS_64_OS.create(0x567567345L);
+        final int n = 11;
+        final double min = -15.7;
+        final double max = 123.4;
+        addContinuousSamplers(builder, n, min, max, rng);
+        assertContinuousSamplerSamples(builder.build(rng), min, max);
+    }
+
+    /**
+     * Add samplers to the builder that sample from contiguous ranges between the
+     * minimum and maximum. Note: {@code max - min >= n}
+     *
+     * @param builder the builder
+     * @param n the number of samplers (must be {@code >= 2})
+     * @param min the minimum (inclusive)
+     * @param max the maximum (exclusive)
+     * @param rng the source of randomness
+     */
+    private static void addContinuousSamplers(Builder<? super SharedStateContinuousSampler> builder, int n, double min,
+            double max, UniformRandomProvider rng) {
+        // Create the ranges using n-1 random ticks in the range (min, max),
+        // adding the limits and then sorting in ascending order.
+        // The samplers are then constructed:
+        //
+        // min-------A-----B----max
+        // Sampler 1 = [min, A)
+        // Sampler 2 = [A, B)
+        // Sampler 3 = [B, max)
+
+        // For double values it is extremely unlikely the same value will be generated.
+        // An assertion is performed to ensure we create the correct number of samplers.
+        DoubleRangeSampler sampler = new DoubleRangeSampler(min, max, rng);
+        final double[] ticks = new double[n + 1];
+        ticks[0] = min;
+        ticks[1] = max;
+        // Shift the ticks into the range
+        for (int i = 2; i < ticks.length; i++) {
+            ticks[i] = sampler.sample();
+        }
+        Arrays.sort(ticks);
+
+        // Sample within the ranges between the ticks
+        final int before = builder.size();
+        for (int i = 1; i < ticks.length; i++) {
+            sampler = new DoubleRangeSampler(ticks[i - 1], ticks[i], rng);
+            // Weight using the range
+            builder.add(sampler, sampler.range());
+        }
+
+        Assert.assertEquals("Failed to add the correct number of samplers", n, builder.size() - before);
+    }
+
+    /**
+     * Assert sampling is uniform between the minimum and maximum.
+     *
+     * @param sampler the sampler
+     * @param min the minimum (inclusive)
+     * @param max the maximum (exclusive)
+     */
+    private static void assertContinuousSamplerSamples(ContinuousSampler sampler, double min, double max) {
+        final int n = 100000;
+        final int bins = 200;
+        final long[] observed = new long[bins];
+        final double scale = bins / (max - min);
+        for (int i = 0; i < n; i++) {
+            // scale the sample into a bin within the range:
+            // bin = bins * (x - min) / (max - min)
+            observed[(int) (scale * (sampler.sample() - min))]++;
+        }
+
+        final double[] expected = new double[observed.length];
+        Arrays.fill(expected, (double) n / expected.length);
+        final double p = new ChiSquareTest().chiSquareTest(expected, observed);
+        Assert.assertFalse("p-value too small: " + p, p < 0.001);
+    }
+
+    /**
+     * Test sampling is uniform across several LongSampler samplers.
+     */
+    @Test
+    public void testLongSamplerSamples() {
+        final Builder<LongSampler> builder = CompositeSamplers.newLongSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.KISS.create(67842321783L);
+        final int n = 15;
+        final long min = -134;
+        final long max = 1L << 54;
+        addLongSamplers(builder, n, min, max, rng);
+        assertLongSamplerSamples(builder.build(rng), min, max);
+    }
+
+    /**
+     * Test sampling is uniform across several SharedStateLongSampler samplers.
+     */
+    @Test
+    public void testSharedStateLongSamplerSamples() {
+        final Builder<SharedStateLongSampler> builder = CompositeSamplers.newSharedStateLongSamplerBuilder();
+        final UniformRandomProvider rng = RandomSource.KISS.create(12369279382030L);
+        final int n = 11;
+        final long min = 42;
+        final long max = 1L << 53;
+        addLongSamplers(builder, n, min, max, rng);
+        assertLongSamplerSamples(builder.build(rng), min, max);
+    }
+
+    /**
+     * Add samplers to the builder that sample from contiguous ranges between the
+     * minimum and maximum. Note: {@code max - min >= n}
+     *
+     * @param builder the builder
+     * @param n the number of samplers (must be {@code >= 2})
+     * @param min the minimum (inclusive)
+     * @param max the maximum (exclusive)
+     * @param rng the source of randomness
+     */
+    private static void addLongSamplers(Builder<? super SharedStateLongSampler> builder, int n, long min,
+            long max, UniformRandomProvider rng) {
+        // Create the ranges using n-1 random ticks in the range (min, max),
+        // adding the limits and then sorting in ascending order.
+        // The samplers are then constructed:
+        //
+        // min-------A-----B----max
+        // Sampler 1 = [min, A)
+        // Sampler 2 = [A, B)
+        // Sampler 3 = [B, max)
+
+        // For long values it is extremely unlikely the same value will be generated.
+        // An assertion is performed to ensure we create the correct number of samplers.
+        LongRangeSampler sampler = new LongRangeSampler(min, max, rng);
+        final long[] ticks = new long[n + 1];
+        ticks[0] = min;
+        ticks[1] = max;
+        // Shift the ticks into the range
+        for (int i = 2; i < ticks.length; i++) {
+            ticks[i] = sampler.sample();
+        }
+        Arrays.sort(ticks);
+
+
+        // Sample within the ranges between the ticks
+        final int before = builder.size();
+        for (int i = 1; i < ticks.length; i++) {
+            sampler = new LongRangeSampler(ticks[i - 1], ticks[i], rng);
+            // Weight using the range
+            builder.add(sampler, sampler.range);
+        }
+
+        Assert.assertEquals("Failed to add the correct number of samplers", n, builder.size() - before);
+    }
+
+    /**
+     * Assert sampling is uniform between the minimum and maximum.
+     *
+     * @param sampler the sampler
+     * @param min the minimum (inclusive)
+     * @param max the maximum (exclusive)
+     */
+    private static void assertLongSamplerSamples(LongSampler sampler, long min, long max) {
+        final int n = 100000;
+        final int bins = 200;
+        final long[] observed = new long[bins];
+        final long range = max - min;
+        for (int i = 0; i < n; i++) {
+            // scale the sample into a bin within the range:
+            observed[(int) (bins * (sampler.sample() - min) / range)]++;
+        }
+
+        final double[] expected = new double[observed.length];
+        Arrays.fill(expected, (double) n / expected.length);
+        final double p = new ChiSquareTest().chiSquareTest(expected, observed);
+        Assert.assertFalse("p-value too small: " + p, p < 0.001);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateObjectSampler.
+     */
+    @Test
+    public void testSharedStateObjectSampler() {
+        testSharedStateObjectSampler(false);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateObjectSampler with a factory that does not support a shared state sampler.
+     */
+    @Test
+    public void testSharedStateObjectSamplerWithCustomFactory() {
+        testSharedStateObjectSampler(true);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateObjectSampler.
+     *
+     * @param customFactory Set to true to use a custom discrete sampler factory that does not
+     * support a shared stated sampler.
+     */
+    private static void testSharedStateObjectSampler(boolean customFactory) {
+        final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L);
+        final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L);
+
+        final Builder<SharedStateObjectSampler<Integer>> builder = CompositeSamplers
+                .newSharedStateObjectSamplerBuilder();
+
+        if (customFactory) {
+            addFactoryWithNoSharedStateSupport(builder);
+        }
+
+        // Sample within the ranges between the ticks
+        final int[] ticks = {6, 13, 42, 99};
+        for (int i = 1; i < ticks.length; i++) {
+            final RangeSampler sampler = new RangeSampler(ticks[i - 1], ticks[i], rng1);
+            // Weight using the range
+            builder.add(sampler, sampler.range);
+        }
+
+        final SharedStateObjectSampler<Integer> sampler1 = builder.build(rng1);
+        final SharedStateObjectSampler<Integer> sampler2 = sampler1.withUniformRandomProvider(rng2);
+        RandomAssert.assertProduceSameSequence(new RandomAssert.Sampler<Integer>() {
+            @Override
+            public Integer sample() {
+                return sampler1.sample();
+            }
+        }, new RandomAssert.Sampler<Integer>() {
+            @Override
+            public Integer sample() {
+                return sampler2.sample();
+            }
+        });
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateDiscreteSampler.
+     */
+    @Test
+    public void testSharedStateDiscreteSampler() {
+        testSharedStateDiscreteSampler(false);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateDiscreteSampler with a factory that does not support a shared state sampler.
+     */
+    @Test
+    public void testSharedStateDiscreteSamplerWithCustomFactory() {
+        testSharedStateDiscreteSampler(true);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateDiscreteSampler.
+     *
+     * @param customFactory Set to true to use a custom discrete sampler factory that does not
+     * support a shared stated sampler.
+     */
+    private static void testSharedStateDiscreteSampler(boolean customFactory) {
+        final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L);
+        final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L);
+
+        final Builder<SharedStateDiscreteSampler> builder = CompositeSamplers.newSharedStateDiscreteSamplerBuilder();
+
+        if (customFactory) {
+            addFactoryWithNoSharedStateSupport(builder);
+        }
+
+        // Sample within the ranges between the ticks
+        final int[] ticks = {-3, 5, 14, 22};
+        for (int i = 1; i < ticks.length; i++) {
+            final IntRangeSampler sampler = new IntRangeSampler(ticks[i - 1], ticks[i], rng1);
+            // Weight using the range
+            builder.add(sampler, sampler.range);
+        }
+
+        final SharedStateDiscreteSampler sampler1 = builder.build(rng1);
+        final SharedStateDiscreteSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
+        RandomAssert.assertProduceSameSequence(sampler1, sampler2);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateContinuousSampler.
+     */
+    @Test
+    public void testSharedStateContinuousSampler() {
+        testSharedStateContinuousSampler(false);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateContinuousSampler with a factory that does not support a shared state sampler.
+     */
+    @Test
+    public void testSharedStateContinuousSamplerWithCustomFactory() {
+        testSharedStateContinuousSampler(true);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateContinuousSampler.
+     *
+     * @param customFactory Set to true to use a custom discrete sampler factory that does not
+     * support a shared stated sampler.
+     */
+    private static void testSharedStateContinuousSampler(boolean customFactory) {
+        final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L);
+        final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L);
+
+        final Builder<SharedStateContinuousSampler> builder = CompositeSamplers
+                .newSharedStateContinuousSamplerBuilder();
+
+        if (customFactory) {
+            addFactoryWithNoSharedStateSupport(builder);
+        }
+
+        // Sample within the ranges between the ticks
+        final double[] ticks = {7.89, 13.99, 21.7, 35.6, 45.5};
+        for (int i = 1; i < ticks.length; i++) {
+            final DoubleRangeSampler sampler = new DoubleRangeSampler(ticks[i - 1], ticks[i], rng1);
+            // Weight using the range
+            builder.add(sampler, sampler.range());
+        }
+
+        final SharedStateContinuousSampler sampler1 = builder.build(rng1);
+        final SharedStateContinuousSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
+        RandomAssert.assertProduceSameSequence(sampler1, sampler2);
+    }
+
+    /**
+     * Adds a DiscreteSamplerFactory to the builder that creates samplers that do not share state.
+     *
+     * @param builder the builder
+     */
+    private static void addFactoryWithNoSharedStateSupport(Builder<?> builder) {
+        builder.setFactory(new DiscreteProbabilitySamplerFactory() {
+            @Override
+            public DiscreteSampler create(UniformRandomProvider rng, double[] probabilities) {
+                // Wrap so it is not a SharedStateSamplerInstance.
+                final DiscreteSampler sampler = GuideTableDiscreteSampler.of(rng, probabilities, 2);
+                // Destroy the probabilities to check that custom factories are not trusted.
+                Arrays.fill(probabilities, Double.NaN);
+                return new DiscreteSampler() {
+                    @Override
+                    public int sample() {
+                        return sampler.sample();
+                    }
+                };
+            }
+        });
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateLongSampler.
+     */
+    @Test
+    public void testSharedStateLongSampler() {
+        testSharedStateLongSampler(false);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateLongSampler with a factory that does not support a shared state sampler.
+     */
+    @Test
+    public void testSharedStateLongSamplerWithCustomFactory() {
+        testSharedStateLongSampler(true);
+    }
+
+    /**
+     * Test the SharedStateSampler implementation for the composite
+     * SharedStateLongSampler.
+     *
+     * @param customFactory Set to true to use a custom discrete sampler factory that does not
+     * support a shared stated sampler.
+     */
+    private static void testSharedStateLongSampler(boolean customFactory) {
+        final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L);
+        final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L);
+
+        final Builder<SharedStateLongSampler> builder = CompositeSamplers.newSharedStateLongSamplerBuilder();
+
+        if (customFactory) {
+            addFactoryWithNoSharedStateSupport(builder);
+        }
+
+        // Sample within the ranges between the ticks
+        final long[] ticks = {-32634628368L, 516234712, 1472839427384234L, 72364572187368423L};
+        for (int i = 1; i < ticks.length; i++) {
+            final LongRangeSampler sampler = new LongRangeSampler(ticks[i - 1], ticks[i], rng1);
+            // Weight using the range
+            builder.add(sampler, sampler.range);
+        }
+
+        final SharedStateLongSampler sampler1 = builder.build(rng1);
+        final SharedStateLongSampler sampler2 = sampler1.withUniformRandomProvider(rng2);
+        RandomAssert.assertProduceSameSequence(sampler1, sampler2);
+    }
+
+    /**
+     * Sample an object {@code Integer} from a range.
+     */
+    private static class RangeSampler implements SharedStateObjectSampler<Integer> {
+        private final int min;
+        private final int range;
+        private final UniformRandomProvider rng;
+
+        /**
+         * @param min the minimum (inclusive)
+         * @param max the maximum (exclusive)
+         * @param rng the source of randomness
+         */
+        RangeSampler(int min, int max, UniformRandomProvider rng) {
+            this.min = min;
+            this.range = max - min;
+            this.rng = rng;
+        }
+
+        @Override
+        public Integer sample() {
+            return min + rng.nextInt(range);
+        }
+
+        @Override
+        public SharedStateObjectSampler<Integer> withUniformRandomProvider(UniformRandomProvider generator) {
+            return new RangeSampler(min, min + range, generator);
+        }
+    }
+
+    /**
+     * Sample a primitive {@code integer} from a range.
+     */
+    private static class IntRangeSampler implements SharedStateDiscreteSampler {
+        private final int min;
+        private final int range;
+        private final UniformRandomProvider rng;
+
+        /**
+         * @param min the minimum (inclusive)
+         * @param max the maximum (exclusive)
+         * @param rng the source of randomness
+         */
+        IntRangeSampler(int min, int max, UniformRandomProvider rng) {
+            this.min = min;
+            this.range = max - min;
+            this.rng = rng;
+        }
+
+        @Override
+        public int sample() {
+            return min + rng.nextInt(range);
+        }
+
+        @Override
+        public SharedStateDiscreteSampler withUniformRandomProvider(UniformRandomProvider generator) {
+            return new IntRangeSampler(min, min + range, generator);
+        }
+    }
+
+    /**
+     * Sample a primitive {@code double} from a range between a and b.
+     */
+    private static class DoubleRangeSampler implements SharedStateContinuousSampler {
+        private final double a;
+        private final double b;
+        private final UniformRandomProvider rng;
+
+        /**
+         * @param a bound a
+         * @param b bound b
+         * @param rng the source of randomness
+         */
+        DoubleRangeSampler(double a, double b, UniformRandomProvider rng) {
+            this.a = a;
+            this.b = b;
+            this.rng = rng;
+        }
+
+        /**
+         * Get the range from a to b.
+         *
+         * @return the range
+         */
+        double range() {
+            return Math.abs(b - a);
+        }
+
+        @Override
+        public double sample() {
+            // a + u * (b - a) == u * b + (1 - u) * a
+            final double u = rng.nextDouble();
+            return u * b + (1 - u) * a;
+        }
+
+        @Override
+        public SharedStateContinuousSampler withUniformRandomProvider(UniformRandomProvider generator) {
+            return new DoubleRangeSampler(a, b, generator);
+        }
+    }
+
+    /**
+     * Sample a primitive {@code long} from a range.
+     */
+    private static class LongRangeSampler implements SharedStateLongSampler {
+        private final long min;
+        private final long range;
+        private final UniformRandomProvider rng;
+
+        /**
+         * @param min the minimum (inclusive)
+         * @param max the maximum (exclusive)
+         * @param rng the source of randomness
+         */
+        LongRangeSampler(long min, long max, UniformRandomProvider rng) {
+            this.min = min;
+            this.range = max - min;
+            this.rng = rng;
+        }
+
+        @Override
+        public long sample() {
+            return min + rng.nextLong(range);
+        }
+
+        @Override
+        public SharedStateLongSampler withUniformRandomProvider(UniformRandomProvider generator) {
+            return new LongRangeSampler(min, min + range, generator);
+        }
+    }
+}
diff --git a/src/main/resources/pmd/pmd-ruleset.xml b/src/main/resources/pmd/pmd-ruleset.xml
index a86c1ae..bf05492 100644
--- a/src/main/resources/pmd/pmd-ruleset.xml
+++ b/src/main/resources/pmd/pmd-ruleset.xml
@@ -75,7 +75,7 @@
       <!-- Array is generated internally in this case. -->
       <property name="violationSuppressXPath"
         value="//ClassOrInterfaceDeclaration[@SimpleName='PoissonSamplerCache' or @SimpleName='AliasMethodDiscreteSampler'
-          or @SimpleName='GuideTableDiscreteSampler']"/>
+          or @SimpleName='GuideTableDiscreteSampler' or @SimpleName='SharedStateDiscreteProbabilitySampler']"/>
     </properties>
   </rule>
   <rule ref="category/java/bestpractices.xml/SystemPrintln">
@@ -117,6 +117,12 @@
         value="//ClassOrInterfaceDeclaration[@SimpleName='ProbabilityDensityApproximationCommand']"/>
     </properties>
   </rule>
+  <rule ref="category/java/codestyle.xml/LinguisticNaming">
+    <properties>
+      <!-- Allow Builder set methods to return the Builder (not void) -->
+      <property name="violationSuppressXPath" value="//ClassOrInterfaceDeclaration[matches(@SimpleName, '^.*Builder$')]"/>
+    </properties>
+  </rule>
 
   <rule ref="category/java/design.xml/NPathComplexity">
     <properties>
@@ -152,7 +158,8 @@
   <rule ref="category/java/design.xml/ExcessiveClassLength">
     <properties>
       <!-- The length is due to multiple implementations as inner classes -->
-      <property name="violationSuppressXPath" value="//ClassOrInterfaceDeclaration[@SimpleName='MarsagliaTsangWangDiscreteSampler']"/>
+      <property name="violationSuppressXPath" value="//ClassOrInterfaceDeclaration[@SimpleName='MarsagliaTsangWangDiscreteSampler'
+        or @SimpleName='CompositeSamplers']"/>
     </properties>
   </rule>
   <rule ref="category/java/design.xml/LogicInversion">
@@ -171,12 +178,24 @@
         or @SimpleName='UniformSamplingVisualCheckCommand']"/>
     </properties>
   </rule>
+  <rule ref="category/java/design.xml/AvoidThrowingNullPointerException">
+    <properties>
+      <!-- Local implementation of Objects.requireNonNull -->
+      <property name="violationSuppressXPath" value="//ClassOrInterfaceDeclaration[@SimpleName='CompositeSamplers']"/>
+    </properties>
+  </rule>
 
   <rule ref="category/java/errorprone.xml/AvoidLiteralsInIfCondition">
     <properties>
       <property name="ignoreMagicNumbers" value="-1,0,1" />
     </properties>
   </rule>
+  <rule ref="category/java/errorprone.xml/AvoidFieldNameMatchingMethodName">
+    <properties>
+      <!-- Field INSTANCE matches instance() which returne a generic typed version of the instance.  -->
+      <property name="violationSuppressXPath" value="//ClassOrInterfaceDeclaration[matches(@SimpleName, '^.*ObjectSamplerFactory$')]"/>
+    </properties>
+  </rule>
 
   <rule ref="category/java/multithreading.xml/UseConcurrentHashMap">
     <properties>