Skip to main content

quantrs2_ml/
qnn.rs

1//! Quantum Neural Networks (QNNs) with parameterised quantum circuits.
2//!
3//! [`QuantumNeuralNetwork`] wraps a parameterised quantum circuit as a
4//! differentiable layer, supporting forward passes, parameter-shift gradient
5//! computation, and stochastic gradient descent-based training.
6
7use crate::error::{MLError, Result};
8use crate::optimization::Optimizer;
9use quantrs2_circuit::builder::Simulator;
10use quantrs2_circuit::prelude::Circuit;
11use quantrs2_sim::statevector::StateVectorSimulator;
12use scirs2_core::ndarray::{Array1, Array2};
13use scirs2_core::random::prelude::*;
14use scirs2_core::Complex64;
15use std::f64::consts::FRAC_PI_2;
16use std::fmt;
17
18/// Maximum number of qubits the built-in state-vector forward pass will
19/// simulate.  Above this size a dense state vector becomes impractical and the
20/// forward pass returns an honest [`MLError::NotSupported`] instead of
21/// fabricating an answer.
22const MAX_FORWARD_QUBITS: usize = 16;
23
24/// When the number of trainable parameters does not exceed this bound, training
25/// uses exact parameter-shift-rule gradients; larger circuits fall back to the
26/// SPSA stochastic gradient estimator to keep the number of circuit evaluations
27/// tractable.
28const PARAMETER_SHIFT_MAX_PARAMS: usize = 64;
29
30/// Compute the expectation value `<ψ|P_q|ψ>` of a single-qubit Pauli operator
31/// `P ∈ {X, Y, Z}` acting on qubit `qubit` for a state vector `amplitudes`.
32///
33/// The state vector has `2^n` entries indexed so that bit `qubit` (LSB = qubit
34/// 0) selects the computational basis state of that qubit.  The result is a real
35/// number in `[-1, 1]`.
36fn single_qubit_pauli_expectation(
37    amplitudes: &[Complex64],
38    pauli: char,
39    qubit: usize,
40) -> Result<f64> {
41    let dim = amplitudes.len();
42    if dim == 0 || dim & (dim - 1) != 0 {
43        return Err(MLError::ComputationError(format!(
44            "state-vector dimension {dim} is not a positive power of two"
45        )));
46    }
47    let n = dim.trailing_zeros() as usize;
48    if qubit >= n {
49        return Err(MLError::ComputationError(format!(
50            "qubit index {qubit} out of range for {n}-qubit state"
51        )));
52    }
53
54    let bit = 1usize << qubit;
55    let value = match pauli {
56        'Z' => {
57            let mut expectation = 0.0_f64;
58            for (j, amp) in amplitudes.iter().enumerate() {
59                let prob = amp.norm_sqr();
60                if j & bit == 0 {
61                    expectation += prob;
62                } else {
63                    expectation -= prob;
64                }
65            }
66            expectation
67        }
68        'X' => {
69            // <X_q> = 2 Re[ Σ_{j: bit q = 0} conj(ψ_j) · ψ_{j ⊕ bit} ]
70            let mut sum = Complex64::new(0.0, 0.0);
71            for (j, amp) in amplitudes.iter().enumerate() {
72                if j & bit == 0 {
73                    sum += amp.conj() * amplitudes[j ^ bit];
74                }
75            }
76            2.0 * sum.re
77        }
78        'Y' => {
79            // <Y_q> = 2 Im[ Σ_{j: bit q = 0} conj(ψ_j) · ψ_{j ⊕ bit} ]
80            let mut sum = Complex64::new(0.0, 0.0);
81            for (j, amp) in amplitudes.iter().enumerate() {
82                if j & bit == 0 {
83                    sum += amp.conj() * amplitudes[j ^ bit];
84                }
85            }
86            2.0 * sum.im
87        }
88        other => {
89            return Err(MLError::ComputationError(format!(
90                "unsupported Pauli operator '{other}'"
91            )))
92        }
93    };
94    Ok(value)
95}
96
97/// Activation function types for quantum layers
98#[derive(Debug, Clone, Copy, PartialEq)]
99pub enum ActivationType {
100    /// Linear activation (identity)
101    Linear,
102    /// ReLU activation
103    ReLU,
104    /// Sigmoid activation
105    Sigmoid,
106    /// Tanh activation
107    Tanh,
108}
109
110/// Represents a layer type in a quantum neural network
111#[derive(Debug, Clone)]
112pub enum QNNLayerType {
113    /// Encoding layer for converting classical data to quantum states
114    EncodingLayer {
115        /// Number of classical features to encode
116        num_features: usize,
117    },
118
119    /// Variational layer with trainable parameters
120    VariationalLayer {
121        /// Number of trainable parameters
122        num_params: usize,
123    },
124
125    /// Entanglement layer to create entanglement between qubits
126    EntanglementLayer {
127        /// Connectivity pattern, e.g., "full", "linear", "circular"
128        connectivity: String,
129    },
130
131    /// Measurement layer to extract classical information
132    MeasurementLayer {
133        /// Measurement basis, e.g., "computational", "Pauli-X", "Pauli-Y", "Pauli-Z"
134        measurement_basis: String,
135    },
136}
137
138/// Results from training a quantum neural network
139#[derive(Debug, Clone)]
140pub struct TrainingResult {
141    /// Final loss value after training
142    pub final_loss: f64,
143
144    /// Training accuracy (for classification tasks)
145    pub accuracy: f64,
146
147    /// Loss history during training
148    pub loss_history: Vec<f64>,
149
150    /// Optimal parameters found during training
151    pub optimal_parameters: Array1<f64>,
152}
153
154/// Represents a quantum neural network.
155///
156/// A QNN consists of an ordered sequence of [`QNNLayerType`] layers that map
157/// classical input vectors to output predictions via a parameterised quantum
158/// circuit evaluated on a state-vector simulator.
159///
160/// # Examples
161///
162/// ```rust
163/// use quantrs2_ml::qnn::{QuantumNeuralNetwork, QNNLayerType};
164///
165/// let layers = vec![
166///     QNNLayerType::EncodingLayer { num_features: 2 },
167///     QNNLayerType::VariationalLayer { num_params: 4 },
168/// ];
169/// let qnn = QuantumNeuralNetwork::new(layers, 2, 2, 1)
170///     .expect("failed to create QNN");
171/// assert_eq!(qnn.num_qubits, 2);
172/// ```
173#[derive(Debug, Clone)]
174pub struct QuantumNeuralNetwork {
175    /// The layers that make up the network
176    pub layers: Vec<QNNLayerType>,
177
178    /// The number of qubits used in the network
179    pub num_qubits: usize,
180
181    /// The dimension of the input data
182    pub input_dim: usize,
183
184    /// The dimension of the output data
185    pub output_dim: usize,
186
187    /// Network parameters (weights)
188    pub parameters: Array1<f64>,
189}
190
191impl QuantumNeuralNetwork {
192    /// Creates a new quantum neural network
193    pub fn new(
194        layers: Vec<QNNLayerType>,
195        num_qubits: usize,
196        input_dim: usize,
197        output_dim: usize,
198    ) -> Result<Self> {
199        // Validate the layers and structure
200        if layers.is_empty() {
201            return Err(MLError::ModelCreationError(
202                "QNN must have at least one layer".to_string(),
203            ));
204        }
205
206        // Determine parameter count from variational layers
207        let num_params = layers
208            .iter()
209            .filter_map(|layer| match layer {
210                QNNLayerType::VariationalLayer { num_params } => Some(num_params),
211                _ => None,
212            })
213            .sum::<usize>();
214
215        // Create random initial parameters
216        let parameters = Array1::from_vec(
217            (0..num_params)
218                .map(|_| thread_rng().random::<f64>() * 2.0 * std::f64::consts::PI)
219                .collect(),
220        );
221
222        Ok(QuantumNeuralNetwork {
223            layers,
224            num_qubits,
225            input_dim,
226            output_dim,
227            parameters,
228        })
229    }
230
231    /// Append the parameterised gates described by [`Self::layers`] onto a
232    /// state-vector circuit of register size `N`.
233    ///
234    /// * `EncodingLayer`   — angle-encodes the classical `input` features onto
235    ///   `RY` rotations (data re-uploading friendly).
236    /// * `VariationalLayer`— applies the trainable `parameters` as alternating
237    ///   `RY`/`RZ` sweeps across the active qubits (every qubit receives an `RY`
238    ///   before any qubit receives an `RZ`).
239    /// * `EntanglementLayer`— applies a `CNOT` pattern (`linear`, `circular`,
240    ///   or `full`) across the active qubits.
241    /// * `MeasurementLayer`— readout is handled separately in [`Self::readout`].
242    fn append_layers<const N: usize>(
243        &self,
244        circuit: &mut Circuit<N>,
245        input: &Array1<f64>,
246        parameters: &Array1<f64>,
247    ) -> Result<()> {
248        let num_qubits = self.num_qubits.min(N);
249        if num_qubits == 0 {
250            return Err(MLError::ModelCreationError(
251                "QNN requires at least one qubit".to_string(),
252            ));
253        }
254
255        let mut param_idx = 0usize;
256        for layer in &self.layers {
257            match layer {
258                QNNLayerType::EncodingLayer { num_features } => {
259                    let count = (*num_features).min(input.len());
260                    for feature in 0..count {
261                        let qubit = feature % num_qubits;
262                        circuit.ry(qubit, input[feature])?;
263                    }
264                }
265                QNNLayerType::VariationalLayer { num_params } => {
266                    // Hardware-efficient ansatz: sweep every qubit with an `RY`
267                    // rotation, then every qubit with an `RZ` rotation, and keep
268                    // alternating for as many parameters as the layer declares.
269                    //
270                    // The axis must be derived from the sweep index rather than
271                    // from `local` itself: with an even qubit count `local % 2` is
272                    // fully determined by the qubit parity, so every odd qubit
273                    // would receive `RZ` rotations only.  Those commute with the
274                    // Pauli-`Z` readout, leaving the odd qubits' expectation values
275                    // without a single trainable parameter of their own.
276                    for local in 0..*num_params {
277                        if param_idx >= parameters.len() {
278                            break;
279                        }
280                        let qubit = local % num_qubits;
281                        let sweep = local / num_qubits;
282                        if sweep % 2 == 0 {
283                            circuit.ry(qubit, parameters[param_idx])?;
284                        } else {
285                            circuit.rz(qubit, parameters[param_idx])?;
286                        }
287                        param_idx += 1;
288                    }
289                }
290                QNNLayerType::EntanglementLayer { connectivity } => {
291                    if num_qubits > 1 {
292                        match connectivity.as_str() {
293                            "linear" => {
294                                for q in 0..num_qubits - 1 {
295                                    circuit.cnot(q, q + 1)?;
296                                }
297                            }
298                            "circular" => {
299                                for q in 0..num_qubits {
300                                    circuit.cnot(q, (q + 1) % num_qubits)?;
301                                }
302                            }
303                            // "full" (default): all-to-all nearest entanglement
304                            _ => {
305                                for a in 0..num_qubits {
306                                    for b in (a + 1)..num_qubits {
307                                        circuit.cnot(a, b)?;
308                                    }
309                                }
310                            }
311                        }
312                    }
313                }
314                QNNLayerType::MeasurementLayer { .. } => {
315                    // Measurement is performed as an expectation-value readout in
316                    // `readout`; no gates are appended here.
317                }
318            }
319        }
320        Ok(())
321    }
322
323    /// Convert a simulated state vector into an `output_dim`-length vector of
324    /// single-qubit Pauli expectation values.
325    ///
326    /// Output `k` reads qubit `k mod num_qubits` in Pauli basis `Z`, `X`, then
327    /// `Y` (cycling as `k` grows), giving up to `3 · num_qubits` distinct
328    /// observables.  Each value lies in `[-1, 1]`.
329    fn readout(&self, amplitudes: &[Complex64]) -> Result<Array1<f64>> {
330        let num_qubits = self.num_qubits;
331        if num_qubits == 0 {
332            return Err(MLError::ModelCreationError(
333                "QNN requires at least one qubit".to_string(),
334            ));
335        }
336        let mut output = Array1::zeros(self.output_dim);
337        for k in 0..self.output_dim {
338            let qubit = k % num_qubits;
339            let pauli = match (k / num_qubits) % 3 {
340                0 => 'Z',
341                1 => 'X',
342                _ => 'Y',
343            };
344            output[k] = single_qubit_pauli_expectation(amplitudes, pauli, qubit)?;
345        }
346        Ok(output)
347    }
348
349    /// Build the parameterised circuit on an `N`-qubit register, simulate it on
350    /// the real state-vector backend, and read out the expectation-value output.
351    fn run_sized<const N: usize>(
352        &self,
353        input: &Array1<f64>,
354        parameters: &Array1<f64>,
355    ) -> Result<Array1<f64>> {
356        let mut circuit = Circuit::<N>::new();
357        self.append_layers::<N>(&mut circuit, input, parameters)?;
358        let simulator = StateVectorSimulator::new();
359        let register = simulator.run(&circuit)?;
360        self.readout(register.amplitudes())
361    }
362
363    /// Simulate the network for `input`/`parameters`, dispatching to the
364    /// smallest supported register that can hold `num_qubits`, and return the
365    /// expectation-value output.
366    fn measure_outputs(
367        &self,
368        input: &Array1<f64>,
369        parameters: &Array1<f64>,
370    ) -> Result<Array1<f64>> {
371        match self.num_qubits {
372            0 => Err(MLError::ModelCreationError(
373                "QNN requires at least one qubit".to_string(),
374            )),
375            1..=2 => self.run_sized::<2>(input, parameters),
376            3..=4 => self.run_sized::<4>(input, parameters),
377            5..=8 => self.run_sized::<8>(input, parameters),
378            9..=16 => self.run_sized::<16>(input, parameters),
379            n => Err(MLError::NotSupported(format!(
380                "QNN forward pass supports at most {MAX_FORWARD_QUBITS} qubits on the \
381                 state-vector backend, got {n}"
382            ))),
383        }
384    }
385
386    /// Runs the network on a given input, returning the measured expectation
387    /// values (one per output dimension, each in `[-1, 1]`).
388    pub fn forward(&self, input: &Array1<f64>) -> Result<Array1<f64>> {
389        self.measure_outputs(input, &self.parameters)
390    }
391
392    /// Parameter-shift gradient of a single output component with respect to
393    /// every trainable parameter, evaluated at the current parameters.
394    ///
395    /// For the Pauli-rotation gates used by [`Self::append_layers`] the exact
396    /// gradient of an expectation value is
397    /// `(⟨O⟩(θ+π/2) − ⟨O⟩(θ−π/2)) / 2`.
398    pub fn output_component_gradient(
399        &self,
400        input: &Array1<f64>,
401        output_index: usize,
402    ) -> Result<Array1<f64>> {
403        if output_index >= self.output_dim {
404            return Err(MLError::InvalidParameter(format!(
405                "output index {output_index} out of range for output dimension {}",
406                self.output_dim
407            )));
408        }
409        let num_params = self.parameters.len();
410        let mut gradient = Array1::zeros(num_params);
411        let mut params = self.parameters.clone();
412        for j in 0..num_params {
413            let original = params[j];
414            params[j] = original + FRAC_PI_2;
415            let plus = self.measure_outputs(input, &params)?[output_index];
416            params[j] = original - FRAC_PI_2;
417            let minus = self.measure_outputs(input, &params)?[output_index];
418            params[j] = original;
419            gradient[j] = (plus - minus) / 2.0;
420        }
421        Ok(gradient)
422    }
423
424    /// Mean-squared-error loss of the network over `(x, y)` using an explicit
425    /// parameter vector (used by the training routines below).
426    fn loss_with_parameters(
427        &self,
428        x: &Array2<f64>,
429        y: &Array2<f64>,
430        parameters: &Array1<f64>,
431    ) -> Result<f64> {
432        let n = x.nrows();
433        if n == 0 {
434            return Err(MLError::DataError("dataset is empty".to_string()));
435        }
436        let mut total = 0.0;
437        for i in 0..n {
438            let out = self.measure_outputs(&x.row(i).to_owned(), parameters)?;
439            let cols = out.len().min(y.ncols());
440            for k in 0..cols {
441                let diff = out[k] - y[[i, k]];
442                total += diff * diff;
443            }
444        }
445        Ok(total / n as f64)
446    }
447
448    /// Exact batch parameter-shift gradient of the MSE loss with respect to
449    /// every trainable parameter.
450    fn parameter_shift_gradient(&self, x: &Array2<f64>, y: &Array2<f64>) -> Result<Array1<f64>> {
451        let n = x.nrows();
452        let num_params = self.parameters.len();
453        let ncols = y.ncols();
454
455        // Base outputs (unshifted) used to form the MSE residuals.
456        let mut base_outputs = Vec::with_capacity(n);
457        for i in 0..n {
458            base_outputs.push(self.forward(&x.row(i).to_owned())?);
459        }
460
461        let mut gradient = Array1::zeros(num_params);
462        let mut params = self.parameters.clone();
463        for j in 0..num_params {
464            let original = params[j];
465            let mut accum = 0.0;
466            for i in 0..n {
467                let xi = x.row(i).to_owned();
468                params[j] = original + FRAC_PI_2;
469                let out_plus = self.measure_outputs(&xi, &params)?;
470                params[j] = original - FRAC_PI_2;
471                let out_minus = self.measure_outputs(&xi, &params)?;
472                let cols = base_outputs[i].len().min(ncols);
473                for k in 0..cols {
474                    let residual = base_outputs[i][k] - y[[i, k]];
475                    let d_output = (out_plus[k] - out_minus[k]) / 2.0;
476                    accum += 2.0 * residual * d_output;
477                }
478            }
479            params[j] = original;
480            gradient[j] = accum / n as f64;
481        }
482        Ok(gradient)
483    }
484
485    /// SPSA (simultaneous perturbation) stochastic estimate of the MSE-loss
486    /// gradient — used when the parameter count makes exact parameter-shift
487    /// gradients too expensive.
488    fn spsa_gradient(&self, x: &Array2<f64>, y: &Array2<f64>) -> Result<Array1<f64>> {
489        let num_params = self.parameters.len();
490        let perturbation = 0.1_f64;
491
492        let mut delta = vec![0.0_f64; num_params];
493        for value in delta.iter_mut() {
494            *value = if thread_rng().random::<f64>() < 0.5 {
495                -1.0
496            } else {
497                1.0
498            };
499        }
500
501        let mut params_plus = self.parameters.clone();
502        let mut params_minus = self.parameters.clone();
503        for j in 0..num_params {
504            params_plus[j] += perturbation * delta[j];
505            params_minus[j] -= perturbation * delta[j];
506        }
507
508        let loss_plus = self.loss_with_parameters(x, y, &params_plus)?;
509        let loss_minus = self.loss_with_parameters(x, y, &params_minus)?;
510
511        let mut gradient = Array1::zeros(num_params);
512        for j in 0..num_params {
513            gradient[j] = (loss_plus - loss_minus) / (2.0 * perturbation * delta[j]);
514        }
515        Ok(gradient)
516    }
517
518    /// Fraction of training samples the network classifies correctly.
519    ///
520    /// For single-output networks a sample counts as correct when the predicted
521    /// value is within `0.5` of the target; for multi-output networks the
522    /// `argmax` of the prediction must match the `argmax` of the target row.
523    fn training_accuracy(&self, x: &Array2<f64>, y: &Array2<f64>) -> Result<f64> {
524        let n = x.nrows();
525        if n == 0 {
526            return Ok(0.0);
527        }
528        let ncols = y.ncols();
529        let mut correct = 0usize;
530        for i in 0..n {
531            let out = self.forward(&x.row(i).to_owned())?;
532            if out.len() == 1 || ncols == 1 {
533                if (out[0] - y[[i, 0]]).abs() < 0.5 {
534                    correct += 1;
535                }
536            } else {
537                let cols = out.len().min(ncols);
538                let mut pred_idx = 0usize;
539                let mut true_idx = 0usize;
540                for k in 1..cols {
541                    if out[k] > out[pred_idx] {
542                        pred_idx = k;
543                    }
544                    if y[[i, k]] > y[[i, true_idx]] {
545                        true_idx = k;
546                    }
547                }
548                if pred_idx == true_idx {
549                    correct += 1;
550                }
551            }
552        }
553        Ok(correct as f64 / n as f64)
554    }
555
556    /// Trains the network on a dataset by gradient descent on the MSE loss.
557    ///
558    /// Gradients are computed with the exact parameter-shift rule for small
559    /// circuits and with the SPSA estimator otherwise.  Parameters are updated
560    /// in place and the real per-epoch loss trajectory is returned.
561    pub fn train(
562        &mut self,
563        x_train: &Array2<f64>,
564        y_train: &Array2<f64>,
565        epochs: usize,
566        learning_rate: f64,
567    ) -> Result<TrainingResult> {
568        let n = x_train.nrows();
569        if n == 0 {
570            return Err(MLError::DataError("training set is empty".to_string()));
571        }
572        if y_train.nrows() != n {
573            return Err(MLError::DimensionMismatch(format!(
574                "x_train has {n} rows but y_train has {}",
575                y_train.nrows()
576            )));
577        }
578
579        let use_parameter_shift = self.parameters.len() <= PARAMETER_SHIFT_MAX_PARAMS
580            && self.num_qubits <= MAX_FORWARD_QUBITS;
581
582        let mut loss_history = Vec::with_capacity(epochs);
583        for _ in 0..epochs {
584            let gradient = if use_parameter_shift {
585                self.parameter_shift_gradient(x_train, y_train)?
586            } else {
587                self.spsa_gradient(x_train, y_train)?
588            };
589            for j in 0..self.parameters.len() {
590                self.parameters[j] -= learning_rate * gradient[j];
591            }
592            loss_history.push(self.loss_with_parameters(x_train, y_train, &self.parameters)?);
593        }
594
595        let final_loss = match loss_history.last() {
596            Some(&loss) => loss,
597            None => self.loss_with_parameters(x_train, y_train, &self.parameters)?,
598        };
599        let accuracy = self.training_accuracy(x_train, y_train)?;
600
601        Ok(TrainingResult {
602            final_loss,
603            accuracy,
604            loss_history,
605            optimal_parameters: self.parameters.clone(),
606        })
607    }
608
609    /// Trains the network on a dataset with 1D labels (compatibility method)
610    pub fn train_1d(
611        &mut self,
612        x_train: &Array2<f64>,
613        y_train: &Array1<f64>,
614        epochs: usize,
615        learning_rate: f64,
616    ) -> Result<TrainingResult> {
617        // Convert 1D labels to 2D
618        let y_2d = y_train.clone().into_shape((y_train.len(), 1))?;
619        self.train(x_train, &y_2d, epochs, learning_rate)
620    }
621
622    /// Predicts the output for a given input
623    pub fn predict(&self, input: &Array1<f64>) -> Result<Array1<f64>> {
624        self.forward(input)
625    }
626
627    /// Predicts the output for a batch of inputs
628    pub fn predict_batch(&self, inputs: &Array2<f64>) -> Result<Array2<f64>> {
629        let batch_size = inputs.nrows();
630        let mut outputs = Array2::zeros((batch_size, self.output_dim));
631
632        for (i, row) in inputs.axis_iter(scirs2_core::ndarray::Axis(0)).enumerate() {
633            let input = row.to_owned();
634            let output = self.predict(&input)?;
635            outputs.row_mut(i).assign(&output);
636        }
637
638        Ok(outputs)
639    }
640}
641
642/// Builder for quantum neural networks
643///
644/// Provides a fluent API to construct a [`QuantumNeuralNetwork`] by adding
645/// encoding, variational, entanglement, and measurement layers.
646///
647/// # Examples
648///
649/// ```rust
650/// use quantrs2_ml::qnn::QNNBuilder;
651///
652/// let qnn = QNNBuilder::new()
653///     .with_qubits(2)
654///     .with_input_dim(2)
655///     .with_output_dim(1)
656///     .add_encoding_layer(2)
657///     .add_variational_layer(4)
658///     .build()
659///     .expect("valid QNN configuration");
660/// assert_eq!(qnn.num_qubits, 2);
661/// ```
662#[derive(Debug, Clone)]
663pub struct QNNBuilder {
664    layers: Vec<QNNLayerType>,
665    num_qubits: usize,
666    input_dim: usize,
667    output_dim: usize,
668}
669
670impl QNNBuilder {
671    /// Creates a new QNN builder
672    pub fn new() -> Self {
673        QNNBuilder {
674            layers: Vec::new(),
675            num_qubits: 0,
676            input_dim: 0,
677            output_dim: 0,
678        }
679    }
680
681    /// Sets the number of qubits
682    pub fn with_qubits(mut self, num_qubits: usize) -> Self {
683        self.num_qubits = num_qubits;
684        self
685    }
686
687    /// Sets the input dimension
688    pub fn with_input_dim(mut self, input_dim: usize) -> Self {
689        self.input_dim = input_dim;
690        self
691    }
692
693    /// Sets the output dimension
694    pub fn with_output_dim(mut self, output_dim: usize) -> Self {
695        self.output_dim = output_dim;
696        self
697    }
698
699    /// Adds an encoding layer
700    pub fn add_encoding_layer(mut self, num_features: usize) -> Self {
701        self.layers
702            .push(QNNLayerType::EncodingLayer { num_features });
703        self
704    }
705
706    /// Adds a layer (alias for add_encoding_layer for compatibility)
707    pub fn add_layer(self, size: usize) -> Self {
708        self.add_encoding_layer(size)
709    }
710
711    /// Adds a variational layer
712    pub fn add_variational_layer(mut self, num_params: usize) -> Self {
713        self.layers
714            .push(QNNLayerType::VariationalLayer { num_params });
715        self
716    }
717
718    /// Adds an entanglement layer
719    pub fn add_entanglement_layer(mut self, connectivity: &str) -> Self {
720        self.layers.push(QNNLayerType::EntanglementLayer {
721            connectivity: connectivity.to_string(),
722        });
723        self
724    }
725
726    /// Adds a measurement layer
727    pub fn add_measurement_layer(mut self, measurement_basis: &str) -> Self {
728        self.layers.push(QNNLayerType::MeasurementLayer {
729            measurement_basis: measurement_basis.to_string(),
730        });
731        self
732    }
733
734    /// Builds the quantum neural network
735    pub fn build(self) -> Result<QuantumNeuralNetwork> {
736        if self.num_qubits == 0 {
737            return Err(MLError::ModelCreationError(
738                "Number of qubits must be greater than 0".to_string(),
739            ));
740        }
741
742        if self.input_dim == 0 {
743            return Err(MLError::ModelCreationError(
744                "Input dimension must be greater than 0".to_string(),
745            ));
746        }
747
748        if self.output_dim == 0 {
749            return Err(MLError::ModelCreationError(
750                "Output dimension must be greater than 0".to_string(),
751            ));
752        }
753
754        if self.layers.is_empty() {
755            return Err(MLError::ModelCreationError(
756                "QNN must have at least one layer".to_string(),
757            ));
758        }
759
760        QuantumNeuralNetwork::new(
761            self.layers,
762            self.num_qubits,
763            self.input_dim,
764            self.output_dim,
765        )
766    }
767}
768
769impl fmt::Display for QNNLayerType {
770    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
771        match self {
772            QNNLayerType::EncodingLayer { num_features } => {
773                write!(f, "Encoding Layer (features: {})", num_features)
774            }
775            QNNLayerType::VariationalLayer { num_params } => {
776                write!(f, "Variational Layer (parameters: {})", num_params)
777            }
778            QNNLayerType::EntanglementLayer { connectivity } => {
779                write!(f, "Entanglement Layer (connectivity: {})", connectivity)
780            }
781            QNNLayerType::MeasurementLayer { measurement_basis } => {
782                write!(f, "Measurement Layer (basis: {})", measurement_basis)
783            }
784        }
785    }
786}
787
788/// Quantum neural network layer for use in other modules
789///
790/// A single dense-like layer in a hybrid quantum-classical network, mapping
791/// `input_dim` features to `output_dim` features through a chosen activation.
792///
793/// # Examples
794///
795/// ```rust
796/// use quantrs2_ml::qnn::{QNNLayer, ActivationType};
797///
798/// let layer = QNNLayer::new(4, 2, ActivationType::ReLU);
799/// assert_eq!(layer.input_dim, 4);
800/// assert_eq!(layer.output_dim, 2);
801/// ```
802#[derive(Debug, Clone)]
803pub struct QNNLayer {
804    /// Input dimension
805    pub input_dim: usize,
806    /// Output dimension
807    pub output_dim: usize,
808    /// Activation function
809    pub activation: ActivationType,
810}
811
812impl QNNLayer {
813    /// Create a new QNN layer
814    pub fn new(input_dim: usize, output_dim: usize, activation: ActivationType) -> Self {
815        Self {
816            input_dim,
817            output_dim,
818            activation,
819        }
820    }
821}