quantrs2_device/process_tomography/
utils.rs

1//! Utility functions for process tomography
2
3use scirs2_core::ndarray::{Array1, Array2, Array4};
4use scirs2_core::Complex64;
5use std::collections::HashMap;
6
7use super::core::SciRS2ProcessTomographer;
8use super::results::*;
9use crate::DeviceResult;
10
11impl SciRS2ProcessTomographer {
12    /// Collect experimental data from device
13    pub async fn collect_experimental_data<
14        const N: usize,
15        E: super::core::ProcessTomographyExecutor,
16    >(
17        &self,
18        process_circuit: &quantrs2_circuit::prelude::Circuit<N>,
19        executor: &E,
20    ) -> DeviceResult<ExperimentalData> {
21        let mut measurement_results = Vec::new();
22        let mut measurement_uncertainties = Vec::new();
23
24        // Execute measurements for all input state and measurement operator combinations
25        for input_state in &self.input_states {
26            for measurement_op in &self.measurement_operators {
27                let result = executor
28                    .execute_process_measurement(
29                        process_circuit,
30                        input_state,
31                        measurement_op,
32                        self.config.shots_per_state,
33                    )
34                    .await?;
35
36                measurement_results.push(result);
37
38                // Estimate measurement uncertainty based on Poisson statistics
39                let uncertainty = if result > 0.0 {
40                    (result / self.config.shots_per_state as f64).sqrt()
41                } else {
42                    1.0 / (self.config.shots_per_state as f64).sqrt()
43                };
44                measurement_uncertainties.push(uncertainty);
45            }
46        }
47
48        Ok(ExperimentalData {
49            input_states: self.input_states.clone(),
50            measurement_operators: self.measurement_operators.clone(),
51            measurement_results,
52            measurement_uncertainties,
53        })
54    }
55
56    /// Create ideal identity Choi matrix
57    pub(crate) fn create_ideal_identity_choi(&self, dim: usize) -> DeviceResult<Array2<Complex64>> {
58        let choi_dim = dim;
59        let mut identity_choi = Array2::zeros((choi_dim, choi_dim));
60
61        // Identity channel Choi matrix is the maximally entangled state
62        for i in 0..choi_dim {
63            identity_choi[[i, i]] = Complex64::new(1.0, 0.0);
64        }
65
66        Ok(identity_choi)
67    }
68
69    /// Calculate Choi fidelity between two Choi matrices
70    pub(crate) fn calculate_choi_fidelity(
71        &self,
72        choi1: &Array2<Complex64>,
73        choi2: &Array2<Complex64>,
74    ) -> DeviceResult<f64> {
75        let dim = choi1.nrows();
76        let mut fidelity = 0.0;
77        let mut norm1 = 0.0;
78        let mut norm2 = 0.0;
79
80        for i in 0..dim {
81            for j in 0..dim {
82                let element1 = choi1[[i, j]];
83                let element2 = choi2[[i, j]];
84
85                fidelity += (element1.conj() * element2).re;
86                norm1 += element1.norm_sqr();
87                norm2 += element2.norm_sqr();
88            }
89        }
90
91        if norm1 > 1e-12 && norm2 > 1e-12 {
92            Ok(fidelity / (norm1 * norm2).sqrt())
93        } else {
94            Ok(0.0)
95        }
96    }
97
98    /// Calculate partial transpose of a matrix
99    pub(crate) fn partial_transpose(
100        &self,
101        matrix: &Array2<Complex64>,
102    ) -> DeviceResult<Array2<f64>> {
103        let dim = matrix.nrows();
104        let sqrt_dim = (dim as f64).sqrt() as usize;
105
106        if sqrt_dim * sqrt_dim != dim {
107            return Ok(Array2::zeros((dim, dim)));
108        }
109
110        let mut pt_matrix = Array2::zeros((dim, dim));
111
112        // Partial transpose operation
113        for i in 0..sqrt_dim {
114            for j in 0..sqrt_dim {
115                for k in 0..sqrt_dim {
116                    for l in 0..sqrt_dim {
117                        let row1 = i * sqrt_dim + j;
118                        let col1 = k * sqrt_dim + l;
119                        let row2 = i * sqrt_dim + l;
120                        let col2 = k * sqrt_dim + j;
121
122                        if row1 < dim && col1 < dim && row2 < dim && col2 < dim {
123                            pt_matrix[[row2, col2]] = matrix[[row1, col1]].re;
124                        }
125                    }
126                }
127            }
128        }
129
130        Ok(pt_matrix)
131    }
132
133    /// Calculate process metrics from process matrix
134    pub fn calculate_process_metrics(
135        &self,
136        process_matrix: &Array4<Complex64>,
137    ) -> DeviceResult<ProcessMetrics> {
138        let process_fidelity = self.calculate_process_fidelity(process_matrix)?;
139        let average_gate_fidelity = self.calculate_average_gate_fidelity(process_matrix)?;
140        let unitarity = self.calculate_unitarity(process_matrix)?;
141        let entangling_power = self.calculate_entangling_power(process_matrix)?;
142        let non_unitality = self.calculate_non_unitality(process_matrix)?;
143        let channel_capacity = self.calculate_channel_capacity(process_matrix)?;
144        let coherent_information = self.calculate_coherent_information(process_matrix)?;
145        let diamond_norm_distance = self.calculate_diamond_norm_distance(process_matrix)?;
146        let process_spectrum = self.calculate_process_spectrum(process_matrix)?;
147
148        Ok(ProcessMetrics {
149            process_fidelity,
150            average_gate_fidelity,
151            unitarity,
152            entangling_power,
153            non_unitality,
154            channel_capacity,
155            coherent_information,
156            diamond_norm_distance,
157            process_spectrum,
158        })
159    }
160
161    /// Calculate average gate fidelity
162    fn calculate_average_gate_fidelity(
163        &self,
164        process_matrix: &Array4<Complex64>,
165    ) -> DeviceResult<f64> {
166        let dim = process_matrix.dim().0;
167
168        // AGF = (d * process_fidelity + 1) / (d + 1) where d is the dimension
169        let process_fidelity = self.calculate_process_fidelity(process_matrix)?;
170        let agf = (dim as f64).mul_add(process_fidelity, 1.0) / (dim as f64 + 1.0);
171
172        Ok(agf)
173    }
174
175    /// Calculate unitarity measure
176    fn calculate_unitarity(&self, process_matrix: &Array4<Complex64>) -> DeviceResult<f64> {
177        let dim = process_matrix.dim().0;
178        let mut unitarity = 0.0;
179
180        // Unitarity is the trace of chi^2 where chi is the process matrix
181        for i in 0..dim {
182            for j in 0..dim {
183                for k in 0..dim {
184                    for l in 0..dim {
185                        for m in 0..dim {
186                            for n in 0..dim {
187                                unitarity += (process_matrix[[i, j, k, l]].conj()
188                                    * process_matrix[[i, j, m, n]]
189                                    * process_matrix[[m, n, k, l]])
190                                .re;
191                            }
192                        }
193                    }
194                }
195            }
196        }
197
198        Ok(unitarity / (dim * dim) as f64)
199    }
200
201    /// Calculate non-unitality measure
202    fn calculate_non_unitality(&self, process_matrix: &Array4<Complex64>) -> DeviceResult<f64> {
203        let dim = process_matrix.dim().0;
204        let mut non_unitality = 0.0;
205
206        // Non-unitality measures deviation from unital channels
207        for i in 0..dim {
208            for j in 0..dim {
209                if i != j {
210                    let mut sum = Complex64::new(0.0, 0.0);
211                    for k in 0..dim {
212                        sum += process_matrix[[i, j, k, k]];
213                    }
214                    non_unitality += sum.norm_sqr();
215                }
216            }
217        }
218
219        Ok(non_unitality / (dim * dim) as f64)
220    }
221
222    /// Calculate channel capacity
223    fn calculate_channel_capacity(&self, process_matrix: &Array4<Complex64>) -> DeviceResult<f64> {
224        // Simplified channel capacity calculation
225        // In practice, this would involve optimization over input states
226        let unitarity = self.calculate_unitarity(process_matrix)?;
227        let capacity = unitarity * (process_matrix.dim().0 as f64).log2();
228
229        Ok(capacity)
230    }
231
232    /// Calculate coherent information
233    fn calculate_coherent_information(
234        &self,
235        process_matrix: &Array4<Complex64>,
236    ) -> DeviceResult<f64> {
237        // Simplified coherent information calculation
238        let unitarity = self.calculate_unitarity(process_matrix)?;
239        let coherent_info = unitarity * 0.8; // Simplified approximation
240
241        Ok(coherent_info)
242    }
243
244    /// Calculate diamond norm distance
245    fn calculate_diamond_norm_distance(
246        &self,
247        process_matrix: &Array4<Complex64>,
248    ) -> DeviceResult<f64> {
249        // Simplified diamond norm calculation
250        // In practice, this would involve semidefinite programming
251        let process_fidelity = self.calculate_process_fidelity(process_matrix)?;
252        let diamond_distance = 2.0 * (1.0 - process_fidelity).sqrt();
253
254        Ok(diamond_distance)
255    }
256
257    /// Calculate process spectrum (eigenvalues)
258    fn calculate_process_spectrum(
259        &self,
260        process_matrix: &Array4<Complex64>,
261    ) -> DeviceResult<Array1<f64>> {
262        let choi_matrix = self.convert_to_choi_matrix(process_matrix)?;
263
264        #[cfg(feature = "scirs2")]
265        {
266            use scirs2_linalg::eigvals;
267
268            let real_choi = choi_matrix.mapv(|x| x.re);
269            if let Ok(eigenvalues) = eigvals(&real_choi.view(), None) {
270                let spectrum = eigenvalues.mapv(|x| x.re);
271                return Ok(spectrum);
272            }
273        }
274
275        // Fallback: return uniform spectrum
276        let dim = choi_matrix.nrows();
277        Ok(Array1::from_elem(dim, 1.0 / dim as f64))
278    }
279}
280
281/// Process tomography utility functions
282pub mod process_utils {
283    use super::*;
284
285    /// Reshape process matrix for different representations
286    pub fn reshape_process_matrix(
287        process_matrix: &Array4<Complex64>,
288        target_shape: (usize, usize),
289    ) -> DeviceResult<Array2<Complex64>> {
290        let (dim1, dim2, dim3, dim4) = process_matrix.dim();
291        let total_elements = dim1 * dim2 * dim3 * dim4;
292
293        if target_shape.0 * target_shape.1 != total_elements {
294            return Err(crate::DeviceError::APIError(
295                "Target shape incompatible with process matrix size".to_string(),
296            ));
297        }
298
299        let mut reshaped = Array2::zeros(target_shape);
300        let mut idx = 0;
301
302        for i in 0..dim1 {
303            for j in 0..dim2 {
304                for k in 0..dim3 {
305                    for l in 0..dim4 {
306                        let row = idx / target_shape.1;
307                        let col = idx % target_shape.1;
308
309                        if row < target_shape.0 && col < target_shape.1 {
310                            reshaped[[row, col]] = process_matrix[[i, j, k, l]];
311                        }
312                        idx += 1;
313                    }
314                }
315            }
316        }
317
318        Ok(reshaped)
319    }
320
321    /// Vectorize process matrix
322    pub fn vectorize_process_matrix(process_matrix: &Array4<Complex64>) -> Array1<Complex64> {
323        let (dim1, dim2, dim3, dim4) = process_matrix.dim();
324        let total_elements = dim1 * dim2 * dim3 * dim4;
325        let mut vector = Array1::zeros(total_elements);
326
327        let mut idx = 0;
328        for i in 0..dim1 {
329            for j in 0..dim2 {
330                for k in 0..dim3 {
331                    for l in 0..dim4 {
332                        vector[idx] = process_matrix[[i, j, k, l]];
333                        idx += 1;
334                    }
335                }
336            }
337        }
338
339        vector
340    }
341
342    /// Convert vector back to process matrix
343    pub fn devectorize_to_process_matrix(
344        vector: &Array1<Complex64>,
345        dim: usize,
346    ) -> DeviceResult<Array4<Complex64>> {
347        let expected_length = dim.pow(4);
348        if vector.len() != expected_length {
349            return Err(crate::DeviceError::APIError(format!(
350                "Vector length {} does not match expected length {} for dimension {}",
351                vector.len(),
352                expected_length,
353                dim
354            )));
355        }
356
357        let mut process_matrix = Array4::zeros((dim, dim, dim, dim));
358        let mut idx = 0;
359
360        for i in 0..dim {
361            for j in 0..dim {
362                for k in 0..dim {
363                    for l in 0..dim {
364                        process_matrix[[i, j, k, l]] = vector[idx];
365                        idx += 1;
366                    }
367                }
368            }
369        }
370
371        Ok(process_matrix)
372    }
373
374    /// Calculate trace distance between two process matrices
375    pub fn trace_distance(
376        process1: &Array4<Complex64>,
377        process2: &Array4<Complex64>,
378    ) -> DeviceResult<f64> {
379        if process1.dim() != process2.dim() {
380            return Err(crate::DeviceError::APIError(
381                "Process matrices must have the same dimensions".to_string(),
382            ));
383        }
384
385        let dim = process1.dim();
386        let mut trace_distance = 0.0;
387
388        for i in 0..dim.0 {
389            for j in 0..dim.1 {
390                for k in 0..dim.2 {
391                    for l in 0..dim.3 {
392                        let diff = process1[[i, j, k, l]] - process2[[i, j, k, l]];
393                        trace_distance += diff.norm();
394                    }
395                }
396            }
397        }
398
399        Ok(trace_distance / 2.0)
400    }
401
402    /// Check if process matrix satisfies trace preservation
403    pub fn check_trace_preservation(process_matrix: &Array4<Complex64>, tolerance: f64) -> bool {
404        let dim = process_matrix.dim().0;
405        let mut trace = Complex64::new(0.0, 0.0);
406
407        for i in 0..dim {
408            for j in 0..dim {
409                trace += process_matrix[[i, j, i, j]];
410            }
411        }
412
413        (trace.re - 1.0).abs() < tolerance && trace.im.abs() < tolerance
414    }
415
416    /// Check if process matrix satisfies complete positivity (simplified)
417    pub fn check_complete_positivity(process_matrix: &Array4<Complex64>, tolerance: f64) -> bool {
418        let dim = process_matrix.dim().0;
419
420        // Simplified check: all diagonal elements should be non-negative
421        for i in 0..dim {
422            for j in 0..dim {
423                if process_matrix[[i, j, i, j]].re < -tolerance {
424                    return false;
425                }
426            }
427        }
428
429        true
430    }
431
432    /// Normalize process matrix to satisfy trace preservation
433    pub fn normalize_process_matrix(process_matrix: &mut Array4<Complex64>) {
434        let dim = process_matrix.dim().0;
435        let mut trace = Complex64::new(0.0, 0.0);
436
437        // Calculate current trace
438        for i in 0..dim {
439            for j in 0..dim {
440                trace += process_matrix[[i, j, i, j]];
441            }
442        }
443
444        // Normalize if trace is non-zero
445        if trace.norm() > 1e-12 {
446            let scale_factor = Complex64::new(1.0, 0.0) / trace;
447            for i in 0..dim {
448                for j in 0..dim {
449                    for k in 0..dim {
450                        for l in 0..dim {
451                            process_matrix[[i, j, k, l]] *= scale_factor;
452                        }
453                    }
454                }
455            }
456        }
457    }
458}