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

Constructing Cliffords from quantum circuits with other Cliffords #9169

Merged
merged 21 commits into from
Jan 19, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
064735e
Fix to visualize circuits containing cliffords
alexanderivrii Nov 20, 2022
f8c0067
Fix to creating cliffords from clifford circuits that contain Cliffor…
alexanderivrii Nov 20, 2022
055d5ed
Merge branch 'main' into clifford-fixes
alexanderivrii Nov 24, 2022
899b111
Merge branch 'main' into clifford-fixes
alexanderivrii Dec 29, 2022
42ecdc2
removing comment
alexanderivrii Dec 29, 2022
c5189b6
extending CollectCliffords to circuits with cliffords, linear functio…
alexanderivrii Dec 29, 2022
194e308
Addings tests
alexanderivrii Dec 29, 2022
b98bc62
typo
alexanderivrii Dec 29, 2022
66a18d4
release notes
alexanderivrii Dec 29, 2022
14d5bd5
Merge branch 'main' into clifford-fixes
alexanderivrii Jan 9, 2023
0f7990f
Merge branch 'main' into clifford-fixes
alexanderivrii Jan 11, 2023
449cfcd
adding a test that a Clifford can be constructed from a quantum circu…
alexanderivrii Jan 11, 2023
e779ff9
Merge branch 'main' into clifford-fixes
alexanderivrii Jan 11, 2023
17f7ce0
Removing redundant copy
alexanderivrii Jan 12, 2023
e5b9850
Merge branch 'main' into clifford-fixes
alexanderivrii Jan 19, 2023
d9d9c17
updating release notes
alexanderivrii Jan 19, 2023
704d385
Improve test
alexanderivrii Jan 19, 2023
468d4ed
Merge branch 'main' into clifford-fixes
mergify[bot] Jan 19, 2023
4effb64
Merge branch 'main' into clifford-fixes
mergify[bot] Jan 19, 2023
76fb841
Merge branch 'main' into clifford-fixes
mergify[bot] Jan 19, 2023
bb02719
Merge branch 'main' into clifford-fixes
mtreinish Jan 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.copy(), qargs=qargs.copy(), front=False)
alexanderivrii marked this conversation as resolved.
Show resolved Hide resolved
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 @@
---
upgrade:
- |
Upgrading :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`.
jakelishman marked this conversation as resolved.
Show resolved Hide resolved
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.
54 changes: 54 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,58 @@ 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])

# Check that Clifford can be constructed from such circuit.
Clifford(circuit)
jakelishman marked this conversation as resolved.
Show resolved Hide resolved


@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))
ShellyGarion marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +561 to +563
Copy link
Member

Choose a reason for hiding this comment

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

These tests use equiv rather than == - the former normalises global phase, so == is a strong condition. Do we need the global-phase fuzziness here? If so, it seems a bit unnerving, since the transpiler is supposed to maintain the global phase entirely.

(Comment repeats for all the tests here.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ouch, your comment points to a real problem with OptimizeCliffords transpiler pass, that I should have but did not think about.

The problem is that Cliffords are only defined up to a global phase: while "in the real world" XY = iZ, when using Clifford's stabilizer/destabilizer tableaus, the multiplication by i get dropped, i.e. "in the Clifford world" XY=Z. This is also the reason why most of the tests that construct an Operator out of Clifford in test_clifford.py check for equiv and not for ==.

The above in code:

qc = QuantumCircuit(1)
qc.x(0)
qc.y(0)
print(Operator(Clifford(qc)) == Operator(qc))

print the value False.

So right now replacing a block of Clifford gates by a Clifford may drop the global phase, which is indeed the problem to "the transpiler maintaining the global phase". That is, the OptimizeCliffords transpiler pass can only guarantee equivalence up to a global phase.

Note: I do believe that when optimizing Cliffords over a subset of qubits the problem is still only with global and not local phases.

BTW, I thought there was an effort to incorporate the full phase into the Clifford class, maybe @ShellyGarion or @ikkoham can comment? This would immediately solve our problems here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I don't think we should replace equiv by == in these tests. However, we should see if there is already a solution for maintaining the global phase. And if not we should clearly document this caveat in the documentation for OptimizeCliffords transpiler pass. What do you think?

Copy link
Member

Choose a reason for hiding this comment

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

That sounds like a sensible plan to me. If the phase non-equivalence is already in the pass and in Terra 0.22, then there's no massive rush to fix it now before the release, and anyway, we can treat it as a bug fix. Let's open a new issue and discuss the right way forwards.


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