use scirs2_core::numeric::Complex64;
use std::f64::consts::PI;
#[derive(Debug, thiserror::Error)]
pub enum ConnectionError {
#[error("Singular connection: {0}")]
Singular(String),
#[error("Domain mismatch: {0}")]
DomainMismatch(String),
#[error("Dimension mismatch: matrix is {rows}x{cols}, coefficients have length {given}")]
DimensionMismatch {
rows: usize,
cols: usize,
given: usize,
},
}
#[derive(Debug, Clone)]
pub struct ConnectionFormula {
pub from_basis: String,
pub to_basis: String,
pub matrix: Vec<Vec<Complex64>>,
pub valid_range: Option<(f64, f64)>,
}
impl ConnectionFormula {
pub fn apply(&self, basis_b_coeffs: &[Complex64]) -> Result<Vec<Complex64>, ConnectionError> {
let rows = self.matrix.len();
if rows == 0 {
return Ok(Vec::new());
}
let cols = self.matrix[0].len();
if basis_b_coeffs.len() != cols {
return Err(ConnectionError::DimensionMismatch {
rows,
cols,
given: basis_b_coeffs.len(),
});
}
let mut result = vec![Complex64::new(0.0, 0.0); rows];
for (i, row) in self.matrix.iter().enumerate() {
for (j, &entry) in row.iter().enumerate() {
result[i] += entry * basis_b_coeffs[j];
}
}
Ok(result)
}
pub fn is_valid_at(&self, param: f64) -> bool {
match self.valid_range {
None => true,
Some((lo, hi)) => param >= lo && param <= hi,
}
}
pub fn rows(&self) -> usize {
self.matrix.len()
}
pub fn cols(&self) -> usize {
self.matrix.first().map(|r| r.len()).unwrap_or(0)
}
}
fn mat2x2(c00: f64, c01: f64, c10: f64, c11: f64) -> Vec<Vec<Complex64>> {
vec![
vec![Complex64::new(c00, 0.0), Complex64::new(c01, 0.0)],
vec![Complex64::new(c10, 0.0), Complex64::new(c11, 0.0)],
]
}
fn mat2x2_c(c00: Complex64, c01: Complex64, c10: Complex64, c11: Complex64) -> Vec<Vec<Complex64>> {
vec![vec![c00, c01], vec![c10, c11]]
}
pub fn bessel_j_to_y_connection(nu: f64) -> Result<ConnectionFormula, ConnectionError> {
let sin_nu_pi = (nu * PI).sin();
if sin_nu_pi.abs() < 1e-12 {
return Err(ConnectionError::Singular(format!(
"nu = {} is an integer; J-Y Bessel connection is degenerate",
nu
)));
}
let cos_nu_pi = (nu * PI).cos();
let matrix = mat2x2(1.0, 0.0, cos_nu_pi, -sin_nu_pi);
Ok(ConnectionFormula {
from_basis: format!("(J_{nu}, J_{{-{nu}}})"),
to_basis: format!("(J_{nu}, Y_{nu})"),
matrix,
valid_range: None, })
}
pub fn bessel_j_to_hankel_connection() -> ConnectionFormula {
let i = Complex64::new(0.0, 1.0);
let one = Complex64::new(1.0, 0.0);
let matrix = mat2x2_c(one, i, one, -i);
ConnectionFormula {
from_basis: "(H^(1)_nu, H^(2)_nu)".to_string(),
to_basis: "(J_nu, Y_nu)".to_string(),
matrix,
valid_range: None,
}
}
pub fn bessel_j_to_modified_connection(nu: f64) -> ConnectionFormula {
let phase_neg = Complex64::new(0.0, -nu * PI / 2.0).exp();
let phase_pos = Complex64::new(0.0, nu * PI / 2.0).exp();
let zero = Complex64::new(0.0, 0.0);
let matrix = mat2x2_c(phase_neg, zero, zero, phase_pos);
ConnectionFormula {
from_basis: "(I_nu, I_{-nu})".to_string(),
to_basis: "(J_nu, J_{-nu})".to_string(),
matrix,
valid_range: None,
}
}
pub fn hypergeometric_z0_to_z1_connection(
a: f64,
b: f64,
c: f64,
) -> Result<ConnectionFormula, ConnectionError> {
for (name, val) in [
("c", c),
("c-a", c - a),
("c-b", c - b),
("a+b-c", a + b - c),
] {
if val <= 0.0 && (val - val.round()).abs() < 1e-10 {
return Err(ConnectionError::Singular(format!(
"{} = {} is a non-positive integer; Gamma function has a pole",
name, val
)));
}
}
let gamma_c = gamma_f64(c);
let gamma_c_minus_a_minus_b = gamma_f64(c - a - b);
let gamma_c_minus_a = gamma_f64(c - a);
let gamma_c_minus_b = gamma_f64(c - b);
let gamma_a_plus_b_minus_c = gamma_f64(a + b - c);
let gamma_a = gamma_f64(a);
let gamma_b = gamma_f64(b);
let c11 = (gamma_c * gamma_c_minus_a_minus_b) / (gamma_c_minus_a * gamma_c_minus_b);
let c12 = (gamma_c * gamma_a_plus_b_minus_c) / (gamma_a * gamma_b);
let gamma_2_minus_c = gamma_f64(2.0 - c);
let gamma_1_minus_a = gamma_f64(1.0 - a);
let gamma_1_minus_b = gamma_f64(1.0 - b);
let gamma_a_minus_c_plus_1 = gamma_f64(a - c + 1.0);
let gamma_b_minus_c_plus_1 = gamma_f64(b - c + 1.0);
let c21 = gamma_2_minus_c / (gamma_1_minus_a * gamma_1_minus_b);
let c22 = gamma_2_minus_c / (gamma_a_minus_c_plus_1 * gamma_b_minus_c_plus_1);
let matrix = mat2x2(c11, c12, c21, c22);
Ok(ConnectionFormula {
from_basis: format!("2F1(a={a},b={b};c={c};z) near z=0"),
to_basis: format!("2F1 near z=1 (Gauss)"),
matrix,
valid_range: Some((-1.0, 1.0)), })
}
pub fn legendre_pq_connection(n: u32) -> ConnectionFormula {
let _ = n; let matrix = mat2x2(1.0, 0.0, 0.0, 1.0); ConnectionFormula {
from_basis: format!("(P_{n}, Q_{n}) [Legendre basis]"),
to_basis: format!("(P_{n}, Q_{n}) [Wronskian normalised]"),
matrix,
valid_range: Some((-1.0, 1.0)), }
}
pub fn kummer_connection(a: f64, b: f64) -> Result<ConnectionFormula, ConnectionError> {
for (name, val) in [("b", b), ("a", a), ("b-a", b - a)] {
if val <= 0.0 && (val - val.round()).abs() < 1e-10 {
return Err(ConnectionError::Singular(format!(
"{} = {} is a non-positive integer; Kummer connection is degenerate",
name, val
)));
}
}
let gamma_b = gamma_f64(b);
let gamma_b_minus_a = gamma_f64(b - a);
let gamma_a = gamma_f64(a);
let c00 = gamma_b / gamma_b_minus_a;
let c01 = gamma_b / gamma_a;
let sin_pi_b = (PI * b).sin();
if sin_pi_b.abs() < 1e-12 {
return Err(ConnectionError::Singular(format!(
"sin(pi*b) = 0 for b = {}; Kummer connection is degenerate",
b
)));
}
let c10 = PI / (sin_pi_b * gamma_b_minus_a * gamma_a);
let c11 = -PI / (sin_pi_b * gamma_b);
let matrix = mat2x2(c00, c01, c10, c11);
Ok(ConnectionFormula {
from_basis: format!("(M({a};{b};z), z^{{1-{b}}}*M({}-{a};{};z))", b, 2.0 - b),
to_basis: format!("(U({a};{b};z), z^{{1-{b}}}*U({}-{a};{};z))", b, 2.0 - b),
matrix,
valid_range: Some((f64::NEG_INFINITY, f64::INFINITY)),
})
}
pub fn list_connections(function_family: &str) -> Vec<String> {
match function_family {
"bessel" => vec![
"J_nu -> Y_nu (standard, non-integer nu)".to_string(),
"J_nu -> H_nu^(1) and H_nu^(2) (Hankel first and second kind)".to_string(),
"J_nu -> I_nu (modified Bessel, imaginary argument)".to_string(),
],
"legendre" => vec![
"P_n -> Q_n (second kind, Wronskian identity)".to_string(),
"P_n^m -> P_n^{-m} (associated Legendre, sign convention)".to_string(),
],
"hypergeometric" => vec![
"2F1(a,b;c;z) -> 2F1(a,b;a+b-c+1;1-z) (Gauss z=0 to z=1)".to_string(),
"2F1(z) -> z^{-a} 2F1(a, a-c+1; a-b+1; 1/z) (Pfaff-Euler)".to_string(),
"2F1(z) -> (1-z)^{-a} 2F1(a, c-b; c; z/(z-1)) (Kummer transformation)".to_string(),
],
"kummer" => vec![
"M(a;b;z) = e^z M(b-a;b;-z) (Kummer symmetry)".to_string(),
"M(a;b;z) -> (U(a;b;z), z^{1-b}*U(b-a;2-b;z)) (Tricomi U)".to_string(),
],
"airy" => vec![
"Ai(z) -> Bi(z) (second solution)".to_string(),
"Ai(z) -> Ai(z e^{2pi i/3}) (asymptotic sector rotation)".to_string(),
],
"parabolic" => vec![
"D_nu(z) -> D_{-nu-1}(iz) (rotation by pi/2)".to_string(),
"D_nu(z) -> D_nu(-z) (reflection symmetry)".to_string(),
],
_ => vec![],
}
}
fn gamma_f64(x: f64) -> f64 {
crate::gamma::gamma(x)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_list_connections_bessel() {
let connections = list_connections("bessel");
assert_eq!(connections.len(), 3, "expected 3 Bessel connections");
assert!(connections[0].contains("J_nu -> Y_nu"));
}
#[test]
fn test_list_connections_hypergeometric() {
let connections = list_connections("hypergeometric");
assert_eq!(
connections.len(),
3,
"expected 3 hypergeometric connections"
);
}
#[test]
fn test_list_connections_kummer() {
let connections = list_connections("kummer");
assert!(!connections.is_empty());
assert!(connections.iter().any(|s| s.contains("Kummer")));
}
#[test]
fn test_list_connections_unknown() {
let connections = list_connections("nonexistent_family");
assert!(connections.is_empty());
}
#[test]
fn test_bessel_j_to_y_connection_identity() {
let nu = 0.5_f64;
let cf = bessel_j_to_y_connection(nu).expect("non-integer nu should succeed");
assert_eq!(cf.matrix.len(), 2);
assert!((cf.matrix[0][0].re - 1.0).abs() < 1e-12);
assert!(cf.matrix[0][1].re.abs() < 1e-12);
let cos_val = (nu * PI).cos();
let sin_val = (nu * PI).sin();
assert!((cf.matrix[1][0].re - cos_val).abs() < 1e-12);
assert!((cf.matrix[1][1].re + sin_val).abs() < 1e-12);
}
#[test]
fn test_bessel_j_to_y_integer_nu_error() {
assert!(bessel_j_to_y_connection(2.0).is_err());
assert!(bessel_j_to_y_connection(0.0).is_err());
}
#[test]
fn test_bessel_j_to_hankel_connection() {
let cf = bessel_j_to_hankel_connection();
assert!((cf.matrix[0][0].re - 1.0).abs() < 1e-12);
assert!((cf.matrix[0][1].im - 1.0).abs() < 1e-12);
assert!((cf.matrix[1][0].re - 1.0).abs() < 1e-12);
assert!((cf.matrix[1][1].im + 1.0).abs() < 1e-12);
}
#[test]
fn test_hypergeometric_connection() {
let cf = hypergeometric_z0_to_z1_connection(0.5, 0.5, 1.5).expect("regular parameters");
assert_eq!(cf.matrix.len(), 2);
for row in &cf.matrix {
for entry in row {
assert!(entry.re.is_finite(), "matrix entry should be finite");
}
}
}
#[test]
fn test_hypergeometric_connection_degenerate() {
let err = hypergeometric_z0_to_z1_connection(1.0, 1.0, 0.0);
assert!(err.is_err());
}
#[test]
fn test_legendre_pq_connection() {
let cf = legendre_pq_connection(3);
assert_eq!(cf.matrix.len(), 2);
assert!((cf.matrix[0][0].re - 1.0).abs() < 1e-12);
assert!(cf.matrix[0][1].re.abs() < 1e-12);
assert!(cf.matrix[1][0].re.abs() < 1e-12);
assert!((cf.matrix[1][1].re - 1.0).abs() < 1e-12);
assert!(cf.is_valid_at(0.0));
assert!(!cf.is_valid_at(2.0));
}
#[test]
fn test_kummer_connection() {
let cf = kummer_connection(1.5, 2.5).expect("valid a,b");
assert_eq!(cf.matrix.len(), 2);
for row in &cf.matrix {
for entry in row {
assert!(entry.re.is_finite(), "Kummer matrix entry should be finite");
}
}
}
#[test]
fn test_kummer_connection_degenerate() {
let err = kummer_connection(1.0, -1.0);
assert!(err.is_err());
}
#[test]
fn test_apply_identity_matrix() {
let cf = legendre_pq_connection(2);
let coeffs = vec![Complex64::new(3.0, 0.0), Complex64::new(-2.0, 0.0)];
let result = cf.apply(&coeffs).expect("application should succeed");
assert_eq!(result.len(), 2);
assert!((result[0].re - 3.0).abs() < 1e-12);
assert!((result[1].re + 2.0).abs() < 1e-12);
}
#[test]
fn test_apply_dimension_mismatch() {
let cf = legendre_pq_connection(2); let short_coeffs = vec![Complex64::new(1.0, 0.0)]; assert!(cf.apply(&short_coeffs).is_err());
}
#[test]
fn test_bessel_modified_connection_phases() {
let nu = 1.0_f64;
let cf = bessel_j_to_modified_connection(nu);
let expected_phase_neg = Complex64::new(0.0, -nu * PI / 2.0).exp();
let expected_phase_pos = Complex64::new(0.0, nu * PI / 2.0).exp();
assert!((cf.matrix[0][0] - expected_phase_neg).norm() < 1e-12);
assert!((cf.matrix[1][1] - expected_phase_pos).norm() < 1e-12);
assert!(cf.matrix[0][1].norm() < 1e-12);
assert!(cf.matrix[1][0].norm() < 1e-12);
}
}