Automatically find cuts using CKT

Create a circuit and observables

[1]:
import numpy as np
from qiskit.circuit.random import random_circuit
from qiskit.quantum_info import SparsePauliOp

circuit = random_circuit(7, 6, max_operands=2, seed=1242)
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])


circuit.draw("mpl", scale=0.8)
[1]:
../../_images/circuit_cutting_tutorials_04_automatic_cut_finding_2_0.png

Find cut locations, given a maximum of 4 qubits per subcircuit. This circuit can be separated in two by making a single wire cut and cutting one CRZGate

[2]:
from circuit_knitting.cutting.automated_cut_finding import (
    find_cuts,
    OptimizationParameters,
    DeviceConstraints,
)

# Specify settings for the cut-finding optimizer
optimization_settings = OptimizationParameters(seed=111)

# Specify the size of the QPUs available
device_constraints = DeviceConstraints(qubits_per_subcircuit=4)

cut_circuit, metadata = find_cuts(circuit, optimization_settings, device_constraints)
print(
    f'Found solution using {len(metadata["cuts"])} cuts with a sampling '
    f'overhead of {metadata["sampling_overhead"]}.'
)
for cut in metadata["cuts"]:
    print(f"{cut[0]} at circuit instruction index {cut[1]}")
cut_circuit.draw("mpl", scale=0.8, fold=-1)
Found solution using 2 cuts with a sampling overhead of 127.06026169907257.
Wire Cut at circuit instruction index 19
Gate Cut at circuit instruction index 28
[2]:
../../_images/circuit_cutting_tutorials_04_automatic_cut_finding_4_1.png

Add ancillas for wire cuts and expand the observables to account for ancilla qubits

[3]:
from circuit_knitting.cutting import cut_wires, expand_observables

qc_w_ancilla = cut_wires(cut_circuit)
observables_expanded = expand_observables(observable.paulis, circuit, qc_w_ancilla)
qc_w_ancilla.draw("mpl", scale=0.8, fold=-1)
[3]:
../../_images/circuit_cutting_tutorials_04_automatic_cut_finding_6_0.png

Partition the circuit and observables into subcircuits and subobservables. Calculate the sampling overhead incurred from cutting these gates and wires.

[4]:
from circuit_knitting.cutting import partition_problem

partitioned_problem = partition_problem(
    circuit=qc_w_ancilla, observables=observables_expanded
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
print(
    f"Sampling overhead: {np.prod([basis.overhead for basis in partitioned_problem.bases])}"
)
Sampling overhead: 127.06026169907257
[5]:
subobservables
[5]:
{0: PauliList(['IIII', 'IZII', 'IIIZ']),
 1: PauliList(['ZIII', 'IIII', 'IIII'])}
[6]:
subcircuits[0].draw("mpl", style="iqp", scale=0.8)
[6]:
../../_images/circuit_cutting_tutorials_04_automatic_cut_finding_10_0.png
[7]:
subcircuits[1].draw("mpl", style="iqp", scale=0.8)
[7]:
../../_images/circuit_cutting_tutorials_04_automatic_cut_finding_11_0.png

Generate the experiments to run on the backend.

[8]:
from circuit_knitting.cutting import generate_cutting_experiments

subexperiments, coefficients = generate_cutting_experiments(
    circuits=subcircuits, observables=subobservables, num_samples=1_000
)
print(
    f"{len(subexperiments[0]) + len(subexperiments[1])} total subexperiments to run on backend."
)
96 total subexperiments to run on backend.