qdk_sim/instrument.rs
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use crate::{states::StateData::Mixed, StateData};
5use crate::{Process, ProcessData, C64};
6use num_traits::{One, Zero};
7use rand::Rng;
8use std::iter::Iterator;
9
10use crate::linalg::Trace;
11use crate::State;
12
13use serde::{Deserialize, Serialize};
14
15// TODO[design]: Instrument works pretty differently from State and Process; should
16// likely refactor for consistency.
17
18#[derive(Serialize, Deserialize, Debug)]
19/// Represents a quantum instrument; that is, a process that accepts a quantum
20/// state and returns the new state of a system and classical data extracted
21/// from that system.
22pub enum Instrument {
23 /// The effects of the instrument, represented as completely positive
24 /// trace non-increasing (CPTNI) processes.
25 Effects(Vec<Process>),
26
27 /// An instrument that measures a single qubit in the $Z$-basis, up to a
28 /// readout error (probability of result being flipped).
29 ///
30 /// Primarily useful when working with stabilizer states or other
31 /// subtheories.
32 ZMeasurement {
33 /// Probability with which a result is flipped.
34 pr_readout_error: f64,
35 },
36}
37
38impl Instrument {
39 /// Samples from this instrument, returning the measurement result and
40 /// the new state of the system conditioned on that measurement result.
41 pub fn sample(&self, idx_qubits: &[usize], state: &State) -> (usize, State) {
42 match self {
43 Instrument::Effects(ref effects) => sample_effects(effects, idx_qubits, state),
44 Instrument::ZMeasurement { pr_readout_error } => {
45 if idx_qubits.len() != 1 {
46 panic!("Z-basis measurement instruments only supported for single qubits.");
47 }
48 let idx_target = idx_qubits[0];
49 match state.data {
50 StateData::Pure(_) | StateData::Mixed(_) => {
51 // Get the ideal Z measurement instrument, apply it,
52 // and then assign a readout error.
53 // TODO[perf]: Cache this instrument as a lazy static.
54 let ideal_z_meas = Instrument::Effects(vec![
55 Process {
56 n_qubits: 1,
57 data: ProcessData::KrausDecomposition(array![[
58 [C64::one(), C64::zero()],
59 [C64::zero(), C64::zero()]
60 ]]),
61 },
62 Process {
63 n_qubits: 1,
64 data: ProcessData::KrausDecomposition(array![[
65 [C64::zero(), C64::zero()],
66 [C64::zero(), C64::one()]
67 ]]),
68 },
69 ]);
70 let (result, new_state) = ideal_z_meas.sample(idx_qubits, state);
71 let result = (result == 1) ^ rand::thread_rng().gen_bool(*pr_readout_error);
72 (if result { 1 } else { 0 }, new_state)
73 }
74 StateData::Stabilizer(ref tableau) => {
75 // TODO[perf]: allow instruments to sample in-place,
76 // reducing copying.
77 let mut new_tableau = tableau.clone();
78 let result = new_tableau.meas_mut(idx_target)
79 ^ rand::thread_rng().gen_bool(*pr_readout_error);
80 (
81 if result { 1 } else { 0 },
82 State {
83 n_qubits: state.n_qubits,
84 data: StateData::Stabilizer(new_tableau),
85 },
86 )
87 }
88 }
89 }
90 }
91 }
92
93 // TODO: Add more methods for making new instruments in convenient ways.
94
95 /// Returns a serialization of this instrument as a JSON object.
96 pub fn as_json(&self) -> String {
97 serde_json::to_string(&self).unwrap()
98 }
99}
100
101fn sample_effects(effects: &[Process], idx_qubits: &[usize], state: &State) -> (usize, State) {
102 let mut possible_outcomes = effects
103 .iter()
104 .enumerate()
105 .map(|(idx, effect)| {
106 let output_state = effect.apply_to(idx_qubits, state).unwrap();
107 let tr = (&output_state).trace();
108 (idx, output_state, tr.norm())
109 })
110 .collect::<Vec<_>>();
111 // TODO[perf]: Downgrade this to a debug_assert!, and configure the CI
112 // build to enable debug_assertions at full_validation.
113 assert!(
114 possible_outcomes.iter().any(|post_state| post_state.1.trace().norm() >= 1e-10),
115 "Expected output of applying instrument to be nonzero trace.\nInstrument effects:\n{:?}\n\nInput state:\n{}\n\nPostselected states:\n{:?}",
116 effects, state, possible_outcomes
117 );
118 let mut rng = rand::thread_rng();
119 let random_sample: f64 = rng.gen();
120 for (idx, cum_pr) in possible_outcomes
121 .iter()
122 .scan(0.0f64, |acc, (_idx, _, pr)| {
123 *acc += *pr;
124 Some(*acc)
125 })
126 .enumerate()
127 {
128 if random_sample < cum_pr {
129 // In order to not have to copy the output state, we need
130 // to be able to move it out from the vector. To do so,
131 // we retain only the element of the vector whose index
132 // is the one we want and then pop it, leaving an empty
133 // vector (that is, a vector that owns no data).
134 possible_outcomes.retain(|(i, _, _)| idx == *i);
135 let (_, mut output_state, tr) = possible_outcomes.pop().unwrap();
136 if tr.abs() >= 1e-10 {
137 if let Mixed(ref rho) = output_state.data {
138 output_state.data = Mixed(rho * (1.0f64 / tr));
139 } else {
140 panic!("Couldn't renormalize, expected mixed output from instrument.");
141 }
142 }
143 assert!(
144 (output_state.trace() - 1.0).norm() <= 1e-10,
145 "Expected output of instrument to be trace 1."
146 );
147 return (idx, output_state);
148 }
149 }
150 let (idx, output_state, _) = possible_outcomes.pop().unwrap();
151 (idx, output_state)
152}