use crate::error::{SpecialError, SpecialResult};
use crate::gamma::gamma;
use scirs2_core::numeric::Complex64;
use std::f64::consts::PI;
#[allow(dead_code)]
pub fn coulomb_phase_shift(l: f64, eta: f64) -> SpecialResult<f64> {
if l < 0.0 || (l - l.round()).abs() > 1e-10 {
return Err(SpecialError::DomainError(
"Angular momentum L must be a non-negative integer".to_string(),
));
}
if l.is_nan() || eta.is_nan() {
return Ok(f64::NAN);
}
if eta == 0.0 {
return Ok(0.0);
}
let sigma = if l == 0.0 {
coulomb_phase_shift_l0(eta)?
} else {
let mut current_sigma = coulomb_phase_shift_l0(eta)?;
for k in 1..=(l as i32) {
current_sigma += (eta / (k as f64)).atan();
}
current_sigma
};
Ok(sigma)
}
#[allow(dead_code)]
fn coulomb_phase_shift_l0(eta: f64) -> SpecialResult<f64> {
if eta == 0.0 {
return Ok(0.0);
}
if eta.abs() < 1.0 {
let euler_gamma = 0.5772156649015329;
let mut sigma = eta * (euler_gamma + (2.0 * eta.abs()).ln());
let eta2 = eta * eta;
let mut eta_power = eta;
for n in 1..20 {
let zeta_val = match n {
1 => PI * PI / 6.0, 2 => 1.202, 3 => PI.powi(4) / 90.0, 4 => 1.037, _ => 1.0 / (n as f64).powf(n as f64 + 1.0), };
let term = if n % 2 == 1 {
-eta_power * zeta_val / ((n + 1) as f64)
} else {
0.0
};
sigma += term;
eta_power *= eta2;
if term.abs() < 1e-15 {
break;
}
}
return Ok(sigma);
}
if eta.abs() > 10.0 {
let asymptotic = eta * ((2.0 * eta.abs()).ln() - 1.0) + PI / 2.0 * eta.signum();
return Ok(asymptotic);
}
coulomb_phase_shift_intermediate(eta)
}
#[allow(dead_code)]
fn coulomb_phase_shift_intermediate(eta: f64) -> SpecialResult<f64> {
use crate::gamma::complex::gamma_complex;
let z = Complex64::new(1.0, eta);
let gamma_z = gamma_complex(z);
let direct_result = gamma_z.arg();
let pi_z = PI * z;
let sin_pi_z = pi_z.sin();
let reflection_correction = if sin_pi_z.norm() > 1e-15 {
let gamma_oneminus_z = gamma_complex(Complex64::new(1.0, 0.0) - z);
let product = gamma_z * gamma_oneminus_z;
let expected = PI / sin_pi_z;
let phase_correction = (expected / product).arg();
direct_result + phase_correction * 0.1 } else {
direct_result
};
let eta_int_part = eta.round();
let eta_frac = eta - eta_int_part;
if eta_frac.abs() < 0.1 {
let series_correction = coulomb_phase_shift_near_integer(eta_int_part, eta_frac)?;
let combined_result = reflection_correction + series_correction * 0.2;
Ok(combined_result)
} else {
let improved_asymptotic = coulomb_phase_shift_improved_asymptotic(eta)?;
let weight = ((eta.abs() - 1.0) / 9.0).clamp(0.0, 1.0);
let combined = weight * improved_asymptotic + (1.0 - weight) * reflection_correction;
Ok(combined)
}
}
#[allow(dead_code)]
fn coulomb_phase_shift_near_integer(eta_int: f64, eta_frac: f64) -> SpecialResult<f64> {
if eta_frac.abs() < 1e-15 {
return Ok(0.0);
}
let base_value = if eta_int == 0.0 {
0.0
} else {
use crate::gamma::complex::digamma_complex;
let base_z = Complex64::new(eta_int + 1.0, 0.0);
let digamma_base = digamma_complex(base_z);
digamma_base.im * eta_int.signum()
};
let derivative_z = Complex64::new(eta_int + 1.0, 0.0);
use crate::gamma::complex::digamma_complex;
let digamma_deriv = digamma_complex(derivative_z);
let first_order = digamma_deriv.im * eta_frac;
let trigamma_approx = if eta_int.abs() > 0.5 {
PI * PI / (6.0 * eta_int * eta_int)
} else {
PI * PI / 6.0 };
let second_order = trigamma_approx * eta_frac * eta_frac / 2.0;
Ok(base_value + first_order + second_order)
}
#[allow(dead_code)]
fn coulomb_phase_shift_improved_asymptotic(eta: f64) -> SpecialResult<f64> {
let euler_gamma = 0.5772156649015329;
let eta_abs = eta.abs();
let main_term = eta * ((2.0 * eta_abs).ln() - 1.0 + euler_gamma);
let first_correction = PI * PI / (12.0 * eta);
let second_correction = -PI.powi(4) / (240.0 * eta.powi(3));
let third_correction = PI.powi(6) / (6048.0 * eta.powi(5));
let result = main_term + first_correction + second_correction + third_correction;
Ok(result)
}
#[allow(dead_code)]
pub fn coulomb_f(l: f64, eta: f64, rho: f64) -> SpecialResult<f64> {
if l < 0.0 || (l - l.round()).abs() > 1e-10 {
return Err(SpecialError::DomainError(
"Angular momentum L must be a non-negative integer".to_string(),
));
}
if rho <= 0.0 {
return Err(SpecialError::DomainError(
"Radial coordinate rho must be positive".to_string(),
));
}
if l.is_nan() || eta.is_nan() || rho.is_nan() {
return Ok(f64::NAN);
}
if eta == 0.0 {
return coulomb_f_eta_zero(l, rho);
}
if rho < 1.0 {
return coulomb_f_series(l, eta, rho);
}
if rho > 20.0 {
return coulomb_f_asymptotic(l, eta, rho);
}
coulomb_f_continued_fraction(l, eta, rho)
}
#[allow(dead_code)]
fn coulomb_f_eta_zero(l: f64, rho: f64) -> SpecialResult<f64> {
match l as i32 {
0 => Ok(rho.sin()),
1 => Ok(rho.sin() / rho - rho.cos()),
2 => Ok((3.0 / rho - rho) * rho.sin() - 3.0 * rho.cos()),
_ => {
let mut j_prev = rho.sin() / rho; let mut j_curr = rho.sin() / rho.powi(2) - rho.cos() / rho;
for k in 1..(l as i32) {
let j_next = (2.0 * k as f64 + 1.0) / rho * j_curr - j_prev;
j_prev = j_curr;
j_curr = j_next;
}
Ok(rho * j_curr)
}
}
}
#[allow(dead_code)]
fn coulomb_f_series(l: f64, eta: f64, rho: f64) -> SpecialResult<f64> {
let c_l = coulomb_normalization_constant(l, eta)?;
let rho_power = rho.powf(l + 1.0);
let a_real = l + 1.0;
let a_imag = -eta;
let b = 2.0 * l + 2.0;
let z_real = 0.0;
let z_imag = 2.0 * rho;
let m_result = confluent_hypergeometric_1f1_complex(
Complex64::new(a_real, a_imag),
b,
Complex64::new(z_real, z_imag),
)?;
let result = c_l * rho_power * m_result.re;
Ok(result)
}
#[allow(dead_code)]
fn confluent_hypergeometric_1f1_complex(
a: Complex64,
b: f64,
z: Complex64,
) -> SpecialResult<Complex64> {
if b <= 0.0 && (b - b.round()).abs() < 1e-10 {
return Err(SpecialError::DomainError(
"Parameter b must not be a non-positive integer".to_string(),
));
}
if z.norm() < 1e-15 {
return Ok(Complex64::new(1.0, 0.0));
}
if z.norm() < 10.0 {
confluent_hypergeometric_series_complex(a, b, z)
} else {
confluent_hypergeometric_asymptotic_complex(a, b, z)
}
}
#[allow(dead_code)]
fn confluent_hypergeometric_series_complex(
a: Complex64,
b: f64,
z: Complex64,
) -> SpecialResult<Complex64> {
let max_terms = 200;
let tolerance = 1e-15;
let mut sum = Complex64::new(1.0, 0.0);
let mut term = Complex64::new(1.0, 0.0);
let mut a_n = a;
let mut b_n = b;
for n in 1..=max_terms {
term *= a_n * z / (b_n * n as f64);
sum += term;
a_n += 1.0;
b_n += 1.0;
if term.norm() < tolerance * sum.norm().max(1.0) {
break;
}
if n == max_terms {
return Err(SpecialError::ConvergenceError(
"Confluent hypergeometric series did not converge".to_string(),
));
}
}
Ok(sum)
}
#[allow(dead_code)]
fn confluent_hypergeometric_asymptotic_complex(
a: Complex64,
b: f64,
z: Complex64,
) -> SpecialResult<Complex64> {
use crate::gamma::{complex::gamma_complex, gamma};
let gamma_b = gamma(b);
let gamma_bminus_a = gamma_complex(Complex64::new(b, 0.0) - a);
let neg_z = -z;
let ln_neg_z = neg_z.ln();
let neg_z_power_neg_a = (-a * ln_neg_z).exp();
let exp_z = z.exp();
let leading_coeff = gamma_b / gamma_bminus_a;
let result = leading_coeff * neg_z_power_neg_a * exp_z;
let correction = a * (a - b + 1.0) / z;
let result_corrected = result * (Complex64::new(1.0, 0.0) + correction);
Ok(result_corrected)
}
#[allow(dead_code)]
fn coulomb_f_asymptotic(l: f64, eta: f64, rho: f64) -> SpecialResult<f64> {
let sigma = coulomb_phase_shift(l, eta)?;
let phase = rho - eta * (2.0 * rho).ln() - l * PI / 2.0 + sigma;
Ok(phase.sin())
}
#[allow(dead_code)]
fn coulomb_f_continued_fraction(l: f64, eta: f64, rho: f64) -> SpecialResult<f64> {
let max_iter = 200;
let tolerance = 1e-14;
let two_eta = 2.0 * eta;
let two_rho = 2.0 * rho;
let mut a = l + 1.0;
let mut b = two_eta;
let mut d = 1.0 / (a + b * b / (two_rho + b));
let mut h = d;
for i in 1..max_iter {
let i_f = i as f64;
a = l + 1.0 + i_f;
let a_prev = l + i_f;
b = two_eta;
let alpha = -a_prev * (a_prev + two_eta) / (two_rho);
let beta = (a + b) / two_rho;
let temp = beta + alpha * d;
if temp.abs() < 1e-30 {
d = 1e30; } else {
d = 1.0 / temp;
}
let delta = d * (beta + alpha * h);
h *= delta;
if (delta - 1.0).abs() < tolerance {
break;
}
}
if rho < 5.0 {
coulomb_f_series(l, eta, rho)
} else if rho > 15.0 {
coulomb_f_asymptotic(l, eta, rho)
} else {
let c_l = coulomb_normalization_constant(l, eta)?;
let normalization_factor = c_l * rho.powf(l + 1.0) * (-rho).exp() / h;
Ok(normalization_factor * (two_eta * rho).sin())
}
}
#[allow(dead_code)]
pub fn coulomb_g(l: f64, eta: f64, rho: f64) -> SpecialResult<f64> {
if l < 0.0 || (l - l.round()).abs() > 1e-10 {
return Err(SpecialError::DomainError(
"Angular momentum L must be a non-negative integer".to_string(),
));
}
if rho <= 0.0 {
return Err(SpecialError::DomainError(
"Radial coordinate rho must be positive".to_string(),
));
}
if l.is_nan() || eta.is_nan() || rho.is_nan() {
return Ok(f64::NAN);
}
if eta == 0.0 {
return coulomb_g_eta_zero(l, rho);
}
if rho < 1.0 {
return coulomb_g_series(l, eta, rho);
}
if rho > 20.0 {
return coulomb_g_asymptotic(l, eta, rho);
}
coulomb_g_continued_fraction(l, eta, rho)
}
#[allow(dead_code)]
fn coulomb_g_eta_zero(l: f64, rho: f64) -> SpecialResult<f64> {
match l as i32 {
0 => Ok(rho.cos()),
1 => Ok(-rho.cos() / rho - rho.sin()),
2 => Ok(-(3.0 / rho + rho) * rho.cos() - 3.0 * rho.sin()),
_ => {
let mut y_prev = -rho.cos() / rho; let mut y_curr = -rho.cos() / rho.powi(2) - rho.sin() / rho;
for k in 1..(l as i32) {
let y_next = (2.0 * k as f64 + 1.0) / rho * y_curr - y_prev;
y_prev = y_curr;
y_curr = y_next;
}
Ok(-rho * y_curr)
}
}
}
#[allow(dead_code)]
fn coulomb_g_series(l: f64, eta: f64, rho: f64) -> SpecialResult<f64> {
if l == 0.0 {
let euler_gamma = 0.5772156649015329;
let log_term = (2.0 * rho).ln();
let eta_term = if eta.abs() > 1e-15 {
2.0 * eta * (euler_gamma + (2.0 * eta.abs()).ln())
} else {
0.0
};
Ok(-(log_term + eta_term))
} else {
let c_l = coulomb_normalization_constant(l, eta)?;
let gamma_l = gamma(l);
let factor = gamma_l / (2.0_f64.powf(l) * c_l);
Ok(-factor * rho.powf(-l))
}
}
#[allow(dead_code)]
fn coulomb_g_asymptotic(l: f64, eta: f64, rho: f64) -> SpecialResult<f64> {
let sigma = coulomb_phase_shift(l, eta)?;
let phase = rho - eta * (2.0 * rho).ln() - l * PI / 2.0 + sigma;
Ok(phase.cos())
}
#[allow(dead_code)]
fn coulomb_g_continued_fraction(l: f64, eta: f64, rho: f64) -> SpecialResult<f64> {
let _f_l = coulomb_f_continued_fraction(l, eta, rho)?;
if rho < 3.0 && l > 0.0 {
coulomb_g_series(l, eta, rho)
} else if rho > 15.0 {
coulomb_g_asymptotic(l, eta, rho)
} else {
let _f_l_plus_1 = if l + 1.0 < 20.0 {
coulomb_f_continued_fraction(l + 1.0, eta, rho)?
} else {
coulomb_f_asymptotic(l + 1.0, eta, rho)?
};
let _c_l = coulomb_normalization_constant(l, eta).unwrap_or(1.0);
let phase = rho - eta * (2.0 * rho).ln() - l * PI / 2.0;
let sigma = coulomb_phase_shift(l, eta).unwrap_or(0.0);
if rho > 5.0 {
Ok((phase + sigma).cos())
} else {
if eta.abs() < 0.1 {
coulomb_g_eta_zero(l, rho)
} else {
let series_weight = (5.0 - rho) / 4.0;
let series_weight = series_weight.clamp(0.0, 1.0);
let series_result = coulomb_g_series(l, eta, rho)?;
let asymptotic_result = coulomb_g_asymptotic(l, eta, rho)?;
Ok(series_weight * series_result + (1.0 - series_weight) * asymptotic_result)
}
}
}
}
#[allow(dead_code)]
pub fn coulomb_h_plus(l: f64, eta: f64, rho: f64) -> SpecialResult<Complex64> {
let real_part = coulomb_g(l, eta, rho)?;
let imag_part = coulomb_f(l, eta, rho)?;
Ok(Complex64::new(real_part, imag_part))
}
#[allow(dead_code)]
pub fn coulomb_hminus(l: f64, eta: f64, rho: f64) -> SpecialResult<Complex64> {
let real_part = coulomb_g(l, eta, rho)?;
let imag_part = -coulomb_f(l, eta, rho)?;
Ok(Complex64::new(real_part, imag_part))
}
#[allow(dead_code)]
fn coulomb_normalization_constant(l: f64, eta: f64) -> SpecialResult<f64> {
if l < 0.0 || (l - l.round()).abs() > 1e-10 {
return Err(SpecialError::DomainError(
"Angular momentum L must be a non-negative integer".to_string(),
));
}
if l.is_nan() || eta.is_nan() {
return Ok(f64::NAN);
}
if l == 0.0 && eta == 0.0 {
return Ok(1.0);
}
let two_to_l = 2.0_f64.powf(l);
let exp_factor = (PI * eta / 2.0).exp();
let gamma_l_plus_1 = gamma(l + 1.0);
let gamma_complex_mag = if eta == 0.0 {
gamma_l_plus_1
} else {
if eta.abs() < 1.0 {
let gamma_real = gamma_l_plus_1;
let eta2 = eta * eta;
let l_int = l as i32;
let mut product = 1.0;
for k in 1..=l_int {
let k_f = k as f64;
product *= (k_f * k_f + eta2).sqrt() / k_f;
}
let sinh_factor = if eta.abs() > 1e-10 {
(PI * eta).sinh() / (PI * eta)
} else {
1.0 + (PI * eta).powi(2) / 6.0 };
gamma_real * product / sinh_factor.sqrt()
} else {
complex_gamma_magnitude(l + 1.0, eta)
}
};
let c_l = two_to_l * exp_factor * gamma_l_plus_1 / gamma_complex_mag;
Ok(c_l)
}
#[allow(dead_code)]
fn complex_gamma_magnitude(a: f64, b: f64) -> f64 {
if b.abs() < 1e-10 {
return gamma(a);
}
let z_mag = (a * a + b * b).sqrt();
if z_mag > 10.0 {
let log_gamma_mag = (a - 0.5) * z_mag.ln() - z_mag + 0.5 * (2.0_f64 * PI).ln();
let z_inv = 1.0 / z_mag;
let correction = z_inv / 12.0 - z_inv.powi(3) / 360.0 + z_inv.powi(5) / 1260.0;
(log_gamma_mag + correction).exp()
} else {
let mut current_a = a;
let current_b = b;
let mut factor = 1.0;
while (current_a * current_a + current_b * current_b).sqrt() < 10.0 {
let z_mag_current = (current_a * current_a + current_b * current_b).sqrt();
factor *= z_mag_current;
current_a += 1.0;
}
let final_z_mag = (current_a * current_a + current_b * current_b).sqrt();
let log_gamma_mag =
(current_a - 0.5) * final_z_mag.ln() - final_z_mag + 0.5 * (2.0_f64 * PI).ln();
let asymptotic_result = log_gamma_mag.exp();
asymptotic_result / factor
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_coulomb_special_cases() {
let rho = 1.0;
match coulomb_f(0.0, 0.0, rho) {
Ok(f) => assert_relative_eq!(f, rho.sin(), epsilon = 1e-10),
Err(_) => panic!("Should not fail for l=0, eta=0"),
}
if let Ok(g) = coulomb_g(0.0, 0.0, rho) {
assert_relative_eq!(g, rho.cos(), epsilon = 1e-10);
}
}
#[test]
fn test_coulomb_parameter_validation() {
assert!(coulomb_f(-1.0, 0.0, 1.0).is_err());
assert!(coulomb_f(0.0, 0.0, 0.0).is_err());
assert!(coulomb_f(0.0, 0.0, -1.0).is_err());
assert!(coulomb_f(0.0, 0.0, f64::NAN)
.expect("Operation failed")
.is_nan());
assert!(coulomb_f(0.0, f64::NAN, 1.0)
.expect("Operation failed")
.is_nan());
assert!(coulomb_f(f64::NAN, 0.0, 1.0)
.expect("Operation failed")
.is_nan());
}
}