pub fn solve_newton_batch<F>(
problems: &[RootProblem],
settings: RootSettings,
backend: ComputeBackend,
fdf: F,
) -> RootBatchExpand description
Solve N independent root problems by bracket-safeguarded Newton iteration,
on the chosen backend.
Use this when the derivative is cheap to obtain alongside the value — which
for a thermodynamic inversion it is, because dh/dT is the specific heat the
same evaluation already computed. It is typically 2-4x fewer function
evaluations than RootMethod::Brent on a smooth residual.
§Why it is safeguarded, and not bare Newton
Bare Newton has no guarantee at all: a small derivative throws the iterate to infinity, and a nearby inflection makes it cycle. Neither failure is acceptable in a per-cell inversion where one bad cell out of a million stalls the timestep. This solver therefore keeps the bracket that Newton lacks: it takes the Newton step only when the step lands inside the current bracket and is shrinking the interval fast enough, and bisects otherwise. The result converges on anything a bisection would converge on, at Newton’s rate wherever Newton behaves.
§Arguments
problems— oneRootProblemper lane. Both the bracket and theguessare used: the bracket safeguards, the guess starts the iteration. A guess outside the bracket or non-finite is replaced by the midpoint.settings— tolerances and iteration cap. The abscissa tolerance is tested against the step,|dx| <= x_tol_abs + x_tol_rel * |x|.backend— requested execution backend; seeroot_batch_backend_for.fdf—fdf(i, x)returns(value, derivative)of lanei’s residual atx. Returning both together is deliberate: an enthalpy and its specific heat come out of one polynomial evaluation, and asking for them separately would double the cost. Must be a pure deterministic function; see the module-level “Determinism” section.
§Returns
A RootBatch with one RootSolution per problem, in problem order.
§Determinism
Bit-for-bit identical across backends and thread counts for a pure fdf, on
the same terms as solve_bracketed_batch.
§Non-convergence
As solve_bracketed_batch, plus one Newton-specific case: a zero or
non-finite derivative is not a failure — the lane simply bisects that
iteration and carries on. Only a non-finite value stops the lane, with
RootStatus::NotFinite.
§Verification
Methodology. Checked against the analytic root of x^2 - k = 0, i.e.
sqrt(k), over 64 lanes with k spread across [1, 65], and against the
three known roots of (x - 1)(x - 2)(x - 3); pass criterion
|x_computed - x_analytic| <= 1e-12 with status == Converged on every
lane. Separately checked against RootMethod::Brent on the same problems,
with the iteration counts of both recorded.
Results, measured 2026-08-12 by newton_matches_analytic_roots and
newton_beats_brent_on_iterations in parallel/tests.rs, release build:
worst absolute error over the 64 sqrt(k) lanes 8.881784e-16; worst over
the three cubic roots 0.000000e0 (all three recovered exactly); mean
iterations 7.73 for safeguarded Newton against 13.14 for Brent on the
same 64 lanes. Interpretation: Newton reaches the same rounding-floor
accuracy — in fact 2x better than Brent on the sqrt(k) family — in 59% of
Brent’s iterations, which is the expected quadratic-versus-superlinear margin
and is the reason to pay for a derivative. Note the counts include the
safeguard’s bisection steps, which is why they are higher than a textbook
bare-Newton count on the same problem.
§Example — the uom boundary
The batch is dimensionless, and the caller converts at its edge. This lane
inverts a linear h(T) relation for a ThermodynamicTemperature, keeping
uom typing on both sides of the call:
use outram_foam_basic_lib::compute::ComputeBackend;
use outram_foam_basic_lib::math::parallel::{
solve_newton_batch, RootProblem, RootSettings,
};
use uom::si::f64::{AvailableEnergy, ThermodynamicTemperature};
use uom::si::available_energy::joule_per_kilogram;
use uom::si::thermodynamic_temperature::kelvin;
// h(T) = cp * T with cp = 1000 J/(kg K); invert for two target enthalpies.
let cp = 1000.0_f64; // J / (kg K)
let targets = [
AvailableEnergy::new::<joule_per_kilogram>(400_000.0),
AvailableEnergy::new::<joule_per_kilogram>(900_000.0),
];
// Convert in: brackets in kelvin, as plain f64.
let lo = ThermodynamicTemperature::new::<kelvin>(200.0);
let hi = ThermodynamicTemperature::new::<kelvin>(3000.0);
let problems = [
RootProblem::new(lo.get::<kelvin>(), hi.get::<kelvin>()),
RootProblem::new(lo.get::<kelvin>(), hi.get::<kelvin>()),
];
let batch = solve_newton_batch(
&problems,
RootSettings::default(),
ComputeBackend::CpuMulti,
|i, t| (cp * t - targets[i].get::<joule_per_kilogram>(), cp),
);
// Convert out: back to a typed temperature.
let temperatures: Vec<ThermodynamicTemperature> = batch
.roots()
.expect("both lanes converge")
.into_iter()
.map(ThermodynamicTemperature::new::<kelvin>)
.collect();
assert!((temperatures[0].get::<kelvin>() - 400.0).abs() < 1e-9);
assert!((temperatures[1].get::<kelvin>() - 900.0).abs() < 1e-9);