DiffEq - Modern C++ ODE Integration Library 1.0.0
High-performance C++ library for solving ODEs with async signal processing
Loading...
Searching...
No Matches
bdf.hpp
1#pragma once
2#include <core/concepts.hpp>
3#include <core/adaptive_integrator.hpp>
4#include <core/state_creator.hpp>
5#include <vector>
6#include <array>
7#include <cmath>
8#include <stdexcept>
9#include <algorithm>
10#include <limits>
11#include <numeric>
12#include <iostream>
13
14namespace diffeq {
15
16// SciPy BDF constants
17constexpr int MAX_ORDER = 5;
18constexpr int NEWTON_MAXITER = 4;
19constexpr double MIN_FACTOR = 0.2;
20constexpr double MAX_FACTOR = 10.0;
21
34template<typename S>
36public:
38 using state_type = typename base_type::state_type;
39 using time_type = typename base_type::time_type;
40 using value_type = typename base_type::value_type;
41 using system_function = typename base_type::system_function;
42
43 explicit BDFIntegrator(system_function sys,
44 time_type rtol = static_cast<time_type>(1e-3),
45 time_type atol = static_cast<time_type>(1e-6),
46 int max_order = 5)
47 : base_type(std::move(sys), rtol, atol),
48 max_order_((max_order < MAX_ORDER) ? max_order : MAX_ORDER),
49 current_order_(1),
50 newton_tolerance_(static_cast<time_type>(1e-3)),
51 is_initialized_(false),
52 h_abs_(static_cast<time_type>(0.0)),
53 h_abs_old_(static_cast<time_type>(0.0)),
54 error_norm_old_(static_cast<time_type>(0.0)),
55 n_equal_steps_(0) {
56
57 // Initialize BDF coefficients (SciPy style)
58 initialize_bdf_coefficients();
59 }
60
61 void step(state_type& state, time_type dt) override {
62 adaptive_step(state, dt);
63 }
64
65 void fixed_step(state_type& state, time_type dt) {
66 // Simple fixed-step BDF1 implementation
67 if (!is_initialized_) {
68 initialize_history(state, dt);
69 is_initialized_ = true;
70 }
71
72 state_type y_new = StateCreator<state_type>::create(state);
73 state_type error = StateCreator<state_type>::create(state);
74
75 bool success = bdf_step(state, y_new, error, dt);
76
77 if (success) {
78 state = y_new;
79 this->advance_time(dt);
80 } else {
81 // Fallback to explicit Euler if Newton fails
82 state_type f = StateCreator<state_type>::create(state);
83 this->sys_(this->current_time_, state, f);
84 for (std::size_t i = 0; i < state.size(); ++i) {
85 state[i] += dt * f[i];
86 }
87 this->advance_time(dt);
88 }
89 }
90
91 time_type adaptive_step(state_type& state, time_type dt) override {
92 if (!is_initialized_) {
93 initialize_history(state, dt);
94 is_initialized_ = true;
95 }
96
97 // SciPy-style step size control
98 time_type t = this->current_time_;
99 time_type max_step = this->dt_max_;
100 time_type min_step = (this->dt_min_ > static_cast<time_type>(1e-10)) ? this->dt_min_ : static_cast<time_type>(1e-10);
101
102 time_type h_abs = h_abs_;
103 if (h_abs > max_step) {
104 h_abs = max_step;
105 change_D(current_order_, max_step / h_abs_);
106 n_equal_steps_ = 0;
107 } else if (h_abs < min_step) {
108 h_abs = min_step;
109 change_D(current_order_, min_step / h_abs_);
110 n_equal_steps_ = 0;
111 }
112
113 bool step_accepted = false;
114 int attempts = 0;
115 const int max_attempts = 10; // Limit attempts to avoid infinite loops
116
117 while (!step_accepted && attempts < max_attempts) {
118 attempts++;
119
120 if (h_abs < min_step) {
121 return fallback_step(state, dt);
122 }
123
124 time_type h = h_abs;
125 time_type t_new = t + h;
126
127 // Check boundary
128 if (t_new > this->current_time_ + dt) {
129 t_new = this->current_time_ + dt;
130 change_D(current_order_, std::abs(t_new - t) / h_abs);
131 n_equal_steps_ = 0;
132 }
133
134 h = t_new - t;
135 h_abs = std::abs(h);
136
137 state_type y_new = StateCreator<state_type>::create(state);
138 state_type error = StateCreator<state_type>::create(state);
139
140 time_type factor = static_cast<time_type>(1.0); // Declare here for all branches
141
142 bool bdf_success = bdf_step(state, y_new, error, h);
143 if (bdf_success) {
144 // Calculate error norm with proper scaling
145 state_type scale = StateCreator<state_type>::create(y_new);
146 for (std::size_t i = 0; i < scale.size(); ++i) {
147 scale[i] = this->atol_ + this->rtol_ * std::abs(y_new[i]);
148 }
149
150 time_type error_norm = static_cast<time_type>(0.0);
151 for (std::size_t i = 0; i < error.size(); ++i) {
152 time_type scaled_error = error[i] / scale[i];
153 error_norm += scaled_error * scaled_error;
154 }
155 error_norm = std::sqrt(error_norm / static_cast<time_type>(error.size()));
156
157 if (error_norm <= static_cast<time_type>(1.0)) {
158 // Accept step
159 step_accepted = true;
160 n_equal_steps_++;
161
162 state = y_new;
163 this->advance_time(h);
164 h_abs_ = h_abs;
165
166 // Differences array is updated in bdf_step
167
168 // Adjust order if we have enough equal steps
169 adjust_order(error_norm, scale);
170
171 // Calculate next step size with SciPy-style adaptive logic
172 time_type safety = static_cast<time_type>(0.8); // More conservative safety factor
173 time_type factor = safety * std::pow(error_norm, static_cast<time_type>(-1.0) / static_cast<time_type>(current_order_ + 1));
174
175 // Apply SciPy-style step size bounds (more conservative)
176 time_type max_factor_limit = static_cast<time_type>(1.5); // Even more conservative step size growth
177 if (factor > max_factor_limit) factor = max_factor_limit;
178 if (factor < static_cast<time_type>(MIN_FACTOR)) factor = static_cast<time_type>(MIN_FACTOR);
179
180 h_abs_ *= factor;
181 change_D(current_order_, factor);
182 n_equal_steps_ = 0;
183 } else {
184 // Reject step - be more conservative
185 factor = static_cast<time_type>(0.5); // Fixed reduction factor
186 h_abs *= factor;
187 change_D(current_order_, factor);
188 n_equal_steps_ = 0;
189 }
190 } else {
191 // Newton failed - try smaller step
192 factor = static_cast<time_type>(0.25); // More aggressive reduction for Newton failure
193 h_abs *= factor;
194 change_D(current_order_, factor);
195 n_equal_steps_ = 0;
196 }
197 }
198
199 // If we exit the loop without accepting a step, use fallback
200 if (!step_accepted) {
201 return fallback_step(state, dt);
202 }
203
204 return h_abs_;
205 }
206
207 void set_newton_parameters(int max_iterations, time_type tolerance) {
208 // max_iterations is now fixed at NEWTON_MAXITER = 4 (SciPy style)
209 newton_tolerance_ = tolerance;
210 }
211
212private:
213 int max_order_;
214 int current_order_;
215 time_type newton_tolerance_;
216 bool is_initialized_;
217 time_type h_abs_;
218 time_type h_abs_old_;
219 time_type error_norm_old_;
220 int n_equal_steps_;
221
222 // SciPy-style differences array (D) - stores solution and derivatives
223 std::vector<state_type> D_; // D[0] = y, D[1] = h*f, D[2] = differences, etc.
224
225 // SciPy BDF coefficients
226 std::array<time_type, MAX_ORDER + 2> gamma_; // Gamma coefficients
227 std::array<time_type, MAX_ORDER + 2> alpha_; // Alpha coefficients
228 std::array<time_type, MAX_ORDER + 2> error_const_; // Error constants
229
230 void initialize_bdf_coefficients() {
231 // SciPy BDF coefficients - exactly matching the Python implementation
232 std::array<time_type, MAX_ORDER + 1> kappa = {
233 static_cast<time_type>(0),
234 static_cast<time_type>(-0.1850),
235 static_cast<time_type>(-1.0/9.0),
236 static_cast<time_type>(-0.0823),
237 static_cast<time_type>(-0.0415),
238 static_cast<time_type>(0)
239 };
240
241 // Initialize gamma array: gamma[0] = 0, gamma[k] = sum(1/j for j=1..k)
242 gamma_[0] = static_cast<time_type>(0.0);
243 for (int k = 1; k <= MAX_ORDER; ++k) {
244 gamma_[k] = gamma_[k-1] + static_cast<time_type>(1.0) / static_cast<time_type>(k);
245 }
246
247 // Initialize alpha array: alpha = (1 - kappa) * gamma
248 for (int k = 0; k <= MAX_ORDER; ++k) {
249 alpha_[k] = (static_cast<time_type>(1.0) - kappa[k]) * gamma_[k];
250 }
251
252 // Initialize error constants: error_const = kappa * gamma + 1/(k+1)
253 for (int k = 0; k <= MAX_ORDER; ++k) {
254 error_const_[k] = kappa[k] * gamma_[k] + static_cast<time_type>(1.0) / static_cast<time_type>(k + 1);
255 }
256 }
257
258 // SciPy-style helper functions for differences array
259 std::vector<std::vector<time_type>> compute_R(int order, time_type factor) {
260 std::vector<std::vector<time_type>> M(order + 1, std::vector<time_type>(order + 1, static_cast<time_type>(0.0)));
261
262 // Initialize M matrix for changing differences array
263 for (int i = 1; i <= order; ++i) {
264 for (int j = 1; j <= order; ++j) {
265 M[i][j] = (static_cast<time_type>(i - 1) - factor * static_cast<time_type>(j)) / static_cast<time_type>(i);
266 }
267 }
268 M[0][0] = static_cast<time_type>(1.0);
269
270 // Compute cumulative product along rows (SciPy style)
271 std::vector<std::vector<time_type>> R(order + 1, std::vector<time_type>(order + 1, static_cast<time_type>(0.0)));
272 for (int i = 0; i <= order; ++i) {
273 R[i][0] = M[i][0];
274 for (int j = 1; j <= order; ++j) {
275 R[i][j] = R[i][j-1] * M[i][j];
276 }
277 }
278
279 return R;
280 }
281
282 void change_D(int order, time_type factor) {
283 auto R = compute_R(order, factor);
284 auto U = compute_R(order, static_cast<time_type>(1.0));
285
286 // Compute RU = R * U
287 std::vector<std::vector<time_type>> RU(order + 1, std::vector<time_type>(order + 1, static_cast<time_type>(0.0)));
288 for (int i = 0; i <= order; ++i) {
289 for (int j = 0; j <= order; ++j) {
290 for (int k = 0; k <= order; ++k) {
291 RU[i][j] += R[i][k] * U[k][j];
292 }
293 }
294 }
295
296 // Apply transformation: D[:order+1] = RU.T @ D[:order+1]
297 std::vector<state_type> D_new(order + 1);
298 for (int i = 0; i <= order; ++i) {
299 D_new[i] = StateCreator<state_type>::create(D_[0]);
300 for (std::size_t k = 0; k < D_[0].size(); ++k) {
301 D_new[i][k] = static_cast<time_type>(0.0);
302 for (int j = 0; j <= order; ++j) {
303 D_new[i][k] += RU[j][i] * D_[j][k];
304 }
305 }
306 }
307
308 // Copy back
309 for (int i = 0; i <= order; ++i) {
310 D_[i] = D_new[i];
311 }
312 }
313
314 void initialize_history(const state_type& y0, time_type dt) {
315 // Initialize differences array D (SciPy style)
316 D_.clear();
317 D_.resize(MAX_ORDER + 3);
318
319 for (int i = 0; i < MAX_ORDER + 3; ++i) {
321 // Initialize all to zero except D[0]
322 for (std::size_t j = 0; j < y0.size(); ++j) {
323 D_[i][j] = static_cast<time_type>(0.0);
324 }
325 }
326
327 // D[0] = y0
328 D_[0] = y0;
329
330 // Initialize D[1] = h * f(t0, y0) for the first step
331 state_type f0 = StateCreator<state_type>::create(y0);
332 this->sys_(this->current_time_, y0, f0);
333 for (std::size_t i = 0; i < y0.size(); ++i) {
334 D_[1][i] = dt * f0[i];
335 }
336
337 current_order_ = 1;
338 n_equal_steps_ = 0;
339 h_abs_ = dt;
340 }
341
342 void update_history(const state_type& y_new, time_type dt) {
343 // Update differences array after successful step (done in main step function)
344 h_abs_old_ = h_abs_;
345 h_abs_ = dt;
346 }
347
348 void adjust_order(time_type error_norm, const state_type& scale) {
349 // SciPy-style order selection - only adjust if we have enough equal steps
350 if (n_equal_steps_ < current_order_ + 1) {
351 return;
352 }
353
354 // Calculate error norms for different orders
355 time_type error_m_norm = std::numeric_limits<time_type>::infinity();
356 time_type error_p_norm = std::numeric_limits<time_type>::infinity();
357
358 if (current_order_ > 1) {
359 // Error estimate for order-1
360 time_type error_m = static_cast<time_type>(0.0);
361 for (std::size_t i = 0; i < D_[0].size(); ++i) {
362 time_type err = error_const_[current_order_ - 1] * D_[current_order_][i] / scale[i];
363 error_m += err * err;
364 }
365 error_m_norm = std::sqrt(error_m / static_cast<time_type>(D_[0].size()));
366 }
367
368 if (current_order_ < max_order_ && current_order_ + 2 < static_cast<int>(D_.size())) {
369 // Error estimate for order+1
370 time_type error_p = static_cast<time_type>(0.0);
371 for (std::size_t i = 0; i < D_[0].size(); ++i) {
372 time_type err = error_const_[current_order_ + 1] * D_[current_order_ + 2][i] / scale[i];
373 error_p += err * err;
374 }
375 error_p_norm = std::sqrt(error_p / static_cast<time_type>(D_[0].size()));
376 }
377
378 // Calculate factors for order selection (SciPy style)
379 std::array<time_type, 3> error_norms = {error_m_norm, error_norm, error_p_norm};
380 std::array<time_type, 3> factors;
381
382 for (int i = 0; i < 3; ++i) {
383 if (error_norms[i] > static_cast<time_type>(0) && std::isfinite(error_norms[i])) {
384 factors[i] = std::pow(error_norms[i], static_cast<time_type>(-1.0) / static_cast<time_type>(current_order_ + i - 1));
385 } else {
386 factors[i] = static_cast<time_type>(0.0); // Prefer current order if error estimate is invalid
387 }
388 }
389
390 // Select order with maximum factor
391 int best_order_idx = 1; // Default to current order
392 for (int i = 0; i < 3; ++i) {
393 if (factors[i] > factors[best_order_idx]) {
394 best_order_idx = i;
395 }
396 }
397
398 int delta_order = best_order_idx - 1;
399 int new_order = current_order_ + delta_order;
400
401 // Ensure order is within bounds
402 new_order = (new_order < 1) ? 1 : ((new_order > max_order_) ? max_order_ : new_order);
403
404 if (new_order != current_order_) {
405 current_order_ = new_order;
406 n_equal_steps_ = 0; // Reset step counter when order changes
407 }
408 }
409
410 // SciPy-style BDF system solver
411 struct NewtonResult {
412 bool converged;
413 int iterations;
414 state_type y;
415 state_type d;
416 };
417
418 NewtonResult solve_bdf_system(time_type t_new, const state_type& y_predict,
419 time_type c, const state_type& psi, const state_type& scale) {
420 NewtonResult result;
421 result.converged = false;
422 result.iterations = 0;
423 result.y = y_predict;
424 result.d = StateCreator<state_type>::create(y_predict);
425
426 // Initialize d to zero
427 for (std::size_t i = 0; i < result.d.size(); ++i) {
428 result.d[i] = static_cast<time_type>(0.0);
429 }
430
431 // Newton iteration to solve: d - c * f(t_new, y_predict + d) = -psi
432 for (int k = 0; k < NEWTON_MAXITER; ++k) {
433 result.iterations = k + 1;
434
435 // Evaluate f at current y = y_predict + d
436 state_type f = StateCreator<state_type>::create(result.y);
437 this->sys_(t_new, result.y, f);
438
439 // Check if f is finite
440 bool f_finite = true;
441 for (std::size_t i = 0; i < f.size(); ++i) {
442 if (!std::isfinite(f[i])) {
443 f_finite = false;
444 break;
445 }
446 }
447 if (!f_finite) break;
448
449 // Compute residual: F(d) = d - c * f(t_new, y_predict + d) + psi
450 state_type residual = StateCreator<state_type>::create(result.d);
451 for (std::size_t i = 0; i < residual.size(); ++i) {
452 residual[i] = result.d[i] - c * f[i] + psi[i];
453 }
454
455 // Check convergence
456 time_type norm = static_cast<time_type>(0.0);
457 for (std::size_t i = 0; i < residual.size(); ++i) {
458 time_type scaled_res = residual[i] / scale[i];
459 norm += scaled_res * scaled_res;
460 }
461 norm = std::sqrt(norm / static_cast<time_type>(residual.size()));
462
463 if (norm < newton_tolerance_) {
464 result.converged = true;
465 break;
466 }
467
468 // Newton step: solve (I - c*J) * dd = -residual for dd
469 // Then update: d = d + dd, y = y_predict + d
470 state_type dd = StateCreator<state_type>::create(result.d);
471 for (std::size_t i = 0; i < result.d.size(); ++i) {
472 // Approximate Jacobian diagonal element
473 time_type jac_diag = estimate_jacobian_diagonal(i, result.y, t_new);
474 time_type denominator = static_cast<time_type>(1.0) - c * jac_diag;
475 if (std::abs(denominator) > static_cast<time_type>(1e-12)) {
476 dd[i] = -residual[i] / denominator;
477 } else {
478 dd[i] = -residual[i];
479 }
480 result.d[i] += dd[i];
481 result.y[i] = y_predict[i] + result.d[i];
482 }
483 }
484
485 return result;
486 }
487
488 bool bdf_step(const state_type& y_current, state_type& y_new, state_type& error, time_type dt) {
489 // SciPy-style BDF step implementation using differences array
490 time_type t_new = this->current_time_ + dt;
491
492 // Update D[1] for the current step size
493 // D[1] = h * f(t_current, y_current)
494 state_type f_current = StateCreator<state_type>::create(D_[0]);
495 this->sys_(this->current_time_, D_[0], f_current);
496 for (std::size_t i = 0; i < D_[0].size(); ++i) {
497 D_[1][i] = dt * f_current[i];
498 }
499
500 // Calculate y_predict = sum(D[:order+1])
501 state_type y_predict = StateCreator<state_type>::create(D_[0]);
502 for (std::size_t i = 0; i < y_predict.size(); ++i) {
503 y_predict[i] = static_cast<time_type>(0.0);
504 for (int j = 0; j <= current_order_; ++j) {
505 y_predict[i] += D_[j][i];
506 }
507 }
508
509 // Calculate scale = atol + rtol * abs(y_predict)
510 state_type scale = StateCreator<state_type>::create(y_predict);
511 for (std::size_t i = 0; i < scale.size(); ++i) {
512 scale[i] = this->atol_ + this->rtol_ * std::abs(y_predict[i]);
513 }
514
515 // Calculate psi = dot(D[1:order+1].T, gamma[1:order+1]) / alpha[order]
516 state_type psi = StateCreator<state_type>::create(y_predict);
517 for (std::size_t i = 0; i < psi.size(); ++i) {
518 psi[i] = static_cast<time_type>(0.0);
519 for (int j = 1; j <= current_order_; ++j) {
520 psi[i] += D_[j][i] * gamma_[j];
521 }
522 psi[i] /= alpha_[current_order_];
523 }
524
525 // Calculate c = h / alpha[order]
526 time_type c = dt / alpha_[current_order_];
527
528 // Solve BDF system
529 auto result = solve_bdf_system(t_new, y_predict, c, psi, scale);
530 if (!result.converged) {
531 return false;
532 }
533
534 // Calculate error = error_const[order] * d
535 for (std::size_t i = 0; i < error.size(); ++i) {
536 error[i] = error_const_[current_order_] * result.d[i];
537 }
538
539 // Calculate error norm
540 time_type error_norm = static_cast<time_type>(0.0);
541 for (std::size_t i = 0; i < error.size(); ++i) {
542 time_type scaled_error = error[i] / scale[i];
543 error_norm += scaled_error * scaled_error;
544 }
545 error_norm = std::sqrt(error_norm / static_cast<time_type>(error.size()));
546
547 if (error_norm > static_cast<time_type>(1.0)) {
548 return false; // Step rejected
549 }
550
551 // Step accepted - update differences array (SciPy style)
552 // Store the new solution
553 y_new = result.y;
554
555 // Update differences array according to SciPy's algorithm
556 // D[0] should be the new solution
557 D_[0] = y_new;
558
559 // Update differences: D[order+2] = d - D[order+1], D[order+1] = d
560 if (current_order_ + 2 < static_cast<int>(D_.size())) {
561 for (std::size_t i = 0; i < result.d.size(); ++i) {
562 D_[current_order_ + 2][i] = result.d[i] - D_[current_order_ + 1][i];
563 }
564 }
565 D_[current_order_ + 1] = result.d;
566
567 // Update differences: D[i] += D[i+1] for i = order, order-1, ..., 1 (not 0!)
568 for (int i = current_order_; i >= 1; --i) {
569 for (std::size_t j = 0; j < D_[i].size(); ++j) {
570 D_[i][j] += D_[i + 1][j];
571 }
572 }
573
574 return true;
575 }
576
577 time_type estimate_jacobian_diagonal(std::size_t i, const state_type& y, time_type t) {
578 // Estimate diagonal element of Jacobian using finite differences
579 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);
580 state_type y_pert = StateCreator<state_type>::create(y);
581 state_type f_orig = StateCreator<state_type>::create(y);
582 state_type f_pert = StateCreator<state_type>::create(y);
583
584 // Evaluate f at original point
585 this->sys_(t, y, f_orig);
586
587 // Perturb y[i] and evaluate f
588 y_pert = y;
589 y_pert[i] += epsilon;
590 this->sys_(t, y_pert, f_pert);
591
592 // Estimate ∂f_i/∂y_i with better numerical stability
593 time_type jac = (f_pert[i] - f_orig[i]) / epsilon;
594
595 // For exponential decay dy/dt = -y, Jacobian should be -1
596 // Clamp to reasonable range to avoid numerical issues
597 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);
598 }
599
600 // Note: update_differences_array is now handled directly in bdf_step
601
602 time_type fallback_step(state_type& state, time_type dt) {
603 // Fallback to simple forward Euler for problematic cases
604 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);
605
606 state_type f_current = StateCreator<state_type>::create(state);
607
608 // Simple forward Euler step
609 this->sys_(this->current_time_, state, f_current);
610
611 for (std::size_t i = 0; i < state.size(); ++i) {
612 state[i] = state[i] + small_dt * f_current[i];
613 }
614
615
616 this->advance_time(small_dt);
617
618 // Reset to order 1 and reinitialize
619 current_order_ = 1;
620 n_equal_steps_ = 0;
621 h_abs_ = small_dt;
622
623 return small_dt;
624 }
625};
626
627} // namespace diffeq
BDF (Backward Differentiation Formula) integrator.
Definition bdf.hpp:35