qdk_sim/processes/
apply.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Public implementations and crate-private functions for applying processes
5//! in each different representation.
6
7use itertools::Itertools;
8use ndarray::{Array, Array2, Array3, ArrayView2, Axis};
9use rand::{distributions::WeightedIndex, prelude::Distribution, thread_rng};
10
11use crate::{
12    chp_decompositions::ChpOperation, linalg::ConjBy, log, log_as_err, Pauli, Process,
13    ProcessData::*, State, StateData::*, Tableau, C64,
14};
15
16use super::promote_pauli_channel;
17
18impl Process {
19    /// Applies this process to a quantum register with a given
20    /// state, returning the new state of that register.
21    pub fn apply(&self, state: &State) -> Result<State, String> {
22        if state.n_qubits != self.n_qubits {
23            return Err(format!(
24                "Channel acts on {} qubits, but was applied to {}-qubit state.",
25                self.n_qubits, state.n_qubits
26            ));
27        }
28
29        match &self.data {
30            Unitary(u) => apply_unitary(u, state),
31            KrausDecomposition(ks) => apply_kraus_decomposition(ks, state),
32            MixedPauli(paulis) => apply_pauli_channel(paulis, state),
33            Sequence(processes) => {
34                // TODO[perf]: eliminate the extraneous clone here.
35                let mut acc_state = state.clone();
36                for process in processes {
37                    acc_state = process.apply(state)?;
38                }
39                Ok(acc_state)
40            }
41            ChpDecomposition(_operations) => todo!(),
42            Unsupported => Err("Unsupported quantum process.".to_string()),
43        }
44    }
45
46    /// Applies this process to the given qubits in a register with a given
47    /// state, returning the new state of that register.
48    pub fn apply_to(&self, idx_qubits: &[usize], state: &State) -> Result<State, String> {
49        // If we have a sequence, we can apply each in turn and exit early.
50        if let Sequence(channels) = &self.data {
51            // TODO[perf]: eliminate the extraneous clone here.
52            let mut acc_state = state.clone();
53            for channel in channels {
54                acc_state = channel.apply_to(idx_qubits, &acc_state)?;
55            }
56            return Ok(acc_state);
57        }
58
59        // Fail if there's not enough qubits.
60        if state.n_qubits < self.n_qubits {
61            return log_as_err(format!(
62                "Channel acts on {} qubits, but a state on only {} qubits was given.\n\nChannel:\n{:?}\n\nState:\n{:?}",
63                self.n_qubits, state.n_qubits, self, state
64            ));
65        }
66
67        // Fail if any indices are repeated.
68        if idx_qubits.iter().unique().count() < idx_qubits.len() {
69            return log_as_err(format!(
70                "List of qubit indices {:?} contained repeated elements.",
71                idx_qubits
72            ));
73        }
74
75        // Make sure that there are only as many indices as qubits that this
76        // channel acts upon.
77        if idx_qubits.len() != self.n_qubits {
78            return log_as_err(format!(
79                "Qubit indices were specified as {:?}, but this channel only acts on {} qubits.",
80                idx_qubits, self.n_qubits
81            ));
82        }
83
84        // At this point we know that idx_qubits has self.n_qubits many unique
85        // indices, such that we can meaningfully apply the channel to the
86        // qubits described by idx_qubits.
87        //
88        // To do so in general, we can proceed to make a new channel
89        // that expands this channel to act on the full register and then use
90        // the ordinary apply method.
91        //
92        // In some cases, however, we can do so more efficiently by working
93        // with the small channel directly, so we check for those cases first
94        // before falling through to the general case.
95
96        // TODO[perf]: For larger systems, we could add another "fast path" using
97        //             matrix multiplication kernels to avoid extending
98        //             channels to larger Hilbert spaces.
99        //             For smaller systems, extending channels and possibly
100        //             caching them is likely to be more performant; need to
101        //             tune to find crossover point.
102        if let ChpDecomposition(operations) = &self.data {
103            if let Stabilizer(tableau) = &state.data {
104                return apply_chp_decomposition_to(operations, state.n_qubits, idx_qubits, tableau);
105            }
106        }
107
108        // Having tried fast paths above, we now fall back to the most general
109        // case.
110        match self.n_qubits {
111            1 => {
112                if state.n_qubits == 1 {
113                    self.apply(state)
114                } else {
115                    self.extend_one_to_n(idx_qubits[0], state.n_qubits)
116                        .apply(state)
117                }
118            }
119            // TODO[perf]: If the size of the register matches the size of the
120            //             channel, permute rather than expanding.
121            2 => self
122                .extend_two_to_n(idx_qubits[0], idx_qubits[1], state.n_qubits)
123                .apply(state),
124            _ => {
125                log(&format!(
126                    "Expanding {}-qubit channels is not yet implemented.",
127                    self.n_qubits
128                ));
129                unimplemented!("");
130            }
131        }
132    }
133}
134
135fn apply_chp_decomposition_to(
136    operations: &[ChpOperation],
137    n_qubits: usize,
138    idx_qubits: &[usize],
139    tableau: &Tableau,
140) -> Result<State, String> {
141    let mut new_tableau = tableau.clone();
142    for operation in operations {
143        match *operation {
144            ChpOperation::Phase(idx) => new_tableau.apply_s_mut(idx_qubits[idx]),
145            ChpOperation::AdjointPhase(idx) => new_tableau.apply_s_adj_mut(idx_qubits[idx]),
146            ChpOperation::Hadamard(idx) => new_tableau.apply_h_mut(idx_qubits[idx]),
147            ChpOperation::Cnot(idx_control, idx_target) => {
148                new_tableau.apply_cnot_mut(idx_qubits[idx_control], idx_qubits[idx_target])
149            }
150        };
151    }
152    Ok(State {
153        n_qubits,
154        data: Stabilizer(new_tableau),
155    })
156}
157
158pub(crate) fn apply_unitary(u: &Array2<C64>, state: &State) -> Result<State, String> {
159    Ok(State {
160        n_qubits: state.n_qubits,
161        data: match &state.data {
162            Pure(psi) => Pure(u.dot(psi)),
163            Mixed(rho) => Mixed(rho.conjugate_by(&u.into())),
164            Stabilizer(_tableau) => {
165                return Err(
166                    "TODO: Promote stabilizer state to state vector and recurse.".to_string(),
167                )
168            }
169        },
170    })
171}
172
173pub(crate) fn apply_kraus_decomposition(ks: &Array3<C64>, state: &State) -> Result<State, String> {
174    Ok(State {
175        n_qubits: state.n_qubits,
176        data: match &state.data {
177            Pure(psi) => {
178                // We can't apply a channel with more than one Kraus operator (Choi rank > 1) to a
179                // pure state directly, so if the Choi rank is bigger than 1, promote to
180                // Mixed and recurse.
181                if ks.shape()[0] == 1 {
182                    Pure({
183                        let k: ArrayView2<C64> = ks.slice(s![0, .., ..]);
184                        k.dot(psi)
185                    })
186                } else {
187                    apply_kraus_decomposition(ks, &state.to_mixed())?.data
188                }
189            }
190            Mixed(rho) => Mixed({
191                let mut sum: Array2<C64> = Array::zeros((rho.shape()[0], rho.shape()[1]));
192                for k in ks.axis_iter(Axis(0)) {
193                    sum = sum + rho.conjugate_by(&k);
194                }
195                sum
196            }),
197            Stabilizer(_tableau) => {
198                return Err(
199                    "TODO: Promote stabilizer state to state vector and recurse.".to_string(),
200                )
201            }
202        },
203    })
204}
205
206pub(crate) fn apply_pauli_channel(
207    paulis: &[(f64, Vec<Pauli>)],
208    state: &State,
209) -> Result<State, String> {
210    Ok(State {
211        n_qubits: state.n_qubits,
212        data: match &state.data {
213            Pure(_) | Mixed(_) => {
214                // Promote and recurse.
215                let promoted = promote_pauli_channel(paulis);
216                return promoted.apply(state);
217            }
218            Stabilizer(tableau) => {
219                // TODO[perf]: Introduce an apply_mut method to
220                //             avoid extraneous cloning.
221                let mut new_tableau = tableau.clone();
222                // Sample a Pauli and apply it.
223                let weighted = WeightedIndex::new(paulis.iter().map(|(pr, _)| pr)).unwrap();
224                let idx = weighted.sample(&mut thread_rng());
225                let pauli = &(&paulis)[idx].1;
226                // TODO: Consider moving the following to a method
227                //       on Tableau itself.
228                for (idx_qubit, p) in pauli.iter().enumerate() {
229                    match p {
230                        Pauli::I => (),
231                        Pauli::X => new_tableau.apply_x_mut(idx_qubit),
232                        Pauli::Y => new_tableau.apply_y_mut(idx_qubit),
233                        Pauli::Z => new_tableau.apply_z_mut(idx_qubit),
234                    }
235                }
236                Stabilizer(new_tableau)
237            }
238        },
239    })
240}