1use crate::Result;
6use scirs2_core::ndarray::{s, Array1, Array2, Axis};
7use scirs2_core::random::thread_rng;
8use scirs2_core::Complex64;
9use std::collections::HashMap;
10use thiserror::Error;
11
12use super::functions::{generate_pauli_basis, kronecker_product, matrix_trace, purity};
13
14#[derive(Debug, Clone)]
16pub struct TomographyResult {
17 pub density_matrix: Array2<Complex64>,
19 pub fidelity_estimate: Option<f64>,
21 pub purity: f64,
23 pub uncertainty: f64,
25 pub total_measurements: usize,
27}
28#[derive(Debug, Error)]
30pub enum QuantumInfoError {
31 #[error("Dimension mismatch: {0}")]
32 DimensionMismatch(String),
33 #[error("Invalid quantum state: {0}")]
34 InvalidState(String),
35 #[error("Invalid density matrix: {0}")]
36 InvalidDensityMatrix(String),
37 #[error("Numerical error: {0}")]
38 NumericalError(String),
39 #[error("Tomography error: {0}")]
40 TomographyError(String),
41 #[error("Not implemented: {0}")]
42 NotImplemented(String),
43}
44#[derive(Debug, Clone)]
46pub struct QuantumChannel {
47 kraus_operators: Vec<Array2<Complex64>>,
49 input_dim: usize,
51 output_dim: usize,
53}
54impl QuantumChannel {
55 pub fn from_kraus(
57 kraus: Vec<Array2<Complex64>>,
58 ) -> std::result::Result<Self, QuantumInfoError> {
59 if kraus.is_empty() {
60 return Err(QuantumInfoError::InvalidState(
61 "Kraus operators cannot be empty".to_string(),
62 ));
63 }
64 let input_dim = kraus[0].ncols();
65 let output_dim = kraus[0].nrows();
66 Ok(Self {
67 kraus_operators: kraus,
68 input_dim,
69 output_dim,
70 })
71 }
72 pub fn identity(dim: usize) -> Self {
74 let mut identity = Array2::zeros((dim, dim));
75 for i in 0..dim {
76 identity[[i, i]] = Complex64::new(1.0, 0.0);
77 }
78 Self {
79 kraus_operators: vec![identity],
80 input_dim: dim,
81 output_dim: dim,
82 }
83 }
84 pub fn unitary(u: Array2<Complex64>) -> std::result::Result<Self, QuantumInfoError> {
86 let dim = u.nrows();
87 if dim != u.ncols() {
88 return Err(QuantumInfoError::InvalidState(
89 "Unitary matrix must be square".to_string(),
90 ));
91 }
92 Ok(Self {
93 kraus_operators: vec![u],
94 input_dim: dim,
95 output_dim: dim,
96 })
97 }
98 pub fn depolarizing(p: f64) -> Self {
102 let sqrt_1_p = (1.0 - p).sqrt();
103 let sqrt_p_3 = (p / 3.0).sqrt();
104 let k0 = Array2::from_shape_vec(
105 (2, 2),
106 vec![
107 Complex64::new(sqrt_1_p, 0.0),
108 Complex64::new(0.0, 0.0),
109 Complex64::new(0.0, 0.0),
110 Complex64::new(sqrt_1_p, 0.0),
111 ],
112 )
113 .expect("Valid shape");
114 let k1 = Array2::from_shape_vec(
115 (2, 2),
116 vec![
117 Complex64::new(0.0, 0.0),
118 Complex64::new(sqrt_p_3, 0.0),
119 Complex64::new(sqrt_p_3, 0.0),
120 Complex64::new(0.0, 0.0),
121 ],
122 )
123 .expect("Valid shape");
124 let k2 = Array2::from_shape_vec(
125 (2, 2),
126 vec![
127 Complex64::new(0.0, 0.0),
128 Complex64::new(0.0, -sqrt_p_3),
129 Complex64::new(0.0, sqrt_p_3),
130 Complex64::new(0.0, 0.0),
131 ],
132 )
133 .expect("Valid shape");
134 let k3 = Array2::from_shape_vec(
135 (2, 2),
136 vec![
137 Complex64::new(sqrt_p_3, 0.0),
138 Complex64::new(0.0, 0.0),
139 Complex64::new(0.0, 0.0),
140 Complex64::new(-sqrt_p_3, 0.0),
141 ],
142 )
143 .expect("Valid shape");
144 Self {
145 kraus_operators: vec![k0, k1, k2, k3],
146 input_dim: 2,
147 output_dim: 2,
148 }
149 }
150 pub fn amplitude_damping(gamma: f64) -> Self {
156 let sqrt_gamma = gamma.sqrt();
157 let sqrt_1_gamma = (1.0 - gamma).sqrt();
158 let k0 = Array2::from_shape_vec(
159 (2, 2),
160 vec![
161 Complex64::new(1.0, 0.0),
162 Complex64::new(0.0, 0.0),
163 Complex64::new(0.0, 0.0),
164 Complex64::new(sqrt_1_gamma, 0.0),
165 ],
166 )
167 .expect("Valid shape");
168 let k1 = Array2::from_shape_vec(
169 (2, 2),
170 vec![
171 Complex64::new(0.0, 0.0),
172 Complex64::new(sqrt_gamma, 0.0),
173 Complex64::new(0.0, 0.0),
174 Complex64::new(0.0, 0.0),
175 ],
176 )
177 .expect("Valid shape");
178 Self {
179 kraus_operators: vec![k0, k1],
180 input_dim: 2,
181 output_dim: 2,
182 }
183 }
184 pub fn phase_damping(gamma: f64) -> Self {
188 let sqrt_gamma = gamma.sqrt();
189 let sqrt_1_gamma = (1.0 - gamma).sqrt();
190 let k0 = Array2::from_shape_vec(
191 (2, 2),
192 vec![
193 Complex64::new(1.0, 0.0),
194 Complex64::new(0.0, 0.0),
195 Complex64::new(0.0, 0.0),
196 Complex64::new(sqrt_1_gamma, 0.0),
197 ],
198 )
199 .expect("Valid shape");
200 let k1 = Array2::from_shape_vec(
201 (2, 2),
202 vec![
203 Complex64::new(0.0, 0.0),
204 Complex64::new(0.0, 0.0),
205 Complex64::new(0.0, 0.0),
206 Complex64::new(sqrt_gamma, 0.0),
207 ],
208 )
209 .expect("Valid shape");
210 Self {
211 kraus_operators: vec![k0, k1],
212 input_dim: 2,
213 output_dim: 2,
214 }
215 }
216 pub fn apply(
218 &self,
219 state: &QuantumState,
220 ) -> std::result::Result<QuantumState, QuantumInfoError> {
221 let rho = state.to_density_matrix()?;
222 let mut output = Array2::zeros((self.output_dim, self.output_dim));
223 for k in &self.kraus_operators {
224 let k_dag = k.t().mapv(|c| c.conj());
225 let k_rho = k.dot(&rho);
226 let k_rho_k_dag = k_rho.dot(&k_dag);
227 output = output + k_rho_k_dag;
228 }
229 Ok(QuantumState::Mixed(output))
230 }
231 pub fn input_dim(&self) -> usize {
233 self.input_dim
234 }
235 pub fn output_dim(&self) -> usize {
237 self.output_dim
238 }
239 pub fn to_choi(&self) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
244 let d = self.input_dim;
245 let choi_dim = d * self.output_dim;
246 let mut choi = Array2::zeros((choi_dim, choi_dim));
247 for k in &self.kraus_operators {
248 let mut vec_k = Array1::zeros(choi_dim);
249 for j in 0..d {
250 for i in 0..self.output_dim {
251 vec_k[j * self.output_dim + i] = k[[i, j]];
252 }
253 }
254 for i in 0..choi_dim {
255 for j in 0..choi_dim {
256 choi[[i, j]] += vec_k[i] * vec_k[j].conj();
257 }
258 }
259 }
260 Ok(choi)
261 }
262 pub fn to_ptm(&self) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
267 let d = self.input_dim;
268 let num_paulis = d * d;
269 let paulis = generate_pauli_basis(d)?;
270 let mut ptm = Array2::zeros((num_paulis, num_paulis));
271 for (j, pj) in paulis.iter().enumerate() {
272 let state_j = QuantumState::Mixed(pj.clone());
273 let output = self.apply(&state_j)?;
274 let rho_out = output.to_density_matrix()?;
275 for (i, pi) in paulis.iter().enumerate() {
276 let trace = matrix_trace(&pi.dot(&rho_out));
277 ptm[[i, j]] = trace / Complex64::new(d as f64, 0.0);
278 }
279 }
280 Ok(ptm)
281 }
282}
283pub struct ProcessTomography {
285 num_qubits: usize,
287 config: StateTomographyConfig,
289}
290impl ProcessTomography {
291 pub fn new(num_qubits: usize, config: StateTomographyConfig) -> Self {
293 Self { num_qubits, config }
294 }
295 pub fn reconstruct(
303 &self,
304 data: &ProcessTomographyData,
305 ) -> std::result::Result<QuantumChannel, QuantumInfoError> {
306 let dim = 1 << self.num_qubits;
307 let mut choi = Array2::zeros((dim * dim, dim * dim));
308 for (input_state, output_dm) in &data.state_pairs {
309 let input_dm = input_state.to_density_matrix()?;
310 let contrib = kronecker_product(&input_dm, output_dm);
311 choi = choi + contrib;
312 }
313 choi /= Complex64::new(data.state_pairs.len() as f64, 0.0);
314 let kraus = self.choi_to_kraus(&choi)?;
315 QuantumChannel::from_kraus(kraus)
316 }
317 pub(super) fn choi_to_kraus(
319 &self,
320 choi: &Array2<Complex64>,
321 ) -> std::result::Result<Vec<Array2<Complex64>>, QuantumInfoError> {
322 let dim = 1 << self.num_qubits;
323 let mut k0 = Array2::zeros((dim, dim));
324 for i in 0..dim {
325 k0[[i, i]] = (choi[[i * dim + i, i * dim + i]]).sqrt();
326 }
327 Ok(vec![k0])
328 }
329}
330#[derive(Debug, Clone)]
332pub enum QuantumState {
333 Pure(Array1<Complex64>),
335 Mixed(Array2<Complex64>),
337}
338impl QuantumState {
339 pub fn pure(state_vector: Array1<Complex64>) -> Self {
341 QuantumState::Pure(state_vector)
342 }
343 pub fn mixed(density_matrix: Array2<Complex64>) -> Self {
345 QuantumState::Mixed(density_matrix)
346 }
347 pub fn computational_basis(dim: usize, index: usize) -> Self {
349 let mut state = Array1::zeros(dim);
350 if index < dim {
351 state[index] = Complex64::new(1.0, 0.0);
352 }
353 QuantumState::Pure(state)
354 }
355 pub fn maximally_mixed(dim: usize) -> Self {
357 let mut rho = Array2::zeros((dim, dim));
358 let val = Complex64::new(1.0 / dim as f64, 0.0);
359 for i in 0..dim {
360 rho[[i, i]] = val;
361 }
362 QuantumState::Mixed(rho)
363 }
364 pub fn bell_state(index: usize) -> Self {
366 let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
367 let state = match index {
368 0 => Array1::from_vec(vec![
369 Complex64::new(inv_sqrt2, 0.0),
370 Complex64::new(0.0, 0.0),
371 Complex64::new(0.0, 0.0),
372 Complex64::new(inv_sqrt2, 0.0),
373 ]),
374 1 => Array1::from_vec(vec![
375 Complex64::new(inv_sqrt2, 0.0),
376 Complex64::new(0.0, 0.0),
377 Complex64::new(0.0, 0.0),
378 Complex64::new(-inv_sqrt2, 0.0),
379 ]),
380 2 => Array1::from_vec(vec![
381 Complex64::new(0.0, 0.0),
382 Complex64::new(inv_sqrt2, 0.0),
383 Complex64::new(inv_sqrt2, 0.0),
384 Complex64::new(0.0, 0.0),
385 ]),
386 _ => Array1::from_vec(vec![
387 Complex64::new(0.0, 0.0),
388 Complex64::new(inv_sqrt2, 0.0),
389 Complex64::new(-inv_sqrt2, 0.0),
390 Complex64::new(0.0, 0.0),
391 ]),
392 };
393 QuantumState::Pure(state)
394 }
395 pub fn ghz_state(n: usize) -> Self {
397 let dim = 1 << n;
398 let mut state = Array1::zeros(dim);
399 let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
400 state[0] = Complex64::new(inv_sqrt2, 0.0);
401 state[dim - 1] = Complex64::new(inv_sqrt2, 0.0);
402 QuantumState::Pure(state)
403 }
404 pub fn w_state(n: usize) -> Self {
406 let dim = 1 << n;
407 let amplitude = 1.0 / (n as f64).sqrt();
408 let mut state = Array1::zeros(dim);
409 for i in 0..n {
410 let index = 1 << i;
411 state[index] = Complex64::new(amplitude, 0.0);
412 }
413 QuantumState::Pure(state)
414 }
415 pub fn to_density_matrix(&self) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
417 match self {
418 QuantumState::Pure(psi) => {
419 let n = psi.len();
420 let mut rho = Array2::zeros((n, n));
421 for i in 0..n {
422 for j in 0..n {
423 rho[[i, j]] = psi[i] * psi[j].conj();
424 }
425 }
426 Ok(rho)
427 }
428 QuantumState::Mixed(rho) => Ok(rho.clone()),
429 }
430 }
431 pub fn dim(&self) -> usize {
433 match self {
434 QuantumState::Pure(psi) => psi.len(),
435 QuantumState::Mixed(rho) => rho.nrows(),
436 }
437 }
438 pub fn is_pure(&self) -> bool {
440 matches!(self, QuantumState::Pure(_))
441 }
442}
443#[derive(Debug, Clone, Copy, PartialEq)]
445pub enum TomographyMethod {
446 LinearInversion,
448 MaximumLikelihood,
450 CompressedSensing,
452 Bayesian,
454}
455#[derive(Debug, Clone)]
457pub struct MeasurementData {
458 pub basis: String,
460 pub counts: std::collections::HashMap<String, usize>,
464}
465impl MeasurementData {
466 pub fn new(basis: &str, counts: std::collections::HashMap<String, usize>) -> Self {
468 Self {
469 basis: basis.to_string(),
470 counts,
471 }
472 }
473 pub fn total_shots(&self) -> usize {
475 self.counts.values().sum()
476 }
477 pub fn expectation_value(&self) -> f64 {
479 let total = self.total_shots() as f64;
480 if total < 1e-10 {
481 return 0.0;
482 }
483 let mut expectation = 0.0;
484 for (outcome, &count) in &self.counts {
485 let parity: usize = outcome.chars().filter(|&c| c == '1').count();
486 let eigenvalue = if parity % 2 == 0 { 1.0 } else { -1.0 };
487 expectation += eigenvalue * count as f64;
488 }
489 expectation / total
490 }
491}
492#[derive(Debug, Clone)]
494pub struct ClassicalShadow {
495 num_snapshots: usize,
497 bases: Vec<String>,
499 outcomes: Vec<String>,
501 num_qubits: usize,
503}
504impl ClassicalShadow {
505 pub fn from_measurements(
507 num_qubits: usize,
508 bases: Vec<String>,
509 outcomes: Vec<String>,
510 ) -> std::result::Result<Self, QuantumInfoError> {
511 if bases.len() != outcomes.len() {
512 return Err(QuantumInfoError::InvalidState(
513 "Number of bases must match number of outcomes".to_string(),
514 ));
515 }
516 Ok(Self {
517 num_snapshots: bases.len(),
518 bases,
519 outcomes,
520 num_qubits,
521 })
522 }
523 pub fn generate_random_bases(num_qubits: usize, num_snapshots: usize) -> Vec<String> {
525 let mut rng = thread_rng();
526 let paulis = ['X', 'Y', 'Z'];
527 (0..num_snapshots)
528 .map(|_| {
529 (0..num_qubits)
530 .map(|_| paulis[rng.random_range(0..3)])
531 .collect()
532 })
533 .collect()
534 }
535 pub fn estimate_observable(
543 &self,
544 observable: &str,
545 ) -> std::result::Result<f64, QuantumInfoError> {
546 if observable.len() != self.num_qubits {
547 return Err(QuantumInfoError::DimensionMismatch(format!(
548 "Observable length {} doesn't match qubit count {}",
549 observable.len(),
550 self.num_qubits
551 )));
552 }
553 let mut sum = 0.0;
554 let mut valid_snapshots = 0;
555 for (basis, outcome) in self.bases.iter().zip(self.outcomes.iter()) {
556 let mut useful = true;
557 let mut contrib = 1.0;
558 for ((obs_char, basis_char), out_char) in
559 observable.chars().zip(basis.chars()).zip(outcome.chars())
560 {
561 if obs_char == 'I' {
562 continue;
563 }
564 if obs_char != basis_char {
565 useful = false;
566 break;
567 }
568 let eigenvalue = if out_char == '0' { 1.0 } else { -1.0 };
569 contrib *= 3.0 * eigenvalue;
570 }
571 if useful {
572 sum += contrib;
573 valid_snapshots += 1;
574 }
575 }
576 if valid_snapshots == 0 {
577 return Ok(0.0);
578 }
579 Ok(sum / valid_snapshots as f64)
580 }
581 pub fn estimate_observables(
583 &self,
584 observables: &[String],
585 ) -> std::result::Result<Vec<f64>, QuantumInfoError> {
586 observables
587 .iter()
588 .map(|obs| self.estimate_observable(obs))
589 .collect()
590 }
591 pub fn estimate_fidelity(
593 &self,
594 target: &QuantumState,
595 ) -> std::result::Result<f64, QuantumInfoError> {
596 let target_dm = target.to_density_matrix()?;
597 let dim = target_dm.nrows();
598 let mut fidelity_sum = 0.0;
599 let num_samples = 100;
600 let mut rng = thread_rng();
601 for _ in 0..num_samples {
602 let paulis = ['I', 'X', 'Y', 'Z'];
603 let pauli_string: String = (0..self.num_qubits)
604 .map(|_| paulis[rng.random_range(0..4)])
605 .collect();
606 if let Ok(shadow_exp) = self.estimate_observable(&pauli_string) {
607 fidelity_sum += shadow_exp.abs();
608 }
609 }
610 Ok((fidelity_sum / num_samples as f64).min(1.0))
611 }
612 pub fn num_snapshots(&self) -> usize {
614 self.num_snapshots
615 }
616}
617pub struct StateTomography {
619 config: StateTomographyConfig,
620 num_qubits: usize,
621}
622impl StateTomography {
623 pub fn new(num_qubits: usize, config: StateTomographyConfig) -> Self {
625 Self { config, num_qubits }
626 }
627 pub fn reconstruct(
634 &self,
635 measurements: &[MeasurementData],
636 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
637 match self.config.method {
638 TomographyMethod::LinearInversion => self.linear_inversion(measurements),
639 TomographyMethod::MaximumLikelihood => self.maximum_likelihood(measurements),
640 TomographyMethod::CompressedSensing => self.compressed_sensing(measurements),
641 TomographyMethod::Bayesian => self.bayesian_estimation(measurements),
642 }
643 }
644 pub(super) fn linear_inversion(
646 &self,
647 measurements: &[MeasurementData],
648 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
649 let dim = 1 << self.num_qubits;
650 let mut rho = Array2::zeros((dim, dim));
651 for data in measurements {
652 let expectation = data.expectation_value();
653 let pauli = self.basis_to_pauli(&data.basis)?;
654 rho = rho + &pauli * Complex64::new(expectation / dim as f64, 0.0);
655 }
656 let mut identity = Array2::zeros((dim, dim));
657 for i in 0..dim {
658 identity[[i, i]] = Complex64::new(1.0 / dim as f64, 0.0);
659 }
660 rho = rho + identity;
661 if self.config.physical_constraints {
662 rho = self.make_physical(rho)?;
663 }
664 let state = QuantumState::Mixed(rho.clone());
665 let purity_val = purity(&state)?;
666 let total_measurements: usize = measurements.iter().map(|m| m.total_shots()).sum();
667 Ok(TomographyResult {
668 density_matrix: rho,
669 fidelity_estimate: None,
670 purity: purity_val,
671 uncertainty: 1.0 / (total_measurements as f64).sqrt(),
672 total_measurements,
673 })
674 }
675 pub(super) fn maximum_likelihood(
677 &self,
678 measurements: &[MeasurementData],
679 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
680 let dim = 1 << self.num_qubits;
681 let initial = self.linear_inversion(measurements)?;
682 let mut rho = initial.density_matrix;
683 let max_iterations = 100;
684 let tolerance = 1e-6;
685 for _iter in 0..max_iterations {
686 let r = self.compute_r_matrix(&rho, measurements)?;
687 let r_rho = r.dot(&rho);
688 let r_rho_r = r_rho.dot(&r);
689 let trace: Complex64 = (0..dim).map(|i| r_rho_r[[i, i]]).sum();
690 let trace_re = trace.re.max(1e-15);
691 let rho_new = r_rho_r / Complex64::new(trace_re, 0.0);
692 let diff: f64 = rho
693 .iter()
694 .zip(rho_new.iter())
695 .map(|(a, b)| (a - b).norm())
696 .sum();
697 if diff < tolerance {
698 rho = rho_new;
699 break;
700 }
701 rho = rho_new;
702 }
703 let state = QuantumState::Mixed(rho.clone());
704 let purity_val = purity(&state)?;
705 let total_measurements: usize = measurements.iter().map(|m| m.total_shots()).sum();
706 Ok(TomographyResult {
707 density_matrix: rho,
708 fidelity_estimate: None,
709 purity: purity_val,
710 uncertainty: 1.0 / (total_measurements as f64).sqrt(),
711 total_measurements,
712 })
713 }
714 pub(super) fn compute_r_matrix(
716 &self,
717 rho: &Array2<Complex64>,
718 measurements: &[MeasurementData],
719 ) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
720 let dim = rho.nrows();
721 let mut r = Array2::zeros((dim, dim));
722 for data in measurements {
723 let pauli = self.basis_to_pauli(&data.basis)?;
724 let p_rho = pauli.dot(rho);
725 let exp_rho: Complex64 = (0..dim).map(|i| p_rho[[i, i]]).sum();
726 let exp_data = data.expectation_value();
727 if exp_rho.re.abs() > 1e-10 {
728 let weight = exp_data / exp_rho.re;
729 r = r + &pauli * Complex64::new(weight, 0.0);
730 }
731 }
732 let trace: Complex64 = (0..dim).map(|i| r[[i, i]]).sum();
733 if trace.re.abs() > 1e-10 {
734 r /= Complex64::new(trace.re, 0.0);
735 }
736 Ok(r)
737 }
738 pub(super) fn compressed_sensing(
740 &self,
741 measurements: &[MeasurementData],
742 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
743 let mut result = self.linear_inversion(measurements)?;
744 result.density_matrix = self.truncate_rank(result.density_matrix, 4)?;
745 Ok(result)
746 }
747 pub(super) fn bayesian_estimation(
749 &self,
750 measurements: &[MeasurementData],
751 ) -> std::result::Result<TomographyResult, QuantumInfoError> {
752 let dim = 1 << self.num_qubits;
753 let mut rho = Array2::zeros((dim, dim));
754 for i in 0..dim {
755 rho[[i, i]] = Complex64::new(1.0 / dim as f64, 0.0);
756 }
757 for data in measurements {
758 let pauli = self.basis_to_pauli(&data.basis)?;
759 let exp_data = data.expectation_value();
760 let p_rho = pauli.dot(&rho);
761 let exp_rho: Complex64 = (0..dim).map(|i| p_rho[[i, i]]).sum();
762 let diff = exp_data - exp_rho.re;
763 let learning_rate = 0.1;
764 rho = rho + &pauli * Complex64::new(learning_rate * diff / dim as f64, 0.0);
765 }
766 rho = self.make_physical(rho)?;
767 let state = QuantumState::Mixed(rho.clone());
768 let purity_val = purity(&state)?;
769 let total_measurements: usize = measurements.iter().map(|m| m.total_shots()).sum();
770 Ok(TomographyResult {
771 density_matrix: rho,
772 fidelity_estimate: None,
773 purity: purity_val,
774 uncertainty: 1.0 / (total_measurements as f64).sqrt(),
775 total_measurements,
776 })
777 }
778 pub(super) fn basis_to_pauli(
780 &self,
781 basis: &str,
782 ) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
783 let dim = 1 << self.num_qubits;
784 if basis.len() != self.num_qubits {
785 return Err(QuantumInfoError::InvalidState(format!(
786 "Basis string length {} doesn't match qubit count {}",
787 basis.len(),
788 self.num_qubits
789 )));
790 }
791 let mut result: Option<Array2<Complex64>> = None;
792 for c in basis.chars() {
793 let single_qubit = match c {
794 'I' => Array2::from_shape_vec(
795 (2, 2),
796 vec![
797 Complex64::new(1.0, 0.0),
798 Complex64::new(0.0, 0.0),
799 Complex64::new(0.0, 0.0),
800 Complex64::new(1.0, 0.0),
801 ],
802 )
803 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?,
804 'X' => Array2::from_shape_vec(
805 (2, 2),
806 vec![
807 Complex64::new(0.0, 0.0),
808 Complex64::new(1.0, 0.0),
809 Complex64::new(1.0, 0.0),
810 Complex64::new(0.0, 0.0),
811 ],
812 )
813 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?,
814 'Y' => Array2::from_shape_vec(
815 (2, 2),
816 vec![
817 Complex64::new(0.0, 0.0),
818 Complex64::new(0.0, -1.0),
819 Complex64::new(0.0, 1.0),
820 Complex64::new(0.0, 0.0),
821 ],
822 )
823 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?,
824 'Z' => Array2::from_shape_vec(
825 (2, 2),
826 vec![
827 Complex64::new(1.0, 0.0),
828 Complex64::new(0.0, 0.0),
829 Complex64::new(0.0, 0.0),
830 Complex64::new(-1.0, 0.0),
831 ],
832 )
833 .map_err(|e| QuantumInfoError::NumericalError(e.to_string()))?,
834 _ => {
835 return Err(QuantumInfoError::InvalidState(format!(
836 "Unknown basis character: {}",
837 c
838 )));
839 }
840 };
841 result = Some(match result {
842 None => single_qubit,
843 Some(r) => kronecker_product(&r, &single_qubit),
844 });
845 }
846 result.ok_or_else(|| QuantumInfoError::InvalidState("Empty basis string".to_string()))
847 }
848 pub(super) fn make_physical(
850 &self,
851 mut rho: Array2<Complex64>,
852 ) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
853 let dim = rho.nrows();
854 let rho_dag = rho.t().mapv(|c| c.conj());
855 rho = (&rho + &rho_dag) / Complex64::new(2.0, 0.0);
856 let trace: Complex64 = (0..dim).map(|i| rho[[i, i]]).sum();
857 if trace.re.abs() > 1e-10 {
858 rho /= Complex64::new(trace.re, 0.0);
859 }
860 Ok(rho)
861 }
862 pub(super) fn truncate_rank(
864 &self,
865 rho: Array2<Complex64>,
866 max_rank: usize,
867 ) -> std::result::Result<Array2<Complex64>, QuantumInfoError> {
868 Ok(rho)
869 }
870}
871#[derive(Debug, Clone)]
873pub struct ProcessTomographyData {
874 pub state_pairs: Vec<(QuantumState, Array2<Complex64>)>,
876}
877impl ProcessTomographyData {
878 pub fn new() -> Self {
880 Self {
881 state_pairs: Vec::new(),
882 }
883 }
884 pub fn add_pair(&mut self, input: QuantumState, output: Array2<Complex64>) {
886 self.state_pairs.push((input, output));
887 }
888}
889#[derive(Debug, Clone)]
891pub struct StateTomographyConfig {
892 pub shots_per_basis: usize,
894 pub method: TomographyMethod,
896 pub physical_constraints: bool,
898 pub threshold: f64,
900}