qdk_sim/processes/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4mod apply;
5
6use crate::chp_decompositions::ChpOperation;
7use crate::linalg::{extend_one_to_n, extend_two_to_n, zeros_like};
8use crate::processes::ProcessData::{KrausDecomposition, MixedPauli, Unitary};
9use crate::NoiseModel;
10use crate::QubitSized;
11use crate::C64;
12use crate::{AsUnitary, Pauli};
13use itertools::Itertools;
14use ndarray::{Array, Array2, Array3, Axis, NewAxis};
15use num_complex::Complex;
16use num_traits::{One, Zero};
17use serde::{Deserialize, Serialize};
18use std::convert::TryInto;
19use std::ops::Add;
20use std::ops::Mul;
21
22/// A linear function from quantum states to quantum states.
23///
24/// # Remarks
25/// A process that is completely positive and trace preserving is a channel.
26pub type Process = QubitSized<ProcessData>;
27
28/// Data used to represent a given process.
29#[derive(Clone, Debug, Serialize, Deserialize)]
30pub enum ProcessData {
31    /// Representation of a process as a mixture of Pauli operators
32    /// $\{(p_i, P_i)\}$ such that the channel acts as $\rho \mapsto
33    /// \sum_i p_i P_i \rho P_i^{\dagger}$.
34    MixedPauli(Vec<(f64, Vec<Pauli>)>),
35
36    /// Representation of the process by an arbitrary unitary matrix.
37    Unitary(Array2<C64>),
38
39    /// Representation of the process by the singular vectors of its Choi
40    /// representation (colloquially, the Kraus decomposition).
41    ///
42    /// The first index denotes each Kraus operator, with the second and third
43    /// indices representing the indices of each operator.
44    KrausDecomposition(Array3<C64>), // TODO: Superoperator and Choi reps.
45
46    /// Representation of a process as a sequence of other processes.
47    Sequence(Vec<Process>),
48
49    /// Representation of a Clifford operation in terms of a decomposition
50    /// into CNOT, Hadamard, and phase operations.
51    ChpDecomposition(Vec<ChpOperation>),
52
53    /// Represents a process that is not supported by a given noise model,
54    /// and thus always fails when applied.
55    Unsupported,
56}
57
58impl Process {
59    /// Returns a new Pauli channel, given a mixture of Pauli operators.
60    pub fn new_pauli_channel<T: IntoPauliMixture>(data: T) -> Self {
61        let data = data.into_pauli_mixture();
62        // How many qubits?
63        // TODO: check that every Pauli is supported on the same number of
64        //       qubits.
65        let n_qubits = data[0].1.len();
66        Process {
67            n_qubits,
68            data: MixedPauli(data),
69        }
70    }
71    // TODO: methods to forcibly convert representations.
72
73    /// Returns a serialization of this quantum process as a JSON object.
74    pub fn as_json(&self) -> String {
75        serde_json::to_string(&self).unwrap()
76    }
77
78    /// Returns a copy of this process that applies to registers of a given
79    /// size.
80    pub fn extend_one_to_n(&self, idx_qubit: usize, n_qubits: usize) -> Process {
81        assert_eq!(self.n_qubits, 1);
82        Process {
83            n_qubits,
84            data: match &self.data {
85                Unitary(u) => Unitary(extend_one_to_n(u.view(), idx_qubit, n_qubits)),
86                KrausDecomposition(ks) => {
87                    let new_dim = 2usize.pow(n_qubits.try_into().unwrap());
88                    let n_kraus = ks.shape()[0];
89                    let mut extended: Array3<C64> = Array::zeros((n_kraus, new_dim, new_dim));
90                    for (idx_kraus, kraus) in ks.axis_iter(Axis(0)).enumerate() {
91                        let mut target = extended.index_axis_mut(Axis(0), idx_kraus);
92                        let big_kraus = extend_one_to_n(kraus.view(), idx_qubit, n_qubits);
93                        target.assign(&big_kraus);
94                    }
95                    KrausDecomposition(extended)
96                },
97                MixedPauli(paulis) => MixedPauli(
98                    paulis.iter()
99                        .map(|(pr, pauli)| {
100                            if pauli.len() != 1 {
101                                panic!("Pauli channel acts on more than one qubit, cannot extend to 𝑛 qubits.");
102                            }
103                            let p = pauli[0];
104                            let mut extended = std::iter::repeat(Pauli::I).take(n_qubits).collect_vec();
105                            extended[idx_qubit] = p;
106                            (*pr, extended)
107                        })
108                        .collect_vec()
109                ),
110                ProcessData::Unsupported => ProcessData::Unsupported,
111                ProcessData::Sequence(processes) => ProcessData::Sequence(
112                    processes.iter().map(|p| p.extend_one_to_n(idx_qubit, n_qubits)).collect()
113                ),
114                ProcessData::ChpDecomposition(_) => todo!(),
115            },
116        }
117    }
118
119    /// Returns a copy of this process that applies to registers of a given
120    /// size.
121    pub fn extend_two_to_n(
122        &self,
123        idx_qubit1: usize,
124        idx_qubit2: usize,
125        n_qubits: usize,
126    ) -> Process {
127        assert_eq!(self.n_qubits, 2);
128        Process {
129            n_qubits,
130            data: match &self.data {
131                Unitary(u) => Unitary(extend_two_to_n(u.view(), idx_qubit1, idx_qubit2, n_qubits)),
132                KrausDecomposition(ks) => {
133                    // TODO: consolidate with extend_one_to_n, above.
134                    let new_dim = 2usize.pow(n_qubits.try_into().unwrap());
135                    let n_kraus = ks.shape()[0];
136                    let mut extended: Array3<C64> = Array::zeros((n_kraus, new_dim, new_dim));
137                    for (idx_kraus, kraus) in ks.axis_iter(Axis(0)).enumerate() {
138                        let mut target = extended.index_axis_mut(Axis(0), idx_kraus);
139                        let big_kraus = extend_two_to_n(kraus, idx_qubit1, idx_qubit2, n_qubits);
140                        target.assign(&big_kraus);
141                    }
142                    KrausDecomposition(extended)
143                },
144                MixedPauli(paulis) => MixedPauli(
145                    paulis.iter()
146                        .map(|(pr, pauli)| {
147                            if pauli.len() != 2 {
148                                panic!("Pauli channel acts on more than one qubit, cannot extend to 𝑛 qubits.");
149                            }
150                            let p = (pauli[0], pauli[1]);
151                            let mut extended = std::iter::repeat(Pauli::I).take(n_qubits).collect_vec();
152                            extended[idx_qubit1] = p.0;
153                            extended[idx_qubit2] = p.1;
154                            (*pr, extended)
155                        })
156                        .collect_vec()
157                ),
158                ProcessData::Unsupported => ProcessData::Unsupported,
159                ProcessData::Sequence(processes) => ProcessData::Sequence(
160                    processes.iter().map(|p| p.extend_two_to_n(idx_qubit1, idx_qubit2, n_qubits)).collect()
161                ),
162                ProcessData::ChpDecomposition(_) => todo!(),
163            },
164        }
165    }
166}
167
168impl Mul<&Process> for C64 {
169    type Output = Process;
170
171    fn mul(self, channel: &Process) -> Self::Output {
172        Process {
173            n_qubits: channel.n_qubits,
174            data: match &channel.data {
175                // Note that we need to multiply by the square root in
176                // both cases, since these representations are both in terms
177                // of linear operators, but the multiplication is on
178                // superoperators (two copies of the original vectorspace).
179                Unitary(u) => KrausDecomposition({
180                    let mut ks = Array3::<C64>::zeros((1, u.shape()[0], u.shape()[1]));
181                    ks.index_axis_mut(Axis(0), 0).assign(&(self.sqrt() * u));
182                    ks
183                }),
184                KrausDecomposition(ks) => KrausDecomposition(self.sqrt() * ks),
185                MixedPauli(paulis) => (self * promote_pauli_channel(paulis)).data,
186                ProcessData::Unsupported => ProcessData::Unsupported,
187                ProcessData::Sequence(processes) => {
188                    ProcessData::Sequence(processes.iter().map(|p| self * p).collect())
189                }
190                ProcessData::ChpDecomposition(_) => todo!(),
191            },
192        }
193    }
194}
195
196impl Mul<Process> for C64 {
197    type Output = Process;
198    fn mul(self, channel: Process) -> Self::Output {
199        self * (&channel)
200    }
201}
202
203impl Mul<&Process> for f64 {
204    type Output = Process;
205    fn mul(self, chanel: &Process) -> Self::Output {
206        C64::new(self, 0f64) * chanel
207    }
208}
209
210impl Mul<Process> for f64 {
211    type Output = Process;
212    fn mul(self, channel: Process) -> Self::Output {
213        self * (&channel)
214    }
215}
216
217// Base case: both channels that we're composing are borrowed.
218impl Mul<&Process> for &Process {
219    type Output = Process;
220
221    fn mul(self, rhs: &Process) -> Self::Output {
222        assert_eq!(self.n_qubits, rhs.n_qubits);
223        Process {
224            n_qubits: self.n_qubits,
225            data: match (&self.data, &rhs.data) {
226                (Unitary(u), Unitary(v)) => Unitary(u.dot(v)),
227                (Unitary(u), KrausDecomposition(ks)) => {
228                    // post-multiply each kraus operator by u.
229                    let mut post = zeros_like(ks);
230                    for (idx_kraus, kraus) in ks.axis_iter(Axis(0)).enumerate() {
231                        post.index_axis_mut(Axis(0), idx_kraus)
232                            .assign(&u.dot(&kraus));
233                    }
234                    KrausDecomposition(post)
235                }
236                // TODO: product of two kraus decompositions would be... not
237                //       fun.
238                _ => todo!(),
239            },
240        }
241    }
242}
243
244impl Add<&Process> for &Process {
245    type Output = Process;
246
247    fn add(self, rhs: &Process) -> Self::Output {
248        assert_eq!(self.n_qubits, rhs.n_qubits);
249        Process {
250            n_qubits: self.n_qubits,
251            data: match (&self.data, &rhs.data) {
252                (KrausDecomposition(ks1), KrausDecomposition(ks2)) => {
253                    let mut sum = Array::zeros([
254                        ks1.shape()[0] + ks2.shape()[0],
255                        ks1.shape()[1],
256                        ks1.shape()[2],
257                    ]);
258                    for (idx_kraus, kraus) in ks1.axis_iter(Axis(0)).enumerate() {
259                        sum.index_axis_mut(Axis(0), idx_kraus).assign(&kraus);
260                    }
261                    for (idx_kraus, kraus) in ks2.axis_iter(Axis(0)).enumerate() {
262                        sum.index_axis_mut(Axis(0), ks1.shape()[0] + idx_kraus)
263                            .assign(&kraus);
264                    }
265                    KrausDecomposition(sum)
266                }
267                _ => todo!(),
268            },
269        }
270    }
271}
272
273impl Mul<Process> for &Process {
274    type Output = Process;
275
276    fn mul(self, rhs: Process) -> Self::Output {
277        self * &rhs
278    }
279}
280
281impl Mul<&Process> for Process {
282    type Output = Process;
283
284    fn mul(self, rhs: &Process) -> Self::Output {
285        &self * rhs
286    }
287}
288
289impl Mul<Process> for Process {
290    type Output = Process;
291
292    fn mul(self, rhs: Process) -> Self::Output {
293        &self * &rhs
294    }
295}
296
297impl Add<Process> for &Process {
298    type Output = Process;
299
300    fn add(self, rhs: Process) -> Self::Output {
301        self + &rhs
302    }
303}
304
305impl Add<&Process> for Process {
306    type Output = Process;
307
308    fn add(self, rhs: &Process) -> Self::Output {
309        &self + rhs
310    }
311}
312
313impl Add<Process> for Process {
314    type Output = Process;
315
316    fn add(self, rhs: Process) -> Self::Output {
317        &self + &rhs
318    }
319}
320
321/// Returns a copy of a depolarizing channel of a given strength (that is, a
322/// channel representing relaxation to the maximally mixed state).
323pub fn depolarizing_channel(p: f64) -> Process {
324    let ideal = NoiseModel::ideal();
325    (1.0 - p) * ideal.i + p / 3.0 * ideal.x + p / 3.0 * ideal.y + p / 3.0 * ideal.z
326}
327
328/// Returns a copy of an amplitude damping channel of a given strength (that is,
329/// a channel representing relaxation to the $\ket{0}$ state in a
330/// characteristic time given by $1 / \gamma$).
331pub fn amplitude_damping_channel(gamma: f64) -> Process {
332    Process {
333        n_qubits: 1,
334        data: KrausDecomposition(array![
335            [
336                [C64::one(), C64::zero()],
337                [C64::zero(), C64::one() * (1.0 - gamma).sqrt()]
338            ],
339            [
340                [C64::zero(), C64::one() * gamma.sqrt()],
341                [C64::zero(), C64::zero()]
342            ]
343        ]),
344    }
345}
346
347/// A type that can be converted into a mixture of Pauli operators.
348pub trait IntoPauliMixture {
349    /// Convert this value into a mixture of multi-qubit Pauli operators.
350    fn into_pauli_mixture(self) -> Vec<(f64, Vec<Pauli>)>;
351}
352
353impl IntoPauliMixture for Vec<(f64, Vec<Pauli>)> {
354    fn into_pauli_mixture(self) -> Vec<(f64, Vec<Pauli>)> {
355        self
356    }
357}
358
359impl IntoPauliMixture for Vec<Pauli> {
360    fn into_pauli_mixture(self) -> Vec<(f64, Vec<Pauli>)> {
361        vec![(1.0, self)]
362    }
363}
364
365impl IntoPauliMixture for Vec<(f64, Pauli)> {
366    fn into_pauli_mixture(self) -> Vec<(f64, Vec<Pauli>)> {
367        self.iter().map(|(pr, p)| (*pr, vec![*p])).collect_vec()
368    }
369}
370
371impl IntoPauliMixture for Pauli {
372    fn into_pauli_mixture(self) -> Vec<(f64, Vec<Pauli>)> {
373        vec![(1.0, vec![self])]
374    }
375}
376
377/// Given a vector of Paulis and the probability of applying each one, promotes
378/// that vector to a process that acts on state vectors (e.g.: a unitary matrix,
379/// or Kraus decomposition).
380///
381/// This function is a private utility mainly used in handling the case where
382/// a mixed Pauli channel is applied to a pure or mixed state.
383fn promote_pauli_channel(paulis: &[(f64, Vec<Pauli>)]) -> Process {
384    // TODO: Check that there's at least one Pauli... empty vectors aren't
385    //       supported here.
386    if paulis.len() == 1 {
387        // Just one Pauli, so can box it up into a unitary.
388        let (_, pauli) = &paulis[0];
389        // TODO[testing]: check that pr is 1.0.
390        Process {
391            n_qubits: pauli.len(),
392            data: Unitary(pauli.as_unitary()),
393        }
394    } else {
395        // To turn a mixed Pauli channel into a Kraus decomposition, we need
396        // to take the square root of each probability.
397        let matrices = paulis
398            .iter()
399            .map(|p| {
400                p.1.as_unitary()
401                    * Complex {
402                        re: p.0.sqrt(),
403                        im: 0.0f64,
404                    }
405            })
406            .map(|u| {
407                let x = u.slice(s![NewAxis, .., ..]);
408                x.to_owned()
409            })
410            .collect_vec();
411        Process {
412            n_qubits: paulis[0].1.len(),
413            data: KrausDecomposition(
414                ndarray::concatenate(
415                    Axis(0),
416                    matrices.iter().map(|u| u.view()).collect_vec().as_slice(),
417                )
418                .unwrap(),
419            ),
420        }
421    }
422}