Skip to main content

quantrs2_ml/keras_api/
mod.rs

1//! Keras-style model building API for QuantRS2-ML
2//!
3//! This module provides a Keras-like interface for building quantum machine learning
4//! models, with both Sequential and Functional API patterns familiar to Keras users.
5
6mod attention;
7mod callbacks;
8mod conv;
9mod layers;
10mod quantum_layers;
11mod rnn;
12mod schedules;
13
14pub use attention::*;
15pub use callbacks::*;
16pub use conv::*;
17pub use layers::*;
18pub use quantum_layers::*;
19pub use rnn::*;
20pub use schedules::*;
21
22use crate::error::{MLError, Result};
23use scirs2_core::ndarray::{s, ArrayD, Axis, IxDyn};
24use scirs2_core::random::prelude::*;
25use std::collections::HashMap;
26
27/// Keras-style layer trait
28pub trait KerasLayer: Send + Sync {
29    /// Build the layer (called during model compilation)
30    fn build(&mut self, input_shape: &[usize]) -> Result<()>;
31
32    /// Forward pass through the layer
33    fn call(&self, inputs: &ArrayD<f64>) -> Result<ArrayD<f64>>;
34
35    /// Compute output shape given input shape
36    fn compute_output_shape(&self, input_shape: &[usize]) -> Vec<usize>;
37
38    /// Get layer name
39    fn name(&self) -> &str;
40
41    /// Get trainable parameters
42    fn get_weights(&self) -> Vec<ArrayD<f64>>;
43
44    /// Set trainable parameters
45    fn set_weights(&mut self, weights: Vec<ArrayD<f64>>) -> Result<()>;
46
47    /// Get number of parameters
48    fn count_params(&self) -> usize {
49        self.get_weights().iter().map(|w| w.len()).sum()
50    }
51
52    /// Check if layer is built
53    fn built(&self) -> bool;
54
55    /// Get a stable type tag identifying this layer's concrete kind (e.g.
56    /// `"Dense"`, `"QuantumDense"`, `"Activation"`).
57    ///
58    /// Exporters such as [`crate::onnx_export::ONNXExporter`] only see layers
59    /// behind `dyn KerasLayer` and must still dispatch on the concrete layer
60    /// kind. This default implementation derives the tag from the concrete
61    /// Rust type name (via `std::any::type_name`), so every existing
62    /// `KerasLayer` implementor gets a correct, distinct tag for free without
63    /// needing to override this method; a layer may still override it if it
64    /// wants a different public-facing name than its Rust type name.
65    fn layer_type(&self) -> &'static str {
66        let full_path = std::any::type_name::<Self>();
67        full_path.rsplit("::").next().unwrap_or(full_path)
68    }
69}
70
71/// Activation function types
72#[derive(Debug, Clone)]
73pub enum ActivationFunction {
74    /// Linear activation (identity)
75    Linear,
76    /// ReLU activation
77    ReLU,
78    /// Sigmoid activation
79    Sigmoid,
80    /// Tanh activation
81    Tanh,
82    /// Softmax activation
83    Softmax,
84    /// Leaky ReLU with alpha
85    LeakyReLU(f64),
86    /// ELU with alpha
87    ELU(f64),
88}
89
90/// Weight initializer types
91#[derive(Debug, Clone)]
92pub enum InitializerType {
93    /// All zeros
94    Zeros,
95    /// All ones
96    Ones,
97    /// Glorot uniform (Xavier uniform)
98    GlorotUniform,
99    /// Glorot normal (Xavier normal)
100    GlorotNormal,
101    /// He uniform
102    HeUniform,
103}
104
105/// Sequential model
106pub struct Sequential {
107    /// Layers in the model
108    layers: Vec<Box<dyn KerasLayer>>,
109    /// Model name
110    name: String,
111    /// Built flag
112    built: bool,
113    /// Compiled flag
114    compiled: bool,
115    /// Input shape
116    input_shape: Option<Vec<usize>>,
117    /// Loss function
118    loss: Option<LossFunction>,
119    /// Optimizer
120    optimizer: Option<OptimizerType>,
121    /// Metrics
122    metrics: Vec<MetricType>,
123}
124
125impl Sequential {
126    /// Create new sequential model
127    pub fn new() -> Self {
128        Self {
129            layers: Vec::new(),
130            name: format!("sequential_{}", fastrand::u32(..)),
131            built: false,
132            compiled: false,
133            input_shape: None,
134            loss: None,
135            optimizer: None,
136            metrics: Vec::new(),
137        }
138    }
139
140    /// Set model name
141    pub fn name(mut self, name: impl Into<String>) -> Self {
142        self.name = name.into();
143        self
144    }
145
146    /// Add layer to model
147    pub fn add(&mut self, layer: Box<dyn KerasLayer>) {
148        self.layers.push(layer);
149        self.built = false;
150    }
151
152    /// Get the model's layers in order.
153    ///
154    /// Exposed so callers outside this module (e.g. the ONNX exporter in
155    /// [`crate::onnx_export`]) can walk the real layer list rather than being
156    /// forced to reach for a hardcoded stand-in, since `layers` itself is a
157    /// private field of `Sequential`.
158    pub fn layers(&self) -> &[Box<dyn KerasLayer>] {
159        &self.layers
160    }
161
162    /// Compute the model's output shape for a given input shape by chaining
163    /// each layer's own [`KerasLayer::compute_output_shape`] in sequence,
164    /// exactly like [`Self::build`] does.
165    pub fn compute_output_shape(&self, input_shape: &[usize]) -> Vec<usize> {
166        let mut current_shape = input_shape.to_vec();
167        for layer in &self.layers {
168            current_shape = layer.compute_output_shape(&current_shape);
169        }
170        current_shape
171    }
172
173    /// Build the model with given input shape
174    pub fn build(&mut self, input_shape: Vec<usize>) -> Result<()> {
175        self.input_shape = Some(input_shape.clone());
176        let mut current_shape = input_shape;
177
178        for layer in &mut self.layers {
179            layer.build(&current_shape)?;
180            current_shape = layer.compute_output_shape(&current_shape);
181        }
182
183        self.built = true;
184        Ok(())
185    }
186
187    /// Compile the model
188    pub fn compile(
189        mut self,
190        loss: LossFunction,
191        optimizer: OptimizerType,
192        metrics: Vec<MetricType>,
193    ) -> Self {
194        self.loss = Some(loss);
195        self.optimizer = Some(optimizer);
196        self.metrics = metrics;
197        self.compiled = true;
198        self
199    }
200
201    /// Get model summary
202    pub fn summary(&self) -> ModelSummary {
203        let mut layers_info = Vec::new();
204        let mut total_params = 0;
205        let mut trainable_params = 0;
206
207        let mut current_shape = self.input_shape.clone().unwrap_or_default();
208
209        for layer in &self.layers {
210            let output_shape = layer.compute_output_shape(&current_shape);
211            let params = layer.count_params();
212
213            layers_info.push(LayerInfo {
214                name: layer.name().to_string(),
215                layer_type: "Layer".to_string(),
216                output_shape: output_shape.clone(),
217                param_count: params,
218            });
219
220            total_params += params;
221            trainable_params += params;
222            current_shape = output_shape;
223        }
224
225        ModelSummary {
226            layers: layers_info,
227            total_params,
228            trainable_params,
229            non_trainable_params: 0,
230        }
231    }
232
233    /// Forward pass (predict)
234    pub fn predict(&self, inputs: &ArrayD<f64>) -> Result<ArrayD<f64>> {
235        if !self.built {
236            return Err(MLError::InvalidConfiguration(
237                "Model must be built before prediction".to_string(),
238            ));
239        }
240
241        let mut current = inputs.clone();
242
243        for layer in &self.layers {
244            current = layer.call(&current)?;
245        }
246
247        Ok(current)
248    }
249
250    /// Train the model
251    #[allow(non_snake_case)]
252    pub fn fit(
253        &mut self,
254        X: &ArrayD<f64>,
255        y: &ArrayD<f64>,
256        epochs: usize,
257        batch_size: Option<usize>,
258        validation_data: Option<(&ArrayD<f64>, &ArrayD<f64>)>,
259        callbacks: Vec<Box<dyn Callback>>,
260    ) -> Result<TrainingHistory> {
261        if !self.compiled {
262            return Err(MLError::InvalidConfiguration(
263                "Model must be compiled before training".to_string(),
264            ));
265        }
266
267        let batch_size = batch_size.unwrap_or(32);
268        let n_samples = X.shape()[0];
269        let n_batches = (n_samples + batch_size - 1) / batch_size;
270
271        let mut history = TrainingHistory::new();
272
273        for epoch in 0..epochs {
274            let mut epoch_loss = 0.0;
275            let mut epoch_metrics: HashMap<String, f64> = HashMap::new();
276
277            for metric in &self.metrics {
278                epoch_metrics.insert(metric.name(), 0.0);
279            }
280
281            for batch_idx in 0..n_batches {
282                let start_idx = batch_idx * batch_size;
283                let end_idx = ((batch_idx + 1) * batch_size).min(n_samples);
284
285                let X_batch = X.slice(s![start_idx..end_idx, ..]);
286                let y_batch = y.slice(s![start_idx..end_idx, ..]);
287
288                let predictions = self.predict(&X_batch.to_owned().into_dyn())?;
289
290                let loss = self.compute_loss(&predictions, &y_batch.to_owned().into_dyn())?;
291                epoch_loss += loss;
292
293                self.backward_pass(
294                    &X_batch.to_owned().into_dyn(),
295                    &y_batch.to_owned().into_dyn(),
296                )?;
297
298                for metric in &self.metrics {
299                    let metric_value =
300                        metric.compute(&predictions, &y_batch.to_owned().into_dyn())?;
301                    *epoch_metrics.entry(metric.name()).or_insert(0.0) += metric_value;
302                }
303            }
304
305            epoch_loss /= n_batches as f64;
306            for value in epoch_metrics.values_mut() {
307                *value /= n_batches as f64;
308            }
309
310            let (val_loss, val_metrics) = if let Some((X_val, y_val)) = validation_data {
311                let val_predictions = self.predict(X_val)?;
312                let val_loss = self.compute_loss(&val_predictions, y_val)?;
313
314                let mut val_metrics = HashMap::new();
315                for metric in &self.metrics {
316                    let metric_value = metric.compute(&val_predictions, y_val)?;
317                    val_metrics.insert(format!("val_{}", metric.name()), metric_value);
318                }
319
320                (Some(val_loss), val_metrics)
321            } else {
322                (None, HashMap::new())
323            };
324
325            history.add_epoch(epoch_loss, epoch_metrics, val_loss, val_metrics);
326
327            for callback in &callbacks {
328                callback.on_epoch_end(epoch, &history)?;
329            }
330
331            println!("Epoch {}/{} - loss: {:.4}", epoch + 1, epochs, epoch_loss);
332        }
333
334        Ok(history)
335    }
336
337    /// Evaluate the model
338    #[allow(non_snake_case)]
339    pub fn evaluate(
340        &self,
341        X: &ArrayD<f64>,
342        y: &ArrayD<f64>,
343        _batch_size: Option<usize>,
344    ) -> Result<HashMap<String, f64>> {
345        let predictions = self.predict(X)?;
346        let loss = self.compute_loss(&predictions, y)?;
347
348        let mut results = HashMap::new();
349        results.insert("loss".to_string(), loss);
350
351        for metric in &self.metrics {
352            let metric_value = metric.compute(&predictions, y)?;
353            results.insert(metric.name(), metric_value);
354        }
355
356        Ok(results)
357    }
358
359    /// Compute loss
360    fn compute_loss(&self, predictions: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<f64> {
361        if let Some(ref loss_fn) = self.loss {
362            loss_fn.compute(predictions, targets)
363        } else {
364            Err(MLError::InvalidConfiguration(
365                "Loss function not specified".to_string(),
366            ))
367        }
368    }
369
370    /// Backward pass: for every layer with trainable weights, estimate a
371    /// real SPSA (simultaneous perturbation stochastic approximation)
372    /// gradient of the compiled loss function with respect to that layer's
373    /// flattened weights (by actually re-running `predict` on `x_batch` and
374    /// `compute_loss` at perturbed weight values -- not fabricated), then
375    /// apply a real gradient-descent step at the compiled optimizer's
376    /// learning rate. Previously this was a complete no-op, so `fit`
377    /// reported an honest per-batch loss but never updated any layer's
378    /// weights.
379    fn backward_pass(&mut self, x_batch: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<()> {
380        const PERTURBATION_SCALE: f64 = 1e-3;
381        let learning_rate = self
382            .optimizer
383            .as_ref()
384            .map(optimizer_learning_rate)
385            .unwrap_or(0.01);
386
387        for layer_idx in 0..self.layers.len() {
388            let weights = self.layers[layer_idx].get_weights();
389            if weights.is_empty() {
390                continue;
391            }
392            let shapes: Vec<Vec<usize>> = weights.iter().map(|w| w.shape().to_vec()).collect();
393            let flat: Vec<f64> = weights.iter().flat_map(|w| w.iter().cloned()).collect();
394
395            let mut rng = thread_rng();
396            let direction: Vec<f64> = (0..flat.len())
397                .map(|_| if rng.random::<f64>() < 0.5 { -1.0 } else { 1.0 })
398                .collect();
399
400            let plus_flat: Vec<f64> = flat
401                .iter()
402                .zip(direction.iter())
403                .map(|(w, d)| w + PERTURBATION_SCALE * d)
404                .collect();
405            self.set_layer_weights_from_flat(layer_idx, &shapes, &plus_flat)?;
406            let predictions_plus = self.predict(x_batch)?;
407            let loss_plus = self.compute_loss(&predictions_plus, targets)?;
408
409            let minus_flat: Vec<f64> = flat
410                .iter()
411                .zip(direction.iter())
412                .map(|(w, d)| w - PERTURBATION_SCALE * d)
413                .collect();
414            self.set_layer_weights_from_flat(layer_idx, &shapes, &minus_flat)?;
415            let predictions_minus = self.predict(x_batch)?;
416            let loss_minus = self.compute_loss(&predictions_minus, targets)?;
417
418            let loss_delta = loss_plus - loss_minus;
419            let gradient: Vec<f64> = direction
420                .iter()
421                .map(|d| (loss_delta / (2.0 * PERTURBATION_SCALE)) * d)
422                .collect();
423            let updated_flat: Vec<f64> = flat
424                .iter()
425                .zip(gradient.iter())
426                .map(|(w, g)| w - learning_rate * g)
427                .collect();
428            self.set_layer_weights_from_flat(layer_idx, &shapes, &updated_flat)?;
429        }
430
431        Ok(())
432    }
433
434    /// Reshape a flat weight vector back into the per-tensor shapes it came
435    /// from and apply it to layer `layer_idx` via [`KerasLayer::set_weights`].
436    fn set_layer_weights_from_flat(
437        &mut self,
438        layer_idx: usize,
439        shapes: &[Vec<usize>],
440        flat: &[f64],
441    ) -> Result<()> {
442        let mut offset = 0;
443        let mut weights = Vec::with_capacity(shapes.len());
444        for shape in shapes {
445            let len: usize = shape.iter().product();
446            let slice = flat[offset..offset + len].to_vec();
447            let array = ArrayD::from_shape_vec(IxDyn(shape), slice).map_err(|e| {
448                MLError::ComputationError(format!("Failed to reshape layer weights: {e}"))
449            })?;
450            weights.push(array);
451            offset += len;
452        }
453        self.layers[layer_idx].set_weights(weights)
454    }
455}
456
457/// Extract the learning rate carried by an [`OptimizerType`] variant.
458fn optimizer_learning_rate(optimizer: &OptimizerType) -> f64 {
459    match optimizer {
460        OptimizerType::SGD { learning_rate, .. } => *learning_rate,
461        OptimizerType::Adam { learning_rate, .. } => *learning_rate,
462        OptimizerType::RMSprop { learning_rate, .. } => *learning_rate,
463        OptimizerType::AdaGrad { learning_rate, .. } => *learning_rate,
464    }
465}
466
467impl Default for Sequential {
468    fn default() -> Self {
469        Self::new()
470    }
471}
472
473/// Loss functions
474#[derive(Debug, Clone)]
475pub enum LossFunction {
476    /// Mean squared error
477    MeanSquaredError,
478    /// Binary crossentropy
479    BinaryCrossentropy,
480    /// Categorical crossentropy
481    CategoricalCrossentropy,
482    /// Sparse categorical crossentropy
483    SparseCategoricalCrossentropy,
484    /// Mean absolute error
485    MeanAbsoluteError,
486    /// Huber loss
487    Huber(f64),
488}
489
490impl LossFunction {
491    /// Compute loss
492    pub fn compute(&self, predictions: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<f64> {
493        match self {
494            LossFunction::MeanSquaredError => {
495                let diff = predictions - targets;
496                diff.mapv(|x| x * x).mean().ok_or_else(|| {
497                    MLError::ComputationError("Failed to compute mean of empty array".to_string())
498                })
499            }
500            LossFunction::BinaryCrossentropy => {
501                let epsilon = 1e-15;
502                let clipped_preds = predictions.mapv(|x| x.max(epsilon).min(1.0 - epsilon));
503                let loss = targets * clipped_preds.mapv(|x| x.ln())
504                    + (1.0 - targets) * clipped_preds.mapv(|x| (1.0 - x).ln());
505                loss.mean().map(|m| -m).ok_or_else(|| {
506                    MLError::ComputationError("Failed to compute mean of empty array".to_string())
507                })
508            }
509            LossFunction::MeanAbsoluteError => {
510                let diff = predictions - targets;
511                diff.mapv(|x| x.abs()).mean().ok_or_else(|| {
512                    MLError::ComputationError("Failed to compute mean of empty array".to_string())
513                })
514            }
515            _ => Err(MLError::InvalidConfiguration(
516                "Loss function not implemented".to_string(),
517            )),
518        }
519    }
520}
521
522/// Optimizer types
523#[derive(Debug, Clone)]
524pub enum OptimizerType {
525    /// Stochastic Gradient Descent
526    SGD { learning_rate: f64, momentum: f64 },
527    /// Adam optimizer
528    Adam {
529        learning_rate: f64,
530        beta1: f64,
531        beta2: f64,
532        epsilon: f64,
533    },
534    /// RMSprop optimizer
535    RMSprop {
536        learning_rate: f64,
537        rho: f64,
538        epsilon: f64,
539    },
540    /// AdaGrad optimizer
541    AdaGrad { learning_rate: f64, epsilon: f64 },
542}
543
544/// Metric types
545#[derive(Debug, Clone)]
546pub enum MetricType {
547    /// Accuracy
548    Accuracy,
549    /// Precision
550    Precision,
551    /// Recall
552    Recall,
553    /// F1 Score
554    F1Score,
555    /// Mean Absolute Error
556    MeanAbsoluteError,
557    /// Mean Squared Error
558    MeanSquaredError,
559}
560
561impl MetricType {
562    /// Get metric name
563    pub fn name(&self) -> String {
564        match self {
565            MetricType::Accuracy => "accuracy".to_string(),
566            MetricType::Precision => "precision".to_string(),
567            MetricType::Recall => "recall".to_string(),
568            MetricType::F1Score => "f1_score".to_string(),
569            MetricType::MeanAbsoluteError => "mean_absolute_error".to_string(),
570            MetricType::MeanSquaredError => "mean_squared_error".to_string(),
571        }
572    }
573
574    /// Compute metric
575    pub fn compute(&self, predictions: &ArrayD<f64>, targets: &ArrayD<f64>) -> Result<f64> {
576        match self {
577            MetricType::Accuracy => {
578                let pred_classes = predictions.mapv(|x| if x > 0.5 { 1.0 } else { 0.0 });
579                let correct = pred_classes
580                    .iter()
581                    .zip(targets.iter())
582                    .filter(|(&pred, &target)| (pred - target).abs() < 1e-6)
583                    .count();
584                Ok(correct as f64 / targets.len() as f64)
585            }
586            MetricType::MeanAbsoluteError => {
587                let diff = predictions - targets;
588                diff.mapv(|x| x.abs()).mean().ok_or_else(|| {
589                    MLError::ComputationError("Failed to compute mean of empty array".to_string())
590                })
591            }
592            MetricType::MeanSquaredError => {
593                let diff = predictions - targets;
594                diff.mapv(|x| x * x).mean().ok_or_else(|| {
595                    MLError::ComputationError("Failed to compute mean of empty array".to_string())
596                })
597            }
598            MetricType::Precision => {
599                let true_positives = predictions
600                    .iter()
601                    .zip(targets.iter())
602                    .filter(|(&pred, &target)| pred > 0.5 && target > 0.5)
603                    .count() as f64;
604                let predicted_positives =
605                    predictions.iter().filter(|&&pred| pred > 0.5).count() as f64;
606                if predicted_positives > 0.0 {
607                    Ok(true_positives / predicted_positives)
608                } else {
609                    Ok(0.0)
610                }
611            }
612            MetricType::Recall => {
613                let true_positives = predictions
614                    .iter()
615                    .zip(targets.iter())
616                    .filter(|(&pred, &target)| pred > 0.5 && target > 0.5)
617                    .count() as f64;
618                let actual_positives =
619                    targets.iter().filter(|&&target| target > 0.5).count() as f64;
620                if actual_positives > 0.0 {
621                    Ok(true_positives / actual_positives)
622                } else {
623                    Ok(0.0)
624                }
625            }
626            MetricType::F1Score => {
627                let precision = MetricType::Precision.compute(predictions, targets)?;
628                let recall = MetricType::Recall.compute(predictions, targets)?;
629                if precision + recall > 0.0 {
630                    Ok(2.0 * precision * recall / (precision + recall))
631                } else {
632                    Ok(0.0)
633                }
634            }
635        }
636    }
637}
638
639/// Training history
640#[derive(Debug, Clone)]
641pub struct TrainingHistory {
642    /// Training loss for each epoch
643    pub loss: Vec<f64>,
644    /// Training metrics for each epoch
645    pub metrics: Vec<HashMap<String, f64>>,
646    /// Validation loss for each epoch
647    pub val_loss: Vec<f64>,
648    /// Validation metrics for each epoch
649    pub val_metrics: Vec<HashMap<String, f64>>,
650}
651
652impl TrainingHistory {
653    /// Create new training history
654    pub fn new() -> Self {
655        Self {
656            loss: Vec::new(),
657            metrics: Vec::new(),
658            val_loss: Vec::new(),
659            val_metrics: Vec::new(),
660        }
661    }
662
663    /// Add epoch results
664    pub fn add_epoch(
665        &mut self,
666        loss: f64,
667        metrics: HashMap<String, f64>,
668        val_loss: Option<f64>,
669        val_metrics: HashMap<String, f64>,
670    ) {
671        self.loss.push(loss);
672        self.metrics.push(metrics);
673
674        if let Some(val_loss) = val_loss {
675            self.val_loss.push(val_loss);
676        }
677        self.val_metrics.push(val_metrics);
678    }
679}
680
681impl Default for TrainingHistory {
682    fn default() -> Self {
683        Self::new()
684    }
685}
686
687/// Model summary information
688#[derive(Debug)]
689pub struct ModelSummary {
690    /// Layer information
691    pub layers: Vec<LayerInfo>,
692    /// Total number of parameters
693    pub total_params: usize,
694    /// Number of trainable parameters
695    pub trainable_params: usize,
696    /// Number of non-trainable parameters
697    pub non_trainable_params: usize,
698}
699
700/// Layer information for summary
701#[derive(Debug)]
702pub struct LayerInfo {
703    /// Layer name
704    pub name: String,
705    /// Layer type
706    pub layer_type: String,
707    /// Output shape
708    pub output_shape: Vec<usize>,
709    /// Parameter count
710    pub param_count: usize,
711}
712
713/// Model input specification
714pub struct Input {
715    /// Input shape (excluding batch dimension)
716    pub shape: Vec<usize>,
717    /// Input name
718    pub name: Option<String>,
719    /// Data type
720    pub dtype: DataType,
721}
722
723impl Input {
724    /// Create new input specification
725    pub fn new(shape: Vec<usize>) -> Self {
726        Self {
727            shape,
728            name: None,
729            dtype: DataType::Float64,
730        }
731    }
732
733    /// Set input name
734    pub fn name(mut self, name: impl Into<String>) -> Self {
735        self.name = Some(name.into());
736        self
737    }
738
739    /// Set data type
740    pub fn dtype(mut self, dtype: DataType) -> Self {
741        self.dtype = dtype;
742        self
743    }
744}
745
746/// Data types
747#[derive(Debug, Clone)]
748pub enum DataType {
749    /// 32-bit float
750    Float32,
751    /// 64-bit float
752    Float64,
753    /// 32-bit integer
754    Int32,
755    /// 64-bit integer
756    Int64,
757}
758
759/// Utility functions for building models
760pub mod utils {
761    use super::*;
762
763    /// Create a simple sequential model for classification
764    pub fn create_classification_model(
765        _input_dim: usize,
766        num_classes: usize,
767        hidden_layers: Vec<usize>,
768    ) -> Sequential {
769        let mut model = Sequential::new();
770
771        for (i, &units) in hidden_layers.iter().enumerate() {
772            model.add(Box::new(
773                Dense::new(units)
774                    .activation(ActivationFunction::ReLU)
775                    .name(format!("dense_{}", i)),
776            ));
777        }
778
779        let output_activation = if num_classes == 2 {
780            ActivationFunction::Sigmoid
781        } else {
782            ActivationFunction::Softmax
783        };
784
785        model.add(Box::new(
786            Dense::new(num_classes)
787                .activation(output_activation)
788                .name("output"),
789        ));
790
791        model
792    }
793
794    /// Create a quantum neural network model
795    pub fn create_quantum_model(
796        num_qubits: usize,
797        num_classes: usize,
798        num_layers: usize,
799    ) -> Sequential {
800        let mut model = Sequential::new();
801
802        model.add(Box::new(
803            QuantumDense::new(num_qubits, num_classes)
804                .num_layers(num_layers)
805                .ansatz_type(QuantumAnsatzType::HardwareEfficient)
806                .name("quantum_layer"),
807        ));
808
809        if num_classes > 1 {
810            model.add(Box::new(
811                Activation::new(ActivationFunction::Softmax).name("softmax"),
812            ));
813        }
814
815        model
816    }
817
818    /// Create a hybrid quantum-classical model
819    pub fn create_hybrid_model(
820        _input_dim: usize,
821        num_qubits: usize,
822        num_classes: usize,
823        classical_hidden: Vec<usize>,
824    ) -> Sequential {
825        let mut model = Sequential::new();
826
827        for (i, &units) in classical_hidden.iter().enumerate() {
828            model.add(Box::new(
829                Dense::new(units)
830                    .activation(ActivationFunction::ReLU)
831                    .name(format!("classical_{}", i)),
832            ));
833        }
834
835        model.add(Box::new(
836            QuantumDense::new(num_qubits, 64)
837                .num_layers(2)
838                .ansatz_type(QuantumAnsatzType::HardwareEfficient)
839                .name("quantum_layer"),
840        ));
841
842        model.add(Box::new(
843            Dense::new(num_classes)
844                .activation(if num_classes == 2 {
845                    ActivationFunction::Sigmoid
846                } else {
847                    ActivationFunction::Softmax
848                })
849                .name("output"),
850        ));
851
852        model
853    }
854}
855
856#[cfg(test)]
857mod tests {
858    use super::*;
859    use scirs2_core::ndarray::Array;
860
861    #[test]
862    fn test_dense_layer() {
863        let mut dense = Dense::new(10)
864            .activation(ActivationFunction::ReLU)
865            .name("test_dense");
866
867        assert!(!dense.built());
868
869        dense.build(&[5]).expect("Should build successfully");
870
871        assert!(dense.built());
872        assert_eq!(dense.compute_output_shape(&[32, 5]), vec![32, 10]);
873    }
874
875    #[test]
876    fn test_sequential_model() {
877        let mut model = Sequential::new();
878        model.add(Box::new(Dense::new(10)));
879        model.add(Box::new(Activation::new(ActivationFunction::ReLU)));
880        model.add(Box::new(Dense::new(5)));
881
882        model
883            .build(vec![32, 20])
884            .expect("Should build successfully");
885
886        let summary = model.summary();
887        assert_eq!(summary.layers.len(), 3);
888    }
889
890    #[test]
891    fn test_activation_functions() {
892        let relu = ActivationFunction::ReLU;
893        let sigmoid = ActivationFunction::Sigmoid;
894        let _tanh = ActivationFunction::Tanh;
895
896        let mut act_relu = Activation::new(relu);
897        act_relu.build(&[10]).expect("Should build");
898
899        let mut act_sigmoid = Activation::new(sigmoid);
900        act_sigmoid.build(&[10]).expect("Should build");
901    }
902
903    /// Regression test for the "backward_pass is a no-op" bug: `fit` must
904    /// actually change the Dense layer's weights and reduce the training
905    /// loss on an easily learnable linear-regression toy problem.
906    #[test]
907    fn test_fit_updates_weights_and_reduces_loss() {
908        let mut model = Sequential::new();
909        model.add(Box::new(Dense::new(2).name("dense1")));
910        model.build(vec![4, 3]).expect("build should succeed");
911        let mut model = model.compile(
912            LossFunction::MeanSquaredError,
913            OptimizerType::SGD {
914                learning_rate: 0.5,
915                momentum: 0.0,
916            },
917            vec![],
918        );
919
920        let x = Array::from_shape_vec(
921            IxDyn(&[4, 3]),
922            vec![0.1, 0.2, 0.3, 0.4, 0.1, 0.2, 0.3, 0.4, 0.1, 0.2, 0.3, 0.4],
923        )
924        .expect("valid shape");
925        let y = Array::from_shape_vec(IxDyn(&[4, 2]), vec![1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0])
926            .expect("valid shape");
927
928        let weights_before = model.layers[0].get_weights();
929        let initial_predictions = model.predict(&x).expect("predict should succeed");
930        let initial_loss = model
931            .compute_loss(&initial_predictions, &y)
932            .expect("loss should compute");
933
934        let history = model
935            .fit(&x, &y, 150, Some(4), None, vec![])
936            .expect("fit should succeed");
937
938        let weights_after = model.layers[0].get_weights();
939        let weights_changed = weights_before[0]
940            .iter()
941            .zip(weights_after[0].iter())
942            .any(|(a, b)| (a - b).abs() > 1e-9);
943        assert!(
944            weights_changed,
945            "expected Dense layer weights to change after fit"
946        );
947
948        let final_loss = *history.loss.last().expect("history should have losses");
949        assert!(
950            final_loss < initial_loss,
951            "expected training loss to decrease: initial={initial_loss}, final={final_loss}"
952        );
953    }
954
955    /// Regression test for the "Precision/Recall/F1Score always error" gap:
956    /// these metrics must now be computed from real confusion-matrix counts.
957    #[test]
958    fn test_precision_recall_f1_are_computed() {
959        let predictions =
960            Array::from_shape_vec(IxDyn(&[4]), vec![0.9, 0.1, 0.8, 0.3]).expect("valid shape");
961        let targets =
962            Array::from_shape_vec(IxDyn(&[4]), vec![1.0, 0.0, 0.0, 1.0]).expect("valid shape");
963
964        let precision = MetricType::Precision
965            .compute(&predictions, &targets)
966            .expect("precision should compute");
967        let recall = MetricType::Recall
968            .compute(&predictions, &targets)
969            .expect("recall should compute");
970        let f1 = MetricType::F1Score
971            .compute(&predictions, &targets)
972            .expect("f1 should compute");
973
974        // Predicted positives (pred > 0.5): indices 0, 2. True positives
975        // among those (target > 0.5 too): index 0 only -> precision = 1/2.
976        assert!((precision - 0.5).abs() < 1e-9, "precision was {precision}");
977        // Actual positives (target > 0.5): indices 0, 3. True positives
978        // found: index 0 only -> recall = 1/2.
979        assert!((recall - 0.5).abs() < 1e-9, "recall was {recall}");
980        // Equal precision and recall -> F1 equals the same value.
981        assert!((f1 - 0.5).abs() < 1e-9, "f1 was {f1}");
982    }
983}