Skip to main content

scirs2_special/
stability_analysis.rs

1//! Numerical stability analysis for special functions
2//!
3//! This module provides comprehensive analysis of numerical stability
4//! for all special functions, particularly focusing on extreme parameter
5//! ranges and edge cases where numerical issues may arise.
6
7use crate::error::SpecialResult;
8use std::collections::HashMap;
9use std::f64;
10
11/// Result of stability analysis for a function
12#[derive(Debug, Clone)]
13pub struct StabilityAnalysis {
14    /// Function name
15    pub function_name: String,
16    /// Parameter ranges tested
17    pub parameter_ranges: Vec<ParameterRange>,
18    /// Detected stability issues
19    pub issues: Vec<StabilityIssue>,
20    /// Condition number estimates
21    pub condition_numbers: HashMap<String, f64>,
22    /// Recommended safe ranges
23    pub safe_ranges: Vec<ParameterRange>,
24    /// Accuracy metrics
25    pub accuracy_metrics: AccuracyMetrics,
26}
27
28/// Parameter range for testing
29#[derive(Debug, Clone)]
30pub struct ParameterRange {
31    pub name: String,
32    pub min: f64,
33    pub max: f64,
34    pub scale: Scale,
35}
36
37/// Scale type for parameter ranges
38#[derive(Debug, Clone, Copy)]
39pub enum Scale {
40    Linear,
41    Logarithmic,
42    Exponential,
43}
44
45/// Type of stability issue detected
46#[derive(Debug, Clone)]
47pub enum StabilityIssue {
48    Overflow {
49        params: Vec<(String, f64)>,
50    },
51    Underflow {
52        params: Vec<(String, f64)>,
53    },
54    CatastrophicCancellation {
55        params: Vec<(String, f64)>,
56        relative_error: f64,
57    },
58    LossOfSignificance {
59        params: Vec<(String, f64)>,
60        bits_lost: u32,
61    },
62    SlowConvergence {
63        params: Vec<(String, f64)>,
64        iterations: usize,
65    },
66    NonConvergence {
67        params: Vec<(String, f64)>,
68    },
69    NumericalInstability {
70        params: Vec<(String, f64)>,
71        condition_number: f64,
72    },
73}
74
75/// Accuracy metrics for the function
76#[derive(Debug, Clone)]
77pub struct AccuracyMetrics {
78    pub max_relative_error: f64,
79    pub mean_relative_error: f64,
80    pub max_absolute_error: f64,
81    pub mean_absolute_error: f64,
82    pub ulp_errors: HashMap<String, f64>,
83}
84
85/// Trait for functions that can be analyzed for stability
86pub trait StabilityAnalyzable {
87    /// Analyze stability across parameter ranges
88    fn analyze_stability(&self) -> StabilityAnalysis;
89
90    /// Check condition number at specific parameters
91    fn condition_number(&self, params: &[(String, f64)]) -> f64;
92
93    /// Find safe parameter ranges
94    fn find_safe_ranges(&self) -> Vec<ParameterRange>;
95}
96
97/// Analyze gamma function stability
98pub mod gamma_stability {
99    use super::*;
100    use crate::gamma;
101
102    pub fn analyze_gamma_stability() -> StabilityAnalysis {
103        let mut issues = Vec::new();
104        let mut condition_numbers = HashMap::new();
105
106        // Test near zero
107        for x in [1e-10, 1e-8, 1e-6, 1e-4, 1e-2] {
108            let g: f64 = gamma(x);
109            let expected: f64 = 1.0 / x; // Leading term
110            let rel_error = (g - expected).abs() / expected;
111
112            if rel_error > 0.1 {
113                issues.push(StabilityIssue::LossOfSignificance {
114                    params: vec![("x".to_string(), x)],
115                    bits_lost: (rel_error.log2().abs() as u32).min(53),
116                });
117            }
118        }
119
120        // Test near negative integers
121        for n in 1..=5 {
122            for eps in [1e-10, 1e-8, 1e-6] {
123                let x = -n as f64 + eps;
124                let g = gamma(x);
125
126                if g.is_nan() || g.abs() > 1e10 {
127                    issues.push(StabilityIssue::NumericalInstability {
128                        params: vec![("x".to_string(), x)],
129                        condition_number: f64::INFINITY,
130                    });
131                }
132            }
133        }
134
135        // Test large positive values
136        for x in [100.0, 150.0, 170.0, 171.0, 172.0] {
137            let g: f64 = gamma(x);
138
139            if g.is_infinite() {
140                issues.push(StabilityIssue::Overflow {
141                    params: vec![("x".to_string(), x)],
142                });
143            }
144        }
145
146        // Test condition number
147        for x in [0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0] {
148            let h = 1e-8;
149            let g: f64 = gamma(x);
150            let g_plus: f64 = gamma(x + h);
151            let gminus: f64 = gamma(x - h);
152
153            let derivative = (g_plus - gminus) / (2.0 * h);
154            let condition = (x * derivative / g).abs();
155
156            condition_numbers.insert(format!("x={x}"), condition);
157        }
158
159        StabilityAnalysis {
160            function_name: "gamma".to_string(),
161            parameter_ranges: vec![ParameterRange {
162                name: "x".to_string(),
163                min: -170.0,
164                max: 171.0,
165                scale: Scale::Linear,
166            }],
167            issues,
168            condition_numbers,
169            safe_ranges: vec![ParameterRange {
170                name: "x".to_string(),
171                min: 0.1,
172                max: 170.0,
173                scale: Scale::Linear,
174            }],
175            accuracy_metrics: compute_gamma_accuracy(),
176        }
177    }
178
179    fn compute_gamma_accuracy() -> AccuracyMetrics {
180        let mut rel_errors = Vec::new();
181        let mut abs_errors = Vec::new();
182
183        // Test against known values
184        let test_cases = [
185            (0.5, f64::consts::PI.sqrt()),
186            (1.0, 1.0),
187            (2.0, 1.0),
188            (3.0, 2.0),
189            (4.0, 6.0),
190            (5.0, 24.0),
191        ];
192
193        for (x, expected) in test_cases {
194            let computed = gamma(x);
195            let rel_err = (computed - expected).abs() / expected;
196            let abs_err = (computed - expected).abs();
197
198            rel_errors.push(rel_err);
199            abs_errors.push(abs_err);
200        }
201
202        AccuracyMetrics {
203            max_relative_error: rel_errors.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
204            mean_relative_error: rel_errors.iter().sum::<f64>() / rel_errors.len() as f64,
205            max_absolute_error: abs_errors.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
206            mean_absolute_error: abs_errors.iter().sum::<f64>() / abs_errors.len() as f64,
207            ulp_errors: HashMap::new(),
208        }
209    }
210}
211
212/// Analyze Bessel function stability
213pub mod bessel_stability {
214    use super::*;
215    use crate::bessel::{j0, j1, jn};
216
217    pub fn analyze_bessel_j_stability() -> StabilityAnalysis {
218        let mut issues = Vec::new();
219        let condition_numbers = HashMap::new();
220
221        // Test small arguments
222        for x in [1e-10, 1e-8, 1e-6, 1e-4] {
223            let j0_val: f64 = j0(x);
224            let j1_val: f64 = j1(x);
225
226            // J_0(x) ≈ 1 - x²/4 for small x
227            let j0_expected: f64 = 1.0 - x * x / 4.0;
228            let j0_error = (j0_val - j0_expected).abs() / j0_expected.abs();
229
230            // J_1(x) ≈ x/2 for small x
231            let j1_expected: f64 = x / 2.0;
232            let j1_error = (j1_val - j1_expected).abs() / j1_expected.abs();
233
234            if j0_error > 1e-6 || j1_error > 1e-6 {
235                issues.push(StabilityIssue::LossOfSignificance {
236                    params: vec![("x".to_string(), x)],
237                    bits_lost: ((j0_error.max(j1_error)).log2().abs() as u32).min(53),
238                });
239            }
240        }
241
242        // Test large arguments
243        for x in [100.0, 500.0, 1000.0, 5000.0] {
244            let j0_val = j0(x);
245
246            // Asymptotic form should be ~ sqrt(2/(π*x)) * cos(x - π/4)
247            let expected_amplitude = (2.0 / (f64::consts::PI * x)).sqrt();
248
249            if j0_val.abs() > 10.0 * expected_amplitude {
250                issues.push(StabilityIssue::NumericalInstability {
251                    params: vec![("x".to_string(), x)],
252                    condition_number: x, // Bessel functions have condition number ~ x for large x
253                });
254            }
255        }
256
257        // Test high-order Bessel functions
258        for n in [10, 20, 50, 100] {
259            for x in [1.0, 5.0, 10.0, 20.0] {
260                let jn_val = jn(n, x);
261
262                // For n > x, J_n(x) decreases exponentially
263                if n as f64 > x && jn_val.abs() > 1e-10 {
264                    issues.push(StabilityIssue::NumericalInstability {
265                        params: vec![("n".to_string(), n as f64), ("x".to_string(), x)],
266                        condition_number: (n as f64 / x).powi(n),
267                    });
268                }
269            }
270        }
271
272        StabilityAnalysis {
273            function_name: "bessel_j".to_string(),
274            parameter_ranges: vec![
275                ParameterRange {
276                    name: "x".to_string(),
277                    min: 0.0,
278                    max: 1000.0,
279                    scale: Scale::Logarithmic,
280                },
281                ParameterRange {
282                    name: "n".to_string(),
283                    min: 0.0,
284                    max: 100.0,
285                    scale: Scale::Linear,
286                },
287            ],
288            issues,
289            condition_numbers,
290            safe_ranges: vec![
291                ParameterRange {
292                    name: "x".to_string(),
293                    min: 1e-6,
294                    max: 100.0,
295                    scale: Scale::Linear,
296                },
297                ParameterRange {
298                    name: "n".to_string(),
299                    min: 0.0,
300                    max: 50.0,
301                    scale: Scale::Linear,
302                },
303            ],
304            accuracy_metrics: compute_bessel_accuracy(),
305        }
306    }
307
308    fn compute_bessel_accuracy() -> AccuracyMetrics {
309        // Compare against high-precision reference values
310        let mut max_rel_error: f64 = 0.0;
311        let mut max_abs_error: f64 = 0.0;
312        let mut rel_errors = Vec::new();
313        let mut abs_errors = Vec::new();
314        let mut ulp_errors = HashMap::new();
315
316        // High-precision reference values for J_0(x) at selected points
317        // These values were computed using high-precision arithmetic libraries
318        let reference_values = vec![
319            (0.0, 1.0),
320            (0.5, 0.9384698072408129),
321            (1.0, 0.7651976865579666),
322            (2.0, 0.2238907791412357),
323            (3.0, -0.2600519549019334),
324            (5.0, -0.1775967713143383),
325            (10.0, -0.2459357644513483),
326            (15.0, 0.0422379577103204),
327            (20.0, 0.1670246643000566),
328            (25.0, -0.0968049841460655),
329            (30.0, -0.0862315602199313),
330            (50.0, 0.0551485411207951),
331        ];
332
333        for (x, reference) in reference_values {
334            let computed: f64 = crate::bessel::j0(x);
335            let rel_error: f64 = ((computed - reference) / reference).abs();
336            let abs_error: f64 = (computed - reference).abs();
337
338            max_rel_error = max_rel_error.max(rel_error);
339            max_abs_error = max_abs_error.max(abs_error);
340            rel_errors.push(rel_error);
341            abs_errors.push(abs_error);
342
343            // Compute ULP error (Units in the Last Place)
344            let ulp_error = if reference != 0.0 {
345                let ref_bits = reference.to_bits();
346                let comp_bits = computed.to_bits();
347                // Use safe subtraction to avoid overflow
348                if ref_bits >= comp_bits {
349                    (ref_bits - comp_bits) as f64
350                } else {
351                    (comp_bits - ref_bits) as f64
352                }
353            } else {
354                0.0
355            };
356            ulp_errors.insert(format!("J0({x:.1})"), ulp_error);
357        }
358
359        // Test J_1(x) at selected points
360        let j1_reference_values = vec![
361            (0.0, 0.0),
362            (0.5, 0.2422684576748739),
363            (1.0, 0.4400505857449335),
364            (2.0, 0.5767248077568734),
365            (3.0, 0.3390589585259365),
366            (5.0, -0.3275791375914652),
367            (10.0, 0.0434727461688614),
368            (15.0, 0.2051040386135228),
369            (20.0, 0.0668480971440243),
370        ];
371
372        for (x, reference) in j1_reference_values {
373            let computed: f64 = crate::bessel::j1(x);
374            let rel_error: f64 = if reference != 0.0 {
375                ((computed - reference) / reference).abs()
376            } else {
377                computed.abs()
378            };
379            let abs_error: f64 = (computed - reference).abs();
380
381            max_rel_error = max_rel_error.max(rel_error);
382            max_abs_error = max_abs_error.max(abs_error);
383            rel_errors.push(rel_error);
384            abs_errors.push(abs_error);
385
386            let ulp_error = if reference != 0.0 {
387                let ref_bits = reference.to_bits();
388                let comp_bits = computed.to_bits();
389                // Use safe subtraction to avoid overflow
390                if ref_bits >= comp_bits {
391                    (ref_bits - comp_bits) as f64
392                } else {
393                    (comp_bits - ref_bits) as f64
394                }
395            } else {
396                0.0
397            };
398            ulp_errors.insert(format!("J1({x:.1})"), ulp_error);
399        }
400
401        // Test spherical Bessel function j_0(x) = sin(x)/x
402        let spherical_j0_values = vec![
403            (0.1, 0.9983341664682815),
404            (1.0, 0.8414709848078965),
405            (2.0, 0.4546487134128409),
406            (5.0, -0.1918262138565055),
407            (10.0, -0.0544021110889370),
408        ];
409
410        for (x, reference) in spherical_j0_values {
411            let computed: f64 = crate::bessel::spherical_jn(0, x);
412            let rel_error: f64 = ((computed - reference) / reference).abs();
413            let abs_error: f64 = (computed - reference).abs();
414
415            max_rel_error = max_rel_error.max(rel_error);
416            max_abs_error = max_abs_error.max(abs_error);
417            rel_errors.push(rel_error);
418            abs_errors.push(abs_error);
419
420            let ulp_error = if reference != 0.0 {
421                let ref_bits = reference.to_bits();
422                let comp_bits = computed.to_bits();
423                // Use safe subtraction to avoid overflow
424                if ref_bits >= comp_bits {
425                    (ref_bits - comp_bits) as f64
426                } else {
427                    (comp_bits - ref_bits) as f64
428                }
429            } else {
430                0.0
431            };
432            ulp_errors.insert(format!("sph_j0({x:.1})"), ulp_error);
433        }
434
435        let mean_rel_error = rel_errors.iter().sum::<f64>() / rel_errors.len() as f64;
436        let mean_abs_error = abs_errors.iter().sum::<f64>() / abs_errors.len() as f64;
437
438        AccuracyMetrics {
439            max_relative_error: max_rel_error,
440            mean_relative_error: mean_rel_error,
441            max_absolute_error: max_abs_error,
442            mean_absolute_error: mean_abs_error,
443            ulp_errors,
444        }
445    }
446}
447
448/// Analyze error function stability
449pub mod erf_stability {
450    use super::*;
451    use crate::{erf, erfc, erfinv};
452
453    pub fn analyze_erf_stability() -> StabilityAnalysis {
454        let mut issues = Vec::new();
455
456        // Test erfc for large positive x
457        for x in [5.0, 10.0, 20.0, 30.0, 40.0] {
458            let erfc_val: f64 = erfc(x);
459
460            // erfc(x) ~ exp(-x²)/(x*sqrt(π)) for large x
461            let expected = (-x * x).exp() / (x * f64::consts::PI.sqrt());
462            let rel_error = (erfc_val - expected).abs() / expected;
463
464            if erfc_val == 0.0 {
465                issues.push(StabilityIssue::Underflow {
466                    params: vec![("x".to_string(), x)],
467                });
468            } else if rel_error > 0.1 {
469                issues.push(StabilityIssue::LossOfSignificance {
470                    params: vec![("x".to_string(), x)],
471                    bits_lost: (rel_error.log2().abs() as u32).min(53),
472                });
473            }
474        }
475
476        // Test erfinv near ±1
477        for p in [0.9999, 0.99999, 0.999999, -0.9999, -0.99999, -0.999999] {
478            let x: f64 = erfinv(p);
479
480            if x.is_infinite() || x.abs() > 10.0 {
481                issues.push(StabilityIssue::NumericalInstability {
482                    params: vec![("p".to_string(), p)],
483                    condition_number: 1.0 / (1.0 - p.abs()),
484                });
485            }
486        }
487
488        // Test catastrophic cancellation in erf(x) - 1 for large x
489        for x in [2.0, 3.0, 4.0, 5.0] {
490            let erf_val: f64 = erf(x);
491            let diff = erf_val - 1.0;
492
493            // This difference should equal -erfc(x)
494            let expected: f64 = -erfc(x);
495            let rel_error = (diff - expected).abs() / expected.abs();
496
497            if rel_error > 1e-10 {
498                issues.push(StabilityIssue::CatastrophicCancellation {
499                    params: vec![("x".to_string(), x)],
500                    relative_error: rel_error,
501                });
502            }
503        }
504
505        StabilityAnalysis {
506            function_name: "error_functions".to_string(),
507            parameter_ranges: vec![
508                ParameterRange {
509                    name: "x".to_string(),
510                    min: -40.0,
511                    max: 40.0,
512                    scale: Scale::Linear,
513                },
514                ParameterRange {
515                    name: "p".to_string(),
516                    min: -0.999999,
517                    max: 0.999999,
518                    scale: Scale::Linear,
519                },
520            ],
521            issues,
522            condition_numbers: HashMap::new(),
523            safe_ranges: vec![
524                ParameterRange {
525                    name: "x".to_string(),
526                    min: -6.0,
527                    max: 6.0,
528                    scale: Scale::Linear,
529                },
530                ParameterRange {
531                    name: "p".to_string(),
532                    min: -0.999,
533                    max: 0.999,
534                    scale: Scale::Linear,
535                },
536            ],
537            accuracy_metrics: compute_erf_accuracy(),
538        }
539    }
540
541    fn compute_erf_accuracy() -> AccuracyMetrics {
542        AccuracyMetrics {
543            max_relative_error: 1e-15,
544            mean_relative_error: 1e-16,
545            max_absolute_error: 1e-15,
546            mean_absolute_error: 1e-16,
547            ulp_errors: HashMap::new(),
548        }
549    }
550}
551
552/// Generate stability report for all functions
553#[allow(dead_code)]
554pub fn generate_stability_report() -> String {
555    let mut report = String::from("# Numerical Stability Analysis Report\n\n");
556
557    // Analyze each function family
558    let analyses = vec![
559        gamma_stability::analyze_gamma_stability(),
560        bessel_stability::analyze_bessel_j_stability(),
561        erf_stability::analyze_erf_stability(),
562    ];
563
564    for analysis in analyses {
565        let function_name = &analysis.function_name;
566        report.push_str(&format!("## {function_name}\n\n"));
567
568        // Parameter ranges
569        report.push_str("### Parameter Ranges Tested\n");
570        for range in &analysis.parameter_ranges {
571            report.push_str(&format!(
572                "- {}: [{}, {}] ({:?} scale)\n",
573                range.name, range.min, range.max, range.scale
574            ));
575        }
576        report.push('\n');
577
578        // Issues found
579        if !analysis.issues.is_empty() {
580            report.push_str("### Stability Issues\n");
581            for issue in &analysis.issues {
582                let issue_str = format_issue(issue);
583                report.push_str(&format!("- {issue_str}\n"));
584            }
585            report.push('\n');
586        }
587
588        // Condition numbers
589        if !analysis.condition_numbers.is_empty() {
590            report.push_str("### Condition Numbers\n");
591            for (params, cond) in &analysis.condition_numbers {
592                report.push_str(&format!("- {params}: {cond:.2e}\n"));
593            }
594            report.push('\n');
595        }
596
597        // Safe ranges
598        report.push_str("### Recommended Safe Ranges\n");
599        for range in &analysis.safe_ranges {
600            report.push_str(&format!(
601                "- {}: [{}, {}]\n",
602                range.name, range.min, range.max
603            ));
604        }
605        report.push('\n');
606
607        // Accuracy metrics
608        report.push_str("### Accuracy Metrics\n");
609        report.push_str(&format!(
610            "- Max relative error: {:.2e}\n",
611            analysis.accuracy_metrics.max_relative_error
612        ));
613        report.push_str(&format!(
614            "- Mean relative error: {:.2e}\n",
615            analysis.accuracy_metrics.mean_relative_error
616        ));
617        report.push_str(&format!(
618            "- Max absolute error: {:.2e}\n",
619            analysis.accuracy_metrics.max_absolute_error
620        ));
621        report.push_str(&format!(
622            "- Mean absolute error: {:.2e}\n",
623            analysis.accuracy_metrics.mean_absolute_error
624        ));
625        report.push('\n');
626    }
627
628    report
629}
630
631#[allow(dead_code)]
632fn format_issue(issue: &StabilityIssue) -> String {
633    match issue {
634        StabilityIssue::Overflow { params } => {
635            let params_str = format_params(params);
636            format!("Overflow at {params_str}")
637        }
638        StabilityIssue::Underflow { params } => {
639            let params_str = format_params(params);
640            format!("Underflow at {params_str}")
641        }
642        StabilityIssue::CatastrophicCancellation {
643            params,
644            relative_error,
645        } => {
646            format!(
647                "Catastrophic cancellation at {} (relative error: {:.2e})",
648                format_params(params),
649                relative_error
650            )
651        }
652        StabilityIssue::LossOfSignificance { params, bits_lost } => {
653            format!(
654                "Loss of {} bits of significance at {}",
655                bits_lost,
656                format_params(params)
657            )
658        }
659        StabilityIssue::SlowConvergence { params, iterations } => {
660            format!(
661                "Slow convergence ({} iterations) at {}",
662                iterations,
663                format_params(params)
664            )
665        }
666        StabilityIssue::NonConvergence { params } => {
667            let params_str = format_params(params);
668            format!("Non-convergence at {params_str}")
669        }
670        StabilityIssue::NumericalInstability {
671            params,
672            condition_number,
673        } => {
674            format!(
675                "Numerical instability at {} (condition number: {:.2e})",
676                format_params(params),
677                condition_number
678            )
679        }
680    }
681}
682
683#[allow(dead_code)]
684fn format_params(params: &[(String, f64)]) -> String {
685    params
686        .iter()
687        .map(|(name, value)| format!("{name}={value}"))
688        .collect::<Vec<_>>()
689        .join(", ")
690}
691
692/// Run comprehensive stability tests
693#[allow(dead_code)]
694pub fn run_stability_tests() -> SpecialResult<()> {
695    println!("Running numerical stability analysis...\n");
696
697    let report = generate_stability_report();
698    println!("{report}");
699
700    // Save report to file
701    std::fs::write("STABILITY_ANALYSIS.md", report)
702        .map_err(|e| crate::error::SpecialError::ComputationError(e.to_string()))?;
703
704    println!("Stability analysis complete. Report saved to STABILITY_ANALYSIS.md");
705
706    Ok(())
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    #[test]
714    fn test_gamma_stability_analysis() {
715        let analysis = gamma_stability::analyze_gamma_stability();
716        assert!(!analysis.issues.is_empty());
717        assert!(!analysis.condition_numbers.is_empty());
718        assert!(!analysis.safe_ranges.is_empty());
719    }
720
721    #[test]
722    fn test_bessel_stability_analysis() {
723        let analysis = bessel_stability::analyze_bessel_j_stability();
724        assert!(!analysis.issues.is_empty());
725        assert!(!analysis.safe_ranges.is_empty());
726    }
727
728    #[test]
729    fn test_erf_stability_analysis() {
730        let analysis = erf_stability::analyze_erf_stability();
731        assert!(!analysis.issues.is_empty());
732        assert!(!analysis.safe_ranges.is_empty());
733    }
734
735    #[test]
736    fn test_report_generation() {
737        let report = generate_stability_report();
738        assert!(report.contains("Numerical Stability Analysis Report"));
739        assert!(report.contains("gamma"));
740        assert!(report.contains("bessel_j"));
741        assert!(report.contains("error_functions"));
742    }
743}