brents-method
Version:
A root solver for functions using Brent's Method.
158 lines (141 loc) • 5.11 kB
JavaScript
/*
* In the following implementation, the inverse quadratic interpolation is applied directly.
* In other cases the Lagrange polynomial is reduced defining the variables p, q, r, s, and t as explained at the link
* below and the x value is not overwritten with the bisection method, but modified.
* For simplicity of the code, here the inverse quadratic interpolation is applied directly and the new guess is
* overwritten if needed.
*
* http://mathworld.wolfram.com/BrentsMethod.html
*
* Based in part on https://mmas.github.io/brent-julia with edits and error corrections.
* (See: https://www.reddit.com/r/math/comments/htmaui/several_explanations_of_brents_method_appear_to/)
*
* Other useful resources:
* https://www.youtube.com/watch?v=-bLSRiokgFk
* https://en.wikipedia.org/wiki/Brent%27s_method#Brent's_method
* https://rosettacode.org/wiki/Roots_of_a_function#Brent.27s_Method_2 (Note errors are present)
* https://en.wikipedia.org/wiki?title=Talk:Brent%27s_method#Removed_code
* https://en.wikipedia.org/wiki?title=Talk:Brent%27s_method#Thanks_to_the_authors
*
*/
/**
* @function brentsMethod
* @param {Function} f The function for which roots are desired.
* @param {Number} lowerLimit The lower value used to bracket root finding into the function.
* @param {Number} upperLimit The upper value used to bracket root finding into the function.
* @param {Object} options An optional arguments object.
* @param {Number} [options.errorTolerance=1e-7] Used to determine how close the bounds bracketing root finding must
* converge before quitting.
* @param {Number} [options.maxIterations=50] The maximum number of iterations before root finding will fail.
* @return {Number|Boolean} Returns a input value resulting in a suitable root if successful. Returns false on failure.
*/
module.exports = (
f,
lowerLimit,
upperLimit,
{
errorTolerance = 1e-7,
maxIterations = 50,
} = {},
) => {
// xTolerance is used to determine how close the bounds need to converge before quitting.
const xTolerance = errorTolerance
// This is a computational limit and doesn't need to be configured
const yTolerance = 2 * Number.EPSILON
// x0 = a
// x1 = b
// y0 = fa
// y1 = fb
let x0 = lowerLimit
let x1 = upperLimit
let y0 = f(x0)
let y1 = f(x1)
// Swap lower and upper bounds so that y1 is closest to 0
// This makes b a better approximation of the intercept than a and it simulates a being the previous iteration of b
if (Math.abs(y0) < Math.abs(y1)) {
let temporary = x0
x0 = x1
x1 = temporary
temporary = y0
y0 = y1
y1 = temporary
}
// c = a
let x2 = x0
let y2 = y0
// d
let x3 = x2
let bisected = true
while (maxIterations) {
maxIterations--
// Stop the bounds converge to less than the tolerance
if (Math.abs(x1 - x0) < xTolerance) {
return x1
}
// Stop if it's found a suitable on a root
if (Math.abs(y1) < yTolerance) {
return x1
}
let x
// Use inverse quadratic interpolation if f(x0) !== f(x1) !== f(x2)
// (You can't draw a parabola without some distinct y values)
if (Math.abs(y0 - y2) > yTolerance && Math.abs(y1 - y2) > yTolerance) {
x = ((x0 * y1 * y2) / ((y0 - y1) * (y0 - y2))) +
((x1 * y0 * y2) / ((y1 - y0) * (y1 - y2))) +
((x2 * y0 * y1) / ((y2 - y0) * (y2 - y1)))
// If IQI is not possible use the secant method of linear interpolation
} else {
x = x1 - (y2 * ((x1 - x0) / (y1 - y0)))
}
let delta = Math.abs(2 * Number.EPSILON * Math.abs(x1))
let currentStep = Math.abs(x - x1)
let previousStep = Math.abs(x1 - x2)
let secondStep = Math.abs(x2 - x3)
// Use the bisection method under these conditions:
if (
// (x < ((3 * x0) + x1) / 4 && x > x1) ||
((x - (((3 * x0) + x1) / 4)) * (x - x1) >= 0) ||
// If interpolation resulting in a very small step, use bisection instead
(bisected && currentStep >= previousStep / 2) ||
(!bisected && currentStep >= secondStep / 2) ||
// If previous steps were very close to b then use bisection
(bisected && previousStep < delta) ||
(!bisected && secondStep < delta)
) {
x = (x0 + x1) / 2
bisected = true
} else {
bisected = false
}
let y = f(x)
// Stop if it's found a suitable on a root
if (Math.abs(y) < yTolerance) {
return x
}
// Set c and d to the previous steps
x3 = x2
x2 = x1
// Choose the new a and b to bracket the intercept
if ((y0 * y < 0)) {
// b = s
x1 = x
y1 = y
} else {
// a = s
x0 = x
y0 = y
}
// Swap lower and upper bounds so that y1 is closest to 0
// This makes b a better approximation of the intercept than a
if (Math.abs(y0) < Math.abs(y1)) {
let temporary = x0
x0 = x1
x1 = temporary
temporary = y0
y0 = y1
y1 = temporary
}
}
// Max iterations hit and no root found
return false
}