from ode.core import (
Dense, OdeCfg, OdeState, BDF, MAX_FACTOR, MIN_FACTOR, NOT_FINITE, STEP_TOO_SMALL,
all_finite, jacobian, min_step_at, rms,
)
from ode.lu import lu_factor, lu_solve
MAX_ORDER: Int = 5
NEWTON_MAXITER: Int = 4
# rows of the difference array: orders 0..MAX_ORDER and two more for the error estimates
D_ROWS: Int = MAX_ORDER + 3
# the NDF modifications kappa_k, k = 0..MAX_ORDER (Shampine and Reichelt, table 1)
KAPPA = [0.0, -0.1850, -1.0 / 9.0, -0.0823, -0.0415, 0.0]
#| gamma_k = sum_{j=1}^{k} 1/j, k = 0..MAX_ORDER.
def bdf_gamma() -> Arr {
g = zeros(MAX_ORDER + 1)
for k from 1 to MAX_ORDER + 1 {
g[k] = g[k - 1] + 1.0 / k
}
return g
}
#| alpha_k = (1 - kappa_k) gamma_k, the NDF's leading coefficient.
def bdf_alpha() -> Arr {
return (1.0 - KAPPA) * bdf_gamma()
}
#| C_k = kappa_k gamma_k + 1/(k+1), the error constant of order k.
def bdf_error_const() -> Arr {
c = zeros(MAX_ORDER + 2)
g = bdf_gamma()
for k to MAX_ORDER + 2 {
if k <= MAX_ORDER {
c[k] = KAPPA[k] * g[k] + 1.0 / (k + 1.0)
}
else {
c[k] = 1.0 / (k + 1.0)
}
}
return c
}
#| The matrix that rescales differences of order `order` to a grid of
#| spacing `factor` times the old (Shampine and Reichelt, eq. 3.3):
#| R[i, j] = prod_{m=1}^{i} (m - 1 - factor j) / m.
def compute_R(order: Int, factor: Num) -> Arr {
k = order + 1
R = zeros(k, k)
for j to k {
R[0, j] = 1.0
}
for i from 1 to k {
for j to k {
mij = 0.0
if j >= 1 {
mij = (i - 1.0 - factor * j) / i
}
R[i, j] = R[i - 1, j] * mij
}
}
return R
}
#| Rescale the first order+1 rows of the differences D in place for a step
#| `factor` times the current one.
def change_D(D: Arr, order: Int, factor: Num) {
k = order + 1
RU = compute_R(order, factor) @ compute_R(order, 1.0)
Dn = transpose(RU) @ D[0:k, :]
for i to k {
D[i, :] = Dn[i, :]
}
}
#| Simplified Newton for the correction d. Returns whether it converged,
#| the iterations, the new y, d, and the rhs evaluations.
def bdf_newton(f, s: OdeState, t_new: Num, y_pred: Arr, cc: Num, psi: Arr, scale: Arr, tol: Num, p: Arr) {
n = len(y_pred)
d = zeros(n)
y = copy(y_pred)
norm_old = -1.0
rate = -1.0
converged = false
iters: Int = 0
nfev: Int = 0
going = true
r = zeros(n)
while going and iters < NEWTON_MAXITER {
fy = f(t_new, y, p)
nfev += 1
iters += 1
if not all_finite(fy) {
going = false
}
else {
for i to n {
r[i] = cc * fy[i] - psi[i] - d[i]
}
dy = lu_solve(s.LU1, s.piv1, r)
acc = 0.0
for i to n {
u = dy[i] / scale[i]
acc += u * u
}
dnorm = sqrt(acc / n)
if norm_old >= 0.0 {
rate = dnorm / norm_old
}
if rate >= 0.0 and (rate >= 1.0 or pow(rate, NEWTON_MAXITER - iters + 1) / (1.0 - rate) * dnorm > tol) {
going = false
}
else {
for i to n {
y[i] += dy[i]
d[i] += dy[i]
}
if dnorm == 0.0 or (rate >= 0.0 and rate / (1.0 - rate) * dnorm < tol) {
converged = true
going = false
}
norm_old = dnorm
}
}
}
return converged, iters, y, d, nfev
}
#| The state a BDF integration starts from: order 1, the differences y0 and
#| h f0, and the Jacobian at the start.
def bdf_start(f, jac, s0: OdeState, c: OdeCfg, p: Arr) -> OdeState {
s = s0
n = len(s.y)
s.D = zeros(D_ROWS, n)
s.D[0, :] = s.y
s.D[1, :] = s.fy * s.h_abs * s.dir
s.order = 1
s.n_equal = 0
s.have_lu = false
Jn, nj = jacobian(f, jac, s.t, s.y, s.fy, p, c)
s.J = Jn
s.nfev += nj
s.njev += 1
s.jac_current = true
return s
}
#| One accepted BDF step from s, then the order and step for the next one.
def bdf_step(f, jac, s0: OdeState, c: OdeCfg, p: Arr) -> OdeState {
s = s0
t = s.t
n = len(s.y)
alpha = bdf_alpha()
gamma = bdf_gamma()
econst = bdf_error_const()
min_step = min_step_at(t)
h_abs = s.h_abs
if h_abs > c.max_step {
change_D(s.D, s.order, c.max_step / h_abs)
h_abs = c.max_step
s.n_equal = 0
s.have_lu = false
}
else {
if h_abs < min_step {
change_D(s.D, s.order, min_step / h_abs)
h_abs = min_step
s.n_equal = 0
s.have_lu = false
}
}
order = s.order
accepted = false
t_new = t
y_new = s.y
d = zeros(n)
err_norm = 0.0
safety = 0.0
scale = c.atol
while not accepted {
if h_abs < min_step {
s.status = STEP_TOO_SMALL
return s
}
t_new = t + h_abs * s.dir
if s.dir * (t_new - c.t_bound) > 0.0 {
t_new = c.t_bound
change_D(s.D, order, abs(t_new - t) / h_abs)
s.n_equal = 0
s.have_lu = false
}
h = t_new - t
h_abs = abs(h)
#: the predictor and the history term of the NDF
y_pred = zeros(n)
psi = zeros(n)
scale = zeros(n)
for i to n {
acc = 0.0
for k to order + 1 {
acc += s.D[k, i]
}
y_pred[i] = acc
scale[i] = c.atol[i] + c.rtol * abs(acc)
acc = 0.0
for k from 1 to order + 1 {
acc += gamma[k] * s.D[k, i]
}
psi[i] = acc / alpha[order]
}
cc = h / alpha[order]
converged = false
iters: Int = 0
trying = true
while trying {
if not s.have_lu {
s.LU1 = eye(n) - cc * s.J
ok = lu_factor(s.LU1, s.piv1)
s.have_lu = true
s.nlu += 1
}
converged, iters, y_new, d, nf = bdf_newton(f, s, t_new, y_pred, cc, psi, scale, c.newton_tol, p)
s.nfev += nf
if converged or s.jac_current {
trying = false
}
else {
fp = f(t_new, y_pred, p)
s.nfev += 1
Jn, nj = jacobian(f, jac, t_new, y_pred, fp, p, c)
s.J = Jn
s.nfev += nj
s.njev += 1
s.jac_current = true
s.have_lu = false
}
}
if not converged {
change_D(s.D, order, 0.5)
h_abs = 0.5 * h_abs
s.n_equal = 0
s.have_lu = false
s.nreject += 1
}
else {
safety = 0.9 * (2.0 * NEWTON_MAXITER + 1.0) / (2.0 * NEWTON_MAXITER + iters)
scale = c.atol + c.rtol * abs(y_new)
err_norm = rms(econst[order] * d / scale)
if err_norm > 1.0 {
factor = max(MIN_FACTOR, safety * pow(err_norm, -1.0 / (order + 1.0)))
change_D(s.D, order, factor)
h_abs = h_abs * factor
s.n_equal = 0
s.nreject += 1
}
else {
accepted = true
}
}
}
if not all_finite(y_new) {
s.status = NOT_FINITE
return s
}
s.n_equal += 1
s.naccept += 1
s.jac_current = false
#: update the differences: D^{j+1} y_n = D^j y_n - D^j y_{n-1}, and d = D^{k+1} y_n
for j to n {
s.D[order + 2, j] = d[j] - s.D[order + 1, j]
s.D[order + 1, j] = d[j]
for i to order + 1 reversed {
s.D[i, j] += s.D[i + 1, j]
}
}
#: the dense output, before any change of order or step rescales D
Q = zeros(order + 1, n)
for k to order + 1 {
Q[k, :] = s.D[k, :]
}
s.dense = Dense { kind: BDF, t_old: t, t_new: t_new, h: t_new - t, Q: Q, y_old: s.y, order: order }
s.has_dense = true
s.t = t_new
s.y = y_new
s.h_abs = h_abs
if s.n_equal < order + 1 {
return s
}
#: after order+1 equal steps: compare the errors orders k-1, k, k+1 would make
em = inf
if order > 1 {
em = rms(econst[order - 1] * s.D[order, :] / scale)
}
ep = inf
if order < MAX_ORDER {
ep = rms(econst[order + 1] * s.D[order + 2, :] / scale)
}
norms = [em, err_norm, ep]
best = -1.0
delta = 0
for j to len(norms) {
fj = inf
if norms[j] > 0.0 {
fj = pow(norms[j], -1.0 / (order + j))
}
if fj > best {
best = fj
delta = j - 1
}
}
s.order = order + int(delta)
factor = min(MAX_FACTOR, safety * best)
s.h_abs = h_abs * factor
change_D(s.D, s.order, factor)
s.n_equal = 0
s.have_lu = false
return s
}
test "the NDF constants, and rescaling by 1 changes nothing" {
assert_allclose(bdf_gamma(), [0.0, 1.0, 1.5, 11.0 / 6.0, 25.0 / 12.0, 137.0 / 60.0], 1.0e-15)
# order 5 is the plain BDF: error constant 1/6
assert_near(bdf_error_const()[MAX_ORDER], 1.0 / 6.0, 1.0e-16)
# R(order, 1) is its own inverse, so U U = I (Shampine and Reichelt, section 3.1)
U = compute_R(3, 1.0)
assert_allclose(U @ U, eye(4), 1.0e-14)
}
# SPDX-License-Identifier: MIT OR Apache-2.0
# SPDX-FileCopyrightText: 2026 The Viv authors