1use crate::error::SpecialResult;
8use std::collections::HashMap;
9use std::f64;
10
11#[derive(Debug, Clone)]
13pub struct StabilityAnalysis {
14 pub function_name: String,
16 pub parameter_ranges: Vec<ParameterRange>,
18 pub issues: Vec<StabilityIssue>,
20 pub condition_numbers: HashMap<String, f64>,
22 pub safe_ranges: Vec<ParameterRange>,
24 pub accuracy_metrics: AccuracyMetrics,
26}
27
28#[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#[derive(Debug, Clone, Copy)]
39pub enum Scale {
40 Linear,
41 Logarithmic,
42 Exponential,
43}
44
45#[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#[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
85pub trait StabilityAnalyzable {
87 fn analyze_stability(&self) -> StabilityAnalysis;
89
90 fn condition_number(&self, params: &[(String, f64)]) -> f64;
92
93 fn find_safe_ranges(&self) -> Vec<ParameterRange>;
95}
96
97pub 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 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; 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 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 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 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 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
212pub 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 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 let j0_expected: f64 = 1.0 - x * x / 4.0;
228 let j0_error = (j0_val - j0_expected).abs() / j0_expected.abs();
229
230 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 for x in [100.0, 500.0, 1000.0, 5000.0] {
244 let j0_val = j0(x);
245
246 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, });
254 }
255 }
256
257 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 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 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 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 let ulp_error = if reference != 0.0 {
345 let ref_bits = reference.to_bits();
346 let comp_bits = computed.to_bits();
347 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 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 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 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 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
448pub 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 for x in [5.0, 10.0, 20.0, 30.0, 40.0] {
458 let erfc_val: f64 = erfc(x);
459
460 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 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 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 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#[allow(dead_code)]
554pub fn generate_stability_report() -> String {
555 let mut report = String::from("# Numerical Stability Analysis Report\n\n");
556
557 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 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 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 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 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 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#[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 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}