001/*
002 * Java Genetic Algorithm Library (jenetics-8.3.0).
003 * Copyright (c) 2007-2025 Franz Wilhelmstötter
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 *
017 * Author:
018 *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019 */
020package io.jenetics.stat;
021
022import static java.lang.Math.pow;
023import static java.lang.Math.sqrt;
024
025import java.util.random.RandomGenerator;
026
027import io.jenetics.util.DoubleRange;
028
029/**
030 * This class defines some default samplers.
031 *
032 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
033 * @version 8.3
034 * @since 8.0
035 */
036public final class Samplers {
037
038        private Samplers() {
039        }
040
041        /**
042         * Return a new sampler for a <em>linear</em> distribution with the given
043         * {@code mean} value, when creating sample points for the <em>normalized</em>
044         * range {@code [0, 1)}.
045         * <p>
046         *      <img src="doc-files/LinearDistributionPDF.svg" width="450"
047         *           alt="Linear distribution sampler" >
048         *
049         * @param mean the mean value of the sampler distribution
050         * @return a new linear sampler with the given {@code mean} value
051         * @throws IllegalArgumentException if the given {@code mean} value is not
052         *         within the range {@code [0, 1)}
053         */
054        public static Sampler linear(final double mean) {
055                if (mean < 0 || mean >= 1 || !Double.isFinite(mean)) {
056                        throw new IllegalArgumentException(
057                                "Mean value not within allowed range [0, 1): %f."
058                                        .formatted(mean)
059                        );
060                }
061
062                final class Range {
063                        static final double MIN = 0.0;
064                        static final double MAX = Math.nextDown(1.0);
065                        static final double LIMIT1 = 1.0 - 1.0/sqrt(2);
066                        static final double LIMIT2 = 1.0/sqrt(2);
067                        static double clamp(final double value) {
068                                return Math.clamp(value, MIN, MAX);
069                        }
070                }
071
072                if (Double.compare(mean, 0) == 0) {
073                        return (random, range) -> range.min();
074                } else if (mean == Range.MAX) {
075                        return (random, range) -> Math.nextDown(range.max());
076                }
077
078                final double b, m;
079                if (mean < Range.LIMIT1) {
080                        b = (2 - sqrt(2))/mean;
081                        m = -pow(b, 2)/2;
082                } else if (mean < Range.LIMIT2) {
083                        b = (pow(mean, 2) - 0.5)/(pow(mean, 2) - mean);
084                        m = 2 - 2*b;
085                } else if (mean < 1) {
086                        b = 0.5*(1 - pow(((2 - sqrt(2))/(1 - mean) - 1), 2));
087                        m = -b + sqrt(1 - 2*b) + 1;
088                } else {
089                        b = m = 1;
090                }
091
092                return (random, range) -> {
093                        final var r = random.nextDouble();
094
095                        final double sample;
096                        if (mean < 0.5) {
097                                sample = Range.clamp((-b + sqrt(b*b + 2*r*m))/m);
098                        } else if (mean == 0.5) {
099                                sample = r;
100                        } else {
101                                sample = Range.clamp((b - sqrt(b*b + 2*m*(r - 1 + b + 0.5*m)))/-m);
102                        }
103
104                        return stretch(sample, range);
105                };
106        }
107
108        /**
109         * Create a new sampler for a triangle distribution with the given
110         * parameters. All parameters must be within the <em>normalized</em> range
111         * {@code [0, 1]}. The sample value, returned by the
112         * {@link Sampler#sample(RandomGenerator, DoubleRange)} method, is then
113         * <em>stretched</em> to the desired range.
114         * <p>
115         *      <img src="doc-files/TriangularDistributionPDF.svg" width="450"
116         *           alt="Triangle distribution sampler" >
117         *
118         * @see #triangular(double)
119         *
120         * @param a the <em>normalized</em> start point of the triangle
121         * @param c the <em>normalized</em> middle point of the triangle
122         * @param b the <em>normalized</em> end point of the triangle
123         * @return a new triangle distribution sampler
124         * @throws IllegalArgumentException if one of the parameters is not within
125         *         the range {@code [0, 1]} or {@code b <= a || c > b || c < a}
126         */
127        public static Sampler
128        triangular(final double a, final double c, final double b) {
129                if (!Double.isFinite(a) || !Double.isFinite(b) || !Double.isFinite(c) ||
130                        a < 0 || b < 0 || c < 0  ||
131                        a > 1 || b > 1 || c > 1 ||
132                        b <= a || c > b || c < a)
133                {
134                        throw new IllegalArgumentException(
135                                "Invalid triangular: [a=%f, c=%f, b=%f].".formatted(a, c, b)
136                        );
137                }
138
139                final double fc = (c - a)/(b - a);
140
141                return (random, range) -> {
142                        final var r = random.nextDouble();
143
144                        final double sample;
145                        if (Double.compare(r, 0.0) == 0) {
146                                sample = 0;
147                        } else if (r < fc) {
148                                sample = a + sqrt(r*(b - a)*(c - a));
149                        } else {
150                                sample = b - sqrt((1 - r)*(b - a)*(b - c));
151                        }
152
153                        return stretch(sample, range);
154                };
155        }
156
157        /**
158         * Return a new sampler for a <em>normalized</em> triangle distribution
159         * with the points {@code [0, c, 1]}.
160         *
161         * @see #triangular(double, double, double)
162         *
163         * @param c the middle point of the triangle within the range {@code [0, 1]}
164         * @return a new triangle distribution sampler
165         * @throws IllegalArgumentException if c not within {@code [0, 1]}
166         */
167        public static Sampler triangular(final double c) {
168                return triangular(0, c, 1.0);
169        }
170
171        private static double stretch(final double sample, DoubleRange range) {
172                return range.min() + sample*(range.max() - range.min());
173        }
174
175        /**
176         * Return a <em>Gaussian</em> sampler with the given parameter. The sampler
177         * returns {@link Double#NaN} if it is not possible to create a normal
178         * distributed value within the desired range.
179         *
180         * @since 8.3
181         *
182         * @param mean the mean value of the <em>Gaussian</em> sampler
183         * @param stddev the standard deviation of the <em>Gaussian</em> sampler
184         * @param maxRetries the maximal number of retries, if a generated value
185         *        is not in the desired range.
186         * @return a <em>Gaussian</em> sampler
187         * @throws IllegalArgumentException if {@code maxRetries} is smaller than one
188         */
189        public static Sampler gaussian(
190                final double mean,
191                final double stddev,
192                final int maxRetries
193        ) {
194                if (maxRetries < 1) {
195                        throw new IllegalArgumentException(
196                                "Max retries must be greater than zero: %d."
197                                        .formatted(maxRetries)
198                        );
199                }
200
201                return (random, range) -> {
202                        double x = random.nextGaussian(mean, stddev);
203                        int retries = 0;
204                        while (!range.contains(x) && retries++ < maxRetries) {
205                                x = random.nextGaussian(mean, stddev);
206                        }
207
208                        return retries <= maxRetries ? x : Double.NaN;
209                };
210        }
211
212        /**
213         * Return a <em>Gaussian</em> sampler with the given parameter. The sampler
214         * returns {@link Double#NaN} if it is not possible to create a normal
215         * distributed value within the desired range after 25 tries.
216         *
217         * @since 8.3
218         *
219         * @param mean the mean value of the <em>Gaussian</em> sampler
220         * @param stddev the standard deviation of the <em>Gaussian</em> sampler
221         * @return a <em>Gaussian</em> sampler
222         * @throws IllegalArgumentException if {@code maxRetries} is smaller than one
223         */
224        public static Sampler gaussian(final double mean, final double stddev) {
225                return gaussian(mean, stddev, 10_000);
226        }
227
228}
229