001/*
002 * Java Genetic Algorithm Library (jenetics-8.2.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.0
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