Skip to main content

quantrs2_ml/
scirs2_integration.rs

1//! SciRS2 integration layer for quantum machine learning
2//!
3//! This module provides integration with the SciRS2 scientific computing framework,
4//! enabling quantum ML models to leverage SciRS2's optimized tensor operations,
5//! distributed training capabilities, and serialization formats.
6
7use crate::error::{MLError, Result};
8use scirs2_core::ndarray::{
9    Array, Array1, Array2, Array3, ArrayD, ArrayViewD, Axis, Dimension, Ix2, IxDyn,
10};
11use std::collections::HashMap;
12use std::sync::{Arc, Mutex};
13
14/// Trait for tensor operations compatible with SciRS2
15pub trait SciRS2Tensor {
16    /// Get tensor shape
17    fn shape(&self) -> &[usize];
18
19    /// Get tensor data as ArrayViewD
20    fn view(&self) -> ArrayViewD<f64>;
21
22    /// Convert to SciRS2 format (placeholder)
23    fn to_scirs2(&self) -> Result<SciRS2Array>;
24
25    /// Perform tensor operations using SciRS2 backend
26    fn matmul(&self, other: &dyn SciRS2Tensor) -> Result<SciRS2Array>;
27
28    /// Element-wise operations
29    fn add(&self, other: &dyn SciRS2Tensor) -> Result<SciRS2Array>;
30    fn mul(&self, other: &dyn SciRS2Tensor) -> Result<SciRS2Array>;
31    fn sub(&self, other: &dyn SciRS2Tensor) -> Result<SciRS2Array>;
32
33    /// Reduction operations
34    fn sum(&self, axis: Option<usize>) -> Result<SciRS2Array>;
35    fn mean(&self, axis: Option<usize>) -> Result<SciRS2Array>;
36    fn max(&self, axis: Option<usize>) -> Result<SciRS2Array>;
37    fn min(&self, axis: Option<usize>) -> Result<SciRS2Array>;
38}
39
40/// SciRS2 array wrapper for quantum ML operations
41pub struct SciRS2Array {
42    /// Array data
43    pub data: ArrayD<f64>,
44    /// Whether gradients are required
45    pub requires_grad: bool,
46    /// Gradient accumulator. Wrapped in `Arc<Mutex<_>>` (rather than a bare
47    /// `ArrayD<f64>`) so that a `GradFunction` created for a downstream op
48    /// can hold a *shared handle* to this same accumulator and actually
49    /// write the real backpropagated gradient into it from `backward()`,
50    /// instead of only ever seeing a disconnected clone of the data with no
51    /// way back to the original leaf tensor. `Arc<Mutex<_>>` (rather than
52    /// `Rc<RefCell<_>>`) is used so that `GradFunction: Send + Sync`
53    /// remains satisfiable.
54    pub grad: Option<Arc<Mutex<ArrayD<f64>>>>,
55    /// Operation history for backpropagation
56    pub grad_fn: Option<Box<dyn GradFunction>>,
57}
58
59impl std::fmt::Debug for SciRS2Array {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("SciRS2Array")
62            .field("data", &self.data)
63            .field("requires_grad", &self.requires_grad)
64            .field("grad", &self.grad)
65            .field("grad_fn", &"<gradient_function>")
66            .finish()
67    }
68}
69
70impl Clone for SciRS2Array {
71    fn clone(&self) -> Self {
72        Self {
73            data: self.data.clone(),
74            requires_grad: self.requires_grad,
75            grad: self.grad.clone(),
76            grad_fn: None, // Cannot clone trait objects
77        }
78    }
79}
80
81impl SciRS2Array {
82    /// Create a new SciRS2Array
83    pub fn new(data: ArrayD<f64>, requires_grad: bool) -> Self {
84        let grad = if requires_grad {
85            Some(Arc::new(Mutex::new(ArrayD::zeros(data.raw_dim()))))
86        } else {
87            None
88        };
89        Self {
90            data,
91            requires_grad,
92            grad,
93            grad_fn: None,
94        }
95    }
96
97    /// Create from ndarray
98    pub fn from_array<D: Dimension>(arr: Array<f64, D>) -> Self {
99        let data = arr.into_dyn();
100        Self::new(data, false)
101    }
102
103    /// Create with gradient tracking
104    pub fn with_grad<D: Dimension>(arr: Array<f64, D>) -> Self {
105        let data = arr.into_dyn();
106        Self::new(data, true)
107    }
108
109    /// Zero gradients
110    pub fn zero_grad(&mut self) {
111        if let Some(ref grad) = self.grad {
112            lock_grad(grad).fill(0.0);
113        }
114    }
115
116    /// Set the seed gradient w.r.t. this array (typically all-ones for a
117    /// scalar loss) before calling [`Self::backward`].
118    pub fn set_grad(&mut self, grad: ArrayD<f64>) {
119        match &self.grad {
120            Some(cell) => *lock_grad(cell) = grad,
121            None => self.grad = Some(Arc::new(Mutex::new(grad))),
122        }
123    }
124
125    /// Backward pass: propagates `self.grad` one step upstream through
126    /// `self.grad_fn`, accumulating real gradients into the operands' own
127    /// (shared) gradient cells.
128    pub fn backward(&mut self) -> Result<()> {
129        // Extract grad_fn to avoid borrow conflicts
130        if let Some(grad_fn) = self.grad_fn.take() {
131            grad_fn.backward(self)?;
132            self.grad_fn = Some(grad_fn);
133        }
134        Ok(())
135    }
136
137    /// Matrix multiplication using SciRS2 backend
138    pub fn matmul(&self, other: &SciRS2Array) -> Result<SciRS2Array> {
139        let result_data = if self.data.ndim() == 2 && other.data.ndim() == 2 {
140            let self_2d = self
141                .data
142                .view()
143                .into_dimensionality::<Ix2>()
144                .map_err(|e| MLError::ComputationError(format!("Shape error: {}", e)))?;
145            let other_2d = other
146                .data
147                .view()
148                .into_dimensionality::<Ix2>()
149                .map_err(|e| MLError::ComputationError(format!("Shape error: {}", e)))?;
150            self_2d.dot(&other_2d).into_dyn()
151        } else {
152            return Err(MLError::InvalidConfiguration(
153                "Matrix multiplication requires 2D arrays".to_string(),
154            ));
155        };
156
157        let requires_grad = self.requires_grad || other.requires_grad;
158        let mut result = SciRS2Array::new(result_data, requires_grad);
159
160        if requires_grad {
161            result.grad_fn = Some(Box::new(MatmulGradFn {
162                left_grad: self.grad.clone(),
163                right_grad: other.grad.clone(),
164                left_data: self.data.clone(),
165                right_data: other.data.clone(),
166            }));
167        }
168
169        Ok(result)
170    }
171
172    /// Element-wise addition
173    pub fn add(&self, other: &SciRS2Array) -> Result<SciRS2Array> {
174        let result_data = &self.data + &other.data;
175        let requires_grad = self.requires_grad || other.requires_grad;
176        let mut result = SciRS2Array::new(result_data, requires_grad);
177
178        if requires_grad {
179            result.grad_fn = Some(Box::new(AddGradFn {
180                left_grad: self.grad.clone(),
181                right_grad: other.grad.clone(),
182            }));
183        }
184
185        Ok(result)
186    }
187
188    /// Element-wise multiplication
189    pub fn mul(&self, other: &SciRS2Array) -> Result<SciRS2Array> {
190        let result_data = &self.data * &other.data;
191        let requires_grad = self.requires_grad || other.requires_grad;
192        let mut result = SciRS2Array::new(result_data, requires_grad);
193
194        if requires_grad {
195            result.grad_fn = Some(Box::new(MulGradFn {
196                left_grad: self.grad.clone(),
197                right_grad: other.grad.clone(),
198                left_data: self.data.clone(),
199                right_data: other.data.clone(),
200            }));
201        }
202
203        Ok(result)
204    }
205
206    /// Reduction sum
207    pub fn sum(&self, axis: Option<usize>) -> Result<SciRS2Array> {
208        let result_data = match axis {
209            Some(ax) => self
210                .data
211                .sum_axis(scirs2_core::ndarray::Axis(ax))
212                .into_dyn(),
213            None => {
214                let sum_val = self.data.sum();
215                ArrayD::from_elem(IxDyn(&[]), sum_val)
216            }
217        };
218
219        let mut result = SciRS2Array::new(result_data, self.requires_grad);
220
221        if self.requires_grad {
222            result.grad_fn = Some(Box::new(SumGradFn {
223                axis,
224                input_shape: self.data.raw_dim(),
225                input_grad: self.grad.clone(),
226            }));
227        }
228
229        Ok(result)
230    }
231}
232
233impl SciRS2Tensor for SciRS2Array {
234    fn shape(&self) -> &[usize] {
235        self.data.shape()
236    }
237
238    fn view(&self) -> ArrayViewD<f64> {
239        self.data.view()
240    }
241
242    fn to_scirs2(&self) -> Result<SciRS2Array> {
243        Ok(self.clone())
244    }
245
246    fn matmul(&self, other: &dyn SciRS2Tensor) -> Result<SciRS2Array> {
247        // Convert other to SciRS2Array for computation
248        let other_array = other.to_scirs2()?;
249        self.matmul(&other_array)
250    }
251
252    fn add(&self, other: &dyn SciRS2Tensor) -> Result<SciRS2Array> {
253        let other_array = other.to_scirs2()?;
254        self.add(&other_array)
255    }
256
257    fn mul(&self, other: &dyn SciRS2Tensor) -> Result<SciRS2Array> {
258        let other_array = other.to_scirs2()?;
259        self.mul(&other_array)
260    }
261
262    fn sub(&self, other: &dyn SciRS2Tensor) -> Result<SciRS2Array> {
263        let other_array = other.to_scirs2()?;
264        let result_data = &self.data - &other_array.data;
265        let requires_grad = self.requires_grad || other_array.requires_grad;
266        let mut result = SciRS2Array::new(result_data, requires_grad);
267
268        if requires_grad {
269            result.grad_fn = Some(Box::new(SubGradFn {
270                left_grad: self.grad.clone(),
271                right_grad: other_array.grad.clone(),
272            }));
273        }
274
275        Ok(result)
276    }
277
278    fn sum(&self, axis: Option<usize>) -> Result<SciRS2Array> {
279        self.sum(axis)
280    }
281
282    fn mean(&self, axis: Option<usize>) -> Result<SciRS2Array> {
283        let result_data = match axis {
284            Some(ax) => self
285                .data
286                .mean_axis(scirs2_core::ndarray::Axis(ax))
287                .ok_or_else(|| {
288                    MLError::ComputationError("Empty axis for mean computation".to_string())
289                })?
290                .into_dyn(),
291            None => {
292                let mean_val = self.data.mean().ok_or_else(|| {
293                    MLError::ComputationError("Empty array for mean computation".to_string())
294                })?;
295                ArrayD::from_elem(IxDyn(&[]), mean_val)
296            }
297        };
298        Ok(SciRS2Array::new(result_data, self.requires_grad))
299    }
300
301    fn max(&self, axis: Option<usize>) -> Result<SciRS2Array> {
302        let result_data = match axis {
303            Some(ax) => self
304                .data
305                .map_axis(scirs2_core::ndarray::Axis(ax), |view| {
306                    *view
307                        .iter()
308                        .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
309                        .expect("map_axis guarantees non-empty view for valid axis")
310                })
311                .into_dyn(),
312            None => {
313                let max_val = *self
314                    .data
315                    .iter()
316                    .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
317                    .ok_or_else(|| {
318                        MLError::ComputationError("Empty array for max computation".to_string())
319                    })?;
320                ArrayD::from_elem(IxDyn(&[]), max_val)
321            }
322        };
323        Ok(SciRS2Array::new(result_data, self.requires_grad))
324    }
325
326    fn min(&self, axis: Option<usize>) -> Result<SciRS2Array> {
327        let result_data = match axis {
328            Some(ax) => self
329                .data
330                .map_axis(scirs2_core::ndarray::Axis(ax), |view| {
331                    *view
332                        .iter()
333                        .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
334                        .expect("map_axis guarantees non-empty view for valid axis")
335                })
336                .into_dyn(),
337            None => {
338                let min_val = *self
339                    .data
340                    .iter()
341                    .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
342                    .ok_or_else(|| {
343                        MLError::ComputationError("Empty array for min computation".to_string())
344                    })?;
345                ArrayD::from_elem(IxDyn(&[]), min_val)
346            }
347        };
348        Ok(SciRS2Array::new(result_data, self.requires_grad))
349    }
350}
351
352/// Lock a shared gradient cell, recovering the inner data even if a prior
353/// panic poisoned the mutex (a plain `.lock().unwrap()` would instead panic
354/// again here, which production code must avoid).
355fn lock_grad(cell: &Arc<Mutex<ArrayD<f64>>>) -> std::sync::MutexGuard<'_, ArrayD<f64>> {
356    cell.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
357}
358
359/// Reads the seed gradient stored in `output.grad`, defaulting to an
360/// all-ones array (the standard implicit seed for a scalar loss) the first
361/// time `backward()` is called on a node whose gradient was never
362/// explicitly set via [`SciRS2Array::set_grad`].
363fn output_grad_or_ones(output: &SciRS2Array) -> ArrayD<f64> {
364    match &output.grad {
365        Some(cell) => lock_grad(cell).clone(),
366        None => ArrayD::ones(output.data.raw_dim()),
367    }
368}
369
370/// Accumulate `contribution` into a (possibly absent) shared gradient cell.
371fn accumulate_grad(cell: &Option<Arc<Mutex<ArrayD<f64>>>>, contribution: &ArrayD<f64>) {
372    if let Some(cell) = cell {
373        let mut guard = lock_grad(cell);
374        *guard = &*guard + contribution;
375    }
376}
377
378/// Trait for gradient functions
379pub trait GradFunction: Send + Sync {
380    fn backward(&self, output: &mut SciRS2Array) -> Result<()>;
381}
382
383/// Gradient function for matrix multiplication: for `C = A @ B`,
384/// `dL/dA = dL/dC @ B^T` and `dL/dB = A^T @ dL/dC`.
385#[derive(Debug)]
386struct MatmulGradFn {
387    left_grad: Option<Arc<Mutex<ArrayD<f64>>>>,
388    right_grad: Option<Arc<Mutex<ArrayD<f64>>>>,
389    left_data: ArrayD<f64>,
390    right_data: ArrayD<f64>,
391}
392
393impl GradFunction for MatmulGradFn {
394    fn backward(&self, output: &mut SciRS2Array) -> Result<()> {
395        let output_grad = output_grad_or_ones(output);
396        let grad_2d = output_grad
397            .view()
398            .into_dimensionality::<Ix2>()
399            .map_err(|e| MLError::ComputationError(format!("Shape error: {}", e)))?;
400
401        if self.left_grad.is_some() {
402            let right_2d = self
403                .right_data
404                .view()
405                .into_dimensionality::<Ix2>()
406                .map_err(|e| MLError::ComputationError(format!("Shape error: {}", e)))?;
407            let grad_left = grad_2d.dot(&right_2d.t()).into_dyn();
408            accumulate_grad(&self.left_grad, &grad_left);
409        }
410
411        if self.right_grad.is_some() {
412            let left_2d = self
413                .left_data
414                .view()
415                .into_dimensionality::<Ix2>()
416                .map_err(|e| MLError::ComputationError(format!("Shape error: {}", e)))?;
417            let grad_right = left_2d.t().dot(&grad_2d).into_dyn();
418            accumulate_grad(&self.right_grad, &grad_right);
419        }
420
421        Ok(())
422    }
423}
424
425/// Gradient function for addition: `d(A+B)/dA = d(A+B)/dB = 1`, so the
426/// output gradient flows through unchanged (but must still be *copied* into
427/// both operands' accumulators, unlike the previous no-op).
428#[derive(Debug)]
429struct AddGradFn {
430    left_grad: Option<Arc<Mutex<ArrayD<f64>>>>,
431    right_grad: Option<Arc<Mutex<ArrayD<f64>>>>,
432}
433
434impl GradFunction for AddGradFn {
435    fn backward(&self, output: &mut SciRS2Array) -> Result<()> {
436        let output_grad = output_grad_or_ones(output);
437        accumulate_grad(&self.left_grad, &output_grad);
438        accumulate_grad(&self.right_grad, &output_grad);
439        Ok(())
440    }
441}
442
443/// Gradient function for element-wise subtraction: `d(A-B)/dA = 1`,
444/// `d(A-B)/dB = -1`.
445#[derive(Debug)]
446struct SubGradFn {
447    left_grad: Option<Arc<Mutex<ArrayD<f64>>>>,
448    right_grad: Option<Arc<Mutex<ArrayD<f64>>>>,
449}
450
451impl GradFunction for SubGradFn {
452    fn backward(&self, output: &mut SciRS2Array) -> Result<()> {
453        let output_grad = output_grad_or_ones(output);
454        accumulate_grad(&self.left_grad, &output_grad);
455        let negated = output_grad.mapv(|x| -x);
456        accumulate_grad(&self.right_grad, &negated);
457        Ok(())
458    }
459}
460
461/// Gradient function for element-wise multiplication:
462/// `d(A*B)/dA = B`, `d(A*B)/dB = A` (Hadamard product with the output
463/// gradient).
464#[derive(Debug)]
465struct MulGradFn {
466    left_grad: Option<Arc<Mutex<ArrayD<f64>>>>,
467    right_grad: Option<Arc<Mutex<ArrayD<f64>>>>,
468    left_data: ArrayD<f64>,
469    right_data: ArrayD<f64>,
470}
471
472impl GradFunction for MulGradFn {
473    fn backward(&self, output: &mut SciRS2Array) -> Result<()> {
474        let output_grad = output_grad_or_ones(output);
475        let grad_left = &output_grad * &self.right_data;
476        let grad_right = &output_grad * &self.left_data;
477        accumulate_grad(&self.left_grad, &grad_left);
478        accumulate_grad(&self.right_grad, &grad_right);
479        Ok(())
480    }
481}
482
483/// Gradient function for sum reduction: broadcasts the (scalar or
484/// reduced-axis) output gradient back across the reduced axis/axes to the
485/// original input shape.
486#[derive(Debug)]
487struct SumGradFn {
488    axis: Option<usize>,
489    input_shape: IxDyn,
490    input_grad: Option<Arc<Mutex<ArrayD<f64>>>>,
491}
492
493impl GradFunction for SumGradFn {
494    fn backward(&self, output: &mut SciRS2Array) -> Result<()> {
495        let output_grad = output_grad_or_ones(output);
496
497        let broadcasted = match self.axis {
498            None => {
499                let scalar = output_grad.iter().next().copied().unwrap_or(0.0);
500                ArrayD::from_elem(self.input_shape.clone(), scalar)
501            }
502            Some(ax) => {
503                let expanded = output_grad.insert_axis(Axis(ax));
504                expanded
505                    .broadcast(self.input_shape.clone())
506                    .ok_or_else(|| {
507                        MLError::ComputationError(
508                            "Failed to broadcast sum gradient back to input shape".to_string(),
509                        )
510                    })?
511                    .to_owned()
512            }
513        };
514
515        accumulate_grad(&self.input_grad, &broadcasted);
516        Ok(())
517    }
518}
519
520/// SciRS2 optimization interface
521pub struct SciRS2Optimizer {
522    /// Optimizer type
523    pub optimizer_type: String,
524    /// Configuration parameters
525    pub config: HashMap<String, f64>,
526    /// Parameter state (for stateful optimizers like Adam)
527    pub state: HashMap<String, ArrayD<f64>>,
528}
529
530impl SciRS2Optimizer {
531    /// Create a new SciRS2 optimizer
532    pub fn new(optimizer_type: impl Into<String>) -> Self {
533        Self {
534            optimizer_type: optimizer_type.into(),
535            config: HashMap::new(),
536            state: HashMap::new(),
537        }
538    }
539
540    /// Set optimizer configuration
541    pub fn with_config(mut self, key: impl Into<String>, value: f64) -> Self {
542        self.config.insert(key.into(), value);
543        self
544    }
545
546    /// Update parameters using computed gradients
547    pub fn step(&mut self, params: &mut HashMap<String, SciRS2Array>) -> Result<()> {
548        match self.optimizer_type.as_str() {
549            "adam" => self.adam_step(params),
550            "sgd" => self.sgd_step(params),
551            "lbfgs" => self.lbfgs_step(params),
552            _ => Err(MLError::InvalidConfiguration(format!(
553                "Unknown optimizer type: {}",
554                self.optimizer_type
555            ))),
556        }
557    }
558
559    /// Adam optimizer step
560    fn adam_step(&mut self, params: &mut HashMap<String, SciRS2Array>) -> Result<()> {
561        let learning_rate = self.config.get("learning_rate").unwrap_or(&0.001);
562        let beta1 = self.config.get("beta1").unwrap_or(&0.9);
563        let beta2 = self.config.get("beta2").unwrap_or(&0.999);
564        let epsilon = self.config.get("epsilon").unwrap_or(&1e-8);
565
566        for (name, param) in params.iter_mut() {
567            let grad_cell = match &param.grad {
568                Some(cell) => cell.clone(),
569                None => continue,
570            };
571            let grad = lock_grad(&grad_cell).clone();
572            {
573                // Initialize momentum and velocity if not present
574                let m_key = format!("{}_m", name);
575                let v_key = format!("{}_v", name);
576
577                if !self.state.contains_key(&m_key) {
578                    self.state
579                        .insert(m_key.clone(), ArrayD::zeros(grad.raw_dim()));
580                    self.state
581                        .insert(v_key.clone(), ArrayD::zeros(grad.raw_dim()));
582                }
583
584                // Update first moment estimate
585                {
586                    let m = self
587                        .state
588                        .get_mut(&m_key)
589                        .expect("m_key was just inserted if not present");
590                    *m = *beta1 * &*m + (1.0 - *beta1) * &grad;
591                }
592
593                // Update second moment estimate
594                {
595                    let v = self
596                        .state
597                        .get_mut(&v_key)
598                        .expect("v_key was just inserted if not present");
599                    *v = *beta2 * &*v + (1.0 - *beta2) * &grad * &grad;
600                }
601
602                // Get references for bias correction
603                let m_hat = self
604                    .state
605                    .get(&m_key)
606                    .expect("m_key exists after update")
607                    .clone();
608                let v_hat = self
609                    .state
610                    .get(&v_key)
611                    .expect("v_key exists after update")
612                    .clone();
613
614                // Update parameters
615                param.data =
616                    &param.data - *learning_rate * &m_hat / (v_hat.mapv(|x| x.sqrt()) + *epsilon);
617            }
618        }
619
620        Ok(())
621    }
622
623    /// SGD optimizer step
624    fn sgd_step(&mut self, params: &mut HashMap<String, SciRS2Array>) -> Result<()> {
625        let learning_rate = self.config.get("learning_rate").unwrap_or(&0.01);
626        let momentum = self.config.get("momentum").unwrap_or(&0.0);
627
628        for (name, param) in params.iter_mut() {
629            let grad_cell = match &param.grad {
630                Some(cell) => cell.clone(),
631                None => continue,
632            };
633            let grad = lock_grad(&grad_cell).clone();
634            {
635                if *momentum > 0.0 {
636                    let v_key = format!("{}_v", name);
637                    if !self.state.contains_key(&v_key) {
638                        self.state
639                            .insert(v_key.clone(), ArrayD::zeros(grad.raw_dim()));
640                    }
641
642                    let v = self
643                        .state
644                        .get_mut(&v_key)
645                        .expect("v_key was just inserted if not present");
646                    *v = *momentum * &*v + *learning_rate * &grad;
647                    param.data = &param.data - &*v;
648                } else {
649                    param.data = &param.data - *learning_rate * &grad;
650                }
651            }
652        }
653
654        Ok(())
655    }
656
657    /// L-BFGS optimizer step (placeholder)
658    fn lbfgs_step(&mut self, _params: &mut HashMap<String, SciRS2Array>) -> Result<()> {
659        // Placeholder - would implement L-BFGS using SciRS2
660        Ok(())
661    }
662}
663
664/// SciRS2 distributed training support
665pub struct SciRS2DistributedTrainer {
666    /// World size (number of processes)
667    pub world_size: usize,
668    /// Local rank
669    pub rank: usize,
670    /// Backend for communication
671    pub backend: String,
672}
673
674impl SciRS2DistributedTrainer {
675    /// Create a new distributed trainer
676    pub fn new(world_size: usize, rank: usize) -> Self {
677        Self {
678            world_size,
679            rank,
680            backend: "nccl".to_string(),
681        }
682    }
683
684    /// All-reduce operation for gradient synchronization
685    pub fn all_reduce(&self, tensor: &mut SciRS2Array) -> Result<()> {
686        // Placeholder - would use SciRS2 distributed operations
687        Ok(())
688    }
689
690    /// All-reduce scalar operation for metrics synchronization
691    pub fn all_reduce_scalar(&self, value: f64) -> Result<f64> {
692        // Placeholder - would use SciRS2 distributed operations
693        // For now, just return the value unchanged (single process behavior)
694        Ok(value)
695    }
696
697    /// Broadcast operation
698    pub fn broadcast(&self, tensor: &mut SciRS2Array, root: usize) -> Result<()> {
699        // Placeholder - would use SciRS2 distributed operations
700        Ok(())
701    }
702
703    /// All-gather operation
704    pub fn all_gather(&self, tensor: &SciRS2Array) -> Result<Vec<SciRS2Array>> {
705        // Placeholder - would use SciRS2 distributed operations
706        Ok(vec![tensor.clone(); self.world_size])
707    }
708
709    /// Wrap a model for distributed training
710    pub fn wrap_model<T>(&self, model: T) -> Result<T> {
711        // Placeholder - would wrap the model with distributed training capabilities
712        // For now, just return the model unchanged
713        Ok(model)
714    }
715}
716
717/// SciRS2 model serialization interface
718pub struct SciRS2Serializer;
719
720impl SciRS2Serializer {
721    /// Serialize model parameters to SciRS2 format
722    pub fn save_model(params: &HashMap<String, SciRS2Array>, path: &str) -> Result<()> {
723        // Placeholder - would use SciRS2 serialization
724        Ok(())
725    }
726
727    /// Load model parameters from SciRS2 format
728    pub fn load_model(path: &str) -> Result<HashMap<String, SciRS2Array>> {
729        // Placeholder - would use SciRS2 deserialization
730        Ok(HashMap::new())
731    }
732
733    /// Save checkpoint with optimizer state
734    pub fn save_checkpoint(
735        params: &HashMap<String, SciRS2Array>,
736        optimizer: &SciRS2Optimizer,
737        epoch: usize,
738        path: &str,
739    ) -> Result<()> {
740        // Placeholder - would use SciRS2 checkpoint format
741        Ok(())
742    }
743
744    /// Load checkpoint with optimizer state
745    pub fn load_checkpoint(
746        path: &str,
747    ) -> Result<(HashMap<String, SciRS2Array>, SciRS2Optimizer, usize)> {
748        // Placeholder - would use SciRS2 checkpoint format
749        Ok((HashMap::new(), SciRS2Optimizer::new("adam"), 0))
750    }
751}
752
753/// SciRS2 Dataset wrapper for quantum ML
754pub struct SciRS2Dataset {
755    /// Training data
756    pub data: ArrayD<f64>,
757    /// Labels
758    pub labels: ArrayD<f64>,
759    /// Dataset size
760    pub size: usize,
761}
762
763impl SciRS2Dataset {
764    /// Create a new dataset
765    pub fn new(data: ArrayD<f64>, labels: ArrayD<f64>) -> Result<Self> {
766        let size = data.shape()[0];
767        if labels.shape()[0] != size {
768            return Err(MLError::InvalidConfiguration(
769                "Data and labels must have same number of samples".to_string(),
770            ));
771        }
772
773        Ok(Self { data, labels, size })
774    }
775}
776
777/// SciRS2 DataLoader for batch processing
778pub struct SciRS2DataLoader {
779    /// Dataset reference
780    pub dataset: SciRS2Dataset,
781    /// Batch size
782    pub batch_size: usize,
783    /// Current index
784    pub current_index: usize,
785}
786
787impl SciRS2DataLoader {
788    /// Create a new data loader
789    pub fn new(dataset: SciRS2Dataset, batch_size: usize) -> Self {
790        Self {
791            dataset,
792            batch_size,
793            current_index: 0,
794        }
795    }
796
797    /// Iterator-like enumeration support
798    pub fn enumerate(&mut self) -> DataLoaderIterator {
799        DataLoaderIterator {
800            loader: self,
801            batch_idx: 0,
802        }
803    }
804}
805
806/// Iterator for DataLoader
807pub struct DataLoaderIterator<'a> {
808    loader: &'a mut SciRS2DataLoader,
809    batch_idx: usize,
810}
811
812impl<'a> Iterator for DataLoaderIterator<'a> {
813    type Item = (usize, (SciRS2Array, SciRS2Array));
814
815    fn next(&mut self) -> Option<Self::Item> {
816        if self.loader.current_index >= self.loader.dataset.size {
817            return None;
818        }
819
820        let start = self.loader.current_index;
821        let end = (start + self.loader.batch_size).min(self.loader.dataset.size);
822
823        // Extract batch data and labels
824        let batch_data = self
825            .loader
826            .dataset
827            .data
828            .slice(scirs2_core::ndarray::s![start..end, ..])
829            .to_owned();
830        let batch_labels = self
831            .loader
832            .dataset
833            .labels
834            .slice(scirs2_core::ndarray::s![start..end, ..])
835            .to_owned();
836
837        let data_array = SciRS2Array::from_array(batch_data);
838        let label_array = SciRS2Array::from_array(batch_labels);
839
840        self.loader.current_index = end;
841        let batch_idx = self.batch_idx;
842        self.batch_idx += 1;
843
844        Some((batch_idx, (data_array, label_array)))
845    }
846}
847
848/// SciRS2 Device enumeration
849#[derive(Debug, Clone, Copy)]
850pub enum SciRS2Device {
851    CPU,
852    GPU,
853    Quantum,
854}
855
856/// Additional SciRS2Array methods for compatibility
857impl SciRS2Array {
858    /// Create array with specified device
859    pub fn randn(shape: Vec<usize>, device: SciRS2Device) -> Result<Self> {
860        use scirs2_core::random::prelude::*;
861        let total_size = shape.iter().product();
862        let mut rng = thread_rng();
863        let data: Vec<f64> = (0..total_size)
864            .map(|_| rng.random_range(-1.0..1.0))
865            .collect();
866        let array = ArrayD::from_shape_vec(IxDyn(&shape), data)
867            .map_err(|e| MLError::ComputationError(format!("Shape error: {}", e)))?;
868        Ok(Self::new(array, false))
869    }
870
871    /// Create ones_like array
872    pub fn ones_like(&self) -> Result<Self> {
873        let ones = ArrayD::ones(self.data.raw_dim());
874        Ok(Self::new(ones, false))
875    }
876
877    /// Create random integers
878    pub fn randint(low: i32, high: i32, shape: Vec<usize>, device: SciRS2Device) -> Result<Self> {
879        use scirs2_core::random::prelude::*;
880        let total_size = shape.iter().product();
881        let mut rng = thread_rng();
882        let data: Vec<f64> = (0..total_size)
883            .map(|_| rng.random_range(low..high) as f64)
884            .collect();
885        let array = ArrayD::from_shape_vec(IxDyn(&shape), data)
886            .map_err(|e| MLError::ComputationError(format!("Shape error: {}", e)))?;
887        Ok(Self::new(array, false))
888    }
889
890    /// Create quantum observable
891    pub fn quantum_observable(name: &str, num_qubits: usize) -> Result<Self> {
892        match name {
893            "pauli_z_all" => {
894                let size = 1 << num_qubits;
895                let mut data = ArrayD::zeros(IxDyn(&[size, size]));
896                for i in 0..size {
897                    let parity = i.count_ones() % 2;
898                    data[[i, i]] = if parity == 0 { 1.0 } else { -1.0 };
899                }
900                Ok(Self::new(data, false))
901            }
902            _ => Err(MLError::InvalidConfiguration(format!(
903                "Unknown observable: {}",
904                name
905            ))),
906        }
907    }
908}
909
910/// Integration helper functions
911pub mod integration {
912    use super::*;
913
914    /// Convert ndarray to SciRS2Array
915    pub fn from_ndarray<D: Dimension>(arr: Array<f64, D>) -> SciRS2Array {
916        SciRS2Array::from_array(arr)
917    }
918
919    /// Convert SciRS2Array to ndarray
920    pub fn to_ndarray<D: Dimension>(arr: &SciRS2Array) -> Result<Array<f64, D>> {
921        arr.data
922            .view()
923            .into_dimensionality::<D>()
924            .map(|v| v.to_owned())
925            .map_err(|e| MLError::ComputationError(format!("Dimension error: {}", e)))
926    }
927
928    /// Create SciRS2 optimizer from configuration
929    pub fn create_optimizer(optimizer_type: &str, config: HashMap<String, f64>) -> SciRS2Optimizer {
930        let mut optimizer = SciRS2Optimizer::new(optimizer_type);
931        for (key, value) in config {
932            optimizer = optimizer.with_config(key, value);
933        }
934        optimizer
935    }
936
937    /// Setup distributed training
938    pub fn setup_distributed(world_size: usize, rank: usize) -> SciRS2DistributedTrainer {
939        SciRS2DistributedTrainer::new(world_size, rank)
940    }
941}
942
943#[cfg(test)]
944mod tests {
945    use super::*;
946    use scirs2_core::ndarray::Array2;
947
948    #[test]
949    fn test_scirs2_array_creation() {
950        let arr = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0])
951            .expect("valid shape for 2x2 array");
952        let scirs2_arr = SciRS2Array::from_array(arr);
953
954        assert_eq!(scirs2_arr.data.shape(), &[2, 2]);
955        assert!(!scirs2_arr.requires_grad);
956    }
957
958    #[test]
959    fn test_scirs2_array_with_grad() {
960        let arr = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0])
961            .expect("valid shape for 2x2 array");
962        let scirs2_arr = SciRS2Array::with_grad(arr);
963
964        assert!(scirs2_arr.requires_grad);
965        assert!(scirs2_arr.grad.is_some());
966    }
967
968    #[test]
969    fn test_scirs2_matmul() {
970        let arr1 = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
971            .expect("valid shape for 2x3 array");
972        let arr2 = Array2::from_shape_vec((3, 2), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
973            .expect("valid shape for 3x2 array");
974
975        let scirs2_arr1 = SciRS2Array::from_array(arr1);
976        let scirs2_arr2 = SciRS2Array::from_array(arr2);
977
978        let result = scirs2_arr1
979            .matmul(&scirs2_arr2)
980            .expect("matmul should succeed for compatible shapes");
981        assert_eq!(result.data.shape(), &[2, 2]);
982    }
983
984    #[test]
985    fn test_scirs2_optimizer() {
986        let mut optimizer = SciRS2Optimizer::new("adam")
987            .with_config("learning_rate", 0.001)
988            .with_config("beta1", 0.9);
989
990        let mut params = HashMap::new();
991        let param_arr = SciRS2Array::with_grad(Array1::from_vec(vec![1.0, 2.0, 3.0]));
992        params.insert("weight".to_string(), param_arr);
993
994        let result = optimizer.step(&mut params);
995        assert!(result.is_ok());
996    }
997
998    /// Regression test for the fabricated-autograd bug: `MatmulGradFn`,
999    /// `MulGradFn`, and `SumGradFn`'s `backward()` used to be a bare `Ok(())`
1000    /// no-op, and `SciRS2Array` had no way to reach back to the input nodes
1001    /// that produced an output, so gradients could never propagate through
1002    /// matmul/mul/sum. This checks the exact analytic gradients:
1003    /// `d(A@B)/dA = grad@B^T`, `d(A@B)/dB = A^T@grad`,
1004    /// `d(A*B)/dA = grad*B`, `d(A*B)/dB = grad*A`, and that `sum()`
1005    /// broadcasts its scalar seed gradient back across every input element.
1006    #[test]
1007    fn test_matmul_mul_sum_backward_are_real() {
1008        // matmul: A (2x2) @ B (2x2) -> C; seed dL/dC with a known,
1009        // non-uniform gradient and check dL/dA, dL/dB analytically.
1010        let a_arr = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0])
1011            .expect("valid shape for 2x2 array");
1012        let b_arr = Array2::from_shape_vec((2, 2), vec![5.0, 6.0, 7.0, 8.0])
1013            .expect("valid shape for 2x2 array");
1014        let a = SciRS2Array::with_grad(a_arr.clone());
1015        let b = SciRS2Array::with_grad(b_arr.clone());
1016
1017        let mut c = a.matmul(&b).expect("matmul should succeed");
1018        let seed = Array2::from_shape_vec((2, 2), vec![1.0, 0.0, 0.0, 1.0])
1019            .expect("valid shape for 2x2 array")
1020            .into_dyn();
1021        c.set_grad(seed.clone());
1022        c.backward().expect("matmul backward should succeed");
1023
1024        // dL/dA = seed @ B^T, dL/dB = A^T @ seed (both computed independently
1025        // here via plain ndarray ops, not by re-using the code under test).
1026        let seed_2d = seed.into_dimensionality::<Ix2>().expect("2d seed");
1027        let expected_grad_a = seed_2d.dot(&b_arr.t());
1028        let expected_grad_b = a_arr.t().dot(&seed_2d);
1029
1030        let grad_a = a
1031            .grad
1032            .as_ref()
1033            .expect("a should have a grad cell")
1034            .lock()
1035            .expect("grad lock should not be poisoned")
1036            .clone();
1037        let grad_b = b
1038            .grad
1039            .as_ref()
1040            .expect("b should have a grad cell")
1041            .lock()
1042            .expect("grad lock should not be poisoned")
1043            .clone();
1044
1045        for ((i, j), &expected) in expected_grad_a.indexed_iter() {
1046            assert!(
1047                (grad_a[[i, j]] - expected).abs() < 1e-9,
1048                "matmul dL/dA mismatch at ({i},{j}): got {}, expected {expected}",
1049                grad_a[[i, j]]
1050            );
1051        }
1052        for ((i, j), &expected) in expected_grad_b.indexed_iter() {
1053            assert!(
1054                (grad_b[[i, j]] - expected).abs() < 1e-9,
1055                "matmul dL/dB mismatch at ({i},{j}): got {}, expected {expected}",
1056                grad_b[[i, j]]
1057            );
1058        }
1059
1060        // mul: element-wise x * y; dL/dx = seed * y, dL/dy = seed * x.
1061        let x_arr = Array1::from_vec(vec![2.0, 3.0, 4.0]);
1062        let y_arr = Array1::from_vec(vec![10.0, 20.0, 30.0]);
1063        let x = SciRS2Array::with_grad(x_arr.clone());
1064        let y = SciRS2Array::with_grad(y_arr.clone());
1065
1066        let mut z = x.mul(&y).expect("mul should succeed");
1067        z.set_grad(ArrayD::from_elem(IxDyn(&[3]), 2.0));
1068        z.backward().expect("mul backward should succeed");
1069
1070        let grad_x = x
1071            .grad
1072            .as_ref()
1073            .expect("x should have a grad cell")
1074            .lock()
1075            .expect("grad lock should not be poisoned")
1076            .clone();
1077        let grad_y = y
1078            .grad
1079            .as_ref()
1080            .expect("y should have a grad cell")
1081            .lock()
1082            .expect("grad lock should not be poisoned")
1083            .clone();
1084
1085        for (i, (&gx, &yv)) in grad_x.iter().zip(y_arr.iter()).enumerate() {
1086            assert!(
1087                (gx - 2.0 * yv).abs() < 1e-9,
1088                "mul dL/dx mismatch at {i}: got {gx}, expected {}",
1089                2.0 * yv
1090            );
1091        }
1092        for (i, (&gy, &xv)) in grad_y.iter().zip(x_arr.iter()).enumerate() {
1093            assert!(
1094                (gy - 2.0 * xv).abs() < 1e-9,
1095                "mul dL/dy mismatch at {i}: got {gy}, expected {}",
1096                2.0 * xv
1097            );
1098        }
1099
1100        // sum (full reduction): dL/d(input[i]) = seed for every element.
1101        let s_arr = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
1102        let s = SciRS2Array::with_grad(s_arr);
1103        let mut total = s.sum(None).expect("sum should succeed");
1104        total.set_grad(ArrayD::from_elem(IxDyn(&[]), 3.5));
1105        total.backward().expect("sum backward should succeed");
1106
1107        let grad_s = s
1108            .grad
1109            .as_ref()
1110            .expect("s should have a grad cell")
1111            .lock()
1112            .expect("grad lock should not be poisoned")
1113            .clone();
1114        for &g in grad_s.iter() {
1115            assert!(
1116                (g - 3.5).abs() < 1e-9,
1117                "sum backward did not broadcast the seed gradient: got {g}, expected 3.5"
1118            );
1119        }
1120    }
1121
1122    #[test]
1123    fn test_integration_helpers() {
1124        let arr = Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0])
1125            .expect("valid shape for 2x2 array");
1126        let scirs2_arr = integration::from_ndarray(arr.clone());
1127
1128        let back_to_ndarray: Array2<f64> = integration::to_ndarray(&scirs2_arr)
1129            .expect("conversion back to ndarray should succeed");
1130        assert_eq!(arr, back_to_ndarray);
1131    }
1132}