blob: b55042ffbefd7b409e36a318885a5a2fd581aefd [file] [log] [blame]
/*
* 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.statistics.inference;
import java.util.function.DoubleUnaryOperator;
/**
* Provide an interval that brackets a local minimum of a function.
* This code is based on a Python implementation (from <em>SciPy</em>,
* module {@code optimize.py} v0.5).
*
* <p>This class has been extracted from {@code o.a.c.math4.optim.univariate}
* and modified to: remove support for bracketing a maximum; support bounds
* on the bracket; correct the sign of the denominator when the magnitude is small;
* and return true/false if there is a minimum strictly inside the bounds.
*
* @since 1.1
*/
class BracketFinder {
/** Tolerance to avoid division by zero. */
private static final double EPS_MIN = 1e-21;
/** Golden section. */
private static final double GOLD = 1.6180339887498948482;
/** Factor for expanding the interval. */
private final double growLimit;
/** Number of allowed function evaluations. */
private final int maxEvaluations;
/** Number of function evaluations performed in the last search. */
private int evaluations;
/** Lower bound of the bracket. */
private double lo;
/** Higher bound of the bracket. */
private double hi;
/** Point inside the bracket. */
private double mid;
/** Function value at {@link #lo}. */
private double fLo;
/** Function value at {@link #hi}. */
private double fHi;
/** Function value at {@link #mid}. */
private double fMid;
/**
* Constructor with default values {@code 100, 100000} (see the
* {@link #BracketFinder(double,int) other constructor}).
*/
BracketFinder() {
this(100, 100000);
}
/**
* Create a bracketing interval finder.
*
* @param growLimit Expanding factor.
* @param maxEvaluations Maximum number of evaluations allowed for finding
* a bracketing interval.
* @throws IllegalArgumentException if the {@code growLimit} or {@code maxEvalutations}
* are not strictly positive.
*/
BracketFinder(double growLimit, int maxEvaluations) {
Arguments.checkStrictlyPositive(growLimit);
Arguments.checkStrictlyPositive(maxEvaluations);
this.growLimit = growLimit;
this.maxEvaluations = maxEvaluations;
}
/**
* Search downhill from the initial points to obtain new points that bracket a local
* minimum of the function. Note that the initial points do not have to bracket a minimum.
* An exception is raised if a minimum cannot be found within the configured number
* of function evaluations.
*
* <p>The bracket is limited to the provided bounds if they create a positive interval
* {@code min < max}. It is possible that the middle of the bracket is at the bounds as
* the final bracket is {@code f(mid) <= min(f(lo), f(hi))} and {@code lo <= mid <= hi}.
*
* <p>No exception is raised if the initial points are not within the bounds; the points
* are updated to be within the bounds.
*
* <p>No exception is raised if the initial points are equal; the bracket will be returned
* as a single point {@code lo == mid == hi}.
*
* @param func Function whose optimum should be bracketed.
* @param a Initial point.
* @param b Initial point.
* @param min Minimum bound of the bracket (inclusive).
* @param max Maximum bound of the bracket (inclusive).
* @return true if the mid-point is strictly within the final bracket {@code [lo, hi]};
* false if there is no local minima.
* @throws IllegalStateException if the maximum number of evaluations is exceeded.
*/
boolean search(DoubleUnaryOperator func,
double a, double b,
double min, double max) {
evaluations = 0;
// Limit the range of x
DoubleUnaryOperator range;
if (min < max) {
// Limit: min <= x <= max
range = x -> {
if (x > min) {
return x < max ? x : max;
}
return min;
};
} else {
range = DoubleUnaryOperator.identity();
}
double xA = range.applyAsDouble(a);
double xB = range.applyAsDouble(b);
double fA = value(func, xA);
double fB = value(func, xB);
// Ensure fB <= fA
if (fA < fB) {
double tmp = xA;
xA = xB;
xB = tmp;
tmp = fA;
fA = fB;
fB = tmp;
}
double xC = range.applyAsDouble(xB + GOLD * (xB - xA));
double fC = value(func, xC);
// Note: When a [min, max] interval is provided and there is no minima then this
// loop will terminate when B == C and both are at the min/max bound.
while (fC < fB) {
final double tmp1 = (xB - xA) * (fB - fC);
final double tmp2 = (xB - xC) * (fB - fA);
final double val = tmp2 - tmp1;
// limit magnitude of val to a small value
final double denom = 2 * Math.copySign(Math.max(Math.abs(val), EPS_MIN), val);
double w = range.applyAsDouble(xB - ((xB - xC) * tmp2 - (xB - xA) * tmp1) / denom);
final double wLim = range.applyAsDouble(xB + growLimit * (xC - xB));
double fW;
if ((w - xC) * (xB - w) > 0) {
// xB < w < xC
fW = value(func, w);
if (fW < fC) {
// minimum in [xB, xC]
xA = xB;
xB = w;
fA = fB;
fB = fW;
break;
} else if (fW > fB) {
// minimum in [xA, w]
xC = w;
fC = fW;
break;
}
// continue downhill
w = range.applyAsDouble(xC + GOLD * (xC - xB));
fW = value(func, w);
} else if ((w - wLim) * (xC - w) > 0) {
// xC < w < limit
fW = value(func, w);
if (fW < fC) {
// continue downhill
xB = xC;
xC = w;
w = range.applyAsDouble(xC + GOLD * (xC - xB));
fB = fC;
fC = fW;
fW = value(func, w);
}
} else if ((w - wLim) * (wLim - xC) >= 0) {
// xC <= limit <= w
w = wLim;
fW = value(func, w);
} else {
// possibly w == xC; reject w and take a default step
w = range.applyAsDouble(xC + GOLD * (xC - xB));
fW = value(func, w);
}
xA = xB;
fA = fB;
xB = xC;
fB = fC;
xC = w;
fC = fW;
}
mid = xB;
fMid = fB;
// Store the bracket: lo <= mid <= hi
if (xC < xA) {
lo = xC;
fLo = fC;
hi = xA;
fHi = fA;
} else {
lo = xA;
fLo = fA;
hi = xC;
fHi = fC;
}
return lo < mid && mid < hi;
}
/**
* @return the number of evaluations.
*/
int getEvaluations() {
return evaluations;
}
/**
* @return the lower bound of the bracket.
* @see #getFLo()
*/
double getLo() {
return lo;
}
/**
* Get function value at {@link #getLo()}.
* @return function value at {@link #getLo()}
*/
double getFLo() {
return fLo;
}
/**
* @return the higher bound of the bracket.
* @see #getFHi()
*/
double getHi() {
return hi;
}
/**
* Get function value at {@link #getHi()}.
* @return function value at {@link #getHi()}
*/
double getFHi() {
return fHi;
}
/**
* @return a point in the middle of the bracket.
* @see #getFMid()
*/
double getMid() {
return mid;
}
/**
* Get function value at {@link #getMid()}.
* @return function value at {@link #getMid()}
*/
double getFMid() {
return fMid;
}
/**
* Get the value of the function.
*
* @param func Function.
* @param x Point.
* @return the value
* @throws IllegalStateException if the maximal number of evaluations is exceeded.
*/
private double value(DoubleUnaryOperator func, double x) {
if (evaluations >= maxEvaluations) {
throw new IllegalStateException("Too many evaluations: " + evaluations);
}
evaluations++;
return func.applyAsDouble(x);
}
}