Skip to main content

solve_newton_batch

Function solve_newton_batch 

Source
pub fn solve_newton_batch<F>(
    problems: &[RootProblem],
    settings: RootSettings,
    backend: ComputeBackend,
    fdf: F,
) -> RootBatch
where F: Fn(usize, f64) -> (f64, f64) + Sync,
Expand 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 — one RootProblem per lane. Both the bracket and the guess are 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; see root_batch_backend_for.
  • fdf — fdf(i, x) returns (value, derivative) of lane i’s residual at x. 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);