diff --git a/qiskit/quantum_info/operators/symplectic/clifford_circuits.py b/qiskit/quantum_info/operators/symplectic/clifford_circuits.py index ba91d85fa4b4..437913034764 100644 --- a/qiskit/quantum_info/operators/symplectic/clifford_circuits.py +++ b/qiskit/quantum_info/operators/symplectic/clifford_circuits.py @@ -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 diff --git a/qiskit/transpiler/passes/optimization/collect_cliffords.py b/qiskit/transpiler/passes/optimization/collect_cliffords.py index 0eea7563b1ef..762fe3e79051 100644 --- a/qiskit/transpiler/passes/optimization/collect_cliffords.py +++ b/qiskit/transpiler/passes/optimization/collect_cliffords.py @@ -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): diff --git a/releasenotes/notes/improve-collect-cliffords-f57aeafe95460b18.yaml b/releasenotes/notes/improve-collect-cliffords-f57aeafe95460b18.yaml new file mode 100644 index 000000000000..52a9b8ab8e59 --- /dev/null +++ b/releasenotes/notes/improve-collect-cliffords-f57aeafe95460b18.yaml @@ -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. diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index f251459ef1c1..8009b4541d2b 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -31,6 +31,8 @@ XGate, YGate, ZGate, + LinearFunction, + PauliGate, ) from qiskit.exceptions import QiskitError from qiskit.quantum_info import random_clifford @@ -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): diff --git a/test/python/transpiler/test_clifford_passes.py b/test/python/transpiler/test_clifford_passes.py index 7e443aab70b3..b610f59fa1ff 100644 --- a/test/python/transpiler/test_clifford_passes.py +++ b/test/python/transpiler/test_clifford_passes.py @@ -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 @@ -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()