use alloc::{vec, vec::Vec};
use crate::{
dense_la::{lu_in_place, lu_solve},
error::SemiflowError,
float::{from_f64, SemiflowFloat},
reaction::Kinetics,
scratch::ScratchPool,
};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[non_exhaustive]
pub enum ReactionSolver {
#[default]
Rk4,
Rosenbrock(RosenbrockOptions),
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct RosenbrockOptions {
pub rtol: f64,
pub atol: f64,
pub max_steps: usize,
}
impl Default for RosenbrockOptions {
fn default() -> Self {
Self {
rtol: 1e-6,
atol: 1e-9,
max_steps: 100_000,
}
}
}
impl RosenbrockOptions {
#[must_use]
pub fn with_rtol(mut self, rtol: f64) -> Self {
self.rtol = rtol;
self
}
#[must_use]
pub fn with_atol(mut self, atol: f64) -> Self {
self.atol = atol;
self
}
#[must_use]
pub fn with_max_steps(mut self, max_steps: usize) -> Self {
self.max_steps = max_steps;
self
}
pub(crate) fn validate(&self) -> Result<(), SemiflowError> {
let bad = |what, value| Err(SemiflowError::DomainViolation { what, value });
if !(self.rtol > 0.0 && self.rtol < 1.0) {
return bad("Rosenbrock: rtol must be in (0, 1)", self.rtol);
}
if !(self.atol >= 0.0 && self.atol.is_finite()) {
return bad("Rosenbrock: atol must be finite and >= 0", self.atol);
}
if self.max_steps == 0 {
return bad("Rosenbrock: max_steps must be >= 1", 0.0);
}
Ok(())
}
}
pub(crate) struct RosBuffers<F> {
k: usize,
f0: Vec<F>,
k1: Vec<F>,
k2: Vec<F>,
stage: Vec<F>,
next: Vec<F>,
jac: Vec<F>,
lu: Vec<F>,
work: Vec<F>,
piv: Vec<usize>,
}
impl<F: SemiflowFloat> RosBuffers<F> {
pub(crate) fn new(k: usize, n: usize, scratch: &mut ScratchPool<F>) -> Self {
Self {
k,
f0: scratch.take_vec(k * n),
k1: scratch.take_vec(k * n),
k2: scratch.take_vec(k * n),
stage: scratch.take_vec(k * n),
next: scratch.take_vec(k * n),
jac: scratch.take_vec(k * k * n),
lu: scratch.take_vec(k * k * n),
work: scratch.take_vec(2 * k),
piv: vec![0; k * n],
}
}
pub(crate) fn release(self, scratch: &mut ScratchPool<F>) {
for b in [
self.f0, self.k1, self.k2, self.stage, self.next, self.jac, self.lu, self.work,
] {
scratch.return_vec(b);
}
}
}
fn gamma<F: SemiflowFloat>() -> F {
from_f64(1.0 + core::f64::consts::FRAC_1_SQRT_2)
}
#[allow(clippy::too_many_arguments, clippy::many_single_char_names)] pub(crate) fn ros2_flow<F: SemiflowFloat>(
kin: &dyn Kinetics<F>,
opts: &RosenbrockOptions,
t: F,
h: F,
n: usize,
x: &[F],
u: &mut [F],
b: &mut RosBuffers<F>,
) -> Result<usize, SemiflowError> {
let (mut hs, mut elapsed, mut steps) = (h, F::zero(), 0_usize);
while elapsed < h {
let tc = t + elapsed;
kin.eval_batch(tc, n, x, u, &mut b.f0)?;
jacobian(kin, tc, n, x, u, b)?;
check_finite(&b.f0, &b.jac)?;
let mut last = hs >= h - elapsed;
loop {
if steps >= opts.max_steps {
return Err(SemiflowError::ConvergenceFailed {
last_residual: hs.to_f64().unwrap_or(f64::NAN),
max_iter: opts.max_steps,
});
}
if last {
hs = h - elapsed;
} else if stalled(hs, elapsed) {
return Err(SemiflowError::DomainViolation {
what: "Rosenbrock: step size underflow (kinetics non-finite near the state, or tolerances unattainable)",
value: hs.to_f64().unwrap_or(f64::NAN),
});
}
steps += 1;
let err = attempt(kin, opts, tc, hs, n, x, u, b)?;
if err <= 1.0 {
u.copy_from_slice(&b.next);
elapsed = if last { h } else { elapsed + hs };
hs *= from_f64::<F>(next_factor(err));
break;
}
hs *= from_f64::<F>(next_factor(err).min(1.0));
last = false;
}
}
Ok(steps)
}
fn check_finite<F: SemiflowFloat>(f0: &[F], jac: &[F]) -> Result<(), SemiflowError> {
match f0.iter().chain(jac).find(|v| !v.is_finite()) {
Some(bad) => Err(SemiflowError::DomainViolation {
what: "ReactionDiffusion: non-finite reaction result or Jacobian at an accepted state (Rosenbrock; kinetics left its domain?)",
value: bad.to_f64().unwrap_or(f64::NAN),
}),
None => Ok(()),
}
}
fn stalled<F: SemiflowFloat>(hs: F, elapsed: F) -> bool {
hs <= from_f64::<F>(16.0) * F::epsilon() * elapsed || elapsed + hs == elapsed
}
fn next_factor(err: f64) -> f64 {
if err.is_finite() {
(0.9 / libm::sqrt(err.max(1e-300))).clamp(0.2, 5.0)
} else {
0.2
}
}
#[allow(clippy::too_many_arguments, clippy::many_single_char_names)]
fn attempt<F: SemiflowFloat>(
kin: &dyn Kinetics<F>,
opts: &RosenbrockOptions,
tc: F,
hs: F,
n: usize,
x: &[F],
u: &[F],
b: &mut RosBuffers<F>,
) -> Result<f64, SemiflowError> {
if factor_w(hs, n, b).is_err() {
return Ok(f64::INFINITY);
}
b.k1.copy_from_slice(&b.f0);
solve_nodes(n, &b.lu, &b.piv, &mut b.k1, &mut b.work, b.k);
for ((s, &ui), &k1) in b.stage.iter_mut().zip(u).zip(&b.k1) {
*s = ui + hs * k1;
}
kin.eval_batch(tc + hs, n, x, &b.stage, &mut b.k2)?;
let two = from_f64::<F>(2.0);
for (k2, &k1) in b.k2.iter_mut().zip(&b.k1) {
*k2 -= two * k1;
}
solve_nodes(n, &b.lu, &b.piv, &mut b.k2, &mut b.work, b.k);
let (w1, w2, half) = (from_f64::<F>(1.5), from_f64::<F>(0.5), from_f64::<F>(0.5));
let mut err = 0.0_f64;
for (i, nx) in b.next.iter_mut().enumerate() {
let (k1, k2) = (b.k1[i], b.k2[i]);
*nx = u[i] + hs * (w1 * k1 + w2 * k2);
let e = (hs * half * (k1 + k2)).abs().to_f64().unwrap_or(f64::NAN);
let scale = opts.atol + opts.rtol * u[i].abs().max(nx.abs()).to_f64().unwrap_or(f64::NAN);
let ratio = if e == 0.0 { 0.0 } else { e / scale };
err = if ratio.is_nan() || !nx.is_finite() {
f64::INFINITY
} else {
err.max(ratio)
};
}
Ok(err)
}
fn factor_w<F: SemiflowFloat>(hs: F, n: usize, b: &mut RosBuffers<F>) -> Result<(), SemiflowError> {
let k = b.k;
let kk = k * k;
let gh = gamma::<F>() * hs;
for i in 0..n {
let w = &mut b.lu[i * kk..(i + 1) * kk];
for (e, wv) in w.iter_mut().enumerate() {
let id = if e / k == e % k { F::one() } else { F::zero() };
*wv = id - gh * b.jac[e * n + i];
}
lu_in_place(k, w, &mut b.piv[i * k..(i + 1) * k])?;
}
Ok(())
}
fn solve_nodes<F: SemiflowFloat>(
n: usize,
lu: &[F],
piv: &[usize],
v: &mut [F],
work: &mut [F],
k: usize,
) {
let (node, scratch) = work.split_at_mut(k);
for i in 0..n {
for (s, nv) in node.iter_mut().enumerate() {
*nv = v[s * n + i];
}
lu_solve(
k,
&lu[i * k * k..(i + 1) * k * k],
&piv[i * k..(i + 1) * k],
node,
scratch,
);
for (s, &nv) in node.iter().enumerate() {
v[s * n + i] = nv;
}
}
}
#[allow(clippy::many_single_char_names)] fn jacobian<F: SemiflowFloat>(
kin: &dyn Kinetics<F>,
tc: F,
n: usize,
x: &[F],
u: &[F],
b: &mut RosBuffers<F>,
) -> Result<(), SemiflowError> {
if let Some(r) = kin.jacobian_batch(tc, n, x, u, &mut b.jac) {
return r;
}
let k = b.k;
let sq = F::epsilon().sqrt();
b.stage.copy_from_slice(u);
for l in 0..k {
for i in 0..n {
let base = u[l * n + i];
b.stage[l * n + i] = base + sq * base.abs().max(F::one());
}
kin.eval_batch(tc, n, x, &b.stage, &mut b.k2)?;
for i in 0..n {
let delta = b.stage[l * n + i] - u[l * n + i];
for row in 0..k {
b.jac[(row * k + l) * n + i] = (b.k2[row * n + i] - b.f0[row * n + i]) / delta;
}
b.stage[l * n + i] = u[l * n + i];
}
}
Ok(())
}
#[cfg(test)]
#[path = "reaction_stiff_tests.rs"]
mod tests;