pub fn cubic_roots_batch(
eqns: &[CubicEqn],
backend: ComputeBackend,
) -> Vec<Roots<3>>Expand description
Roots of N independent cubic equations a x^3 + b x^2 + c x + d = 0, on
the chosen backend.
A batched wrapper over CubicEqn::roots — the depressed-cubic Cardano
solver with Kahan-compensated discriminants, unchanged and not reimplemented
here.
This is the kernel a cubic-equation-of-state caller wants: a Peng-Robinson or
Redlich-Kwong compressibility factor is the root of a cubic in Z, one cubic
per cell, and the whole field can be solved in a single call.
§Arguments
eqns— oneCubicEqnper lane, dimensionless coefficients.backend— requested execution backend; seepoly_roots_backend_for.
§Returns
One Roots<3> per equation, in input order. Three Real roots, or one
Real plus a Complex-tagged (real, imaginary) pair, or a triple root, or —
when |a| underflows — the quadratic’s roots plus a Nan slot. The tags are
the caller’s guide; do not read a slot without checking
Roots::root_type.
There is no _into variant, unlike crate::ldu_matrix::parallel’s
kernels: Roots has no Default, so a caller-owned output buffer would
have to be pre-filled with a dummy value, and the per-lane Cardano solve
dominates the allocation anyway.
§Determinism
Bit-for-bit identical to calling CubicEqn::roots in a loop, on every
backend and at any thread count. Each lane is one independent closed-form
evaluation; no arithmetic crosses lanes.
§Verification
Methodology. Cubics are constructed from known roots as
(x - r1)(x - r2)(x - r3) with r1, r2, r3 drawn from a fixed-seed
xorshift64* on [-8, 8), expanded to coefficients — so the analytic answer
is known exactly, which makes this an oracle rather than a second opinion.
4 096 equations. Two pass criteria: every Real-tagged root satisfies
|a x^3 + b x^2 + c x + d| <= 1e-6 (loose because a cubic’s residual is
steeply amplified near a triple-ish root), and every returned bit is equal to
the per-equation scalar CubicEqn::roots.
Result, measured 2026-08-12 by cubic_batch_matches_analytic_construction
in parallel/tests.rs, release build: worst residual 3.979039e-13 over
12 288 real roots, worst distance from the nearest constructed root
2.079981e-10; bitwise equality against the scalar solver held on every
lane, and (per poly_batch_bitwise_identical_across_backends) across
Serial / CpuMulti at 1, 2, 4 and 8 worker threads.
Interpretation: both figures belong to the scalar Cardano solver, not to
the batching — the bitwise-equality half of the test proves the batch adds
exactly zero error. That the worst root displacement (2.1e-10) is three
orders larger than the worst residual (4.0e-13) is the signature of an
ill-conditioned root, not of a bad solve: with roots drawn at random from
[-8, 8), some triples come out nearly coincident, and near a near-double
root the cubic is flat, so a tiny residual still corresponds to a visibly
displaced abscissa. A caller solving a cubic equation of state should expect
the same behaviour near the critical point and judge accuracy by the
residual.
§Example
use outram_foam_basic_lib::compute::ComputeBackend;
use outram_foam_basic_lib::math::parallel::cubic_roots_batch;
use outram_foam_basic_lib::polynomial::{CubicEqn, RootType};
// (x-1)(x-2)(x-3) = x^3 - 6x^2 + 11x - 6
let eqns = [CubicEqn::new(1.0, -6.0, 11.0, -6.0)];
let roots = cubic_roots_batch(&eqns, ComputeBackend::CpuMulti);
let mut real: Vec<f64> = (0..3)
.filter(|&k| roots[0].root_type(k) == RootType::Real)
.map(|k| roots[0].get(k))
.collect();
real.sort_by(f64::total_cmp);
assert_eq!(real.len(), 3);
assert!((real[0] - 1.0).abs() < 1e-9);
assert!((real[2] - 3.0).abs() < 1e-9);