scirs2_integrate/ode/utils/jacobian/
specialized.rs

1//! Specialized Jacobian approximation techniques
2//!
3//! This module provides specialized techniques for Jacobian approximation
4//! for various types of ODE systems. These include methods for banded Jacobians,
5//! sparse Jacobians, and systems with special structure.
6
7use crate::common::IntegrateFloat;
8use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
9
10/// Computes Jacobian for a banded system with specified number of lower and upper diagonals
11#[allow(dead_code)]
12pub fn compute_banded_jacobian<F, Func>(
13    f: &Func,
14    t: F,
15    y: &Array1<F>,
16    f_current: &Array1<F>,
17    lower: usize,
18    upper: usize,
19) -> Array2<F>
20where
21    F: IntegrateFloat,
22    Func: Fn(F, ArrayView1<F>) -> Array1<F>,
23{
24    let n = y.len();
25    let mut jac = Array2::<F>::zeros((n, n));
26    let eps = F::from_f64(1e-8).unwrap();
27
28    // Compute only the diagonals within the band
29    for j in 0..n {
30        // Define the range of rows affected by column j
31        let row_start = j.saturating_sub(lower);
32        let row_end = (j + upper + 1).min(n);
33
34        // Only compute if there are rows in range
35        if row_start < row_end {
36            // Perturb the j-th component
37            let mut y_perturbed = y.clone();
38            let perturbation = eps * (F::one() + y[j].abs()).max(F::one());
39            y_perturbed[j] += perturbation;
40
41            // Evaluate function at perturbed point
42            let f_perturbed = f(t, y_perturbed.view());
43
44            // Compute entries in the band for this column
45            for i in row_start..row_end {
46                jac[[i, j]] = (f_perturbed[i] - f_current[i]) / perturbation;
47            }
48        }
49    }
50
51    jac
52}
53
54/// Computes Jacobian for a system with diagonal or block-diagonal structure
55#[allow(dead_code)]
56pub fn compute_diagonal_jacobian<F, Func>(
57    f: &Func,
58    t: F,
59    y: &Array1<F>,
60    f_current: &Array1<F>,
61    block_size: usize,
62) -> Array2<F>
63where
64    F: IntegrateFloat,
65    Func: Fn(F, ArrayView1<F>) -> Array1<F>,
66{
67    let n = y.len();
68    let mut jac = Array2::<F>::zeros((n, n));
69    let eps = F::from_f64(1e-8).unwrap();
70
71    // For each column, only compute the elements in the block-diagonal
72    for j in 0..n {
73        // Determine which block this column belongs to
74        let block_idx = j / block_size;
75        let block_start = block_idx * block_size;
76        let block_end = (block_start + block_size).min(n);
77
78        // Perturb the j-th component
79        let mut y_perturbed = y.clone();
80        let perturbation = eps * (F::one() + y[j].abs()).max(F::one());
81        y_perturbed[j] += perturbation;
82
83        // Evaluate function at perturbed point
84        let f_perturbed = f(t, y_perturbed.view());
85
86        // Compute only the elements in the same block
87        for i in block_start..block_end {
88            jac[[i, j]] = (f_perturbed[i] - f_current[i]) / perturbation;
89        }
90    }
91
92    jac
93}
94
95/// Group variables based on their interactions to minimize function evaluations
96#[allow(dead_code)]
97pub fn compute_colored_jacobian<F, Func>(
98    f: &Func,
99    t: F,
100    y: &Array1<F>,
101    f_current: &Array1<F>,
102    coloring: &[usize], // Each entry contains the color of the corresponding variable
103) -> Array2<F>
104where
105    F: IntegrateFloat,
106    Func: Fn(F, ArrayView1<F>) -> Array1<F>,
107{
108    let n = y.len();
109    let mut jac = Array2::<F>::zeros((n, n));
110    let eps = F::from_f64(1e-8).unwrap();
111
112    // Determine the number of colors
113    let max_color = coloring.iter().max().map_or(0, |&x| x) + 1;
114
115    // For each color, perturb all variables of that color simultaneously
116    for color in 0..max_color {
117        // Create a perturbation vector that perturbs all variables of this color
118        let mut y_perturbed = y.clone();
119        let mut perturbations = vec![F::zero(); n];
120
121        // Set perturbations for variables of this color
122        for j in 0..n {
123            if coloring[j] == color {
124                let perturbation = eps * (F::one() + y[j].abs()).max(F::one());
125                y_perturbed[j] += perturbation;
126                perturbations[j] = perturbation;
127            }
128        }
129
130        // Evaluate function with all variables of this color perturbed
131        let f_perturbed = f(t, y_perturbed.view());
132
133        // Compute Jacobian columns for variables of this color
134        for j in 0..n {
135            if coloring[j] == color && perturbations[j] > F::zero() {
136                for i in 0..n {
137                    jac[[i, j]] = (f_perturbed[i] - f_current[i]) / perturbations[j];
138                }
139            }
140        }
141    }
142
143    jac
144}
145
146/// Generate a simple coloring for a banded matrix
147#[allow(dead_code)]
148pub fn generate_banded_coloring(n: usize, lower: usize, upper: usize) -> Vec<usize> {
149    let bandwidth = lower + upper + 1;
150    let mut coloring = vec![0; n];
151
152    for (i, color) in coloring.iter_mut().enumerate().take(n) {
153        // Assign a color (modulo bandwidth) to each variable
154        // This ensures no two variables that could interact have the same color
155        *color = i % bandwidth;
156    }
157
158    coloring
159}
160
161/// Update the Jacobian using Broyden's method (rank-1 update)
162/// J_{k+1} = J_k + (df - J_k * dy) * dy^T / (dy^T * dy)
163#[allow(dead_code)]
164pub fn broyden_update<F>(_jac: &mut Array2<F>, delta_y: &Array1<F>, deltaf: &Array1<F>)
165where
166    F: IntegrateFloat,
167{
168    let n = delta_y.len();
169
170    // Compute J_k * dy
171    let mut jac_dy = Array1::zeros(n);
172    for i in 0..n {
173        for j in 0..n {
174            jac_dy[i] += _jac[[i, j]] * delta_y[j];
175        }
176    }
177
178    // Compute correction vector: df - J_k * dy
179    let correction = deltaf - &jac_dy;
180
181    // Compute denominator: dy^T * dy
182    let dy_norm_squared = delta_y.iter().map(|&x| x * x).sum::<F>();
183
184    // Apply update if denominator is not too small
185    if dy_norm_squared > F::from_f64(1e-14).unwrap() {
186        for i in 0..n {
187            for j in 0..n {
188                _jac[[i, j]] += correction[i] * delta_y[j] / dy_norm_squared;
189            }
190        }
191    }
192}
193
194/// Performs a block-update of the Jacobian using block structure
195#[allow(dead_code)]
196pub fn block_update<F>(
197    jac: &mut Array2<F>,
198    delta_y: &Array1<F>,
199    delta_f: &Array1<F>,
200    block_size: usize,
201) where
202    F: IntegrateFloat,
203{
204    let n = delta_y.len();
205    let n_blocks = n.div_ceil(block_size);
206
207    // Process each block separately
208    for block in 0..n_blocks {
209        let start = block * block_size;
210        let end = (start + block_size).min(n);
211
212        // Extract block components
213        let mut block_dy = Array1::zeros(end - start);
214        let mut block_df = Array1::zeros(end - start);
215
216        for i in start..end {
217            block_dy[i - start] = delta_y[i];
218            block_df[i - start] = delta_f[i];
219        }
220
221        // Compute block_jac * block_dy
222        let mut block_jac_dy = Array1::zeros(end - start);
223        for i in 0..(end - start) {
224            for j in 0..(end - start) {
225                block_jac_dy[i] += jac[[i + start, j + start]] * block_dy[j];
226            }
227        }
228
229        // Update the block using Broyden's formula
230        let correction = &block_df - &block_jac_dy;
231        let dy_norm_squared = block_dy.iter().map(|&x| x * x).sum::<F>();
232
233        if dy_norm_squared > F::from_f64(1e-14).unwrap() {
234            for i in 0..(end - start) {
235                for j in 0..(end - start) {
236                    jac[[i + start, j + start]] += correction[i] * block_dy[j] / dy_norm_squared;
237                }
238            }
239        }
240    }
241}