Skip to content
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

Merged
merged 14 commits into from
Apr 12, 2023
136 changes: 133 additions & 3 deletions qiskit/quantum_info/operators/symplectic/clifford.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""
Clifford operator class.
"""
import functools
import itertools
import re

Expand Down Expand Up @@ -554,10 +555,50 @@ def to_matrix(self):
"""Convert operator to Numpy matrix."""
return self.to_operator().data

@classmethod
def from_matrix(cls, matrix):
"""Create a Clifford from a unitary matrix.

Note that this function takes exponentially long time w.r.t. the number of qubits.

Args:
matrix (np.array): A unitary matrix representing a Clifford to be converted.

Returns:
Clifford: the Clifford object for the unitary matrix.

Raises:
QiskitError: if the input is not a Clifford matrix.
"""
tableau = cls._unitary_matrix_to_tableau(matrix)
if tableau is None:
raise QiskitError("Non-Clifford matrix is not convertible")
return cls(tableau)

def to_operator(self):
"""Convert to an Operator object."""
return Operator(self.to_instruction())

@classmethod
def from_operator(cls, operator):
"""Create a Clifford from a operator.

Note that this function takes exponentially long time w.r.t. the number of qubits.

Args:
operator (Operator): An operator representing a Clifford to be converted.

Returns:
Clifford: the Clifford object for the operator.

Raises:
QiskitError: if the input is not a Clifford operator.
"""
tableau = cls._unitary_matrix_to_tableau(operator.to_matrix())
if tableau is None:
raise QiskitError("Non-Clifford operator is not convertible")
return cls(tableau)

def to_circuit(self):
"""Return a QuantumCircuit implementing the Clifford.

Expand Down Expand Up @@ -606,9 +647,9 @@ def from_circuit(circuit):
# Initialize an identity Clifford
clifford = Clifford(np.eye(2 * circuit.num_qubits), validate=False)
if isinstance(circuit, QuantumCircuit):
_append_circuit(clifford, circuit)
clifford = _append_circuit(clifford, circuit)
else:
_append_operation(clifford, circuit)
clifford = _append_operation(clifford, circuit)
return clifford

@staticmethod
Expand Down Expand Up @@ -664,7 +705,7 @@ def from_label(label):
num_qubits = len(label)
op = Clifford(np.eye(2 * num_qubits, dtype=bool))
for qubit, char in enumerate(reversed(label)):
_append_operation(op, label_gates[char], qargs=[qubit])
op = _append_operation(op, label_gates[char], qargs=[qubit])
return op

def to_labels(self, array=False, mode="B"):
Expand Down Expand Up @@ -851,6 +892,95 @@ def _from_label(label):
symp[-1] = phase
return symp

@staticmethod
def _pauli_matrix_to_row(mat, num_qubits):
"""Generate a binary vector (a row of tableau representation) from a Pauli matrix.
Return None if the non-Pauli matrix is supplied."""
# pylint: disable=too-many-return-statements

def find_one_index(x, decimals=6):
indices = np.where(np.round(np.abs(x), decimals) == 1)
return indices[0][0] if len(indices[0]) == 1 else None

def bitvector(n, num_bits):
return np.array([int(digit) for digit in format(n, f"0{num_bits}b")], dtype=bool)[::-1]

# compute x-bits
xint = find_one_index(mat[0, :])
if xint is None:
return None
xbits = bitvector(xint, num_qubits)

# extract non-zero elements from matrix (rounded to 1, -1, 1j or -1j)
entries = np.empty(len(mat), dtype=complex)
for i, row in enumerate(mat):
index = find_one_index(row)
if index is None:
return None
expected = xint ^ i
if index != expected:
return None
entries[i] = np.round(mat[i, index])

# compute z-bits
zbits = np.empty(num_qubits, dtype=bool)
for k in range(num_qubits):
sign = np.round(entries[2**k] / entries[0])
if sign == 1:
zbits[k] = False
elif sign == -1:
zbits[k] = True
else:
return None

# compute phase
phase = None
num_y = sum(xbits & zbits)
positive_phase = (-1j) ** num_y
if entries[0] == positive_phase:
phase = False
elif entries[0] == -1 * positive_phase:
phase = True
if phase is None:
return None

# validate all non-zero elements
coef = ((-1) ** phase) * positive_phase
ivec, zvec = np.ones(2), np.array([1, -1])
expected = coef * functools.reduce(np.kron, [zvec if z else ivec for z in zbits[::-1]])
if not np.allclose(entries, expected):
return None

return np.hstack([xbits, zbits, phase])

@staticmethod
def _unitary_matrix_to_tableau(matrix):
# pylint: disable=invalid-name
num_qubits = int(np.log2(len(matrix)))

stab = np.empty((num_qubits, 2 * num_qubits + 1), dtype=bool)
for i in range(num_qubits):
label = "I" * (num_qubits - i - 1) + "X" + "I" * i
Xi = Operator.from_label(label).to_matrix()
target = matrix @ Xi @ np.conj(matrix).T
row = Clifford._pauli_matrix_to_row(target, num_qubits)
if row is None:
return None
stab[i] = row

Copy link
Contributor

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?

Copy link
Contributor Author

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 with for not if. So they do go over all of the Pauli matrices.

Copy link
Contributor

@alexanderivrii alexanderivrii Jan 31, 2023

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 or U * Zi * Udg) is close to a Pauli matrix without explicitly checking np.close() against the 2*4^n Pauli matrices.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

@itoko itoko Feb 7, 2023

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

  1. Check if the U * Xi * Udg (or U * Zi * Udg) has exactly 2^N non-zero (i.e. 1, -1, i or -i) elements (if not, U is non-Clifford)
  2. Reconstruct the Pauli string (a row of the resulting tableau) from the positions and values of the 2^N non-zero elements (if impossible, 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.

Copy link
Contributor

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):

  • If a gate is going to be used a lot, it might be best to provide methods that directly operate on the Clifford tableau (like what is done in _append_cx or _append_w )
  • However, having a generic fallback mechanism is a great idea, so I think this development is very useful
  • If the gate has a definition, then it might be more efficient to consider the definition 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 possible

Copy link
Contributor Author

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

destab = np.empty((num_qubits, 2 * num_qubits + 1), dtype=bool)
for i in range(num_qubits):
label = "I" * (num_qubits - i - 1) + "Z" + "I" * i
Zi = Operator.from_label(label).to_matrix()
target = matrix @ Zi @ np.conj(matrix).T
row = Clifford._pauli_matrix_to_row(target, num_qubits)
if row is None:
return None
destab[i] = row

tableau = np.vstack([stab, destab])
return tableau


# Update docstrings for API docs
generate_apidocs(Clifford)
116 changes: 98 additions & 18 deletions qiskit/quantum_info/operators/symplectic/clifford_circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, a quick question. With the current order of if-statements, would the new code be executed even for gates that already have a definition? I have not done any experiments, but it feels that constructing a Clifford from a matrix could be slower than constructing a Clifford from the defining quantum circuit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. I know construction from definition is faster if possible but it does not always work because the definition may contain non-Clifford gates. For example, the definition of ECRGate contains RZX(pi/4), which is not a Clifford. The definition of a gate is up to the gate designer, so we cannot fully rely on it. And I just prioritize capability to performance in this commit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another suggestion would be to manually define all the relevant gates here:
https://github.com/Qiskit/qiskit-terra/blob/main/qiskit/quantum_info/operators/symplectic/clifford_circuits.py
(you can check both gate name and parameters, and see if they fit one of the known patterns)

Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I was merely suggesting to change the order of checks:

if gate.definition is not None:
    return _append_circuit(clifford, gate.definition, qargs)

try:
   # new code here

raise QiskitError(f"Cannot apply {gate}")

Copy link
Contributor Author

@itoko itoko Feb 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we change the order of check, we may need to copy clifford and when we have any exception, i.e. we fails to all-Clifford decomposition (based on definition), we need to restore the clifford that was before trying unroll. So the actual code would require more lines, but it's possible, so I'll try that in another branch for comparison.

Copy link
Contributor Author

@itoko itoko Feb 17, 2023

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's generally good to add more gate (like Rz(pi/2) and u for angles that are multiples of pi/2) to clifford_circuits.py.
My PR #9623 may also be useful. Would you like to add other gates (also with parameters) directly to clifford_circuits.py?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ShellyGarion I'm not thinking adding more _append_* to clifford_circuits.py is the best approach but it should a option to be considered for improving the performance of Clifford.from_circuit (see my comment in #9582 for other options). Let's continue to discuss it in #9582.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the order of check, I've added at b64d969 with the conversion of U gate c4bc00a

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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about if not isinstance(param, ParameterExpression)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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).

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):
Expand Down
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.
Loading