diff --git a/circuit_knitting/cutting/cutting_decomposition.py b/circuit_knitting/cutting/cutting_decomposition.py index e8b96928a..9e367c3f1 100644 --- a/circuit_knitting/cutting/cutting_decomposition.py +++ b/circuit_knitting/cutting/cutting_decomposition.py @@ -26,7 +26,7 @@ from qiskit.quantum_info import PauliList from ..utils.observable_grouping import observables_restricted_to_subsystem -from ..utils.transforms import separate_circuit +from ..utils.transforms import separate_circuit, _partition_labels_from_circuit from .qpd.qpd_basis import QPDBasis from .qpd.instructions import TwoQubitQPDGate @@ -96,10 +96,7 @@ def partition_circuit_qubits( if isinstance(instruction.operation, TwoQubitQPDGate): continue - decomposition = QPDBasis.from_gate(instruction.operation) - qpd_gate = TwoQubitQPDGate( - decomposition, label=f"cut_{instruction.operation.name}" - ) + qpd_gate = TwoQubitQPDGate.from_instruction(instruction.operation) circuit.data[i] = CircuitInstruction(qpd_gate, qubits=qubit_indices) return circuit @@ -156,9 +153,8 @@ def cut_gates( for gate_id in gate_ids: gate = circuit.data[gate_id] qubit_indices = [circuit.find_bit(qubit).index for qubit in gate.qubits] - decomposition = QPDBasis.from_gate(gate.operation) - bases.append(decomposition) - qpd_gate = TwoQubitQPDGate(decomposition, label=f"cut_{gate.operation.name}") + qpd_gate = TwoQubitQPDGate.from_instruction(gate.operation) + bases.append(qpd_gate.basis) circuit.data[gate_id] = CircuitInstruction(qpd_gate, qubits=qubit_indices) return circuit, bases @@ -166,17 +162,27 @@ def cut_gates( def partition_problem( circuit: QuantumCircuit, - partition_labels: Sequence[str | int], + partition_labels: Sequence[str | int] | None = None, observables: PauliList | None = None, ) -> PartitionedCuttingProblem: r""" - Separate an input circuit and observable(s) along qubit partition labels. + Separate an input circuit and observable(s). + + If ``partition_labels`` is provided, then qubits with matching partition + labels will be grouped together, and non-local gates spanning more than one + partition will be cut. + + If ``partition_labels`` is not provided, then it will be determined + automatically from the connectivity of the circuit. This automatic + determination ignores any :class:`.TwoQubitQPDGate`\ s in the ``circuit``, + as these denote instructions that are explicitly destined for cutting. The + resulting partition labels, in the automatic case, will be consecutive + integers starting with 0. - Circuit qubits with matching partition labels will be grouped together, and non-local - gates spanning more than one partition will be replaced with :class:`.SingleQubitQPDGate`\ s. + All cut instructions will be replaced with :class:`.SingleQubitQPDGate`\ s. - If provided, the observables will be separated along the boundaries specified by - ``partition_labels``. + If provided, ``observables`` will be separated along the boundaries specified by + the partition labels. Args: circuit: The circuit to partition and separate @@ -196,7 +202,7 @@ def partition_problem( ValueError: An input observable has a phase not equal to 1. ValueError: The input circuit should contain no classical bits or registers. """ - if len(partition_labels) != circuit.num_qubits: + if partition_labels is not None and len(partition_labels) != circuit.num_qubits: raise ValueError( f"The number of partition labels ({len(partition_labels)}) must equal the number " f"of qubits in the circuit ({circuit.num_qubits})." @@ -215,6 +221,13 @@ def partition_problem( "Circuits input to execute_experiments should contain no classical registers or bits." ) + # Determine partition labels from connectivity (ignoring TwoQubitQPDGates) + # if partition_labels is not specified + if partition_labels is None: + partition_labels = _partition_labels_from_circuit( + circuit, ignore=lambda inst: isinstance(inst.operation, TwoQubitQPDGate) + ) + # Partition the circuit with TwoQubitQPDGates and assign the order via their labels qpd_circuit = partition_circuit_qubits(circuit, partition_labels) diff --git a/circuit_knitting/cutting/qpd/instructions/qpd_gate.py b/circuit_knitting/cutting/qpd/instructions/qpd_gate.py index 555699d6b..480b4888b 100644 --- a/circuit_knitting/cutting/qpd/instructions/qpd_gate.py +++ b/circuit_knitting/cutting/qpd/instructions/qpd_gate.py @@ -144,6 +144,12 @@ def _define(self) -> None: self.definition = qc + @classmethod + def from_instruction(cls, instruction: Instruction, /): + """Create a :class:`TwoQubitQPDGate` which represents a cut version of the given ``instruction``.""" + decomposition = QPDBasis.from_gate(instruction) + return TwoQubitQPDGate(decomposition, label=f"cut_{instruction.name}") + class SingleQubitQPDGate(BaseQPDGate): """ diff --git a/circuit_knitting/utils/transforms.py b/circuit_knitting/utils/transforms.py index a88898adb..2cd0bd372 100644 --- a/circuit_knitting/utils/transforms.py +++ b/circuit_knitting/utils/transforms.py @@ -30,7 +30,7 @@ from uuid import uuid4, UUID from collections import defaultdict from collections.abc import Sequence, Iterable, Hashable, MutableMapping -from typing import NamedTuple +from typing import NamedTuple, Callable from rustworkx import PyGraph, connected_components # type: ignore[attr-defined] from qiskit.circuit import ( @@ -134,12 +134,17 @@ def separate_circuit( return SeparatedCircuits(subcircuits, qubit_map) -def _partition_labels_from_circuit(circuit: QuantumCircuit) -> list[int]: +def _partition_labels_from_circuit( + circuit: QuantumCircuit, + ignore: Callable[[CircuitInstruction], bool] = lambda instr: False, +) -> list[int]: """Generate partition labels from the connectivity of a quantum circuit.""" # Determine connectivity structure of the circuit graph: PyGraph = PyGraph() graph.add_nodes_from(range(circuit.num_qubits)) for instruction in circuit.data: + if ignore(instruction): + continue qubits = instruction.qubits for i, q1 in enumerate(qubits): for q2 in qubits[i + 1 :]: diff --git a/releasenotes/notes/automatic-partition-labels-f90428f66ec5543a.yaml b/releasenotes/notes/automatic-partition-labels-f90428f66ec5543a.yaml new file mode 100644 index 000000000..8576487ed --- /dev/null +++ b/releasenotes/notes/automatic-partition-labels-f90428f66ec5543a.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + :func:`.partition_problem` now works even if ``partition_labels`` + is not explicitly provided. In this case, the labels are + determined automatically from the connectivity of the input + circuit. For the sake of determining connectivity, + :class:`.TwoQubitQPDGate`\ s are ignored, as these instructions + are already marked for cutting. To support this workflow, this + release also introduces a new method, + :meth:`.TwoQubitQPDGate.from_instruction`, which allows one to + create a :class:`.TwoQubitQPDGate` that wraps a given instruction. diff --git a/test/cutting/test_cutting_decomposition.py b/test/cutting/test_cutting_decomposition.py index ee2537809..1ee2c0aee 100644 --- a/test/cutting/test_cutting_decomposition.py +++ b/test/cutting/test_cutting_decomposition.py @@ -240,6 +240,22 @@ def test_partition_problem(self): qc, "AABB", observables=PauliList(["IZIZ"]) ) assert len(subcircuits) == len(bases) == len(subobservables) == 2 + with self.subTest("Automatic partition_labels"): + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 2) + qc.cx(0, 1) + qc.s(3) + # Add a TwoQubitQPDGate that, when cut, allows the circuit to + # separate + qc.append(TwoQubitQPDGate.from_instruction(CXGate()), [1, 3]) + # Add a TwoQubitQPDGate that, when cut, does *not* allow the + # circuit to separate + qc.append(TwoQubitQPDGate.from_instruction(CXGate()), [2, 0]) + subcircuit, *_ = partition_problem(qc) + assert subcircuit.keys() == {0, 1} + assert subcircuit[0].num_qubits == 3 + assert subcircuit[1].num_qubits == 1 def test_cut_gates(self): with self.subTest("simple circuit"):