-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
Add Clifford.from_matrix #9475
Add Clifford.from_matrix #9475
Changes from 13 commits
37bce39
6019306
dba2eff
bad10de
7a0c373
b64d969
c4bc00a
8ee86cd
bbc1535
577f306
96af80b
c34d9ca
bf028fc
a4d0b75
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 |
---|---|---|
|
@@ -12,19 +12,22 @@ | |
""" | ||
Circuit simulation for the Clifford class. | ||
""" | ||
import copy | ||
import numpy as np | ||
|
||
from qiskit.circuit.barrier import Barrier | ||
from qiskit.circuit.delay import Delay | ||
from qiskit.circuit import Barrier, Delay, Gate | ||
from qiskit.circuit.exceptions import CircuitError | ||
from qiskit.exceptions import QiskitError | ||
|
||
|
||
def _append_circuit(clifford, circuit, qargs=None): | ||
def _append_circuit(clifford, circuit, qargs=None, recursion_depth=0): | ||
"""Update Clifford inplace by applying a Clifford circuit. | ||
|
||
Args: | ||
clifford (Clifford): the Clifford to update. | ||
circuit (QuantumCircuit): the circuit to apply. | ||
clifford (Clifford): The Clifford to update. | ||
circuit (QuantumCircuit): The circuit to apply. | ||
qargs (list or None): The qubits to apply circuit to. | ||
recursion_depth (int): The depth of mutual recursion with _append_operation | ||
|
||
Returns: | ||
Clifford: the updated Clifford. | ||
|
@@ -42,24 +45,26 @@ def _append_circuit(clifford, circuit, qargs=None): | |
) | ||
# Get the integer position of the flat register | ||
new_qubits = [qargs[circuit.find_bit(bit).index] for bit in instruction.qubits] | ||
_append_operation(clifford, instruction.operation, new_qubits) | ||
clifford = _append_operation(clifford, instruction.operation, new_qubits, recursion_depth) | ||
return clifford | ||
|
||
|
||
def _append_operation(clifford, operation, qargs=None): | ||
def _append_operation(clifford, operation, qargs=None, recursion_depth=0): | ||
"""Update Clifford inplace by applying a Clifford operation. | ||
|
||
Args: | ||
clifford (Clifford): the Clifford to update. | ||
operation (Instruction or str): the operation or composite operation to apply. | ||
clifford (Clifford): The Clifford to update. | ||
operation (Instruction or Clifford or str): The operation or composite operation to apply. | ||
qargs (list or None): The qubits to apply operation to. | ||
recursion_depth (int): The depth of mutual recursion with _append_circuit | ||
|
||
Returns: | ||
Clifford: the updated Clifford. | ||
|
||
Raises: | ||
QiskitError: if input operation cannot be decomposed into Clifford operations. | ||
QiskitError: if input operation cannot be converted into Clifford operations. | ||
""" | ||
# pylint: disable=too-many-return-statements | ||
if isinstance(operation, (Barrier, Delay)): | ||
return clifford | ||
|
||
|
@@ -91,6 +96,28 @@ def _append_operation(clifford, operation, qargs=None): | |
raise QiskitError("Invalid qubits for 2-qubit gate.") | ||
return _BASIS_2Q[name](clifford, qargs[0], qargs[1]) | ||
|
||
# If u gate, check if it is a Clifford, and if so, apply it | ||
if isinstance(gate, Gate) and name == "u" and len(qargs) == 1: | ||
try: | ||
theta, phi, lambd = tuple(_n_half_pis(par) for par in gate.params) | ||
except ValueError as err: | ||
raise QiskitError("U gate angles must be multiples of pi/2 to be a Clifford") from err | ||
if theta == 0: | ||
clifford = _append_rz(clifford, qargs[0], lambd + phi) | ||
elif theta == 1: | ||
clifford = _append_rz(clifford, qargs[0], lambd - 2) | ||
clifford = _append_h(clifford, qargs[0]) | ||
clifford = _append_rz(clifford, qargs[0], phi) | ||
elif theta == 2: | ||
clifford = _append_rz(clifford, qargs[0], lambd - 1) | ||
clifford = _append_x(clifford, qargs[0]) | ||
clifford = _append_rz(clifford, qargs[0], phi + 1) | ||
elif theta == 3: | ||
clifford = _append_rz(clifford, qargs[0], lambd) | ||
clifford = _append_h(clifford, qargs[0]) | ||
clifford = _append_rz(clifford, qargs[0], phi + 2) | ||
return clifford | ||
|
||
# If gate is a Clifford, we can either unroll the gate using the "to_circuit" | ||
# method, or we can compose the Cliffords directly. Experimentally, for large | ||
# cliffords the second method is considerably faster. | ||
|
@@ -103,19 +130,72 @@ def _append_operation(clifford, operation, qargs=None): | |
clifford.tableau = composed_clifford.tableau | ||
return clifford | ||
|
||
# If not a Clifford basis gate we try to unroll the gate and | ||
# raise an exception if unrolling reaches a non-Clifford gate. | ||
# TODO: We could also check u3 params to see if they | ||
# are a single qubit Clifford gate rather than raise an exception. | ||
if gate.definition is None: | ||
raise QiskitError(f"Cannot apply Instruction: {gate.name}") | ||
|
||
return _append_circuit(clifford, gate.definition, qargs) | ||
# If the gate is not directly appendable, we try to unroll the gate with its definition. | ||
# This succeeds only if the gate has all-Clifford definition (decomposition). | ||
# If fails, we need to restore the clifford that was before attempting to unroll and append. | ||
if gate.definition is not None: | ||
if recursion_depth > 0: | ||
return _append_circuit(clifford, gate.definition, qargs, recursion_depth + 1) | ||
else: # recursion_depth == 0 | ||
# clifford may be updated in _append_circuit | ||
org_clifford = copy.deepcopy(clifford) | ||
try: | ||
return _append_circuit(clifford, gate.definition, qargs, 1) | ||
except (QiskitError, RecursionError): | ||
# discard incompletely updated clifford and continue | ||
clifford = org_clifford | ||
|
||
# As a final attempt, if the gate is up to 3 qubits, | ||
# we try to construct a Clifford to be appended from its matrix representation. | ||
if isinstance(gate, Gate) and len(qargs) <= 3: | ||
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. Hi, a quick question. With the current order of 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. Good question. I know construction from 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. Another suggestion would be to manually define all the relevant gates 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. If I understand correctly from the tests, there about 10-15 gates, where theta is an integer product of pi/2 or pi, right? 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! I was merely suggesting to change the order of checks:
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. If we change the order of check, we may need to copy 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. @alexanderivrii I've tried the implementation that changes the order of check in this branch. I've confirmed that it gets faster by increasing the complexity of the implementation. Do you want to include the change in this PR? 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. It's generally good to add more gate (like 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. @ShellyGarion I'm not thinking adding more 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. |
||
try: | ||
matrix = gate.to_matrix() | ||
gate_cliff = Clifford.from_matrix(matrix) | ||
return _append_operation(clifford, gate_cliff, qargs=qargs) | ||
except TypeError as err: | ||
raise QiskitError(f"Cannot apply {gate.name} gate with unbounded parameters") from err | ||
except CircuitError as err: | ||
raise QiskitError(f"Cannot apply {gate.name} gate without to_matrix defined") from err | ||
except QiskitError as err: | ||
raise QiskitError(f"Cannot apply non-Clifford gate: {gate.name}") from err | ||
|
||
raise QiskitError(f"Cannot apply {gate}") | ||
|
||
|
||
def _n_half_pis(param) -> int: | ||
try: | ||
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. How about 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. We need to try cast here because that checks if a ParameterExpression is bounded or not (not checking the type of |
||
param = float(param) | ||
epsilon = (abs(param) + 0.5 * 1e-10) % (np.pi / 2) | ||
if epsilon > 1e-10: | ||
raise ValueError(f"{param} is not to a multiple of pi/2") | ||
multiple = int(np.round(param / (np.pi / 2))) | ||
return multiple % 4 | ||
except TypeError as err: | ||
raise ValueError(f"{param} is not bounded") from err | ||
|
||
|
||
# --------------------------------------------------------------------- | ||
# Helper functions for applying basis gates | ||
# --------------------------------------------------------------------- | ||
def _append_rz(clifford, qubit, multiple): | ||
"""Apply an Rz gate to a Clifford. | ||
|
||
Args: | ||
clifford (Clifford): a Clifford. | ||
qubit (int): gate qubit index. | ||
multiple (int): z-rotation angle in a multiple of pi/2 | ||
|
||
Returns: | ||
Clifford: the updated Clifford. | ||
""" | ||
if multiple % 4 == 1: | ||
return _append_s(clifford, qubit) | ||
if multiple % 4 == 2: | ||
return _append_z(clifford, qubit) | ||
if multiple % 4 == 3: | ||
return _append_sdg(clifford, qubit) | ||
|
||
return clifford | ||
|
||
|
||
def _append_i(clifford, qubit): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
--- | ||
features: | ||
- | | ||
Added :meth:`.Clifford.from_matrix` and :meth:`.Clifford.from_operator` method that | ||
creates a ``Clifford`` object from its unitary matrix and operator representation respectively. | ||
- | | ||
The constructor of :class:`.Clifford` now can take any Clifford gate object up to 3 qubits | ||
as long it supports :meth:`to_matrix` method, | ||
including parameterized gates such as ``Rz(pi/2)``, which were not convertible before. |
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.
One more quick question. I have not done the math, but I am wondering if it's possible to check whether a given matrix (here
U * Xi * Udg
) is close to some Pauli matrix without explicitly going over all of the Pauli matrices?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 lines are using a bit strange grammar of Python: This
else
clause is coupled withfor
notif
. So they do go over all of the Pauli matrices.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.
Sorry, I did not highlight the right lines. I believe (but I have not done the math) that the (tensor products of) Pauli matrices are very special unitary matrices, and it should be possible to check whether a given matrix (either
U * Xi * Udg
orU * Zi * Udg
) is close to a Pauli matrix without explicitly checkingnp.close()
against the 2*4^n Pauli matrices.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.
Yeah, I'm a bit concerned that there may be a more efficient algorithm then current one. If anyone knows such an algorithm, please let me know.
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.
I believe that if you look at the 2^N x 2^N unitary matrix corresponding to a Pauli matrix, then this unitary matrix must have only one nonzero entry in every row and every column, moreover each nonzero element can only be 1, -1, i or -i. Intuitively, I think this should be enough to work out the exact Pauli.
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.
Great suggestion, thanks! The new algorithm should be
U * Xi * Udg
(orU * Zi * Udg
) has exactly 2^N non-zero (i.e. 1, -1, i or -i) elements (if not, U is non-Clifford)Hence, we don't need to create the table with 2^N x 2^N matrices corresponding 4^N Paulis in advance. I'll try the above algorithm.
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.
Thank you! Here are my five cents (and @ikkoham and @ShellyGarion are welcome to correct this):
_append_cx
or_append_w
)definition
, then it might be more efficient to consider thedefinition
first, before constructing the unitary matrix and converting it to Clifford. On the other hand, it is possible that the default definition of a Clifford gate might contain non-Clifford gates, so the construction based on the definition alone might not be possibleThere 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.
Regarding the more efficient O(4^n) (instead of O(16^n)) algorithm, implemented at bad10de