scirs2_integrate/analysis/
utils.rs

1//! Utility functions for dynamical systems analysis
2//!
3//! This module contains helper functions and utilities used across
4//! the analysis modules.
5
6use crate::error::{IntegrateError, IntegrateResult};
7use scirs2_core::ndarray::{Array1, Array2};
8use scirs2_core::numeric::Complex64;
9
10/// Compute determinant of a square matrix using LU decomposition
11pub fn compute_determinant(matrix: &Array2<f64>) -> f64 {
12    let n = matrix.nrows();
13    if n != matrix.ncols() {
14        return 0.0; // Not square
15    }
16
17    let mut lu = matrix.clone();
18    let mut determinant = 1.0;
19
20    // LU decomposition with partial pivoting
21    for k in 0..n {
22        // Find pivot
23        let mut max_val = lu[[k, k]].abs();
24        let mut max_idx = k;
25
26        for i in (k + 1)..n {
27            if lu[[i, k]].abs() > max_val {
28                max_val = lu[[i, k]].abs();
29                max_idx = i;
30            }
31        }
32
33        // Swap rows if needed
34        if max_idx != k {
35            for j in 0..n {
36                let temp = lu[[k, j]];
37                lu[[k, j]] = lu[[max_idx, j]];
38                lu[[max_idx, j]] = temp;
39            }
40            determinant *= -1.0; // Row swap changes sign
41        }
42
43        // Check for singular matrix
44        if lu[[k, k]].abs() < 1e-14 {
45            return 0.0;
46        }
47
48        determinant *= lu[[k, k]];
49
50        // Eliminate
51        for i in (k + 1)..n {
52            let factor = lu[[i, k]] / lu[[k, k]];
53            for j in (k + 1)..n {
54                lu[[i, j]] -= factor * lu[[k, j]];
55            }
56        }
57    }
58
59    determinant
60}
61
62/// Compute trace of a matrix (sum of diagonal elements)
63pub fn compute_trace(matrix: &Array2<f64>) -> f64 {
64    let n = std::cmp::min(matrix.nrows(), matrix.ncols());
65    (0..n).map(|i| matrix[[i, i]]).sum()
66}
67
68/// Estimate the rank of a matrix using QR-like decomposition
69pub fn estimate_matrix_rank(matrix: &Array2<f64>, tolerance: f64) -> usize {
70    // Simplified rank estimation using QR decomposition
71    let (m, n) = matrix.dim();
72    let mut a = matrix.clone();
73    let mut rank = 0;
74
75    for k in 0..std::cmp::min(m, n) {
76        // Find the column with maximum norm
77        let mut max_norm = 0.0;
78        let mut max_col = k;
79
80        for j in k..n {
81            let col_norm: f64 = (k..m).map(|i| a[[i, j]].powi(2)).sum::<f64>().sqrt();
82            if col_norm > max_norm {
83                max_norm = col_norm;
84                max_col = j;
85            }
86        }
87
88        // If maximum norm is below tolerance, we've found the rank
89        if max_norm < tolerance {
90            break;
91        }
92
93        // Swap columns
94        if max_col != k {
95            for i in 0..m {
96                let temp = a[[i, k]];
97                a[[i, k]] = a[[i, max_col]];
98                a[[i, max_col]] = temp;
99            }
100        }
101
102        rank += 1;
103
104        // Normalize and orthogonalize
105        for i in k..m {
106            a[[i, k]] /= max_norm;
107        }
108
109        for j in (k + 1)..n {
110            let dot_product: f64 = (k..m).map(|i| a[[i, k]] * a[[i, j]]).sum();
111            for i in k..m {
112                a[[i, j]] -= dot_product * a[[i, k]];
113            }
114        }
115    }
116
117    rank
118}
119
120/// Solve linear system Ax = b using LU decomposition with partial pivoting
121pub fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> IntegrateResult<Array1<f64>> {
122    let n = a.nrows();
123    let mut lu = a.clone();
124    let mut x = b.clone();
125
126    // LU decomposition with partial pivoting
127    let mut pivot = Array1::<usize>::zeros(n);
128    for i in 0..n {
129        pivot[i] = i;
130    }
131
132    for k in 0..n - 1 {
133        // Find pivot
134        let mut max_val = lu[[k, k]].abs();
135        let mut max_idx = k;
136
137        for i in k + 1..n {
138            if lu[[i, k]].abs() > max_val {
139                max_val = lu[[i, k]].abs();
140                max_idx = i;
141            }
142        }
143
144        // Swap rows
145        if max_idx != k {
146            for j in 0..n {
147                let temp = lu[[k, j]];
148                lu[[k, j]] = lu[[max_idx, j]];
149                lu[[max_idx, j]] = temp;
150            }
151            pivot.swap(k, max_idx);
152        }
153
154        // Eliminate
155        for i in k + 1..n {
156            if lu[[k, k]].abs() < 1e-14 {
157                return Err(IntegrateError::ComputationError(
158                    "Matrix is singular".to_string(),
159                ));
160            }
161
162            let factor = lu[[i, k]] / lu[[k, k]];
163            lu[[i, k]] = factor;
164
165            for j in k + 1..n {
166                lu[[i, j]] -= factor * lu[[k, j]];
167            }
168        }
169    }
170
171    // Apply row swaps to RHS
172    for k in 0..n - 1 {
173        x.swap(k, pivot[k]);
174    }
175
176    // Forward substitution
177    for i in 1..n {
178        for j in 0..i {
179            x[i] -= lu[[i, j]] * x[j];
180        }
181    }
182
183    // Back substitution
184    for i in (0..n).rev() {
185        for j in i + 1..n {
186            x[i] -= lu[[i, j]] * x[j];
187        }
188        // Check for zero diagonal element
189        if lu[[i, i]].abs() < 1e-14 {
190            return Err(IntegrateError::ComputationError(
191                "Zero diagonal element in back substitution".to_string(),
192            ));
193        }
194        x[i] /= lu[[i, i]];
195    }
196
197    Ok(x)
198}
199
200/// Check if point is a bifurcation point based on eigenvalues crossing imaginary axis
201pub fn is_bifurcation_point(eigenvalues: &[Complex64]) -> bool {
202    // Check for eigenvalues crossing the imaginary axis
203    eigenvalues.iter().any(|&eig| eig.re.abs() < 1e-8)
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use approx::assert_abs_diff_eq;
210    use scirs2_core::ndarray::array;
211
212    #[test]
213    fn test_compute_determinant() {
214        // Test 2x2 matrix
215        let matrix = array![[1.0, 2.0], [3.0, 4.0]];
216        let det = compute_determinant(&matrix);
217        assert_abs_diff_eq!(det, -2.0, epsilon = 1e-10);
218
219        // Test 3x3 identity matrix
220        let identity = array![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
221        let det = compute_determinant(&identity);
222        assert_abs_diff_eq!(det, 1.0, epsilon = 1e-10);
223
224        // Test singular matrix
225        let singular = array![[1.0, 2.0], [2.0, 4.0]];
226        let det = compute_determinant(&singular);
227        assert_abs_diff_eq!(det, 0.0, epsilon = 1e-10);
228    }
229
230    #[test]
231    fn test_compute_trace() {
232        let matrix = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]];
233        let trace = compute_trace(&matrix);
234        assert_abs_diff_eq!(trace, 15.0, epsilon = 1e-10);
235    }
236
237    #[test]
238    fn test_estimate_matrix_rank() {
239        // Full rank matrix
240        let matrix = array![[1.0, 0.0], [0.0, 1.0]];
241        let rank = estimate_matrix_rank(&matrix, 1e-10);
242        assert_eq!(rank, 2);
243
244        // Rank-deficient matrix
245        let matrix = array![[1.0, 2.0], [2.0, 4.0]];
246        let rank = estimate_matrix_rank(&matrix, 1e-10);
247        assert_eq!(rank, 1);
248    }
249
250    #[test]
251    fn test_solve_linear_system() {
252        // Simple 2x2 system
253        let a = array![[2.0, 1.0], [1.0, 1.0]];
254        let b = array![3.0, 2.0];
255        let result = solve_linear_system(&a, &b).unwrap();
256        let expected = array![1.0, 1.0];
257
258        for i in 0..result.len() {
259            assert_abs_diff_eq!(result[i], expected[i], epsilon = 1e-10);
260        }
261    }
262
263    #[test]
264    fn test_is_bifurcation_point() {
265        use scirs2_core::numeric::Complex64;
266
267        // Eigenvalue on imaginary axis (bifurcation)
268        let eigenvalues = vec![Complex64::new(0.0, 1.0), Complex64::new(-1.0, 0.0)];
269        assert!(is_bifurcation_point(&eigenvalues));
270
271        // All eigenvalues away from imaginary axis (no bifurcation)
272        let eigenvalues = vec![Complex64::new(-1.0, 0.0), Complex64::new(-2.0, 0.0)];
273        assert!(!is_bifurcation_point(&eigenvalues));
274    }
275}