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

Allow partition_labels to be determined automatically in partition_problem #367

Merged
merged 1 commit into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 28 additions & 15 deletions circuit_knitting/cutting/cutting_decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator

@caleb-johnson caleb-johnson Aug 16, 2023

Choose a reason for hiding this comment

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

Maybe we should make this function public in another PR as well?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Since we're in a time crunch, we can push this through and make that function public next release. I'd be fine with that too. Up to you

from .qpd.qpd_basis import QPDBasis
from .qpd.instructions import TwoQubitQPDGate

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -156,27 +153,36 @@ 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


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
Expand All @@ -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})."
Expand All @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions circuit_knitting/cutting/qpd/instructions/qpd_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
9 changes: 7 additions & 2 deletions circuit_knitting/utils/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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 :]:
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 16 additions & 0 deletions test/cutting/test_cutting_decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down