Skip to content

Commit

Permalink
Constructing Cliffords from quantum circuits with other Cliffords (#9169
Browse files Browse the repository at this point in the history
)

* Fix to visualize circuits containing cliffords

* Fix to creating cliffords from clifford circuits that contain Clifford objects

* removing comment

* extending CollectCliffords to circuits with cliffords, linear functions and pauli gates

* Addings tests

* typo

* release notes

* adding a test that a Clifford can be constructed from a quantum circuit with various clifford-like objects

* Removing redundant copy

* updating release notes

* Improve test

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Matthew Treinish <[email protected]>
  • Loading branch information
3 people authored Jan 19, 2023
1 parent b2311c4 commit 13bb17c
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 1 deletion.
12 changes: 12 additions & 0 deletions qiskit/quantum_info/operators/symplectic/clifford_circuits.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ 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 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.

# pylint: disable=cyclic-import
from qiskit.quantum_info import Clifford

if isinstance(gate, Clifford):
composed_clifford = clifford.compose(gate, qargs=qargs, front=False)
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
Expand Down
16 changes: 15 additions & 1 deletion qiskit/transpiler/passes/optimization/collect_cliffords.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,21 @@ def __init__(self, do_commutative_analysis=False, split_blocks=True, min_block_s
)


clifford_gate_names = ["x", "y", "z", "h", "s", "sdg", "cx", "cy", "cz", "swap"]
clifford_gate_names = [
"x",
"y",
"z",
"h",
"s",
"sdg",
"cx",
"cy",
"cz",
"swap",
"clifford",
"linear_function",
"pauli",
]


def _is_clifford_gate(node):
Expand Down
47 changes: 47 additions & 0 deletions releasenotes/notes/improve-collect-cliffords-f57aeafe95460b18.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
features:
- |
Extending :class:`~CollectCliffords` transpiler pass to collect and combine blocks
of "clifford gates" into :class:`qiskit.quantum_info.Clifford` objects, where the
"clifford gates" may now also include objects of type :class:`.LinearFunction`,
:class:`qiskit.quantum_info.Clifford`, and :class:`~qiskit.circuit.library.PauliGate`.
As an example::
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import LinearFunction, PauliGate
from qiskit.quantum_info.operators import Clifford
from qiskit.transpiler.passes import CollectCliffords
from qiskit.transpiler import PassManager
# Create a Clifford
cliff_circuit = QuantumCircuit(2)
cliff_circuit.cx(0, 1)
cliff_circuit.h(0)
cliff = Clifford(cliff_circuit)
# Create a linear function
lf = LinearFunction([[0, 1], [1, 0]])
# Create a pauli gate
pauli_gate = PauliGate("XYZ")
# Create a quantum circuit with the above and also simple clifford gates.
qc = QuantumCircuit(4)
qc.cz(0, 1)
qc.append(cliff, [0, 1])
qc.h(0)
qc.append(lf, [0, 2])
qc.append(pauli_gate, [0, 2, 1])
qc.x(2)
# Run CollectCliffords transpiler pass
qct = PassManager(CollectCliffords()).run(qc)
All the gates will be collected and combined into a single Clifford. Thus the final
circuit consists of a single Clifford object.
fixes:
- |
Restoring the functionality to construct :class:`qiskit.quantum_info.Clifford`
objects from quantum circuits containing other :class:`qiskit.quantum_info.Clifford`
objects.
62 changes: 62 additions & 0 deletions test/python/quantum_info/operators/symplectic/test_clifford.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
XGate,
YGate,
ZGate,
LinearFunction,
PauliGate,
)
from qiskit.exceptions import QiskitError
from qiskit.quantum_info import random_clifford
Expand Down Expand Up @@ -432,6 +434,66 @@ def test_from_circuit_with_conditional_gate(self):
with self.assertRaises(QiskitError):
Clifford(qc)

def test_from_circuit_with_other_clifford(self):
"""Test initialization from circuit containing another clifford."""
cliff = random_clifford(1, seed=777)
qc = QuantumCircuit(1)
qc.append(cliff, [0])
cliff1 = Clifford(qc)
self.assertEqual(cliff, cliff1)

def test_from_circuit_with_multiple_cliffords(self):
"""Test initialization from circuit containing multiple clifford."""
cliff1 = random_clifford(2, seed=777)
cliff2 = random_clifford(2, seed=999)

# Append the two cliffords to circuit and create the clifford from this circuit
qc1 = QuantumCircuit(3)
qc1.append(cliff1, [0, 1])
qc1.append(cliff2, [1, 2])
expected_cliff1 = Clifford(qc1)

# Compose the two cliffords directly
qc2 = QuantumCircuit(3)
expected_cliff2 = Clifford(qc2)
expected_cliff2 = Clifford.compose(expected_cliff2, cliff1, qargs=[0, 1], front=False)
expected_cliff2 = Clifford.compose(expected_cliff2, cliff2, qargs=[1, 2], front=False)
self.assertEqual(expected_cliff1, expected_cliff2)

def test_from_circuit_with_all_types(self):
"""Test initialization from circuit containing various Clifford-like objects."""

# Construct objects that can go onto a Clifford circuit.
# These include regular clifford gates, linear functions, Pauli gates, other Clifford,
# and even circuits with other clifford objects.
linear_function = LinearFunction([[0, 1], [1, 1]])
pauli_gate = PauliGate("YZ")
cliff = random_clifford(2, seed=777)
qc = QuantumCircuit(2)
qc.cx(0, 1)
qc.append(random_clifford(1, seed=999), [1])

# Construct a quantum circuit with these objects and convert it to clifford
circuit = QuantumCircuit(3)
circuit.h(0)
circuit.append(linear_function, [0, 2])
circuit.cz(0, 1)
circuit.append(pauli_gate, [2, 1])
circuit.append(cliff, [0, 1])
circuit.swap(0, 2)
circuit.append(qc, [0, 1])

# Make sure that Clifford can be constructed from such circuit.
combined_clifford = Clifford(circuit)

# Additionally, make sure that it produces the correct clifford.
expected_clifford_dict = {
"stabilizer": ["-IZX", "+ZYZ", "+ZII"],
"destabilizer": ["+ZIZ", "+ZXZ", "-XIX"],
}
expected_clifford = Clifford.from_dict(expected_clifford_dict)
self.assertEqual(combined_clifford, expected_clifford)


@ddt
class TestCliffordSynthesis(QiskitTestCase):
Expand Down
117 changes: 117 additions & 0 deletions test/python/transpiler/test_clifford_passes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import numpy as np

from qiskit.circuit import QuantumCircuit, Gate
from qiskit.circuit.library import LinearFunction, PauliGate
from qiskit.converters import dag_to_circuit, circuit_to_dag
from qiskit.dagcircuit import DAGOpNode
from qiskit.transpiler.passes import HighLevelSynthesis
Expand Down Expand Up @@ -536,6 +537,122 @@ def test_do_not_merge_conditional_gates(self):
# Make sure that the condition on the middle gate is not lost
self.assertIsNotNone(qct.data[1].operation.condition)

def test_collect_with_cliffords(self):
"""Make sure that collecting Clifford gates and replacing them by Clifford
works correctly when the gates include other cliffords."""

# Create a Clifford over 2 qubits
cliff_circuit = QuantumCircuit(2)
cliff_circuit.cx(0, 1)
cliff_circuit.h(0)
cliff = Clifford(cliff_circuit)

qc = QuantumCircuit(3)
qc.h(0)
qc.append(cliff, [1, 0])
qc.cx(1, 2)

# Collect clifford gates from the circuit (in this case all the gates must be collected).
qct = PassManager(CollectCliffords()).run(qc)
self.assertEqual(len(qct.data), 1)

# Make sure that the operator for the initial quantum circuit is equivalent to the
# operator for the collected clifford.
op1 = Operator(qc)
op2 = Operator(qct)
self.assertTrue(op1.equiv(op2))

def test_collect_with_linear_functions(self):
"""Make sure that collecting Clifford gates and replacing them by Clifford
works correctly when the gates include LinearFunctions."""

# Create a linear function over 2 qubits
lf = LinearFunction([[0, 1], [1, 0]])

qc = QuantumCircuit(3)
qc.h(0)
qc.append(lf, [1, 0])
qc.cx(1, 2)

# Collect clifford gates from the circuit (in this case all the gates must be collected).
qct = PassManager(CollectCliffords()).run(qc)
self.assertEqual(len(qct.data), 1)

# Make sure that the operator for the initial quantum circuit is equivalent to the
# operator for the collected clifford.
op1 = Operator(qc)
op2 = Operator(qct)
self.assertTrue(op1.equiv(op2))

def test_collect_with_pauli_gates(self):
"""Make sure that collecting Clifford gates and replacing them by Clifford
works correctly when the gates include PauliGates."""

# Create a pauli gate over 2 qubits
pauli_gate = PauliGate("XY")

qc = QuantumCircuit(3)
qc.h(0)
qc.append(pauli_gate, [1, 0])
qc.cx(1, 2)

# Collect clifford gates from the circuit (in this case all the gates must be collected).
qct = PassManager(CollectCliffords()).run(qc)
self.assertEqual(len(qct.data), 1)

# Make sure that the operator for the initial quantum circuit is equivalent to the
# operator for the collected clifford.
op1 = Operator(qc)
op2 = Operator(qct)
self.assertTrue(op1.equiv(op2))

def test_collect_with_all_types(self):
"""Make sure that collecting Clifford gates and replacing them by Clifford
works correctly when the gates include all possible clifford gate types."""

cliff_circuit0 = QuantumCircuit(1)
cliff_circuit0.h(0)
cliff0 = Clifford(cliff_circuit0)

cliff_circuit1 = QuantumCircuit(2)
cliff_circuit1.cz(0, 1)
cliff_circuit1.s(1)
cliff1 = Clifford(cliff_circuit1)

lf1 = LinearFunction([[0, 1], [1, 1]])
lf2 = LinearFunction([[0, 1, 0], [1, 0, 0], [0, 0, 1]])

pauli_gate1 = PauliGate("X")
pauli_gate2 = PauliGate("YZX")

qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0, 1)
qc.append(cliff0, [1])
qc.cy(0, 1)

# not a clifford gate (separating the circuit)
qc.rx(np.pi / 2, 0)

qc.append(pauli_gate2, [0, 2, 1])
qc.append(lf2, [2, 1, 0])
qc.x(0)
qc.append(pauli_gate1, [1])
qc.append(lf1, [1, 0])
qc.h(2)
qc.append(cliff1, [1, 2])

# Collect clifford gates from the circuit (we should get two Clifford blocks separated by
# the RX gate).
qct = PassManager(CollectCliffords()).run(qc)
self.assertEqual(len(qct.data), 3)

# Make sure that the operator for the initial quantum circuit is equivalent to the
# operator for the circuit with the collected cliffords.
op1 = Operator(qc)
op2 = Operator(qct)
self.assertTrue(op1.equiv(op2))


if __name__ == "__main__":
unittest.main()

0 comments on commit 13bb17c

Please sign in to comment.