1use 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
18const MAX_FORWARD_QUBITS: usize = 16;
23
24const PARAMETER_SHIFT_MAX_PARAMS: usize = 64;
29
30fn 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 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 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#[derive(Debug, Clone, Copy, PartialEq)]
99pub enum ActivationType {
100 Linear,
102 ReLU,
104 Sigmoid,
106 Tanh,
108}
109
110#[derive(Debug, Clone)]
112pub enum QNNLayerType {
113 EncodingLayer {
115 num_features: usize,
117 },
118
119 VariationalLayer {
121 num_params: usize,
123 },
124
125 EntanglementLayer {
127 connectivity: String,
129 },
130
131 MeasurementLayer {
133 measurement_basis: String,
135 },
136}
137
138#[derive(Debug, Clone)]
140pub struct TrainingResult {
141 pub final_loss: f64,
143
144 pub accuracy: f64,
146
147 pub loss_history: Vec<f64>,
149
150 pub optimal_parameters: Array1<f64>,
152}
153
154#[derive(Debug, Clone)]
174pub struct QuantumNeuralNetwork {
175 pub layers: Vec<QNNLayerType>,
177
178 pub num_qubits: usize,
180
181 pub input_dim: usize,
183
184 pub output_dim: usize,
186
187 pub parameters: Array1<f64>,
189}
190
191impl QuantumNeuralNetwork {
192 pub fn new(
194 layers: Vec<QNNLayerType>,
195 num_qubits: usize,
196 input_dim: usize,
197 output_dim: usize,
198 ) -> Result<Self> {
199 if layers.is_empty() {
201 return Err(MLError::ModelCreationError(
202 "QNN must have at least one layer".to_string(),
203 ));
204 }
205
206 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 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 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 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 _ => {
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 }
318 }
319 }
320 Ok(())
321 }
322
323 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 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 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 pub fn forward(&self, input: &Array1<f64>) -> Result<Array1<f64>> {
389 self.measure_outputs(input, &self.parameters)
390 }
391
392 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, ¶ms)?[output_index];
416 params[j] = original - FRAC_PI_2;
417 let minus = self.measure_outputs(input, ¶ms)?[output_index];
418 params[j] = original;
419 gradient[j] = (plus - minus) / 2.0;
420 }
421 Ok(gradient)
422 }
423
424 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 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 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, ¶ms)?;
470 params[j] = original - FRAC_PI_2;
471 let out_minus = self.measure_outputs(&xi, ¶ms)?;
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 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, ¶ms_plus)?;
509 let loss_minus = self.loss_with_parameters(x, y, ¶ms_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 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 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 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 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 pub fn predict(&self, input: &Array1<f64>) -> Result<Array1<f64>> {
624 self.forward(input)
625 }
626
627 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#[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 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 pub fn with_qubits(mut self, num_qubits: usize) -> Self {
683 self.num_qubits = num_qubits;
684 self
685 }
686
687 pub fn with_input_dim(mut self, input_dim: usize) -> Self {
689 self.input_dim = input_dim;
690 self
691 }
692
693 pub fn with_output_dim(mut self, output_dim: usize) -> Self {
695 self.output_dim = output_dim;
696 self
697 }
698
699 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 pub fn add_layer(self, size: usize) -> Self {
708 self.add_encoding_layer(size)
709 }
710
711 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 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 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 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#[derive(Debug, Clone)]
803pub struct QNNLayer {
804 pub input_dim: usize,
806 pub output_dim: usize,
808 pub activation: ActivationType,
810}
811
812impl QNNLayer {
813 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}