scirs2-integrate 0.4.2

Numerical integration module for SciRS2 (scirs2-integrate)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
//! Enhanced BDF method with improved Jacobian handling
//!
//! This module provides an enhanced version of the Backward Differentiation Formula (BDF)
//! method for ODE solving with improved Jacobian handling, better error estimation,
//! and more efficient linear solvers.

use crate::error::{IntegrateError, IntegrateResult};
use crate::ode::types::{ODEMethod, ODEOptions, ODEResult};
use crate::ode::utils::common::{calculate_error_weights, extrapolate};
use crate::ode::utils::jacobian::{
    newton_solve, JacobianManager, JacobianStrategy, JacobianStructure, NewtonParameters,
};
use crate::IntegrateFloat;
use scirs2_core::ndarray::{Array1, ArrayView1};

/// Solve ODE using an enhanced Backward Differentiation Formula (BDF) method
///
/// This implementation features:
/// - Improved Jacobian handling with reuse, quasi-updates, and coloring
/// - Better error estimation for both step size and order control
/// - More efficient linear solvers for different Jacobian structures
/// - Comprehensive diagnostic information
///
/// # Arguments
///
/// * `f` - ODE function dy/dt = f(t, y)
/// * `t_span` - Time span [t_start, t_end]
/// * `y0` - Initial condition
/// * `opts` - Solver options
///
/// # Returns
///
/// The solution as an ODEResult or an error
#[allow(dead_code)]
pub fn enhanced_bdf_method<F, Func>(
    f: Func,
    t_span: [F; 2],
    y0: Array1<F>,
    opts: ODEOptions<F>,
) -> IntegrateResult<ODEResult<F>>
where
    F: IntegrateFloat + std::default::Default,
    Func: Fn(F, ArrayView1<F>) -> Array1<F>,
{
    // Check BDF order is valid (1-5 supported)
    let order = opts.max_order.unwrap_or(2);
    if !(1..=5).contains(&order) {
        return Err(IntegrateError::ValueError(
            "BDF order must be between 1 and 5".to_string(),
        ));
    }

    // Initialize
    let [t_start, t_end] = t_span;
    let n_dim = y0.len();

    // Determine initial step size if not provided
    let h0 = opts.h0.unwrap_or_else(|| {
        // Simple heuristic for initial step size
        let _span = t_end - t_start;
        _span / F::from_usize(100).expect("Operation failed")
            * F::from_f64(0.1).expect("Operation failed") // 0.1% of interval
    });

    // Determine minimum and maximum step sizes
    let min_step = opts.min_step.unwrap_or_else(|| {
        let _span = t_end - t_start;
        _span * F::from_f64(1e-10).expect("Operation failed") // Minimal step size
    });

    let max_step = opts.max_step.unwrap_or_else(|| {
        t_end - t_start // Maximum step can be the whole interval
    });

    // Create the Jacobian manager with an appropriate strategy
    let jacobian_strategy = if n_dim <= 5 {
        JacobianStrategy::FiniteDifference // For small systems, full finite difference is fine
    } else if n_dim <= 100 {
        JacobianStrategy::BroydenUpdate // For medium systems, Broyden updates are efficient
    } else {
        JacobianStrategy::ModifiedNewton // For large systems, reuse Jacobian as much as possible
    };

    // Create Jacobian manager
    let jacobian_structure = if opts.use_banded_jacobian {
        let lower = opts.ml.unwrap_or(n_dim);
        let upper = opts.mu.unwrap_or(n_dim);
        JacobianStructure::Banded { lower, upper }
    } else {
        JacobianStructure::Dense
    };

    let mut jac_manager = JacobianManager::with_strategy(jacobian_strategy, jacobian_structure);

    // For BDF methods we need previous steps
    // We'll use another method (RK4) to bootstrap the initial steps
    // We need order previous points for a BDF method of order 'order'

    // First run RK4 to get the first 'order' points
    let mut h = h0;
    let mut t_values = vec![t_start];
    let mut y_values = vec![y0.clone()];
    let mut func_evals = 0;
    let mut step_count = 0;
    let mut accepted_steps = 0;
    let mut rejected_steps = 0;
    let mut newton_iters = F::zero();
    let mut n_lu = 0;
    let mut n_jac = 0;

    // Generate initial points using RK4 (more accurate than Euler)
    if order > 1 {
        let two = F::from_f64(2.0).expect("Operation failed");
        let mut t = t_start;
        let mut y = y0.clone();

        for _ in 0..(order - 1) {
            // Don't go beyond t_end
            if t + h > t_end {
                h = t_end - t;
            }

            // Standard RK4 step
            let half_step = h / two;

            let k1 = f(t, y.view());
            let k2 = f(t + half_step, (y.clone() + k1.clone() * half_step).view());
            let k3 = f(t + half_step, (y.clone() + k2.clone() * half_step).view());
            let k4 = f(t + h, (y.clone() + k3.clone() * h).view());
            func_evals += 4;

            // Combine slopes with appropriate weights
            let slope = (k1 + k2.clone() * two + k3.clone() * two + k4)
                / F::from_f64(6.0).expect("Operation failed");
            y = y + slope * h;

            // Update time
            t += h;

            // Store results
            t_values.push(t);
            y_values.push(y.clone());

            step_count += 1;
            accepted_steps += 1;

            // Don't continue if we've reached t_end
            if t >= t_end {
                break;
            }
        }
    }

    // Now we have enough points to start BDF
    let mut t = *t_values.last().expect("Operation failed");
    let mut y = y_values.last().expect("Operation failed").clone();

    // BDF coefficients for different orders
    // These are the coefficients for the BDF formula
    // For BDF of order p, we use p previous points

    // Coefficients for BDF1 (Implicit Euler) through BDF5
    let bdf_coefs: [Vec<F>; 5] = [
        // BDF1 (Implicit Euler): y_{n+1} - y_n = h * f(t_{n+1}, y_{n+1})
        vec![F::one(), F::from_f64(-1.0).expect("Operation failed")],
        // BDF2: 3/2 * y_{n+1} - 2 * y_n + 1/2 * y_{n-1} = h * f(t_{n+1}, y_{n+1})
        vec![
            F::from_f64(3.0 / 2.0).expect("Operation failed"),
            F::from_f64(-2.0).expect("Operation failed"),
            F::from_f64(1.0 / 2.0).expect("Operation failed"),
        ],
        // BDF3
        vec![
            F::from_f64(11.0 / 6.0).expect("Operation failed"),
            F::from_f64(-3.0).expect("Operation failed"),
            F::from_f64(3.0 / 2.0).expect("Operation failed"),
            F::from_f64(-1.0 / 3.0).expect("Operation failed"),
        ],
        // BDF4
        vec![
            F::from_f64(25.0 / 12.0).expect("Operation failed"),
            F::from_f64(-4.0).expect("Operation failed"),
            F::from_f64(3.0).expect("Operation failed"),
            F::from_f64(-4.0 / 3.0).expect("Operation failed"),
            F::from_f64(1.0 / 4.0).expect("Operation failed"),
        ],
        // BDF5
        vec![
            F::from_f64(137.0 / 60.0).expect("Operation failed"),
            F::from_f64(-5.0).expect("Operation failed"),
            F::from_f64(5.0).expect("Operation failed"),
            F::from_f64(-10.0 / 3.0).expect("Operation failed"),
            F::from_f64(5.0 / 4.0).expect("Operation failed"),
            F::from_f64(-1.0 / 5.0).expect("Operation failed"),
        ],
    ];

    // Initialize current order to the requested order (or lower if not enough history points)
    let mut current_order = order.min(t_values.len());
    let mut last_order_change = 0;
    let min_steps_before_order_change = 5;

    // Main integration loop
    while t < t_end && step_count < opts.max_steps {
        // Adjust step size for the last step if needed
        if t + h > t_end {
            h = t_end - t;
        }

        // Limit step size to bounds
        h = h.min(max_step).max(min_step);

        // Get coefficients for current order
        let coeffs = &bdf_coefs[current_order - 1];

        // Next time point
        let next_t = t + h;

        // Create historical times and values arrays for extrapolation
        let mut hist_times = Vec::with_capacity(t_values.len());
        let mut hist_values = Vec::with_capacity(y_values.len());

        // Collect the required history points based on current order
        let history_start = y_values.len().saturating_sub(current_order);
        for i in history_start..y_values.len() {
            hist_times.push(t_values[i]);
            hist_values.push(y_values[i].clone());
        }

        // Predict initial value using extrapolation from previous points
        let y_pred = if hist_values.len() > 1 {
            extrapolate(&hist_times, &hist_values, next_t)?
        } else {
            y.clone()
        };

        // Create the nonlinear system for BDF
        let bdf_system = |y_next: &Array1<F>| {
            // Compute BDF residual:
            // c_0 * y_{n+1} - h * f(t_{n+1}, y_{n+1}) - sum_{j=1}^p c_j * y_{n+1-j} = 0

            // Evaluate function at the current iterate
            let f_eval = f(next_t, y_next.view());

            // Initialize residual with c_0 * y_{n+1} term
            let mut residual = y_next.clone() * coeffs[0];

            // Subtract previous values contribution
            for (j, coeff) in coeffs.iter().enumerate().skip(1).take(current_order) {
                if j <= y_values.len() {
                    let idx = y_values.len() - j;
                    residual = residual - y_values[idx].clone() * *coeff;
                }
            }

            // Subtract h * f(t_{n+1}, y_{n+1}) term
            residual = residual - f_eval.clone() * h;

            residual
        };

        // Set up Newton solver parameters based on Jacobian strategy
        let update_freq = match jacobian_strategy {
            JacobianStrategy::FiniteDifference => 1, // Update every iteration
            JacobianStrategy::BroydenUpdate => 1,    // Update every iteration with Broyden
            JacobianStrategy::ModifiedNewton => 5,   // Update less frequently
            _ => 3,                                  // Default
        };

        let newton_params = NewtonParameters {
            max_iterations: 10,
            abs_tolerance: opts.rtol * F::from_f64(0.1).expect("Operation failed"),
            rel_tolerance: opts.rtol,
            jacobian_update_freq: update_freq,
            damping_factor: F::one(),
            min_damping: F::from_f64(0.1).expect("Operation failed"),
            force_jacobian_init: step_count == 0, // Force update on first step
            ..Default::default()
        };

        // Solve the nonlinear system using Newton's method
        let newton_result =
            newton_solve(bdf_system, y_pred.clone(), &mut jac_manager, newton_params);

        match newton_result {
            Ok(result) => {
                // Update counters
                func_evals += result.func_evals;
                n_jac += result.jac_evals;
                n_lu += result.linear_solves;
                newton_iters += F::from(result.iterations).expect("Failed to convert to float");

                // Update state
                let y_next = result.solution;

                // Compute error estimate by comparing with lower order solution
                // This is a more accurate error estimation strategy
                let error = if current_order > 1 {
                    // Compute error using solution from method one order lower
                    let lower_order = current_order - 1;
                    let lower_coeffs = &bdf_coefs[lower_order - 1];

                    // Compute the lower order solution using the values we already have
                    let mut rhs = Array1::<F>::zeros(n_dim);
                    for (j, &coeff) in lower_coeffs.iter().enumerate().skip(1).take(lower_order) {
                        if j <= y_values.len() {
                            let idx = y_values.len() - j;
                            rhs = rhs + y_values[idx].clone() * coeff;
                        }
                    }

                    // Add h * f term
                    let f_next = f(next_t, y_next.view());
                    func_evals += 1;
                    rhs = rhs + f_next.clone() * h;

                    // Solve for lower order solution
                    let mut y_lower = Array1::<F>::zeros(n_dim);
                    for i in 0..n_dim {
                        y_lower[i] = rhs[i] / lower_coeffs[0];
                    }

                    // Compute scaled error between higher and lower order solutions
                    let tol_scale = calculate_error_weights(&y_next, opts.atol, opts.rtol);
                    let mut max_err = F::zero();
                    for i in 0..n_dim {
                        let err = (y_next[i] - y_lower[i]).abs() / tol_scale[i];
                        max_err = max_err.max(err);
                    }
                    max_err
                } else {
                    // For order 1, we can't compare with a lower order
                    // Use a simpler error estimate based on residual
                    result.error_estimate
                };

                // Step size adjustment based on error
                let err_order = F::from_usize(current_order + 1).expect("Operation failed");
                let mut factor = if error > F::zero() {
                    F::from_f64(0.9).expect("Operation failed")
                        * (F::one() / error).powf(F::one() / err_order)
                } else {
                    F::from_f64(5.0).expect("Operation failed") // Maximum increase if error is zero
                };

                // Apply safety factors
                let factor_max = F::from_f64(5.0).expect("Operation failed");
                let factor_min = F::from_f64(0.2).expect("Operation failed");
                factor = factor.min(factor_max).max(factor_min);

                // Check if step is acceptable
                if error <= F::one() {
                    // Step accepted
                    t = next_t;
                    y = y_next;

                    // Store results
                    t_values.push(t);
                    y_values.push(y.clone());

                    step_count += 1;
                    accepted_steps += 1;

                    // Adjust step size for next step
                    h *= factor;

                    // Consider order adjustment - but not too frequently
                    if step_count - last_order_change >= min_steps_before_order_change {
                        // Strategy: try to estimate the error of the current order and one order higher
                        if current_order < order
                            && error < opts.rtol
                            && y_values.len() > current_order
                        {
                            // Consider increasing order
                            current_order += 1;
                            last_order_change = step_count;
                        } else if current_order > 1
                            && (error > F::from_f64(0.5).expect("Operation failed")
                                || result.iterations > 8)
                        {
                            // Consider decreasing order
                            current_order -= 1;
                            last_order_change = step_count;
                        }
                    }
                } else {
                    // Step rejected
                    h *= factor;
                    rejected_steps += 1;

                    // If we've reduced step size too much, report error
                    if h < min_step {
                        return Err(IntegrateError::ConvergenceError(
                            "Step size too small after rejection".to_string(),
                        ));
                    }
                }
            }
            Err(e) => {
                // Newton failed to converge
                h *= F::from_f64(0.5).expect("Operation failed");
                rejected_steps += 1;

                // If step size is too small, return error
                if h < min_step {
                    return Err(e);
                }
            }
        }
    }

    let success = t >= t_end;
    let message = if !success {
        Some(format!(
            "Maximum number of steps ({}) reached",
            opts.max_steps
        ))
    } else {
        Some(format!(
            "Integration completed successfully. Jacobian strategy: {jacobian_strategy:?}, Final order: {current_order}"
        ))
    };

    // Return the solution
    Ok(ODEResult {
        t: t_values,
        y: y_values,
        success,
        message,
        n_eval: func_evals,
        n_steps: step_count,
        n_accepted: accepted_steps,
        n_rejected: rejected_steps,
        n_lu,
        n_jac,
        method: ODEMethod::Bdf,
    })
}