Skip to main content

quantrs2_device/dynamical_decoupling/
fallback_scirs2.rs

1//! Fallback implementations for SciRS2 functions when the feature is not available
2
3use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
4use std::collections::HashMap;
5
6/// Fallback optimization result
7#[derive(Debug, Clone)]
8pub struct OptimizeResult {
9    /// Optimal parameters
10    pub x: Array1<f64>,
11    /// Optimal function value
12    pub fun: f64,
13    /// Number of iterations
14    pub nit: usize,
15    /// Number of function evaluations
16    pub nfev: usize,
17    /// Success flag
18    pub success: bool,
19    /// Status message
20    pub message: String,
21}
22
23/// Fallback minimize function
24pub fn minimize<F>(
25    objective: F,
26    initial: &Array1<f64>,
27    _method: &str,
28) -> Result<OptimizeResult, String>
29where
30    F: Fn(&Array1<f64>) -> f64,
31{
32    // Simple gradient-free optimization using Nelder-Mead-like approach
33    let mut current_x = initial.clone();
34    let mut current_f = objective(&current_x);
35    let n_params = initial.len();
36
37    let mut step_size = 0.1;
38    let max_iterations = 1000;
39    let tolerance = 1e-6;
40
41    let mut nfev = 1;
42
43    for _iteration in 0..max_iterations {
44        let mut improved = false;
45
46        // Try step in each parameter direction
47        for i in 0..n_params {
48            // Positive step
49            let mut test_x = current_x.clone();
50            test_x[i] += step_size;
51            let test_f = objective(&test_x);
52            nfev += 1;
53
54            if test_f < current_f {
55                current_x = test_x;
56                current_f = test_f;
57                improved = true;
58                continue;
59            }
60
61            // Negative step
62            let mut test_x = current_x.clone();
63            test_x[i] -= step_size;
64            let test_f = objective(&test_x);
65            nfev += 1;
66
67            if test_f < current_f {
68                current_x = test_x;
69                current_f = test_f;
70                improved = true;
71            }
72        }
73
74        if !improved {
75            // Reduce step size
76            step_size *= 0.5;
77            if step_size < tolerance {
78                break;
79            }
80        }
81    }
82
83    Ok(OptimizeResult {
84        x: current_x,
85        fun: current_f,
86        nit: max_iterations,
87        nfev,
88        success: true,
89        message: "Optimization completed".to_string(),
90    })
91}
92
93/// Fallback statistical functions
94pub fn mean(data: &ArrayView1<f64>) -> Result<f64, String> {
95    if data.is_empty() {
96        return Err("Cannot compute mean of empty array".to_string());
97    }
98    Ok(data.sum() / data.len() as f64)
99}
100
101pub fn std(data: &ArrayView1<f64>, ddof: i32, _workers: Option<usize>) -> Result<f64, String> {
102    if data.len() <= ddof as usize {
103        return Err("Insufficient data for standard deviation calculation".to_string());
104    }
105
106    let mean_val = mean(data)?;
107    let variance = data.iter().map(|x| (x - mean_val).powi(2)).sum::<f64>()
108        / (data.len() as f64 - ddof as f64);
109
110    Ok(variance.sqrt())
111}
112
113pub fn var(data: &ArrayView1<f64>, ddof: i32, _workers: Option<usize>) -> Result<f64, String> {
114    if data.len() <= ddof as usize {
115        return Err("Insufficient data for variance calculation".to_string());
116    }
117
118    let mean_val = mean(data)?;
119    let variance = data.iter().map(|x| (x - mean_val).powi(2)).sum::<f64>()
120        / (data.len() as f64 - ddof as f64);
121
122    Ok(variance)
123}
124
125pub fn pearsonr(x: &ArrayView1<f64>, y: &ArrayView1<f64>) -> Result<f64, String> {
126    if x.len() != y.len() {
127        return Err("Arrays must have same length".to_string());
128    }
129
130    if x.len() < 2 {
131        return Err("Need at least 2 data points".to_string());
132    }
133
134    let mean_x = mean(x)?;
135    let mean_y = mean(y)?;
136
137    let numerator: f64 = x
138        .iter()
139        .zip(y.iter())
140        .map(|(&xi, &yi)| (xi - mean_x) * (yi - mean_y))
141        .sum();
142
143    let sum_sq_x: f64 = x.iter().map(|&xi| (xi - mean_x).powi(2)).sum();
144    let sum_sq_y: f64 = y.iter().map(|&yi| (yi - mean_y).powi(2)).sum();
145
146    let denominator = (sum_sq_x * sum_sq_y).sqrt();
147
148    if denominator == 0.0 {
149        Ok(0.0)
150    } else {
151        Ok(numerator / denominator)
152    }
153}
154
155pub fn spearmanr(x: &ArrayView1<f64>, y: &ArrayView1<f64>) -> Result<f64, String> {
156    if x.len() != y.len() {
157        return Err("Arrays must have same length".to_string());
158    }
159
160    // Convert to ranks and compute Pearson correlation of ranks
161    let x_ranks = rank_array(x);
162    let y_ranks = rank_array(y);
163
164    let x_ranks_view = x_ranks.view();
165    let y_ranks_view = y_ranks.view();
166
167    pearsonr(&x_ranks_view, &y_ranks_view)
168}
169
170fn rank_array(data: &ArrayView1<f64>) -> Array1<f64> {
171    let mut indexed_data: Vec<(usize, f64)> =
172        data.iter().enumerate().map(|(i, &x)| (i, x)).collect();
173    indexed_data.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
174
175    let mut ranks = Array1::zeros(data.len());
176    for (rank, &(index, _)) in indexed_data.iter().enumerate() {
177        ranks[index] = rank as f64 + 1.0;
178    }
179
180    ranks
181}
182
183pub fn ttest_1samp(data: &ArrayView1<f64>, pop_mean: f64) -> Result<(f64, f64), String> {
184    if data.len() < 2 {
185        return Err("Need at least 2 data points for t-test".to_string());
186    }
187
188    let sample_mean = mean(data)?;
189    let sample_std = std(data, 1, None)?;
190    let n = data.len() as f64;
191
192    let t_statistic = (sample_mean - pop_mean) / (sample_std / n.sqrt());
193
194    // Simplified p-value calculation (assuming normal distribution)
195    let df = n - 1.0;
196    let p_value = 2.0 * (1.0 - normal_cdf(t_statistic.abs()));
197
198    Ok((t_statistic, p_value))
199}
200
201pub fn ks_2samp(x: &ArrayView1<f64>, y: &ArrayView1<f64>) -> Result<(f64, f64), String> {
202    if x.is_empty() || y.is_empty() {
203        return Err("Both samples must be non-empty".to_string());
204    }
205
206    let mut x_sorted = x.to_vec();
207    let mut y_sorted = y.to_vec();
208    x_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
209    y_sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
210
211    let mut all_values = x_sorted.clone();
212    all_values.extend_from_slice(&y_sorted);
213    all_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
214    all_values.dedup();
215
216    let mut max_diff = 0.0f64;
217
218    for &value in &all_values {
219        let cdf_x = x_sorted.iter().filter(|&&x| x <= value).count() as f64 / x_sorted.len() as f64;
220        let cdf_y = y_sorted.iter().filter(|&&y| y <= value).count() as f64 / y_sorted.len() as f64;
221        let diff = (cdf_x - cdf_y).abs();
222        max_diff = max_diff.max(diff);
223    }
224
225    // Simplified p-value calculation
226    let n_x = x.len() as f64;
227    let n_y = y.len() as f64;
228    let sqrt_term = ((n_x + n_y) / (n_x * n_y)).sqrt();
229    let ks_statistic = max_diff;
230    let p_value = 2.0f64 * (-2.0f64 * ks_statistic.powi(2) / sqrt_term.powi(2)).exp();
231
232    Ok((ks_statistic, p_value.min(1.0)))
233}
234
235pub fn shapiro_wilk(data: &ArrayView1<f64>) -> Result<(f64, f64), String> {
236    if data.len() < 3 || data.len() > 5000 {
237        return Err("Shapiro-Wilk test requires 3-5000 observations".to_string());
238    }
239
240    // Simplified implementation - just check if data looks roughly normal
241    let mean_val = mean(data)?;
242    let std_val = std(data, 1, None)?;
243
244    // Calculate skewness and kurtosis as rough normality indicators
245    let n = data.len() as f64;
246    let skewness = data
247        .iter()
248        .map(|&x| ((x - mean_val) / std_val).powi(3))
249        .sum::<f64>()
250        / n;
251
252    let kurtosis = data
253        .iter()
254        .map(|&x| ((x - mean_val) / std_val).powi(4))
255        .sum::<f64>()
256        / n
257        - 3.0;
258
259    // Simple heuristic for W statistic
260    let w_statistic = 1.0 - (skewness.abs() + kurtosis.abs()) / 10.0;
261    let w_statistic = w_statistic.clamp(0.0, 1.0);
262
263    // Simple p-value based on W statistic
264    let p_value = if w_statistic > 0.95 {
265        0.5
266    } else if w_statistic > 0.9 {
267        0.1
268    } else {
269        0.01
270    };
271
272    Ok((w_statistic, p_value))
273}
274
275pub fn percentile(data: &ArrayView1<f64>, percentile: f64) -> Result<f64, String> {
276    if data.is_empty() {
277        return Err("Cannot compute percentile of empty array".to_string());
278    }
279
280    if !(0.0..=100.0).contains(&percentile) {
281        return Err("Percentile must be between 0 and 100".to_string());
282    }
283
284    let mut sorted_data = data.to_vec();
285    sorted_data.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
286
287    let index = (percentile / 100.0) * (sorted_data.len() - 1) as f64;
288    let lower_index = index.floor() as usize;
289    let upper_index = index.ceil() as usize;
290
291    if lower_index == upper_index {
292        Ok(sorted_data[lower_index])
293    } else {
294        let weight = index - lower_index as f64;
295        Ok(sorted_data[lower_index].mul_add(1.0 - weight, sorted_data[upper_index] * weight))
296    }
297}
298
299pub fn trace(matrix: &ArrayView2<f64>) -> Result<f64, String> {
300    let (rows, cols) = matrix.dim();
301    if rows != cols {
302        return Err("Matrix must be square for trace calculation".to_string());
303    }
304
305    let mut trace_val = 0.0;
306    for i in 0..rows {
307        trace_val += matrix[(i, i)];
308    }
309
310    Ok(trace_val)
311}
312
313/// Real matrix inverse via Gauss-Jordan elimination with partial pivoting
314/// (pure Rust fallback for when the `scirs2` feature is disabled). Returns an
315/// honest error on non-square or singular matrices.
316pub fn inv(matrix: &ArrayView2<f64>) -> Result<Array2<f64>, String> {
317    let (rows, cols) = matrix.dim();
318    if rows != cols {
319        return Err("Matrix must be square for inversion".to_string());
320    }
321    let n = rows;
322    if n == 0 {
323        return Ok(Array2::zeros((0, 0)));
324    }
325
326    // Working copy of A and the identity that becomes A^-1.
327    let mut a = matrix.to_owned();
328    let mut inverse = Array2::<f64>::eye(n);
329
330    for col in 0..n {
331        // Partial pivot: pick the row with the largest magnitude in this column.
332        let mut pivot_row = col;
333        let mut pivot_val = a[(col, col)].abs();
334        for r in (col + 1)..n {
335            let v = a[(r, col)].abs();
336            if v > pivot_val {
337                pivot_val = v;
338                pivot_row = r;
339            }
340        }
341        if pivot_val <= f64::MIN_POSITIVE {
342            return Err("singular matrix".to_string());
343        }
344        if pivot_row != col {
345            for c in 0..n {
346                a.swap((col, c), (pivot_row, c));
347                inverse.swap((col, c), (pivot_row, c));
348            }
349        }
350
351        // Scale the pivot row so the pivot becomes 1.
352        let pivot = a[(col, col)];
353        for c in 0..n {
354            a[(col, c)] /= pivot;
355            inverse[(col, c)] /= pivot;
356        }
357
358        // Eliminate the pivot column from every other row.
359        for r in 0..n {
360            if r == col {
361                continue;
362            }
363            let factor = a[(r, col)];
364            if factor == 0.0 {
365                continue;
366            }
367            for c in 0..n {
368                let a_val = a[(col, c)];
369                let inv_val = inverse[(col, c)];
370                a[(r, c)] -= factor * a_val;
371                inverse[(r, c)] -= factor * inv_val;
372            }
373        }
374    }
375
376    Ok(inverse)
377}
378
379// Helper function for normal CDF approximation
380fn normal_cdf(x: f64) -> f64 {
381    // Simplified normal CDF approximation
382    0.5 * (1.0 + erf(x / 2.0_f64.sqrt()))
383}
384
385// Simplified error function approximation
386fn erf(x: f64) -> f64 {
387    // Abramowitz and Stegun approximation
388    let a1 = 0.254_829_592;
389    let a2 = -0.284_496_736;
390    let a3 = 1.421_413_741;
391    let a4 = -1.453_152_027;
392    let a5 = 1.061_405_429;
393    let p = 0.327_591_1;
394
395    let sign = if x < 0.0 { -1.0 } else { 1.0 };
396    let x = x.abs();
397
398    let t = 1.0 / (1.0 + p * x);
399    let y = ((a5 * t + a4).mul_add(t, a3).mul_add(t, a2).mul_add(t, a1) * t)
400        .mul_add(-(-x * x).exp(), 1.0);
401
402    sign * y
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408    use scirs2_core::ndarray::Array1;
409
410    #[test]
411    fn test_mean() {
412        let data = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
413        let result = mean(&data.view()).expect("Mean calculation should succeed");
414        assert!((result - 3.0).abs() < 1e-10);
415    }
416
417    #[test]
418    fn test_std() {
419        let data = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
420        let result = std(&data.view(), 1, None).expect("Std calculation should succeed");
421        // Standard deviation of [1,2,3,4,5] with ddof=1 is sqrt(2.5) ≈ 1.58
422        assert!((result - 1.5811388300841898).abs() < 1e-10);
423    }
424
425    #[test]
426    fn test_pearsonr() {
427        let x = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
428        let y = Array1::from_vec(vec![2.0, 4.0, 6.0, 8.0, 10.0]);
429        let result = pearsonr(&x.view(), &y.view()).expect("Pearson correlation should succeed");
430        // Perfect correlation should be 1.0
431        assert!((result - 1.0).abs() < 1e-10);
432    }
433
434    #[test]
435    fn test_minimize() {
436        let initial = Array1::from_vec(vec![0.0]);
437        let objective = |x: &Array1<f64>| (x[0] - 2.0).powi(2);
438
439        let result =
440            minimize(objective, &initial, "nelder-mead").expect("Minimization should succeed");
441
442        // Should find minimum near x = 2.0
443        assert!((result.x[0] - 2.0).abs() < 0.5);
444        assert!(result.success);
445    }
446
447    #[test]
448    fn test_inv_identity_property() {
449        use scirs2_core::ndarray::array;
450        let a = array![[4.0, 7.0], [2.0, 6.0]];
451        let a_inv = inv(&a.view()).expect("inverse should succeed");
452        for r in 0..2 {
453            for c in 0..2 {
454                let mut acc = 0.0;
455                for k in 0..2 {
456                    acc += a[(r, k)] * a_inv[(k, c)];
457                }
458                let expected = if r == c { 1.0 } else { 0.0 };
459                assert!((acc - expected).abs() < 1e-6, "[{r},{c}]={acc}");
460            }
461        }
462    }
463
464    #[test]
465    fn test_inv_3x3_identity_property() {
466        use scirs2_core::ndarray::array;
467        let a = array![[2.0, 1.0, 1.0], [1.0, 3.0, 2.0], [1.0, 0.0, 0.0]];
468        let a_inv = inv(&a.view()).expect("inverse should succeed");
469        for r in 0..3 {
470            for c in 0..3 {
471                let mut acc = 0.0;
472                for k in 0..3 {
473                    acc += a[(r, k)] * a_inv[(k, c)];
474                }
475                let expected = if r == c { 1.0 } else { 0.0 };
476                assert!((acc - expected).abs() < 1e-6, "[{r},{c}]={acc}");
477            }
478        }
479    }
480
481    #[test]
482    fn test_inv_singular_errors() {
483        use scirs2_core::ndarray::array;
484        let a = array![[1.0, 2.0], [2.0, 4.0]];
485        assert!(inv(&a.view()).is_err());
486    }
487
488    #[test]
489    fn test_inv_non_square_errors() {
490        use scirs2_core::ndarray::array;
491        let a = array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
492        assert!(inv(&a.view()).is_err());
493    }
494}