Skip to main content

scirs2_special/
connection_formulas.rs

1//! Connection formula generator for special functions.
2//!
3//! Connection formulas express one basis of solutions of a differential
4//! equation in terms of another, enabling analytic continuation and
5//! uniform evaluation across different parameter regimes.
6//!
7//! This module provides:
8//! - A generic `ConnectionFormula` type with a connection matrix
9//! - Factory functions for standard connections (Bessel, hypergeometric,
10//!   Legendre, Kummer)
11//! - A catalogue of all known formula names for each function family
12//!
13//! # Mathematical background
14//!
15//! If `f_A` and `f_B` are two bases of solutions to the same ODE, the
16//! connection formula is a matrix `C` (possibly depending on parameters)
17//! such that `f_A = C * f_B` (as column vectors).
18//!
19//! # References
20//! - Abramowitz & Stegun, chapters 9, 13, 15
21//! - Olver, "Asymptotics and Special Functions"
22//! - DLMF, <https://dlmf.nist.gov/>
23
24use scirs2_core::numeric::Complex64;
25use std::f64::consts::PI;
26
27/// Errors arising from connection formula operations.
28#[derive(Debug, thiserror::Error)]
29pub enum ConnectionError {
30    /// A singular or degenerate connection (e.g., sin(nu*pi) = 0).
31    #[error("Singular connection: {0}")]
32    Singular(String),
33    /// Parameter out of valid domain for this formula.
34    #[error("Domain mismatch: {0}")]
35    DomainMismatch(String),
36    /// The coefficient array has wrong length for the connection matrix.
37    #[error("Dimension mismatch: matrix is {rows}x{cols}, coefficients have length {given}")]
38    DimensionMismatch {
39        rows: usize,
40        cols: usize,
41        given: usize,
42    },
43}
44
45// ─────────────────────────────────────────────────────────────────────────────
46// ConnectionFormula type
47// ─────────────────────────────────────────────────────────────────────────────
48
49/// A connection formula: expresses basis A in terms of basis B.
50///
51/// If `b = (b_1, ..., b_n)^T` are the coefficients in basis B and
52/// `a = (a_1, ..., a_n)^T` in basis A, then `a = matrix * b`.
53#[derive(Debug, Clone)]
54pub struct ConnectionFormula {
55    /// Human-readable label for the source basis.
56    pub from_basis: String,
57    /// Human-readable label for the target basis.
58    pub to_basis: String,
59    /// Connection matrix stored in row-major order.
60    pub matrix: Vec<Vec<Complex64>>,
61    /// Parameter interval `(lo, hi)` for which the formula is valid.
62    pub valid_range: Option<(f64, f64)>,
63}
64
65impl ConnectionFormula {
66    /// Apply the connection formula: compute `a = matrix * b`.
67    ///
68    /// Returns an error when `basis_b_coeffs` has a different length than the
69    /// number of columns in `matrix`.
70    pub fn apply(&self, basis_b_coeffs: &[Complex64]) -> Result<Vec<Complex64>, ConnectionError> {
71        let rows = self.matrix.len();
72        if rows == 0 {
73            return Ok(Vec::new());
74        }
75        let cols = self.matrix[0].len();
76        if basis_b_coeffs.len() != cols {
77            return Err(ConnectionError::DimensionMismatch {
78                rows,
79                cols,
80                given: basis_b_coeffs.len(),
81            });
82        }
83        let mut result = vec![Complex64::new(0.0, 0.0); rows];
84        for (i, row) in self.matrix.iter().enumerate() {
85            for (j, &entry) in row.iter().enumerate() {
86                result[i] += entry * basis_b_coeffs[j];
87            }
88        }
89        Ok(result)
90    }
91
92    /// Check whether a given parameter value falls within the valid range.
93    pub fn is_valid_at(&self, param: f64) -> bool {
94        match self.valid_range {
95            None => true,
96            Some((lo, hi)) => param >= lo && param <= hi,
97        }
98    }
99
100    /// Return the number of rows (output dimension) of the matrix.
101    pub fn rows(&self) -> usize {
102        self.matrix.len()
103    }
104
105    /// Return the number of columns (input dimension) of the matrix.
106    pub fn cols(&self) -> usize {
107        self.matrix.first().map(|r| r.len()).unwrap_or(0)
108    }
109}
110
111// ─────────────────────────────────────────────────────────────────────────────
112// Helper: build a 2×2 complex matrix from four f64 entries
113// ─────────────────────────────────────────────────────────────────────────────
114
115fn mat2x2(c00: f64, c01: f64, c10: f64, c11: f64) -> Vec<Vec<Complex64>> {
116    vec![
117        vec![Complex64::new(c00, 0.0), Complex64::new(c01, 0.0)],
118        vec![Complex64::new(c10, 0.0), Complex64::new(c11, 0.0)],
119    ]
120}
121
122fn mat2x2_c(c00: Complex64, c01: Complex64, c10: Complex64, c11: Complex64) -> Vec<Vec<Complex64>> {
123    vec![vec![c00, c01], vec![c10, c11]]
124}
125
126// ─────────────────────────────────────────────────────────────────────────────
127// Bessel connection formulas
128// ─────────────────────────────────────────────────────────────────────────────
129
130/// Generate the connection formula expressing `(J_nu, J_{-nu})` in terms of
131/// `(Y_nu, J_nu)` — equivalently, express `Y_nu` as a linear combination of
132/// `J_nu` and `J_{-nu}` for non-integer `nu`.
133///
134/// The standard relation is:
135/// `Y_nu(z) = (cos(nu*pi) * J_nu(z) - J_{-nu}(z)) / sin(nu*pi)`
136///
137/// Inverted, the connection matrix C such that
138/// `[J_nu, J_{-nu}]^T = C * [J_nu, Y_nu]^T` is:
139/// ```text
140/// C = [ 1               0           ]
141///     [ cos(nu*pi)  -sin(nu*pi)     ]
142/// ```
143///
144/// Returns an error when `nu` is an integer (formula degenerates).
145pub fn bessel_j_to_y_connection(nu: f64) -> Result<ConnectionFormula, ConnectionError> {
146    let sin_nu_pi = (nu * PI).sin();
147    if sin_nu_pi.abs() < 1e-12 {
148        return Err(ConnectionError::Singular(format!(
149            "nu = {} is an integer; J-Y Bessel connection is degenerate",
150            nu
151        )));
152    }
153    let cos_nu_pi = (nu * PI).cos();
154    // Matrix C: [J_nu, J_{-nu}] = C * [J_nu, Y_nu]
155    //   C[0,0]=1, C[0,1]=0
156    //   C[1,0]=cos(nu*pi), C[1,1]=-sin(nu*pi)
157    let matrix = mat2x2(1.0, 0.0, cos_nu_pi, -sin_nu_pi);
158    Ok(ConnectionFormula {
159        from_basis: format!("(J_{nu}, J_{{-{nu}}})"),
160        to_basis: format!("(J_{nu}, Y_{nu})"),
161        matrix,
162        valid_range: None, // valid for all non-integer nu
163    })
164}
165
166/// Generate the connection formula expressing the Hankel functions
167/// `(H^(1)_nu, H^(2)_nu)` in terms of `(J_nu, Y_nu)`.
168///
169/// ```text
170/// H^(1)_nu = J_nu + i Y_nu
171/// H^(2)_nu = J_nu - i Y_nu
172/// ```
173///
174/// Matrix: `[H^(1), H^(2)]^T = C * [J_nu, Y_nu]^T`
175/// ```text
176/// C = [ 1   i  ]
177///     [ 1  -i  ]
178/// ```
179pub fn bessel_j_to_hankel_connection() -> ConnectionFormula {
180    let i = Complex64::new(0.0, 1.0);
181    let one = Complex64::new(1.0, 0.0);
182    let matrix = mat2x2_c(one, i, one, -i);
183    ConnectionFormula {
184        from_basis: "(H^(1)_nu, H^(2)_nu)".to_string(),
185        to_basis: "(J_nu, Y_nu)".to_string(),
186        matrix,
187        valid_range: None,
188    }
189}
190
191/// Generate the connection formula expressing `(I_nu, I_{-nu})` in terms
192/// of `(J_nu, J_{-nu})` via the imaginary-argument substitution.
193///
194/// `I_nu(z) = e^{-i nu pi/2} J_nu(i z)`  →  on real axis: `I_nu(x) ~ J_nu(ix)`.
195///
196/// The connection matrix C such that
197/// `[I_nu, I_{-nu}]^T = C * [J_nu, J_{-nu}]^T` (entry-wise phase factors):
198/// ```text
199/// C = [ e^{-i nu pi/2}    0              ]
200///     [ 0                  e^{i nu pi/2} ]
201/// ```
202pub fn bessel_j_to_modified_connection(nu: f64) -> ConnectionFormula {
203    let phase_neg = Complex64::new(0.0, -nu * PI / 2.0).exp();
204    let phase_pos = Complex64::new(0.0, nu * PI / 2.0).exp();
205    let zero = Complex64::new(0.0, 0.0);
206    let matrix = mat2x2_c(phase_neg, zero, zero, phase_pos);
207    ConnectionFormula {
208        from_basis: "(I_nu, I_{-nu})".to_string(),
209        to_basis: "(J_nu, J_{-nu})".to_string(),
210        matrix,
211        valid_range: None,
212    }
213}
214
215// ─────────────────────────────────────────────────────────────────────────────
216// Hypergeometric connection formulas (Gauss, z=0 -> z=1)
217// ─────────────────────────────────────────────────────────────────────────────
218
219/// Generate the Gauss connection formula for `_2F_1(a,b;c;z)` around `z=1`.
220///
221/// Near `z=0`, the two linearly independent solutions are:
222///   - `f_1 = 2F1(a,b;c;z)`
223///   - `f_2 = z^{1-c} * 2F1(a-c+1, b-c+1; 2-c; z)`
224///
225/// Near `z=1`, using the substitution `1-z = w`, the two solutions are:
226///   - `g_1 = 2F1(a,b;a+b-c+1; 1-z)`
227///   - `g_2 = (1-z)^{c-a-b} * 2F1(c-a, c-b; c-a-b+1; 1-z)`
228///
229/// The Gauss connection formula is:
230/// ```text
231/// f_1 = [Gamma(c) Gamma(c-a-b)] / [Gamma(c-a) Gamma(c-b)] * g_1
232///       + [Gamma(c) Gamma(a+b-c)] / [Gamma(a) Gamma(b)] * g_2
233/// ```
234///
235/// Returns an error when `c - a - b` or `a + b - c` is a non-positive integer
236/// (degenerate Gamma poles).
237pub fn hypergeometric_z0_to_z1_connection(
238    a: f64,
239    b: f64,
240    c: f64,
241) -> Result<ConnectionFormula, ConnectionError> {
242    // Check non-degeneracy: c, c-a, c-b, a+b-c must not be non-positive integers.
243    for (name, val) in [
244        ("c", c),
245        ("c-a", c - a),
246        ("c-b", c - b),
247        ("a+b-c", a + b - c),
248    ] {
249        if val <= 0.0 && (val - val.round()).abs() < 1e-10 {
250            return Err(ConnectionError::Singular(format!(
251                "{} = {} is a non-positive integer; Gamma function has a pole",
252                name, val
253            )));
254        }
255    }
256
257    let gamma_c = gamma_f64(c);
258    let gamma_c_minus_a_minus_b = gamma_f64(c - a - b);
259    let gamma_c_minus_a = gamma_f64(c - a);
260    let gamma_c_minus_b = gamma_f64(c - b);
261    let gamma_a_plus_b_minus_c = gamma_f64(a + b - c);
262    let gamma_a = gamma_f64(a);
263    let gamma_b = gamma_f64(b);
264
265    // Connection coefficients.
266    let c11 = (gamma_c * gamma_c_minus_a_minus_b) / (gamma_c_minus_a * gamma_c_minus_b);
267    let c12 = (gamma_c * gamma_a_plus_b_minus_c) / (gamma_a * gamma_b);
268
269    // f_1 = c11 * g_1 + c12 * g_2
270    // f_2 is the second solution; we also need the connection for it.
271    // Using Kummer's 24 relations, the second row is:
272    // Gamma(2-c)/Gamma(1-a) / Gamma(1-b) * g1 + Gamma(2-c)/Gamma(a-c+1)/Gamma(b-c+1) * g2
273    let gamma_2_minus_c = gamma_f64(2.0 - c);
274    let gamma_1_minus_a = gamma_f64(1.0 - a);
275    let gamma_1_minus_b = gamma_f64(1.0 - b);
276    let gamma_a_minus_c_plus_1 = gamma_f64(a - c + 1.0);
277    let gamma_b_minus_c_plus_1 = gamma_f64(b - c + 1.0);
278    let c21 = gamma_2_minus_c / (gamma_1_minus_a * gamma_1_minus_b);
279    let c22 = gamma_2_minus_c / (gamma_a_minus_c_plus_1 * gamma_b_minus_c_plus_1);
280
281    let matrix = mat2x2(c11, c12, c21, c22);
282
283    Ok(ConnectionFormula {
284        from_basis: format!("2F1(a={a},b={b};c={c};z) near z=0"),
285        to_basis: format!("2F1 near z=1 (Gauss)"),
286        matrix,
287        valid_range: Some((-1.0, 1.0)), // valid for |z| < 1 and |1-z| < 1
288    })
289}
290
291// ─────────────────────────────────────────────────────────────────────────────
292// Legendre P/Q connection
293// ─────────────────────────────────────────────────────────────────────────────
294
295/// Generate the connection formula relating `Q_n(x)` to `P_n(x)`.
296///
297/// The Legendre function of the second kind satisfies:
298/// `Q_n(x) = P_n(x) * Q_0(x) - W_{n-1}(x)`
299/// where `W_{n-1}` is a polynomial of degree `n-1`.
300///
301/// For the purpose of a 2×2 connection matrix at the leading polynomial
302/// level, the standard connection between `P_n` (first kind) and `Q_n`
303/// (second kind) is encoded as the identity
304/// `[P_n, Q_n]^T = C * [P_n, Q_n]^T` with `C = I_2` (trivial: they are
305/// already a basis).
306///
307/// More usefully, the *Wronskian connection* gives
308/// `W[P_n, Q_n](x) = 1/(1 - x^2)` for `|x| < 1`.
309/// The corresponding "change of basis to Wronskian normalisation" matrix is
310/// encoded as:
311/// ```text
312/// C = [ 1   0 ]
313///     [ 0   1 ]   with det(C) = 1  and off-diagonal = Wronskian factor
314/// ```
315///
316/// For practical use in connection formula applications, the formula returned
317/// here represents the expansion of the *Wronskian identity*:
318/// `[P_n * dQ_n/dx - Q_n * dP_n/dx] = -1/(x^2-1)`.
319pub fn legendre_pq_connection(n: u32) -> ConnectionFormula {
320    // The Wronskian W[P_n, Q_n] = -1/(x^2-1).
321    // Encode the two-element basis {P_n, Q_n} and the trivial identity
322    // connection (P_n, Q_n) -> (P_n, Q_n), with the Wronskian noted in
323    // the metadata.
324    // For n=0: P_0 = 1, Q_0 = (1/2) ln((1+x)/(1-x)).
325    // For n=1: P_1 = x, Q_1 = (x/2) ln((1+x)/(1-x)) - 1.
326    // The connection matrix from {P_n, Q_n} to itself is I_2; the actual
327    // content is the identity plus the Wronskian normalisation.
328    let _ = n; // n affects only the specific polynomial, not the 2x2 structure.
329    let matrix = mat2x2(1.0, 0.0, 0.0, 1.0); // identity: trivial connection
330    ConnectionFormula {
331        from_basis: format!("(P_{n}, Q_{n}) [Legendre basis]"),
332        to_basis: format!("(P_{n}, Q_{n}) [Wronskian normalised]"),
333        matrix,
334        valid_range: Some((-1.0, 1.0)), // valid for |x| < 1 (cut plane)
335    }
336}
337
338// ─────────────────────────────────────────────────────────────────────────────
339// Kummer transformation
340// ─────────────────────────────────────────────────────────────────────────────
341
342/// Generate the Kummer transformation connection formula.
343///
344/// The Kummer transformation for the confluent hypergeometric function
345/// (Kummer's function M, also written `_1F_1`):
346///
347/// `M(a; b; z) = e^z * M(b-a; b; -z)`
348///
349/// This is a scalar identity, encoded as a 1×1 connection matrix.
350/// The "matrix" is `[e^z]` but since we cannot fix `z` at formula-generation
351/// time, we record the rule as a symbolic description with a unit matrix
352/// (the exponential factor is to be applied at evaluation time).
353///
354/// For a 2×2 version relating the two standard Kummer solutions
355/// `M(a;b;z)` and `U(a;b;z)`, the Kummer identity gives:
356/// ```text
357/// M(a;b;z) = (Gamma(b)/Gamma(b-a)) U(a;b;z) + (Gamma(b)/Gamma(a)) z^{1-b} U(b-a; 2-b; z)
358/// ```
359/// (valid for Re(b) > 0, b not a non-positive integer).
360pub fn kummer_connection(a: f64, b: f64) -> Result<ConnectionFormula, ConnectionError> {
361    // Validate that Gamma(b), Gamma(a), Gamma(b-a) are finite.
362    for (name, val) in [("b", b), ("a", a), ("b-a", b - a)] {
363        if val <= 0.0 && (val - val.round()).abs() < 1e-10 {
364            return Err(ConnectionError::Singular(format!(
365                "{} = {} is a non-positive integer; Kummer connection is degenerate",
366                name, val
367            )));
368        }
369    }
370    let gamma_b = gamma_f64(b);
371    let gamma_b_minus_a = gamma_f64(b - a);
372    let gamma_a = gamma_f64(a);
373    // Connection: [M(a;b;z), z^{1-b}*M(b-a;2-b;z)] = C * [U(a;b;z), z^{1-b}*U(b-a;2-b;z)]
374    let c00 = gamma_b / gamma_b_minus_a;
375    let c01 = gamma_b / gamma_a;
376    // The second row uses the standard connection for U:
377    // U(a;b;z) = (pi/sin(pi*b)) * [M(a;b;z)/Gamma(b-a+1)Gamma(a) - z^{1-b}M(b-a;2-b;z)/Gamma(b)]
378    // Simplified 2nd row using the Wronskian W[M, U] = -Gamma(b)/Gamma(a) * z^{-b} * e^z:
379    let sin_pi_b = (PI * b).sin();
380    if sin_pi_b.abs() < 1e-12 {
381        return Err(ConnectionError::Singular(format!(
382            "sin(pi*b) = 0 for b = {}; Kummer connection is degenerate",
383            b
384        )));
385    }
386    let c10 = PI / (sin_pi_b * gamma_b_minus_a * gamma_a);
387    let c11 = -PI / (sin_pi_b * gamma_b);
388    let matrix = mat2x2(c00, c01, c10, c11);
389    Ok(ConnectionFormula {
390        from_basis: format!("(M({a};{b};z), z^{{1-{b}}}*M({}-{a};{};z))", b, 2.0 - b),
391        to_basis: format!("(U({a};{b};z), z^{{1-{b}}}*U({}-{a};{};z))", b, 2.0 - b),
392        matrix,
393        valid_range: Some((f64::NEG_INFINITY, f64::INFINITY)),
394    })
395}
396
397// ─────────────────────────────────────────────────────────────────────────────
398// Catalogue
399// ─────────────────────────────────────────────────────────────────────────────
400
401/// List all known connection formulas for a named function family.
402///
403/// Returns a `Vec<String>` of human-readable formula names.
404pub fn list_connections(function_family: &str) -> Vec<String> {
405    match function_family {
406        "bessel" => vec![
407            "J_nu -> Y_nu (standard, non-integer nu)".to_string(),
408            "J_nu -> H_nu^(1) and H_nu^(2) (Hankel first and second kind)".to_string(),
409            "J_nu -> I_nu (modified Bessel, imaginary argument)".to_string(),
410        ],
411        "legendre" => vec![
412            "P_n -> Q_n (second kind, Wronskian identity)".to_string(),
413            "P_n^m -> P_n^{-m} (associated Legendre, sign convention)".to_string(),
414        ],
415        "hypergeometric" => vec![
416            "2F1(a,b;c;z) -> 2F1(a,b;a+b-c+1;1-z) (Gauss z=0 to z=1)".to_string(),
417            "2F1(z) -> z^{-a} 2F1(a, a-c+1; a-b+1; 1/z) (Pfaff-Euler)".to_string(),
418            "2F1(z) -> (1-z)^{-a} 2F1(a, c-b; c; z/(z-1)) (Kummer transformation)".to_string(),
419        ],
420        "kummer" => vec![
421            "M(a;b;z) = e^z M(b-a;b;-z) (Kummer symmetry)".to_string(),
422            "M(a;b;z) -> (U(a;b;z), z^{1-b}*U(b-a;2-b;z)) (Tricomi U)".to_string(),
423        ],
424        "airy" => vec![
425            "Ai(z) -> Bi(z) (second solution)".to_string(),
426            "Ai(z) -> Ai(z e^{2pi i/3}) (asymptotic sector rotation)".to_string(),
427        ],
428        "parabolic" => vec![
429            "D_nu(z) -> D_{-nu-1}(iz) (rotation by pi/2)".to_string(),
430            "D_nu(z) -> D_nu(-z) (reflection symmetry)".to_string(),
431        ],
432        _ => vec![],
433    }
434}
435
436// ─────────────────────────────────────────────────────────────────────────────
437// Gamma function helper (forward to scirs2-special gamma module)
438// ─────────────────────────────────────────────────────────────────────────────
439
440/// Gamma function evaluated at a real argument via the crate's gamma module.
441fn gamma_f64(x: f64) -> f64 {
442    crate::gamma::gamma(x)
443}
444
445// ─────────────────────────────────────────────────────────────────────────────
446// Tests
447// ─────────────────────────────────────────────────────────────────────────────
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn test_list_connections_bessel() {
455        let connections = list_connections("bessel");
456        assert_eq!(connections.len(), 3, "expected 3 Bessel connections");
457        assert!(connections[0].contains("J_nu -> Y_nu"));
458    }
459
460    #[test]
461    fn test_list_connections_hypergeometric() {
462        let connections = list_connections("hypergeometric");
463        assert_eq!(
464            connections.len(),
465            3,
466            "expected 3 hypergeometric connections"
467        );
468    }
469
470    #[test]
471    fn test_list_connections_kummer() {
472        let connections = list_connections("kummer");
473        assert!(!connections.is_empty());
474        assert!(connections.iter().any(|s| s.contains("Kummer")));
475    }
476
477    #[test]
478    fn test_list_connections_unknown() {
479        let connections = list_connections("nonexistent_family");
480        assert!(connections.is_empty());
481    }
482
483    #[test]
484    fn test_bessel_j_to_y_connection_identity() {
485        // For nu = 0.5, construct the connection matrix and verify its
486        // determinant equals -sin(nu*pi) (from the 2x2 structure).
487        let nu = 0.5_f64;
488        let cf = bessel_j_to_y_connection(nu).expect("non-integer nu should succeed");
489        assert_eq!(cf.matrix.len(), 2);
490        // C[0,0] = 1, C[0,1] = 0.
491        assert!((cf.matrix[0][0].re - 1.0).abs() < 1e-12);
492        assert!(cf.matrix[0][1].re.abs() < 1e-12);
493        // C[1,0] = cos(0.5 pi) = 0, C[1,1] = -sin(0.5 pi) = -1.
494        let cos_val = (nu * PI).cos();
495        let sin_val = (nu * PI).sin();
496        assert!((cf.matrix[1][0].re - cos_val).abs() < 1e-12);
497        assert!((cf.matrix[1][1].re + sin_val).abs() < 1e-12);
498    }
499
500    #[test]
501    fn test_bessel_j_to_y_integer_nu_error() {
502        // Integer nu should return a Singular error.
503        assert!(bessel_j_to_y_connection(2.0).is_err());
504        assert!(bessel_j_to_y_connection(0.0).is_err());
505    }
506
507    #[test]
508    fn test_bessel_j_to_hankel_connection() {
509        let cf = bessel_j_to_hankel_connection();
510        // H^(1) = J + iY: row 0 = [1, i]
511        assert!((cf.matrix[0][0].re - 1.0).abs() < 1e-12);
512        assert!((cf.matrix[0][1].im - 1.0).abs() < 1e-12);
513        // H^(2) = J - iY: row 1 = [1, -i]
514        assert!((cf.matrix[1][0].re - 1.0).abs() < 1e-12);
515        assert!((cf.matrix[1][1].im + 1.0).abs() < 1e-12);
516    }
517
518    #[test]
519    fn test_hypergeometric_connection() {
520        // a=0.5, b=0.5, c=1.5: Gauss connection should succeed.
521        // a+b-c = 0.5+0.5-1.5 = -0.5, c-a = 1.0, c-b = 1.0, c-a-b = 0.5 — all nonzero.
522        let cf = hypergeometric_z0_to_z1_connection(0.5, 0.5, 1.5).expect("regular parameters");
523        assert_eq!(cf.matrix.len(), 2);
524        // Check that the matrix entries are finite.
525        for row in &cf.matrix {
526            for entry in row {
527                assert!(entry.re.is_finite(), "matrix entry should be finite");
528            }
529        }
530    }
531
532    #[test]
533    fn test_hypergeometric_connection_degenerate() {
534        // c = 0 is a non-positive integer -> should fail.
535        let err = hypergeometric_z0_to_z1_connection(1.0, 1.0, 0.0);
536        assert!(err.is_err());
537    }
538
539    #[test]
540    fn test_legendre_pq_connection() {
541        let cf = legendre_pq_connection(3);
542        assert_eq!(cf.matrix.len(), 2);
543        // Identity matrix.
544        assert!((cf.matrix[0][0].re - 1.0).abs() < 1e-12);
545        assert!(cf.matrix[0][1].re.abs() < 1e-12);
546        assert!(cf.matrix[1][0].re.abs() < 1e-12);
547        assert!((cf.matrix[1][1].re - 1.0).abs() < 1e-12);
548        // Valid range should be (-1, 1).
549        assert!(cf.is_valid_at(0.0));
550        assert!(!cf.is_valid_at(2.0));
551    }
552
553    #[test]
554    fn test_kummer_connection() {
555        // a=1.5, b=2.5: standard Kummer connection (non-integer b avoids sin(pi*b)=0).
556        let cf = kummer_connection(1.5, 2.5).expect("valid a,b");
557        assert_eq!(cf.matrix.len(), 2);
558        for row in &cf.matrix {
559            for entry in row {
560                assert!(entry.re.is_finite(), "Kummer matrix entry should be finite");
561            }
562        }
563    }
564
565    #[test]
566    fn test_kummer_connection_degenerate() {
567        // b = -1 is a non-positive integer -> should fail.
568        let err = kummer_connection(1.0, -1.0);
569        assert!(err.is_err());
570    }
571
572    #[test]
573    fn test_apply_identity_matrix() {
574        let cf = legendre_pq_connection(2);
575        let coeffs = vec![Complex64::new(3.0, 0.0), Complex64::new(-2.0, 0.0)];
576        let result = cf.apply(&coeffs).expect("application should succeed");
577        assert_eq!(result.len(), 2);
578        // Identity matrix: result should equal coeffs.
579        assert!((result[0].re - 3.0).abs() < 1e-12);
580        assert!((result[1].re + 2.0).abs() < 1e-12);
581    }
582
583    #[test]
584    fn test_apply_dimension_mismatch() {
585        let cf = legendre_pq_connection(2); // 2x2 matrix
586        let short_coeffs = vec![Complex64::new(1.0, 0.0)]; // only 1 element
587        assert!(cf.apply(&short_coeffs).is_err());
588    }
589
590    #[test]
591    fn test_bessel_modified_connection_phases() {
592        let nu = 1.0_f64;
593        let cf = bessel_j_to_modified_connection(nu);
594        // C[0,0] = e^{-i nu pi/2} = e^{-i pi/2} = -i
595        let expected_phase_neg = Complex64::new(0.0, -nu * PI / 2.0).exp();
596        let expected_phase_pos = Complex64::new(0.0, nu * PI / 2.0).exp();
597        assert!((cf.matrix[0][0] - expected_phase_neg).norm() < 1e-12);
598        assert!((cf.matrix[1][1] - expected_phase_pos).norm() < 1e-12);
599        assert!(cf.matrix[0][1].norm() < 1e-12);
600        assert!(cf.matrix[1][0].norm() < 1e-12);
601    }
602}