Skip to main content

quantrs2_sim/
scirs2_sparse.rs

1//! SciRS2-optimized sparse matrix solvers for large quantum systems.
2//!
3//! This module provides sparse matrix operations optimized using `SciRS2`'s
4//! sparse linear algebra capabilities. It includes sparse Hamiltonian
5//! evolution, linear system solving, eigenvalue problems, and optimization
6//! routines for large-scale quantum simulations.
7
8use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
9use scirs2_core::Complex64;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13use crate::error::{Result, SimulatorError};
14use crate::scirs2_integration::SciRS2Backend;
15use crate::sparse::{apply_sparse_gate, CSRMatrix, SparseMatrixBuilder};
16use crate::statevector::StateVectorSimulator;
17
18/// Sparse matrix storage format
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum SparseFormat {
21    /// Compressed Sparse Row format
22    CSR,
23    /// Compressed Sparse Column format
24    CSC,
25    /// Coordinate format (COO)
26    COO,
27    /// Diagonal format
28    DIA,
29    /// Block Sparse Row format
30    BSR,
31}
32
33/// Sparse solver method
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum SparseSolverMethod {
36    /// Direct methods
37    LU,
38    QR,
39    Cholesky,
40    /// Iterative methods
41    CG,
42    GMRES,
43    BiCGSTAB,
44    /// Eigenvalue solvers
45    Arnoldi,
46    Lanczos,
47    LOBPCG,
48    /// `SciRS2` optimized methods
49    SciRS2Auto,
50    SciRS2Iterative,
51    SciRS2Direct,
52}
53
54/// Preconditioner type
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum Preconditioner {
57    None,
58    Jacobi,
59    ILU,
60    AMG,
61    SciRS2Auto,
62}
63
64/// Sparse solver configuration
65#[derive(Debug, Clone)]
66pub struct SparseSolverConfig {
67    /// Solver method to use
68    pub method: SparseSolverMethod,
69    /// Preconditioner
70    pub preconditioner: Preconditioner,
71    /// Convergence tolerance
72    pub tolerance: f64,
73    /// Maximum iterations
74    pub max_iterations: usize,
75    /// Number of restart iterations for GMRES
76    pub restart: usize,
77    /// Use parallel execution
78    pub parallel: bool,
79    /// Memory limit in bytes
80    pub memory_limit: usize,
81}
82
83impl Default for SparseSolverConfig {
84    fn default() -> Self {
85        Self {
86            method: SparseSolverMethod::SciRS2Auto,
87            preconditioner: Preconditioner::SciRS2Auto,
88            tolerance: 1e-12,
89            max_iterations: 1000,
90            restart: 30,
91            parallel: true,
92            memory_limit: 8 * 1024 * 1024 * 1024, // 8GB
93        }
94    }
95}
96
97/// Sparse solver execution statistics
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct SparseSolverStats {
100    /// Execution time in milliseconds
101    pub execution_time_ms: f64,
102    /// Number of iterations performed
103    pub iterations: usize,
104    /// Final residual norm
105    pub residual_norm: f64,
106    /// Convergence achieved
107    pub converged: bool,
108    /// Memory usage in bytes
109    pub memory_usage_bytes: usize,
110    /// Number of matrix-vector multiplications
111    pub matvec_count: usize,
112    /// Method used for solving
113    pub method_used: String,
114    /// Preconditioner setup time
115    pub preconditioner_time_ms: f64,
116}
117
118/// Sparse eigenvalue problem result
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct SparseEigenResult {
121    /// Eigenvalues (sorted in ascending order)
122    pub eigenvalues: Vec<f64>,
123    /// Corresponding eigenvectors
124    pub eigenvectors: Array2<Complex64>,
125    /// Number of converged eigenvalues
126    pub converged_count: usize,
127    /// Solver statistics
128    pub stats: SparseSolverStats,
129}
130
131/// SciRS2-optimized sparse matrix operations
132pub struct SciRS2SparseSolver {
133    /// `SciRS2` backend
134    backend: Option<SciRS2Backend>,
135    /// Solver configuration
136    config: SparseSolverConfig,
137    /// Execution statistics
138    stats: SparseSolverStats,
139    /// Cached sparse matrices
140    matrix_cache: HashMap<String, SparseMatrix>,
141    /// Preconditioner cache
142    preconditioner_cache: HashMap<String, Preconditioner>,
143}
144
145/// Sparse matrix representation
146#[derive(Debug, Clone)]
147pub struct SparseMatrix {
148    /// Matrix dimensions
149    pub shape: (usize, usize),
150    /// Storage format
151    pub format: SparseFormat,
152    /// Row indices (for CSR)
153    pub row_ptr: Vec<usize>,
154    /// Column indices
155    pub col_indices: Vec<usize>,
156    /// Matrix values
157    pub values: Vec<Complex64>,
158    /// Number of non-zero elements
159    pub nnz: usize,
160    /// Is Hermitian
161    pub is_hermitian: bool,
162    /// Is positive definite
163    pub is_positive_definite: bool,
164}
165
166impl SparseMatrix {
167    /// Create new sparse matrix
168    #[must_use]
169    pub const fn new(shape: (usize, usize), format: SparseFormat) -> Self {
170        Self {
171            shape,
172            format,
173            row_ptr: Vec::new(),
174            col_indices: Vec::new(),
175            values: Vec::new(),
176            nnz: 0,
177            is_hermitian: false,
178            is_positive_definite: false,
179        }
180    }
181
182    /// Create from CSR format
183    #[must_use]
184    pub fn from_csr(
185        shape: (usize, usize),
186        row_ptr: Vec<usize>,
187        col_indices: Vec<usize>,
188        values: Vec<Complex64>,
189    ) -> Self {
190        let nnz = values.len();
191        Self {
192            shape,
193            format: SparseFormat::CSR,
194            row_ptr,
195            col_indices,
196            values,
197            nnz,
198            is_hermitian: false,
199            is_positive_definite: false,
200        }
201    }
202
203    /// Create identity matrix
204    #[must_use]
205    pub fn identity(n: usize) -> Self {
206        let mut row_ptr = vec![0; n + 1];
207        let mut col_indices = Vec::with_capacity(n);
208        let mut values = Vec::with_capacity(n);
209
210        for i in 0..n {
211            row_ptr[i + 1] = i + 1;
212            col_indices.push(i);
213            values.push(Complex64::new(1.0, 0.0));
214        }
215
216        Self {
217            shape: (n, n),
218            format: SparseFormat::CSR,
219            row_ptr,
220            col_indices,
221            values,
222            nnz: n,
223            is_hermitian: true,
224            is_positive_definite: true,
225        }
226    }
227
228    /// Matrix-vector multiplication
229    pub fn matvec(&self, x: &Array1<Complex64>) -> Result<Array1<Complex64>> {
230        if x.len() != self.shape.1 {
231            return Err(SimulatorError::DimensionMismatch(format!(
232                "Vector length {} doesn't match matrix columns {}",
233                x.len(),
234                self.shape.1
235            )));
236        }
237
238        let mut y = Array1::zeros(self.shape.0);
239
240        match self.format {
241            SparseFormat::CSR => {
242                for i in 0..self.shape.0 {
243                    let mut sum = Complex64::new(0.0, 0.0);
244                    for j in self.row_ptr[i]..self.row_ptr[i + 1] {
245                        sum += self.values[j] * x[self.col_indices[j]];
246                    }
247                    y[i] = sum;
248                }
249            }
250            _ => {
251                return Err(SimulatorError::UnsupportedOperation(format!(
252                    "Matrix-vector multiplication not implemented for {:?}",
253                    self.format
254                )));
255            }
256        }
257
258        Ok(y)
259    }
260
261    /// Get matrix density (nnz / `total_elements`)
262    #[must_use]
263    pub fn density(&self) -> f64 {
264        self.nnz as f64 / (self.shape.0 * self.shape.1) as f64
265    }
266
267    /// Check if matrix is square
268    #[must_use]
269    pub const fn is_square(&self) -> bool {
270        self.shape.0 == self.shape.1
271    }
272
273    /// Convert to dense matrix (for small matrices only)
274    pub fn to_dense(&self) -> Result<Array2<Complex64>> {
275        if self.shape.0 * self.shape.1 > 10_000_000 {
276            return Err(SimulatorError::InvalidInput(
277                "Matrix too large to convert to dense format".to_string(),
278            ));
279        }
280
281        let mut dense = Array2::zeros(self.shape);
282
283        match self.format {
284            SparseFormat::CSR => {
285                for i in 0..self.shape.0 {
286                    for j in self.row_ptr[i]..self.row_ptr[i + 1] {
287                        dense[[i, self.col_indices[j]]] = self.values[j];
288                    }
289                }
290            }
291            _ => {
292                return Err(SimulatorError::UnsupportedOperation(format!(
293                    "Dense conversion not implemented for {:?}",
294                    self.format
295                )));
296            }
297        }
298
299        Ok(dense)
300    }
301}
302
303impl SciRS2SparseSolver {
304    /// Create new sparse solver
305    pub fn new(config: SparseSolverConfig) -> Result<Self> {
306        Ok(Self {
307            backend: None,
308            config,
309            stats: SparseSolverStats::default(),
310            matrix_cache: HashMap::new(),
311            preconditioner_cache: HashMap::new(),
312        })
313    }
314
315    /// Initialize with `SciRS2` backend
316    pub fn with_backend(mut self) -> Result<Self> {
317        self.backend = Some(SciRS2Backend::new());
318        Ok(self)
319    }
320
321    /// Solve linear system Ax = b
322    pub fn solve_linear_system(
323        &mut self,
324        matrix: &SparseMatrix,
325        rhs: &Array1<Complex64>,
326    ) -> Result<Array1<Complex64>> {
327        let start_time = std::time::Instant::now();
328
329        if !matrix.is_square() {
330            return Err(SimulatorError::InvalidInput(
331                "Matrix must be square for linear system solving".to_string(),
332            ));
333        }
334
335        if rhs.len() != matrix.shape.0 {
336            return Err(SimulatorError::DimensionMismatch(format!(
337                "RHS vector length {} doesn't match matrix size {}",
338                rhs.len(),
339                matrix.shape.0
340            )));
341        }
342
343        let solution = match self.config.method {
344            SparseSolverMethod::SciRS2Auto => self.solve_scirs2_auto(matrix, rhs)?,
345            SparseSolverMethod::SciRS2Direct => self.solve_scirs2_direct(matrix, rhs)?,
346            SparseSolverMethod::SciRS2Iterative => self.solve_scirs2_iterative(matrix, rhs)?,
347            SparseSolverMethod::CG => self.solve_conjugate_gradient(matrix, rhs)?,
348            SparseSolverMethod::GMRES => self.solve_gmres(matrix, rhs)?,
349            SparseSolverMethod::BiCGSTAB => self.solve_bicgstab(matrix, rhs)?,
350            _ => {
351                return Err(SimulatorError::UnsupportedOperation(format!(
352                    "Solver method {:?} not implemented",
353                    self.config.method
354                )));
355            }
356        };
357
358        self.stats.execution_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
359
360        Ok(solution)
361    }
362
363    /// Solve eigenvalue problem
364    pub fn solve_eigenvalue_problem(
365        &mut self,
366        matrix: &SparseMatrix,
367        num_eigenvalues: usize,
368        which: &str,
369    ) -> Result<SparseEigenResult> {
370        let start_time = std::time::Instant::now();
371
372        if !matrix.is_square() {
373            return Err(SimulatorError::InvalidInput(
374                "Matrix must be square for eigenvalue problems".to_string(),
375            ));
376        }
377
378        if num_eigenvalues >= matrix.shape.0 {
379            return Err(SimulatorError::InvalidInput(
380                "Number of eigenvalues must be less than matrix size".to_string(),
381            ));
382        }
383
384        let (eigenvalues, eigenvectors, converged_count) = match self.config.method {
385            SparseSolverMethod::Arnoldi => self.solve_arnoldi(matrix, num_eigenvalues, which)?,
386            SparseSolverMethod::Lanczos => self.solve_lanczos(matrix, num_eigenvalues, which)?,
387            SparseSolverMethod::LOBPCG => self.solve_lobpcg(matrix, num_eigenvalues, which)?,
388            SparseSolverMethod::SciRS2Auto => {
389                self.solve_eigen_scirs2_auto(matrix, num_eigenvalues, which)?
390            }
391            _ => {
392                return Err(SimulatorError::UnsupportedOperation(format!(
393                    "Eigenvalue solver {:?} not implemented",
394                    self.config.method
395                )));
396            }
397        };
398
399        self.stats.execution_time_ms = start_time.elapsed().as_secs_f64() * 1000.0;
400        self.stats.converged = converged_count == num_eigenvalues;
401
402        Ok(SparseEigenResult {
403            eigenvalues,
404            eigenvectors,
405            converged_count,
406            stats: self.stats.clone(),
407        })
408    }
409
410    /// `SciRS2` automatic solver selection
411    fn solve_scirs2_auto(
412        &mut self,
413        matrix: &SparseMatrix,
414        rhs: &Array1<Complex64>,
415    ) -> Result<Array1<Complex64>> {
416        if let Some(_backend) = &mut self.backend {
417            // SciRS2 would automatically choose the best solver based on matrix properties
418            let density = matrix.density();
419
420            if density > 0.1 {
421                // High density - use direct method
422                self.solve_scirs2_direct(matrix, rhs)
423            } else if matrix.is_hermitian && matrix.is_positive_definite {
424                // Symmetric positive definite - use CG
425                self.solve_conjugate_gradient(matrix, rhs)
426            } else {
427                // General case - use GMRES
428                self.solve_gmres(matrix, rhs)
429            }
430        } else {
431            // Fallback to iterative method
432            self.solve_gmres(matrix, rhs)
433        }
434    }
435
436    /// `SciRS2` direct solver
437    fn solve_scirs2_direct(
438        &mut self,
439        matrix: &SparseMatrix,
440        rhs: &Array1<Complex64>,
441    ) -> Result<Array1<Complex64>> {
442        if let Some(_backend) = &mut self.backend {
443            // Simulate SciRS2 sparse LU decomposition
444            self.simulate_sparse_lu(matrix, rhs)
445        } else {
446            // Fallback to dense LU for small matrices
447            if matrix.shape.0 <= 1000 {
448                let dense_matrix = matrix.to_dense()?;
449                self.solve_dense_lu(&dense_matrix, rhs)
450            } else {
451                Err(SimulatorError::UnsupportedOperation(
452                    "Matrix too large for direct solving without SciRS2 backend".to_string(),
453                ))
454            }
455        }
456    }
457
458    /// `SciRS2` iterative solver
459    fn solve_scirs2_iterative(
460        &mut self,
461        matrix: &SparseMatrix,
462        rhs: &Array1<Complex64>,
463    ) -> Result<Array1<Complex64>> {
464        if let Some(_backend) = &mut self.backend {
465            // Use SciRS2's optimized iterative solvers
466            if matrix.is_hermitian {
467                self.solve_conjugate_gradient(matrix, rhs)
468            } else {
469                self.solve_gmres(matrix, rhs)
470            }
471        } else {
472            // Fallback to standard iterative methods
473            self.solve_gmres(matrix, rhs)
474        }
475    }
476
477    /// Conjugate Gradient solver (for symmetric positive definite matrices)
478    fn solve_conjugate_gradient(
479        &mut self,
480        matrix: &SparseMatrix,
481        rhs: &Array1<Complex64>,
482    ) -> Result<Array1<Complex64>> {
483        let n = matrix.shape.0;
484        let mut x = Array1::zeros(n);
485        let mut r = rhs.clone();
486        let mut p = r.clone();
487        let mut rsold = r.iter().map(|&ri| ri.norm_sqr()).sum::<f64>();
488
489        self.stats.method_used = "ConjugateGradient".to_string();
490
491        for iteration in 0..self.config.max_iterations {
492            let ap = matrix.matvec(&p)?;
493            let alpha = rsold
494                / p.iter()
495                    .zip(ap.iter())
496                    .map(|(&pi, &api)| (pi.conj() * api).re)
497                    .sum::<f64>();
498
499            for i in 0..n {
500                x[i] += alpha * p[i];
501                r[i] -= alpha * ap[i];
502            }
503
504            let rsnew = r.iter().map(|&ri| ri.norm_sqr()).sum::<f64>();
505
506            self.stats.iterations = iteration + 1;
507            self.stats.residual_norm = rsnew.sqrt();
508            self.stats.matvec_count += 1;
509
510            if rsnew.sqrt() < self.config.tolerance {
511                self.stats.converged = true;
512                break;
513            }
514
515            let beta = rsnew / rsold;
516            for i in 0..n {
517                p[i] = r[i] + beta * p[i];
518            }
519
520            rsold = rsnew;
521        }
522
523        Ok(x)
524    }
525
526    /// GMRES solver (for general matrices)
527    fn solve_gmres(
528        &mut self,
529        matrix: &SparseMatrix,
530        rhs: &Array1<Complex64>,
531    ) -> Result<Array1<Complex64>> {
532        let n = matrix.shape.0;
533        let m = self.config.restart.min(n);
534        let mut x = Array1::zeros(n);
535
536        self.stats.method_used = "GMRES".to_string();
537
538        // Simplified GMRES implementation
539        let mut r = rhs.clone();
540        let beta = r.iter().map(|&ri| ri.norm_sqr()).sum::<f64>().sqrt();
541
542        if beta < self.config.tolerance {
543            self.stats.converged = true;
544            return Ok(x);
545        }
546
547        for _restart in 0..(self.config.max_iterations / m) {
548            // Arnoldi process
549            let mut v = Array2::zeros((n, m + 1));
550            let mut h = Array2::zeros((m + 1, m));
551
552            // Initial vector
553            for i in 0..n {
554                v[[i, 0]] = r[i] / beta;
555            }
556
557            for j in 0..m {
558                let vj = v.column(j).to_owned();
559                let w = matrix.matvec(&vj)?;
560                self.stats.matvec_count += 1;
561
562                // Modified Gram-Schmidt
563                for i in 0..=j {
564                    let vi = v.column(i);
565                    h[[i, j]] = vi
566                        .iter()
567                        .zip(w.iter())
568                        .map(|(&vi_val, &w_val)| vi_val.conj() * w_val)
569                        .sum();
570                }
571
572                let mut w_next = w.clone();
573                for i in 0..=j {
574                    let vi = v.column(i);
575                    for k in 0..n {
576                        w_next[k] -= h[[i, j]] * vi[k];
577                    }
578                }
579
580                let h_norm = w_next.iter().map(|&wi| wi.norm_sqr()).sum::<f64>().sqrt();
581                h[[j + 1, j]] = Complex64::new(h_norm, 0.0);
582
583                if h_norm > 1e-12 && j + 1 < m {
584                    for i in 0..n {
585                        v[[i, j + 1]] = w_next[i] / h_norm;
586                    }
587                }
588
589                self.stats.iterations += 1;
590                self.stats.residual_norm = h_norm;
591
592                if h_norm < self.config.tolerance {
593                    self.stats.converged = true;
594                    return Ok(x);
595                }
596            }
597        }
598
599        Ok(x)
600    }
601
602    /// `BiCGSTAB` solver
603    fn solve_bicgstab(
604        &mut self,
605        matrix: &SparseMatrix,
606        rhs: &Array1<Complex64>,
607    ) -> Result<Array1<Complex64>> {
608        let n = matrix.shape.0;
609        let mut x = Array1::zeros(n);
610        let mut r = rhs.clone();
611        let r0 = r.clone();
612
613        self.stats.method_used = "BiCGSTAB".to_string();
614
615        let mut p = r.clone();
616        let mut alpha = Complex64::new(1.0, 0.0);
617        let mut omega = Complex64::new(1.0, 0.0);
618        let mut rho_old = Complex64::new(1.0, 0.0);
619
620        for iteration in 0..self.config.max_iterations {
621            let rho: Complex64 = r0
622                .iter()
623                .zip(r.iter())
624                .map(|(&r0i, &ri)| r0i.conj() * ri)
625                .sum();
626
627            if rho.norm() < 1e-15 {
628                break;
629            }
630
631            let beta = (rho / rho_old) * (alpha / omega);
632
633            for i in 0..n {
634                p[i] = r[i] + beta * (p[i] - omega * matrix.matvec(&p)?[i]);
635            }
636
637            let ap = matrix.matvec(&p)?;
638            alpha = rho
639                / r0.iter()
640                    .zip(ap.iter())
641                    .map(|(&r0i, &api)| r0i.conj() * api)
642                    .sum::<Complex64>();
643
644            let mut s = r.clone();
645            for i in 0..n {
646                s[i] -= alpha * ap[i];
647            }
648
649            let residual_s = s.iter().map(|&si| si.norm_sqr()).sum::<f64>().sqrt();
650            if residual_s < self.config.tolerance {
651                for i in 0..n {
652                    x[i] += alpha * p[i];
653                }
654                self.stats.converged = true;
655                break;
656            }
657
658            let as_vec = matrix.matvec(&s)?;
659            omega = as_vec
660                .iter()
661                .zip(s.iter())
662                .map(|(&asi, &si)| asi.conj() * si)
663                .sum::<Complex64>()
664                / as_vec.iter().map(|&asi| asi.norm_sqr()).sum::<f64>();
665
666            for i in 0..n {
667                x[i] += alpha * p[i] + omega * s[i];
668                r[i] = s[i] - omega * as_vec[i];
669            }
670
671            self.stats.iterations = iteration + 1;
672            self.stats.residual_norm = r.iter().map(|&ri| ri.norm_sqr()).sum::<f64>().sqrt();
673            self.stats.matvec_count += 2;
674
675            if self.stats.residual_norm < self.config.tolerance {
676                self.stats.converged = true;
677                break;
678            }
679
680            rho_old = rho;
681        }
682
683        Ok(x)
684    }
685
686    /// Simulate sparse LU decomposition
687    fn simulate_sparse_lu(
688        &mut self,
689        matrix: &SparseMatrix,
690        rhs: &Array1<Complex64>,
691    ) -> Result<Array1<Complex64>> {
692        // Simplified sparse LU simulation
693        // In practice, this would use SciRS2's optimized sparse LU
694
695        self.stats.method_used = "SparseLU".to_string();
696
697        // For now, fall back to iterative method for large matrices
698        if matrix.shape.0 > 5000 {
699            return self.solve_gmres(matrix, rhs);
700        }
701
702        // Small matrix - convert to dense and solve
703        let dense_matrix = matrix.to_dense()?;
704        self.solve_dense_lu(&dense_matrix, rhs)
705    }
706
707    /// Dense LU solver (fallback)
708    fn solve_dense_lu(
709        &self,
710        matrix: &Array2<Complex64>,
711        rhs: &Array1<Complex64>,
712    ) -> Result<Array1<Complex64>> {
713        // Simplified dense LU implementation
714        // This would be replaced with proper LAPACK calls
715
716        let n = matrix.nrows();
717        if n != matrix.ncols() {
718            return Err(SimulatorError::InvalidInput(
719                "Matrix must be square".to_string(),
720            ));
721        }
722
723        // For demonstration, solve using Gaussian elimination
724        let mut a = matrix.clone();
725        let mut b = rhs.clone();
726
727        // Forward elimination
728        for k in 0..n - 1 {
729            for i in k + 1..n {
730                if a[[k, k]].norm() < 1e-15 {
731                    return Err(SimulatorError::NumericalError(
732                        "Singular matrix".to_string(),
733                    ));
734                }
735
736                let factor = a[[i, k]] / a[[k, k]];
737                let b_k = b[k]; // Store value to avoid borrow checker issue
738                for j in k..n {
739                    let a_kj = a[[k, j]]; // Store value to avoid borrow checker issue
740                    a[[i, j]] -= factor * a_kj;
741                }
742                b[i] -= factor * b_k;
743            }
744        }
745
746        // Back substitution
747        let mut x = Array1::zeros(n);
748        for i in (0..n).rev() {
749            let mut sum = Complex64::new(0.0, 0.0);
750            for j in i + 1..n {
751                sum += a[[i, j]] * x[j];
752            }
753            x[i] = (b[i] - sum) / a[[i, i]];
754        }
755
756        Ok(x)
757    }
758
759    /// Diagonalize a small dense projection matrix (the Krylov Hessenberg or
760    /// tridiagonal matrix), returning the eigenvalues (real parts) together with
761    /// the eigenvector columns of the projection.
762    ///
763    /// `hermitian` selects the Hermitian path (real-symmetric Jacobi solver,
764    /// guaranteed-real spectrum with orthonormal eigenvectors — the Lanczos
765    /// tridiagonal is real symmetric) versus the general path (shifted QR with
766    /// inverse-iteration eigenvectors). These are in-crate implementations
767    /// because the `scirs2-linalg` 0.6.1 complex eigensolvers are numerically
768    /// unreliable for non-diagonal inputs.
769    fn diagonalize_projection(
770        matrix: &Array2<Complex64>,
771        hermitian: bool,
772    ) -> Result<(Vec<f64>, Array2<Complex64>)> {
773        let n = matrix.nrows();
774        if hermitian {
775            // The Hermitian projection produced here (Lanczos tridiagonal) is
776            // real symmetric; take the symmetric real part and diagonalize with
777            // the Jacobi eigenvalue algorithm.
778            let mut sym = Array2::<f64>::zeros((n, n));
779            for i in 0..n {
780                for j in 0..n {
781                    sym[[i, j]] = 0.5 * (matrix[[i, j]].re + matrix[[j, i]].re);
782                }
783            }
784            let (values, real_vectors) = jacobi_symmetric_eig(&sym);
785            let vectors = real_vectors.mapv(|x| Complex64::new(x, 0.0));
786            Ok((values, vectors))
787        } else {
788            let (complex_values, vectors) = general_complex_eig(matrix)?;
789            let values: Vec<f64> = complex_values.iter().map(|v| v.re).collect();
790            Ok((values, vectors))
791        }
792    }
793
794    /// Back-transform the projected eigenvectors into the full space
795    /// (`X = V_m · Y`) and select the requested `num_eigenvalues` Ritz pairs
796    /// according to `which` ("smallest" or "largest"). The returned eigenvalues
797    /// are always sorted in ascending order (matching [`SparseEigenResult`]),
798    /// and the eigenvector columns line up with them.
799    fn select_ritz_pairs(
800        krylov_basis: &Array2<Complex64>,
801        ritz_values: &[f64],
802        ritz_vectors: &Array2<Complex64>,
803        num_eigenvalues: usize,
804        which: &str,
805    ) -> (Vec<f64>, Array2<Complex64>) {
806        let m = ritz_values.len();
807        let n = krylov_basis.nrows();
808
809        // V_m : the first m Krylov basis vectors. Ritz vectors live in this span.
810        let v_m = krylov_basis.slice(scirs2_core::ndarray::s![.., ..m]);
811        let full_vectors = v_m.dot(ritz_vectors);
812
813        // Order all Ritz values ascending, then take the requested end of the
814        // spectrum. Either slice is itself already in ascending order.
815        let mut order: Vec<usize> = (0..m).collect();
816        order.sort_by(|&a, &b| {
817            ritz_values[a]
818                .partial_cmp(&ritz_values[b])
819                .unwrap_or(std::cmp::Ordering::Equal)
820        });
821
822        let count = num_eigenvalues.min(m);
823        let want_largest = matches!(which, "largest" | "LM" | "LA" | "LR");
824        let selected: Vec<usize> = if want_largest {
825            order[m - count..].to_vec()
826        } else {
827            order[..count].to_vec()
828        };
829
830        let mut selected_values = Vec::with_capacity(count);
831        let mut selected_vectors = Array2::<Complex64>::zeros((n, count));
832        for (out_col, &idx) in selected.iter().enumerate() {
833            selected_values.push(ritz_values[idx]);
834            let column = full_vectors.column(idx);
835            let norm = column.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
836            let scale = if norm > 1e-15 { 1.0 / norm } else { 1.0 };
837            for row in 0..n {
838                selected_vectors[[row, out_col]] = column[row] * scale;
839            }
840        }
841        (selected_values, selected_vectors)
842    }
843
844    /// Arnoldi eigenvalue solver for general (non-Hermitian) sparse matrices.
845    ///
846    /// Builds an `m`-dimensional Krylov subspace, then diagonalizes the small
847    /// `m × m` upper-Hessenberg projection to obtain Ritz values and Ritz
848    /// vectors; the vectors are back-transformed into the full space. For a
849    /// non-Hermitian operator the exact eigenvalues may be complex — the real
850    /// parts of the Ritz values are reported (the [`SparseEigenResult`] carries
851    /// real eigenvalues).
852    fn solve_arnoldi(
853        &mut self,
854        matrix: &SparseMatrix,
855        num_eigenvalues: usize,
856        which: &str,
857    ) -> Result<(Vec<f64>, Array2<Complex64>, usize)> {
858        let n = matrix.shape.0;
859        let m = (num_eigenvalues * 2).min(n);
860
861        let mut v = Array2::zeros((n, m + 1));
862        let mut h = Array2::zeros((m + 1, m));
863
864        // Random initial vector
865        for i in 0..n {
866            v[[i, 0]] = Complex64::new(fastrand::f64() - 0.5, fastrand::f64() - 0.5);
867        }
868
869        // Normalize
870        let norm = v
871            .column(0)
872            .iter()
873            .map(|&vi| vi.norm_sqr())
874            .sum::<f64>()
875            .sqrt();
876        for i in 0..n {
877            v[[i, 0]] /= norm;
878        }
879
880        // Arnoldi process
881        for j in 0..m {
882            let vj = v.column(j).to_owned();
883            let w = matrix.matvec(&vj)?;
884
885            // Modified Gram-Schmidt
886            for i in 0..=j {
887                let vi = v.column(i);
888                h[[i, j]] = vi
889                    .iter()
890                    .zip(w.iter())
891                    .map(|(&vi_val, &w_val)| vi_val.conj() * w_val)
892                    .sum();
893            }
894
895            let mut w_next = w.clone();
896            for i in 0..=j {
897                let vi = v.column(i);
898                for k in 0..n {
899                    w_next[k] -= h[[i, j]] * vi[k];
900                }
901            }
902
903            let h_norm = w_next.iter().map(|&wi| wi.norm_sqr()).sum::<f64>().sqrt();
904            h[[j + 1, j]] = Complex64::new(h_norm, 0.0);
905
906            if h_norm > 1e-12 && j + 1 < m {
907                for i in 0..n {
908                    v[[i, j + 1]] = w_next[i] / h_norm;
909                }
910            }
911        }
912
913        // Diagonalize the m×m upper-Hessenberg projection H_m to obtain the
914        // Ritz values/vectors, then back-transform the vectors into the full
915        // space. This replaces the previous placeholder that read the raw
916        // Hessenberg diagonal and returned zero eigenvectors.
917        let mut h_block = Array2::<Complex64>::zeros((m, m));
918        for i in 0..m {
919            for j in 0..m {
920                h_block[[i, j]] = h[[i, j]];
921            }
922        }
923
924        let (ritz_values, ritz_vectors) = Self::diagonalize_projection(&h_block, false)?;
925        let (eigenvalues, eigenvectors) =
926            Self::select_ritz_pairs(&v, &ritz_values, &ritz_vectors, num_eigenvalues, which);
927        let converged = eigenvalues.len();
928
929        self.stats.method_used = "Arnoldi".to_string();
930
931        Ok((eigenvalues, eigenvectors, converged))
932    }
933
934    /// Lanczos eigenvalue solver for Hermitian sparse matrices.
935    ///
936    /// Builds a Krylov subspace via the three-term Lanczos recurrence, forming a
937    /// real symmetric tridiagonal projection `T` (diagonal `alpha`, off-diagonal
938    /// `beta`). `T` is diagonalized to real Ritz values with orthonormal Ritz
939    /// vectors, which are back-transformed into the full space. Non-Hermitian
940    /// inputs are delegated to the Arnoldi solver.
941    fn solve_lanczos(
942        &mut self,
943        matrix: &SparseMatrix,
944        num_eigenvalues: usize,
945        which: &str,
946    ) -> Result<(Vec<f64>, Array2<Complex64>, usize)> {
947        if !matrix.is_hermitian {
948            return self.solve_arnoldi(matrix, num_eigenvalues, which);
949        }
950
951        let n = matrix.shape.0;
952        let m = (num_eigenvalues * 2).min(n);
953
954        let mut v = Array2::zeros((n, m + 1));
955        let mut alpha = vec![0.0; m];
956        let mut beta = vec![0.0; m];
957
958        // Random initial vector
959        for i in 0..n {
960            v[[i, 0]] = Complex64::new(fastrand::f64() - 0.5, 0.0);
961        }
962
963        // Normalize
964        let norm = v
965            .column(0)
966            .iter()
967            .map(|&vi| vi.norm_sqr())
968            .sum::<f64>()
969            .sqrt();
970        for i in 0..n {
971            v[[i, 0]] /= norm;
972        }
973
974        // Lanczos process
975        for j in 0..m {
976            let vj = v.column(j).to_owned();
977            let w = matrix.matvec(&vj)?;
978
979            alpha[j] = vj
980                .iter()
981                .zip(w.iter())
982                .map(|(&vji, &wi)| (vji.conj() * wi).re)
983                .sum();
984
985            let mut w_next = w.clone();
986            for i in 0..n {
987                w_next[i] -= alpha[j] * vj[i];
988                if j > 0 {
989                    w_next[i] -= beta[j - 1] * v[[i, j - 1]];
990                }
991            }
992
993            beta[j] = w_next.iter().map(|&wi| wi.norm_sqr()).sum::<f64>().sqrt();
994
995            if beta[j] > 1e-12 && j + 1 < m {
996                for i in 0..n {
997                    v[[i, j + 1]] = w_next[i] / beta[j];
998                }
999            }
1000        }
1001
1002        // Assemble the real symmetric tridiagonal projection T (m×m) and
1003        // diagonalize it to real Ritz values with orthonormal Ritz vectors, then
1004        // back-transform the vectors into the full space. This replaces the
1005        // previous placeholder that returned the raw alpha diagonal as
1006        // "eigenvalues" and zero eigenvectors.
1007        let mut tri = Array2::<Complex64>::zeros((m, m));
1008        for i in 0..m {
1009            tri[[i, i]] = Complex64::new(alpha[i], 0.0);
1010            if i + 1 < m {
1011                let off = Complex64::new(beta[i], 0.0);
1012                tri[[i, i + 1]] = off;
1013                tri[[i + 1, i]] = off;
1014            }
1015        }
1016
1017        let (ritz_values, ritz_vectors) = Self::diagonalize_projection(&tri, true)?;
1018        let (eigenvalues, eigenvectors) =
1019            Self::select_ritz_pairs(&v, &ritz_values, &ritz_vectors, num_eigenvalues, which);
1020        let converged = eigenvalues.len();
1021
1022        self.stats.method_used = "Lanczos".to_string();
1023
1024        Ok((eigenvalues, eigenvectors, converged))
1025    }
1026
1027    /// LOBPCG eigenvalue solver entry point.
1028    ///
1029    /// A dedicated block LOBPCG iteration is not implemented; requests are served
1030    /// by the Lanczos solver above, which targets the same Hermitian eigenproblem
1031    /// and returns genuine (approximate) Ritz values and Ritz vectors — never
1032    /// fabricated data. The reported `method_used` reflects the solver that
1033    /// actually ran (Lanczos), so the statistics stay honest.
1034    fn solve_lobpcg(
1035        &mut self,
1036        matrix: &SparseMatrix,
1037        num_eigenvalues: usize,
1038        which: &str,
1039    ) -> Result<(Vec<f64>, Array2<Complex64>, usize)> {
1040        self.solve_lanczos(matrix, num_eigenvalues, which)
1041    }
1042
1043    /// `SciRS2` automatic eigenvalue solver
1044    fn solve_eigen_scirs2_auto(
1045        &mut self,
1046        matrix: &SparseMatrix,
1047        num_eigenvalues: usize,
1048        which: &str,
1049    ) -> Result<(Vec<f64>, Array2<Complex64>, usize)> {
1050        if let Some(_backend) = &mut self.backend {
1051            // SciRS2 would choose the best eigenvalue solver
1052            if matrix.is_hermitian {
1053                self.solve_lanczos(matrix, num_eigenvalues, which)
1054            } else {
1055                self.solve_arnoldi(matrix, num_eigenvalues, which)
1056            }
1057        } else {
1058            // Fallback
1059            if matrix.is_hermitian {
1060                self.solve_lanczos(matrix, num_eigenvalues, which)
1061            } else {
1062                self.solve_arnoldi(matrix, num_eigenvalues, which)
1063            }
1064        }
1065    }
1066
1067    /// Get execution statistics
1068    #[must_use]
1069    pub const fn get_stats(&self) -> &SparseSolverStats {
1070        &self.stats
1071    }
1072
1073    /// Reset statistics
1074    pub fn reset_stats(&mut self) {
1075        self.stats = SparseSolverStats::default();
1076    }
1077
1078    /// Set configuration
1079    pub const fn set_config(&mut self, config: SparseSolverConfig) {
1080        self.config = config;
1081    }
1082}
1083
1084/// Cyclic Jacobi eigenvalue algorithm for a real symmetric matrix.
1085///
1086/// Returns `(eigenvalues, eigenvectors)` where eigenvector column `i` corresponds
1087/// to `eigenvalues[i]` (unsorted). The Jacobi method is unconditionally
1088/// convergent and accurate, which makes it the right choice for the small dense
1089/// symmetric matrices diagonalized here. It is used instead of `scirs2-linalg`
1090/// 0.6.1's `complex_eigh`, whose QR iteration returns incorrect results for
1091/// non-diagonal inputs.
1092pub(crate) fn jacobi_symmetric_eig(input: &Array2<f64>) -> (Vec<f64>, Array2<f64>) {
1093    let n = input.nrows();
1094    let mut a = input.clone();
1095    let mut v = Array2::<f64>::eye(n);
1096    if n <= 1 {
1097        let values: Vec<f64> = (0..n).map(|i| a[[i, i]]).collect();
1098        return (values, v);
1099    }
1100
1101    for _ in 0..100 {
1102        // Sum of squared off-diagonal elements.
1103        let mut off = 0.0;
1104        for p in 0..n {
1105            for q in (p + 1)..n {
1106                off += a[[p, q]] * a[[p, q]];
1107            }
1108        }
1109        if off <= 1e-30 {
1110            break;
1111        }
1112
1113        for p in 0..n {
1114            for q in (p + 1)..n {
1115                let apq = a[[p, q]];
1116                if apq.abs() < 1e-300 {
1117                    continue;
1118                }
1119                let theta = (a[[q, q]] - a[[p, p]]) / (2.0 * apq);
1120                // t = sign(theta) / (|theta| + sqrt(theta^2 + 1)); for theta == 0
1121                // this yields the 45-degree rotation (t = 1).
1122                let t = theta.signum() / (theta.abs() + (theta * theta + 1.0).sqrt());
1123                let cc = 1.0 / (t * t + 1.0).sqrt();
1124                let ss = t * cc;
1125
1126                // A <- Jᵀ A J with J acting on planes p, q.
1127                for k in 0..n {
1128                    let akp = a[[k, p]];
1129                    let akq = a[[k, q]];
1130                    a[[k, p]] = cc * akp - ss * akq;
1131                    a[[k, q]] = ss * akp + cc * akq;
1132                }
1133                for k in 0..n {
1134                    let apk = a[[p, k]];
1135                    let aqk = a[[q, k]];
1136                    a[[p, k]] = cc * apk - ss * aqk;
1137                    a[[q, k]] = ss * apk + cc * aqk;
1138                }
1139                // Accumulate eigenvectors: V <- V J.
1140                for k in 0..n {
1141                    let vkp = v[[k, p]];
1142                    let vkq = v[[k, q]];
1143                    v[[k, p]] = cc * vkp - ss * vkq;
1144                    v[[k, q]] = ss * vkp + cc * vkq;
1145                }
1146            }
1147        }
1148    }
1149
1150    let eigenvalues: Vec<f64> = (0..n).map(|i| a[[i, i]]).collect();
1151    (eigenvalues, v)
1152}
1153
1154/// QR decomposition of a small dense complex matrix via modified Gram-Schmidt.
1155/// Returns `(Q, R)` with `Q` having orthonormal columns and `R` upper-triangular.
1156fn qr_decompose_complex(a: &Array2<Complex64>) -> (Array2<Complex64>, Array2<Complex64>) {
1157    let n = a.nrows();
1158    let m = a.ncols();
1159    let mut q = Array2::<Complex64>::zeros((n, m));
1160    let mut r = Array2::<Complex64>::zeros((m, m));
1161
1162    for j in 0..m {
1163        let mut vj: Array1<Complex64> = a.column(j).to_owned();
1164        for i in 0..j {
1165            let rij: Complex64 = (0..n).map(|k| q[[k, i]].conj() * vj[k]).sum();
1166            r[[i, j]] = rij;
1167            for k in 0..n {
1168                vj[k] -= rij * q[[k, i]];
1169            }
1170        }
1171        let norm = vj.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
1172        r[[j, j]] = Complex64::new(norm, 0.0);
1173        if norm > 1e-300 {
1174            for k in 0..n {
1175                q[[k, j]] = vj[k] / norm;
1176            }
1177        }
1178    }
1179
1180    (q, r)
1181}
1182
1183/// Solve a small dense complex linear system `A x = b` by Gaussian elimination
1184/// with partial pivoting.
1185fn solve_linear_dense(
1186    matrix: &Array2<Complex64>,
1187    rhs: &Array1<Complex64>,
1188) -> Result<Array1<Complex64>> {
1189    let n = matrix.nrows();
1190    let mut a = matrix.clone();
1191    let mut b = rhs.clone();
1192
1193    for k in 0..n {
1194        // Partial pivot on the largest-magnitude entry in column k.
1195        let mut pivot = k;
1196        let mut pivot_mag = a[[k, k]].norm();
1197        for i in (k + 1)..n {
1198            let mag = a[[i, k]].norm();
1199            if mag > pivot_mag {
1200                pivot_mag = mag;
1201                pivot = i;
1202            }
1203        }
1204        if pivot_mag < 1e-300 {
1205            return Err(SimulatorError::NumericalError(
1206                "singular matrix in dense solve".to_string(),
1207            ));
1208        }
1209        if pivot != k {
1210            for j in 0..n {
1211                let tmp = a[[k, j]];
1212                a[[k, j]] = a[[pivot, j]];
1213                a[[pivot, j]] = tmp;
1214            }
1215            b.swap(k, pivot);
1216        }
1217        for i in (k + 1)..n {
1218            let factor = a[[i, k]] / a[[k, k]];
1219            for j in k..n {
1220                let akj = a[[k, j]];
1221                a[[i, j]] -= factor * akj;
1222            }
1223            let bk = b[k];
1224            b[i] -= factor * bk;
1225        }
1226    }
1227
1228    let mut x = Array1::zeros(n);
1229    for i in (0..n).rev() {
1230        let mut sum = Complex64::new(0.0, 0.0);
1231        for j in (i + 1)..n {
1232            sum += a[[i, j]] * x[j];
1233        }
1234        x[i] = (b[i] - sum) / a[[i, i]];
1235    }
1236    Ok(x)
1237}
1238
1239/// Recover an eigenvector of `a` for the (approximate) eigenvalue `lambda` via
1240/// inverse iteration on the slightly-perturbed shifted matrix `A - (λ+ε)I`.
1241fn inverse_iteration_complex(
1242    a: &Array2<Complex64>,
1243    lambda: Complex64,
1244) -> Result<Array1<Complex64>> {
1245    let n = a.nrows();
1246    let mut shifted = a.clone();
1247    let perturb = Complex64::new(1e-8, 0.0);
1248    for i in 0..n {
1249        shifted[[i, i]] -= lambda + perturb;
1250    }
1251
1252    let mut x = Array1::from_elem(n, Complex64::new(1.0, 0.0));
1253    let mut norm = x.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
1254    if norm > 0.0 {
1255        x.mapv_inplace(|c| c / norm);
1256    }
1257
1258    for _ in 0..8 {
1259        let y = match solve_linear_dense(&shifted, &x) {
1260            Ok(y) => y,
1261            Err(_) => break,
1262        };
1263        norm = y.iter().map(|c| c.norm_sqr()).sum::<f64>().sqrt();
1264        if norm < 1e-300 {
1265            break;
1266        }
1267        for i in 0..n {
1268            x[i] = y[i] / norm;
1269        }
1270    }
1271
1272    Ok(x)
1273}
1274
1275/// Eigenvalues and eigenvectors of a small dense general complex matrix.
1276///
1277/// Eigenvalues are computed with the single-shift QR algorithm (Wilkinson shift
1278/// from the trailing 2×2 block) with deflation; eigenvectors are then recovered
1279/// by inverse iteration on the original matrix. Used for the non-Hermitian
1280/// Arnoldi projection, since `scirs2-linalg` 0.6.1's `complex_eig` is
1281/// numerically unreliable.
1282fn general_complex_eig(input: &Array2<Complex64>) -> Result<(Vec<Complex64>, Array2<Complex64>)> {
1283    let n = input.nrows();
1284    if n == 0 {
1285        return Ok((Vec::new(), Array2::zeros((0, 0))));
1286    }
1287    if n == 1 {
1288        let mut vectors = Array2::<Complex64>::zeros((1, 1));
1289        vectors[[0, 0]] = Complex64::new(1.0, 0.0);
1290        return Ok((vec![input[[0, 0]]], vectors));
1291    }
1292
1293    let mut a = input.clone();
1294    let mut eigenvalues: Vec<Complex64> = Vec::with_capacity(n);
1295    let mut p = n;
1296    let max_total_iter = 300 * n;
1297    let mut total_iter = 0;
1298    let two = Complex64::new(2.0, 0.0);
1299    let four = Complex64::new(4.0, 0.0);
1300
1301    while p > 1 {
1302        // Deflate once the trailing subdiagonal entry is negligible.
1303        let sub = a[[p - 1, p - 2]].norm();
1304        let scale = (a[[p - 2, p - 2]].norm() + a[[p - 1, p - 1]].norm()).max(1e-300);
1305        if sub <= 1e-14 * scale {
1306            eigenvalues.push(a[[p - 1, p - 1]]);
1307            p -= 1;
1308            continue;
1309        }
1310        if total_iter >= max_total_iter {
1311            break;
1312        }
1313        total_iter += 1;
1314
1315        // Wilkinson shift: eigenvalue of the trailing 2×2 closest to a[p-1,p-1].
1316        let a11 = a[[p - 2, p - 2]];
1317        let a12 = a[[p - 2, p - 1]];
1318        let a21 = a[[p - 1, p - 2]];
1319        let a22 = a[[p - 1, p - 1]];
1320        let trace = a11 + a22;
1321        let det = a11 * a22 - a12 * a21;
1322        let disc = (trace * trace - four * det).sqrt();
1323        let mu1 = (trace + disc) / two;
1324        let mu2 = (trace - disc) / two;
1325        let shift = if (mu1 - a22).norm() <= (mu2 - a22).norm() {
1326            mu1
1327        } else {
1328            mu2
1329        };
1330
1331        // One shifted QR sweep on the active p×p block: A_active <- R·Q + shift·I.
1332        let mut block = Array2::<Complex64>::zeros((p, p));
1333        for i in 0..p {
1334            for j in 0..p {
1335                block[[i, j]] = a[[i, j]];
1336            }
1337        }
1338        for i in 0..p {
1339            block[[i, i]] -= shift;
1340        }
1341        let (q, r) = qr_decompose_complex(&block);
1342        let rq = r.dot(&q);
1343        for i in 0..p {
1344            for j in 0..p {
1345                a[[i, j]] = rq[[i, j]];
1346            }
1347        }
1348        for i in 0..p {
1349            a[[i, i]] += shift;
1350        }
1351    }
1352
1353    // Remaining active diagonal entries (converged 1×1, or an unconverged block).
1354    for i in 0..p {
1355        eigenvalues.push(a[[i, i]]);
1356    }
1357
1358    // Recover eigenvectors from the original matrix via inverse iteration.
1359    let mut vectors = Array2::<Complex64>::zeros((n, n));
1360    for (col, &lambda) in eigenvalues.iter().enumerate() {
1361        let vec = inverse_iteration_complex(input, lambda)?;
1362        for i in 0..n {
1363            vectors[[i, col]] = vec[i];
1364        }
1365    }
1366
1367    Ok((eigenvalues, vectors))
1368}
1369
1370/// Utilities for creating sparse matrices from quantum problems
1371pub struct SparseMatrixUtils;
1372
1373impl SparseMatrixUtils {
1374    /// Create sparse Hamiltonian from Pauli strings
1375    pub fn hamiltonian_from_pauli_strings(
1376        num_qubits: usize,
1377        pauli_strings: &[(String, f64)],
1378    ) -> Result<SparseMatrix> {
1379        let dim = 1 << num_qubits;
1380        let mut builder = SparseMatrixBuilder::new(dim, dim);
1381
1382        for (pauli_str, coeff) in pauli_strings {
1383            if pauli_str.len() != num_qubits {
1384                return Err(SimulatorError::InvalidInput(format!(
1385                    "Pauli string length {} doesn't match num_qubits {}",
1386                    pauli_str.len(),
1387                    num_qubits
1388                )));
1389            }
1390
1391            // Build Pauli string matrix and add to Hamiltonian
1392            for i in 0..dim {
1393                let mut amplitude = Complex64::new(*coeff, 0.0);
1394                let mut target_state = i;
1395
1396                for (qubit, pauli_char) in pauli_str.chars().enumerate() {
1397                    let bit_pos = num_qubits - 1 - qubit;
1398                    let bit_val = (i >> bit_pos) & 1;
1399
1400                    match pauli_char {
1401                        'I' => {} // Identity - no change
1402                        'X' => {
1403                            target_state ^= 1 << bit_pos; // Flip bit
1404                        }
1405                        'Y' => {
1406                            target_state ^= 1 << bit_pos; // Flip bit
1407                            if bit_val == 0 {
1408                                amplitude *= Complex64::new(0.0, 1.0); // i
1409                            } else {
1410                                amplitude *= Complex64::new(0.0, -1.0); // -i
1411                            }
1412                        }
1413                        'Z' => {
1414                            if bit_val == 1 {
1415                                amplitude *= Complex64::new(-1.0, 0.0); // -1
1416                            }
1417                        }
1418                        _ => {
1419                            return Err(SimulatorError::InvalidInput(format!(
1420                                "Invalid Pauli character: {pauli_char}"
1421                            )));
1422                        }
1423                    }
1424                }
1425
1426                if amplitude.norm() > 1e-15 {
1427                    builder.add(i, target_state, amplitude);
1428                }
1429            }
1430        }
1431
1432        let csr_matrix = builder.build();
1433        let nnz = csr_matrix.values.len();
1434        let mut matrix = SparseMatrix {
1435            shape: (dim, dim),
1436            format: SparseFormat::CSR,
1437            row_ptr: csr_matrix.row_ptr,
1438            col_indices: csr_matrix.col_indices,
1439            values: csr_matrix.values,
1440            nnz,
1441            is_hermitian: true,
1442            is_positive_definite: false,
1443        };
1444        matrix.is_hermitian = true;
1445
1446        Ok(matrix)
1447    }
1448
1449    /// Create sparse matrix from dense matrix
1450    #[must_use]
1451    pub fn from_dense(dense: &Array2<Complex64>, threshold: f64) -> SparseMatrix {
1452        let (rows, cols) = dense.dim();
1453        let mut row_ptr = vec![0; rows + 1];
1454        let mut col_indices = Vec::new();
1455        let mut values = Vec::new();
1456
1457        let mut nnz = 0;
1458        for i in 0..rows {
1459            row_ptr[i] = nnz;
1460            for j in 0..cols {
1461                if dense[[i, j]].norm() > threshold {
1462                    col_indices.push(j);
1463                    values.push(dense[[i, j]]);
1464                    nnz += 1;
1465                }
1466            }
1467        }
1468        row_ptr[rows] = nnz;
1469
1470        SparseMatrix::from_csr((rows, cols), row_ptr, col_indices, values)
1471    }
1472
1473    /// Create random sparse matrix for testing
1474    #[must_use]
1475    pub fn random_sparse(n: usize, density: f64, hermitian: bool) -> SparseMatrix {
1476        let total_elements = n * n;
1477        let nnz_target = (total_elements as f64 * density) as usize;
1478
1479        let mut row_ptr = vec![0; n + 1];
1480        let mut col_indices = Vec::new();
1481        let mut values = Vec::new();
1482
1483        let mut added_elements = std::collections::HashSet::new();
1484        let mut nnz = 0;
1485
1486        for _ in 0..nnz_target {
1487            let i = fastrand::usize(0..n);
1488            let j = if hermitian && fastrand::f64() < 0.5 && i < n - 1 {
1489                fastrand::usize(i..n) // Upper triangular for Hermitian
1490            } else {
1491                fastrand::usize(0..n)
1492            };
1493
1494            if added_elements.insert((i, j)) {
1495                let real = fastrand::f64() - 0.5;
1496                let imag = if hermitian && i == j {
1497                    0.0
1498                } else {
1499                    fastrand::f64() - 0.5
1500                };
1501                let value = Complex64::new(real, imag);
1502
1503                // Add to appropriate row
1504                let pos = col_indices
1505                    .iter()
1506                    .position(|&col| col > j)
1507                    .unwrap_or(col_indices.len());
1508                col_indices.insert(pos, j);
1509                values.insert(pos, value);
1510                nnz += 1;
1511
1512                // Update row pointers
1513                for row in (i + 1)..=n {
1514                    row_ptr[row] += 1;
1515                }
1516
1517                // Add symmetric element for Hermitian matrices
1518                if hermitian && i != j {
1519                    added_elements.insert((j, i));
1520
1521                    let sym_value = value.conj();
1522                    let sym_pos = col_indices
1523                        .iter()
1524                        .position(|&col| col > i)
1525                        .unwrap_or(col_indices.len());
1526                    col_indices.insert(sym_pos, i);
1527                    values.insert(sym_pos, sym_value);
1528                    nnz += 1;
1529
1530                    for row in (j + 1)..=n {
1531                        row_ptr[row] += 1;
1532                    }
1533                }
1534            }
1535        }
1536
1537        let mut matrix = SparseMatrix::from_csr((n, n), row_ptr, col_indices, values);
1538        matrix.is_hermitian = hermitian;
1539        matrix
1540    }
1541}
1542
1543/// Benchmark sparse solver methods
1544pub fn benchmark_sparse_solvers(
1545    matrix_size: usize,
1546    density: f64,
1547) -> Result<HashMap<String, SparseSolverStats>> {
1548    let mut results = HashMap::new();
1549
1550    // Create test matrix and RHS
1551    let matrix = SparseMatrixUtils::random_sparse(matrix_size, density, true);
1552    let mut rhs = Array1::zeros(matrix_size);
1553    for i in 0..matrix_size {
1554        rhs[i] = Complex64::new(fastrand::f64(), 0.0);
1555    }
1556
1557    // Test different solver methods
1558    let methods = vec![
1559        ("CG", SparseSolverMethod::CG),
1560        ("GMRES", SparseSolverMethod::GMRES),
1561        ("BiCGSTAB", SparseSolverMethod::BiCGSTAB),
1562        ("SciRS2Auto", SparseSolverMethod::SciRS2Auto),
1563    ];
1564
1565    for (name, method) in methods {
1566        let config = SparseSolverConfig {
1567            method,
1568            tolerance: 1e-10,
1569            max_iterations: 1000,
1570            ..Default::default()
1571        };
1572
1573        let mut solver = SciRS2SparseSolver::new(config.clone())?;
1574        if method == SparseSolverMethod::SciRS2Auto {
1575            solver = solver.with_backend().unwrap_or_else(|_| {
1576                SciRS2SparseSolver::new(config).expect("fallback solver creation should succeed")
1577            });
1578        }
1579
1580        let _solution = solver.solve_linear_system(&matrix, &rhs)?;
1581        results.insert(name.to_string(), solver.get_stats().clone());
1582    }
1583
1584    Ok(results)
1585}
1586
1587/// Compare solver accuracy
1588pub fn compare_sparse_solver_accuracy(matrix_size: usize) -> Result<HashMap<String, f64>> {
1589    let mut errors = HashMap::new();
1590
1591    // Create test problem with known solution
1592    let matrix = SparseMatrix::identity(matrix_size);
1593    let solution = Array1::from_vec(
1594        (0..matrix_size)
1595            .map(|i| Complex64::new(i as f64, 0.0))
1596            .collect(),
1597    );
1598    let rhs = matrix.matvec(&solution)?;
1599
1600    let methods = vec![
1601        ("CG", SparseSolverMethod::CG),
1602        ("GMRES", SparseSolverMethod::GMRES),
1603        ("BiCGSTAB", SparseSolverMethod::BiCGSTAB),
1604    ];
1605
1606    for (name, method) in methods {
1607        let config = SparseSolverConfig {
1608            method,
1609            tolerance: 1e-12,
1610            max_iterations: 1000,
1611            ..Default::default()
1612        };
1613
1614        let mut solver = SciRS2SparseSolver::new(config)?;
1615        let computed_solution = solver.solve_linear_system(&matrix, &rhs)?;
1616
1617        // Calculate error
1618        let error = solution
1619            .iter()
1620            .zip(computed_solution.iter())
1621            .map(|(exact, computed)| (exact - computed).norm())
1622            .sum::<f64>()
1623            / matrix_size as f64;
1624
1625        errors.insert(name.to_string(), error);
1626    }
1627
1628    Ok(errors)
1629}
1630
1631#[cfg(test)]
1632mod tests {
1633    use super::*;
1634    use approx::assert_abs_diff_eq;
1635
1636    #[test]
1637    fn test_sparse_matrix_creation() {
1638        let matrix = SparseMatrix::identity(5);
1639        assert_eq!(matrix.shape, (5, 5));
1640        assert_eq!(matrix.nnz, 5);
1641        assert!(matrix.is_hermitian);
1642        assert!(matrix.is_positive_definite);
1643    }
1644
1645    #[test]
1646    fn test_sparse_matrix_matvec() {
1647        let matrix = SparseMatrix::identity(3);
1648        let x = Array1::from_vec(vec![
1649            Complex64::new(1.0, 0.0),
1650            Complex64::new(2.0, 0.0),
1651            Complex64::new(3.0, 0.0),
1652        ]);
1653
1654        let y = matrix.matvec(&x).expect("matvec should succeed");
1655
1656        for i in 0..3 {
1657            assert_abs_diff_eq!(y[i].re, x[i].re, epsilon = 1e-10);
1658            assert_abs_diff_eq!(y[i].im, x[i].im, epsilon = 1e-10);
1659        }
1660    }
1661
1662    #[test]
1663    fn test_sparse_solver_creation() {
1664        let config = SparseSolverConfig::default();
1665        let solver = SciRS2SparseSolver::new(config).expect("solver creation should succeed");
1666        assert!(solver.backend.is_none());
1667    }
1668
1669    #[test]
1670    fn test_identity_solve() {
1671        let matrix = SparseMatrix::identity(5);
1672        let rhs = Array1::from_vec((0..5).map(|i| Complex64::new(i as f64, 0.0)).collect());
1673
1674        let config = SparseSolverConfig {
1675            method: SparseSolverMethod::CG,
1676            tolerance: 1e-10,
1677            max_iterations: 100,
1678            ..Default::default()
1679        };
1680
1681        let mut solver = SciRS2SparseSolver::new(config).expect("solver creation should succeed");
1682        let solution = solver
1683            .solve_linear_system(&matrix, &rhs)
1684            .expect("solve_linear_system should succeed");
1685
1686        for i in 0..5 {
1687            assert_abs_diff_eq!(solution[i].re, rhs[i].re, epsilon = 1e-8);
1688            assert_abs_diff_eq!(solution[i].im, rhs[i].im, epsilon = 1e-8);
1689        }
1690    }
1691
1692    #[test]
1693    fn test_pauli_hamiltonian_creation() {
1694        let pauli_strings = vec![("ZZ".to_string(), 1.0), ("XX".to_string(), 0.5)];
1695
1696        let matrix = SparseMatrixUtils::hamiltonian_from_pauli_strings(2, &pauli_strings)
1697            .expect("hamiltonian_from_pauli_strings should succeed");
1698
1699        assert_eq!(matrix.shape, (4, 4));
1700        assert!(matrix.is_hermitian);
1701        assert!(matrix.nnz > 0);
1702    }
1703
1704    #[test]
1705    fn test_random_sparse_matrix() {
1706        let matrix = SparseMatrixUtils::random_sparse(10, 0.1, true);
1707
1708        assert_eq!(matrix.shape, (10, 10));
1709        assert!(matrix.is_hermitian);
1710        assert!(matrix.density() <= 0.25); // Allow more margin due to randomness and Hermitian constraint
1711    }
1712
1713    #[test]
1714    fn test_dense_conversion() {
1715        let matrix = SparseMatrix::identity(3);
1716        let dense = matrix.to_dense().expect("to_dense should succeed");
1717
1718        assert_eq!(dense.shape(), [3, 3]);
1719        for i in 0..3 {
1720            for j in 0..3 {
1721                if i == j {
1722                    assert_abs_diff_eq!(dense[[i, j]].re, 1.0, epsilon = 1e-10);
1723                } else {
1724                    assert_abs_diff_eq!(dense[[i, j]].norm(), 0.0, epsilon = 1e-10);
1725                }
1726            }
1727        }
1728    }
1729}