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}