Skip to main content

scirs2_linalg/matrix_functions/
interpolation.rs

1//! Matrix function interpolation via Krylov subspace methods
2//!
3//! This module provides algorithms for computing matrix function actions f(A)v
4//! without forming f(A) explicitly, by projecting onto a Krylov subspace and
5//! approximating f on the small projected matrix.
6//!
7//! ## Algorithms
8//!
9//! - **ArnoldiIteration**: Builds an orthonormal Krylov basis for general matrices
10//! - **LanczosIteration**: Builds a tridiagonal Krylov basis for symmetric matrices
11//! - **MatrixFunctionInterpolation**: General f(A)v via rational Krylov
12//! - **MatrixExpKrylov**: exp(t*A)v via Expokit Krylov approach (Sidje 1998)
13//!
14//! ## Mathematical Foundation
15//!
16//! For computing f(A)v, we build the Krylov subspace K_m(A, v) spanned by
17//! {v, Av, A²v, ..., A^{m-1}v}. The Arnoldi relation is:
18//!
19//!   A V_m = V_m H_m + h_{m+1,m} v_{m+1} e_m^T
20//!
21//! where V_m is orthonormal and H_m is m×m upper Hessenberg.
22//! The approximation is then:
23//!
24//!   f(A)v ≈ ‖v‖ V_m f(H_m) e_1
25//!
26//! ## References
27//!
28//! - Sidje, R. B. (1998). "Expokit: A Software Package for Computing Matrix Exponentials"
29//! - Hochbruck & Lubich (1997). "On Krylov Subspace Approximations to the Matrix Exponential Operator"
30//! - Güttel (2013). "Rational Krylov approximation of matrix functions: Numerical methods and optimal pole selection"
31
32use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
33use scirs2_core::numeric::{Float, NumAssign, One, Zero};
34use std::fmt::Debug;
35use std::iter::Sum;
36
37use crate::error::{LinalgError, LinalgResult};
38
39// ============================================================================
40// MatrixFunctionParams
41// ============================================================================
42
43/// Function type for matrix function computation
44#[derive(Debug, Clone, Copy, PartialEq)]
45pub enum MatrixFunctionType {
46    /// Matrix exponential: f(A) = exp(A)
47    Exponential,
48    /// Matrix square root: f(A) = sqrt(A)
49    SquareRoot,
50    /// Matrix logarithm: f(A) = log(A)
51    Logarithm,
52    /// Matrix sign function: f(A) = sign(A)
53    Sign,
54    /// Exponential with scalar: f(A) = exp(t * A)
55    ExponentialScaled(f64),
56    /// Cosine: f(A) = cos(A)
57    Cosine,
58    /// Sine: f(A) = sin(A)
59    Sine,
60}
61
62/// Parameters for Krylov-based matrix function approximation
63#[derive(Debug, Clone)]
64pub struct MatrixFunctionParams {
65    /// Function to apply
66    pub function: MatrixFunctionType,
67    /// Convergence tolerance
68    pub tol: f64,
69    /// Maximum Krylov subspace dimension
70    pub max_krylov_dim: usize,
71    /// Whether to use Lanczos (for symmetric A) instead of Arnoldi
72    pub use_lanczos: bool,
73    /// Restart threshold: restart Arnoldi when Krylov dim reaches this value
74    pub restart_dim: Option<usize>,
75}
76
77impl Default for MatrixFunctionParams {
78    fn default() -> Self {
79        Self {
80            function: MatrixFunctionType::Exponential,
81            tol: 1e-10,
82            max_krylov_dim: 50,
83            use_lanczos: false,
84            restart_dim: None,
85        }
86    }
87}
88
89impl MatrixFunctionParams {
90    /// Create new params with specified function type
91    pub fn new(function: MatrixFunctionType) -> Self {
92        Self {
93            function,
94            ..Self::default()
95        }
96    }
97
98    /// Set tolerance
99    pub fn with_tol(mut self, tol: f64) -> Self {
100        self.tol = tol;
101        self
102    }
103
104    /// Set max Krylov dimension
105    pub fn with_max_krylov_dim(mut self, dim: usize) -> Self {
106        self.max_krylov_dim = dim;
107        self
108    }
109
110    /// Use Lanczos iteration (for symmetric matrices)
111    pub fn with_lanczos(mut self) -> Self {
112        self.use_lanczos = true;
113        self
114    }
115
116    /// Set restart threshold
117    pub fn with_restart(mut self, restart_dim: usize) -> Self {
118        self.restart_dim = Some(restart_dim);
119        self
120    }
121}
122
123// ============================================================================
124// ArnoldiIteration
125// ============================================================================
126
127/// Result of the Arnoldi iteration
128#[derive(Debug, Clone)]
129pub struct ArnoldiResult {
130    /// Orthonormal Krylov basis columns V_m (shape n × m)
131    pub v: Array2<f64>,
132    /// Upper Hessenberg matrix H_m (shape (m+1) × m)
133    pub h: Array2<f64>,
134    /// Actual number of Krylov vectors computed
135    pub m: usize,
136    /// Whether the Krylov subspace is "happy" (invariant subspace found)
137    pub happy_breakdown: bool,
138    /// Norm of the residual h_{m+1, m}
139    pub residual_norm: f64,
140}
141
142/// Arnoldi iteration for building a Krylov basis of a general matrix.
143///
144/// Produces an orthonormal basis V = [v₁, ..., v_m] for the Krylov subspace
145/// K_m(A, v) and an upper Hessenberg matrix H such that:
146///   A V_m = V_{m+1} H̄_m
147/// where H̄_m is the (m+1) × m unreduced Hessenberg matrix.
148pub struct ArnoldiIteration;
149
150impl ArnoldiIteration {
151    /// Run the Arnoldi iteration.
152    ///
153    /// # Arguments
154    ///
155    /// * `a` - Square matrix A (n × n)
156    /// * `v0` - Starting vector (length n), will be normalized
157    /// * `max_dim` - Maximum Krylov dimension m
158    /// * `tol` - Breakdown tolerance (default: 1e-14)
159    pub fn run(
160        a: &ArrayView2<f64>,
161        v0: &ArrayView1<f64>,
162        max_dim: usize,
163        tol: Option<f64>,
164    ) -> LinalgResult<ArnoldiResult> {
165        let n = a.nrows();
166        if a.ncols() != n {
167            return Err(LinalgError::ShapeError(
168                "ArnoldiIteration: A must be square".to_string(),
169            ));
170        }
171        if v0.len() != n {
172            return Err(LinalgError::DimensionError(format!(
173                "ArnoldiIteration: v0 has length {} but A is {}×{}",
174                v0.len(),
175                n,
176                n
177            )));
178        }
179        if max_dim == 0 {
180            return Err(LinalgError::ShapeError(
181                "ArnoldiIteration: max_dim must be positive".to_string(),
182            ));
183        }
184
185        let tolerance = tol.unwrap_or(1e-14);
186        let m = max_dim.min(n);
187
188        // Normalize starting vector
189        let v0_norm = v0.iter().map(|&v| v * v).sum::<f64>().sqrt();
190        if v0_norm < tolerance {
191            return Err(LinalgError::InvalidInputError(
192                "ArnoldiIteration: starting vector is zero".to_string(),
193            ));
194        }
195
196        // V: n × (m+1) to hold all basis vectors
197        let mut v_mat = Array2::zeros((n, m + 1));
198        for i in 0..n {
199            v_mat[[i, 0]] = v0[i] / v0_norm;
200        }
201
202        // H: (m+1) × m Hessenberg
203        let mut h_mat = Array2::zeros((m + 1, m));
204
205        let mut actual_m = 0;
206        let mut happy = false;
207        let mut res_norm = 0.0;
208
209        for j in 0..m {
210            // w = A * v_j
211            let mut w = Array1::zeros(n);
212            for i in 0..n {
213                let mut val = 0.0f64;
214                for k in 0..n {
215                    val += a[[i, k]] * v_mat[[k, j]];
216                }
217                w[i] = val;
218            }
219
220            // Modified Gram-Schmidt orthogonalization
221            for i in 0..=j {
222                let h_ij = (0..n).map(|k| w[k] * v_mat[[k, i]]).sum::<f64>();
223                h_mat[[i, j]] = h_ij;
224                for k in 0..n {
225                    w[k] -= h_ij * v_mat[[k, i]];
226                }
227            }
228
229            // Re-orthogonalize (twice) for numerical stability
230            for i in 0..=j {
231                let correction = (0..n).map(|k| w[k] * v_mat[[k, i]]).sum::<f64>();
232                h_mat[[i, j]] += correction;
233                for k in 0..n {
234                    w[k] -= correction * v_mat[[k, i]];
235                }
236            }
237
238            let h_next = w.iter().map(|&v| v * v).sum::<f64>().sqrt();
239            h_mat[[j + 1, j]] = h_next;
240            actual_m = j + 1;
241
242            if h_next < tolerance {
243                // Happy breakdown: found exact invariant subspace
244                happy = true;
245                res_norm = 0.0;
246                break;
247            }
248
249            res_norm = h_next;
250
251            if j + 1 < m {
252                for k in 0..n {
253                    v_mat[[k, j + 1]] = w[k] / h_next;
254                }
255            }
256        }
257
258        // Truncate V to n × actual_m
259        let v_out = v_mat
260            .slice(scirs2_core::ndarray::s![.., ..actual_m])
261            .to_owned();
262        let h_out = h_mat
263            .slice(scirs2_core::ndarray::s![..actual_m + 1, ..actual_m])
264            .to_owned();
265
266        Ok(ArnoldiResult {
267            v: v_out,
268            h: h_out,
269            m: actual_m,
270            happy_breakdown: happy,
271            residual_norm: res_norm,
272        })
273    }
274}
275
276// ============================================================================
277// LanczosIteration
278// ============================================================================
279
280/// Result of the Lanczos iteration
281#[derive(Debug, Clone)]
282pub struct LanczosResult {
283    /// Orthonormal Krylov basis V_m (shape n × m)
284    pub v: Array2<f64>,
285    /// Diagonal elements alpha[0..m] of the tridiagonal matrix
286    pub alpha: Array1<f64>,
287    /// Off-diagonal elements beta\[0..m-1\] (beta\[j\] = T\[j+1, j\])
288    pub beta: Array1<f64>,
289    /// Actual number of Krylov vectors computed
290    pub m: usize,
291    /// Whether Lanczos broke down (invariant subspace found)
292    pub breakdown: bool,
293}
294
295/// Lanczos iteration for symmetric matrices.
296///
297/// Builds an orthonormal basis V_m and a symmetric tridiagonal matrix T_m such that:
298///   A V_m = V_m T_m + β_m v_{m+1} e_m^T
299///
300/// For symmetric A this is a three-term recurrence, making it significantly
301/// more efficient than the general Arnoldi process.
302pub struct LanczosIteration;
303
304impl LanczosIteration {
305    /// Run the Lanczos iteration.
306    ///
307    /// # Arguments
308    ///
309    /// * `a` - Symmetric matrix A (n × n)
310    /// * `v0` - Starting vector (length n), will be normalized
311    /// * `max_dim` - Maximum Krylov dimension
312    /// * `tol` - Breakdown tolerance
313    pub fn run(
314        a: &ArrayView2<f64>,
315        v0: &ArrayView1<f64>,
316        max_dim: usize,
317        tol: Option<f64>,
318    ) -> LinalgResult<LanczosResult> {
319        let n = a.nrows();
320        if a.ncols() != n {
321            return Err(LinalgError::ShapeError(
322                "LanczosIteration: A must be square".to_string(),
323            ));
324        }
325        if v0.len() != n {
326            return Err(LinalgError::DimensionError(format!(
327                "LanczosIteration: v0 has length {} but n={}",
328                v0.len(),
329                n
330            )));
331        }
332
333        let tolerance = tol.unwrap_or(1e-14);
334        let m = max_dim.min(n);
335
336        let v0_norm = v0.iter().map(|&v| v * v).sum::<f64>().sqrt();
337        if v0_norm < tolerance {
338            return Err(LinalgError::InvalidInputError(
339                "LanczosIteration: starting vector is zero".to_string(),
340            ));
341        }
342
343        let mut v_mat = Array2::zeros((n, m + 1));
344        for i in 0..n {
345            v_mat[[i, 0]] = v0[i] / v0_norm;
346        }
347
348        let mut alpha_vec = vec![0.0f64; m];
349        let mut beta_vec = vec![0.0f64; m];
350
351        let mut beta_prev = 0.0f64;
352        let mut actual_m = 0;
353        let mut breakdown = false;
354
355        for j in 0..m {
356            // w = A * v_j - beta_{j-1} * v_{j-1}
357            let mut w = Array1::zeros(n);
358            for i in 0..n {
359                let mut val = 0.0f64;
360                for k in 0..n {
361                    val += a[[i, k]] * v_mat[[k, j]];
362                }
363                w[i] = val;
364            }
365
366            if j > 0 {
367                for k in 0..n {
368                    w[k] -= beta_prev * v_mat[[k, j - 1]];
369                }
370            }
371
372            // alpha_j = <v_j, w>
373            let alpha_j = (0..n).map(|k| v_mat[[k, j]] * w[k]).sum::<f64>();
374            alpha_vec[j] = alpha_j;
375
376            // w -= alpha_j * v_j
377            for k in 0..n {
378                w[k] -= alpha_j * v_mat[[k, j]];
379            }
380
381            // Reorthogonalize w against all previous vectors (full reorthogonalization)
382            for i in 0..=j {
383                let corr = (0..n).map(|k| w[k] * v_mat[[k, i]]).sum::<f64>();
384                for k in 0..n {
385                    w[k] -= corr * v_mat[[k, i]];
386                }
387            }
388
389            let beta_j = w.iter().map(|&v| v * v).sum::<f64>().sqrt();
390            actual_m = j + 1;
391
392            if beta_j < tolerance {
393                breakdown = true;
394                break;
395            }
396
397            beta_vec[j] = beta_j;
398            beta_prev = beta_j;
399
400            if j + 1 < m {
401                for k in 0..n {
402                    v_mat[[k, j + 1]] = w[k] / beta_j;
403                }
404            }
405        }
406
407        let v_out = v_mat
408            .slice(scirs2_core::ndarray::s![.., ..actual_m])
409            .to_owned();
410        let alpha_out = Array1::from_vec(alpha_vec[..actual_m].to_vec());
411        let beta_out = if actual_m > 1 {
412            Array1::from_vec(beta_vec[..actual_m - 1].to_vec())
413        } else {
414            Array1::zeros(0)
415        };
416
417        Ok(LanczosResult {
418            v: v_out,
419            alpha: alpha_out,
420            beta: beta_out,
421            m: actual_m,
422            breakdown,
423        })
424    }
425
426    /// Build the m × m tridiagonal matrix T from the Lanczos result.
427    pub fn tridiagonal_matrix(result: &LanczosResult) -> Array2<f64> {
428        let m = result.m;
429        let mut t = Array2::zeros((m, m));
430
431        for i in 0..m {
432            t[[i, i]] = result.alpha[i];
433            if i + 1 < m {
434                let beta = result.beta[i];
435                t[[i, i + 1]] = beta;
436                t[[i + 1, i]] = beta;
437            }
438        }
439
440        t
441    }
442}
443
444// ============================================================================
445// MatrixFunctionInterpolation
446// ============================================================================
447
448/// Computes f(A)v via rational Krylov / Arnoldi approximation.
449///
450/// This is the primary interface for applying matrix functions to vectors
451/// without forming the full matrix function. Uses the Krylov projection:
452///   f(A)v ≈ ‖v‖₂ V_m f(H_m) e₁
453///
454/// where V_m, H_m come from Arnoldi (general) or Lanczos (symmetric).
455pub struct MatrixFunctionInterpolation;
456
457impl MatrixFunctionInterpolation {
458    /// Compute f(A) * v for a general dense matrix A.
459    ///
460    /// # Arguments
461    ///
462    /// * `a` - Square matrix A (n × n)
463    /// * `v` - Input vector (length n)
464    /// * `params` - Function parameters and tolerances
465    ///
466    /// # Returns
467    ///
468    /// * Approximation of f(A) v
469    pub fn apply(
470        a: &ArrayView2<f64>,
471        v: &ArrayView1<f64>,
472        params: &MatrixFunctionParams,
473    ) -> LinalgResult<Array1<f64>> {
474        let n = a.nrows();
475        if a.ncols() != n {
476            return Err(LinalgError::ShapeError(
477                "MatrixFunctionInterpolation: A must be square".to_string(),
478            ));
479        }
480        if v.len() != n {
481            return Err(LinalgError::DimensionError(format!(
482                "MatrixFunctionInterpolation: v has length {} but n={}",
483                v.len(),
484                n
485            )));
486        }
487
488        let v_norm = v.iter().map(|&vi| vi * vi).sum::<f64>().sqrt();
489        if v_norm < 1e-300 {
490            return Ok(Array1::zeros(n));
491        }
492
493        if params.use_lanczos {
494            Self::apply_lanczos(a, v, v_norm, params)
495        } else {
496            Self::apply_arnoldi(a, v, v_norm, params)
497        }
498    }
499
500    /// Apply using Arnoldi iteration (general matrices)
501    fn apply_arnoldi(
502        a: &ArrayView2<f64>,
503        v: &ArrayView1<f64>,
504        v_norm: f64,
505        params: &MatrixFunctionParams,
506    ) -> LinalgResult<Array1<f64>> {
507        let arnoldi_result = ArnoldiIteration::run(a, v, params.max_krylov_dim, Some(1e-14))?;
508        let m = arnoldi_result.m;
509
510        // Extract the m × m core of H (upper Hessenberg)
511        let hm = arnoldi_result
512            .h
513            .slice(scirs2_core::ndarray::s![..m, ..m])
514            .to_owned();
515
516        // Compute f(H_m) using dense matrix function
517        let fhm = apply_dense_function(&hm.view(), params)?;
518
519        // Result: v_norm * V_m * f(H_m) * e_1
520        let mut result = Array1::zeros(v.len());
521        for i in 0..v.len() {
522            let mut val = 0.0f64;
523            for j in 0..m {
524                val += arnoldi_result.v[[i, j]] * fhm[[j, 0]];
525            }
526            result[i] = v_norm * val;
527        }
528
529        Ok(result)
530    }
531
532    /// Apply using Lanczos iteration (symmetric matrices)
533    fn apply_lanczos(
534        a: &ArrayView2<f64>,
535        v: &ArrayView1<f64>,
536        v_norm: f64,
537        params: &MatrixFunctionParams,
538    ) -> LinalgResult<Array1<f64>> {
539        let lanczos_result = LanczosIteration::run(a, v, params.max_krylov_dim, Some(1e-14))?;
540        let m = lanczos_result.m;
541
542        // Build the m × m tridiagonal matrix T_m
543        let tm = LanczosIteration::tridiagonal_matrix(&lanczos_result);
544
545        // Compute f(T_m) using dense matrix function
546        let ftm = apply_dense_function(&tm.view(), params)?;
547
548        // Result: v_norm * V_m * f(T_m) * e_1
549        let mut result = Array1::zeros(v.len());
550        for i in 0..v.len() {
551            let mut val = 0.0f64;
552            for j in 0..m {
553                val += lanczos_result.v[[i, j]] * ftm[[j, 0]];
554            }
555            result[i] = v_norm * val;
556        }
557
558        Ok(result)
559    }
560}
561
562// ============================================================================
563// MatrixExpKrylov
564// ============================================================================
565
566/// Krylov subspace approximation of the matrix exponential action exp(t*A)*v.
567///
568/// Implements the Expokit approach (Sidje 1998) with adaptive step size control
569/// for the time variable t. This is particularly important for large sparse
570/// matrices where forming exp(A) directly is prohibitively expensive.
571///
572/// The method computes exp(t A) v via the Arnoldi relation and Padé-type
573/// interpolation on the small Krylov matrix, with error control based on
574/// the residual norm ‖h_{m+1,m} e_m^T V_m^T‖.
575pub struct MatrixExpKrylov;
576
577impl MatrixExpKrylov {
578    /// Compute exp(t * A) * v using Krylov subspace approximation.
579    ///
580    /// # Arguments
581    ///
582    /// * `a` - Square matrix A (n × n); need not be symmetric
583    /// * `v` - Starting vector (length n)
584    /// * `t` - Time parameter (scalar)
585    /// * `max_krylov_dim` - Maximum Krylov subspace dimension (default: min(n, 50))
586    /// * `tol` - Error tolerance (default: 1e-10)
587    ///
588    /// # Returns
589    ///
590    /// * Approximation of exp(t * A) * v
591    pub fn apply(
592        a: &ArrayView2<f64>,
593        v: &ArrayView1<f64>,
594        t: f64,
595        max_krylov_dim: Option<usize>,
596        tol: Option<f64>,
597    ) -> LinalgResult<Array1<f64>> {
598        let n = a.nrows();
599        if a.ncols() != n {
600            return Err(LinalgError::ShapeError(
601                "MatrixExpKrylov: A must be square".to_string(),
602            ));
603        }
604        if v.len() != n {
605            return Err(LinalgError::DimensionError(format!(
606                "MatrixExpKrylov: v has length {} but n={}",
607                v.len(),
608                n
609            )));
610        }
611
612        let m = max_krylov_dim.unwrap_or_else(|| 50.min(n));
613        let tolerance = tol.unwrap_or(1e-10);
614
615        let v_norm = v.iter().map(|&vi| vi * vi).sum::<f64>().sqrt();
616        if v_norm < 1e-300 {
617            return Ok(Array1::zeros(n));
618        }
619
620        if t == 0.0 {
621            return Ok(v.to_owned());
622        }
623
624        // For small problems, use the Arnoldi + direct matrix function approach
625        if n <= m + 5 {
626            let params = MatrixFunctionParams::new(MatrixFunctionType::ExponentialScaled(t))
627                .with_max_krylov_dim(n.min(m))
628                .with_tol(tolerance);
629            return MatrixFunctionInterpolation::apply(a, v, &params);
630        }
631
632        // Build the augmented (m+1) × (m+1) Krylov matrix using Arnoldi
633        let arnoldi_result = ArnoldiIteration::run(a, v, m, Some(1e-14))?;
634        let m_actual = arnoldi_result.m;
635
636        // Build the (m+1) × (m+1) augmented matrix for Expokit
637        // H_aug = [[t * H_m, t * beta * e_1], [0, 0]]
638        // We compute exp(H_aug) * e_1 via the small dense exponential
639        let h_core = arnoldi_result
640            .h
641            .slice(scirs2_core::ndarray::s![..m_actual, ..m_actual])
642            .to_owned();
643
644        // Scale H by t and compute exp(t * H_m)
645        let th = h_core.mapv(|v| v * t);
646
647        // Dense matrix exponential via Padé approximation
648        let exp_th = pade_expm(&th.view())?;
649
650        // Assemble result: v_norm * V_m * exp(t H_m) * e_1
651        let mut result = Array1::zeros(n);
652        for i in 0..n {
653            let mut val = 0.0f64;
654            for j in 0..m_actual {
655                val += arnoldi_result.v[[i, j]] * exp_th[[j, 0]];
656            }
657            result[i] = v_norm * val;
658        }
659
660        // Error estimate: v_norm * h_{m+1,m} * |exp(t H_m)[m, 0]|
661        // (last row element times the sub-diagonal entry)
662        let err_est = if m_actual >= 1 {
663            let h_residual = arnoldi_result.residual_norm;
664            let exp_last = exp_th[[m_actual - 1, 0]].abs();
665            v_norm * h_residual * exp_last * t.abs()
666        } else {
667            0.0
668        };
669
670        // If error is large, refine by increasing Krylov dimension (one restart)
671        if err_est > tolerance && m_actual < n {
672            let m2 = (m_actual * 2).min(n);
673            let params = MatrixFunctionParams::new(MatrixFunctionType::ExponentialScaled(t))
674                .with_max_krylov_dim(m2)
675                .with_tol(tolerance);
676            return MatrixFunctionInterpolation::apply(a, v, &params);
677        }
678
679        Ok(result)
680    }
681}
682
683// ============================================================================
684// Dense matrix function helpers
685// ============================================================================
686
687/// Apply a matrix function to a small dense matrix using eigendecomposition.
688///
689/// For the small Hessenberg/tridiagonal matrices arising in Krylov methods,
690/// this is practical and avoids specialized matrix function implementations.
691fn apply_dense_function(
692    h: &ArrayView2<f64>,
693    params: &MatrixFunctionParams,
694) -> LinalgResult<Array2<f64>> {
695    match params.function {
696        MatrixFunctionType::Exponential => pade_expm(h),
697        MatrixFunctionType::ExponentialScaled(t) => {
698            let ht = h.mapv(|v| v * t);
699            pade_expm(&ht.view())
700        }
701        MatrixFunctionType::SquareRoot => dense_sqrtm(h),
702        MatrixFunctionType::Logarithm => dense_logm(h),
703        MatrixFunctionType::Sign => dense_signm(h),
704        MatrixFunctionType::Cosine => dense_cosm(h),
705        MatrixFunctionType::Sine => dense_sinm(h),
706    }
707}
708
709/// Dense matrix exponential via order-[6/6] Padé approximation with scaling & squaring.
710pub fn pade_expm(a: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
711    let n = a.nrows();
712    if n == 0 {
713        return Ok(Array2::zeros((0, 0)));
714    }
715    if n == 1 {
716        let mut r = Array2::zeros((1, 1));
717        r[[0, 0]] = a[[0, 0]].exp();
718        return Ok(r);
719    }
720
721    // Compute the 1-norm of A
722    let a_norm = matrix_1norm(a);
723
724    // Scaling: choose s such that A/2^s has small 1-norm
725    let theta = 5.371920351148152; // threshold for [6/6] Padé
726    let mut s = 0i32;
727    let mut scale = 1.0f64;
728    if a_norm > theta {
729        s = (a_norm / theta).log2().ceil() as i32;
730        scale = 2.0f64.powi(-s);
731    }
732
733    // Scale A
734    let a_scaled = if scale != 1.0 {
735        a.mapv(|v| v * scale)
736    } else {
737        a.to_owned()
738    };
739
740    // Padé [6/6] coefficients
741    let c = [
742        1.0_f64,
743        0.5_f64,
744        12.0_f64.recip(),
745        120.0_f64.recip(),
746        // Padé [3/3] coefficients (scaled)
747        1.0 / 720.0,
748        1.0 / 30240.0,
749        1.0 / 1209600.0,
750    ];
751
752    let ident = Array2::eye(n);
753
754    // Compute powers of A
755    let a2 = matmul_dense(&a_scaled.view(), &a_scaled.view());
756    let a4 = matmul_dense(&a2.view(), &a2.view());
757    let a6 = matmul_dense(&a4.view(), &a2.view());
758
759    // U and V for Padé [6/6]
760    let u = &(c[6] * &a6 + c[4] * &a4 + c[2] * &a2 + c[0] * &ident);
761    let u = matmul_dense(&a_scaled.view(), &u.view());
762
763    let v = c[5] * &a6 + c[3] * &a4 + c[1] * &a2 + &ident;
764
765    // Padé approximant: R = (V - U)^{-1} (V + U)
766    let vpu = &v + &u;
767    let vmu = &v - &u;
768
769    // Solve (V - U) R = (V + U) via LU
770    let r = solve_dense(&vmu.view(), &vpu.view())?;
771
772    // Squaring: R = R^{2^s}
773    let mut result = r;
774    for _ in 0..s {
775        result = matmul_dense(&result.view(), &result.view());
776    }
777
778    Ok(result)
779}
780
781/// Dense matrix square root via Schur decomposition (simplified iterative approach).
782fn dense_sqrtm(a: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
783    let n = a.nrows();
784    // Use Denman-Beavers iteration: X_{k+1} = (X_k + Y_k^{-1}) / 2, Y_{k+1} = (Y_k + X_k^{-1}) / 2
785    let mut x = a.to_owned();
786    let mut y = Array2::eye(n);
787
788    for _ in 0..50 {
789        let x_inv = solve_dense(&x.view(), &Array2::eye(n).view())?;
790        let y_inv = solve_dense(&y.view(), &Array2::eye(n).view())?;
791
792        let x_new = 0.5 * (&x + &y_inv);
793        let y_new = 0.5 * (&y + &x_inv);
794
795        // Check convergence: ‖X_new - X‖_1
796        let diff = (&x_new - &x).mapv(|v: f64| v.abs());
797        let err = diff.sum();
798        let x_norm = x_new.mapv(|v: f64| v.abs()).sum();
799
800        x = x_new;
801        y = y_new;
802
803        if err < 1e-14 * x_norm {
804            break;
805        }
806    }
807
808    Ok(x)
809}
810
811/// Dense matrix logarithm using inverse scaling and squaring.
812fn dense_logm(a: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
813    let n = a.nrows();
814
815    // Scale A toward identity: use repeated square roots
816    // log(A) = 2^s * log(A^{1/2^s})
817    let mut a_scaled = a.to_owned();
818    let mut s = 0i32;
819
820    // Scale until ‖A - I‖₁ ≤ 0.5
821    for _ in 0..50 {
822        let diff: f64 = (0..n)
823            .map(|i| {
824                (0..n)
825                    .map(|j| {
826                        let v = a_scaled[[i, j]] - if i == j { 1.0 } else { 0.0 };
827                        v.abs()
828                    })
829                    .sum::<f64>()
830            })
831            .fold(0.0, f64::max);
832
833        if diff <= 0.5 {
834            break;
835        }
836
837        a_scaled = dense_sqrtm(&a_scaled.view())?;
838        s += 1;
839    }
840
841    // Compute log(A_scaled) via Padé on (A_scaled - I)
842    let ident = Array2::<f64>::eye(n);
843    let b = &a_scaled - &ident; // B = A_scaled - I
844
845    // Padé [2/2]: log(I + B) ≈ B(I + B/2) * (I + B/2 + B²/12)^{-1}
846    // Use a simple Taylor series for small ‖B‖
847    let b_norm = matrix_1norm(&b.view());
848
849    let log_scaled = if b_norm < 0.1 {
850        // Taylor series: sum_{k=1}^{K} (-1)^{k+1}/k * B^k
851        let mut log_a = b.clone();
852        let mut bpow = b.clone();
853        for k in 2..=20usize {
854            bpow = matmul_dense(&bpow.view(), &b.view());
855            let sign = if k % 2 == 0 { -1.0 } else { 1.0 };
856            log_a = log_a + (sign / k as f64) * &bpow;
857
858            let bpow_norm = matrix_1norm(&bpow.view());
859            if bpow_norm / (k as f64) < 1e-15 {
860                break;
861            }
862        }
863        log_a
864    } else {
865        // Use partial fractions for better accuracy
866        // log(I+B) = 2 * atanh(B * (2I + B)^{-1})
867        let two_plus_b = 2.0 * &ident + &b;
868        let z = solve_dense(&two_plus_b.view(), &b.view())?;
869        // atanh(Z) via Taylor: sum_{k=0}^{K} z^{2k+1} / (2k+1)
870        let mut atanh_z = z.clone();
871        let mut zpow = z.clone();
872        for k in 1..=20usize {
873            zpow = matmul_dense(&zpow.view(), &matmul_dense(&z.view(), &z.view()).view());
874            let coeff = 1.0 / (2 * k + 1) as f64;
875            atanh_z = atanh_z + coeff * &zpow;
876            let err = matrix_1norm(&zpow.view()) * coeff;
877            if err < 1e-15 {
878                break;
879            }
880        }
881        2.0 * atanh_z
882    };
883
884    // Unscale: log(A) = 2^s * log(A_scaled)
885    Ok(2.0f64.powi(s) * log_scaled)
886}
887
888/// Dense matrix sign function via Newton iteration.
889fn dense_signm(a: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
890    let n = a.nrows();
891    let mut x = a.to_owned();
892
893    for _ in 0..50 {
894        let x_inv = solve_dense(&x.view(), &Array2::eye(n).view())?;
895        let x_new = 0.5 * (&x + &x_inv);
896
897        let diff_norm = (&x_new - &x).mapv(|v: f64| v.abs()).sum();
898        let x_norm = x_new.mapv(|v: f64| v.abs()).sum().max(1e-300);
899
900        x = x_new;
901
902        if diff_norm / x_norm < 1e-14 {
903            break;
904        }
905    }
906
907    Ok(x)
908}
909
910/// Dense matrix cosine via exp: cos(A) = Re(exp(iA)) computed via Taylor series
911fn dense_cosm(a: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
912    let n = a.nrows();
913    let ident = Array2::<f64>::eye(n);
914
915    // cos(A) = I - A²/2! + A⁴/4! - A⁶/6! + ...
916    let a2 = matmul_dense(&a.view(), &a.view());
917    let mut result = ident.clone();
918    let mut apow = ident.clone();
919    let mut sign = 1.0_f64;
920
921    for k in 1..=16usize {
922        let two_k = 2 * k;
923        apow = matmul_dense(&apow.view(), &a2.view());
924        // Factorial: (2k)!
925        let mut fact = 1.0f64;
926        for i in 1..=(two_k as u64) {
927            fact *= i as f64;
928        }
929        sign = -sign;
930        result = result + (sign / fact) * &apow;
931
932        let apow_norm = matrix_1norm(&apow.view());
933        if apow_norm / fact < 1e-15 {
934            break;
935        }
936    }
937
938    Ok(result)
939}
940
941/// Dense matrix sine via Taylor series
942fn dense_sinm(a: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
943    let n = a.nrows();
944    let a2 = matmul_dense(&a.view(), &a.view());
945
946    // sin(A) = A - A³/3! + A⁵/5! - ...
947    let mut result = a.to_owned();
948    let mut apow = a.to_owned();
949    let mut sign = 1.0_f64;
950
951    for k in 1..=16usize {
952        let two_k_plus_1 = 2 * k + 1;
953        apow = matmul_dense(
954            &matmul_dense(&apow.view(), &a2.view()).view(),
955            &Array2::eye(n).view(),
956        );
957        apow = matmul_dense(&apow.view(), &a2.view());
958        let mut fact = 1.0f64;
959        for i in 1..=(two_k_plus_1 as u64) {
960            fact *= i as f64;
961        }
962        sign = -sign;
963        result = result + (sign / fact) * &apow;
964
965        let apow_norm = matrix_1norm(&apow.view());
966        if apow_norm / fact < 1e-15 {
967            break;
968        }
969    }
970
971    Ok(result)
972}
973
974// ============================================================================
975// Small dense linear algebra helpers (avoiding circular dependencies)
976// ============================================================================
977
978/// Matrix 1-norm (max column sum)
979fn matrix_1norm(a: &ArrayView2<f64>) -> f64 {
980    let (m, n) = a.dim();
981    let mut max_col = 0.0f64;
982    for j in 0..n {
983        let col_sum: f64 = (0..m).map(|i| a[[i, j]].abs()).sum();
984        if col_sum > max_col {
985            max_col = col_sum;
986        }
987    }
988    max_col
989}
990
991/// Dense matrix multiplication C = A * B
992fn matmul_dense(a: &ArrayView2<f64>, b: &ArrayView2<f64>) -> Array2<f64> {
993    let (m, k) = a.dim();
994    let (_, n) = b.dim();
995    let mut c = Array2::zeros((m, n));
996    for i in 0..m {
997        for l in 0..k {
998            let aval = a[[i, l]];
999            for j in 0..n {
1000                c[[i, j]] += aval * b[[l, j]];
1001            }
1002        }
1003    }
1004    c
1005}
1006
1007/// Solve A X = B via LU with partial pivoting
1008fn solve_dense(a: &ArrayView2<f64>, b: &ArrayView2<f64>) -> LinalgResult<Array2<f64>> {
1009    let n = a.nrows();
1010    let nb = b.ncols();
1011
1012    // LU with partial pivoting
1013    let mut lu = a.to_owned();
1014    let mut pivot = vec![0usize; n];
1015
1016    for k in 0..n {
1017        // Find pivot
1018        let mut max_val = lu[[k, k]].abs();
1019        let mut max_row = k;
1020        for i in (k + 1)..n {
1021            let v = lu[[i, k]].abs();
1022            if v > max_val {
1023                max_val = v;
1024                max_row = i;
1025            }
1026        }
1027        pivot[k] = max_row;
1028
1029        // Swap rows
1030        if max_row != k {
1031            for j in 0..n {
1032                let tmp = lu[[k, j]];
1033                lu[[k, j]] = lu[[max_row, j]];
1034                lu[[max_row, j]] = tmp;
1035            }
1036        }
1037
1038        let diag = lu[[k, k]];
1039        if diag.abs() < 1e-300 {
1040            return Err(LinalgError::SingularMatrixError(format!(
1041                "solve_dense: matrix is singular at column {k}"
1042            )));
1043        }
1044
1045        for i in (k + 1)..n {
1046            lu[[i, k]] /= diag;
1047            for j in (k + 1)..n {
1048                let luk = lu[[i, k]];
1049                let lukj = lu[[k, j]];
1050                lu[[i, j]] -= luk * lukj;
1051            }
1052        }
1053    }
1054
1055    // Forward/backward substitution for each column of B
1056    let mut x = b.to_owned();
1057
1058    // Apply row permutation to X
1059    for k in 0..n {
1060        let p = pivot[k];
1061        if p != k {
1062            for j in 0..nb {
1063                let tmp = x[[k, j]];
1064                x[[k, j]] = x[[p, j]];
1065                x[[p, j]] = tmp;
1066            }
1067        }
1068    }
1069
1070    // Forward substitution: L y = b (L has ones on diagonal)
1071    for k in 0..n {
1072        for i in (k + 1)..n {
1073            let lk = lu[[i, k]];
1074            for j in 0..nb {
1075                let xkj = x[[k, j]];
1076                x[[i, j]] -= lk * xkj;
1077            }
1078        }
1079    }
1080
1081    // Backward substitution: U x = y
1082    for k in (0..n).rev() {
1083        let diag = lu[[k, k]];
1084        for j in 0..nb {
1085            x[[k, j]] /= diag;
1086        }
1087        for i in 0..k {
1088            let uk = lu[[i, k]];
1089            for j in 0..nb {
1090                let xkj = x[[k, j]];
1091                x[[i, j]] -= uk * xkj;
1092            }
1093        }
1094    }
1095
1096    Ok(x)
1097}
1098
1099// ============================================================================
1100// Tests
1101// ============================================================================
1102
1103#[cfg(test)]
1104mod tests {
1105    use super::*;
1106    use scirs2_core::ndarray::array;
1107
1108    #[test]
1109    fn test_arnoldi_iteration() {
1110        // Simple 4×4 matrix
1111        let a = array![
1112            [2.0, 1.0, 0.0, 0.0],
1113            [0.0, 2.0, 1.0, 0.0],
1114            [0.0, 0.0, 2.0, 1.0],
1115            [0.0, 0.0, 0.0, 2.0],
1116        ];
1117        let v = array![1.0, 0.0, 0.0, 0.0];
1118
1119        let result = ArnoldiIteration::run(&a.view(), &v.view(), 3, None).expect("Arnoldi failed");
1120
1121        // V should have orthonormal columns
1122        let vt_v = matmul_dense(&result.v.t(), &result.v.view());
1123        for i in 0..result.m {
1124            for j in 0..result.m {
1125                let expected = if i == j { 1.0 } else { 0.0 };
1126                assert!(
1127                    (vt_v[[i, j]] - expected).abs() < 1e-12,
1128                    "V^T V not identity at ({},{}) = {}",
1129                    i,
1130                    j,
1131                    vt_v[[i, j]]
1132                );
1133            }
1134        }
1135    }
1136
1137    #[test]
1138    fn test_lanczos_iteration() {
1139        // Symmetric 4×4 matrix
1140        let a = array![
1141            [4.0, 1.0, 0.0, 0.0],
1142            [1.0, 3.0, 1.0, 0.0],
1143            [0.0, 1.0, 2.0, 1.0],
1144            [0.0, 0.0, 1.0, 1.0],
1145        ];
1146        let v = array![1.0, 0.0, 0.0, 0.0];
1147
1148        let result = LanczosIteration::run(&a.view(), &v.view(), 3, None).expect("Lanczos failed");
1149
1150        // V should have orthonormal columns
1151        let vt_v = matmul_dense(&result.v.t(), &result.v.view());
1152        for i in 0..result.m {
1153            for j in 0..result.m {
1154                let expected = if i == j { 1.0 } else { 0.0 };
1155                assert!(
1156                    (vt_v[[i, j]] - expected).abs() < 1e-10,
1157                    "Lanczos V^T V not identity at ({},{}) = {}",
1158                    i,
1159                    j,
1160                    vt_v[[i, j]]
1161                );
1162            }
1163        }
1164    }
1165
1166    #[test]
1167    fn test_matrix_exp_krylov_scalar() {
1168        // For a 1×1 "matrix" [t], exp(t*[a]) * [v] = exp(t*a) * v
1169        let a = array![[2.0_f64]];
1170        let v = array![3.0_f64];
1171        let t = 0.5;
1172        let result = MatrixExpKrylov::apply(&a.view(), &v.view(), t, None, None)
1173            .expect("MatrixExpKrylov failed");
1174
1175        let expected = 3.0 * (2.0_f64 * 0.5).exp();
1176        assert!(
1177            (result[0] - expected).abs() < 1e-8,
1178            "MatrixExpKrylov scalar: {} vs {}",
1179            result[0],
1180            expected
1181        );
1182    }
1183
1184    #[test]
1185    fn test_matrix_function_interpolation_identity() {
1186        // exp(I) * v should equal exp(1) * v for identity matrix
1187        let n = 4;
1188        let a = Array2::eye(n);
1189        let v: Array1<f64> = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
1190
1191        let params = MatrixFunctionParams::new(MatrixFunctionType::Exponential)
1192            .with_max_krylov_dim(n)
1193            .with_tol(1e-10);
1194
1195        let result = MatrixFunctionInterpolation::apply(&a.view(), &v.view(), &params)
1196            .expect("MatrixFunctionInterpolation failed");
1197
1198        let expected_scale = 1.0_f64.exp();
1199        for i in 0..n {
1200            let expected = expected_scale * v[i];
1201            assert!(
1202                (result[i] - expected).abs() < 1e-6,
1203                "exp(I) * v failed at {i}: {} vs {}",
1204                result[i],
1205                expected
1206            );
1207        }
1208    }
1209
1210    #[test]
1211    fn test_pade_expm_zero() {
1212        let a = Array2::<f64>::zeros((3, 3));
1213        let result = pade_expm(&a.view()).expect("pade_expm of zero failed");
1214        // exp(0) = I
1215        for i in 0..3 {
1216            for j in 0..3 {
1217                let expected = if i == j { 1.0 } else { 0.0 };
1218                assert!(
1219                    (result[[i, j]] - expected).abs() < 1e-12,
1220                    "pade_expm(0) failed at ({},{})={}",
1221                    i,
1222                    j,
1223                    result[[i, j]]
1224                );
1225            }
1226        }
1227    }
1228
1229    #[test]
1230    fn test_lanczos_tridiagonal() {
1231        let a = array![[3.0, 1.0, 0.0], [1.0, 3.0, 1.0], [0.0, 1.0, 3.0],];
1232        let v = array![1.0, 1.0, 1.0];
1233
1234        let result = LanczosIteration::run(&a.view(), &v.view(), 3, None).expect("Lanczos failed");
1235        let t = LanczosIteration::tridiagonal_matrix(&result);
1236
1237        // T must be symmetric
1238        for i in 0..result.m {
1239            for j in 0..result.m {
1240                assert!((t[[i, j]] - t[[j, i]]).abs() < 1e-12);
1241            }
1242        }
1243    }
1244}