-
Notifications
You must be signed in to change notification settings - Fork 625
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
QubitUnitary
accepts sparse matrices as inputs
#6889
base: master
Are you sure you want to change the base?
Changes from all commits
72aa2bd
ee7c3f0
e10cf01
a4f30eb
e71929c
fd23deb
53ed0cd
9411ec0
4538d1a
3ffc8f9
c859837
694c9e8
40c1432
de02c89
942bcdb
34f1817
5eda77c
2fc2199
525a649
403f8ee
ee20832
3dbefbf
ebb5b03
831304d
387fb80
de14b45
d388c2a
edbb15b
0aa15f4
8ca3b79
05dbb85
d24ff67
158068e
7e157c6
dd18ca7
f6e0ad7
f9b478c
6866871
9dd92aa
f76a9cf
855ebfc
4e81229
8991ddf
f306c6e
94ba0c6
98b3287
63c01c2
bf1c1f2
1de2acf
e32e73c
52270f9
9579701
6c45746
4d0536b
0d63b41
b182411
e19ace6
a24cf6b
8d72ae1
81535e8
a250409
53a9de4
3e3b31e
a54a8f5
cadc8f9
f66ffed
b3648bf
fee05fc
628cda6
d0c7246
c923692
167400b
35d9ab9
b02ff12
0cb34f8
09124f4
6abc33a
5d50ae8
c394554
feee2c4
839652b
0d61d57
516bc28
306bd8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,8 +14,10 @@ | |
"""Contains transforms and helpers functions for decomposing arbitrary unitary | ||
operations into elementary gates. | ||
""" | ||
from functools import singledispatch | ||
|
||
import numpy as np | ||
import scipy as sp | ||
|
||
import pennylane as qml | ||
from pennylane import math | ||
|
@@ -42,11 +44,16 @@ def _convert_to_su2(U, return_global_phase=False): | |
with np.errstate(divide="ignore", invalid="ignore"): | ||
determinants = math.linalg.det(U) | ||
phase = math.angle(determinants) / 2 | ||
U = math.cast_like(U, determinants) * math.exp(-1j * math.cast_like(phase, 1j))[:, None, None] | ||
U = ( | ||
math.cast_like(U, determinants) * math.exp(-1j * math.cast_like(phase, 1j))[:, None, None] | ||
if not sp.sparse.issparse(U) | ||
else U * math.exp(-1j * phase) | ||
) | ||
|
||
return (U, phase) if return_global_phase else U | ||
|
||
|
||
@singledispatch | ||
def _zyz_get_rotation_angles(U): | ||
r"""Computes the rotation angles :math:`\phi`, :math:`\theta`, :math:`\omega` | ||
for a unitary :math:`U` that is :math:`SU(2)` | ||
|
@@ -91,6 +98,51 @@ def _zyz_get_rotation_angles(U): | |
return phis, thetas, omegas | ||
|
||
|
||
@_zyz_get_rotation_angles.register(sp.sparse.csr_matrix) | ||
def _zyz_get_rotation_angles_sparse(U): | ||
r"""Computes the rotation angles :math:`\phi`, :math:`\theta`, :math:`\omega` | ||
for a unitary :math:`U` that is :math:`SU(2)`, sparse case | ||
|
||
Args: | ||
U (array[complex]): A matrix that is :math:`SU(2)` | ||
|
||
Returns: | ||
tuple[array[float]]: A tuple containing the rotation angles | ||
:math:`\phi`, :math:`\theta`, :math:`\omega` | ||
|
||
""" | ||
|
||
assert sp.sparse.issparse(U), "Do not use this method if U is not sparse" | ||
|
||
u00 = U[0, 0] | ||
u01 = U[0, 1] | ||
u10 = U[1, 0] | ||
|
||
# For batched U or single U with non-zero off-diagonal, compute the | ||
# normal decomposition instead | ||
off_diagonal_elements = math.clip(math.abs(u01), 0, 1) | ||
thetas = 2 * math.arcsin(off_diagonal_elements) | ||
|
||
# Compute phi and omega from the angles of the top row; use atan2 to keep | ||
# the angle within -np.pi and np.pi, and add very small value to the real | ||
# part to avoid division by zero. | ||
epsilon = 1e-64 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I remember that there was a discussion some time ago in a stand-up, mentioning a previous discussion about epsilons in pennylane (when, why, and how they should be used), but I don't know the details. Might be relevant in this context. Probably @mlxd or @AmintorDusko can help us here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am also very curious about many occurences of epsilon existing in many places of our codebase. If there is quick fix to them I'm happy to help clean them up here, but if there's quite some extended discussion TBD then I'll suggest we skip over it. |
||
angles_U00 = math.arctan2(math.imag(u00), math.real(u00) + epsilon) | ||
angles_U10 = math.arctan2(math.imag(u10), math.real(u10) + epsilon) | ||
|
||
phis = -angles_U10 - angles_U00 | ||
omegas = angles_U10 - angles_U00 | ||
|
||
phis, thetas, omegas = map(math.squeeze, [phis, thetas, omegas]) | ||
|
||
# Normalize the angles | ||
phis = phis % (4 * np.pi) | ||
thetas = thetas % (4 * np.pi) | ||
omegas = omegas % (4 * np.pi) | ||
|
||
return phis, thetas, omegas | ||
|
||
|
||
def _rot_decomposition(U, wire, return_global_phase=False): | ||
r"""Compute the decomposition of a single-qubit matrix :math:`U` in terms of | ||
elementary operations, as a single :class:`.RZ` gate or a :class:`.Rot` gate. | ||
|
@@ -167,7 +219,7 @@ def _get_single_qubit_rot_angles_via_matrix( | |
of the matrix of the target operation using ZYZ rotations. | ||
""" | ||
# Cast to batched format for more consistent code | ||
U = math.expand_dims(U, axis=0) if len(U.shape) == 2 else U | ||
U = math.expand_dims(U, axis=0) if len(U.shape) == 2 and not sp.sparse.issparse(U) else U | ||
|
||
# Convert to SU(2) format and extract global phase | ||
U_su2, global_phase = _convert_to_su2(U, return_global_phase=True) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,9 @@ | |
from typing import Optional, Union | ||
|
||
import numpy as np | ||
import scipy as sp | ||
from scipy.linalg import fractional_matrix_power | ||
from scipy.sparse import csr_matrix | ||
|
||
import pennylane as qml | ||
from pennylane import numpy as pnp | ||
|
@@ -78,6 +80,10 @@ class QubitUnitary(Operation): | |
r"""QubitUnitary(U, wires) | ||
Apply an arbitrary unitary matrix with a dimension that is a power of two. | ||
|
||
.. warning:: | ||
|
||
The sparse matrix representation of QubitUnitary is still under development. Currently we only support a limited set of interfaces that preserve the sparsity of the matrix, including ..method::`adjoint`, ..method::`pow`, ..method::`compute_sparse_matrix` and ..method::`compute_decomposition`. Differentiability is not supported for sparse matrices. | ||
|
||
Comment on lines
+83
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a warning to indicate the limitedness for any potential users in between this PR and the final dispatch system is completed. @PietropaoloFrisoni There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks @JerryChen97 . Although I am fine with that, I am not sure that a warning is necessary here. Maybe a simple statement in the doc (without the warning) is sufficient. I would ask @isaacdevlugt just to be sure |
||
**Details:** | ||
|
||
* Number of wires: Any (the operation can act on any number of wires) | ||
|
@@ -86,7 +92,7 @@ class QubitUnitary(Operation): | |
* Gradient recipe: None | ||
|
||
Args: | ||
U (array[complex]): square unitary matrix | ||
U (array[complex] or csr_matrix): square unitary matrix | ||
wires (Sequence[int] or int): the wire(s) the operation acts on | ||
id (str): custom label given to an operator instance, | ||
can be useful for some applications where the instance has to be identified | ||
|
@@ -121,7 +127,7 @@ class QubitUnitary(Operation): | |
|
||
def __init__( | ||
self, | ||
U: TensorLike, | ||
U: Union[TensorLike, csr_matrix], | ||
wires: WiresLike, | ||
id: Optional[str] = None, | ||
unitary_check: bool = False, | ||
|
@@ -135,19 +141,16 @@ def __init__( | |
if len(U_shape) not in {2, 3} or U_shape[-2:] != (dim, dim): | ||
raise ValueError( | ||
f"Input unitary must be of shape {(dim, dim)} or (batch_size, {dim}, {dim}) " | ||
f"to act on {len(wires)} wires." | ||
f"to act on {len(wires)} wires. Got shape {U_shape} instead." | ||
) | ||
|
||
# If the matrix is sparse, we need to convert it to a csr_matrix | ||
if sp.sparse.issparse(U): | ||
U = U.tocsr() | ||
|
||
# Check for unitarity; due to variable precision across the different ML frameworks, | ||
# here we issue a warning to check the operation, instead of raising an error outright. | ||
if unitary_check and not ( | ||
qml.math.is_abstract(U) | ||
or qml.math.allclose( | ||
qml.math.einsum("...ij,...kj->...ik", U, qml.math.conj(U)), | ||
qml.math.eye(dim), | ||
atol=1e-6, | ||
) | ||
): | ||
if unitary_check and not self._unitary_check(U, dim): | ||
warnings.warn( | ||
f"Operator {U}\n may not be unitary. " | ||
"Verify unitarity of operation, or use a datatype with increased precision.", | ||
|
@@ -156,6 +159,18 @@ def __init__( | |
|
||
super().__init__(U, wires=wires, id=id) | ||
|
||
@staticmethod | ||
def _unitary_check(U, dim): | ||
if isinstance(U, csr_matrix): | ||
U_dagger = U.conjugate().transpose() | ||
identity = sp.sparse.eye(dim, format="csr") | ||
return sp.sparse.linalg.norm(U @ U_dagger - identity) < 1e-10 | ||
return qml.math.allclose( | ||
qml.math.einsum("...ij,...kj->...ik", U, qml.math.conj(U)), | ||
qml.math.eye(dim), | ||
atol=1e-6, | ||
) | ||
|
||
@staticmethod | ||
def compute_matrix(U: TensorLike): # pylint: disable=arguments-differ | ||
r"""Representation of the operator as a canonical matrix in the computational basis (static method). | ||
|
@@ -180,6 +195,26 @@ def compute_matrix(U: TensorLike): # pylint: disable=arguments-differ | |
""" | ||
return U | ||
|
||
@staticmethod | ||
def compute_sparse_matrix(U: TensorLike): # pylint: disable=arguments-differ | ||
r"""Representation of the operator as a sparse matrix. | ||
|
||
Args: | ||
U (tensor_like): unitary matrix | ||
|
||
Returns: | ||
csr_matrix: sparse matrix representation | ||
|
||
**Example** | ||
|
||
>>> U = np.array([[0.98877108+0.j, 0.-0.14943813j], [0.-0.14943813j, 0.98877108+0.j]]) | ||
>>> qml.QubitUnitary.compute_sparse_matrix(U) | ||
<2x2 sparse matrix of type '<class 'numpy.complex128'>' | ||
with 2 stored elements in Compressed Sparse Row format> | ||
""" | ||
U = qml.math.asarray(U, like="numpy") | ||
return sp.sparse.csr_matrix(U) | ||
|
||
@staticmethod | ||
def compute_decomposition(U: TensorLike, wires: WiresLike): | ||
r"""Representation of the operator as a product of other operators (static method). | ||
|
@@ -236,10 +271,17 @@ def has_decomposition(self) -> bool: | |
|
||
def adjoint(self) -> "QubitUnitary": | ||
U = self.matrix() | ||
if isinstance(U, csr_matrix): | ||
adjoint_sp_mat = U.conjugate().transpose() | ||
# Note: it is necessary to explicitly cast back to csr, or it will be come csc | ||
JerryChen97 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return QubitUnitary(csr_matrix(adjoint_sp_mat), wires=self.wires) | ||
return QubitUnitary(qml.math.moveaxis(qml.math.conj(U), -2, -1), wires=self.wires) | ||
JerryChen97 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def pow(self, z: Union[int, float]): | ||
mat = self.matrix() | ||
if isinstance(mat, csr_matrix): | ||
pow_mat = sp.sparse.linalg.matrix_power(mat, z) | ||
return [QubitUnitary(pow_mat, wires=self.wires)] | ||
if isinstance(z, int) and qml.math.get_deep_interface(mat) != "tensorflow": | ||
pow_mat = qml.math.linalg.matrix_power(mat, z) | ||
elif self.batch_size is not None or qml.math.shape(z) != (): | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are necessary for decompositions to run successfully. Further registers will be added in a following PR #6947 so that this PR does not go far beyond initial scope