Skip to main content

quantrs2_device/mid_circuit_measurements/
fallback.rs

1//! Fallback implementations when SciRS2 is not available
2
3use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
4
5/// Fallback mean calculation
6pub fn mean(data: &ArrayView1<f64>) -> Result<f64, String> {
7    Ok(data.mean().unwrap_or(0.0))
8}
9
10/// Fallback standard deviation calculation
11pub fn std(data: &ArrayView1<f64>, _ddof: i32) -> Result<f64, String> {
12    Ok(data.std(1.0))
13}
14
15/// Fallback Pearson correlation calculation
16pub fn pearsonr(
17    x: &ArrayView1<f64>,
18    y: &ArrayView1<f64>,
19    _alternative: &str,
20) -> Result<(f64, f64), String> {
21    if x.len() != y.len() || x.len() < 2 {
22        return Ok((0.0, 0.5));
23    }
24
25    let x_mean = x.mean().unwrap_or(0.0);
26    let y_mean = y.mean().unwrap_or(0.0);
27
28    let mut num = 0.0;
29    let mut x_sum_sq = 0.0;
30    let mut y_sum_sq = 0.0;
31
32    for i in 0..x.len() {
33        let x_diff = x[i] - x_mean;
34        let y_diff = y[i] - y_mean;
35        num += x_diff * y_diff;
36        x_sum_sq += x_diff * x_diff;
37        y_sum_sq += y_diff * y_diff;
38    }
39
40    let denom = (x_sum_sq * y_sum_sq).sqrt();
41    let corr = if denom > 1e-10 { num / denom } else { 0.0 };
42
43    Ok((corr, 0.05)) // p-value placeholder
44}
45
46/// Fallback optimization function: a real (if simple) derivative-free
47/// coordinate/pattern search ("compass search"). Repeatedly tries stepping
48/// each coordinate up or down by the current step size, accepting any move
49/// that reduces the objective, and halves the step size once a full sweep
50/// finds no improvement; stops once the step size is negligible or the
51/// iteration budget is exhausted. `objective` is genuinely evaluated (many
52/// times), so `fun`/`x` reflect real optimization rather than a fixed
53/// constant.
54pub fn minimize(
55    objective: fn(&[f64]) -> f64,
56    x0: &[f64],
57    bounds: Option<&[(f64, f64)]>,
58) -> Result<OptimizeResult, String> {
59    if x0.is_empty() {
60        return Err("minimize: x0 must not be empty".to_string());
61    }
62
63    let n = x0.len();
64    let mut x = x0.to_vec();
65    let mut best = objective(&x);
66    let mut step = 1.0_f64;
67    const MAX_ITERATIONS: usize = 100;
68    const MIN_STEP: f64 = 1e-8;
69    let mut iterations_used = 0;
70
71    let clamp_dim = |i: usize, val: f64| -> f64 {
72        bounds
73            .and_then(|b| b.get(i))
74            .map_or(val, |&(lo, hi)| val.clamp(lo, hi))
75    };
76
77    while step > MIN_STEP && iterations_used < MAX_ITERATIONS {
78        let mut improved = false;
79        for i in 0..n {
80            for delta in [step, -step] {
81                let mut candidate = x.clone();
82                candidate[i] = clamp_dim(i, candidate[i] + delta);
83                let value = objective(&candidate);
84                if value < best {
85                    best = value;
86                    x = candidate;
87                    improved = true;
88                }
89            }
90        }
91        iterations_used += 1;
92        if !improved {
93            step *= 0.5;
94        }
95    }
96
97    Ok(OptimizeResult {
98        x,
99        fun: best,
100        success: true,
101        nit: iterations_used,
102        message: "Fallback coordinate/pattern search (no SciRS2 optimizer available)".to_string(),
103    })
104}
105
106/// Fallback optimization result
107pub struct OptimizeResult {
108    pub x: Vec<f64>,
109    pub fun: f64,
110    pub success: bool,
111    pub nit: usize,
112    pub message: String,
113}
114
115/// Solve the square linear system `a * x = b` via Gaussian elimination with
116/// partial pivoting. Used by [`LinearRegression::fit`] to solve the normal
117/// equations; returns an honest error for a singular/near-singular system
118/// rather than a fabricated solution.
119fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> Result<Array1<f64>, String> {
120    let n = a.nrows();
121    if a.ncols() != n || b.len() != n {
122        return Err(format!(
123            "solve_linear_system: dimension mismatch (A is {}x{}, b has length {})",
124            a.nrows(),
125            a.ncols(),
126            b.len()
127        ));
128    }
129
130    let mut aug = a.clone();
131    let mut rhs = b.clone();
132
133    for col in 0..n {
134        let mut pivot_row = col;
135        let mut max_val = aug[[col, col]].abs();
136        for row in (col + 1)..n {
137            if aug[[row, col]].abs() > max_val {
138                max_val = aug[[row, col]].abs();
139                pivot_row = row;
140            }
141        }
142        if max_val < 1e-12 {
143            return Err(format!(
144                "solve_linear_system: matrix is singular or nearly singular (pivot magnitude {max_val:.3e} at column {col})"
145            ));
146        }
147        if pivot_row != col {
148            for k in 0..n {
149                aug.swap((col, k), (pivot_row, k));
150            }
151            rhs.swap(col, pivot_row);
152        }
153
154        let pivot_val = aug[[col, col]];
155        for k in 0..n {
156            aug[[col, k]] /= pivot_val;
157        }
158        rhs[col] /= pivot_val;
159
160        for row in 0..n {
161            if row != col {
162                let factor = aug[[row, col]];
163                if factor != 0.0 {
164                    for k in 0..n {
165                        let aug_col_k = aug[[col, k]];
166                        aug[[row, k]] -= factor * aug_col_k;
167                    }
168                    let rhs_col = rhs[col];
169                    rhs[row] -= factor * rhs_col;
170                }
171            }
172        }
173    }
174
175    Ok(rhs)
176}
177
178/// Fallback linear regression implementation: real ordinary-least-squares
179/// fitting via the normal equations (`(X^T X) beta = X^T y`, solved with
180/// [`solve_linear_system`]), not a no-op. The design matrix is augmented
181/// with a column of ones so the intercept is fitted along with the feature
182/// coefficients.
183pub struct LinearRegression {
184    coefficients: Vec<f64>,
185    intercept: f64,
186}
187
188impl LinearRegression {
189    pub const fn new() -> Self {
190        Self {
191            coefficients: Vec::new(),
192            intercept: 0.0,
193        }
194    }
195
196    pub fn fit(&mut self, x: &Array2<f64>, y: &Array1<f64>) -> Result<(), String> {
197        let n_samples = x.nrows();
198        let n_features = x.ncols();
199        if n_samples != y.len() {
200            return Err(format!(
201                "LinearRegression::fit: x has {n_samples} rows but y has {} elements",
202                y.len()
203            ));
204        }
205        if n_samples == 0 || n_features == 0 {
206            return Err("LinearRegression::fit: x must be non-empty".to_string());
207        }
208
209        // Augment X with a column of ones (last column) for the intercept.
210        let mut x_aug = Array2::<f64>::ones((n_samples, n_features + 1));
211        for i in 0..n_samples {
212            for j in 0..n_features {
213                x_aug[[i, j]] = x[[i, j]];
214            }
215        }
216
217        let xt = x_aug.t();
218        let xtx = xt.dot(&x_aug);
219        let xty = xt.dot(y);
220
221        let beta = solve_linear_system(&xtx, &xty)?;
222
223        self.coefficients = beta.iter().take(n_features).copied().collect();
224        self.intercept = beta[n_features];
225        Ok(())
226    }
227
228    pub fn predict(&self, x: &Array2<f64>) -> Array1<f64> {
229        let n_samples = x.nrows();
230        let mut predictions = Array1::<f64>::zeros(n_samples);
231        for i in 0..n_samples {
232            let mut value = self.intercept;
233            for (j, &coefficient) in self.coefficients.iter().enumerate() {
234                if j < x.ncols() {
235                    value += coefficient * x[[i, j]];
236                }
237            }
238            predictions[i] = value;
239        }
240        predictions
241    }
242}
243
244impl Default for LinearRegression {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_minimize_actually_evaluates_objective_and_reduces_it() {
256        // f(x) = (x0 - 3)^2 + (x1 + 2)^2, minimized at (3, -2) with f = 0.
257        fn objective(x: &[f64]) -> f64 {
258            (x[0] - 3.0).powi(2) + (x[1] + 2.0).powi(2)
259        }
260
261        let x0 = [0.0, 0.0];
262        let initial_value = objective(&x0);
263
264        let result = minimize(objective, &x0, None).expect("minimize should succeed");
265
266        assert!(
267            result.fun < initial_value,
268            "fallback optimizer must actually reduce the objective (got {} vs initial {})",
269            result.fun,
270            initial_value
271        );
272        assert!(
273            (result.x[0] - 3.0).abs() < 0.1,
274            "expected x0 near 3.0, got {}",
275            result.x[0]
276        );
277        assert!(
278            (result.x[1] + 2.0).abs() < 0.1,
279            "expected x1 near -2.0, got {}",
280            result.x[1]
281        );
282    }
283
284    #[test]
285    fn test_minimize_respects_bounds() {
286        fn objective(x: &[f64]) -> f64 {
287            (x[0] - 100.0).powi(2)
288        }
289
290        let x0 = [0.0];
291        let bounds = [(-1.0, 1.0)];
292        let result = minimize(objective, &x0, Some(&bounds)).expect("minimize should succeed");
293
294        assert!(
295            (-1.0..=1.0).contains(&result.x[0]),
296            "optimizer must respect bounds, got x={}",
297            result.x[0]
298        );
299    }
300
301    #[test]
302    fn test_linear_regression_recovers_known_line() {
303        // y = 2*x + 1
304        let x = Array2::from_shape_vec((4, 1), vec![0.0, 1.0, 2.0, 3.0]).unwrap();
305        let y = Array1::from_vec(vec![1.0, 3.0, 5.0, 7.0]);
306
307        let mut model = LinearRegression::new();
308        model
309            .fit(&x, &y)
310            .expect("fit should succeed for a well-posed system");
311
312        let predictions = model.predict(&x);
313        for i in 0..4 {
314            assert!(
315                (predictions[i] - y[i]).abs() < 1e-6,
316                "prediction {} should match training target {} at index {i}",
317                predictions[i],
318                y[i]
319            );
320        }
321    }
322
323    #[test]
324    fn test_linear_regression_rejects_mismatched_shapes() {
325        let x = Array2::from_shape_vec((3, 1), vec![0.0, 1.0, 2.0]).unwrap();
326        let y = Array1::from_vec(vec![1.0, 2.0]); // wrong length
327
328        let mut model = LinearRegression::new();
329        assert!(
330            model.fit(&x, &y).is_err(),
331            "fit must honestly error on mismatched shapes rather than silently succeeding"
332        );
333    }
334
335    #[test]
336    fn test_solve_linear_system_basic() {
337        // [[2, 0], [0, 3]] * x = [4, 9] => x = [2, 3]
338        let a = Array2::from_shape_vec((2, 2), vec![2.0, 0.0, 0.0, 3.0]).unwrap();
339        let b = Array1::from_vec(vec![4.0, 9.0]);
340        let x = solve_linear_system(&a, &b).expect("solve should succeed");
341        assert!((x[0] - 2.0).abs() < 1e-9);
342        assert!((x[1] - 3.0).abs() < 1e-9);
343    }
344}