001/*
002 * Java Genetic Algorithm Library (jenetics-8.1.0).
003 * Copyright (c) 2007-2024 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 io.jenetics.util.DoubleRange;
026
027/**
028 * This class defines some default samplers.
029 *
030 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
031 * @version 8.0
032 * @since 8.0
033 */
034public final class Samplers {
035
036        private Samplers() {
037        }
038
039        /**
040         * Return a new sampler for a <em>linear</em> distribution with the given
041         * {@code mean} value, when creating sample points for the range
042         * {@code [0, 1)}.
043         * <p>
044         *      <img src="doc-files/LinearDistributionPDF.svg" width="450"
045         *           alt="Linear distribution sampler" >
046         *
047         * @param mean the mean value of the samplers distribution
048         * @return a new linear sampler with the given {@code mean} value
049         * @throws IllegalArgumentException if the given {@code mean} value is not
050         *         within the range {@code [0, 1)}
051         */
052        public static Sampler linear(final double mean) {
053                if (mean < 0 || mean >= 1) {
054                        throw new IllegalArgumentException(
055                                "Mean value not within allowed range [0, 1): %f."
056                                        .formatted(mean)
057                        );
058                }
059
060                final class Range {
061                        static final double MIN = 0.0;
062                        static final double MAX = Math.nextDown(1.0);
063                        static final double LIMIT1 = 1.0 - 1.0/sqrt(2);
064                        static final double LIMIT2 = 1.0/sqrt(2);
065                        static double clamp(final double value) {
066                                return Math.clamp(value, MIN, MAX);
067                        }
068                }
069
070                if (Double.compare(mean, 0) == 0) {
071                        return (random, range) -> Range.MIN;
072                } else if (mean == Range.MAX) {
073                        return (random, range) -> Range.MAX* range.max();
074                }
075
076                final double b, m;
077                if (mean < Range.LIMIT1) {
078                        b = (2 - sqrt(2))/mean;
079                        m = -pow(b, 2)/2;
080                } else if (mean < Range.LIMIT2) {
081                        b = (pow(mean, 2) - 0.5)/(pow(mean, 2) - mean);
082                        m = 2 - 2*b;
083                } else if (mean < 1) {
084                        b = 0.5*(1 - pow(((2 - sqrt(2))/(1 - mean) - 1), 2));
085                        m = -b + sqrt(1 - 2*b) + 1;
086                } else {
087                        b = m = 1;
088                }
089
090                return (random, range) -> {
091                        final var r = random.nextDouble();
092
093                        final double sample;
094                        if (mean < 0.5) {
095                                sample = Range.clamp((-b + sqrt(b*b + 2*r*m))/m);
096                        } else if (mean == 0.5) {
097                                sample = r;
098                        } else {
099                                sample = Range.clamp((b - sqrt(b*b + 2*m*(r - 1 + b + 0.5*m)))/-m);
100                        }
101
102                        return stretch(sample, range);
103                };
104        }
105
106        /**
107         * Create a new sampler for a triangle distribution with the given
108         * parameters. All parameters must be within the range {@code [0, 1]}.
109         * <p>
110         *      <img src="doc-files/TriangularDistributionPDF.svg" width="450"
111         *           alt="Triangle distribution sampler" >
112         *
113         * @see #triangular(double)
114         *
115         * @param a the start point of the triangle
116         * @param c the middle point of the triangle
117         * @param b the end point of the triangle
118         * @return a new triangle distribution sampler
119         * @throws IllegalArgumentException if one of the parameters is not within
120         *         the range {@code [0, 1]} or {@code b <= a || c > b || c < a}
121         */
122        public static Sampler
123        triangular(final double a, final double c, final double b) {
124                if (a < 0 || b < 0 || c < 0  ||
125                        a > 1 || b > 1 || c > 1 ||
126                        b <= a || c > b || c < a)
127                {
128                        throw new IllegalArgumentException(
129                                "Invalid triangular: [a=%f, c=%f, b=%f].".formatted(a, c, b)
130                        );
131                }
132
133                final double fc = (c - a)/(b - a);
134
135                return (random, range) -> {
136                        final var r = random.nextDouble();
137
138                        final double sample;
139                        if (Double.compare(r, 0.0) == 0) {
140                                sample = 0;
141                        } else if (r < fc) {
142                                sample = a + sqrt(r*(b - a)*(c - a));
143                        } else {
144                                sample = b - sqrt((1 - r)*(b - a)*(b - c));
145                        }
146
147                        return stretch(sample, range);
148                };
149        }
150
151        /**
152         * Return a new sampler for a <em>normalized</em> triangle distribution
153         * with the points {@code [0, c, 1]}.
154         *
155         * @see #triangular(double, double, double)
156         *
157         * @param c the middle point of the triangle within the range {@code [0, 1}
158         * @return a new triangle distribution sampler
159         * @throws IllegalArgumentException if c not within {@code [0, 1]}
160         */
161        public static Sampler triangular(final double c) {
162                return triangular(0, c, 1.0);
163        }
164
165        private static double stretch(final double sample, DoubleRange range) {
166                return range.min() + sample*(range.max() - range.min());
167        }
168
169}
170