diff --git a/circuit_knitting/cutting/cutting_experiments.py b/circuit_knitting/cutting/cutting_experiments.py index 70e947354..cfe62d438 100644 --- a/circuit_knitting/cutting/cutting_experiments.py +++ b/circuit_knitting/cutting/cutting_experiments.py @@ -19,8 +19,11 @@ import numpy as np from qiskit.circuit import QuantumCircuit, ClassicalRegister from qiskit.quantum_info import PauliList +from qiskit.transpiler import PassManager +from qiskit.transpiler.passes import RemoveResetInZeroState, DAGFixedPoint from ..utils.iteration import strict_zip +from ..utils.transpiler_passes import RemoveFinalReset, ConsolidateResets from ..utils.observable_grouping import ObservableCollection, CommutingObservableGroup from .qpd import ( WeightType, @@ -58,6 +61,12 @@ def generate_cutting_experiments( The coefficients will always be returned as a 1D array -- one coefficient for each unique sample. + Note that this function also runs some transpiler passes on each generated + circuit, namely :class:`~qiskit.transpiler.passes.RemoveResetInZeroState`, + :class:`.RemoveFinalReset`, and :class:`.ConsolidateResets`, in order to + remove unnecessary :class:`~qiskit.circuit.library.Reset`\ s from the + circuit that are added by the subexperiment decompositions for cut wires. + Args: circuits: The circuit(s) to partition and separate observables: The observable(s) to evaluate for each unique sample @@ -156,6 +165,25 @@ def generate_cutting_experiments( _append_measurement_circuit(new_qc, cog, inplace=True) subexperiments_dict[label].append(new_qc) + # Remove initial and final resets from the subexperiments. This will + # enable the `Move` operation to work on backends that don't support + # `Reset`, as long as qubits are not re-used. See + # https://github.com/Qiskit-Extensions/circuit-knitting-toolbox/issues/452. + # While we are at it, we also consolidate each run of multiple resets + # (which can arise when re-using qubits) into a single reset. + pass_manager = PassManager() + pass_manager.append( + [ + RemoveResetInZeroState(), + RemoveFinalReset(), + ConsolidateResets(), + DAGFixedPoint(), + ], + do_while=lambda property_set: not property_set["dag_fixed_point"], + ) + for label, subexperiments in subexperiments_dict.items(): + subexperiments_dict[label] = pass_manager.run(subexperiments) + # If the input was a single quantum circuit, return the subexperiments as a list subexperiments_out: list[QuantumCircuit] | dict[ Hashable, list[QuantumCircuit] diff --git a/circuit_knitting/utils/__init__.py b/circuit_knitting/utils/__init__.py index d8dd0b15e..debf2ac14 100644 --- a/circuit_knitting/utils/__init__.py +++ b/circuit_knitting/utils/__init__.py @@ -59,6 +59,12 @@ ============================================================= .. automodule:: circuit_knitting.utils.transforms + +=================================================================== +Transpiler passes (:mod:`circuit_knitting.utils.transpiler_passes`) +=================================================================== + +.. automodule:: circuit_knitting.utils.transpiler_passes """ from .orbital_reduction import reduce_bitstrings diff --git a/circuit_knitting/utils/transpiler_passes.py b/circuit_knitting/utils/transpiler_passes.py new file mode 100644 index 000000000..c4885461e --- /dev/null +++ b/circuit_knitting/utils/transpiler_passes.py @@ -0,0 +1,66 @@ +# This code is a Qiskit project. + +# (C) Copyright IBM 2023. + +# 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. + +""" +Transpiler passes useful for circuit knitting. + +.. currentmodule:: circuit_knitting.utils.transpiler_passes + +.. autosummary:: + :toctree: ../stubs/ + + RemoveFinalReset + ConsolidateResets +""" + +from qiskit.circuit import Reset, Qubit +from qiskit.dagcircuit import DAGOpNode +from qiskit.transpiler.basepasses import TransformationPass + + +class RemoveFinalReset(TransformationPass): + """Remove reset when it is the final instruction on a qubit wire.""" + + def run(self, dag): + """Run the RemoveFinalReset pass on ``dag``. + + Args: + dag (DAGCircuit): the DAG to be optimized. + + Returns: + DAGCircuit: the optimized DAG. + """ + for output_node in dag.output_map.values(): + if isinstance(output_node.wire, Qubit): + pred = next(dag.predecessors(output_node)) + if isinstance(pred, DAGOpNode) and isinstance(pred.op, Reset): + dag.remove_op_node(pred) + return dag + + +class ConsolidateResets(TransformationPass): + """Consolidate a run duplicate resets in to a single reset.""" + + def run(self, dag): + """Run the ConsolidateResets pass on ``dag``. + + Args: + dag (DAGCircuit): the DAG to be optimized. + + Returns: + DAGCircuit: the optimized DAG. + """ + resets = dag.op_nodes(Reset) + for reset in resets: + successor = next(dag.successors(reset)) + if isinstance(successor, DAGOpNode) and isinstance(successor.op, Reset): + dag.remove_op_node(reset) + return dag diff --git a/releasenotes/notes/remove-redundant-resets-1893a61a341e6ce8.yaml b/releasenotes/notes/remove-redundant-resets-1893a61a341e6ce8.yaml new file mode 100644 index 000000000..e8dbf6def --- /dev/null +++ b/releasenotes/notes/remove-redundant-resets-1893a61a341e6ce8.yaml @@ -0,0 +1,12 @@ +--- +upgrade: + - | + The :func:`.generate_cutting_experiments` function now performs + some optimizations on the generated circuits before returning them + to the user. In particular, it performs the + :class:`~qiskit.transpiler.passes.RemoveResetInZeroState`, + :class:`.RemoveFinalReset`, and :class:`.ConsolidateResets` + passes, so that circuits with cut wires and no re-used qubits are + transformed into subexperiments that contain no + :class:`~qiskit.circuit.library.Reset`\ s. This allows such circuits to + work on a greater variety of hardware backends. diff --git a/test/cutting/test_cutting_workflows.py b/test/cutting/test_cutting_workflows.py index ea5b4389f..7d3b1ed02 100644 --- a/test/cutting/test_cutting_workflows.py +++ b/test/cutting/test_cutting_workflows.py @@ -14,6 +14,7 @@ import pytest from copy import deepcopy +import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.circuit.library import EfficientSU2, CXGate from qiskit.quantum_info import PauliList @@ -23,10 +24,13 @@ from circuit_knitting.cutting.qpd.instructions import SingleQubitQPDGate from circuit_knitting.cutting.qpd import QPDBasis +from circuit_knitting.cutting.instructions import CutWire, Move from circuit_knitting.cutting import ( partition_problem, generate_cutting_experiments, reconstruct_expectation_values, + cut_wires, + expand_observables, ) @@ -109,3 +113,69 @@ def test_workflow_with_unused_qubits(): subobservables, num_samples=10, ) + + +def test_wire_cut_workflow_without_reused_qubits(): + """Test no resets in subexperiments when wire cut workflow has no re-used qubits.""" + qc = QuantumCircuit(2) + qc.h(range(2)) + qc.cx(0, 1) + qc.append(CutWire(), [0]) + qc.cx(1, 0) + + observables = PauliList(["IZ", "ZI", "ZZ", "XX"]) + + qc_1 = cut_wires(qc) + assert qc_1.num_qubits == 3 + + observables_1 = expand_observables(observables, qc, qc_1) + + partitioned_problem = partition_problem(circuit=qc_1, observables=observables_1) + + subexperiments, coefficients = generate_cutting_experiments( + circuits=partitioned_problem.subcircuits, + observables=partitioned_problem.subobservables, + num_samples=np.inf, + ) + + for subsystem_subexpts in subexperiments.values(): + for subexpt in subsystem_subexpts: + assert "reset" not in subexpt.count_ops() + + +def test_wire_cut_workflow_with_reused_qubits(): + """Test at most a single reset in subexperiments when wire cut workflow has a single re-used qubit.""" + qc = QuantumCircuit(8) + for i in [*range(4), *range(5, 8)]: + qc.rx(np.pi / 4, i) + qc.cx(0, 3) + qc.cx(1, 3) + qc.cx(2, 3) + qc.append(Move(), [3, 4]) + qc.cx(4, 5) + qc.cx(4, 6) + qc.cx(4, 7) + qc.append(Move(), [4, 3]) + qc.cx(0, 3) + qc.cx(1, 3) + qc.cx(2, 3) + + observables = PauliList(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"]) + + partitioned_problem = partition_problem( + circuit=qc, partition_labels="AAAABBBB", observables=observables + ) + + subexperiments, coefficients = generate_cutting_experiments( + circuits=partitioned_problem.subcircuits, + observables=partitioned_problem.subobservables, + num_samples=np.inf, + ) + + # The initial circuit had a single instance of qubit re-use with a Move + # instruction. Each A subexperiment should have a single reset, and each B + # subexperiment should be free of resets. + for subexpt in subexperiments["A"]: + assert subexpt.count_ops()["reset"] == 1 + for subexpt in subexperiments["B"]: + assert "reset" not in subexpt.count_ops() diff --git a/test/utils/test_transpiler_passes.py b/test/utils/test_transpiler_passes.py new file mode 100644 index 000000000..58872c7cd --- /dev/null +++ b/test/utils/test_transpiler_passes.py @@ -0,0 +1,132 @@ +# This code is a Qiskit project. + +# (C) Copyright IBM 2023. + +# 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. + +"""Tests for CKT transpilation passes.""" + +import unittest + +from qiskit import QuantumRegister, QuantumCircuit +from qiskit.transpiler import PassManager +from qiskit.transpiler.passes import DAGFixedPoint +from qiskit.converters import circuit_to_dag + +from circuit_knitting.utils.transpiler_passes import RemoveFinalReset, ConsolidateResets + + +class TestRemoveFinalReset(unittest.TestCase): + """Test remove-reset-in-zero-state optimizations.""" + + def test_optimize_single_reset(self): + """Remove a single final reset + qr0:--[H]--|0>-- ==> qr0:--[H]-- + """ + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + circuit.h(0) + circuit.reset(qr) + dag = circuit_to_dag(circuit) + + expected = QuantumCircuit(qr) + expected.h(0) + + pass_ = RemoveFinalReset() + after = pass_.run(dag) + + self.assertEqual(circuit_to_dag(expected), after) + + def test_dont_optimize_non_final_reset(self): + """Do not remove reset if not final instruction + qr0:--|0>--[H]-- ==> qr0:--|0>--[H]-- + """ + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + circuit.reset(qr) + circuit.h(qr) + dag = circuit_to_dag(circuit) + + expected = QuantumCircuit(qr) + expected.reset(qr) + expected.h(qr) + + pass_ = RemoveFinalReset() + after = pass_.run(dag) + + self.assertEqual(circuit_to_dag(expected), after) + + def test_optimize_single_reset_in_diff_qubits(self): + """Remove a single final reset in different qubits + qr0:--[H]--|0>-- qr0:--[H]-- + ==> + qr1:--[X]--|0>-- qr1:--[X]---- + """ + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.h(0) + circuit.x(1) + circuit.reset(qr) + dag = circuit_to_dag(circuit) + + expected = QuantumCircuit(qr) + expected.h(0) + expected.x(1) + + pass_ = RemoveFinalReset() + after = pass_.run(dag) + + self.assertEqual(circuit_to_dag(expected), after) + + +class TestRemoveFinalResetFixedPoint(unittest.TestCase): + """Test RemoveFinalReset in a transpiler, using fixed point.""" + + def test_two_resets(self): + """Remove two final resets + qr0:--[H]-|0>-|0>-- ==> qr0:--[H]-- + """ + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + circuit.h(qr[0]) + circuit.reset(qr[0]) + circuit.reset(qr[0]) + + expected = QuantumCircuit(qr) + expected.h(qr[0]) + + pass_manager = PassManager() + pass_manager.append( + [RemoveFinalReset(), DAGFixedPoint()], + do_while=lambda property_set: not property_set["dag_fixed_point"], + ) + after = pass_manager.run(circuit) + + self.assertEqual(expected, after) + + +class TestConsolidateResets(unittest.TestCase): + """Test consolidate-resets optimization.""" + + def test_consolidate_double_reset(self): + """Consolidate a pair of resets. + qr0:--|0>--|0>-- ==> qr0:--|0>-- + """ + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + circuit.reset(qr) + circuit.reset(qr) + dag = circuit_to_dag(circuit) + + expected = QuantumCircuit(qr) + expected.reset(qr) + + pass_ = ConsolidateResets() + after = pass_.run(dag) + + self.assertEqual(circuit_to_dag(expected), after)