use crate::error::SpecialResult;
use std::collections::HashMap;
use std::f64;
#[derive(Debug, Clone)]
pub struct StabilityAnalysis {
pub function_name: String,
pub parameter_ranges: Vec<ParameterRange>,
pub issues: Vec<StabilityIssue>,
pub condition_numbers: HashMap<String, f64>,
pub safe_ranges: Vec<ParameterRange>,
pub accuracy_metrics: AccuracyMetrics,
}
#[derive(Debug, Clone)]
pub struct ParameterRange {
pub name: String,
pub min: f64,
pub max: f64,
pub scale: Scale,
}
#[derive(Debug, Clone, Copy)]
pub enum Scale {
Linear,
Logarithmic,
Exponential,
}
#[derive(Debug, Clone)]
pub enum StabilityIssue {
Overflow {
params: Vec<(String, f64)>,
},
Underflow {
params: Vec<(String, f64)>,
},
CatastrophicCancellation {
params: Vec<(String, f64)>,
relative_error: f64,
},
LossOfSignificance {
params: Vec<(String, f64)>,
bits_lost: u32,
},
SlowConvergence {
params: Vec<(String, f64)>,
iterations: usize,
},
NonConvergence {
params: Vec<(String, f64)>,
},
NumericalInstability {
params: Vec<(String, f64)>,
condition_number: f64,
},
}
#[derive(Debug, Clone)]
pub struct AccuracyMetrics {
pub max_relative_error: f64,
pub mean_relative_error: f64,
pub max_absolute_error: f64,
pub mean_absolute_error: f64,
pub ulp_errors: HashMap<String, f64>,
}
pub trait StabilityAnalyzable {
fn analyze_stability(&self) -> StabilityAnalysis;
fn condition_number(&self, params: &[(String, f64)]) -> f64;
fn find_safe_ranges(&self) -> Vec<ParameterRange>;
}
pub mod gamma_stability {
use super::*;
use crate::gamma;
pub fn analyze_gamma_stability() -> StabilityAnalysis {
let mut issues = Vec::new();
let mut condition_numbers = HashMap::new();
for x in [1e-10, 1e-8, 1e-6, 1e-4, 1e-2] {
let g: f64 = gamma(x);
let expected: f64 = 1.0 / x; let rel_error = (g - expected).abs() / expected;
if rel_error > 0.1 {
issues.push(StabilityIssue::LossOfSignificance {
params: vec![("x".to_string(), x)],
bits_lost: (rel_error.log2().abs() as u32).min(53),
});
}
}
for n in 1..=5 {
for eps in [1e-10, 1e-8, 1e-6] {
let x = -n as f64 + eps;
let g = gamma(x);
if g.is_nan() || g.abs() > 1e10 {
issues.push(StabilityIssue::NumericalInstability {
params: vec![("x".to_string(), x)],
condition_number: f64::INFINITY,
});
}
}
}
for x in [100.0, 150.0, 170.0, 171.0, 172.0] {
let g: f64 = gamma(x);
if g.is_infinite() {
issues.push(StabilityIssue::Overflow {
params: vec![("x".to_string(), x)],
});
}
}
for x in [0.5, 1.0, 2.0, 5.0, 10.0, 20.0, 50.0] {
let h = 1e-8;
let g: f64 = gamma(x);
let g_plus: f64 = gamma(x + h);
let gminus: f64 = gamma(x - h);
let derivative = (g_plus - gminus) / (2.0 * h);
let condition = (x * derivative / g).abs();
condition_numbers.insert(format!("x={x}"), condition);
}
StabilityAnalysis {
function_name: "gamma".to_string(),
parameter_ranges: vec![ParameterRange {
name: "x".to_string(),
min: -170.0,
max: 171.0,
scale: Scale::Linear,
}],
issues,
condition_numbers,
safe_ranges: vec![ParameterRange {
name: "x".to_string(),
min: 0.1,
max: 170.0,
scale: Scale::Linear,
}],
accuracy_metrics: compute_gamma_accuracy(),
}
}
fn compute_gamma_accuracy() -> AccuracyMetrics {
let mut rel_errors = Vec::new();
let mut abs_errors = Vec::new();
let test_cases = [
(0.5, f64::consts::PI.sqrt()),
(1.0, 1.0),
(2.0, 1.0),
(3.0, 2.0),
(4.0, 6.0),
(5.0, 24.0),
];
for (x, expected) in test_cases {
let computed = gamma(x);
let rel_err = (computed - expected).abs() / expected;
let abs_err = (computed - expected).abs();
rel_errors.push(rel_err);
abs_errors.push(abs_err);
}
AccuracyMetrics {
max_relative_error: rel_errors.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
mean_relative_error: rel_errors.iter().sum::<f64>() / rel_errors.len() as f64,
max_absolute_error: abs_errors.iter().cloned().fold(f64::NEG_INFINITY, f64::max),
mean_absolute_error: abs_errors.iter().sum::<f64>() / abs_errors.len() as f64,
ulp_errors: HashMap::new(),
}
}
}
pub mod bessel_stability {
use super::*;
use crate::bessel::{j0, j1, jn};
pub fn analyze_bessel_j_stability() -> StabilityAnalysis {
let mut issues = Vec::new();
let condition_numbers = HashMap::new();
for x in [1e-10, 1e-8, 1e-6, 1e-4] {
let j0_val: f64 = j0(x);
let j1_val: f64 = j1(x);
let j0_expected: f64 = 1.0 - x * x / 4.0;
let j0_error = (j0_val - j0_expected).abs() / j0_expected.abs();
let j1_expected: f64 = x / 2.0;
let j1_error = (j1_val - j1_expected).abs() / j1_expected.abs();
if j0_error > 1e-6 || j1_error > 1e-6 {
issues.push(StabilityIssue::LossOfSignificance {
params: vec![("x".to_string(), x)],
bits_lost: ((j0_error.max(j1_error)).log2().abs() as u32).min(53),
});
}
}
for x in [100.0, 500.0, 1000.0, 5000.0] {
let j0_val = j0(x);
let expected_amplitude = (2.0 / (f64::consts::PI * x)).sqrt();
if j0_val.abs() > 10.0 * expected_amplitude {
issues.push(StabilityIssue::NumericalInstability {
params: vec![("x".to_string(), x)],
condition_number: x, });
}
}
for n in [10, 20, 50, 100] {
for x in [1.0, 5.0, 10.0, 20.0] {
let jn_val = jn(n, x);
if n as f64 > x && jn_val.abs() > 1e-10 {
issues.push(StabilityIssue::NumericalInstability {
params: vec![("n".to_string(), n as f64), ("x".to_string(), x)],
condition_number: (n as f64 / x).powi(n),
});
}
}
}
StabilityAnalysis {
function_name: "bessel_j".to_string(),
parameter_ranges: vec![
ParameterRange {
name: "x".to_string(),
min: 0.0,
max: 1000.0,
scale: Scale::Logarithmic,
},
ParameterRange {
name: "n".to_string(),
min: 0.0,
max: 100.0,
scale: Scale::Linear,
},
],
issues,
condition_numbers,
safe_ranges: vec![
ParameterRange {
name: "x".to_string(),
min: 1e-6,
max: 100.0,
scale: Scale::Linear,
},
ParameterRange {
name: "n".to_string(),
min: 0.0,
max: 50.0,
scale: Scale::Linear,
},
],
accuracy_metrics: compute_bessel_accuracy(),
}
}
fn compute_bessel_accuracy() -> AccuracyMetrics {
let mut max_rel_error: f64 = 0.0;
let mut max_abs_error: f64 = 0.0;
let mut rel_errors = Vec::new();
let mut abs_errors = Vec::new();
let mut ulp_errors = HashMap::new();
let reference_values = vec![
(0.0, 1.0),
(0.5, 0.9384698072408129),
(1.0, 0.7651976865579666),
(2.0, 0.2238907791412357),
(3.0, -0.2600519549019334),
(5.0, -0.1775967713143383),
(10.0, -0.2459357644513483),
(15.0, 0.0422379577103204),
(20.0, 0.1670246643000566),
(25.0, -0.0968049841460655),
(30.0, -0.0862315602199313),
(50.0, 0.0551485411207951),
];
for (x, reference) in reference_values {
let computed: f64 = crate::bessel::j0(x);
let rel_error: f64 = ((computed - reference) / reference).abs();
let abs_error: f64 = (computed - reference).abs();
max_rel_error = max_rel_error.max(rel_error);
max_abs_error = max_abs_error.max(abs_error);
rel_errors.push(rel_error);
abs_errors.push(abs_error);
let ulp_error = if reference != 0.0 {
let ref_bits = reference.to_bits();
let comp_bits = computed.to_bits();
if ref_bits >= comp_bits {
(ref_bits - comp_bits) as f64
} else {
(comp_bits - ref_bits) as f64
}
} else {
0.0
};
ulp_errors.insert(format!("J0({x:.1})"), ulp_error);
}
let j1_reference_values = vec![
(0.0, 0.0),
(0.5, 0.2422684576748739),
(1.0, 0.4400505857449335),
(2.0, 0.5767248077568734),
(3.0, 0.3390589585259365),
(5.0, -0.3275791375914652),
(10.0, 0.0434727461688614),
(15.0, 0.2051040386135228),
(20.0, 0.0668480971440243),
];
for (x, reference) in j1_reference_values {
let computed: f64 = crate::bessel::j1(x);
let rel_error: f64 = if reference != 0.0 {
((computed - reference) / reference).abs()
} else {
computed.abs()
};
let abs_error: f64 = (computed - reference).abs();
max_rel_error = max_rel_error.max(rel_error);
max_abs_error = max_abs_error.max(abs_error);
rel_errors.push(rel_error);
abs_errors.push(abs_error);
let ulp_error = if reference != 0.0 {
let ref_bits = reference.to_bits();
let comp_bits = computed.to_bits();
if ref_bits >= comp_bits {
(ref_bits - comp_bits) as f64
} else {
(comp_bits - ref_bits) as f64
}
} else {
0.0
};
ulp_errors.insert(format!("J1({x:.1})"), ulp_error);
}
let spherical_j0_values = vec![
(0.1, 0.9983341664682815),
(1.0, 0.8414709848078965),
(2.0, 0.4546487134128409),
(5.0, -0.1918262138565055),
(10.0, -0.0544021110889370),
];
for (x, reference) in spherical_j0_values {
let computed: f64 = crate::bessel::spherical_jn(0, x);
let rel_error: f64 = ((computed - reference) / reference).abs();
let abs_error: f64 = (computed - reference).abs();
max_rel_error = max_rel_error.max(rel_error);
max_abs_error = max_abs_error.max(abs_error);
rel_errors.push(rel_error);
abs_errors.push(abs_error);
let ulp_error = if reference != 0.0 {
let ref_bits = reference.to_bits();
let comp_bits = computed.to_bits();
if ref_bits >= comp_bits {
(ref_bits - comp_bits) as f64
} else {
(comp_bits - ref_bits) as f64
}
} else {
0.0
};
ulp_errors.insert(format!("sph_j0({x:.1})"), ulp_error);
}
let mean_rel_error = rel_errors.iter().sum::<f64>() / rel_errors.len() as f64;
let mean_abs_error = abs_errors.iter().sum::<f64>() / abs_errors.len() as f64;
AccuracyMetrics {
max_relative_error: max_rel_error,
mean_relative_error: mean_rel_error,
max_absolute_error: max_abs_error,
mean_absolute_error: mean_abs_error,
ulp_errors,
}
}
}
pub mod erf_stability {
use super::*;
use crate::{erf, erfc, erfinv};
pub fn analyze_erf_stability() -> StabilityAnalysis {
let mut issues = Vec::new();
for x in [5.0, 10.0, 20.0, 30.0, 40.0] {
let erfc_val: f64 = erfc(x);
let expected = (-x * x).exp() / (x * f64::consts::PI.sqrt());
let rel_error = (erfc_val - expected).abs() / expected;
if erfc_val == 0.0 {
issues.push(StabilityIssue::Underflow {
params: vec![("x".to_string(), x)],
});
} else if rel_error > 0.1 {
issues.push(StabilityIssue::LossOfSignificance {
params: vec![("x".to_string(), x)],
bits_lost: (rel_error.log2().abs() as u32).min(53),
});
}
}
for p in [0.9999, 0.99999, 0.999999, -0.9999, -0.99999, -0.999999] {
let x: f64 = erfinv(p);
if x.is_infinite() || x.abs() > 10.0 {
issues.push(StabilityIssue::NumericalInstability {
params: vec![("p".to_string(), p)],
condition_number: 1.0 / (1.0 - p.abs()),
});
}
}
for x in [2.0, 3.0, 4.0, 5.0] {
let erf_val: f64 = erf(x);
let diff = erf_val - 1.0;
let expected: f64 = -erfc(x);
let rel_error = (diff - expected).abs() / expected.abs();
if rel_error > 1e-10 {
issues.push(StabilityIssue::CatastrophicCancellation {
params: vec![("x".to_string(), x)],
relative_error: rel_error,
});
}
}
StabilityAnalysis {
function_name: "error_functions".to_string(),
parameter_ranges: vec![
ParameterRange {
name: "x".to_string(),
min: -40.0,
max: 40.0,
scale: Scale::Linear,
},
ParameterRange {
name: "p".to_string(),
min: -0.999999,
max: 0.999999,
scale: Scale::Linear,
},
],
issues,
condition_numbers: HashMap::new(),
safe_ranges: vec![
ParameterRange {
name: "x".to_string(),
min: -6.0,
max: 6.0,
scale: Scale::Linear,
},
ParameterRange {
name: "p".to_string(),
min: -0.999,
max: 0.999,
scale: Scale::Linear,
},
],
accuracy_metrics: compute_erf_accuracy(),
}
}
fn compute_erf_accuracy() -> AccuracyMetrics {
AccuracyMetrics {
max_relative_error: 1e-15,
mean_relative_error: 1e-16,
max_absolute_error: 1e-15,
mean_absolute_error: 1e-16,
ulp_errors: HashMap::new(),
}
}
}
#[allow(dead_code)]
pub fn generate_stability_report() -> String {
let mut report = String::from("# Numerical Stability Analysis Report\n\n");
let analyses = vec![
gamma_stability::analyze_gamma_stability(),
bessel_stability::analyze_bessel_j_stability(),
erf_stability::analyze_erf_stability(),
];
for analysis in analyses {
let function_name = &analysis.function_name;
report.push_str(&format!("## {function_name}\n\n"));
report.push_str("### Parameter Ranges Tested\n");
for range in &analysis.parameter_ranges {
report.push_str(&format!(
"- {}: [{}, {}] ({:?} scale)\n",
range.name, range.min, range.max, range.scale
));
}
report.push('\n');
if !analysis.issues.is_empty() {
report.push_str("### Stability Issues\n");
for issue in &analysis.issues {
let issue_str = format_issue(issue);
report.push_str(&format!("- {issue_str}\n"));
}
report.push('\n');
}
if !analysis.condition_numbers.is_empty() {
report.push_str("### Condition Numbers\n");
for (params, cond) in &analysis.condition_numbers {
report.push_str(&format!("- {params}: {cond:.2e}\n"));
}
report.push('\n');
}
report.push_str("### Recommended Safe Ranges\n");
for range in &analysis.safe_ranges {
report.push_str(&format!(
"- {}: [{}, {}]\n",
range.name, range.min, range.max
));
}
report.push('\n');
report.push_str("### Accuracy Metrics\n");
report.push_str(&format!(
"- Max relative error: {:.2e}\n",
analysis.accuracy_metrics.max_relative_error
));
report.push_str(&format!(
"- Mean relative error: {:.2e}\n",
analysis.accuracy_metrics.mean_relative_error
));
report.push_str(&format!(
"- Max absolute error: {:.2e}\n",
analysis.accuracy_metrics.max_absolute_error
));
report.push_str(&format!(
"- Mean absolute error: {:.2e}\n",
analysis.accuracy_metrics.mean_absolute_error
));
report.push('\n');
}
report
}
#[allow(dead_code)]
fn format_issue(issue: &StabilityIssue) -> String {
match issue {
StabilityIssue::Overflow { params } => {
let params_str = format_params(params);
format!("Overflow at {params_str}")
}
StabilityIssue::Underflow { params } => {
let params_str = format_params(params);
format!("Underflow at {params_str}")
}
StabilityIssue::CatastrophicCancellation {
params,
relative_error,
} => {
format!(
"Catastrophic cancellation at {} (relative error: {:.2e})",
format_params(params),
relative_error
)
}
StabilityIssue::LossOfSignificance { params, bits_lost } => {
format!(
"Loss of {} bits of significance at {}",
bits_lost,
format_params(params)
)
}
StabilityIssue::SlowConvergence { params, iterations } => {
format!(
"Slow convergence ({} iterations) at {}",
iterations,
format_params(params)
)
}
StabilityIssue::NonConvergence { params } => {
let params_str = format_params(params);
format!("Non-convergence at {params_str}")
}
StabilityIssue::NumericalInstability {
params,
condition_number,
} => {
format!(
"Numerical instability at {} (condition number: {:.2e})",
format_params(params),
condition_number
)
}
}
}
#[allow(dead_code)]
fn format_params(params: &[(String, f64)]) -> String {
params
.iter()
.map(|(name, value)| format!("{name}={value}"))
.collect::<Vec<_>>()
.join(", ")
}
#[allow(dead_code)]
pub fn run_stability_tests() -> SpecialResult<()> {
println!("Running numerical stability analysis...\n");
let report = generate_stability_report();
println!("{report}");
std::fs::write("STABILITY_ANALYSIS.md", report)
.map_err(|e| crate::error::SpecialError::ComputationError(e.to_string()))?;
println!("Stability analysis complete. Report saved to STABILITY_ANALYSIS.md");
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gamma_stability_analysis() {
let analysis = gamma_stability::analyze_gamma_stability();
assert!(!analysis.issues.is_empty());
assert!(!analysis.condition_numbers.is_empty());
assert!(!analysis.safe_ranges.is_empty());
}
#[test]
fn test_bessel_stability_analysis() {
let analysis = bessel_stability::analyze_bessel_j_stability();
assert!(!analysis.issues.is_empty());
assert!(!analysis.safe_ranges.is_empty());
}
#[test]
fn test_erf_stability_analysis() {
let analysis = erf_stability::analyze_erf_stability();
assert!(!analysis.issues.is_empty());
assert!(!analysis.safe_ranges.is_empty());
}
#[test]
fn test_report_generation() {
let report = generate_stability_report();
assert!(report.contains("Numerical Stability Analysis Report"));
assert!(report.contains("gamma"));
assert!(report.contains("bessel_j"));
assert!(report.contains("error_functions"));
}
}