Program Listing for File bdf.hpp
↰ Return to documentation for file (include/integrators/ode/bdf.hpp
)
#pragma once
#include <core/concepts.hpp>
#include <core/adaptive_integrator.hpp>
#include <core/state_creator.hpp>
#include <vector>
#include <array>
#include <cmath>
#include <stdexcept>
#include <algorithm>
#include <limits>
#include <numeric>
#include <iostream>
namespace diffeq {
// SciPy BDF constants
constexpr int MAX_ORDER = 5;
constexpr int NEWTON_MAXITER = 4;
constexpr double MIN_FACTOR = 0.2;
constexpr double MAX_FACTOR = 10.0;
template<typename S>
class BDFIntegrator : public core::AdaptiveIntegrator<S> {
public:
using base_type = core::AdaptiveIntegrator<S>;
using state_type = typename base_type::state_type;
using time_type = typename base_type::time_type;
using value_type = typename base_type::value_type;
using system_function = typename base_type::system_function;
explicit BDFIntegrator(system_function sys,
time_type rtol = static_cast<time_type>(1e-3),
time_type atol = static_cast<time_type>(1e-6),
int max_order = 5)
: base_type(std::move(sys), rtol, atol),
max_order_((max_order < MAX_ORDER) ? max_order : MAX_ORDER),
current_order_(1),
newton_tolerance_(static_cast<time_type>(1e-3)),
is_initialized_(false),
h_abs_(static_cast<time_type>(0.0)),
h_abs_old_(static_cast<time_type>(0.0)),
error_norm_old_(static_cast<time_type>(0.0)),
n_equal_steps_(0) {
// Initialize BDF coefficients (SciPy style)
initialize_bdf_coefficients();
}
void step(state_type& state, time_type dt) override {
adaptive_step(state, dt);
}
void fixed_step(state_type& state, time_type dt) {
// Simple fixed-step BDF1 implementation
if (!is_initialized_) {
initialize_history(state, dt);
is_initialized_ = true;
}
state_type y_new = StateCreator<state_type>::create(state);
state_type error = StateCreator<state_type>::create(state);
bool success = bdf_step(state, y_new, error, dt);
if (success) {
state = y_new;
this->advance_time(dt);
} else {
// Fallback to explicit Euler if Newton fails
state_type f = StateCreator<state_type>::create(state);
this->sys_(this->current_time_, state, f);
for (std::size_t i = 0; i < state.size(); ++i) {
state[i] += dt * f[i];
}
this->advance_time(dt);
}
}
time_type adaptive_step(state_type& state, time_type dt) override {
if (!is_initialized_) {
initialize_history(state, dt);
is_initialized_ = true;
}
// SciPy-style step size control
time_type t = this->current_time_;
time_type max_step = this->dt_max_;
time_type min_step = (this->dt_min_ > static_cast<time_type>(1e-10)) ? this->dt_min_ : static_cast<time_type>(1e-10);
time_type h_abs = h_abs_;
if (h_abs > max_step) {
h_abs = max_step;
change_D(current_order_, max_step / h_abs_);
n_equal_steps_ = 0;
} else if (h_abs < min_step) {
h_abs = min_step;
change_D(current_order_, min_step / h_abs_);
n_equal_steps_ = 0;
}
bool step_accepted = false;
int attempts = 0;
const int max_attempts = 10; // Limit attempts to avoid infinite loops
while (!step_accepted && attempts < max_attempts) {
attempts++;
if (h_abs < min_step) {
return fallback_step(state, dt);
}
time_type h = h_abs;
time_type t_new = t + h;
// Check boundary
if (t_new > this->current_time_ + dt) {
t_new = this->current_time_ + dt;
change_D(current_order_, std::abs(t_new - t) / h_abs);
n_equal_steps_ = 0;
}
h = t_new - t;
h_abs = std::abs(h);
state_type y_new = StateCreator<state_type>::create(state);
state_type error = StateCreator<state_type>::create(state);
time_type factor = static_cast<time_type>(1.0); // Declare here for all branches
bool bdf_success = bdf_step(state, y_new, error, h);
if (bdf_success) {
// Calculate error norm with proper scaling
state_type scale = StateCreator<state_type>::create(y_new);
for (std::size_t i = 0; i < scale.size(); ++i) {
scale[i] = this->atol_ + this->rtol_ * std::abs(y_new[i]);
}
time_type error_norm = static_cast<time_type>(0.0);
for (std::size_t i = 0; i < error.size(); ++i) {
time_type scaled_error = error[i] / scale[i];
error_norm += scaled_error * scaled_error;
}
error_norm = std::sqrt(error_norm / static_cast<time_type>(error.size()));
if (error_norm <= static_cast<time_type>(1.0)) {
// Accept step
step_accepted = true;
n_equal_steps_++;
state = y_new;
this->advance_time(h);
h_abs_ = h_abs;
// Differences array is updated in bdf_step
// Adjust order if we have enough equal steps
adjust_order(error_norm, scale);
// Calculate next step size with SciPy-style adaptive logic
time_type safety = static_cast<time_type>(0.8); // More conservative safety factor
time_type factor = safety * std::pow(error_norm, static_cast<time_type>(-1.0) / static_cast<time_type>(current_order_ + 1));
// Apply SciPy-style step size bounds (more conservative)
time_type max_factor_limit = static_cast<time_type>(1.5); // Even more conservative step size growth
if (factor > max_factor_limit) factor = max_factor_limit;
if (factor < static_cast<time_type>(MIN_FACTOR)) factor = static_cast<time_type>(MIN_FACTOR);
h_abs_ *= factor;
change_D(current_order_, factor);
n_equal_steps_ = 0;
} else {
// Reject step - be more conservative
factor = static_cast<time_type>(0.5); // Fixed reduction factor
h_abs *= factor;
change_D(current_order_, factor);
n_equal_steps_ = 0;
}
} else {
// Newton failed - try smaller step
factor = static_cast<time_type>(0.25); // More aggressive reduction for Newton failure
h_abs *= factor;
change_D(current_order_, factor);
n_equal_steps_ = 0;
}
}
// If we exit the loop without accepting a step, use fallback
if (!step_accepted) {
return fallback_step(state, dt);
}
return h_abs_;
}
void set_newton_parameters(int max_iterations, time_type tolerance) {
// max_iterations is now fixed at NEWTON_MAXITER = 4 (SciPy style)
newton_tolerance_ = tolerance;
}
private:
int max_order_;
int current_order_;
time_type newton_tolerance_;
bool is_initialized_;
time_type h_abs_;
time_type h_abs_old_;
time_type error_norm_old_;
int n_equal_steps_;
// SciPy-style differences array (D) - stores solution and derivatives
std::vector<state_type> D_; // D[0] = y, D[1] = h*f, D[2] = differences, etc.
// SciPy BDF coefficients
std::array<time_type, MAX_ORDER + 2> gamma_; // Gamma coefficients
std::array<time_type, MAX_ORDER + 2> alpha_; // Alpha coefficients
std::array<time_type, MAX_ORDER + 2> error_const_; // Error constants
void initialize_bdf_coefficients() {
// SciPy BDF coefficients - exactly matching the Python implementation
std::array<time_type, MAX_ORDER + 1> kappa = {
static_cast<time_type>(0),
static_cast<time_type>(-0.1850),
static_cast<time_type>(-1.0/9.0),
static_cast<time_type>(-0.0823),
static_cast<time_type>(-0.0415),
static_cast<time_type>(0)
};
// Initialize gamma array: gamma[0] = 0, gamma[k] = sum(1/j for j=1..k)
gamma_[0] = static_cast<time_type>(0.0);
for (int k = 1; k <= MAX_ORDER; ++k) {
gamma_[k] = gamma_[k-1] + static_cast<time_type>(1.0) / static_cast<time_type>(k);
}
// Initialize alpha array: alpha = (1 - kappa) * gamma
for (int k = 0; k <= MAX_ORDER; ++k) {
alpha_[k] = (static_cast<time_type>(1.0) - kappa[k]) * gamma_[k];
}
// Initialize error constants: error_const = kappa * gamma + 1/(k+1)
for (int k = 0; k <= MAX_ORDER; ++k) {
error_const_[k] = kappa[k] * gamma_[k] + static_cast<time_type>(1.0) / static_cast<time_type>(k + 1);
}
}
// SciPy-style helper functions for differences array
std::vector<std::vector<time_type>> compute_R(int order, time_type factor) {
std::vector<std::vector<time_type>> M(order + 1, std::vector<time_type>(order + 1, static_cast<time_type>(0.0)));
// Initialize M matrix for changing differences array
for (int i = 1; i <= order; ++i) {
for (int j = 1; j <= order; ++j) {
M[i][j] = (static_cast<time_type>(i - 1) - factor * static_cast<time_type>(j)) / static_cast<time_type>(i);
}
}
M[0][0] = static_cast<time_type>(1.0);
// Compute cumulative product along rows (SciPy style)
std::vector<std::vector<time_type>> R(order + 1, std::vector<time_type>(order + 1, static_cast<time_type>(0.0)));
for (int i = 0; i <= order; ++i) {
R[i][0] = M[i][0];
for (int j = 1; j <= order; ++j) {
R[i][j] = R[i][j-1] * M[i][j];
}
}
return R;
}
void change_D(int order, time_type factor) {
auto R = compute_R(order, factor);
auto U = compute_R(order, static_cast<time_type>(1.0));
// Compute RU = R * U
std::vector<std::vector<time_type>> RU(order + 1, std::vector<time_type>(order + 1, static_cast<time_type>(0.0)));
for (int i = 0; i <= order; ++i) {
for (int j = 0; j <= order; ++j) {
for (int k = 0; k <= order; ++k) {
RU[i][j] += R[i][k] * U[k][j];
}
}
}
// Apply transformation: D[:order+1] = RU.T @ D[:order+1]
std::vector<state_type> D_new(order + 1);
for (int i = 0; i <= order; ++i) {
D_new[i] = StateCreator<state_type>::create(D_[0]);
for (std::size_t k = 0; k < D_[0].size(); ++k) {
D_new[i][k] = static_cast<time_type>(0.0);
for (int j = 0; j <= order; ++j) {
D_new[i][k] += RU[j][i] * D_[j][k];
}
}
}
// Copy back
for (int i = 0; i <= order; ++i) {
D_[i] = D_new[i];
}
}
void initialize_history(const state_type& y0, time_type dt) {
// Initialize differences array D (SciPy style)
D_.clear();
D_.resize(MAX_ORDER + 3);
for (int i = 0; i < MAX_ORDER + 3; ++i) {
D_[i] = StateCreator<state_type>::create(y0);
// Initialize all to zero except D[0]
for (std::size_t j = 0; j < y0.size(); ++j) {
D_[i][j] = static_cast<time_type>(0.0);
}
}
// D[0] = y0
D_[0] = y0;
// Initialize D[1] = h * f(t0, y0) for the first step
state_type f0 = StateCreator<state_type>::create(y0);
this->sys_(this->current_time_, y0, f0);
for (std::size_t i = 0; i < y0.size(); ++i) {
D_[1][i] = dt * f0[i];
}
current_order_ = 1;
n_equal_steps_ = 0;
h_abs_ = dt;
}
void update_history(const state_type& y_new, time_type dt) {
// Update differences array after successful step (done in main step function)
h_abs_old_ = h_abs_;
h_abs_ = dt;
}
void adjust_order(time_type error_norm, const state_type& scale) {
// SciPy-style order selection - only adjust if we have enough equal steps
if (n_equal_steps_ < current_order_ + 1) {
return;
}
// Calculate error norms for different orders
time_type error_m_norm = std::numeric_limits<time_type>::infinity();
time_type error_p_norm = std::numeric_limits<time_type>::infinity();
if (current_order_ > 1) {
// Error estimate for order-1
time_type error_m = static_cast<time_type>(0.0);
for (std::size_t i = 0; i < D_[0].size(); ++i) {
time_type err = error_const_[current_order_ - 1] * D_[current_order_][i] / scale[i];
error_m += err * err;
}
error_m_norm = std::sqrt(error_m / static_cast<time_type>(D_[0].size()));
}
if (current_order_ < max_order_ && current_order_ + 2 < static_cast<int>(D_.size())) {
// Error estimate for order+1
time_type error_p = static_cast<time_type>(0.0);
for (std::size_t i = 0; i < D_[0].size(); ++i) {
time_type err = error_const_[current_order_ + 1] * D_[current_order_ + 2][i] / scale[i];
error_p += err * err;
}
error_p_norm = std::sqrt(error_p / static_cast<time_type>(D_[0].size()));
}
// Calculate factors for order selection (SciPy style)
std::array<time_type, 3> error_norms = {error_m_norm, error_norm, error_p_norm};
std::array<time_type, 3> factors;
for (int i = 0; i < 3; ++i) {
if (error_norms[i] > static_cast<time_type>(0) && std::isfinite(error_norms[i])) {
factors[i] = std::pow(error_norms[i], static_cast<time_type>(-1.0) / static_cast<time_type>(current_order_ + i - 1));
} else {
factors[i] = static_cast<time_type>(0.0); // Prefer current order if error estimate is invalid
}
}
// Select order with maximum factor
int best_order_idx = 1; // Default to current order
for (int i = 0; i < 3; ++i) {
if (factors[i] > factors[best_order_idx]) {
best_order_idx = i;
}
}
int delta_order = best_order_idx - 1;
int new_order = current_order_ + delta_order;
// Ensure order is within bounds
new_order = (new_order < 1) ? 1 : ((new_order > max_order_) ? max_order_ : new_order);
if (new_order != current_order_) {
current_order_ = new_order;
n_equal_steps_ = 0; // Reset step counter when order changes
}
}
// SciPy-style BDF system solver
struct NewtonResult {
bool converged;
int iterations;
state_type y;
state_type d;
};
NewtonResult solve_bdf_system(time_type t_new, const state_type& y_predict,
time_type c, const state_type& psi, const state_type& scale) {
NewtonResult result;
result.converged = false;
result.iterations = 0;
result.y = y_predict;
result.d = StateCreator<state_type>::create(y_predict);
// Initialize d to zero
for (std::size_t i = 0; i < result.d.size(); ++i) {
result.d[i] = static_cast<time_type>(0.0);
}
// Newton iteration to solve: d - c * f(t_new, y_predict + d) = -psi
for (int k = 0; k < NEWTON_MAXITER; ++k) {
result.iterations = k + 1;
// Evaluate f at current y = y_predict + d
state_type f = StateCreator<state_type>::create(result.y);
this->sys_(t_new, result.y, f);
// Check if f is finite
bool f_finite = true;
for (std::size_t i = 0; i < f.size(); ++i) {
if (!std::isfinite(f[i])) {
f_finite = false;
break;
}
}
if (!f_finite) break;
// Compute residual: F(d) = d - c * f(t_new, y_predict + d) + psi
state_type residual = StateCreator<state_type>::create(result.d);
for (std::size_t i = 0; i < residual.size(); ++i) {
residual[i] = result.d[i] - c * f[i] + psi[i];
}
// Check convergence
time_type norm = static_cast<time_type>(0.0);
for (std::size_t i = 0; i < residual.size(); ++i) {
time_type scaled_res = residual[i] / scale[i];
norm += scaled_res * scaled_res;
}
norm = std::sqrt(norm / static_cast<time_type>(residual.size()));
if (norm < newton_tolerance_) {
result.converged = true;
break;
}
// Newton step: solve (I - c*J) * dd = -residual for dd
// Then update: d = d + dd, y = y_predict + d
state_type dd = StateCreator<state_type>::create(result.d);
for (std::size_t i = 0; i < result.d.size(); ++i) {
// Approximate Jacobian diagonal element
time_type jac_diag = estimate_jacobian_diagonal(i, result.y, t_new);
time_type denominator = static_cast<time_type>(1.0) - c * jac_diag;
if (std::abs(denominator) > static_cast<time_type>(1e-12)) {
dd[i] = -residual[i] / denominator;
} else {
dd[i] = -residual[i];
}
result.d[i] += dd[i];
result.y[i] = y_predict[i] + result.d[i];
}
}
return result;
}
bool bdf_step(const state_type& y_current, state_type& y_new, state_type& error, time_type dt) {
// SciPy-style BDF step implementation using differences array
time_type t_new = this->current_time_ + dt;
// Update D[1] for the current step size
// D[1] = h * f(t_current, y_current)
state_type f_current = StateCreator<state_type>::create(D_[0]);
this->sys_(this->current_time_, D_[0], f_current);
for (std::size_t i = 0; i < D_[0].size(); ++i) {
D_[1][i] = dt * f_current[i];
}
// Calculate y_predict = sum(D[:order+1])
state_type y_predict = StateCreator<state_type>::create(D_[0]);
for (std::size_t i = 0; i < y_predict.size(); ++i) {
y_predict[i] = static_cast<time_type>(0.0);
for (int j = 0; j <= current_order_; ++j) {
y_predict[i] += D_[j][i];
}
}
// Calculate scale = atol + rtol * abs(y_predict)
state_type scale = StateCreator<state_type>::create(y_predict);
for (std::size_t i = 0; i < scale.size(); ++i) {
scale[i] = this->atol_ + this->rtol_ * std::abs(y_predict[i]);
}
// Calculate psi = dot(D[1:order+1].T, gamma[1:order+1]) / alpha[order]
state_type psi = StateCreator<state_type>::create(y_predict);
for (std::size_t i = 0; i < psi.size(); ++i) {
psi[i] = static_cast<time_type>(0.0);
for (int j = 1; j <= current_order_; ++j) {
psi[i] += D_[j][i] * gamma_[j];
}
psi[i] /= alpha_[current_order_];
}
// Calculate c = h / alpha[order]
time_type c = dt / alpha_[current_order_];
// Solve BDF system
auto result = solve_bdf_system(t_new, y_predict, c, psi, scale);
if (!result.converged) {
return false;
}
// Calculate error = error_const[order] * d
for (std::size_t i = 0; i < error.size(); ++i) {
error[i] = error_const_[current_order_] * result.d[i];
}
// Calculate error norm
time_type error_norm = static_cast<time_type>(0.0);
for (std::size_t i = 0; i < error.size(); ++i) {
time_type scaled_error = error[i] / scale[i];
error_norm += scaled_error * scaled_error;
}
error_norm = std::sqrt(error_norm / static_cast<time_type>(error.size()));
if (error_norm > static_cast<time_type>(1.0)) {
return false; // Step rejected
}
// Step accepted - update differences array (SciPy style)
// Store the new solution
y_new = result.y;
// Update differences array according to SciPy's algorithm
// D[0] should be the new solution
D_[0] = y_new;
// Update differences: D[order+2] = d - D[order+1], D[order+1] = d
if (current_order_ + 2 < static_cast<int>(D_.size())) {
for (std::size_t i = 0; i < result.d.size(); ++i) {
D_[current_order_ + 2][i] = result.d[i] - D_[current_order_ + 1][i];
}
}
D_[current_order_ + 1] = result.d;
// Update differences: D[i] += D[i+1] for i = order, order-1, ..., 1 (not 0!)
for (int i = current_order_; i >= 1; --i) {
for (std::size_t j = 0; j < D_[i].size(); ++j) {
D_[i][j] += D_[i + 1][j];
}
}
return true;
}
time_type estimate_jacobian_diagonal(std::size_t i, const state_type& y, time_type t) {
// Estimate diagonal element of Jacobian using finite differences
time_type epsilon = (static_cast<time_type>(1e-8) > std::abs(y[i]) * static_cast<time_type>(1e-8)) ? static_cast<time_type>(1e-8) : std::abs(y[i]) * static_cast<time_type>(1e-8);
state_type y_pert = StateCreator<state_type>::create(y);
state_type f_orig = StateCreator<state_type>::create(y);
state_type f_pert = StateCreator<state_type>::create(y);
// Evaluate f at original point
this->sys_(t, y, f_orig);
// Perturb y[i] and evaluate f
y_pert = y;
y_pert[i] += epsilon;
this->sys_(t, y_pert, f_pert);
// Estimate ∂f_i/∂y_i with better numerical stability
time_type jac = (f_pert[i] - f_orig[i]) / epsilon;
// For exponential decay dy/dt = -y, Jacobian should be -1
// Clamp to reasonable range to avoid numerical issues
return (static_cast<time_type>(-100.0) > jac) ? static_cast<time_type>(-100.0) : ((jac > static_cast<time_type>(100.0)) ? static_cast<time_type>(100.0) : jac);
}
// Note: update_differences_array is now handled directly in bdf_step
time_type fallback_step(state_type& state, time_type dt) {
// Fallback to simple forward Euler for problematic cases
time_type small_dt = (this->dt_min_ * static_cast<time_type>(2.0) > dt * static_cast<time_type>(0.1)) ? this->dt_min_ * static_cast<time_type>(2.0) : dt * static_cast<time_type>(0.1);
state_type f_current = StateCreator<state_type>::create(state);
// Simple forward Euler step
this->sys_(this->current_time_, state, f_current);
for (std::size_t i = 0; i < state.size(); ++i) {
state[i] = state[i] + small_dt * f_current[i];
}
this->advance_time(small_dt);
// Reset to order 1 and reinitialize
current_order_ = 1;
n_equal_steps_ = 0;
h_abs_ = small_dt;
return small_dt;
}
};
} // namespace diffeq