Skip to main content

quantrs2_device/process_tomography/
fallback.rs

1//! Fallback implementations when SciRS2 is not available
2//!
3//! These are used (via the `#[cfg(not(feature = "scirs2"))]` call sites in
4//! `reconstruction/linear_inversion.rs` and friends) only when the crate is
5//! built with `--no-default-features` (i.e. without `scirs2-linalg`). Every
6//! matrix operation below is a real, dimension-correct (if numerically
7//! naive) implementation rather than a fixed-size placeholder, so a
8//! no-scirs2 build computes with the actual input shape instead of silently
9//! substituting a hardcoded 2x2 result. The handful of decompositions that
10//! are genuinely impractical to hand-roll correctly (full eigenvalue
11//! decomposition, SVD) honestly return `Err` instead.
12
13use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
14
15/// Fallback statistical mean calculation
16pub const fn mean(_data: &ArrayView1<f64>) -> Result<f64, String> {
17    Ok(0.0)
18}
19
20/// Fallback standard deviation calculation
21pub const fn std(_data: &ArrayView1<f64>, _ddof: i32) -> Result<f64, String> {
22    Ok(1.0)
23}
24
25/// Fallback Pearson correlation calculation
26pub const fn pearsonr(
27    _x: &ArrayView1<f64>,
28    _y: &ArrayView1<f64>,
29    _alt: &str,
30) -> Result<(f64, f64), String> {
31    Ok((0.0, 0.5))
32}
33
34/// Real matrix trace: sum of the diagonal entries (dimension-correct for
35/// any `min(rows, cols)`-length diagonal, not just 2x2).
36pub fn trace(matrix: &ArrayView2<f64>) -> Result<f64, String> {
37    let n = matrix.nrows().min(matrix.ncols());
38    Ok((0..n).map(|i| matrix[[i, i]]).sum())
39}
40
41/// Real matrix inversion via Gauss-Jordan elimination with partial
42/// pivoting. Works for any square NxN matrix (not just 2x2); returns an
43/// honest error for non-square or singular/near-singular input.
44pub fn inv(matrix: &ArrayView2<f64>) -> Result<Array2<f64>, String> {
45    let n = matrix.nrows();
46    if matrix.ncols() != n {
47        return Err(format!(
48            "inv: matrix must be square, got {}x{}",
49            matrix.nrows(),
50            matrix.ncols()
51        ));
52    }
53
54    let mut a = matrix.to_owned();
55    let mut inv_mat = Array2::<f64>::eye(n);
56
57    for col in 0..n {
58        let mut pivot_row = col;
59        let mut max_val = a[[col, col]].abs();
60        for row in (col + 1)..n {
61            if a[[row, col]].abs() > max_val {
62                max_val = a[[row, col]].abs();
63                pivot_row = row;
64            }
65        }
66        if max_val < 1e-12 {
67            return Err(format!(
68                "inv: matrix is singular or nearly singular (pivot magnitude {max_val:.3e} at column {col})"
69            ));
70        }
71        if pivot_row != col {
72            for k in 0..n {
73                a.swap((col, k), (pivot_row, k));
74                inv_mat.swap((col, k), (pivot_row, k));
75            }
76        }
77
78        let pivot_val = a[[col, col]];
79        for k in 0..n {
80            a[[col, k]] /= pivot_val;
81            inv_mat[[col, k]] /= pivot_val;
82        }
83
84        for row in 0..n {
85            if row != col {
86                let factor = a[[row, col]];
87                if factor != 0.0 {
88                    for k in 0..n {
89                        let a_col_k = a[[col, k]];
90                        let inv_col_k = inv_mat[[col, k]];
91                        a[[row, k]] -= factor * a_col_k;
92                        inv_mat[[row, k]] -= factor * inv_col_k;
93                    }
94                }
95            }
96        }
97    }
98
99    Ok(inv_mat)
100}
101
102/// Fallback optimization result
103pub struct OptimizeResult {
104    pub x: Array1<f64>,
105    pub fun: f64,
106    pub success: bool,
107    pub nit: usize,
108}
109
110/// Fallback optimization function
111pub fn minimize(
112    _func: fn(&Array1<f64>) -> f64,
113    _x0: &Array1<f64>,
114    _method: &str,
115) -> Result<OptimizeResult, String> {
116    Ok(OptimizeResult {
117        x: Array1::zeros(2),
118        fun: 0.0,
119        success: true,
120        nit: 0,
121    })
122}
123
124/// Full eigenvalue decomposition is not implemented in this no-SciRS2
125/// fallback (a numerically stable general eigensolver -- e.g. the
126/// shifted-QR algorithm with deflation -- is well beyond a "naive"
127/// reimplementation); honestly report the limitation rather than return a
128/// fixed, wrong-sized identity-like result.
129pub fn eig(
130    matrix: &ArrayView2<f64>,
131) -> Result<
132    (
133        Array1<scirs2_core::Complex64>,
134        Array2<scirs2_core::Complex64>,
135    ),
136    String,
137> {
138    Err(format!(
139        "eig: full eigenvalue decomposition is not implemented in the no-scirs2 fallback \
140         (requested for a {}x{} matrix); rebuild with the `scirs2` feature to use this operation",
141        matrix.nrows(),
142        matrix.ncols()
143    ))
144}
145
146/// Real matrix determinant via LU decomposition with partial pivoting.
147/// Works for any square NxN matrix (not just 2x2).
148pub fn det(matrix: &ArrayView2<f64>) -> Result<f64, String> {
149    let n = matrix.nrows();
150    if matrix.ncols() != n {
151        return Err(format!(
152            "det: matrix must be square, got {}x{}",
153            matrix.nrows(),
154            matrix.ncols()
155        ));
156    }
157
158    let mut a = matrix.to_owned();
159    let mut sign = 1.0_f64;
160
161    for col in 0..n {
162        let mut pivot_row = col;
163        let mut max_val = a[[col, col]].abs();
164        for row in (col + 1)..n {
165            if a[[row, col]].abs() > max_val {
166                max_val = a[[row, col]].abs();
167                pivot_row = row;
168            }
169        }
170        if max_val < 1e-14 {
171            return Ok(0.0); // Singular matrix: determinant is zero.
172        }
173        if pivot_row != col {
174            for k in 0..n {
175                a.swap((col, k), (pivot_row, k));
176            }
177            sign = -sign;
178        }
179
180        let pivot_val = a[[col, col]];
181        for row in (col + 1)..n {
182            let factor = a[[row, col]] / pivot_val;
183            if factor != 0.0 {
184                for k in col..n {
185                    let a_col_k = a[[col, k]];
186                    a[[row, k]] -= factor * a_col_k;
187                }
188            }
189        }
190    }
191
192    let mut det_val = sign;
193    for i in 0..n {
194        det_val *= a[[i, i]];
195    }
196    Ok(det_val)
197}
198
199/// Real QR decomposition via modified Gram-Schmidt orthogonalization. Works
200/// for any MxN matrix (not just 2x2).
201pub fn qr(matrix: &ArrayView2<f64>) -> Result<(Array2<f64>, Array2<f64>), String> {
202    let m = matrix.nrows();
203    let n = matrix.ncols();
204    if m == 0 || n == 0 {
205        return Err(format!("qr: matrix must be non-empty, got {m}x{n}"));
206    }
207
208    let mut q = Array2::<f64>::zeros((m, n));
209    let mut r = Array2::<f64>::zeros((n, n));
210
211    for j in 0..n {
212        let mut v = matrix.column(j).to_owned();
213        for i in 0..j {
214            let qi = q.column(i).to_owned();
215            let r_ij = qi.dot(&matrix.column(j));
216            r[[i, j]] = r_ij;
217            for k in 0..m {
218                v[k] -= r_ij * qi[k];
219            }
220        }
221        let norm = v.dot(&v).sqrt();
222        r[[j, j]] = norm;
223        if norm > 1e-14 {
224            for k in 0..m {
225                q[[k, j]] = v[k] / norm;
226            }
227        }
228    }
229
230    Ok((q, r))
231}
232
233/// Singular value decomposition is not implemented in this no-SciRS2
234/// fallback (a numerically stable general SVD -- e.g. Golub-Kahan
235/// bidiagonalization followed by an implicit-shift QR sweep -- is well
236/// beyond a "naive" reimplementation); honestly report the limitation
237/// rather than return a fixed, wrong-sized identity-like result.
238pub fn svd(matrix: &ArrayView2<f64>) -> Result<(Array2<f64>, Array1<f64>, Array2<f64>), String> {
239    Err(format!(
240        "svd: singular value decomposition is not implemented in the no-scirs2 fallback \
241         (requested for a {}x{} matrix); rebuild with the `scirs2` feature to use this operation",
242        matrix.nrows(),
243        matrix.ncols()
244    ))
245}
246
247/// Fallback matrix norm calculation: the Frobenius norm, computed from the
248/// actual matrix entries (not a fixed constant). Only the Frobenius norm is
249/// supported here -- spectral/nuclear norms would require SVD, which this
250/// no-scirs2 fallback does not implement.
251pub fn matrix_norm(matrix: &ArrayView2<f64>, _ord: Option<&str>) -> Result<f64, String> {
252    Ok(matrix.iter().map(|v| v * v).sum::<f64>().sqrt())
253}
254
255/// Real Cholesky decomposition (lower-triangular `L` such that `L L^T =
256/// matrix`). Works for any square NxN symmetric positive-definite matrix
257/// (not just 2x2); returns an honest error if the matrix is not
258/// positive-definite.
259pub fn cholesky(matrix: &ArrayView2<f64>) -> Result<Array2<f64>, String> {
260    let n = matrix.nrows();
261    if matrix.ncols() != n {
262        return Err(format!(
263            "cholesky: matrix must be square, got {}x{}",
264            matrix.nrows(),
265            matrix.ncols()
266        ));
267    }
268
269    let mut l = Array2::<f64>::zeros((n, n));
270    for i in 0..n {
271        for j in 0..=i {
272            let mut sum = 0.0;
273            for k in 0..j {
274                sum += l[[i, k]] * l[[j, k]];
275            }
276            if i == j {
277                let val = matrix[[i, i]] - sum;
278                if val <= 0.0 {
279                    return Err(format!(
280                        "cholesky: matrix is not positive-definite (diagonal term {val:.3e} <= 0 at index {i})"
281                    ));
282                }
283                l[[i, j]] = val.sqrt();
284            } else {
285                l[[i, j]] = (matrix[[i, j]] - sum) / l[[j, j]];
286            }
287        }
288    }
289
290    Ok(l)
291}
292
293/// Fallback variance calculation
294pub const fn var(_data: &ArrayView1<f64>, _ddof: i32) -> Result<f64, String> {
295    Ok(1.0)
296}
297
298/// Real 2x2 Pearson correlation matrix for two equal-length samples `x`/`y`
299/// (a pairwise correlation matrix for two variables is always 2x2, so
300/// unlike `inv`/`qr`/etc. the *size* here was never the issue -- the value
301/// was; this now computes the actual correlation instead of a fixed
302/// identity matrix).
303pub fn corrcoef(x: &ArrayView1<f64>, y: &ArrayView1<f64>) -> Result<Array2<f64>, String> {
304    if x.len() != y.len() {
305        return Err(format!(
306            "corrcoef: input length mismatch ({} vs {})",
307            x.len(),
308            y.len()
309        ));
310    }
311    if x.len() < 2 {
312        return Err("corrcoef: need at least 2 samples".to_string());
313    }
314
315    let n = x.len() as f64;
316    let mean_x = x.sum() / n;
317    let mean_y = y.sum() / n;
318
319    let mut cov_xx = 0.0_f64;
320    let mut cov_yy = 0.0_f64;
321    let mut cov_xy = 0.0_f64;
322    for i in 0..x.len() {
323        let dx = x[i] - mean_x;
324        let dy = y[i] - mean_y;
325        cov_xx += dx * dx;
326        cov_yy += dy * dy;
327        cov_xy += dx * dy;
328    }
329
330    let denom = (cov_xx * cov_yy).sqrt();
331    let r = if denom > 1e-14 {
332        (cov_xy / denom).clamp(-1.0, 1.0)
333    } else {
334        0.0
335    };
336
337    Array2::from_shape_vec((2, 2), vec![1.0, r, r, 1.0])
338        .map_err(|e| format!("Array creation error: {e}"))
339}
340
341/// Fallback Spearman correlation calculation
342pub const fn spearmanr(
343    _x: &ArrayView1<f64>,
344    _y: &ArrayView1<f64>,
345    _alternative: &str,
346) -> Result<(f64, f64), String> {
347    Ok((0.0, 0.5))
348}
349
350/// Fallback t-test (one sample)
351pub const fn ttest_1samp(
352    _a: &ArrayView1<f64>,
353    _popmean: f64,
354    _alternative: &str,
355) -> Result<TTestResult, String> {
356    Ok(TTestResult {
357        statistic: 0.0,
358        pvalue: 0.5,
359    })
360}
361
362/// Fallback t-test (independent samples)
363pub const fn ttest_ind(
364    _a: &ArrayView1<f64>,
365    _b: &ArrayView1<f64>,
366    _alternative: &str,
367) -> Result<TTestResult, String> {
368    Ok(TTestResult {
369        statistic: 0.0,
370        pvalue: 0.5,
371    })
372}
373
374/// Fallback Kolmogorov-Smirnov 2-sample test
375pub const fn ks_2samp(
376    _data1: &ArrayView1<f64>,
377    _data2: &ArrayView1<f64>,
378    _alternative: &str,
379) -> Result<KSTestResult, String> {
380    Ok(KSTestResult {
381        statistic: 0.0,
382        pvalue: 0.5,
383    })
384}
385
386/// Fallback Shapiro-Wilk test
387pub const fn shapiro_wilk(_data: &ArrayView1<f64>) -> Result<SWTestResult, String> {
388    Ok(SWTestResult {
389        statistic: 0.95,
390        pvalue: 0.1,
391    })
392}
393
394/// T-test result structure
395#[derive(Debug, Clone)]
396pub struct TTestResult {
397    pub statistic: f64,
398    pub pvalue: f64,
399}
400
401/// Kolmogorov-Smirnov test result structure
402#[derive(Debug, Clone)]
403pub struct KSTestResult {
404    pub statistic: f64,
405    pub pvalue: f64,
406}
407
408/// Shapiro-Wilk test result structure
409#[derive(Debug, Clone)]
410pub struct SWTestResult {
411    pub statistic: f64,
412    pub pvalue: f64,
413}
414
415/// Alternative hypothesis type
416#[derive(Debug, Clone)]
417pub enum Alternative {
418    TwoSided,
419    Less,
420    Greater,
421}
422
423/// Distribution types and functions
424pub mod distributions {
425    use super::*;
426
427    /// Normal distribution functions
428    pub mod norm {
429        /// Normal PDF
430        pub const fn pdf(_x: f64, _loc: f64, _scale: f64) -> f64 {
431            0.4
432        }
433
434        /// Normal CDF
435        pub const fn cdf(_x: f64, _loc: f64, _scale: f64) -> f64 {
436            0.5
437        }
438
439        /// Normal PPF (inverse CDF)
440        pub const fn ppf(_q: f64, _loc: f64, _scale: f64) -> f64 {
441            0.0
442        }
443    }
444
445    /// Chi-squared distribution functions
446    pub mod chi2 {
447        /// Chi-squared PDF
448        pub const fn pdf(_x: f64, _df: f64) -> f64 {
449            0.1
450        }
451
452        /// Chi-squared CDF
453        pub const fn cdf(_x: f64, _df: f64) -> f64 {
454            0.5
455        }
456
457        /// Chi-squared PPF
458        pub const fn ppf(_q: f64, _df: f64) -> f64 {
459            1.0
460        }
461    }
462
463    /// Gamma distribution functions
464    pub mod gamma {
465        /// Gamma PDF
466        pub const fn pdf(_x: f64, _a: f64, _scale: f64) -> f64 {
467            0.2
468        }
469
470        /// Gamma CDF
471        pub const fn cdf(_x: f64, _a: f64, _scale: f64) -> f64 {
472            0.5
473        }
474
475        /// Gamma PPF
476        pub const fn ppf(_q: f64, _a: f64, _scale: f64) -> f64 {
477            1.0
478        }
479    }
480}
481
482/// Graph analysis fallback functions
483pub mod graph {
484    use super::*;
485
486    /// Fallback betweenness centrality
487    pub fn betweenness_centrality(_graph: &Array2<f64>) -> Result<Array1<f64>, String> {
488        Ok(Array1::ones(2))
489    }
490
491    /// Fallback closeness centrality
492    pub fn closeness_centrality(_graph: &Array2<f64>) -> Result<Array1<f64>, String> {
493        Ok(Array1::ones(2))
494    }
495
496    /// Fallback minimum spanning tree
497    pub fn minimum_spanning_tree(_graph: &Array2<f64>) -> Result<Array2<f64>, String> {
498        Ok(Array2::eye(2))
499    }
500
501    /// Fallback shortest path
502    pub fn shortest_path(
503        _graph: &Array2<f64>,
504        _start: usize,
505        _end: usize,
506    ) -> Result<Vec<usize>, String> {
507        Ok(vec![0, 1])
508    }
509
510    /// Fallback strongly connected components
511    pub fn strongly_connected_components(_graph: &Array2<f64>) -> Result<Vec<Vec<usize>>, String> {
512        Ok(vec![vec![0], vec![1]])
513    }
514
515    /// Graph structure placeholder
516    pub struct Graph {
517        pub adjacency_matrix: Array2<f64>,
518    }
519
520    impl Graph {
521        pub const fn new(adjacency_matrix: Array2<f64>) -> Self {
522            Self { adjacency_matrix }
523        }
524    }
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_trace_scales_with_matrix_size() {
533        // A fixed-2x2 stub would ignore this 3x3 matrix; the real
534        // implementation must sum all three diagonal entries.
535        let m = Array2::from_shape_vec((3, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0])
536            .unwrap();
537        let t = trace(&m.view()).expect("trace should succeed");
538        assert!((t - 15.0).abs() < 1e-9, "expected trace 15.0, got {t}");
539    }
540
541    #[test]
542    fn test_det_identity_and_known_matrix() {
543        let identity = Array2::<f64>::eye(4);
544        let d = det(&identity.view()).expect("det should succeed");
545        assert!((d - 1.0).abs() < 1e-9);
546
547        // det([[2, 0], [0, 3]]) = 6
548        let diag = Array2::from_shape_vec((2, 2), vec![2.0, 0.0, 0.0, 3.0]).unwrap();
549        let d2 = det(&diag.view()).expect("det should succeed");
550        assert!((d2 - 6.0).abs() < 1e-9, "expected 6.0, got {d2}");
551    }
552
553    #[test]
554    fn test_inv_recovers_identity_for_arbitrary_size() {
555        // A fixed-2x2 stub would corrupt this 3x3 inversion; the real
556        // Gauss-Jordan implementation must produce a genuine inverse.
557        let m = Array2::from_shape_vec((3, 3), vec![2.0, 0.0, 0.0, 0.0, 3.0, 0.0, 0.0, 0.0, 4.0])
558            .unwrap();
559        let inverse = inv(&m.view()).expect("inv should succeed for a well-conditioned matrix");
560        let product = m.dot(&inverse);
561        let identity = Array2::<f64>::eye(3);
562        for i in 0..3 {
563            for j in 0..3 {
564                assert!(
565                    (product[[i, j]] - identity[[i, j]]).abs() < 1e-9,
566                    "M * M^-1 should be the identity at [{i},{j}]: got {}",
567                    product[[i, j]]
568                );
569            }
570        }
571    }
572
573    #[test]
574    fn test_inv_rejects_singular_matrix() {
575        let singular = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 2.0, 4.0]).unwrap();
576        assert!(
577            inv(&singular.view()).is_err(),
578            "inverting a singular matrix must return an honest error, not a fabricated result"
579        );
580    }
581
582    #[test]
583    fn test_qr_reconstructs_original_matrix() {
584        let m = Array2::from_shape_vec((3, 2), vec![1.0, 0.0, 0.0, 1.0, 1.0, 1.0]).unwrap();
585        let (q, r) = qr(&m.view()).expect("qr should succeed");
586        let reconstructed = q.dot(&r);
587        for i in 0..3 {
588            for j in 0..2 {
589                assert!(
590                    (reconstructed[[i, j]] - m[[i, j]]).abs() < 1e-9,
591                    "QR reconstruction mismatch at [{i},{j}]"
592                );
593            }
594        }
595    }
596
597    #[test]
598    fn test_cholesky_reconstructs_positive_definite_matrix() {
599        // A = [[4, 2], [2, 3]] is symmetric positive-definite.
600        let a = Array2::from_shape_vec((2, 2), vec![4.0, 2.0, 2.0, 3.0]).unwrap();
601        let l = cholesky(&a.view()).expect("cholesky should succeed for an SPD matrix");
602        let reconstructed = l.dot(&l.t());
603        for i in 0..2 {
604            for j in 0..2 {
605                assert!(
606                    (reconstructed[[i, j]] - a[[i, j]]).abs() < 1e-9,
607                    "L L^T should reconstruct A at [{i},{j}]"
608                );
609            }
610        }
611    }
612
613    #[test]
614    fn test_cholesky_rejects_non_positive_definite() {
615        let not_spd = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 2.0, 1.0]).unwrap();
616        assert!(
617            cholesky(&not_spd.view()).is_err(),
618            "cholesky of a non-positive-definite matrix must return an honest error"
619        );
620    }
621
622    #[test]
623    fn test_corrcoef_perfect_positive_correlation() {
624        let x = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
625        let y = Array1::from_vec(vec![2.0, 4.0, 6.0, 8.0]);
626        let c = corrcoef(&x.view(), &y.view()).expect("corrcoef should succeed");
627        assert!(
628            (c[[0, 1]] - 1.0).abs() < 1e-9,
629            "expected r=1.0, got {}",
630            c[[0, 1]]
631        );
632        assert!((c[[1, 0]] - 1.0).abs() < 1e-9);
633        assert!((c[[0, 0]] - 1.0).abs() < 1e-9);
634    }
635
636    #[test]
637    fn test_corrcoef_perfect_negative_correlation() {
638        let x = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
639        let y = Array1::from_vec(vec![8.0, 6.0, 4.0, 2.0]);
640        let c = corrcoef(&x.view(), &y.view()).expect("corrcoef should succeed");
641        assert!(
642            (c[[0, 1]] - (-1.0)).abs() < 1e-9,
643            "expected r=-1.0, got {}",
644            c[[0, 1]]
645        );
646    }
647
648    #[test]
649    fn test_matrix_norm_frobenius() {
650        // Frobenius norm of [[3, 0], [0, 4]] is sqrt(9 + 16) = 5.
651        let m = Array2::from_shape_vec((2, 2), vec![3.0, 0.0, 0.0, 4.0]).unwrap();
652        let norm = matrix_norm(&m.view(), None).expect("matrix_norm should succeed");
653        assert!((norm - 5.0).abs() < 1e-9, "expected 5.0, got {norm}");
654    }
655
656    #[test]
657    fn test_eig_and_svd_honestly_error_instead_of_fabricating() {
658        let m = Array2::<f64>::eye(3);
659        assert!(
660            eig(&m.view()).is_err(),
661            "eig fallback must honestly error rather than return a fixed 2x2 result for a 3x3 input"
662        );
663        assert!(
664            svd(&m.view()).is_err(),
665            "svd fallback must honestly error rather than return a fixed 2x2 result for a 3x3 input"
666        );
667    }
668}