# -*- coding: utf-8 -*-
# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2020
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""
Clifford operator class.
"""
# pylint: disable=invalid-name, abstract-method
import re
import numpy as np
from qiskit.exceptions import QiskitError
from qiskit.circuit import QuantumCircuit, Instruction
from qiskit.circuit.library.standard_gates import IGate, XGate, YGate, ZGate, HGate, SGate
from qiskit.quantum_info.operators.base_operator import BaseOperator
from qiskit.quantum_info.operators.operator import Operator
from qiskit.quantum_info.operators.scalar_op import ScalarOp
from qiskit.quantum_info.synthesis.clifford_decompose import decompose_clifford
from .stabilizer_table import StabilizerTable
from .clifford_circuits import _append_circuit
[docs]class Clifford(BaseOperator):
"""An N-qubit unitary operator from the Clifford group.
**Representation**
An *N*-qubit Clifford operator is stored as a length *2N*
:class:`~qiskit.quantum_info.StabilizerTable` using the convention
from reference [1].
* Rows 0 to *N-1* are the *destabilizer* group generators
* Rows *N-1* to *2N-1* are the *stabilizer* group generators.
The internal :class:`~qiskit.quantum_info.StabilizerTable` for the Clifford
can be accessed using the :attr:`table` attribute. The destabilizer or
stabilizer rows can each be accessed as a length-N Stabilizer table using
:attr:`destabilizer` and :attr:`stabilizer` attributes.
A more easily human readible representation of the Clifford operator can
be obtained by calling the :meth:`to_dict` method. This representation is
also used if a Clifford object is printed as in the following example
.. jupyter-execute::
from qiskit import QuantumCircuit
from qiskit.quantum_info import Clifford
# Bell state generation circuit
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
cliff = Clifford(qc)
# Print the Clifford
print(cliff)
# Print the Clifford destabilizer rows
print(cliff.destabilizer)
# Print the Clifford stabilizer rows
print(cliff.stabilizer)
**Circuit Conversion**
Clifford operators can be initialized from circuits containing *only* the
following Clifford gates: :class:`~qiskit.extensions.IGate`,
:class:`~qiskit.extensions.XGate`, :class:`~qiskit.extensions.YGate`,
:class:`~qiskit.extensions.ZGate`, :class:`~qiskit.extensions.HGate`,
:class:`~qiskit.extensions.SGate`, :class:`~qiskit.extensions.SdgGate`,
:class:`~qiskit.extensions.CXGate`, :class:`~qiskit.extensions.CZGate`,
:class:`~qiskit.extensions.SwapGate`.
They can be converted back into a :class:`~qiskit.circuit.QuantumCircuit`,
or :class:`~qiskit.circuit.Gate` object using the :meth:`~Clifford.to_circuit`
or :meth:`~Clifford.to_instruction` methods respectively. Note that this
decomposition is not necessarily optimal in terms of number of gates.
.. note::
A minimally generating set of gates for Clifford circuits is
the :class:`~qiskit.extensions.HGate` and
:class:`~qiskit.extensions.SGate` gate and *either* the
:class:`~qiskit.extensions.CXGate` or
:class:`~qiskit.extensions.CZGate` two-qubit gate.
Clifford operators can also be converted to
:class:`~qiskit.quantum_info.Operator` objects using the
:meth:`to_operator` method. This is done via decomposing to a circuit, and then
simulating the circuit as a unitary operator.
References:
1. S. Aaronson, D. Gottesman, *Improved Simulation of Stabilizer Circuits*,
Phys. Rev. A 70, 052328 (2004).
`arXiv:quant-ph/0406196 <https://arxiv.org/abs/quant-ph/0406196>`_
"""
def __init__(self, data, validate=True):
"""Initialize an operator object."""
# Initialize from another Clifford by sharing the underlying
# StabilizerTable
if isinstance(data, Clifford):
self._table = data._table
# Initialize from ScalarOp as N-qubit identity discarding any global phase
elif isinstance(data, ScalarOp):
if not data.is_unitary() or set(data._input_dims) != set([2]):
raise QiskitError("Can only initalize from N-qubit identity ScalarOp.")
self._table = StabilizerTable(
np.eye(2 * len(data._input_dims), dtype=np.bool))
# Initialize from a QuantumCircuit or Instruction object
elif isinstance(data, (QuantumCircuit, Instruction)):
self._table = Clifford.from_circuit(data)._table
# Initialize StabilizerTable directly from the data
else:
self._table = StabilizerTable(data)
# Validate table is a symplectic matrix
if validate and not Clifford._is_symplectic(self._table.array):
raise QiskitError(
'Invalid Clifford. Input StabilizerTable is not a valid'
' symplectic matrix.')
# Initialize BaseOperator
dims = self._table.num_qubits * (2,)
super().__init__(dims, dims)
def __repr__(self):
return 'Clifford({})'.format(repr(self.table))
def __str__(self):
return 'Clifford: Stabilizer = {}, Destabilizer = {}'.format(
str(self.stabilizer.to_labels()),
str(self.destabilizer.to_labels()))
def __eq__(self, other):
"""Check if two Clifford tables are equal"""
return super().__eq__(other) and self._table == other._table
# ---------------------------------------------------------------------
# Attributes
# ---------------------------------------------------------------------
[docs] def __getitem__(self, key):
"""Return a stabilizer Pauli row"""
return self._table.__getitem__(key)
def __setitem__(self, key, value):
"""Set a stabilizer Pauli row"""
self._table.__setitem__(key, value)
@property
def table(self):
"""Return StabilizerTable"""
return self._table
@table.setter
def table(self, value):
"""Set the stabilizer table"""
# Note this setter cannot change the size of the Clifford
# It can only replace the contents of the StabilizerTable with
# another StabilizerTable of the same size.
if not isinstance(value, StabilizerTable):
value = StabilizerTable(value)
self._table._array[:, :] = value._table._array
self._table._phase[:] = value._table._phase
@property
def stabilizer(self):
"""Return the stabilizer block of the StabilizerTable."""
return StabilizerTable(self._table[self.num_qubits:2*self.num_qubits])
@stabilizer.setter
def stabilizer(self, value):
"""Set the value of stabilizer block of the StabilizerTable"""
inds = slice(self.num_qubits, 2*self.num_qubits)
self._table.__setitem__(inds, value)
@property
def destabilizer(self):
"""Return the destabilizer block of the StabilizerTable."""
return StabilizerTable(self._table[0:self.num_qubits])
@destabilizer.setter
def destabilizer(self, value):
"""Set the value of destabilizer block of the StabilizerTable"""
inds = slice(0, self.num_qubits)
self._table.__setitem__(inds, value)
# ---------------------------------------------------------------------
# Utility Operator methods
# ---------------------------------------------------------------------
[docs] def is_unitary(self):
"""Return True if the Clifford table is valid."""
# A valid Clifford is always unitary, so this function is really
# checking that the underlying Stabilizer table array is a valid
# Clifford array.
return Clifford._is_symplectic(self.table.array)
# ---------------------------------------------------------------------
# BaseOperator Abstract Methods
# ---------------------------------------------------------------------
[docs] def conjugate(self):
"""Return the conjugate of the Clifford."""
return Clifford._conjugate_transpose(self, 'C')
[docs] def adjoint(self):
"""Return the conjugate transpose of the Clifford"""
return Clifford._conjugate_transpose(self, 'A')
[docs] def transpose(self):
"""Return the transpose of the Clifford."""
return Clifford._conjugate_transpose(self, 'T')
[docs] def compose(self, other, qargs=None, front=False):
"""Return the composed operator.
Args:
other (Clifford): an operator object.
qargs (list or None): a list of subsystem positions to apply
other on. If None apply on all
subsystems [default: None].
front (bool): If True compose using right operator multiplication,
instead of left multiplication [default: False].
Returns:
Clifford: The operator self @ other.
Raise:
QiskitError: if operators have incompatible dimensions for
composition.
Additional Information:
Composition (``@``) is defined as `left` matrix multiplication for
matrix operators. That is that ``A @ B`` is equal to ``B * A``.
Setting ``front=True`` returns `right` matrix multiplication
``A * B`` and is equivalent to the :meth:`dot` method.
"""
if qargs is None:
qargs = getattr(other, 'qargs', None)
# If other is a QuantumCircuit we can more efficiently compose
# using the _append_circuit method to update each gate recursively
# to the current Clifford, rather than converting to a Clifford first
# and then doing the composition of tables.
if not front and isinstance(other, (QuantumCircuit, Instruction)):
ret = self.copy()
_append_circuit(ret, other, qargs=qargs)
return ret
if not isinstance(other, Clifford):
other = Clifford(other)
# Validate dimensions. Note we don't need to get updated input or
# output dimensions from `_get_compose_dims` as the dimensions of the
# Clifford object can't be changed by composition
self._get_compose_dims(other, qargs, front)
# Pad other with identities if composeing on subsystem
other = self._pad_with_identity(other, qargs)
return self._compose_clifford(other, front=front)
[docs] def dot(self, other, qargs=None):
"""Return the right multiplied operator self * other.
Args:
other (Clifford): an operator object.
qargs (list or None): a list of subsystem positions to apply
other on. If None apply on all
subsystems [default: None].
Returns:
Clifford: The operator self * other.
Raises:
QiskitError: if operators have incompatible dimensions for
composition.
"""
return super().dot(other, qargs=qargs)
[docs] def tensor(self, other):
"""Return the tensor product operator self ⊗ other.
Args:
other (Clifford): a operator subclass object.
Returns:
Clifford: the tensor product operator self ⊗ other.
"""
return self._tensor_product(other, reverse=False)
[docs] def expand(self, other):
"""Return the tensor product operator other ⊗ self.
Args:
other (Clifford): an operator object.
Returns:
Clifford: the tensor product operator other ⊗ self.
"""
return self._tensor_product(other, reverse=True)
# ---------------------------------------------------------------------
# Representation conversions
# ---------------------------------------------------------------------
[docs] def to_dict(self):
"""Return dictionary represenation of Clifford object."""
return {
"stabilizer": self.stabilizer.to_labels(),
"destabilizer": self.destabilizer.to_labels()
}
[docs] @staticmethod
def from_dict(obj):
"""Load a Clifford from a dictionary"""
destabilizer = StabilizerTable.from_labels(obj.get('destabilizer'))
stabilizer = StabilizerTable.from_labels(obj.get('stabilizer'))
return Clifford(destabilizer + stabilizer)
[docs] def to_matrix(self):
"""Convert operator to Numpy matrix."""
return self.to_operator().data
[docs] def to_operator(self):
"""Convert to an Operator object."""
return Operator(self.to_instruction())
[docs] def to_circuit(self):
"""Return a QuantumCircuit implementing the Clifford.
For N <= 3 qubits this is based on optimal CX cost decomposition
from reference [1]. For N > 3 qubits this is done using the general
non-optimal compilation routine from reference [2].
Return:
QuantumCircuit: a circuit implementation of the Clifford.
References:
1. S. Bravyi, D. Maslov, *Hadamard-free circuits expose the
structure of the Clifford group*,
`arXiv:2003.09412 [quant-ph] <https://arxiv.org/abs/2003.09412>`_
2. S. Aaronson, D. Gottesman, *Improved Simulation of Stabilizer Circuits*,
Phys. Rev. A 70, 052328 (2004).
`arXiv:quant-ph/0406196 <https://arxiv.org/abs/quant-ph/0406196>`_
"""
return decompose_clifford(self)
[docs] def to_instruction(self):
"""Return a Gate instruction implementing the Clifford."""
return self.to_circuit().to_gate()
[docs] @staticmethod
def from_circuit(circuit):
"""Initialize from a QuantumCircuit or Instruction.
Args:
circuit (QuantumCircuit or ~qiskit.circuit.Instruction):
instruction to initialize.
Returns:
Clifford: the Clifford object for the instruction.
Raises:
QiskitError: if the input instruction is non-Clifford or contains
classical register instruction.
"""
if not isinstance(circuit, (QuantumCircuit, Instruction)):
raise QiskitError("Input must be a QuantumCircuit or Instruction")
# Convert circuit to an instruction
if isinstance(circuit, QuantumCircuit):
circuit = circuit.to_instruction()
# Initialize an identity Clifford
clifford = Clifford(np.eye(2 * circuit.num_qubits), validate=False)
_append_circuit(clifford, circuit)
return clifford
[docs] @staticmethod
def from_label(label):
"""Return a tensor product of single-qubit Clifford gates.
Args:
label (string): single-qubit operator string.
Returns:
Clifford: The N-qubit Clifford operator.
Raises:
QiskitError: if the label contains invalid characters.
Additional Information:
The labels correspond to the single-qubit Cliffords are
* - Label
- Stabilizer
- Destabilizer
* - ``"I"``
- +Z
- +X
* - ``"X"``
- -Z
- +X
* - ``"Y"``
- -Z
- -X
* - ``"Z"``
- +Z
- -X
* - ``"H"``
- +X
- +Z
* - ``"S"``
- +Z
- +Y
"""
# Check label is valid
label_gates = {
'I': IGate(), 'X': XGate(), 'Y': YGate(),
'Z': ZGate(), 'H': HGate(), 'S': SGate()
}
if re.match(r'^[IXYZHS\-+]+$', label) is None:
raise QiskitError('Label contains invalid characters.')
# Initialize an identity matrix and apply each gate
num_qubits = len(label)
op = Clifford(np.eye(2 * num_qubits, dtype=np.bool))
for qubit, char in enumerate(reversed(label)):
_append_circuit(op, label_gates[char], qargs=[qubit])
return op
# ---------------------------------------------------------------------
# Internal helper functions
# ---------------------------------------------------------------------
@staticmethod
def _is_symplectic(mat):
"""Return True if input is symplectic matrix."""
# Condition is
# table.T * [[0, 1], [1, 0]] * table = [[0, 1], [1, 0]]
# where we are block matrix multiplying using symplectic product
dim = len(mat) // 2
if mat.shape != (2 * dim, 2 * dim):
return False
one = np.eye(dim, dtype=np.int)
zero = np.zeros((dim, dim), dtype=np.int)
seye = np.block([[zero, one], [one, zero]])
arr = mat.astype(np.int)
return np.array_equal(np.mod(arr.T.dot(seye).dot(arr), 2), seye)
@staticmethod
def _conjugate_transpose(clifford, method):
"""Return the adjoint, conjugate, or transpose of the Clifford.
Args:
clifford (Clifford): a clifford object.
method (str): what function to apply 'A', 'C', or 'T'.
Returns:
Clifford: the modified clifford.
"""
ret = clifford.copy()
if method in ['A', 'T']:
# Apply inverse
# Update table
tmp = ret.destabilizer.X.copy()
ret.destabilizer.X = ret.stabilizer.Z.T
ret.destabilizer.Z = ret.destabilizer.Z.T
ret.stabilizer.X = ret.stabilizer.X.T
ret.stabilizer.Z = tmp.T
# Update phase
ret.table.phase ^= clifford.dot(ret).table.phase
if method in ['C', 'T']:
# Apply conjugate
ret.table.phase ^= np.mod(np.sum(
ret.table.X & ret.table.Z, axis=1), 2).astype(np.bool)
return ret
def _tensor_product(self, other, reverse=False):
"""Return the tensor product operator.
Args:
other (Clifford): another Clifford operator.
reverse (bool): If False return self ⊗ other, if True return
if True return (other ⊗ self) [Default: False].
Returns:
Clifford: the tensor product operator.
Raises:
QiskitError: if other cannot be converted into an Clifford.
"""
if not isinstance(other, Clifford):
other = Clifford(other)
if reverse:
cliff0 = self
cliff1 = other
else:
cliff0 = other
cliff1 = self
# Pad stabilizers and destabilizers
destab = (cliff0.destabilizer.expand(cliff1.num_qubits * 'I') +
cliff1.destabilizer.tensor(cliff0.num_qubits * 'I'))
stab = (cliff0.stabilizer.expand(cliff1.num_qubits * 'I') +
cliff1.stabilizer.tensor(cliff0.num_qubits * 'I'))
# Add the padded table
return Clifford(destab + stab, validate=False)
def _pad_with_identity(self, clifford, qargs):
"""Pad Clifford with identities on other subsystems."""
if qargs is None:
return clifford
padded = Clifford(StabilizerTable(
np.eye(2 * self.num_qubits, dtype=np.bool)), validate=False)
inds = list(qargs) + [self.num_qubits + i for i in qargs]
# Pad Pauli array
pauli = clifford.table.array
for i, pos in enumerate(qargs):
padded.table.array[inds, pos] = pauli[:, i]
padded.table.array[inds, self.num_qubits + pos] = pauli[:, clifford.num_qubits + i]
# Pad phase
padded.table.phase[inds] = clifford.table.phase
return padded
def _compose_clifford(self, other, front=False):
"""Return the composition channel assume other is Clifford of same size as self."""
if front:
table1 = self.table
table2 = other.table
else:
table1 = other.table
table2 = self.table
num_qubits = self.num_qubits
array1 = table1.array.astype(int)
phase1 = table1.phase.astype(int)
array2 = table2.array.astype(int)
phase2 = table2.phase.astype(int)
# Update Pauli table
pauli = StabilizerTable(array2.dot(array1) % 2)
# Add phases
phase = np.mod(array2.dot(phase1) + phase2, 2)
# Correcting for phase due to Pauli multiplication
ifacts = np.zeros(2 * num_qubits, dtype=np.int)
for k in range(2 * num_qubits):
row2 = array2[k]
x2 = table2.X[k]
z2 = table2.Z[k]
# Adding a factor of i for each Y in the image of an operator under the
# first operation, since Y=iXZ
ifacts[k] += np.sum(x2 & z2)
# Adding factors of i due to qubit-wise Pauli multiplication
for j in range(num_qubits):
x = 0
z = 0
for i in range(2 * num_qubits):
if row2[i]:
x1 = array1[i, j]
z1 = array1[i, j + num_qubits]
if (x | z) & (x1 | z1):
val = np.mod(np.abs(3 * z1 - x1) - np.abs(3 * z - x) - 1, 3)
if val == 0:
ifacts[k] += 1
elif val == 1:
ifacts[k] -= 1
x = np.mod(x + x1, 2)
z = np.mod(z + z1, 2)
p = np.mod(ifacts, 4) // 2
phase = np.mod(phase + p, 2)
return Clifford(StabilizerTable(pauli, phase), validate=False)