qdk_sim/
linalg.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Provides common linear algebra functions and traits.
5
6use ndarray::{Array, Array1, Array2, ArrayView1, ArrayView2};
7use num_traits::Zero;
8use std::{convert::TryInto, ops::Mul};
9
10use crate::{common_matrices::nq_eye, C64};
11
12/// Represents types that have hermitian conjugates (e.g.: $A^\dagger$ for
13/// a matrix $A$ is defined as the complex conjugate transpose of $A$,
14/// $(A^\dagger)\_{ij} = A\_{ji}^*$).
15pub trait HasDagger {
16    /// The type of the hermitian conjugate.
17    type Output;
18
19    /// Returns the hermitian conjugate (colloquially, the dagger) of a
20    /// borrowed reference as a new copy.
21    ///
22    /// For most types implementing this trait, the hermitian conjugate
23    /// is represented by the conjugate transpose.
24    fn dag(&self) -> Self::Output;
25}
26
27impl HasDagger for Array2<C64> {
28    type Output = Self;
29
30    fn dag(&self) -> Self {
31        self.t().map(|element| element.conj())
32    }
33}
34
35impl HasDagger for ArrayView2<'_, C64> {
36    type Output = Array2<C64>;
37
38    fn dag(&self) -> Self::Output {
39        self.t().map(|element| element.conj())
40    }
41}
42
43/// Represent types that can be conjugated by 2-dimensional arrays; that is,
44/// as $UXU^{\dagger}$.
45pub trait ConjBy {
46    /// Conjugates this value by a given matrix, returning a copy.
47    fn conjugate_by(&self, op: &ArrayView2<C64>) -> Self;
48}
49
50impl ConjBy for Array2<C64> {
51    fn conjugate_by(&self, op: &ArrayView2<C64>) -> Self {
52        op.dot(self).dot(&op.dag())
53    }
54}
55
56/// The tensor product operator ($\otimes$).
57pub trait Tensor<Rhs = Self> {
58    /// The resulting type after applying the tensor product.
59    type Output;
60
61    /// Performs the tensor product.
62    ///
63    /// # Example
64    /// ```
65    /// // TODO
66    /// ```
67    fn tensor(self, rhs: Rhs) -> Self::Output;
68}
69
70impl<Other: Into<Self>, T: Copy + Mul<Output = T>> Tensor<Other> for ArrayView1<'_, T> {
71    type Output = Array1<T>;
72    fn tensor(self, other: Other) -> Self::Output {
73        let other: Self = other.into();
74        let unflat = Array::from_shape_fn((self.shape()[0], other.shape()[0]), |(i, j)| {
75            self[(i)] * other[(j)]
76        });
77        unflat
78            .into_shape(self.shape()[0] * other.shape()[0])
79            .unwrap()
80    }
81}
82
83impl<Other: Into<Self>, T: Copy + Mul<Output = T>> Tensor<Other> for &Array1<T> {
84    type Output = Array1<T>;
85
86    fn tensor(self, other: Other) -> Self::Output {
87        let other: Self = other.into();
88        self.view().tensor(other)
89    }
90}
91
92impl<Other: Into<Self>, T: Copy + Mul<Output = T>> Tensor<Other> for ArrayView2<'_, T> {
93    type Output = Array2<T>;
94    fn tensor(self, other: Other) -> Self::Output {
95        let other: Self = other.into();
96        let unflat = Array::from_shape_fn(
97            (
98                self.shape()[0],
99                other.shape()[0],
100                self.shape()[1],
101                other.shape()[1],
102            ),
103            |(i, j, k, l)| self[(i, k)] * other[(j, l)],
104        );
105        unflat
106            .into_shape((
107                self.shape()[0] * other.shape()[0],
108                self.shape()[1] * other.shape()[1],
109            ))
110            .unwrap()
111    }
112}
113
114impl<Other: Into<Self>, T: Copy + Mul<Output = T>> Tensor<Other> for &Array2<T> {
115    type Output = Array2<T>;
116
117    fn tensor(self, other: Other) -> Self::Output {
118        let other: Self = other.into();
119        self.view().tensor(other).to_owned()
120    }
121}
122
123/// Represents types for which the trace can be computed.
124pub trait Trace {
125    /// The type returned by the trace.
126    type Output;
127
128    /// The trace (typically, the sum of the eigenvalues,
129    /// or the sum of the diagonal elements $\sum_i A_{ii}$).
130    ///
131    /// # Example
132    /// ```
133    /// // TODO
134    /// ```
135    fn trace(self) -> Self::Output;
136}
137
138impl<T: Clone + Zero> Trace for Array2<T> {
139    type Output = T;
140
141    fn trace(self) -> Self::Output {
142        self.diag().sum()
143    }
144}
145
146impl<T: Clone + Zero> Trace for &Array2<T> {
147    type Output = T;
148
149    fn trace(self) -> Self::Output {
150        self.diag().sum()
151    }
152}
153
154// FIXME: modify to Result<..., String> so that errors can propagate to the C API.
155// FIXME[perf]: This function is significantly slower than would be expected
156//              from microbenchmarks on tensor and nq_eye directly.
157/// Given an array representing an operator acting on single-qubit states,
158/// returns a new operator that acts on $n$-qubit states.
159pub fn extend_one_to_n(data: ArrayView2<C64>, idx_qubit: usize, n_qubits: usize) -> Array2<C64> {
160    let n_left = idx_qubit;
161    let n_right = n_qubits - idx_qubit - 1;
162    match (n_left, n_right) {
163        (0, _) => {
164            let right_eye = nq_eye(n_right);
165            data.view().tensor(&right_eye)
166        }
167        (_, 0) => {
168            let left_eye = Array2::eye(2usize.pow(n_left.try_into().unwrap()));
169            left_eye.view().tensor(&data)
170        }
171        (_, _) => {
172            let eye = nq_eye(n_right);
173            let right = data.view().tensor(&eye);
174            nq_eye(n_left).view().tensor(&right)
175        }
176    }
177}
178
179/// Given a view of an array representing a matrix acting on two-qubit states,
180/// extends that array to act on $n$ qubits.
181pub fn extend_two_to_n(
182    data: ArrayView2<C64>,
183    idx_qubit1: usize,
184    idx_qubit2: usize,
185    n_qubits: usize,
186) -> Array2<C64> {
187    // TODO: double check that data is 4x4.
188    let mut permutation = Array::from((0..n_qubits).collect::<Vec<usize>>());
189    match (idx_qubit1, idx_qubit2) {
190        (1, 0) => permutation.swap(0, 1),
191        (_, 0) => {
192            permutation.swap(1, idx_qubit2);
193            permutation.swap(1, idx_qubit1);
194        }
195        _ => {
196            permutation.swap(1, idx_qubit2);
197            permutation.swap(0, idx_qubit1);
198        }
199    };
200
201    // TODO: there is almost definitely a more elegant way to write this.
202    if n_qubits == 2 {
203        // TODO[perf]: Eliminate the to_owned here by weakening permute_mtx.
204        permute_mtx(&data.to_owned(), &permutation.to_vec()[..])
205    } else {
206        permute_mtx(
207            &data.view().tensor(&nq_eye(n_qubits - 2)),
208            &permutation.to_vec()[..],
209        )
210    }
211}
212
213/// Given a two-index array (i.e.: a matrix) of dimensions 2^n × 2^n for some
214/// n, permutes the left and right indices of the matrix.
215/// Used to represent, for example, swapping qubits in a register.
216pub fn permute_mtx(data: &Array2<C64>, new_order: &[usize]) -> Array2<C64> {
217    // Check that data is square.
218    let (n_rows, n_cols) = (data.shape()[0], data.shape()[1]);
219    assert_eq!(n_rows, n_cols);
220
221    // Check that dims are 2^n and find n.
222    let n_qubits = (n_rows as f64).log2().floor() as usize;
223    assert_eq!(n_rows, 2usize.pow(n_qubits.try_into().unwrap()));
224
225    // Check that the length of new_order is the same as the number of qubits.
226    assert_eq!(n_qubits, new_order.len());
227
228    // FIXME: there has to be a better way to make a vector that consists of
229    //        2n copies of 2usize.
230    let new_dims: Vec<usize> = vec![2usize]
231        .iter()
232        .cycle()
233        .take(2 * n_qubits)
234        .copied()
235        .collect();
236    // FIXME: make this a result and propagate the result out to the return.
237    let tensor = data.clone().into_shared().reshape(new_dims);
238    let mut permutation = new_order.to_vec();
239    permutation.extend(new_order.to_vec().iter().map(|idx| idx + n_qubits));
240    let permuted = tensor.permuted_axes(permutation);
241
242    // Finish by collapsing back down.
243    permuted.reshape([n_rows, n_rows]).into_owned()
244}
245
246/// Returns a new array of the same type and shape as a given array, but
247/// containing only zeros.
248pub fn zeros_like<T: Clone + Zero, Ix: ndarray::Dimension>(data: &Array<T, Ix>) -> Array<T, Ix> {
249    Array::zeros(data.dim())
250}