Root Finding — Numerical Root-Finding Algorithms
Overview
The sangi root-finding module is organized into three categories.
- 1D root finding — find real $x$ satisfying $f(x) = 0$. Over 10 methods from bisection to Brent
- Polynomial roots — find all roots (including complex) of $p(x) = 0$. Closed-form for low degree, iterative for higher
- N-D root finding — find vector $\mathbf{x}$ satisfying $\mathbf{F}(\mathbf{x}) = \mathbf{0}$
Build
// Include all modules at once
#include <math/roots/root_finding.hpp>
// Or include individually
#include <math/roots/root_finding_1d.hpp> // 1D root finding
#include <math/roots/root_finding_nd.hpp> // N-D root finding
#include <math/roots/polynomial_roots.hpp> // Polynomial roots
Header-only. No library linkage required.
Result Types and Convergence Criteria
RootFindingResult<R>
The unified return type used by every 1D, polynomial-iterative, and N-D root finder. Defined in math/roots/root_finding_base.hpp. The template parameter R dispatches automatically: scalar T for 1D, vector V for N-D.
template<typename R>
struct RootFindingResult {
using error_type = /* R if scalar, R::value_type if vector */;
std::optional<R> root; // root approximation (scalar or vector) on success, std::nullopt on failure
bool converged; // true when convergence criteria were met
size_t iterations; // number of iterations actually performed
error_type error_estimate; // |residual| / |step| / bracket-width estimate
};
1D solvers return RootFindingResult<double>, N-D solvers return RootFindingResult<Vector<double>>.
| Member | Type | Meaning |
|---|---|---|
root | std::optional<R> | Approximate root on success; std::nullopt on failure |
converged | bool | Whether the criteria were satisfied. Partial-success cases (e.g. max-iter reached with a usable value) may still set this to true |
iterations | size_t | Iterations actually executed (steps taken, or max_iterations when the cap is hit) |
error_estimate | error_type | Absolute estimated final error (residual, last-step width, bracket width, ...) |
Usage:
auto r = brent_method(f, a, b);
if (r.converged && r.root) {
double x = *r.root;
// ... use x
} else {
// failed (no sign change / max-iter / numerical singularity / ...)
std::cerr << "no root: iter=" << r.iterations
<< " err=" << r.error_estimate << '\n';
}
ConvergenceCriteria<T>
Holds the convergence thresholds. Every root finder takes it as a trailing argument (defaulted).
template<concepts::OrderedField T>
struct ConvergenceCriteria {
T abs_ftol = std::numeric_limits<T>::epsilon() * 100; // |f(x)| absolute tol
T abs_xtol = std::numeric_limits<T>::epsilon() * 100; // |Δx| absolute tol
T rel_ftol = std::numeric_limits<T>::epsilon() * 1000; // |f| relative tol
T rel_xtol = std::numeric_limits<T>::epsilon() * 1000; // Δx relative tol
size_t max_iterations = 100; // iteration cap
};
| Member | Meaning | Default (T=double) |
|---|---|---|
abs_ftol | $|f(x)|$ below this counts as converged (absolute) | $\approx 2.2 \times 10^{-14}$ |
abs_xtol | $|\Delta x|$ or bracket width $|b - a|$ below this counts as converged | $\approx 2.2 \times 10^{-14}$ |
rel_ftol | $|f_n - f_{n-1}| / |f_{n-1}|$ below this counts as converged | $\approx 2.2 \times 10^{-13}$ |
rel_xtol | $|x_n - x_{n-1}| / \max(1, |x_{n-1}|)$ below this counts as converged | $\approx 2.2 \times 10^{-13}$ |
max_iterations | Iteration cap. Exceeding it counts as failure | 100 |
Example:
// Loose criteria for fast cutoff
ConvergenceCriteria<double> loose;
loose.abs_ftol = 1e-6;
loose.max_iterations = 50;
auto r = newton_raphson(f, df, x0, loose);
// Tight criteria with arbitrary precision
ConvergenceCriteria<Float> tight;
tight.abs_ftol = Float("1e-50");
tight.max_iterations = 200;
auto r2 = brent_method<Float>(f, a, b, tight);
1D: Bracketing Methods
Require an interval $[a, b]$ where $f(a)$ and $f(b)$ have opposite signs. Guaranteed to converge.
| Function | Convergence | Description |
|---|---|---|
bisection(f, a, b) | Linear | Bisection. Simplest and most reliable |
regula_falsi(f, a, b) | Superlinear–linear | False position (Regula Falsi, basic form). Can stagnate on convex/concave f |
illinois_method(f, a, b) | Superlinear | False position + Illinois correction. Stagnation-free; for production |
ridders_method(f, a, b) | $\sqrt{2}$ | Ridders' method. Faster than bisection |
brent_method(f, a, b) | Superlinear | Brent's method. Best practical choice |
alefeld_potra_shi(f, a, b) | $\approx 1.84$ | Alefeld-Potra-Shi (TOMS Algorithm 748). Improved Brent |
anderson_bjork_method(f, a, b) | Superlinear | Anderson-Björck method. Illinois-style improvement, simpler than King-Pegasus |
Function reference
bisection
template<concepts::OrderedField T>
RootFindingResult<T> bisection(
const std::function<T(T)>& f, T a, T b,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: Halves the interval $[a, b]$ each step and keeps the side where $f$ changes sign. Linear convergence (error halves per iteration).
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function to find a root of |
a | T | Lower bracket ($f(a) \cdot f(b) < 0$ required) |
b | T | Upper bracket |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional, default-constructed) |
Returns: RootFindingResult<T>. If the interval has no sign change, returns converged = false, root = std::nullopt.
Use when: robustness is paramount and speed is secondary. The worst-case iteration count is predictable: log2((b-a)/tol).
auto r = bisection<double>([](double x){ return x*x - 2.0; }, 1.0, 2.0);
if (r.converged) std::cout << *r.root;
// Run output: iter=42 root=1.41421356237311
// matches the true sqrt(2)=1.41421356237310 to 12 digits
regula_falsi
template<concepts::OrderedField T>
RootFindingResult<T> regula_falsi(
const std::function<T(T)>& f, T a, T b,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: The basic false-position method (Regula Falsi). The line through the bracket endpoints meets the x-axis at $c$, which becomes the next trial point; the endpoint sharing the sign of $f(c)$ is replaced by $c$, keeping a sign-changing bracket. Convergence is always guaranteed (the root stays bracketed), but when $f$ is convex (or concave) one endpoint stays fixed — stagnation — and convergence can degrade to linear. For the stagnation-free production version see illinois_method.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
a | T | Lower bracket ($f(a) \cdot f(b) < 0$ required) |
b | T | Upper bracket |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use: the plain Regula Falsi, for teaching/reference. In practice prefer the stagnation-free variants illinois_method (Illinois), king_method (Pegasus family), or anderson_bjork_method.
illinois_method
template<concepts::OrderedField T>
RootFindingResult<T> illinois_method(
const std::function<T(T)>& f, T a, T b,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: The basic false position (see regula_falsi) with the Illinois correction. Plain Regula Falsi can stagnate on convex/concave functions, degrading to linear order; when one endpoint fails to update for two consecutive steps, scaling fa or fb by weight $1/2$ avoids stagnation and restores super-linear convergence. For even faster variants see anderson_bjork_method and king_method (Pegasus).
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
a | T | Lower bracket ($f(a) \cdot f(b) < 0$ required) |
b | T | Upper bracket |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
ridders_method
template<concepts::OrderedField T>
RootFindingResult<T> ridders_method(
const std::function<T(T)>& f, T a, T b,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: Combines the bracket midpoint $m$ with an exponentially weighted false-position step. Each iteration evaluates $f$ twice in exchange for $\sqrt{2}$ super-linear order (effective rate ~ 1.84).
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
a | T | Lower bracket ($f(a) \cdot f(b) < 0$ required) |
b | T | Upper bracket |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use when: faster than bisection, simpler than Brent. A good middle ground when function evaluations are cheap.
brent_method
template<concepts::OrderedField T>
RootFindingResult<T> brent_method(
const std::function<T(T)>& f, T a, T b,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: Brent (1973) hybrid that switches between inverse-quadratic interpolation, the secant method, and bisection based on internal state. Any non-shrinking step is forced to a bisection so both speed and guaranteed convergence are retained.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
a | T | Lower bracket ($f(a) \cdot f(b) < 0$ required) |
b | T | Upper bracket |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use when: the recommended default for 1D root finding. This is the same algorithm as SciPy's brentq. As long as the endpoints have opposite signs, the function always returns a root.
auto r = brent_method<double>([](double x){ return std::cos(x) - x; }, 0.0, 1.0);
if (r.converged) std::cout << *r.root;
// Run output: iter=26 root=0.739085133215161
// matches the Dottie number 0.7390851332151607 to 14 digits
alefeld_potra_shi
template<concepts::OrderedField T>
RootFindingResult<T> alefeld_potra_shi(
const std::function<T(T)>& f, T a, T b,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: Alefeld-Potra-Shi (1995) TOMS Algorithm 748. Combines inverse cubic interpolation with double-length steps, achieving a higher effective rate (~ 1.84) than Brent on smooth $f$.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
a | T | Lower bracket ($f(a) \cdot f(b) < 0$ required) |
b | T | Upper bracket |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use when: a guaranteed bracketing method that is competitive with Brent on smooth problems. Drop-in replacement for SciPy's brentq.
anderson_bjork_method
template<concepts::OrderedField T>
RootFindingResult<T> anderson_bjork_method(
const std::function<T(T)>& f, T a, T b,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: Improved Regula Falsi. When the same endpoint stagnates for two consecutive iterations, the stagnant function value is rescaled by the weight $m = 1 - f_c / f_b$ (falling back to $m = 1/2$ if $m \le 0$) before the next interpolation. This achieves faster super-linear convergence than the Illinois method (fixed weight $1/2$), comparable to King-Pegasus.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
a | T | Lower bracket ($f(a) \cdot f(b) < 0$ required) |
b | T | Upper bracket |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use when: a fast bracketing super-linear method is desired over plain regula_falsi. Reference: Anderson & Björck, "A new high order method of regula falsi type for computing a root of an equation", BIT (1973).
1D: Non-bracketing Methods
Start from initial value $x_0$ (and optionally derivatives). Faster convergence but initial-value dependent.
| Function | Convergence | Description |
|---|---|---|
newton_raphson(f, f', x0) | Quadratic | Newton-Raphson. Requires derivative |
secant_method(f, x0, x1) | $\approx 1.618$ | Secant. Derivative-free (uses finite differences from 2 points) |
fixed_point_iteration(g, x0) | Linear (general) | Fixed-point (Picard) iteration. $g$ is the iteration map; solves $x = g(x)$ |
steffensen_method(f, x0) | 2nd order | Steffensen's method. Quadratic convergence without derivatives (Aitken $\Delta^2$ acceleration) |
sidi_method(f, x0, x1, criteria, memory) | $\to 2$ (tunable) | Sidi's generalized secant. Interpolates the last $K+1$ points. memory=2 reduces to the secant method |
inverse_quadratic_interpolation(f, x0, x1, x2) | $\approx 1.84$ | Inverse quadratic interpolation (standalone). Brent's internal step; meant to be paired with a safeguard |
muller_method(f, z0, z1, z2) | $\approx 1.84$ | Müller's method. Parabolic interpolation reaches complex roots from real-ish starts |
Function reference
newton_raphson
template<concepts::OrderedField T>
RootFindingResult<T> newton_raphson(
const std::function<T(T)>& f,
const std::function<T(T)>& df,
T x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: $x_{n+1} = x_n - f(x_n) / f'(x_n)$. Quadratic convergence near a simple root ($|x_{n+1} - x^*| \le C |x_n - x^*|^2$); degrades to linear at multiple roots.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
df | const std::function<T(T)>& | First derivative of $f$ |
x0 | T | Initial guess |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Note: triggers a near-stationary detection when $|f'(x)| < \epsilon \cdot 10$ and returns converged = false. A poor initial guess can converge to a different root or diverge; prefer brent_method when a bracket is available.
// f(x) = x^2 - 2 by Newton's method
auto r = newton_raphson<double>(
[](double x) { return x*x - 2.0; },
[](double x) { return 2.0 * x; },
1.0);
if (r.converged) std::cout << *r.root;
// Run output: iter=5 root=1.4142135623731
// matches sqrt(2)=1.41421356237310 to 15 digits — 5 iterations show quadratic convergence
secant_method
template<concepts::OrderedField T>
RootFindingResult<T> secant_method(
const std::function<T(T)>& f,
T x0, T x1,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: $x_{n+1} = x_n - f(x_n)\, (x_n - x_{n-1}) / (f(x_n) - f(x_{n-1}))$. Uses a finite-difference approximation of the derivative; slightly slower than Newton (golden-ratio order $\approx 1.618$).
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
x0 | T | First initial point (need not bracket the root, but not too close to x1) |
x1 | T | Second initial point |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Note: stalls when $|f(x_n) - f(x_{n-1})| < \epsilon$ (slope ≈ 0); returns converged = false.
// f(x) = x - cos(x), root = Dottie number
auto r = secant_method<double>(
[](double x) { return x - std::cos(x); }, 0.0, 1.0);
if (r.converged) std::cout << *r.root;
// Run output: iter=6 root=0.739085133215161
// matches the Dottie number 0.7390851332151607 to 14 digits
fixed_point_iteration
template<concepts::OrderedField T>
RootFindingResult<T> fixed_point_iteration(
const std::function<T(T)>& g,
T x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: iterates $x_{n+1} = g(x_n)$ to find a fixed point $x^* = g(x^*)$ (i.e. a solution of $x = g(x)$). Note that $g$ is the iteration map itself, not the $f$ of $f(x)=0$. If $g$ is a contraction near $x^*$ ($|g'(x^*)| < 1$) it converges linearly; in particular it is at least quadratic when $g'(x^*) = 0$. It diverges when $|g'(x^*)| > 1$.
Parameters:
| Argument | Type | Description |
|---|---|---|
g | const std::function<T(T)>& | Iteration map (solves $x = g(x)$; not the $f$ of $f(x)=0$) |
x0 | T | Initial guess |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional; uses $|x_{n+1} - x_n|$) |
Note: when $g$ is not a contraction ($|g'(x^*)| \ge 1$) it diverges and returns converged = false after reaching the iteration limit (the best estimate is still returned). newton_raphson is the special case of fixed-point iteration with $g(x) = x - f(x)/f'(x)$.
// g(x) = cos(x), fixed point = Dottie number
auto r = fixed_point_iteration<double>(
[](double x) { return std::cos(x); }, 0.0);
if (r.converged) std::cout << *r.root;
// g(x) = cos x is a contraction (|g'(x)| = |sin x| < 1), so it converges to x* = 0.7390851332151607
steffensen_method
template<concepts::OrderedField T>
RootFindingResult<T> steffensen_method(
const std::function<T(T)>& f,
T x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
How it works: update $x_{n+1} = x_n - \frac{f(x_n)^2}{f(x_n + f(x_n)) - f(x_n)}$. The denominator $f(x+f)-f(x)$ approximates $f(x)\,f'(x)$, giving Newton's quadratic convergence with no derivative (Aitken $\Delta^2$ acceleration applied to fixed-point iteration). Unlike the secant method it keeps only one point. Two $f$ evaluations per iteration.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function whose root is sought |
x0 | T | Initial guess (start near the root; may diverge from far away) |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Note: stalls when the denominator is near zero → converged=false. No global convergence guarantee; diverges if the start is far from the root.
// f(x) = x^2 - 2, root = √2, derivative-free
auto r = steffensen_method<double>(
[](double x) { return x * x - 2.0; }, 1.5);
if (r.converged) std::cout << *r.root;
// Result: iter=4 root=1.414213562373095
sidi_method
template<concepts::OrderedField T>
RootFindingResult<T> sidi_method(
const std::function<T(T)>& f,
T x0, T x1,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>(),
size_t memory = 3);
How it works: uses the derivative at the newest point of the Newton interpolating polynomial $p$ through the last $K+1$ points: $x_{n+1} = x_n - f(x_n)/p'(x_n)$. With memory $=2$ it matches the secant method ($\varphi \approx 1.618$); larger values raise the order of convergence (toward $2$). Derivative-free.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function whose root is sought |
x0, x1 | T | Two initial points |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
memory | size_t | Number of points $K+1$ used for interpolation ($\geq 2$, optional; default 3 = quadratic; 2 = secant) |
// f(x) = x^2 - 2, root = √2, quadratic interpolation (memory=3)
auto r = sidi_method<double>(
[](double x) { return x * x - 2.0; }, 1.0, 2.0,
ConvergenceCriteria<double>(), 3);
if (r.converged) std::cout << *r.root;
// Result: iter=6 root=1.414213562373095
inverse_quadratic_interpolation
template<concepts::OrderedField T>
RootFindingResult<T> inverse_quadratic_interpolation(
const std::function<T(T)>& f,
T x0, T x1, T x2,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
How it works: evaluates the quadratic interpolant of the inverse function $x = g(f)$ through 3 points at $f = 0$ ($x = \sum_i x_i \prod_{j \neq i} f_j/(f_i - f_j)$). This is Brent's method's internal step made available standalone. Derivative-free, super-linear ($\approx 1.84$), but unstable when the function values cluster, so it is normally paired with a safeguard (Brent).
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function whose root is sought |
x0, x1, x2 | T | Three distinct initial points (with distinct $f$ values) |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Note: interpolation breaks down when the 3 $f$ values cluster → converged=false.
// f(x) = x^2 - 2, root = √2
auto r = inverse_quadratic_interpolation<double>(
[](double x) { return x * x - 2.0; }, 1.0, 1.3, 1.7);
if (r.converged) std::cout << *r.root;
// Result: iter=5 root=1.414213562373095
muller_method
template<concepts::OrderedField T>
RootFindingResult<Complex<T>> muller_method(
const std::function<Complex<T>(Complex<T>)>& f,
Complex<T> z0, Complex<T> z1, Complex<T> z2,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
How it works: steps to the root (nearest the newest point) of the parabola through 3 points. Because it proceeds in complex arithmetic even when the discriminant is negative, it can reach complex roots from real-ish initial points (useful for polynomial complex roots). Order $\approx 1.84$, derivative-free. $f$ must be evaluable at complex arguments.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<Complex<T>(Complex<T>)>& | Target function (must be evaluable at complex arguments) |
z0, z1, z2 | Complex<T> | Three distinct initial points |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional; uses abs_ftol on $|f|$ and abs_xtol on $|\Delta z|$) |
Note: the return type is RootFindingResult<Complex<T>> (the root is complex). Which complex root it converges to depends on the initial points.
// z^2 + 1 = 0 → ±i (complex root from real-ish starts)
std::function<Complex<double>(Complex<double>)> g =
[](Complex<double> z) { return z * z + Complex<double>(1.0); };
auto r = muller_method<double>(g, {1.0}, {0.5}, {0.0, 0.3});
if (r.converged) std::cout << r.root->re << " + " << r.root->im << "i";
// Result: iter=2 root=0 + 1i (= i)
1D: Higher-Order Methods
| Function | Convergence | Description |
|---|---|---|
halley_method(f, f', f'', x0) | Cubic | Halley's method. Needs 1st and 2nd derivatives |
schroder_method(f, f', f'', x0) | Quadratic (incl. multiple roots) | Schröder's method (Householder order 2). Keeps quadratic convergence at multiple roots |
king_method(f, a, b) | Superlinear | King-Pegasus (BIT 1973). A bracketing method taking an interval $[a, b]$. Improvement on Regula Falsi, faster than Illinois |
popovski_method(f, f', x0) | 4th order | Popovski's method. 4th-order convergence with only the 1st derivative |
householder_method(f, f', f'', f''', x0) | 4th order | Householder's method (order 3). Extends Newton ($d{=}1$) and Halley ($d{=}2$). Needs 1st–3rd derivatives |
Function reference
halley_method
template<concepts::OrderedField T>
RootFindingResult<T> halley_method(
const std::function<T(T)>& f,
const std::function<T(T)>& df,
const std::function<T(T)>& d2f,
T x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: $x_{n+1} = x_n - 2 f f' / (2 f'^2 - f f'')$. Householder order 1, lifting Newton's quadratic convergence to cubic. Needs the 2nd derivative; roughly twice the cost per iteration of Newton but with fewer iterations.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
df | const std::function<T(T)>& | First derivative of $f$ |
d2f | const std::function<T(T)>& | Second derivative of $f$ |
x0 | T | Initial guess |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Note: triggers near-singular detection when the denominator $2 f'^2 - f f''$ becomes too small.
// f(x) = x^3 - 2, root = 2^(1/3)
auto r = halley_method<double>(
[](double x) { return x*x*x - 2.0; },
[](double x) { return 3.0 * x * x; },
[](double x) { return 6.0 * x; },
1.0);
if (r.converged) std::cout << *r.root;
// Run output: iter=3 root=1.25992104989487
// matches 2^(1/3)=1.2599210498948732 to 14 digits — 3 iterations showcase cubic convergence
schroder_method
template<concepts::OrderedField T>
RootFindingResult<T> schroder_method(
const std::function<T(T)>& f,
const std::function<T(T)>& df,
const std::function<T(T)>& d2f,
T x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: $x_{n+1} = x_n - f f' / (f'^2 - f f'')$. Schröder (1870) designed this to retain quadratic convergence even at multiple roots, whereas plain Newton degrades to linear.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
df | const std::function<T(T)>& | First derivative of $f$ |
d2f | const std::function<T(T)>& | Second derivative of $f$ |
x0 | T | Initial guess |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use when: dealing with multiple roots of polynomials; more stable than Halley in that case. For simple roots Halley (cubic order) is faster.
king_method
template<concepts::OrderedField T>
RootFindingResult<T> king_method(
const std::function<T(T)>& f, T a, T b,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: After a Regula Falsi step, if the same endpoint stagnates the fixed endpoint's value is rescaled $f_1 \leftarrow f_1 \cdot f_2 / (f_2 + f)$ before re-interpolating. Lineage: Illinois (fixed weight $1/2$) → Pegasus (Dowell & Jarratt, 1972; weight $f_2/(f_2+f)$) → King (1973). King's method improves Pegasus by removing one or two of its slower substeps for higher asymptotic efficiency (order $\approx 1.839$ using only first-order divided differences), so the name king_method is not a mislabel — it denotes King's improvement of Pegasus. Faster super-linear convergence than the Illinois method.
References: R. F. King, "An improved Pegasus method for root finding", BIT 13, 423–427 (1973); M. Dowell & P. Jarratt, "The Pegasus method for computing the root of an equation", BIT 12, 503–508 (1972).
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
a | T | Lower bracket ($f(a) \cdot f(b) < 0$ required) |
b | T | Upper bracket |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
popovski_method
template<concepts::OrderedField T>
RootFindingResult<T> popovski_method(
const std::function<T(T)>& f,
const std::function<T(T)>& df,
T x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: Two-step update (Newton step + correction term) reaching 4th-order convergence using only the 1st derivative. King-Werner / Ostrowski family. Trades the 2nd derivative for 2 function evaluations per iteration.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function |
df | const std::function<T(T)>& | First derivative of $f$ |
x0 | T | Initial guess |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use when: the 2nd derivative is expensive (e.g. AD chains) but function evaluations are cheap.
householder_method
template<concepts::OrderedField T>
RootFindingResult<T> householder_method(
const std::function<T(T)>& f,
const std::function<T(T)>& df,
const std::function<T(T)>& d2f,
const std::function<T(T)>& d3f,
T x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
How it works: the order-$d{=}3$ member of the Householder family. It extends Newton ($d{=}1$) and Halley ($d{=}2$) to 4th-order convergence: $x_{n+1} = x_n - \frac{6 f f'^2 - 3 f^2 f''}{6 f'^3 - 6 f f' f'' + f^2 f'''}$. Requires the 1st–3rd derivatives.
Parameters:
| Argument | Type | Description |
|---|---|---|
f | const std::function<T(T)>& | Univariate real function whose root is sought |
df, d2f, d3f | const std::function<T(T)>& | 1st, 2nd and 3rd derivatives of $f$ |
x0 | T | Initial guess |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Note: diverges when the denominator $6 f'^3 - 6 f f' f'' + f^2 f'''$ is near zero. Pays off over Newton/Halley when the 3rd derivative is cheap (symbolic / AD).
// f(x) = x^3 - 2, root = 2^(1/3)
auto r = householder_method<double>(
[](double x) { return x*x*x - 2.0; }, // f
[](double x) { return 3.0*x*x; }, // f'
[](double x) { return 6.0*x; }, // f''
[](double) { return 6.0; }, // f'''
1.5);
if (r.converged) std::cout << *r.root;
// Result: iter=3 root=1.259921049894873 (= 2^(1/3))
Polynomial Roots
All functions return std::vector<Complex<T>> (real roots have imaginary part = 0).
Roots are a collection of complex numbers, not elements of a vector space, so the result uses
the STL std::vector rather than sangi::Vector (which is reserved for linear algebra).
The input Polynomial<T> stores coefficients in ascending order (p[0] = constant term).
Low Degree (Closed-Form)
| Function | Description |
|---|---|
solveLinear(p) | Degree 1: $ax + b = 0$ |
solveQuadratic(p) | Degree 2: monic + computes the larger-magnitude root first then derives the other via Vieta (cancellation-safe) |
solveCubic(p) | Degree 3: Cardano's formula, with a trigonometric branch when the discriminant is negative |
solveQuartic(p) | Degree 4: Ferrari's method (resolvent cubic + two quadratics) |
High Degree (Iterative)
| Function | Description |
|---|---|
jenkinsTraub(p) | Three-stage Jenkins-Traub. Most numerically stable; recommended default for degrees 5-19 |
laguerre(p, eps, maxIter) | Laguerre + deflation. Cubic convergence with global convergence to all roots |
durandKernerAberth(p, eps, maxIter) | Durand-Kerner-Aberth + Newton polish. Simultaneous all-roots iteration; recommended for degree ≥ 20 |
bairstow(p, eps, maxIter) | Bairstow. Extracts quadratic factors with real-only arithmetic; complex roots come out as conjugate pairs |
weierstrass(p, eps, maxIter) | Durand-Kerner (Weierstrass). Classic simultaneous iteration (no Aberth correction) |
graeffe(p, maxSquarings, eps) | Graeffe root-squaring. For real roots with separated moduli (historical method) |
lehmerSchur(p, tol, maxDescend) | Lehmer-Schur. Subdivides the complex plane into disks and localizes roots via the Schur-Cohn test (roots on the unit circle too) |
Function reference
solveLinear / solveQuadratic / solveCubic / solveQuartic
template<typename T> std::vector<Complex<T>> solveLinear(const Polynomial<T>& p);
template<typename T> std::vector<Complex<T>> solveQuadratic(const Polynomial<T>& p);
template<typename T> std::vector<Complex<T>> solveCubic(const Polynomial<T>& p);
template<typename T> std::vector<Complex<T>> solveQuartic(const Polynomial<T>& p);
Behaviour: closed-form solutions for the respective degrees. Degree mismatch triggers an assertion.
Parameters:
| Argument | Type | Description |
|---|---|---|
p | const Polynomial<T>& | Polynomial (coefficients in ascending order; degree 1/2/3/4 per function) |
Cancellation handling: solveQuadratic computes the larger-magnitude root directly and derives the other via Vieta's formula when the discriminant is small (Numerical Recipes recommendation; see also Okumura, Algorithm Encyclopedia, p.205).
// x^2 - 5x + 6 = (x-2)(x-3) = 0
Polynomial<double> p({6.0, -5.0, 1.0}); // ascending: c0=6, c1=-5, c2=1
auto roots = solveQuadratic(p);
for (auto& r : roots) std::cout << r.re << ' ';
// Run output: "3 2" (matches true {2, 3} exactly)
jenkinsTraub
template<typename T>
std::vector<Complex<T>> jenkinsTraub(const Polynomial<T>& poly);
Behaviour: Jenkins-Traub (1970) RPOLY algorithm (three-stage shift-shift-shift). Computes roots one at a time and deflates the polynomial. The de-facto reference standard for the past several decades.
Parameters:
| Argument | Type | Description |
|---|---|---|
poly | const Polynomial<T>& | Polynomial (coefficients in ascending order) |
Use when: the recommended default for medium-degree polynomials (5-19). solvePolynomial dispatches here for that range.
// x^6 - 1 = 0 (the six 6th roots of unity)
Polynomial<double> p({-1.0, 0, 0, 0, 0, 0, 1.0});
auto roots = jenkinsTraub(p);
// Run output: 6 roots = {±1, ±exp(±iπ/3)}
// max |p(root)| = 4.97e-16 (16-digit precision)
laguerre
template<typename T>
std::vector<Complex<T>> laguerre(
const Polynomial<T>& poly,
T eps = std::numeric_limits<T>::epsilon() * T(100),
size_t maxIter = 1000);
Behaviour: Laguerre + deflation. Each step computes $L(z) = -n / (G \pm \sqrt{(n-1)(n H - G^2)})$ where $G = p'/p$ and $H = G^2 - p''/p$. Globally convergent to all roots from any initial guess, with cubic order.
Parameters:
| Argument | Type | Description |
|---|---|---|
poly | const Polynomial<T>& | Polynomial (coefficients in ascending order) |
eps | T | Convergence tolerance (optional, default $\epsilon \cdot 100$) |
maxIter | size_t | Iteration cap (optional, default 1000) |
durandKernerAberth
template<typename T>
std::vector<Complex<T>> durandKernerAberth(
const Polynomial<T>& poly,
T eps = std::numeric_limits<T>::epsilon() * T(100),
size_t maxIter = 1000);
Behaviour: Aberth-Ehrlich simultaneous iteration ($z_i \leftarrow z_i - (p(z_i)/p'(z_i)) / (1 - (p(z_i)/p'(z_i)) \cdot \sum_{j \ne i} 1/(z_i - z_j))$) followed by a Newton polish against the original polynomial. Because all roots are updated together, deflation error does not accumulate.
Parameters:
| Argument | Type | Description |
|---|---|---|
poly | const Polynomial<T>& | Polynomial (coefficients in ascending order) |
eps | T | Convergence tolerance (optional, default $\epsilon \cdot 100$) |
maxIter | size_t | Iteration cap (optional, default 1000) |
Use when: the recommended default for degree ≥ 20. solvePolynomial dispatches here for that range.
bairstow
template<typename T>
std::vector<Complex<T>> bairstow(
const Polynomial<T>& poly,
T eps = std::numeric_limits<T>::epsilon() * T(100),
size_t maxIter = 1000);
Behaviour: divides the polynomial by a trial quadratic factor $x^2 + rx + s$ and uses Newton's method to refine $r, s$ until the remainder vanishes, then extracts the quadratic factor. Complex (conjugate-pair) roots emerge naturally from real-only arithmetic. Reference: Press et al., Numerical Recipes §9.5.
Parameters:
| Argument | Type | Description |
|---|---|---|
poly | const Polynomial<T>& | Polynomial (real coefficients, ascending order) |
eps | T | Convergence tolerance (optional, default $\epsilon \cdot 100$) |
maxIter | size_t | Iteration cap (optional, default 1000) |
Use when: targeting embedded environments that prefer to avoid complex arithmetic, or when complex roots should be handled as paired units.
weierstrass
template<typename T>
std::vector<Complex<T>> weierstrass(
const Polynomial<T>& poly,
T eps = std::numeric_limits<T>::epsilon() * T(100),
size_t maxIter = 1000);
How it works: the classic Durand-Kerner (Weierstrass) method that updates all roots simultaneously. Unlike durandKernerAberth it carries no Aberth correction (plain simultaneous iteration, quadratic convergence): $z_i \leftarrow z_i - \frac{P(z_i)}{\mathrm{lc}\cdot\prod_{j \neq i}(z_i - z_j)}$. Starting from complex seeds $(0.4+0.9i)^i$, it converges to all roots (including complex conjugate pairs) when there are no repeated roots.
Parameters:
| Argument | Type | Description |
|---|---|---|
poly | const Polynomial<T>& | Polynomial (ascending order) |
eps | T | Convergence tolerance (optional, default $\epsilon \cdot 100$) |
maxIter | size_t | Iteration cap (optional, default 1000) |
Note: near repeated/clustered roots the denominator $\prod_{j \neq i}(z_i - z_j)$ approaches zero and convergence slows. For more numerical robustness use durandKernerAberth (the Aberth-corrected variant).
// (x-2)(x^2+1) = x^3 - 2x^2 + x - 2 → 2, ±i
Polynomial<double> p({-2, 1, -2, 1});
auto roots = weierstrass(p);
// Result: roots = { 2, i, -i }
graeffe
template<typename T>
std::vector<Complex<T>> graeffe(
const Polynomial<T>& poly,
int maxSquarings = 6,
T eps = std::numeric_limits<T>::epsilon() * T(100));
How it works: Graeffe root-squaring. It repeatedly forms $p(x)\,p(-x) = s(x^2)$, squaring the roots $r_i \to r_i^{2^m}$, and reads the moduli of separated roots from the monic coefficient ratios: $|r_i| \approx \left|\frac{a_{n-i}}{a_{n-i+1}}\right|^{1/2^m}$. Signs of real roots are recovered from $P(\pm|r_i|)$, then a few Newton steps polish each root. Because the coefficients grow doubly-exponentially, each squaring is renormalized to monic and squaring stops just before overflow.
Parameters:
| Argument | Type | Description |
|---|---|---|
poly | const Polynomial<T>& | Polynomial (ascending order) |
maxSquarings | int | Number of squarings (optional, default 6 = exponent $2^6 = 64$) |
eps | T | Tolerance for the Newton polish (optional) |
Scope: for real roots with separated moduli. Equal-modulus roots, complex conjugate pairs and repeated roots cannot be separated by basic Graeffe (a historical method). For general complex roots use jenkinsTraub / durandKernerAberth / weierstrass.
// (x-1)(x-2)(x-4) = x^3 - 7x^2 + 14x - 8 → 4, 2, 1
Polynomial<double> p({-8, 14, -7, 1});
auto roots = graeffe(p);
// Result (descending modulus): roots = { 4, 2, 1 }
lehmerSchur
template<typename T>
std::vector<Complex<T>> lehmerSchur(
const Polynomial<T>& poly,
T tol = std::numeric_limits<T>::epsilon() * T(100),
size_t maxDescend = 200);
How it works: covers the disk containing all roots (Cauchy bound) with nine half-radius sub-disks (center + 8 around) and applies the Schur-Cohn test (Lehmer's unit-disk test) to each, descending into a sub-disk that contains a root. Once the radius is small enough it polishes the center with Newton's method and deflates, repeating degree-many times for all roots. The test iterates the exact Schur transform $T q = \overline{a_0}\,q - a_n\,q^*$ until the degree drops (a root lies inside whenever $|a_0|^2 - |a_n|^2 < 0$). Subdivision supplies the seed and Newton supplies the accuracy, so complex roots (including on the unit circle) are resolved.
Parameters:
| Argument | Type | Description |
|---|---|---|
poly | const Polynomial<T>& | Polynomial (ascending order) |
tol | T | Tolerance for the test and the polish (optional, default $\epsilon \cdot 100$) |
maxDescend | size_t | Max subdivision-descent steps per root (optional, default 200) |
Use when: you want geometric localization of roots, or to reliably separate roots on the unit circle / clustered complex roots. For the most numerically stable general solver use jenkinsTraub.
// x^4 - 1 = 0 → 1, -1, i, -i (complex roots on the unit circle)
Polynomial<double> p({-1, 0, 0, 0, 1});
auto roots = lehmerSchur(p);
// Result: roots = { 1, -1, i, -i }
Unified Dispatcher
template<typename T>
std::vector<Complex<T>> solvePolynomial(
const Polynomial<T>& p,
T eps = std::numeric_limits<T>::epsilon() * T(100),
size_t maxIter = 1000);
solvePolynomial picks the best algorithm by degree:
- Degree 1:
solveLinear(closed-form) - Degree 2:
solveQuadratic(closed-form, cancellation-safe) - Degree 3:
solveCubic(Cardano + trig) - Degree 4:
solveQuartic(Ferrari) - Degree 5-19:
jenkinsTraub(one root at a time + deflation, high accuracy) - Degree ≥ 20:
durandKernerAberth(simultaneous roots + Newton polish, avoids deflation drift)
N-D: Multivariate Root Finding
Find zeros of $\mathbf{F}: \mathbb{R}^n \to \mathbb{R}^n$. The return type is RootFindingResult<V> (a vector-typed specialisation of the same unified template used by 1D solvers).
| Function | Description |
|---|---|
newton_raphson_nd(F, J, x0) | Multivariate Newton. Requires a Jacobian function |
broyden_method(F, x0) | Broyden's method. Jacobian-free (quasi-Newton, rank-1 update) |
newton_krylov(F, x0) | Newton-Krylov (JFNK). F only. Approximates J·v by finite differences and solves with GMRES (matrix-free) |
newton_fd(F, x0) | Finite-difference Newton. F only. Builds an explicit Matrix Jacobian by differences and solves directly (robust at small/medium scale) |
levenberg_marquardt(F, J, x0) | Levenberg-Marquardt with damping; robust against poor initial guesses |
powell_hybrid(F, x0) | Powell's hybrid (trust region + dogleg). Jacobian-free (finite-difference internally) |
Function reference
newton_raphson_nd
template<concepts::OrderedField T, typename V, typename M>
requires concepts::VectorOf<V, T> && concepts::MatrixOf<M, T>
RootFindingResult<V> newton_raphson_nd(
const std::function<V(const V&)>& F,
const std::function<M(const V&)>& J,
const V& x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: each iteration $\mathbf{x}_{n+1} = \mathbf{x}_n - J(\mathbf{x}_n)^{-1} F(\mathbf{x}_n)$. On a singular Jacobian the diagonal is regularised by abs_xtol * 10 and solve is retried.
Parameters:
| Argument | Type | Description |
|---|---|---|
F | const std::function<V(const V&)>& | Vector-valued system $\mathbf{F}: \mathbb{R}^n \to \mathbb{R}^n$ |
J | const std::function<M(const V&)>& | Jacobian-returning function |
x0 | const V& | Initial vector |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Returns: RootFindingResult<V> with root of type std::optional<V>.
Convergence: $\|F(\mathbf{x})\|_2 < \mathrm{abs\_ftol}$ or $\|\Delta\mathbf{x}\|_2$ below the prescribed tolerance.
Example — find the intersection of the unit circle $x^2 + y^2 = 1$ with the line $y = x$.
Pass sangi::Matrix<T>::solve() as the concept-satisfying solver by also including <math/linalg/solvers.hpp>.
#include <math/roots/root_finding.hpp>
#include <math/linalg/solvers.hpp> // body of Matrix::solve() lives here
using namespace sangi;
auto F = [](const Vector<double>& v) {
Vector<double> r(2);
r[0] = v[0]*v[0] + v[1]*v[1] - 1.0; // x^2 + y^2 - 1
r[1] = v[0] - v[1]; // x - y
return r;
};
auto J = [](const Vector<double>& v) {
Matrix<double> m(2, 2);
m(0,0) = 2*v[0]; m(0,1) = 2*v[1];
m(1,0) = 1.0; m(1,1) = -1.0;
return m;
};
Vector<double> x0(2); x0[0] = 0.5; x0[1] = 0.5;
auto r = newton_raphson_nd<double, Vector<double>, Matrix<double>>(F, J, x0);
if (r.converged && r.root) {
const auto& v = *r.root;
std::cout << v[0] << ", " << v[1];
}
// Run output: iter=5 root=(0.707106781186548, 0.707106781186548)
// matches 1/sqrt(2)=0.7071067811865476 to 15 digits
broyden_method
template<concepts::OrderedField T, typename V, typename M>
RootFindingResult<V> broyden_method(
const std::function<V(const V&)>& F,
const V& x0,
std::optional<M> J0 = std::nullopt,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: "good" Broyden quasi-Newton. Avoids re-computing the Jacobian by maintaining a rank-1 update ($B_{n+1} = B_n + \frac{(\Delta F - B_n \Delta \mathbf{x}) \Delta \mathbf{x}^\top}{\|\Delta \mathbf{x}\|^2}$).
Parameters:
| Argument | Type | Description |
|---|---|---|
F | const std::function<V(const V&)>& | Vector-valued system |
x0 | const V& | Initial vector |
J0 | std::optional<M> | Initial approximate Jacobian (defaults to identity when omitted) |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use when: the analytic Jacobian is unavailable or too expensive. $B_0$ defaults to the identity and is improved by the updates.
newton_krylov
template<concepts::OrderedField T>
RootFindingResult<Vector<T>> newton_krylov(
const std::function<Vector<T>(const Vector<T>&)>& F,
const Vector<T>& x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>(),
size_t krylov_restart = 30,
T inner_tol = T(1e-3));
How it works: Jacobian-free Newton-Krylov (JFNK). The user supplies only $F$ (no analytic Jacobian and no explicit numerical Jacobian). Each Newton step solves $\mathbf{J}\,\boldsymbol\delta = -\mathbf{F}$ with GMRES (Krylov), but never forms the Jacobian: it uses only the directional finite difference $\mathbf{J}\,\mathbf{v} \approx (\mathbf{F}(\mathbf{x}+\varepsilon\mathbf{v}) - \mathbf{F}(\mathbf{x}))/\varepsilon$ (matrix-free). Backtracking damping adds global convergence.
Parameters:
| Argument | Type | Description |
|---|---|---|
F | const std::function<Vector<T>(const Vector<T>&)>& | System $\mathbf{F}(\mathbf{x})$ |
x0 | const Vector<T>& | Initial vector |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
krylov_restart | size_t | Max Krylov subspace dimension for GMRES (optional, default 30) |
inner_tol | T | Inner GMRES relative-residual stop threshold (optional, default 1e-3) |
Use when: large nonlinear systems where the Jacobian cannot be formed explicitly / is too large. broyden_method also needs only $F$, but this solves each step's linear system in a Krylov subspace, which is more memory-efficient in high dimensions.
// x² + y² = 1, x - y = 0 → (1/√2, 1/√2) (only F is needed)
using V = Vector<double>;
auto F = [](const V& v) -> V {
return V({v[0]*v[0] + v[1]*v[1] - 1.0, v[0] - v[1]});
};
auto r = newton_krylov<double>(F, V({0.5, 0.5}));
// Result: x = 0.7071067811865476, y = 0.7071067811865476
newton_fd
template<concepts::OrderedField T>
RootFindingResult<Vector<T>> newton_fd(
const std::function<Vector<T>(const Vector<T>&)>& F,
const Vector<T>& x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>(),
T fd_step = std::sqrt(std::numeric_limits<T>::epsilon()));
How it works: finite-difference Newton. The user supplies only $F$. Each Newton step builds the Jacobian by forward differences $J[:,j] = (F(x + h_j e_j) - F(x))/h_j$ as an explicit n×n matrix (sangi's Matrix) and solves $\mathbf{J}\,\boldsymbol\delta = -\mathbf{F}$ directly (LU). It is a thin wrapper that feeds a numerical Jacobian to newton_raphson_nd.
Parameters:
| Argument | Type | Description |
|---|---|---|
F | const std::function<Vector<T>(const Vector<T>&)>& | System $\mathbf{F}(\mathbf{x})$ |
x0 | const Vector<T>& | Initial vector |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
fd_step | T | Base step $h$ for forward differences (actually $h(1+|x_j|)$, optional) |
vs. newton_krylov: newton_fd holds $J$ explicitly (memory $O(n^2)$) and solves the linear system directly, so it is robust at small/medium scale. For large systems where $J$ cannot be formed, use newton_krylov (matrix-free).
// x² + y² = 1, x - y = 0 → (1/√2, 1/√2) (only F is needed; J is built internally)
using V = Vector<double>;
auto F = [](const V& v) -> V {
return V({v[0]*v[0] + v[1]*v[1] - 1.0, v[0] - v[1]});
};
auto r = newton_fd<double>(F, V({0.5, 0.5}));
// Result: x = 0.7071067811865476, y = 0.7071067811865476
levenberg_marquardt
template<concepts::OrderedField T, typename V, typename M>
RootFindingResult<V> levenberg_marquardt(
const std::function<V(const V&)>& F,
const std::function<M(const V&)>& J,
const V& x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: solves $(J^\top J + \lambda I) \Delta\mathbf{x} = -J^\top F$. The damping $\lambda$ is decreased on successful steps and increased on failed ones (Marquardt-Levenberg update rule). Interpolates between Newton and steepest descent.
Parameters:
| Argument | Type | Description |
|---|---|---|
F | const std::function<V(const V&)>& | Residual-returning vector-valued function |
J | const std::function<M(const V&)>& | Jacobian-returning function |
x0 | const V& | Initial vector |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use when: the initial guess is far from a root, the Jacobian is near-singular, or the problem is best framed as residual minimisation rather than pure equation solving.
powell_hybrid
template<concepts::OrderedField T, typename V, typename M>
requires concepts::VectorOf<V, T> && concepts::MatrixOf<M, T>
RootFindingResult<V> powell_hybrid(
const std::function<V(const V&)>& F,
const V& x0,
const ConvergenceCriteria<T>& criteria = ConvergenceCriteria<T>());
Behaviour: Powell (1970) dogleg trust-region. Follows a dogleg path that combines the Newton direction with the steepest-descent direction inside a trust region. The Jacobian is approximated internally by finite differences, so the user supplies only F. Same family as MINPACK's hybrd routine (SciPy's default fsolve).
Parameters:
| Argument | Type | Description |
|---|---|---|
F | const std::function<V(const V&)>& | Vector-valued system |
x0 | const V& | Initial vector |
criteria | const ConvergenceCriteria<T>& | Convergence thresholds (optional) |
Use when: the recommended default for N-D root finding. Combines Newton-like local convergence with global convergence from the trust region.
Example
#include <math/roots/root_finding.hpp>
#include <iostream>
using namespace sangi;
int main() {
// 1D: find root of f(x) = x^2 - 2 using Brent's method
auto result = brent_method<double>(
[](double x) { return x * x - 2.0; },
1.0, 2.0);
if (result.converged && result.root)
std::cout << "sqrt(2) = " << *result.root << std::endl;
// Output: sqrt(2) = 1.41421...
// Polynomial: x^3 - 6x^2 + 11x - 6 = (x-1)(x-2)(x-3)
Polynomial<double> p({-6.0, 11.0, -6.0, 1.0}); // ascending
auto roots = solvePolynomial(p);
for (auto& r : roots)
std::cout << r.re << std::endl;
// Output: 1, 2, 3 (order is implementation-dependent)
// Degree 5+: solvePolynomial dispatches to jenkinsTraub automatically
Polynomial<double> q({1.0, 0.0, 0.0, 0.0, 0.0, 1.0}); // x^5 + 1
auto roots5 = solvePolynomial(q);
for (auto& r : roots5)
std::cout << r.re << " + " << r.im << "i" << std::endl;
}
Related Mathematical Background
The following articles explain the mathematical concepts underlying the Roots module.
- Bisection Method — The most fundamental root-finding algorithm
- Newton's Method — Quadratically convergent iteration
- Secant Method — Derivative-free superlinear convergence
- Advanced Polynomial Root-Finding — Aberth-Ehrlich, companion matrix methods
- Nth Root via Precision Doubling — Multi-precision nth root computation
- Zimmermann Recursive Square Root — Fast multi-precision square root