Qdislib — 03 · Finding cuts

find_cut decides where to cut. You steer it with a few knobs — the device size (max_qubits), whether to look for gate or wire cuts, a cap on the number of cuts (max_cuts), and the partitioning implementation — and it returns the gates to cut, e.g. ['CZ_3'].

find_cut(circuit, max_qubits=None, max_cuts=None,
         wire_cut=True, gate_cut=True, implementation='qdislib', weights=None)
[ ]:
from qibo import models, gates


def example_circuit():
    """A small 5-qubit circuit: cuttable (CZ chain), non-trivial <ZZZZZ> ~= 0.44."""
    circuit = models.Circuit(5)
    circuit.add(gates.RY(0, 0.8))
    circuit.add(gates.CZ(0, 1))
    circuit.add(gates.RY(1, 0.5))
    circuit.add(gates.CZ(1, 2))
    circuit.add(gates.RY(2, 0.6))
    circuit.add(gates.CZ(2, 3))
    circuit.add(gates.RY(3, 0.4))
    circuit.add(gates.CZ(3, 4))
    circuit.add(gates.RX(4, 0.3))
    return circuit


circuit = example_circuit()
print(circuit.draw())
[ ]:
import Qdislib as qd

Gate cuts vs wire cuts

A gate cut splits a two-qubit gate; a wire cut severs a qubit wire between gates. find_cut searches both by default — restrict it with gate_cut= / wire_cut= (at least one must be enabled, or it raises).

[ ]:
circuit = example_circuit()
print("gate only:", qd.find_cut(circuit, gate_cut=True, wire_cut=False))
print("wire only:", qd.find_cut(circuit, gate_cut=False, wire_cut=True))
print("both     :", qd.find_cut(circuit, gate_cut=True, wire_cut=True))

Fit your device: max_qubits

max_qubits caps the qubits per subcircuit — the key hardware constraint. A smaller device forces the circuit into more (smaller) pieces: find_cut aims for about ceil(n_qubits / max_qubits) components.

[ ]:
for mq in (4, 3, 2):
    cut = qd.find_cut(example_circuit(), gate_cut=True, wire_cut=False, max_qubits=mq)
    print(f"max_qubits={mq}: {cut}")

Cap the overhead: max_cuts

Each gate cut multiplies the reconstruction cost by 6 (6**n_cuts), so max_cuts bounds how many cuts find_cut is allowed to use.

[ ]:
gc = lambda **kw: qd.find_cut(example_circuit(), gate_cut=True, wire_cut=False, **kw)
print("max_cuts=1:      ", gc(max_cuts=1))
print("max_qubits=2:    ", gc(max_qubits=2))    # wants 2 cuts (two 2-3 qubit pieces)
print("both (too tight):", gc(max_qubits=2, max_cuts=1))  # no cut satisfies both -> []

Partitioner: implementation

implementation='qdislib' (default) scores several graph-partitioning strategies — Kernighan–Lin, Girvan–Newman, spectral, METIS — with a loss function and keeps the best. implementation='ibm-ckt' instead uses Qiskit’s hardware-aware cutter (gate cuts only; needs the qiskit extra and a Qiskit circuit):

[ ]:
from qiskit import QuantumCircuit
qc = QuantumCircuit(10)
for i in range(9):
    qc.cz(i, i + 1)
cut = qd.find_cut(qc, max_qubits=5, gate_cut=True, wire_cut=False,
                  implementation='ibm-ckt')
print(cut)

🔧 Your turn

Turn on qd.set_log_level('DEBUG') and re-run find_cut — it logs the candidate partitions, their loss scores, and which strategy won. (Advanced: pass weights={...} to retune that loss, which trades off the number of cuts, the number of components, and the largest component’s size.)

[ ]:
qd.set_log_level("DEBUG")
cut = qd.find_cut(example_circuit(), gate_cut=True, wire_cut=False, max_qubits=3)
qd.set_log_level("WARNING")   # quiet again
print("chosen cut:", cut)

Visualise the cut: draw_cut

draw_cut(circuit, cut, filename) renders the circuit’s DAG with the cut gates highlighted, so you can see exactly where the pieces split.

[ ]:
cut = qd.find_cut(example_circuit(), gate_cut=True, wire_cut=False)
qd.draw_cut(example_circuit(), cut, "cut.png")   # writes cut.png
print("wrote cut.png with the cut highlighted")

Next: 04 · Backends & QASM interop — run the pieces on Qibo, Qiskit or CUDA-Q, and bring circuits in from other frameworks.