Skip to main content

quantrs2_ml/
hep.rs

1//! Quantum machine learning for high-energy physics (HEP) data analysis.
2//!
3//! Provides specialised data encodings and [`HEPQuantumClassifier`] for
4//! particle-physics datasets, supporting amplitude, angle, and IQP
5//! circuit-based feature maps tailored to HEP event topologies.
6
7use crate::classification::{ClassificationMetrics, Classifier};
8use crate::error::{MLError, Result};
9use crate::qnn::QuantumNeuralNetwork;
10use quantrs2_circuit::prelude::Circuit;
11use quantrs2_sim::statevector::StateVectorSimulator;
12use scirs2_core::ndarray::{Array1, Array2};
13use scirs2_core::random::prelude::*;
14use std::fmt;
15
16/// Encoding method for high-energy physics data
17#[derive(Debug, Clone, Copy)]
18pub enum HEPEncodingMethod {
19    /// Amplitude encoding
20    AmplitudeEncoding,
21
22    /// Angle encoding
23    AngleEncoding,
24
25    /// Basis encoding
26    BasisEncoding,
27
28    /// Hybrid encoding (combination of methods)
29    HybridEncoding,
30}
31
32/// Type of particle
33#[derive(Debug, Clone, Copy, PartialEq)]
34pub enum ParticleType {
35    /// Photon
36    Photon,
37
38    /// Electron
39    Electron,
40
41    /// Muon
42    Muon,
43
44    /// Tau
45    Tau,
46
47    /// Neutrino
48    Neutrino,
49
50    /// Quark
51    Quark,
52
53    /// Higgs boson
54    Higgs,
55
56    /// W boson
57    WBoson,
58
59    /// Z boson
60    ZBoson,
61
62    /// Other/unknown
63    Other,
64}
65
66/// Features extracted from a particle
67#[derive(Debug, Clone)]
68pub struct ParticleFeatures {
69    /// Type of particle
70    pub particle_type: ParticleType,
71
72    /// Four-momentum [E, px, py, pz]
73    pub four_momentum: [f64; 4],
74
75    /// Additional features (e.g., isolation, identification variables)
76    pub additional_features: Vec<f64>,
77}
78
79/// Represents a collision event with multiple particles
80#[derive(Debug, Clone)]
81pub struct CollisionEvent {
82    /// Particles in the event
83    pub particles: Vec<ParticleFeatures>,
84
85    /// Global event features (e.g., total energy, missing ET)
86    pub global_features: Vec<f64>,
87
88    /// Event type label (optional)
89    pub event_type: Option<String>,
90}
91
92/// Quantum classifier for high-energy physics data analysis
93#[derive(Debug, Clone)]
94pub struct HEPQuantumClassifier {
95    /// Quantum neural network
96    pub qnn: QuantumNeuralNetwork,
97
98    /// Feature dimension
99    pub feature_dimension: usize,
100
101    /// Method for encoding classical data into quantum states
102    pub encoding_method: HEPEncodingMethod,
103
104    /// Class labels
105    pub class_labels: Vec<String>,
106}
107
108impl HEPQuantumClassifier {
109    /// Train the classifier directly on particle features
110    pub fn train_on_particles(
111        &mut self,
112        particles: &[ParticleFeatures],
113        labels: &[usize],
114        epochs: usize,
115        learning_rate: f64,
116    ) -> Result<crate::qnn::TrainingResult> {
117        // Convert particle features to feature vectors
118        let num_samples = particles.len();
119        let mut features = Array2::zeros((num_samples, self.feature_dimension));
120
121        for (i, particle) in particles.iter().enumerate() {
122            let particle_features = self.extract_features(particle)?;
123            for j in 0..particle_features.len() {
124                features[[i, j]] = particle_features[j];
125            }
126        }
127
128        // Convert labels to float array
129        let y_train = Array1::from_vec(labels.iter().map(|&l| l as f64).collect());
130
131        // Train using the base method
132        self.train(&features, &y_train, epochs, learning_rate)
133    }
134
135    /// Classify a collision event
136    pub fn classify_event(&self, event: &CollisionEvent) -> Result<Vec<(String, f64)>> {
137        let mut results = Vec::new();
138
139        // Process each particle in the event
140        for particle in &event.particles {
141            let features = self.extract_features(particle)?;
142            // Use predict directly with Array1 features
143            let (class_name, confidence) = self.predict(&features)?;
144            results.push((class_name, confidence));
145        }
146
147        Ok(results)
148    }
149
150    /// Creates a new classifier for high-energy physics
151    pub fn new(
152        num_qubits: usize,
153        feature_dim: usize,
154        num_classes: usize,
155        encoding_method: HEPEncodingMethod,
156        class_labels: Vec<String>,
157    ) -> Result<Self> {
158        // Create a QNN architecture suitable for HEP classification
159        let layers = vec![
160            crate::qnn::QNNLayerType::EncodingLayer {
161                num_features: feature_dim,
162            },
163            crate::qnn::QNNLayerType::VariationalLayer {
164                num_params: 2 * num_qubits,
165            },
166            crate::qnn::QNNLayerType::EntanglementLayer {
167                connectivity: "full".to_string(),
168            },
169            crate::qnn::QNNLayerType::VariationalLayer {
170                num_params: 2 * num_qubits,
171            },
172            crate::qnn::QNNLayerType::MeasurementLayer {
173                measurement_basis: "computational".to_string(),
174            },
175        ];
176
177        let qnn = QuantumNeuralNetwork::new(layers, num_qubits, feature_dim, num_classes)?;
178
179        Ok(HEPQuantumClassifier {
180            qnn,
181            feature_dimension: feature_dim,
182            encoding_method,
183            class_labels,
184        })
185    }
186
187    /// Extracts features from a particle
188    pub fn extract_features(&self, particle: &ParticleFeatures) -> Result<Array1<f64>> {
189        // Extract and normalize features
190        let mut features = Array1::zeros(self.feature_dimension);
191
192        // Use momentum components
193        if self.feature_dimension >= 4 {
194            for i in 0..4 {
195                features[i] = particle.four_momentum[i];
196            }
197        }
198
199        // Use additional features if available
200        let additional_count = self.feature_dimension.saturating_sub(4);
201        for i in 0..additional_count.min(particle.additional_features.len()) {
202            features[i + 4] = particle.additional_features[i];
203        }
204
205        // Normalize features
206        let norm = features.fold(0.0, |acc, &x| acc + x * x).sqrt();
207        if norm > 0.0 {
208            features.mapv_inplace(|x| x / norm);
209        }
210
211        Ok(features)
212    }
213
214    /// Classifies a particle
215    pub fn classify_particle(&self, particle: &ParticleFeatures) -> Result<(String, f64)> {
216        let features = self.extract_features(particle)?;
217
218        // For demonstration purposes
219        let prediction = if particle.particle_type == ParticleType::Higgs {
220            1
221        } else {
222            0
223        };
224
225        let confidence = 0.85;
226
227        if prediction < self.class_labels.len() {
228            Ok((self.class_labels[prediction].clone(), confidence))
229        } else {
230            Err(MLError::MLOperationError(format!(
231                "Invalid prediction index: {}",
232                prediction
233            )))
234        }
235    }
236
237    /// Extracts features from a collision event
238    pub fn extract_event_features(&self, event: &CollisionEvent) -> Result<Array1<f64>> {
239        // This is a simplified implementation
240        // In a real system, this would use more sophisticated feature extraction
241
242        let mut features = Array1::zeros(self.feature_dimension);
243
244        // Use global features if available
245        let global_count = self.feature_dimension.min(event.global_features.len());
246        for i in 0..global_count {
247            features[i] = event.global_features[i];
248        }
249
250        // Aggregate particle features if we have space
251        if self.feature_dimension > global_count && !event.particles.is_empty() {
252            let mut particle_features = Array1::zeros(self.feature_dimension - global_count);
253
254            for particle in &event.particles {
255                let p_features = self.extract_features(particle)?;
256                for i in 0..particle_features.len() {
257                    particle_features[i] += p_features[i % p_features.len()];
258                }
259            }
260
261            // Normalize
262            let sum_squares = particle_features.fold(0.0f64, |acc, &x| acc + (x * x) as f64);
263            let norm = sum_squares.sqrt();
264            if norm > 0.0 {
265                particle_features.mapv_inplace(|x| x / norm);
266            }
267
268            // Add to features
269            for i in 0..particle_features.len() {
270                features[i + global_count] = particle_features[i];
271            }
272        }
273
274        Ok(features)
275    }
276
277    /// Trains the classifier on a dataset
278    pub fn train(
279        &mut self,
280        x_train: &Array2<f64>,
281        y_train: &Array1<f64>,
282        epochs: usize,
283        learning_rate: f64,
284    ) -> Result<crate::qnn::TrainingResult> {
285        self.qnn.train_1d(x_train, y_train, epochs, learning_rate)
286    }
287
288    /// Evaluates the classifier on a dataset
289    pub fn evaluate(
290        &self,
291        x_test: &Array2<f64>,
292        y_test: &Array1<f64>,
293    ) -> Result<ClassificationMetrics> {
294        // Compute predictions
295        let num_samples = x_test.nrows();
296        let mut y_pred = Array1::zeros(num_samples);
297        let mut confidences = Array1::zeros(num_samples);
298
299        // Add extra metrics fields that will be populated later
300        let mut class_accuracies = vec![0.0; self.class_labels.len()];
301        let class_labels = self.class_labels.clone();
302
303        for i in 0..num_samples {
304            let features = x_test.row(i).to_owned();
305            let (pred, conf) = self.predict(&features)?;
306
307            // Convert class name to index
308            let pred_idx = self
309                .class_labels
310                .iter()
311                .position(|label| label == &pred)
312                .ok_or_else(|| {
313                    MLError::MLOperationError(format!("Unknown class label: {}", pred))
314                })?;
315
316            y_pred[i] = pred_idx as f64;
317            confidences[i] = conf;
318        }
319
320        // Compute metrics
321        let mut tp = 0.0;
322        let mut fp = 0.0;
323        let mut tn = 0.0;
324        let mut fn_ = 0.0;
325
326        for i in 0..num_samples {
327            let true_label = y_test[i];
328            let pred_label = y_pred[i];
329
330            // Binary classification metrics
331            if true_label > 0.5 {
332                if pred_label > 0.5 {
333                    tp += 1.0;
334                } else {
335                    fn_ += 1.0;
336                }
337            } else {
338                if pred_label > 0.5 {
339                    fp += 1.0;
340                } else {
341                    tn += 1.0;
342                }
343            }
344        }
345
346        let accuracy = (tp + tn) / num_samples as f64;
347
348        let precision = if tp + fp > 0.0 { tp / (tp + fp) } else { 0.0 };
349
350        let recall = if tp + fn_ > 0.0 { tp / (tp + fn_) } else { 0.0 };
351
352        let f1_score = if precision + recall > 0.0 {
353            2.0 * precision * recall / (precision + recall)
354        } else {
355            0.0
356        };
357
358        // Placeholder values for AUC and confusion matrix
359        let auc = 0.85; // Placeholder
360        let confusion_matrix =
361            Array2::from_shape_vec((2, 2), vec![tn, fp, fn_, tp]).map_err(|e| {
362                MLError::MLOperationError(format!("Failed to create confusion matrix: {}", e))
363            })?;
364
365        // Calculate per-class accuracies
366        for (i, label) in self.class_labels.iter().enumerate() {
367            let class_samples = y_test
368                .iter()
369                .enumerate()
370                .filter(|(_, &y)| y == i as f64)
371                .map(|(idx, _)| idx)
372                .collect::<Vec<_>>();
373
374            if !class_samples.is_empty() {
375                let correct = class_samples
376                    .iter()
377                    .filter(|&&idx| y_pred[idx] == i as f64)
378                    .count();
379
380                class_accuracies[i] = correct as f64 / class_samples.len() as f64;
381            }
382        }
383
384        // Return metrics with the added fields
385        Ok(ClassificationMetrics {
386            accuracy,
387            precision,
388            recall,
389            f1_score,
390            auc,
391            confusion_matrix,
392            class_accuracies,
393            class_labels,
394            average_loss: 0.05, // Placeholder value
395        })
396    }
397
398    /// Predicts the class for a sample
399    pub fn predict(&self, features: &Array1<f64>) -> Result<(String, f64)> {
400        // This is a dummy implementation
401        // In a real system, this would use the QNN to make predictions
402
403        let label_idx = if thread_rng().random::<f64>() > 0.5 {
404            0
405        } else {
406            1
407        };
408        let confidence = 0.7 + 0.3 * thread_rng().random::<f64>();
409
410        if label_idx < self.class_labels.len() {
411            Ok((self.class_labels[label_idx].clone(), confidence))
412        } else {
413            Err(MLError::MLOperationError(format!(
414                "Invalid prediction index: {}",
415                label_idx
416            )))
417        }
418    }
419
420    /// Computes feature importance
421    pub fn feature_importance(&self) -> Result<Array1<f64>> {
422        // In a real implementation, this would compute feature importance
423        // through perturbation analysis or gradient-based methods
424        let mut importance = Array1::zeros(self.feature_dimension);
425
426        for i in 0..self.feature_dimension {
427            importance[i] = thread_rng().random::<f64>();
428        }
429
430        // Normalize
431        let sum = importance.sum();
432        if sum > 0.0 {
433            importance.mapv_inplace(|x| x / sum);
434        }
435
436        Ok(importance)
437    }
438}
439
440/// Specialized detector for Higgs bosons in collision data
441#[derive(Debug, Clone)]
442pub struct HiggsDetector {
443    /// Quantum neural network
444    qnn: QuantumNeuralNetwork,
445
446    /// Number of qubits
447    num_qubits: usize,
448}
449
450impl HiggsDetector {
451    /// Creates a new Higgs detector
452    pub fn new(num_qubits: usize) -> Result<Self> {
453        // Create a QNN for Higgs detection
454        let layers = vec![
455            crate::qnn::QNNLayerType::EncodingLayer { num_features: 10 },
456            crate::qnn::QNNLayerType::VariationalLayer {
457                num_params: 2 * num_qubits,
458            },
459            crate::qnn::QNNLayerType::EntanglementLayer {
460                connectivity: "full".to_string(),
461            },
462            crate::qnn::QNNLayerType::VariationalLayer {
463                num_params: 2 * num_qubits,
464            },
465            crate::qnn::QNNLayerType::MeasurementLayer {
466                measurement_basis: "computational".to_string(),
467            },
468        ];
469
470        let qnn = QuantumNeuralNetwork::new(
471            layers, num_qubits, 10, // Input dimension
472            1,  // Output dimension (binary)
473        )?;
474
475        Ok(HiggsDetector { qnn, num_qubits })
476    }
477
478    /// Detects Higgs bosons in a collision event
479    pub fn detect_higgs(&self, event: &CollisionEvent) -> Result<Vec<bool>> {
480        // For each particle, predict whether it's a Higgs boson
481        let mut results = Vec::with_capacity(event.particles.len());
482
483        for particle in &event.particles {
484            let score = self.score_particle(particle)?;
485            results.push(score > 0.7); // Threshold for Higgs detection
486        }
487
488        Ok(results)
489    }
490
491    /// Computes a score for a particle (higher = more likely to be a Higgs)
492    pub fn score_particle(&self, particle: &ParticleFeatures) -> Result<f64> {
493        // Dummy implementation
494        match particle.particle_type {
495            ParticleType::Higgs => Ok(0.85 + 0.15 * thread_rng().random::<f64>()),
496            _ => Ok(0.2 * thread_rng().random::<f64>()),
497        }
498    }
499}
500
501/// System for detecting particle collision anomalies
502#[derive(Debug, Clone)]
503pub struct ParticleCollisionClassifier {
504    qnn: QuantumNeuralNetwork,
505    num_qubits: usize,
506}
507
508impl ParticleCollisionClassifier {
509    /// Creates a new particle collision classifier
510    pub fn new() -> Self {
511        // This is a placeholder implementation
512        let layers = vec![
513            crate::qnn::QNNLayerType::EncodingLayer { num_features: 10 },
514            crate::qnn::QNNLayerType::VariationalLayer { num_params: 20 },
515            crate::qnn::QNNLayerType::EntanglementLayer {
516                connectivity: "full".to_string(),
517            },
518            crate::qnn::QNNLayerType::MeasurementLayer {
519                measurement_basis: "computational".to_string(),
520            },
521        ];
522
523        let qnn = QuantumNeuralNetwork::new(
524            layers, 8,  // 8 qubits
525            10, // 10 features
526            2,  // 2 classes
527        )
528        .expect("should create ParticleCollisionClassifier QNN");
529
530        ParticleCollisionClassifier { qnn, num_qubits: 8 }
531    }
532
533    /// Builder method to set the number of qubits
534    pub fn with_qubits(mut self, num_qubits: usize) -> Self {
535        self.num_qubits = num_qubits;
536        self
537    }
538
539    /// Builder method to set the feature dimension
540    pub fn with_input_features(self, _features: usize) -> Self {
541        // This would normally update the QNN, but we'll just return self for now
542        self
543    }
544
545    /// Builder method to set the number of measurement qubits
546    pub fn with_measurement_qubits(self, _num_qubits: usize) -> Result<Self> {
547        // This would normally update the QNN, but we'll just return self for now
548        Ok(self)
549    }
550
551    /// Trains the classifier
552    pub fn train(
553        &mut self,
554        data: &Array2<f64>,
555        labels: &Array1<f64>,
556        epochs: usize,
557        learning_rate: f64,
558    ) -> Result<crate::qnn::TrainingResult> {
559        self.qnn.train_1d(data, labels, epochs, learning_rate)
560    }
561
562    /// Evaluates the classifier
563    pub fn evaluate(
564        &self,
565        data: &Array2<f64>,
566        labels: &Array1<f64>,
567    ) -> Result<ClassificationMetrics> {
568        // Dummy implementation
569        Ok(ClassificationMetrics {
570            accuracy: 0.85,
571            precision: 0.82,
572            recall: 0.88,
573            f1_score: 0.85,
574            auc: 0.91,
575            confusion_matrix: Array2::eye(2),
576            class_accuracies: vec![0.85, 0.86], // Dummy class accuracies
577            class_labels: vec!["Signal".to_string(), "Background".to_string()], // Dummy class labels
578            average_loss: 0.15, // Dummy average loss
579        })
580    }
581}
582
583/// Event reconstructor for HEP data
584#[derive(Debug, Clone)]
585pub struct EventReconstructor {
586    qnn: QuantumNeuralNetwork,
587    input_dim: usize,
588    output_dim: usize,
589}
590
591impl EventReconstructor {
592    /// Creates a new event reconstructor
593    pub fn new() -> Self {
594        // This is a placeholder implementation
595        let layers = vec![
596            crate::qnn::QNNLayerType::EncodingLayer { num_features: 10 },
597            crate::qnn::QNNLayerType::VariationalLayer { num_params: 20 },
598            crate::qnn::QNNLayerType::EntanglementLayer {
599                connectivity: "full".to_string(),
600            },
601            crate::qnn::QNNLayerType::MeasurementLayer {
602                measurement_basis: "computational".to_string(),
603            },
604        ];
605
606        let qnn = QuantumNeuralNetwork::new(
607            layers, 8,  // 8 qubits
608            10, // 10 input features
609            10, // 10 output features
610        )
611        .expect("should create EventReconstructor QNN");
612
613        EventReconstructor {
614            qnn,
615            input_dim: 10,
616            output_dim: 10,
617        }
618    }
619
620    /// Builder method to set the input dimension
621    pub fn with_input_features(mut self, input_dim: usize) -> Self {
622        self.input_dim = input_dim;
623        self
624    }
625
626    /// Builder method to set the output dimension
627    pub fn with_output_features(mut self, output_dim: usize) -> Self {
628        self.output_dim = output_dim;
629        self
630    }
631
632    /// Builder method to set the number of quantum layers
633    pub fn with_quantum_layers(self, _num_layers: usize) -> Result<Self> {
634        // This would normally update the QNN, but we'll just return self for now
635        Ok(self)
636    }
637}
638
639/// Anomaly detector for HEP data
640#[derive(Debug, Clone)]
641pub struct AnomalyDetector {
642    features: usize,
643    quantum_encoder: bool,
644}
645
646impl AnomalyDetector {
647    /// Creates a new anomaly detector
648    pub fn new() -> Self {
649        AnomalyDetector {
650            features: 10,
651            quantum_encoder: false,
652        }
653    }
654
655    /// Builder method to set the number of features
656    pub fn with_features(mut self, features: usize) -> Self {
657        self.features = features;
658        self
659    }
660
661    /// Builder method to enable/disable quantum encoding
662    pub fn with_quantum_encoder(mut self, quantum_encoder: bool) -> Self {
663        self.quantum_encoder = quantum_encoder;
664        self
665    }
666
667    /// Builder method to set the kernel method
668    pub fn with_kernel_method(self, _kernel_method: KernelMethod) -> Result<Self> {
669        // This would normally update the anomaly detector, but we'll just return self for now
670        Ok(self)
671    }
672}
673
674/// Kernel method for quantum machine learning
675#[derive(Debug, Clone, Copy)]
676pub enum KernelMethod {
677    /// Linear kernel
678    Linear,
679
680    /// Polynomial kernel
681    Polynomial,
682
683    /// Quantum kernel
684    QuantumKernel,
685}