quantrs2_device/process_tomography/
core.rs

1//! Core process tomography implementation
2
3use quantrs2_circuit::prelude::*;
4use quantrs2_core::{
5    error::{QuantRS2Error, QuantRS2Result},
6    gate::GateOp,
7    qubit::QubitId,
8};
9use scirs2_core::ndarray::{Array1, Array2, Array3, Array4, ArrayView1, ArrayView2};
10use scirs2_core::Complex64;
11use std::collections::HashMap;
12
13use crate::{
14    backend_traits::{query_backend_capabilities, BackendCapabilities},
15    calibration::{CalibrationManager, DeviceCalibration},
16    characterization::{ProcessTomography, StateTomography},
17    noise_model::CalibrationNoiseModel,
18    translation::HardwareBackend,
19    CircuitResult, DeviceError, DeviceResult,
20};
21
22use super::{
23    analysis::*,
24    config::{ReconstructionMethod, SciRS2ProcessTomographyConfig},
25    reconstruction::*,
26    results::*,
27    utils::*,
28    validation::*,
29};
30
31// Conditional imports for SciRS2
32#[cfg(feature = "scirs2")]
33use scirs2_graph::{
34    betweenness_centrality, closeness_centrality, dijkstra_path, minimum_spanning_tree,
35    strongly_connected_components, Graph,
36};
37#[cfg(feature = "scirs2")]
38use scirs2_linalg::{
39    cholesky, det, eigvals, inv, matrix_norm, prelude::*, qr, svd, trace, LinalgError, LinalgResult,
40};
41#[cfg(feature = "scirs2")]
42use scirs2_optimize::{minimize, OptimizeResult};
43#[cfg(feature = "scirs2")]
44use scirs2_stats::{
45    corrcoef,
46    distributions::{chi2, gamma, norm},
47    ks_2samp, mean, pearsonr, shapiro_wilk, spearmanr, std, ttest_1samp, ttest_ind, var,
48    Alternative, TTestResult,
49};
50
51// Fallback imports when SciRS2 is not available
52#[cfg(not(feature = "scirs2"))]
53use super::fallback::*;
54
55/// Main SciRS2 process tomographer
56pub struct SciRS2ProcessTomographer {
57    pub(crate) config: SciRS2ProcessTomographyConfig,
58    pub(crate) calibration_manager: CalibrationManager,
59    pub(crate) input_states: Vec<Array2<Complex64>>,
60    pub(crate) measurement_operators: Vec<Array2<Complex64>>,
61}
62
63impl SciRS2ProcessTomographer {
64    /// Create a new SciRS2 process tomographer
65    pub const fn new(
66        config: SciRS2ProcessTomographyConfig,
67        calibration_manager: CalibrationManager,
68    ) -> Self {
69        Self {
70            config,
71            calibration_manager,
72            input_states: Vec::new(),
73            measurement_operators: Vec::new(),
74        }
75    }
76
77    /// Generate input states for process tomography
78    pub fn generate_input_states(&mut self, num_qubits: usize) -> DeviceResult<()> {
79        self.input_states = self.create_informationally_complete_states(num_qubits)?;
80        Ok(())
81    }
82
83    /// Generate measurement operators
84    pub fn generate_measurement_operators(&mut self, num_qubits: usize) -> DeviceResult<()> {
85        self.measurement_operators = self.create_pauli_measurements(num_qubits)?;
86        Ok(())
87    }
88
89    /// Perform comprehensive process tomography
90    pub async fn perform_process_tomography<const N: usize, E: ProcessTomographyExecutor>(
91        &self,
92        device_id: &str,
93        process_circuit: &Circuit<N>,
94        executor: &E,
95    ) -> DeviceResult<SciRS2ProcessTomographyResult> {
96        // Step 1: Collect experimental data
97        let experimental_data = self
98            .collect_experimental_data(process_circuit, executor)
99            .await?;
100
101        // Step 2: Reconstruct process matrix using selected method
102        let (process_matrix, reconstruction_quality) = match self.config.reconstruction_method {
103            ReconstructionMethod::LinearInversion => {
104                self.linear_inversion_reconstruction(&experimental_data)?
105            }
106            ReconstructionMethod::MaximumLikelihood => {
107                self.maximum_likelihood_reconstruction(&experimental_data)?
108            }
109            ReconstructionMethod::CompressedSensing => {
110                self.compressed_sensing_reconstruction(&experimental_data)?
111            }
112            ReconstructionMethod::BayesianInference => {
113                self.bayesian_reconstruction(&experimental_data)?
114            }
115            ReconstructionMethod::EnsembleMethods => {
116                self.ensemble_reconstruction(&experimental_data)?
117            }
118            ReconstructionMethod::MachineLearning => self.ml_reconstruction(&experimental_data)?,
119        };
120
121        // Step 3: Convert to Pauli transfer representation
122        let pauli_transfer_matrix = self.convert_to_pauli_transfer(&process_matrix)?;
123
124        // Step 4: Statistical analysis
125        let statistical_analysis =
126            self.perform_statistical_analysis(&process_matrix, &experimental_data)?;
127
128        // Step 5: Calculate process metrics
129        let process_metrics = self.calculate_process_metrics(&process_matrix)?;
130
131        // Step 6: Validation
132        let validation_results = if self.config.validation_config.enable_cross_validation {
133            self.perform_validation(&experimental_data)?
134        } else {
135            ProcessValidationResults {
136                cross_validation: None,
137                bootstrap_results: None,
138                benchmark_results: None,
139                model_selection: ModelSelectionResults {
140                    aic_scores: HashMap::new(),
141                    bic_scores: HashMap::new(),
142                    cross_validation_scores: HashMap::new(),
143                    best_model: "mle".to_string(),
144                    model_weights: HashMap::new(),
145                },
146            }
147        };
148
149        // Step 7: Structure analysis (if enabled)
150        let structure_analysis = if self.config.enable_structure_analysis {
151            Some(self.analyze_process_structure(&process_matrix)?)
152        } else {
153            None
154        };
155
156        // Step 8: Uncertainty quantification
157        let uncertainty_quantification =
158            self.quantify_uncertainty(&process_matrix, &experimental_data)?;
159
160        // Step 9: Process comparisons
161        let process_comparisons = self.compare_with_known_processes(&process_matrix)?;
162
163        Ok(SciRS2ProcessTomographyResult {
164            device_id: device_id.to_string(),
165            config: self.config.clone(),
166            process_matrix,
167            pauli_transfer_matrix,
168            statistical_analysis: ProcessStatisticalAnalysis {
169                reconstruction_quality,
170                statistical_tests: statistical_analysis.0,
171                distribution_analysis: statistical_analysis.1,
172                correlation_analysis: statistical_analysis.2,
173            },
174            process_metrics,
175            validation_results,
176            structure_analysis,
177            uncertainty_quantification,
178            process_comparisons,
179        })
180    }
181
182    /// Create informationally complete set of input states
183    pub(crate) fn create_informationally_complete_states(
184        &self,
185        num_qubits: usize,
186    ) -> DeviceResult<Vec<Array2<Complex64>>> {
187        let mut states = Vec::new();
188        let dim = 1 << num_qubits; // 2^n
189
190        // Create standard IC-POVM states
191        // For 1 qubit: |0⟩, |1⟩, |+⟩, |-⟩, |+i⟩, |-i⟩
192        if num_qubits == 1 {
193            // |0⟩
194            let mut state0 = Array2::zeros((2, 2));
195            state0[[0, 0]] = Complex64::new(1.0, 0.0);
196            states.push(state0);
197
198            // |1⟩
199            let mut state1 = Array2::zeros((2, 2));
200            state1[[1, 1]] = Complex64::new(1.0, 0.0);
201            states.push(state1);
202
203            // |+⟩ = (|0⟩ + |1⟩)/√2
204            let mut state_plus = Array2::zeros((2, 2));
205            state_plus[[0, 0]] = Complex64::new(0.5, 0.0);
206            state_plus[[0, 1]] = Complex64::new(0.5, 0.0);
207            state_plus[[1, 0]] = Complex64::new(0.5, 0.0);
208            state_plus[[1, 1]] = Complex64::new(0.5, 0.0);
209            states.push(state_plus);
210
211            // |-⟩ = (|0⟩ - |1⟩)/√2
212            let mut state_minus = Array2::zeros((2, 2));
213            state_minus[[0, 0]] = Complex64::new(0.5, 0.0);
214            state_minus[[0, 1]] = Complex64::new(-0.5, 0.0);
215            state_minus[[1, 0]] = Complex64::new(-0.5, 0.0);
216            state_minus[[1, 1]] = Complex64::new(0.5, 0.0);
217            states.push(state_minus);
218
219            // |+i⟩ = (|0⟩ + i|1⟩)/√2
220            let mut state_plus_i = Array2::zeros((2, 2));
221            state_plus_i[[0, 0]] = Complex64::new(0.5, 0.0);
222            state_plus_i[[0, 1]] = Complex64::new(0.0, 0.5);
223            state_plus_i[[1, 0]] = Complex64::new(0.0, -0.5);
224            state_plus_i[[1, 1]] = Complex64::new(0.5, 0.0);
225            states.push(state_plus_i);
226
227            // |-i⟩ = (|0⟩ - i|1⟩)/√2
228            let mut state_minus_i = Array2::zeros((2, 2));
229            state_minus_i[[0, 0]] = Complex64::new(0.5, 0.0);
230            state_minus_i[[0, 1]] = Complex64::new(0.0, -0.5);
231            state_minus_i[[1, 0]] = Complex64::new(0.0, 0.5);
232            state_minus_i[[1, 1]] = Complex64::new(0.5, 0.0);
233            states.push(state_minus_i);
234        } else {
235            // For multi-qubit systems, use tensor products of single-qubit states
236            let single_qubit_states = self.create_informationally_complete_states(1)?;
237
238            // Generate all combinations
239            for combination in self.generate_state_combinations(&single_qubit_states, num_qubits)? {
240                states.push(combination);
241            }
242        }
243
244        Ok(states)
245    }
246
247    /// Create Pauli measurement operators
248    pub(crate) fn create_pauli_measurements(
249        &self,
250        num_qubits: usize,
251    ) -> DeviceResult<Vec<Array2<Complex64>>> {
252        let mut measurements = Vec::new();
253        let dim = 1 << num_qubits;
254
255        // Single qubit Pauli operators
256        let pauli_i = Array2::from_shape_vec(
257            (2, 2),
258            vec![
259                Complex64::new(1.0, 0.0),
260                Complex64::new(0.0, 0.0),
261                Complex64::new(0.0, 0.0),
262                Complex64::new(1.0, 0.0),
263            ],
264        )
265        .map_err(|e| DeviceError::APIError(format!("Array creation error: {e}")))?;
266
267        let pauli_x = Array2::from_shape_vec(
268            (2, 2),
269            vec![
270                Complex64::new(0.0, 0.0),
271                Complex64::new(1.0, 0.0),
272                Complex64::new(1.0, 0.0),
273                Complex64::new(0.0, 0.0),
274            ],
275        )
276        .map_err(|e| DeviceError::APIError(format!("Array creation error: {e}")))?;
277
278        let pauli_y = Array2::from_shape_vec(
279            (2, 2),
280            vec![
281                Complex64::new(0.0, 0.0),
282                Complex64::new(0.0, -1.0),
283                Complex64::new(0.0, 1.0),
284                Complex64::new(0.0, 0.0),
285            ],
286        )
287        .map_err(|e| DeviceError::APIError(format!("Array creation error: {e}")))?;
288
289        let pauli_z = Array2::from_shape_vec(
290            (2, 2),
291            vec![
292                Complex64::new(1.0, 0.0),
293                Complex64::new(0.0, 0.0),
294                Complex64::new(0.0, 0.0),
295                Complex64::new(-1.0, 0.0),
296            ],
297        )
298        .map_err(|e| DeviceError::APIError(format!("Array creation error: {e}")))?;
299
300        let single_qubit_paulis = vec![pauli_i, pauli_x, pauli_y, pauli_z];
301
302        if num_qubits == 1 {
303            measurements = single_qubit_paulis;
304        } else {
305            // Generate tensor products for multi-qubit measurements
306            measurements = self.generate_pauli_tensor_products(&single_qubit_paulis, num_qubits)?;
307        }
308
309        Ok(measurements)
310    }
311
312    /// Calculate process fidelity with an ideal process
313    pub fn calculate_process_fidelity(
314        &self,
315        process_matrix: &Array4<Complex64>,
316    ) -> DeviceResult<f64> {
317        // Convert to Choi representation for fidelity calculation
318        let choi_matrix = self.convert_to_choi_matrix(process_matrix)?;
319
320        // Create ideal identity process for comparison
321        let ideal_choi = self.create_ideal_identity_choi(choi_matrix.dim().0)?;
322
323        // Calculate process fidelity using Choi matrices
324        #[cfg(feature = "scirs2")]
325        {
326            let fidelity = self.calculate_choi_fidelity(&choi_matrix, &ideal_choi)?;
327            Ok(fidelity)
328        }
329
330        #[cfg(not(feature = "scirs2"))]
331        {
332            // Fallback: simplified fidelity calculation
333            Ok(0.95) // Placeholder
334        }
335    }
336
337    /// Calculate entangling power of the process
338    pub fn calculate_entangling_power(
339        &self,
340        process_matrix: &Array4<Complex64>,
341    ) -> DeviceResult<f64> {
342        let dim = process_matrix.dim().0;
343
344        // For single qubit processes, entangling power is zero
345        if dim <= 2 {
346            return Ok(0.0);
347        }
348
349        #[cfg(feature = "scirs2")]
350        {
351            // Calculate entangling power using process matrix analysis
352            let choi_matrix = self.convert_to_choi_matrix(process_matrix)?;
353
354            // Compute eigenvalues of the partial transpose
355            let partial_transpose = self.partial_transpose(&choi_matrix)?;
356
357            if let Ok(eigenvalues) = eigvals(&partial_transpose.view(), None) {
358                let negative_eigenvalues: Vec<f64> = eigenvalues
359                    .iter()
360                    .map(|x| x.re)
361                    .filter(|&x| x < 0.0)
362                    .collect();
363
364                let entangling_power = negative_eigenvalues.iter().map(|x| x.abs()).sum::<f64>();
365                Ok(entangling_power)
366            } else {
367                Ok(0.0)
368            }
369        }
370
371        #[cfg(not(feature = "scirs2"))]
372        {
373            // Fallback: check for off-diagonal elements as entanglement indicator
374            let mut entangling_power = 0.0;
375            for i in 0..dim {
376                for j in 0..dim {
377                    for k in 0..dim {
378                        for l in 0..dim {
379                            if i != j || k != l {
380                                entangling_power += process_matrix[[i, j, k, l]].norm_sqr();
381                            }
382                        }
383                    }
384                }
385            }
386            Ok(entangling_power / (dim * dim) as f64)
387        }
388    }
389}
390
391/// Trait for process tomography executors
392pub trait ProcessTomographyExecutor {
393    /// Execute a circuit on input states and perform measurements
394    fn execute_process_measurement<const N: usize>(
395        &self,
396        circuit: &Circuit<N>,
397        input_state: &Array2<Complex64>,
398        measurement: &Array2<Complex64>,
399        shots: usize,
400    ) -> impl std::future::Future<Output = DeviceResult<f64>> + Send;
401}