quantrs2_device/mid_circuit_measurements/
fallback.rs1use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
4
5pub fn mean(data: &ArrayView1<f64>) -> Result<f64, String> {
7 Ok(data.mean().unwrap_or(0.0))
8}
9
10pub fn std(data: &ArrayView1<f64>, _ddof: i32) -> Result<f64, String> {
12 Ok(data.std(1.0))
13}
14
15pub 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)) }
45
46pub 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
106pub 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
115fn 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
178pub 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 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 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 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]); 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 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}