Skip to main content

quantrs2_ml/
gan.rs

1//! Quantum Generative Adversarial Networks (qGANs).
2//!
3//! Provides hybrid classical-quantum and fully-quantum GAN architectures.
4//! The generator and discriminator can each be classical networks, quantum
5//! circuits, or hybrid combinations, trained via adversarial min-max optimisation.
6
7use crate::error::{MLError, Result};
8use crate::qnn::QuantumNeuralNetwork;
9use quantrs2_circuit::prelude::Circuit;
10use quantrs2_sim::statevector::StateVectorSimulator;
11use scirs2_core::ndarray::{Array1, Array2};
12use scirs2_core::random::prelude::*;
13use std::fmt;
14
15/// Type of generator to use in a quantum GAN
16#[derive(Debug, Clone, Copy)]
17pub enum GeneratorType {
18    /// Pure classical generator
19    Classical,
20
21    /// Pure quantum generator
22    QuantumOnly,
23
24    /// Hybrid classical-quantum generator
25    HybridClassicalQuantum,
26}
27
28/// Type of discriminator to use in a quantum GAN
29#[derive(Debug, Clone, Copy)]
30pub enum DiscriminatorType {
31    /// Pure classical discriminator
32    Classical,
33
34    /// Pure quantum discriminator
35    QuantumOnly,
36
37    /// Hybrid with quantum feature extraction
38    HybridQuantumFeatures,
39
40    /// Hybrid with quantum decision function
41    HybridQuantumDecision,
42}
43
44/// Training metrics for a GAN
45#[derive(Debug, Clone)]
46pub struct GANTrainingHistory {
47    /// Generator loss at each epoch
48    pub gen_losses: Vec<f64>,
49
50    /// Discriminator loss at each epoch
51    pub disc_losses: Vec<f64>,
52}
53
54/// Evaluation metrics for a GAN
55#[derive(Debug, Clone)]
56pub struct GANEvaluationMetrics {
57    /// Accuracy of discriminator on real data
58    pub real_accuracy: f64,
59
60    /// Accuracy of discriminator on fake (generated) data
61    pub fake_accuracy: f64,
62
63    /// Overall discriminator accuracy
64    pub overall_accuracy: f64,
65
66    /// Jensen-Shannon divergence between real and generated distributions
67    pub js_divergence: f64,
68}
69
70/// Trait for generator models
71pub trait Generator {
72    /// Generates samples from the latent space
73    fn generate(&self, num_samples: usize) -> Result<Array2<f64>>;
74
75    /// Generates samples with specific conditions
76    fn generate_conditional(
77        &self,
78        num_samples: usize,
79        conditions: &[(usize, f64)],
80    ) -> Result<Array2<f64>>;
81
82    /// Updates the generator based on discriminator feedback
83    fn update(
84        &mut self,
85        latent_vectors: &Array2<f64>,
86        discriminator_outputs: &Array1<f64>,
87        learning_rate: f64,
88    ) -> Result<f64>;
89}
90
91/// Trait for discriminator models
92pub trait Discriminator {
93    /// Discriminates between real and generated samples
94    fn discriminate(&self, samples: &Array2<f64>) -> Result<Array1<f64>>;
95
96    /// Predicts probabilities for a batch of samples
97    fn predict_batch(&self, samples: &Array2<f64>) -> Result<Array1<f64>> {
98        self.discriminate(samples)
99    }
100
101    /// Updates the discriminator based on real and generated samples
102    fn update(
103        &mut self,
104        real_samples: &Array2<f64>,
105        generated_samples: &Array2<f64>,
106        learning_rate: f64,
107    ) -> Result<f64>;
108}
109
110/// Physics-specific GAN implementations for particle physics simulations
111pub mod physics_gan {
112    use super::*;
113
114    /// GAN model specialized for particle physics simulations
115    pub struct ParticleGAN {
116        /// The core quantum GAN implementation
117        pub gan: QuantumGAN,
118
119        /// Specialized parameters for physics simulations
120        pub physics_params: PhysicsParameters,
121    }
122
123    /// Physics-specific parameters for the GAN
124    #[derive(Debug, Clone)]
125    pub struct PhysicsParameters {
126        /// Energy scale for particle simulation
127        pub energy_scale: f64,
128
129        /// Momentum conservation factor
130        pub momentum_conservation: f64,
131
132        /// Whether to include quantum effects
133        pub quantum_effects: bool,
134    }
135
136    impl ParticleGAN {
137        /// Creates a new particle physics GAN
138        pub fn new(
139            num_qubits_gen: usize,
140            num_qubits_disc: usize,
141            latent_dim: usize,
142            data_dim: usize,
143        ) -> Result<Self> {
144            // Create a standard quantum GAN
145            let gan = QuantumGAN::new(
146                num_qubits_gen,
147                num_qubits_disc,
148                latent_dim,
149                data_dim,
150                GeneratorType::HybridClassicalQuantum,
151                DiscriminatorType::HybridQuantumFeatures,
152            )?;
153
154            // Default physics parameters
155            let physics_params = PhysicsParameters {
156                energy_scale: 100.0, // GeV
157                momentum_conservation: 0.99,
158                quantum_effects: true,
159            };
160
161            Ok(ParticleGAN {
162                gan,
163                physics_params,
164            })
165        }
166
167        /// Trains the particle GAN on real particle data
168        pub fn train(
169            &mut self,
170            particle_data: &Array2<f64>,
171            epochs: usize,
172        ) -> Result<&GANTrainingHistory> {
173            // Use the underlying GAN's training method
174            self.gan.train(
175                particle_data,
176                epochs,
177                32,   // batch size
178                0.01, // generator learning rate
179                0.01, // discriminator learning rate
180                1,    // discriminator steps
181            )
182        }
183
184        /// Generates simulated particle data
185        pub fn generate_particles(&self, num_particles: usize) -> Result<Array2<f64>> {
186            // Extends basic generation with physics constraints
187            let raw_data = self.gan.generate(num_particles)?;
188
189            // In a full implementation, we would apply physics constraints here
190            // such as momentum conservation, charge conservation, etc.
191
192            Ok(raw_data)
193        }
194    }
195}
196
197/// Quantum Generator for GAN
198#[derive(Debug, Clone)]
199pub struct QuantumGenerator {
200    /// Number of qubits
201    num_qubits: usize,
202
203    /// Dimension of latent space
204    latent_dim: usize,
205
206    /// Dimension of output data
207    data_dim: usize,
208
209    /// Type of generator
210    generator_type: GeneratorType,
211
212    /// Quantum neural network for generation
213    qnn: QuantumNeuralNetwork,
214}
215
216impl QuantumGenerator {
217    /// Creates a new quantum generator
218    pub fn new(
219        num_qubits: usize,
220        latent_dim: usize,
221        data_dim: usize,
222        generator_type: GeneratorType,
223    ) -> Result<Self> {
224        // Create a QNN architecture suitable for generation
225        let layers = vec![
226            crate::qnn::QNNLayerType::EncodingLayer {
227                num_features: latent_dim,
228            },
229            crate::qnn::QNNLayerType::VariationalLayer {
230                num_params: 2 * num_qubits,
231            },
232            crate::qnn::QNNLayerType::EntanglementLayer {
233                connectivity: "full".to_string(),
234            },
235            crate::qnn::QNNLayerType::VariationalLayer {
236                num_params: 2 * num_qubits,
237            },
238            crate::qnn::QNNLayerType::MeasurementLayer {
239                measurement_basis: "computational".to_string(),
240            },
241        ];
242
243        let qnn = QuantumNeuralNetwork::new(layers, num_qubits, latent_dim, data_dim)?;
244
245        Ok(QuantumGenerator {
246            num_qubits,
247            latent_dim,
248            data_dim,
249            generator_type,
250            qnn,
251        })
252    }
253}
254
255impl QuantumGenerator {
256    /// Generate data samples from an explicit batch of latent vectors by
257    /// evaluating the generator's quantum neural network.
258    ///
259    /// Each latent vector is encoded into the QNN circuit, simulated, and its
260    /// per-feature Pauli expectation values (in `[-1, 1]`) are affinely mapped
261    /// to the `[0, 1]` data range.
262    fn generate_from_latent(&self, latent_vectors: &Array2<f64>) -> Result<Array2<f64>> {
263        let num_samples = latent_vectors.nrows();
264        let mut samples = Array2::zeros((num_samples, self.data_dim));
265        for i in 0..num_samples {
266            let latent = latent_vectors.row(i).to_owned();
267            let output = self.qnn.forward(&latent)?;
268            for j in 0..self.data_dim {
269                let expectation = if j < output.len() { output[j] } else { 0.0 };
270                samples[[i, j]] = (expectation + 1.0) * 0.5;
271            }
272        }
273        Ok(samples)
274    }
275
276    /// Least-squares GAN adversarial loss `mean_i (D(G(z_i)) - 1)²` of the
277    /// generator against `discriminator` on the latent batch `latent_vectors`.
278    fn adversarial_loss(
279        &self,
280        latent_vectors: &Array2<f64>,
281        discriminator: &QuantumDiscriminator,
282    ) -> Result<f64> {
283        let samples = self.generate_from_latent(latent_vectors)?;
284        let outputs = discriminator.discriminate(&samples)?;
285        let n = outputs.len();
286        if n == 0 {
287            return Ok(0.0);
288        }
289        let loss = outputs.iter().map(|&d| (d - 1.0) * (d - 1.0)).sum::<f64>();
290        Ok(loss / n as f64)
291    }
292
293    /// Real adversarial update of the generator against a discriminator.
294    ///
295    /// Minimises the least-squares generator loss `mean_i (D(G(z_i)) - 1)²`
296    /// with a central finite-difference gradient (the loss is a non-linear
297    /// composition of two quantum circuits, so parameter-shift does not apply
298    /// directly), updating the generator's parameters in place.  Returns the
299    /// adversarial loss measured *before* the update.
300    pub fn adversarial_update(
301        &mut self,
302        latent_vectors: &Array2<f64>,
303        discriminator: &QuantumDiscriminator,
304        learning_rate: f64,
305    ) -> Result<f64> {
306        if latent_vectors.nrows() == 0 {
307            return Err(MLError::DataError(
308                "adversarial update received an empty latent batch".to_string(),
309            ));
310        }
311        let num_params = self.qnn.parameters.len();
312        let epsilon = 1e-3;
313        let base_loss = self.adversarial_loss(latent_vectors, discriminator)?;
314        let original = self.qnn.parameters.clone();
315
316        let mut gradient = Array1::<f64>::zeros(num_params);
317        for j in 0..num_params {
318            self.qnn.parameters[j] = original[j] + epsilon;
319            let loss_plus = self.adversarial_loss(latent_vectors, discriminator)?;
320            self.qnn.parameters[j] = original[j] - epsilon;
321            let loss_minus = self.adversarial_loss(latent_vectors, discriminator)?;
322            self.qnn.parameters[j] = original[j];
323            gradient[j] = (loss_plus - loss_minus) / (2.0 * epsilon);
324        }
325
326        for j in 0..num_params {
327            self.qnn.parameters[j] = original[j] - learning_rate * gradient[j];
328        }
329        Ok(base_loss)
330    }
331
332    /// Feature-matching loss `mean_i || G(z_i) - prototype ||²` used by the
333    /// trait-level [`Generator::update`].
334    fn feature_matching_loss(
335        &self,
336        latent_vectors: &Array2<f64>,
337        prototype: &Array1<f64>,
338    ) -> Result<f64> {
339        let samples = self.generate_from_latent(latent_vectors)?;
340        let n = samples.nrows();
341        if n == 0 {
342            return Ok(0.0);
343        }
344        let mut total = 0.0;
345        for i in 0..n {
346            for j in 0..self.data_dim {
347                let diff = samples[[i, j]] - prototype[j];
348                total += diff * diff;
349            }
350        }
351        Ok(total / n as f64)
352    }
353}
354
355impl Generator for QuantumGenerator {
356    fn generate(&self, num_samples: usize) -> Result<Array2<f64>> {
357        // Sample random latent vectors and push them through the quantum
358        // generator network.
359        let mut latent_vectors = Array2::zeros((num_samples, self.latent_dim));
360        for i in 0..num_samples {
361            for j in 0..self.latent_dim {
362                latent_vectors[[i, j]] = thread_rng().random::<f64>() * 2.0 - 1.0;
363            }
364        }
365        self.generate_from_latent(&latent_vectors)
366    }
367
368    fn generate_conditional(
369        &self,
370        num_samples: usize,
371        conditions: &[(usize, f64)],
372    ) -> Result<Array2<f64>> {
373        // Generate samples
374        let mut samples = self.generate(num_samples)?;
375
376        // Apply conditions
377        for &(feature_idx, value) in conditions {
378            if feature_idx < self.data_dim {
379                for i in 0..num_samples {
380                    samples[[i, feature_idx]] = value;
381                }
382            }
383        }
384
385        Ok(samples)
386    }
387
388    fn update(
389        &mut self,
390        latent_vectors: &Array2<f64>,
391        discriminator_outputs: &Array1<f64>,
392        learning_rate: f64,
393    ) -> Result<f64> {
394        // Feature-matching generator update.
395        //
396        // The trait signature does not expose the discriminator model, so a
397        // full adversarial gradient is not available here (use
398        // [`QuantumGenerator::adversarial_update`] / [`QuantumGAN::train`] for
399        // that).  Instead we form a realism-weighted prototype from the current
400        // batch — samples the discriminator rated as more real receive more
401        // weight — and take a real finite-difference gradient step that pulls
402        // the generator's output toward that prototype.  Returns the
403        // feature-matching loss measured before the update.
404        let n = latent_vectors.nrows();
405        if n == 0 {
406            return Err(MLError::DataError(
407                "generator update received an empty latent batch".to_string(),
408            ));
409        }
410
411        let samples = self.generate_from_latent(latent_vectors)?;
412        let weight_sum: f64 = discriminator_outputs.iter().map(|&d| d.max(0.0)).sum();
413
414        let mut prototype = Array1::zeros(self.data_dim);
415        if weight_sum > 1e-12 {
416            for i in 0..n.min(discriminator_outputs.len()) {
417                let weight = discriminator_outputs[i].max(0.0) / weight_sum;
418                for j in 0..self.data_dim {
419                    prototype[j] += weight * samples[[i, j]];
420                }
421            }
422        } else {
423            for i in 0..n {
424                for j in 0..self.data_dim {
425                    prototype[j] += samples[[i, j]] / n as f64;
426                }
427            }
428        }
429
430        let num_params = self.qnn.parameters.len();
431        let epsilon = 1e-3;
432        let base_loss = self.feature_matching_loss(latent_vectors, &prototype)?;
433        let original = self.qnn.parameters.clone();
434
435        let mut gradient = Array1::<f64>::zeros(num_params);
436        for j in 0..num_params {
437            self.qnn.parameters[j] = original[j] + epsilon;
438            let loss_plus = self.feature_matching_loss(latent_vectors, &prototype)?;
439            self.qnn.parameters[j] = original[j] - epsilon;
440            let loss_minus = self.feature_matching_loss(latent_vectors, &prototype)?;
441            self.qnn.parameters[j] = original[j];
442            gradient[j] = (loss_plus - loss_minus) / (2.0 * epsilon);
443        }
444
445        for j in 0..num_params {
446            self.qnn.parameters[j] = original[j] - learning_rate * gradient[j];
447        }
448        Ok(base_loss)
449    }
450}
451
452/// Quantum Discriminator for GAN
453#[derive(Debug, Clone)]
454pub struct QuantumDiscriminator {
455    /// Number of qubits
456    num_qubits: usize,
457
458    /// Dimension of input data
459    data_dim: usize,
460
461    /// Type of discriminator
462    discriminator_type: DiscriminatorType,
463
464    /// Quantum neural network for discrimination
465    qnn: QuantumNeuralNetwork,
466}
467
468impl QuantumDiscriminator {
469    /// Creates a new quantum discriminator
470    pub fn new(
471        num_qubits: usize,
472        data_dim: usize,
473        discriminator_type: DiscriminatorType,
474    ) -> Result<Self> {
475        // Create a QNN architecture suitable for discrimination
476        let layers = vec![
477            crate::qnn::QNNLayerType::EncodingLayer {
478                num_features: data_dim,
479            },
480            crate::qnn::QNNLayerType::VariationalLayer {
481                num_params: 2 * num_qubits,
482            },
483            crate::qnn::QNNLayerType::EntanglementLayer {
484                connectivity: "full".to_string(),
485            },
486            crate::qnn::QNNLayerType::VariationalLayer {
487                num_params: 2 * num_qubits,
488            },
489            crate::qnn::QNNLayerType::MeasurementLayer {
490                measurement_basis: "computational".to_string(),
491            },
492        ];
493
494        let qnn = QuantumNeuralNetwork::new(
495            layers, num_qubits, data_dim, 1, // Binary output (real or fake)
496        )?;
497
498        Ok(QuantumDiscriminator {
499            num_qubits,
500            data_dim,
501            discriminator_type,
502            qnn,
503        })
504    }
505}
506
507impl QuantumDiscriminator {
508    /// Discriminate a single sample, returning the probability (in `[0, 1]`)
509    /// that it is real.
510    ///
511    /// The sample is encoded into the discriminator's quantum neural network;
512    /// its single Pauli-Z expectation output (in `[-1, 1]`) is affinely mapped
513    /// to a probability.
514    fn discriminate_one(&self, sample: &Array1<f64>) -> Result<f64> {
515        let output = self.qnn.forward(sample)?;
516        if output.is_empty() {
517            return Err(MLError::MLOperationError(
518                "discriminator QNN produced an empty output".to_string(),
519            ));
520        }
521        Ok((output[0] + 1.0) * 0.5)
522    }
523
524    /// Least-squares discrimination loss
525    /// `mean_real (D(x) - 1)² + mean_fake (D(x) - 0)²`.
526    fn discrimination_loss(
527        &self,
528        real_samples: &Array2<f64>,
529        generated_samples: &Array2<f64>,
530    ) -> Result<f64> {
531        let n_real = real_samples.nrows();
532        let n_fake = generated_samples.nrows();
533
534        let mut real_loss = 0.0;
535        for i in 0..n_real {
536            let d = self.discriminate_one(&real_samples.row(i).to_owned())?;
537            real_loss += (d - 1.0) * (d - 1.0);
538        }
539        let mut fake_loss = 0.0;
540        for i in 0..n_fake {
541            let d = self.discriminate_one(&generated_samples.row(i).to_owned())?;
542            fake_loss += d * d;
543        }
544
545        let mut loss = 0.0;
546        if n_real > 0 {
547            loss += real_loss / n_real as f64;
548        }
549        if n_fake > 0 {
550            loss += fake_loss / n_fake as f64;
551        }
552        Ok(loss)
553    }
554}
555
556impl Discriminator for QuantumDiscriminator {
557    fn discriminate(&self, samples: &Array2<f64>) -> Result<Array1<f64>> {
558        let num_samples = samples.nrows();
559        let mut outputs = Array1::zeros(num_samples);
560        for i in 0..num_samples {
561            outputs[i] = self.discriminate_one(&samples.row(i).to_owned())?;
562        }
563        Ok(outputs)
564    }
565
566    fn update(
567        &mut self,
568        real_samples: &Array2<f64>,
569        generated_samples: &Array2<f64>,
570        learning_rate: f64,
571    ) -> Result<f64> {
572        // Least-squares GAN discriminator update via parameter-shift gradients.
573        //
574        // D(x) = (⟨Z⟩(x) + 1) / 2, so ∂D/∂θ = ½ · ∂⟨Z⟩/∂θ where ∂⟨Z⟩/∂θ is the
575        // exact parameter-shift gradient.  The least-squares loss gradient for a
576        // real sample (target 1) is (D - 1)·∂⟨Z⟩/∂θ and for a fake sample
577        // (target 0) is D·∂⟨Z⟩/∂θ, averaged within each class.
578        let n_real = real_samples.nrows();
579        let n_fake = generated_samples.nrows();
580        let num_params = self.qnn.parameters.len();
581        let mut gradient = Array1::<f64>::zeros(num_params);
582
583        for i in 0..n_real {
584            let x = real_samples.row(i).to_owned();
585            let d = self.discriminate_one(&x)?;
586            let d_expectation = self.qnn.output_component_gradient(&x, 0)?;
587            let coeff = (d - 1.0) / n_real as f64;
588            for j in 0..num_params {
589                gradient[j] += coeff * d_expectation[j];
590            }
591        }
592        for i in 0..n_fake {
593            let x = generated_samples.row(i).to_owned();
594            let d = self.discriminate_one(&x)?;
595            let d_expectation = self.qnn.output_component_gradient(&x, 0)?;
596            let coeff = d / n_fake as f64;
597            for j in 0..num_params {
598                gradient[j] += coeff * d_expectation[j];
599            }
600        }
601
602        for j in 0..num_params {
603            self.qnn.parameters[j] -= learning_rate * gradient[j];
604        }
605
606        // Report the loss after the update so the training history reflects the
607        // discriminator's real progress.
608        self.discrimination_loss(real_samples, generated_samples)
609    }
610}
611
612/// Quantum Generative Adversarial Network
613#[derive(Debug, Clone)]
614pub struct QuantumGAN {
615    /// Generator model
616    pub generator: QuantumGenerator,
617
618    /// Discriminator model
619    pub discriminator: QuantumDiscriminator,
620
621    /// Training history
622    pub training_history: GANTrainingHistory,
623}
624
625impl QuantumGAN {
626    /// Creates a new quantum GAN
627    pub fn new(
628        num_qubits_gen: usize,
629        num_qubits_disc: usize,
630        latent_dim: usize,
631        data_dim: usize,
632        generator_type: GeneratorType,
633        discriminator_type: DiscriminatorType,
634    ) -> Result<Self> {
635        let generator =
636            QuantumGenerator::new(num_qubits_gen, latent_dim, data_dim, generator_type)?;
637
638        let discriminator =
639            QuantumDiscriminator::new(num_qubits_disc, data_dim, discriminator_type)?;
640
641        let training_history = GANTrainingHistory {
642            gen_losses: Vec::new(),
643            disc_losses: Vec::new(),
644        };
645
646        Ok(QuantumGAN {
647            generator,
648            discriminator,
649            training_history,
650        })
651    }
652
653    /// Trains the GAN on a dataset
654    pub fn train(
655        &mut self,
656        real_data: &Array2<f64>,
657        epochs: usize,
658        batch_size: usize,
659        gen_learning_rate: f64,
660        disc_learning_rate: f64,
661        disc_steps: usize,
662    ) -> Result<&GANTrainingHistory> {
663        let mut gen_losses = Vec::with_capacity(epochs);
664        let mut disc_losses = Vec::with_capacity(epochs);
665
666        for _epoch in 0..epochs {
667            // Train discriminator for several steps
668            let mut disc_loss_sum = 0.0;
669            for _step in 0..disc_steps {
670                // Generate fake samples
671                let fake_samples = self.generator.generate(batch_size)?;
672
673                // Sample real data (random batch)
674                let real_batch = sample_batch(real_data, batch_size)?;
675
676                // Update discriminator
677                let disc_loss =
678                    self.discriminator
679                        .update(&real_batch, &fake_samples, disc_learning_rate)?;
680                disc_loss_sum += disc_loss;
681            }
682            let avg_disc_loss = disc_loss_sum / disc_steps as f64;
683
684            // Train generator against the current discriminator with real
685            // random latent vectors and a genuine adversarial gradient.
686            let latent_dim = self.generator.latent_dim;
687            let mut latent_vectors = Array2::zeros((batch_size, latent_dim));
688            for i in 0..batch_size {
689                for j in 0..latent_dim {
690                    latent_vectors[[i, j]] = thread_rng().random::<f64>() * 2.0 - 1.0;
691                }
692            }
693            let gen_loss = self.generator.adversarial_update(
694                &latent_vectors,
695                &self.discriminator,
696                gen_learning_rate,
697            )?;
698
699            // Record losses
700            gen_losses.push(gen_loss);
701            disc_losses.push(avg_disc_loss);
702        }
703
704        self.training_history = GANTrainingHistory {
705            gen_losses,
706            disc_losses,
707        };
708
709        Ok(&self.training_history)
710    }
711
712    /// Generates samples from the trained generator
713    pub fn generate(&self, num_samples: usize) -> Result<Array2<f64>> {
714        self.generator.generate(num_samples)
715    }
716
717    /// Generates samples with specific conditions
718    pub fn generate_conditional(
719        &self,
720        num_samples: usize,
721        conditions: &[(usize, f64)],
722    ) -> Result<Array2<f64>> {
723        self.generator.generate_conditional(num_samples, conditions)
724    }
725
726    /// Evaluates the GAN model
727    pub fn evaluate(
728        &self,
729        real_data: &Array2<f64>,
730        num_samples: usize,
731    ) -> Result<GANEvaluationMetrics> {
732        // Generate fake samples
733        let fake_samples = self.generate(num_samples)?;
734
735        // Evaluate discriminator on real data
736        let real_preds = self.discriminator.predict_batch(real_data)?;
737        let real_correct = real_preds.iter().filter(|&&p| p > 0.5).count();
738        let real_accuracy = real_correct as f64 / real_preds.len() as f64;
739
740        // Evaluate discriminator on fake data
741        let fake_preds = self.discriminator.predict_batch(&fake_samples)?;
742        let fake_correct = fake_preds.iter().filter(|&&p| p < 0.5).count();
743        let fake_accuracy = fake_correct as f64 / fake_preds.len() as f64;
744
745        // Overall accuracy
746        let overall_correct = real_correct + fake_correct;
747        let overall_total = real_preds.len() + fake_preds.len();
748        let overall_accuracy = overall_correct as f64 / overall_total as f64;
749
750        // Calculate Jensen-Shannon divergence between real and fake data distributions
751        // This is a simplified placeholder calculation
752        let js_divergence = calculate_js_divergence(real_data, &fake_samples)?;
753
754        Ok(GANEvaluationMetrics {
755            real_accuracy,
756            fake_accuracy,
757            overall_accuracy,
758            js_divergence,
759        })
760    }
761}
762
763/// Calculate Jensen-Shannon divergence between two datasets using histogram estimation.
764///
765/// For each column (feature dimension), estimates the probability distributions
766/// with a fixed-bin histogram, then computes JS = 0.5 * KL(p||m) + 0.5 * KL(q||m)
767/// where m = (p + q) / 2.  Results are averaged across columns.
768fn calculate_js_divergence(data1: &Array2<f64>, data2: &Array2<f64>) -> Result<f64> {
769    if data1.ncols() == 0 || data1.nrows() == 0 || data2.nrows() == 0 {
770        return Ok(0.0);
771    }
772
773    let n_bins: usize = 20;
774    let n_cols = data1.ncols().min(data2.ncols());
775    let mut total_js = 0.0;
776
777    for col in 0..n_cols {
778        let col1: Vec<f64> = data1.column(col).to_vec();
779        let col2: Vec<f64> = data2.column(col).to_vec();
780
781        let min_val = col1
782            .iter()
783            .chain(col2.iter())
784            .cloned()
785            .fold(f64::INFINITY, f64::min);
786        let max_val = col1
787            .iter()
788            .chain(col2.iter())
789            .cloned()
790            .fold(f64::NEG_INFINITY, f64::max);
791
792        if (max_val - min_val).abs() < 1e-14 {
793            // All values identical across both datasets → JS divergence is 0
794            continue;
795        }
796
797        let bin_width = (max_val - min_val) / n_bins as f64;
798        let mut hist1 = vec![0.0f64; n_bins];
799        let mut hist2 = vec![0.0f64; n_bins];
800
801        for &v in &col1 {
802            let bin = ((v - min_val) / bin_width) as usize;
803            let bin = bin.min(n_bins - 1);
804            hist1[bin] += 1.0;
805        }
806        for &v in &col2 {
807            let bin = ((v - min_val) / bin_width) as usize;
808            let bin = bin.min(n_bins - 1);
809            hist2[bin] += 1.0;
810        }
811
812        let n1 = col1.len() as f64;
813        let n2 = col2.len() as f64;
814        for i in 0..n_bins {
815            hist1[i] /= n1;
816            hist2[i] /= n2;
817        }
818
819        // JS = 0.5 * KL(p || m) + 0.5 * KL(q || m),  m = (p + q) / 2
820        let mut js = 0.0f64;
821        for i in 0..n_bins {
822            let p = hist1[i];
823            let q = hist2[i];
824            let m = (p + q) * 0.5;
825            if m > 1e-14 {
826                if p > 1e-14 {
827                    js += 0.5 * p * (p / m).ln();
828                }
829                if q > 1e-14 {
830                    js += 0.5 * q * (q / m).ln();
831                }
832            }
833        }
834        total_js += js;
835    }
836
837    Ok(if n_cols > 0 {
838        total_js / n_cols as f64
839    } else {
840        0.0
841    })
842}
843
844// Helper function to sample a random batch from a dataset
845fn sample_batch(data: &Array2<f64>, batch_size: usize) -> Result<Array2<f64>> {
846    let num_samples = data.nrows();
847    let mut batch = Array2::zeros((batch_size.min(num_samples), data.ncols()));
848
849    for i in 0..batch_size.min(num_samples) {
850        let idx = fastrand::usize(0..num_samples);
851        batch.row_mut(i).assign(&data.row(idx));
852    }
853
854    Ok(batch)
855}
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860    use scirs2_core::ndarray::Array2;
861
862    #[test]
863    fn test_js_divergence_identical() {
864        let data = Array2::from_shape_vec((4, 2), vec![0.0, 1.0, 0.5, 0.5, 0.2, 0.8, 0.7, 0.3])
865            .expect("array creation failed");
866        let js = calculate_js_divergence(&data, &data).expect("divergence failed");
867        assert!(js < 0.01, "JS(p,p) should be ≈0, got {js}");
868    }
869
870    #[test]
871    fn test_js_divergence_bounded() {
872        let data1 =
873            Array2::from_shape_vec((4, 1), vec![0.0, 0.0, 0.0, 0.0]).expect("array creation");
874        let data2 =
875            Array2::from_shape_vec((4, 1), vec![1.0, 1.0, 1.0, 1.0]).expect("array creation");
876        let js = calculate_js_divergence(&data1, &data2).expect("divergence failed");
877        assert!(js >= 0.0 && js <= 1.0, "JS should be in [0, 1], got {js}");
878    }
879}
880
881impl fmt::Display for GeneratorType {
882    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
883        match self {
884            GeneratorType::Classical => write!(f, "Classical"),
885            GeneratorType::QuantumOnly => write!(f, "Quantum Only"),
886            GeneratorType::HybridClassicalQuantum => write!(f, "Hybrid Classical-Quantum"),
887        }
888    }
889}
890
891impl fmt::Display for DiscriminatorType {
892    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
893        match self {
894            DiscriminatorType::Classical => write!(f, "Classical"),
895            DiscriminatorType::QuantumOnly => write!(f, "Quantum Only"),
896            DiscriminatorType::HybridQuantumFeatures => write!(f, "Hybrid with Quantum Features"),
897            DiscriminatorType::HybridQuantumDecision => write!(f, "Hybrid with Quantum Decision"),
898        }
899    }
900}