quaru/
operation.rs

1//! The `operation` module provides quantum operation
2//! capabilities to a quantum register using matrix representations.
3//!
4//! # Examples
5//! You can explicitly create an [`Operation`](struct@Operation) by
6//! providing a complex matrix and targets to [`Operation::new()`]:
7//!
8//! ```
9//! use ndarray::{array, Array2};
10//! use quaru::operation::Operation;
11//! use quaru::math::real_arr_to_complex;
12//!
13//! let matrix = real_arr_to_complex(array![[1.0, 0.0], [0.0, 1.0]]);
14//! let targets = vec![0];
15//!
16//! let identity: Option<Operation> = Operation::new(matrix, targets);
17//! ```
18//!
19//! If the custom constructed operation is invalid, [`None`] is returned.
20//!
21//! You can avoid this for already pre-defined operations:
22//!
23//! ```
24//! use quaru::operation::{Operation, identity};
25//!
26//! let identity: Operation = identity(0);
27//! ```
28use crate::math::{c64, int_to_state, real_arr_to_complex};
29use ndarray::linalg;
30use ndarray::{array, Array2};
31use std::{f64::consts, vec};
32
33// Naming?
34pub trait QuantumOperation {
35    fn matrix(&self) -> Array2<c64>;
36    fn targets(&self) -> Vec<usize>;
37    fn arity(&self) -> usize;
38}
39
40/// A quantum computer operation represented by a matrix and targets.
41///
42/// - `matrix` corresponds to the quantum operator
43/// - `targets` corresponds to the operator operands
44///
45/// # Note
46/// In order for an operation to be considered valid, the matrix shape must be square
47/// with length equal to the number of operands.
48#[derive(Clone, Debug)]
49pub struct Operation {
50    matrix: Array2<c64>,
51    targets: Vec<usize>,
52}
53
54impl Operation {
55    /// Constructs an operation with the given matrix and targets.
56    ///
57    /// Returns an operation if `matrix` is square with sides equal to number of `targets`.
58    ///
59    /// Otherwise, returns `None`.
60    pub fn new(matrix: Array2<c64>, targets: Vec<usize>) -> Option<Operation> {
61        let shape = matrix.shape();
62        let len = targets.len();
63
64        if shape[0] != 2_usize.pow(len as u32) || shape[1] != 2_usize.pow(len as u32) {
65            return None;
66        }
67
68        Some(Operation { matrix, targets })
69    }
70}
71
72// TODO: Check if we can return references instead?
73impl QuantumOperation for Operation {
74    fn matrix(&self) -> Array2<c64> {
75        self.matrix.clone()
76    }
77
78    fn targets(&self) -> Vec<usize> {
79        self.targets.to_vec()
80    }
81
82    fn arity(&self) -> usize {
83        self.targets().len()
84    }
85}
86
87/// Returns the identity operation for some `target` qubit.
88pub fn identity(target: usize) -> Operation {
89    Operation {
90        matrix: real_arr_to_complex(array![[1.0, 0.0], [0.0, 1.0]]),
91        targets: vec![target],
92    }
93}
94
95/// Returns the hadamard operation for the given `target` qubit.
96///
97/// Creates an equal superposition of the target qubit's basis states.
98pub fn hadamard(target: usize) -> Operation {
99    Operation {
100        matrix: real_arr_to_complex(consts::FRAC_1_SQRT_2 * array![[1.0, 1.0], [1.0, -1.0]]),
101        targets: vec![target],
102    }
103}
104
105/// Returns a hadamard transformation for the given qubit `targets`.
106pub fn hadamard_transform(targets: Vec<usize>) -> Operation {
107    let mut matrix = hadamard(targets[0]).matrix();
108    let len = targets.len();
109
110    for t in targets.iter().take(len).skip(1) {
111        matrix = linalg::kron(&hadamard(*t).matrix(), &matrix);
112    }
113    Operation { matrix, targets }
114}
115
116/// Returns the controlled NOT operation based on the given `control` qubit and
117/// `target` qubit.
118///
119/// Flips the target qubit if and only if the control qubit is |1⟩.
120pub fn cnot(control: usize, target: usize) -> Operation {
121    Operation {
122        matrix: real_arr_to_complex(array![
123            [1.0, 0.0, 0.0, 0.0],
124            [0.0, 1.0, 0.0, 0.0],
125            [0.0, 0.0, 0.0, 1.0],
126            [0.0, 0.0, 1.0, 0.0]
127        ]),
128        targets: vec![target, control],
129    }
130}
131
132/// Create a quantum gate from a function.
133/// The c:th column of the matrix contains f(c) in binary.
134pub fn to_quantum_gate(f: &dyn Fn(usize) -> usize, targets: Vec<usize>) -> Operation {
135    let t_len = targets.len();
136    let len: usize = 1 << t_len;
137    let mut matrix: Array2<c64> = Array2::zeros((len, len));
138    // Loop through the columns
139    for c in 0..len {
140        let val = f(c);
141        let res_state = int_to_state(val, len);
142
143        // Set each cell in the column
144        for r in 0..len {
145            matrix[(r, c)] = res_state[(r, 0)];
146        }
147    }
148    Operation { matrix, targets }
149}
150
151/// Create a controlled version of an operation.
152/// Doubles the width and height of the matrix, put the original matrix
153/// in the bottom right corner and add an identity matrix in the top left corner.
154pub fn to_controlled(op: Operation, control: usize) -> Operation {
155    let old_sz = 1 << op.arity();
156    let mut matrix = Array2::zeros((2 * old_sz, 2 * old_sz));
157    for i in 0..old_sz {
158        matrix[(i, i)] = c64::new(1.0, 0.0);
159    }
160    for i in 0..old_sz {
161        for j in 0..old_sz {
162            matrix[(i + old_sz, j + old_sz)] = op.matrix[(i, j)];
163        }
164    }
165    let mut targets = op.targets();
166
167    // One more target bit: the control.
168    targets.push(control);
169    Operation { matrix, targets }
170}
171
172/// Returns the swap operation for the given target qubits.
173///
174/// Swaps two qubits in the register.
175pub fn swap(target_1: usize, target_2: usize) -> Operation {
176    Operation {
177        matrix: real_arr_to_complex(array![
178            [1.0, 0.0, 0.0, 0.0],
179            [0.0, 0.0, 1.0, 0.0],
180            [0.0, 1.0, 0.0, 0.0],
181            [0.0, 0.0, 0.0, 1.0]
182        ]),
183        targets: vec![target_1, target_2],
184    }
185}
186
187/// Returns the phase operation for the given `target` qubit.
188///
189/// Maps the basis states |0⟩ -> |0⟩ and |1⟩ -> i|1⟩, modifying the
190/// phase of the quantum state.
191pub fn phase(target: usize) -> Operation {
192    Operation {
193        matrix: array![
194            [c64::new(1.0, 0.0), c64::new(0.0, 0.0)],
195            [c64::new(0.0, 0.0), c64::new(0.0, 1.0)]
196        ],
197        targets: vec![target],
198    }
199}
200
201/// Returns the NOT operation for the given `target` qubit.
202///
203/// Maps the basis states |0⟩ -> |1⟩ and |1⟩ -> |0⟩.
204///
205/// Also referred to as the Pauli-X operation.
206pub fn not(target: usize) -> Operation {
207    Operation {
208        matrix: real_arr_to_complex(array![[0.0, 1.0], [1.0, 0.0]]),
209        targets: vec![target],
210    }
211}
212
213/// Returns the Pauli-Y operation for a given `target` qubit.
214///
215/// Maps the basis states |0⟩ -> i|1⟩ and |1⟩ -> -i|0⟩.
216pub fn pauli_y(target: usize) -> Operation {
217    Operation {
218        matrix: array![
219            [c64::new(0.0, 0.0), c64::new(0.0, -1.0)],
220            [c64::new(0.0, 1.0), c64::new(0.0, 0.0)]
221        ],
222        targets: vec![target],
223    }
224}
225
226/// Returns the Pauli-Z operation for a given `target` qubit.
227///
228/// Maps the basis states |0⟩ -> |0⟩ and |1⟩ -> -|1⟩
229pub fn pauli_z(target: usize) -> Operation {
230    Operation {
231        matrix: real_arr_to_complex(array![[1.0, 0.0], [0.0, -1.0]]),
232        targets: vec![target],
233    }
234}
235
236/// Returns the controlled NOT operation for the given number of `control` qubits on the `target` qubit.
237///
238/// Flips the target qubit if and only if controls are |1⟩.
239pub fn cnx(controls: &[usize], target: usize) -> Operation {
240    let mut targets = vec![target];
241    targets.append(&mut controls.to_owned());
242
243    // Calculates the size of the matrix (2^n) where n is the number of target + control qubits
244    let n: usize = 2_usize.pow(targets.len() as u32);
245
246    // Creates an empty (2^n * 2^n) matrix and starts to fill it in as an identity matrix
247    let mut matrix: Array2<f64> = Array2::<f64>::zeros((n, n));
248    for i in 0..n - 2 {
249        // Does not fill in the last two rows
250        matrix.row_mut(i)[i] = 1.0;
251    }
252
253    // The last two rows are to be "swapped", finalizing the not part of the matrix
254    matrix.row_mut(n - 1)[n - 2] = 1.0;
255    matrix.row_mut(n - 2)[n - 1] = 1.0;
256
257    Operation {
258        matrix: real_arr_to_complex(matrix),
259        targets,
260    }
261}
262
263/// Returns a controlled Pauli-Z operation for the given number of `control` qubits on the `target`
264/// qubit.
265///
266/// Maps the basis states of the target to |0⟩ -> |0⟩ and |1⟩ -> -|1⟩ if and only if the controls
267/// are |1⟩.
268pub fn cnz(controls: &[usize], target: usize) -> Operation {
269    let mut targets = vec![target];
270    targets.append(&mut controls.to_owned());
271
272    let n: usize = 2_usize.pow(targets.len() as u32);
273
274    let mut matrix: Array2<f64> = Array2::<f64>::zeros((n, n));
275    for i in 0..n - 1 {
276        matrix.row_mut(i)[i] = 1.0;
277    }
278    matrix.row_mut(n - 1)[n - 1] = -1.0;
279
280    Operation {
281        matrix: real_arr_to_complex(matrix),
282        targets,
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::{
289        cnot, cnx, hadamard, identity, not, pauli_y, pauli_z, phase, swap, QuantumOperation,
290    };
291    use crate::math::c64;
292    use ndarray::Array2;
293
294    fn all_ops() -> Vec<Box<dyn QuantumOperation>> {
295        vec![
296            Box::new(identity(0)),
297            Box::new(hadamard(0)),
298            Box::new(cnot(0, 1)),
299            Box::new(swap(0, 1)),
300            Box::new(phase(0)),
301            Box::new(not(0)),
302            Box::new(pauli_y(0)),
303            Box::new(pauli_z(0)),
304            Box::new(cnx(&[0], 1)),
305            Box::new(cnx(&[0, 1], 2)),
306            Box::new(cnx(&[0, 1, 2], 3)),
307            Box::new(cnx(&[0, 1, 2, 3], 4)),
308            Box::new(cnx(&[0, 1, 2, 3, 4], 5)),
309        ]
310    }
311
312    #[test]
313    fn sz_matches() {
314        for op in all_ops() {
315            assert_eq!(op.matrix().dim().0, op.matrix().dim().1);
316            assert_eq!(op.matrix().dim().0, 1 << op.arity())
317        }
318    }
319
320    #[test]
321    fn unitary() {
322        // This also guarantees preservation of total probability
323        for op in all_ops() {
324            let conj_transpose = op.matrix().t().map(|e| e.conj());
325            assert!(matrix_is_equal(
326                op.matrix().dot(&conj_transpose),
327                Array2::eye(op.matrix().dim().0),
328                1e-8
329            ))
330        }
331    }
332
333    #[test]
334    fn toffoli2_equals_cnot() {
335        let toffoli_generated_cnot = cnx(&[0], 1);
336        assert!(matrix_is_equal(
337            toffoli_generated_cnot.matrix(),
338            cnot(0, 1).matrix(),
339            1e-8
340        ));
341    }
342
343    fn matrix_is_equal(a: Array2<c64>, b: Array2<c64>, tolerance: f64) -> bool {
344        (a - b).iter().all(|e| e.norm() < tolerance)
345    }
346}