scirs2_integrate/ode/utils/stiffness/
mod.rs

1//! Stiffness detection utilities for ODE solvers
2//!
3//! This module provides algorithms and data structures for detecting stiffness
4//! in ODE systems and making intelligent method switching decisions.
5
6pub mod integration;
7
8use crate::IntegrateFloat;
9use scirs2_core::ndarray::{Array1, Array2};
10use std::fmt::Debug;
11use std::marker::PhantomData;
12
13/// Detection method for stiffness analysis
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum StiffnessDetectionMethod {
16    /// Basic counter-based detection (original LSODA approach)
17    Basic,
18    /// Error pattern analysis
19    ErrorPattern,
20    /// Step size pattern analysis
21    StepPattern,
22    /// Eigenvalue estimation of the Jacobian
23    EigenvalueEstimation,
24    /// Combined approach using multiple indicators
25    #[default]
26    Combined,
27}
28
29/// Configuration for stiffness detection
30#[derive(Debug, Clone)]
31pub struct StiffnessDetectionConfig<F: IntegrateFloat> {
32    /// Method used for stiffness detection
33    pub method: StiffnessDetectionMethod,
34    /// Minimum number of steps before considering method switch
35    pub min_steps_before_switch: usize,
36    /// Minimum number of indicators needed to detect stiffness
37    pub stiffness_threshold: usize,
38    /// Minimum number of indicators needed to detect non-stiffness
39    pub non_stiffness_threshold: usize,
40    /// Window size for analyzing error and step size patterns
41    pub analysis_window: usize,
42    /// Threshold for step size ratio indicating stiffness
43    pub step_size_ratio_threshold: F,
44    /// Threshold for error ratio indicating stiffness
45    pub error_ratio_threshold: F,
46    /// Whether to use eigenvalue estimation (more expensive)
47    pub use_eigenvalue_estimation: bool,
48    /// Minimum period between eigenvalue estimations
49    pub eigenvalue_est_period: usize,
50    /// Weight factors for different indicators
51    pub indicator_weights: IndicatorWeights<F>,
52    /// Phantom marker for the float type
53    _phantom: PhantomData<F>,
54}
55
56impl<F: IntegrateFloat> Default for StiffnessDetectionConfig<F> {
57    fn default() -> Self {
58        StiffnessDetectionConfig {
59            method: StiffnessDetectionMethod::default(),
60            min_steps_before_switch: 5,
61            stiffness_threshold: 3,
62            non_stiffness_threshold: 5,
63            analysis_window: 10,
64            step_size_ratio_threshold: F::from_f64(0.1).unwrap(),
65            error_ratio_threshold: F::from_f64(10.0).unwrap(),
66            use_eigenvalue_estimation: false,
67            eigenvalue_est_period: 25,
68            indicator_weights: IndicatorWeights::default(),
69            _phantom: PhantomData,
70        }
71    }
72}
73
74/// Weight factors for different stiffness indicators
75#[derive(Debug, Clone)]
76pub struct IndicatorWeights<F: IntegrateFloat> {
77    /// Weight for error pattern indicators
78    pub error_pattern_weight: F,
79    /// Weight for step size pattern indicators
80    pub step_pattern_weight: F,
81    /// Weight for Newton convergence indicators
82    pub newton_convergence_weight: F,
83    /// Weight for eigenvalue estimation indicators
84    pub eigenvalue_weight: F,
85}
86
87impl<F: IntegrateFloat> Default for IndicatorWeights<F> {
88    fn default() -> Self {
89        IndicatorWeights {
90            error_pattern_weight: F::from_f64(1.0).unwrap(),
91            step_pattern_weight: F::from_f64(1.0).unwrap(),
92            newton_convergence_weight: F::from_f64(1.5).unwrap(),
93            eigenvalue_weight: F::from_f64(2.0).unwrap(),
94        }
95    }
96}
97
98/// Enhanced stiffness detector that uses multiple indicators
99#[derive(Debug, Clone)]
100pub struct StiffnessDetector<F: IntegrateFloat> {
101    /// Configuration for stiffness detection
102    config: StiffnessDetectionConfig<F>,
103    /// History of step sizes
104    step_size_history: Vec<F>,
105    /// History of error estimates
106    error_history: Vec<F>,
107    /// History of Newton iterations
108    newton_iter_history: Vec<usize>,
109    /// History of rejected steps
110    rejected_step_history: Vec<bool>,
111    /// Estimated stiffness ratio
112    stiffness_ratio: F,
113    /// Stiffness counter (number of indicators suggesting stiff problem)
114    stiffness_indicators: usize,
115    /// Non-stiffness counter (number of indicators suggesting non-stiff problem)
116    non_stiffness_indicators: usize,
117    /// Last time eigenvalues were estimated
118    last_eigenvalue_est: usize,
119    /// Current stiffness score (-1.0 to 1.0, where positive means stiff)
120    stiffness_score: F,
121}
122
123impl<F: IntegrateFloat> Default for StiffnessDetector<F> {
124    fn default() -> Self {
125        Self::new()
126    }
127}
128
129impl<F: IntegrateFloat> StiffnessDetector<F> {
130    /// Create a new stiffness detector with default configuration
131    pub fn new() -> Self {
132        Self::with_config(StiffnessDetectionConfig::default())
133    }
134
135    /// Create a new stiffness detector with specific configuration
136    pub fn with_config(config: StiffnessDetectionConfig<F>) -> Self {
137        StiffnessDetector {
138            config,
139            step_size_history: Vec::with_capacity(20),
140            error_history: Vec::with_capacity(20),
141            newton_iter_history: Vec::with_capacity(20),
142            rejected_step_history: Vec::with_capacity(20),
143            stiffness_ratio: F::zero(),
144            stiffness_indicators: 0,
145            non_stiffness_indicators: 0,
146            last_eigenvalue_est: 0,
147            stiffness_score: F::zero(),
148        }
149    }
150
151    /// Record a step for stiffness analysis
152    pub fn record_step(
153        &mut self,
154        step_size: F,
155        error: F,
156        newton_iterations: usize,
157        rejected: bool,
158        steps_taken: usize,
159    ) {
160        // Add to history
161        self.step_size_history.push(step_size);
162        self.error_history.push(error);
163        self.newton_iter_history.push(newton_iterations);
164        self.rejected_step_history.push(rejected);
165
166        // Keep history limited to window _size
167        let window = self.config.analysis_window;
168        if self.step_size_history.len() > window {
169            self.step_size_history.remove(0);
170            self.error_history.remove(0);
171            self.newton_iter_history.remove(0);
172            self.rejected_step_history.remove(0);
173        }
174
175        // Analyze patterns based on configuration
176        match self.config.method {
177            StiffnessDetectionMethod::Basic => self.analyze_basic(),
178            StiffnessDetectionMethod::ErrorPattern => self.analyze_error_pattern(),
179            StiffnessDetectionMethod::StepPattern => self.analyze_step_pattern(),
180            StiffnessDetectionMethod::EigenvalueEstimation => {
181                // Eigenvalue estimation would be done less frequently
182                if steps_taken - self.last_eigenvalue_est >= self.config.eigenvalue_est_period {
183                    // This would need the Jacobian, which we don't have here
184                    // We'll just use a placeholder until integration with LSODA
185                    self.last_eigenvalue_est = steps_taken;
186                }
187            }
188            StiffnessDetectionMethod::Combined => {
189                self.analyze_basic();
190                self.analyze_error_pattern();
191                self.analyze_step_pattern();
192
193                if self.config.use_eigenvalue_estimation
194                    && steps_taken - self.last_eigenvalue_est >= self.config.eigenvalue_est_period
195                {
196                    // Eigenvalue estimation would go here
197                    self.last_eigenvalue_est = steps_taken;
198                }
199
200                // Update combined stiffness score
201                self.update_stiffness_score();
202            }
203        }
204    }
205
206    /// Basic analysis looking at the most recent step
207    fn analyze_basic(&mut self) {
208        if self.step_size_history.is_empty() {
209            return;
210        }
211
212        let last_idx = self.step_size_history.len() - 1;
213
214        // Check for very small errors (indicating non-stiffness)
215        if self.error_history[last_idx] < F::from_f64(0.01).unwrap() {
216            self.non_stiffness_indicators += 1;
217        }
218
219        // Check for large errors (indicating stiffness)
220        if self.error_history[last_idx] > self.config.error_ratio_threshold {
221            self.stiffness_indicators += 1;
222        }
223
224        // Check Newton iterations (few iterations indicate non-stiffness)
225        if self.newton_iter_history[last_idx] <= 2 {
226            self.non_stiffness_indicators += 1;
227        }
228
229        // Check Newton iterations (many iterations indicate stiffness)
230        if self.newton_iter_history[last_idx] >= 8 {
231            self.stiffness_indicators += 1;
232        }
233
234        // Check rejected steps (frequent rejections may indicate stiffness)
235        if self.rejected_step_history[last_idx] {
236            self.stiffness_indicators += 1;
237        }
238    }
239
240    /// Analyze error patterns for stiffness indicators
241    fn analyze_error_pattern(&mut self) {
242        if self.error_history.len() < 3 {
243            return;
244        }
245
246        // Look for oscillating error patterns (can indicate stiffness)
247        let mut oscillating = true;
248        for i in 2..self.error_history.len() {
249            if (self.error_history[i] > self.error_history[i - 1]
250                && self.error_history[i - 1] > self.error_history[i - 2])
251                || (self.error_history[i] < self.error_history[i - 1]
252                    && self.error_history[i - 1] < self.error_history[i - 2])
253            {
254                oscillating = false;
255                break;
256            }
257        }
258
259        if oscillating {
260            self.stiffness_indicators += 1;
261        }
262
263        // Check for consistent decrease in error (indicates non-stiffness)
264        let mut decreasing = true;
265        for i in 1..self.error_history.len() {
266            if self.error_history[i] > self.error_history[i - 1] {
267                decreasing = false;
268                break;
269            }
270        }
271
272        if decreasing && self.error_history.len() >= 3 {
273            self.non_stiffness_indicators += 1;
274        }
275    }
276
277    /// Analyze step size patterns for stiffness indicators
278    fn analyze_step_pattern(&mut self) {
279        if self.step_size_history.len() < 3 {
280            return;
281        }
282
283        // Count step size decreases
284        let mut decreases = 0;
285        for i in 1..self.step_size_history.len() {
286            if self.step_size_history[i] < self.step_size_history[i - 1] {
287                decreases += 1;
288            }
289        }
290
291        // High proportion of decreases might indicate stiffness
292        let decrease_ratio = F::from_usize(decreases).unwrap()
293            / F::from_usize(self.step_size_history.len() - 1).unwrap();
294
295        if decrease_ratio > F::from_f64(0.7).unwrap() {
296            self.stiffness_indicators += 1;
297        }
298
299        // Consistent growth in step size indicates non-stiffness
300        let mut increasing = true;
301        for i in 1..self.step_size_history.len() {
302            if self.step_size_history[i] < self.step_size_history[i - 1] {
303                increasing = false;
304                break;
305            }
306        }
307
308        if increasing && self.step_size_history.len() >= 3 {
309            self.non_stiffness_indicators += 1;
310        }
311    }
312
313    /// Update the combined stiffness score based on all indicators
314    fn update_stiffness_score(&mut self) {
315        let weights = &self.config.indicator_weights;
316
317        // Calculate weighted stiffness score
318        let stiff_score = F::from_usize(self.stiffness_indicators).unwrap()
319            * (weights.error_pattern_weight
320                + weights.step_pattern_weight
321                + weights.newton_convergence_weight);
322
323        let non_stiff_score = F::from_usize(self.non_stiffness_indicators).unwrap()
324            * (weights.error_pattern_weight
325                + weights.step_pattern_weight
326                + weights.newton_convergence_weight);
327
328        // Normalize to [-1, 1] range where positive is stiff
329        if stiff_score > F::zero() || non_stiff_score > F::zero() {
330            self.stiffness_score =
331                (stiff_score - non_stiff_score) / (stiff_score + non_stiff_score).max(F::one());
332        } else {
333            self.stiffness_score = F::zero();
334        }
335    }
336
337    /// Check if the problem is stiff based on collected indicators
338    pub fn is_stiff(&self, _current_method_is_stiff: bool, steps_sinceswitch: usize) -> bool {
339        // Don't _switch methods too frequently
340        if steps_sinceswitch < self.config.min_steps_before_switch {
341            return _current_method_is_stiff;
342        }
343
344        // For advanced methods, use the stiffness score
345        if self.config.method == StiffnessDetectionMethod::Combined {
346            if _current_method_is_stiff {
347                // We're using BDF (_stiff), consider switching to Adams (non-_stiff)
348                // Higher threshold to _switch away from _stiff method
349                return self.stiffness_score > F::from_f64(-0.3).unwrap();
350            } else {
351                // We're using Adams (non-_stiff), consider switching to BDF (_stiff)
352                // Lower threshold to _switch to _stiff method
353                return self.stiffness_score > F::from_f64(0.2).unwrap();
354            }
355        }
356
357        // For basic method, use simple counters
358        if _current_method_is_stiff {
359            // Currently using BDF, check if we should _switch to Adams
360            self.non_stiffness_indicators >= self.config.non_stiffness_threshold
361        } else {
362            // Currently using Adams, check if we should _switch to BDF
363            self.stiffness_indicators >= self.config.stiffness_threshold
364        }
365    }
366
367    /// Reset indicators after method switch
368    pub fn reset_after_switch(&mut self) {
369        self.stiffness_indicators = 0;
370        self.non_stiffness_indicators = 0;
371        self.stiffness_score = F::zero();
372        // Retain history but reset counters
373    }
374
375    /// Get current stiffness score (-1.0 to 1.0, where positive means stiff)
376    pub fn stiffness_score(&self) -> F {
377        self.stiffness_score
378    }
379
380    /// Estimate stiffness ratio using eigenvalues of the Jacobian
381    pub fn estimate_stiffness_from_jacobian(&mut self, jacobian: &Array2<F>) -> F {
382        // Estimate the largest and smallest eigenvalues to compute stiffness ratio
383        // For efficiency, we use power iteration methods rather than full eigenvalue decomposition
384
385        let n = jacobian.nrows();
386        if n == 0 {
387            return F::one();
388        }
389
390        // Estimate largest eigenvalue magnitude using power iteration
391        let max_eigenval_magnitude = self.estimate_largest_eigenvalue_magnitude(jacobian);
392
393        // Estimate smallest eigenvalue magnitude using inverse power iteration
394        let min_eigenval_magnitude = self.estimate_smallest_eigenvalue_magnitude(jacobian);
395
396        // Compute stiffness ratio: ratio of largest to smallest eigenvalue magnitudes
397        let stiffness_ratio = if min_eigenval_magnitude > F::from_f64(1e-14).unwrap() {
398            max_eigenval_magnitude / min_eigenval_magnitude
399        } else {
400            // If minimum eigenvalue is too small, treat as very stiff
401            F::from_f64(1e6).unwrap()
402        };
403
404        self.stiffness_ratio = stiffness_ratio;
405
406        // Update stiffness indicators based on the computed ratio
407        if stiffness_ratio > F::from_f64(100.0).unwrap() {
408            self.stiffness_indicators += 1;
409        } else if stiffness_ratio < F::from_f64(10.0).unwrap() {
410            self.non_stiffness_indicators += 1;
411        }
412
413        stiffness_ratio
414    }
415
416    /// Estimate the largest eigenvalue magnitude using power iteration
417    fn estimate_largest_eigenvalue_magnitude(&self, jacobian: &Array2<F>) -> F {
418        let n = jacobian.nrows();
419        let max_iterations = 20;
420        let tolerance = F::from_f64(1e-6).unwrap();
421
422        // Initialize with random vector
423        let mut v = Array1::<F>::from_elem(n, F::one());
424
425        // Normalize
426        let mut norm = (v.dot(&v)).sqrt();
427        if norm > F::from_f64(1e-14).unwrap() {
428            v = &v / norm;
429        }
430
431        let mut eigenvalue = F::zero();
432
433        for _ in 0..max_iterations {
434            // Multiply by matrix: v_new = A * v
435            let mut v_new = Array1::<F>::zeros(n);
436            for i in 0..n {
437                for j in 0..n {
438                    v_new[i] += jacobian[[i, j]] * v[j];
439                }
440            }
441
442            // Compute Rayleigh quotient: eigenvalue = v^T * A * v / v^T * v
443            let new_eigenvalue = v.dot(&v_new);
444
445            // Normalize v_new
446            norm = (v_new.dot(&v_new)).sqrt();
447            if norm > F::from_f64(1e-14).unwrap() {
448                v_new = &v_new / norm;
449            }
450
451            // Check convergence
452            if (new_eigenvalue - eigenvalue).abs() < tolerance {
453                eigenvalue = new_eigenvalue;
454                break;
455            }
456
457            eigenvalue = new_eigenvalue;
458            v = v_new;
459        }
460
461        eigenvalue.abs()
462    }
463
464    /// Estimate the smallest eigenvalue magnitude using inverse power iteration
465    fn estimate_smallest_eigenvalue_magnitude(&self, jacobian: &Array2<F>) -> F {
466        let n = jacobian.nrows();
467        let max_iterations = 20;
468        let tolerance = F::from_f64(1e-6).unwrap();
469
470        // For smallest eigenvalue, we use inverse iteration
471        // We need to solve (A - sigma*I) * v_new = v where sigma is a shift
472        // For simplicity, we use sigma = 0 (pure inverse iteration)
473
474        // Create A matrix for LU decomposition (we'll approximate with A itself)
475        let mut a_copy = jacobian.clone();
476
477        // Add small diagonal regularization to avoid singularity
478        let regularization = F::from_f64(1e-12).unwrap();
479        for i in 0..n {
480            a_copy[[i, i]] += regularization;
481        }
482
483        // Initialize with random vector
484        let mut v = Array1::<F>::from_elem(n, F::one());
485
486        // Normalize
487        let mut norm = (v.dot(&v)).sqrt();
488        if norm > F::from_f64(1e-14).unwrap() {
489            v = &v / norm;
490        }
491
492        let mut eigenvalue = F::zero();
493
494        for _ in 0..max_iterations {
495            // Solve A * v_new = v (inverse iteration step)
496            // For simplicity, we approximate this with a few Richardson iterations
497            let mut v_new = v.clone();
498            let damping = F::from_f64(0.1).unwrap();
499
500            // Richardson iteration: v_new = v_new - damping * (A * v_new - v)
501            for _ in 0..5 {
502                let mut av = Array1::<F>::zeros(n);
503                for i in 0..n {
504                    for j in 0..n {
505                        av[i] += a_copy[[i, j]] * v_new[j];
506                    }
507                }
508
509                for i in 0..n {
510                    v_new[i] -= damping * (av[i] - v[i]);
511                }
512            }
513
514            // Compute Rayleigh quotient
515            let mut av_new = Array1::<F>::zeros(n);
516            for i in 0..n {
517                for j in 0..n {
518                    av_new[i] += jacobian[[i, j]] * v_new[j];
519                }
520            }
521
522            let new_eigenvalue = v_new.dot(&av_new) / v_new.dot(&v_new);
523
524            // Normalize v_new
525            norm = (v_new.dot(&v_new)).sqrt();
526            if norm > F::from_f64(1e-14).unwrap() {
527                v_new = &v_new / norm;
528            }
529
530            // Check convergence
531            if (new_eigenvalue - eigenvalue).abs() < tolerance {
532                eigenvalue = new_eigenvalue;
533                break;
534            }
535
536            eigenvalue = new_eigenvalue;
537            v = v_new;
538        }
539
540        eigenvalue.abs()
541    }
542}
543
544/// Provides information about method switching for diagnostic purposes
545#[derive(Debug, Clone)]
546pub struct MethodSwitchInfo<F: IntegrateFloat> {
547    /// Number of switches from non-stiff to stiff method
548    pub nonstiff_to_stiff_switches: usize,
549    /// Number of switches from stiff to non-stiff method
550    pub stiff_to_nonstiff_switches: usize,
551    /// Stiffness score at each switch point (-1.0 to 1.0)
552    pub stiffness_scores: Vec<F>,
553    /// Step at which each switch occurred
554    pub switch_steps: Vec<usize>,
555    /// Reason for each switch
556    pub switch_reasons: Vec<String>,
557}
558
559impl<F: IntegrateFloat> Default for MethodSwitchInfo<F> {
560    fn default() -> Self {
561        Self::new()
562    }
563}
564
565impl<F: IntegrateFloat> MethodSwitchInfo<F> {
566    /// Create a new method switch info tracker
567    pub fn new() -> Self {
568        MethodSwitchInfo {
569            nonstiff_to_stiff_switches: 0,
570            stiff_to_nonstiff_switches: 0,
571            stiffness_scores: Vec::new(),
572            switch_steps: Vec::new(),
573            switch_reasons: Vec::new(),
574        }
575    }
576
577    /// Record a method switch
578    pub fn record_switch(
579        &mut self,
580        from_stiff: bool,
581        step: usize,
582        stiffness_score: F,
583        reason: &str,
584    ) {
585        if from_stiff {
586            self.stiff_to_nonstiff_switches += 1;
587        } else {
588            self.nonstiff_to_stiff_switches += 1;
589        }
590
591        self.stiffness_scores.push(stiffness_score);
592        self.switch_steps.push(step);
593        self.switch_reasons.push(reason.to_string());
594    }
595
596    /// Get a summary of method switching
597    pub fn summary(&self) -> String {
598        format!(
599            "Method switching summary: {} non-stiff to stiff, {} stiff to non-stiff",
600            self.nonstiff_to_stiff_switches, self.stiff_to_nonstiff_switches
601        )
602    }
603}