Wire cutting for expectation values estimation
Usage estimate: 22 seconds on a Heron processor (NOTE: This is an estimate only. Your runtime might vary.)
Learning outcomes
After going through this tutorial, users should understand:
- How to use
qiskit-addon-cuttingto partition a large circuit into smaller subcircuits, thereby reducing the effect of noise
Prerequisites
We suggest that users are familiar with the following topic before going through this tutorial:
- Using the Sampler primitive, which is used in this workflow
Background
Circuit-knitting is an umbrella term that encapsulates various methods of partitioning a circuit in multiple smaller subcircuits involving fewer gates or qubits. Each of the subcircuits can be executed independently, and the final result is obtained by some classical post-processing over the outcome of each subcircuit. This technique is accessible in the Circuit cutting Qiskit addon; see the documentation along with other introductory material for a detailed explanation of the technique.
This tutorial focuses on a method called wire cutting, where the circuit is partitioned along the wire [1], [2]. Note that partitioning is simple in classical circuits since the outcome at the point of partition can be determined deterministically, and is either 0 or 1. However, the state of the qubit at the point of the cut is, in general, a mixed state. Therefore, each subcircuit needs to be measured multiple times in different bases (usually a tomographically complete basis, such as the Pauli basis [3], [4]) and correspondingly prepared in its eigenstate. The figure below (courtesy: [7]) shows an example of wire cutting for a four-qubit GHZ state into three subcircuits. Here denotes a set of bases (usually Pauli X, Y and Z), and denotes a set of eigenstates (usually , , and ).

Since each subcircuit has fewer qubits and gates, they are expected to be less amenable to noise. This tutorial shows an example where this method can be used to effectively suppress the noise in the system.
Requirements
Before starting this tutorial, be sure you have the following installed:
- Qiskit SDK v2.0 or later, with visualization support
- Qiskit Runtime v0.22 or later (
pip install qiskit-ibm-runtime) - Circuit cutting Qiskit addon v0.10.0 or later (
pip install qiskit-addon-cutting) - Qiskit addon utils 0.3 or later (
pip install qiskit-addon-utils) - Qiskit Aer (
pip install qiskit-aer)
Setup
import numpy as np
import matplotlib.pyplot as plt
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.quantum_info import PauliList, SparsePauliOp
from qiskit.transpiler import generate_preset_pass_manager
from qiskit_aer import AerSimulator
from qiskit.result import sampled_expectation_value
from qiskit_addon_cutting.instructions import CutWire
from qiskit_addon_cutting import (
cut_wires,
expand_observables,
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
)
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import SamplerV2, BatchSmall-scale simulator example
This tutorial implements a Qiskit pattern to simulate a one-dimensional (1D) Many-Body Localization (MBL) circuit. The MBL circuit is a hardware-efficient circuit and is parameterized by two parameters and . When is set to and the initial state is prepared in for all the qubits, the ideal expectation value of is for every qubit site irrespective of the values of . More details on this circuit are available in this article.
Note that in a noiseless simulator, the expectation value obtained with and without circuit cutting will be same.
Step 1: Map classical inputs to a quantum problem
Construct the 1D MBL circuit
First, we present a function for constructing the 1D MBL circuit.
class MBLChainCircuit(QuantumCircuit):
def __init__(
self, num_qubits: int, depth: int, use_cut: bool = False
) -> None:
super().__init__(
num_qubits, name=f"MBLChainCircuit<{num_qubits}, {depth}>"
)
evolution = MBLChainEvolution(num_qubits, depth, use_cut)
self.compose(evolution, inplace=True)
class MBLChainEvolution(QuantumCircuit):
def __init__(self, num_qubits: int, depth: int, use_cut) -> None:
super().__init__(
num_qubits, name=f"MBLChainEvolution<{num_qubits}, {depth}>"
)
theta = Parameter("θ")
phis = ParameterVector("φ", num_qubits)
for layer in range(depth):
layer_parity = layer % 2
# print("layer parity", layer_parity)
for qubit in range(layer_parity, num_qubits - 1, 2):
# print(qubit)
self.cz(qubit, qubit + 1)
self.u(theta, 0, np.pi, qubit)
self.u(theta, 0, np.pi, qubit + 1)
if (
use_cut
and layer_parity == 0
and (
qubit == num_qubits // 2 - 1
or qubit == num_qubits // 2
)
):
self.append(CutWire(), [num_qubits // 2])
if use_cut and layer < depth - 1 and layer_parity == 1:
if qubit == num_qubits // 2:
self.append(CutWire(), [qubit])
for qubit in range(num_qubits):
self.p(phis[qubit], qubit)num_qubits = 10
depth = 2
mbl = MBLChainCircuit(num_qubits, depth)
mbl.draw("mpl", fold=-1)Output:
We calculate the average expectation value over all qubits for . Since the ideal expectation value of , the ideal expectation value of is also . The parameters are selected randomly.
np.random.seed(42)
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phisThe circuit needs to be annotated by inserting CutWire in the desired locations to partition it. For this tutorial, we opt for equal partition. The MBL circuit is designed so that setting use_cut=True in the function inserts the annotation properly after qubits, being the number of qubits in the original circuit. We also assigned the randomly generated parameters to the circuit.
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_cut.draw("mpl", fold=-1)Output:
Step 2: Optimize problem for quantum hardware execution
Cut the circuit into smaller subcircuits
Now we partition the circuit into two smaller subcircuits using qiskit-addon-cutting. qiskit-addon-cutting appends a virtual Move gate to split the wire cut location by appropriately adjusting the number of qubits. Now we create the circuit with this virtual gate. Since there is one wire cut, the number of associated qubits will be increased by 1.
mbl_move = cut_wires(mbl_cut)
mbl_move.draw("mpl", fold=-1)Output:
Construct and expand the observable
The observable, as defined before, will be the average of on each qubit. However, upon inserting the virtual Move gate, the effective number of qubits in the circuit increases. The observable must also be expanded accordingly to account for this change in the number of qubits. Note the observable always acts trivially (as in ) on the extra qubit added for the virtual Move gate.
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
observableOutput:
PauliList(['ZIIIIIIIII', 'IZIIIIIIII', 'IIZIIIIIII', 'IIIZIIIIII',
'IIIIZIIIII', 'IIIIIZIIII', 'IIIIIIZIII', 'IIIIIIIZII',
'IIIIIIIIZI', 'IIIIIIIIIZ'])
new_obs = expand_observables(observable, mbl, mbl_move)
new_obsOutput:
PauliList(['ZIIIIIIIIII', 'IZIIIIIIIII', 'IIZIIIIIIII', 'IIIZIIIIIII',
'IIIIZIIIIII', 'IIIIIIZIIII', 'IIIIIIIZIII', 'IIIIIIIIZII',
'IIIIIIIIIZI', 'IIIIIIIIIIZ'])
Now the circuit can be partitioned along the Move gate and we obtain the subcircuits, as well as the subobservable, which is the portion of the original observable associated with each subcircuit.
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservablesHere we visualize the two subcircuits:
subcircuits[0].draw("mpl", fold=-1)Output:
subcircuits[1].draw("mpl", fold=-1)Output:
Expanding the observable using the Move operation requires a PauliList data structure. To reconstruct the expectation value of the original circuit, we require the observable in the SparsePauliOp format.
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)As discussed before, for each cut the upstream circuit must be measured in a Pauli basis, and the downstream circuit must be prepared in the eigenstate of the basis. The function generate_cutting_experiments creates all of these necessary circuits and the coefficients associated with each circuit required for reconstruction. Find more detail in this paper.
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)Transpile the circuits onto the backend
For the first example involving only simulation, we transpile the circuit into the basis gate set of the backend:
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)Output:
<IBMBackend('ibm_fez')>
Step 3: Execute using Qiskit primitives
Now, execute each subexperiment:
pm_basis = generate_preset_pass_manager(
optimization_level=2, basis_gates=backend.configuration().basis_gates
)
basis_subexperiments = {
label: pm_basis.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}sampler = SamplerV2(mode=AerSimulator())
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in basis_subexperiments.items()
}Step 4: Post-process and return result in desired classical format
Now we retrieve the result of each subexperiment run and reconstruct the expectation value of the uncut circuit:
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
reconstructed_expvalOutput:
np.float64(0.9953821063041687)
methods = [
"Uncut",
"Wire cut",
]
values = [
1,
reconstructed_expval,
] # since the ideal expectation value in noiseless simulation is +1
ax = plt.gca()
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
ax.set_ylabel(r"$M_Z$", fontsize=12)Output:
Text(0, 0.5, '$M_Z$')
Large-scale hardware example
Now we demonstrate wire cutting for a 60-qubit MBL circuit. The uncut, as well as the cut circuits, will be executed on IBM Quantum® hardware:
num_qubits = 60
depth = 2
# construct the circuit
mbl = MBLChainCircuit(num_qubits, depth)
# create parameters
phis = list(np.random.rand(mbl.num_parameters - 1))
theta = [0]
params = theta + phis
# construct the cut circuit
mbl_cut = MBLChainCircuit(num_qubits, depth, use_cut=True)
mbl_cut.assign_parameters(params, inplace=True)
mbl_move = cut_wires(mbl_cut)
# Define observable and expand to account for the wire cut
observable = PauliList(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)]
)
new_obs = expand_observables(observable, mbl, mbl_move)
# Construct a SparsePauliOp version of the observable for later use in reconstruction
M_z = SparsePauliOp(
["I" * i + "Z" + "I" * (num_qubits - i - 1) for i in range(num_qubits)],
coeffs=[1 / num_qubits] * num_qubits,
)
# Partition the circuit and get subcircuits and subobservables
partitioned_problem = partition_problem(circuit=mbl_move, observables=new_obs)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
# Obtain subexperiments and coefficients
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits,
observables=subobservables,
num_samples=np.inf,
)
# Transpile the subexperiments to the backend
pm = generate_preset_pass_manager(optimization_level=2, backend=backend)
isa_subexperiments = {
label: pm.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}
# Execute the subexperiments and retrieve results
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
sampler.options.environment.job_tags = ["TUT_WC"]
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
results = {label: job.result() for label, job in jobs.items()}
# Reconstruct the expectation value of the original observable
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, M_z.coeffs).real
# Compute the uncut circuit to obtain the noisy expectation value for comparison
sampler = SamplerV2(mode=backend)
sampler.options.environment.job_tags = ["TUT_WC"]
if mbl.num_clbits == 0:
mbl.measure_all()
isa_mbl = pm.run(mbl)
pub = (isa_mbl, params)
uncut_job = sampler.run([pub])
uncut_counts = uncut_job.result()[0].data.meas.get_counts()
uncut_expval = sampled_expectation_value(uncut_counts, M_z)
# visualize the results
ax = plt.gca()
methods = ["uncut", "cut"]
values = [uncut_expval, reconstructed_expval]
plt.bar(methods, values, color="#a56eff", width=0.4, edgecolor="#8a3ffc")
plt.axhline(y=1, color="k", linestyle="--")
plt.text(0.3, 0.95, "Exact result")
plt.show()Output:
uncut_expvalOutput:
0.9202473958333336
Next steps
If you found this work interesting, you might be interested in the following material:
References
[1] Peng, T., Harrow, A. W., Ozols, M., & Wu, X. (2020). Simulating large quantum circuits on a small quantum computer. Physical review letters, 125(15), 150504.
[2] Tang, W., Tomesh, T., Suchara, M., Larson, J., & Martonosi, M. (2021, April). Cutqc: using small quantum computers for large quantum circuit evaluations. In Proceedings of the 26th ACM International conference on architectural support for programming languages and operating systems (pp. 473-486).
[3] Perlin, M. A., Saleem, Z. H., Suchara, M., & Osborn, J. C. (2021). Quantum circuit cutting with maximum-likelihood tomography. npj Quantum Information, 7(1), 64.
[4] Majumdar, R., & Wood, C. J. (2022). Error mitigated quantum circuit cutting. arXiv preprint arXiv:2211.13431.
[5] Khare, T., Majumdar, R., Sangle, R., Ray, A., Seshadri, P. V., & Simmhan, Y. (2023). Parallelizing Quantum-Classical Workloads: Profiling the Impact of Splitting Techniques. In 2023 IEEE International Conference on Quantum Computing and Engineering (QCE) (Vol. 1, pp. 990-1000). IEEE.
[6] Bhoumik, D., Majumdar, R., Saha, A., & Sur-Kolay, S. (2023). Distributed Scheduling of Quantum Circuits with Noise and Time Optimization. arXiv preprint arXiv:2309.06005.
[7] Majumdar, R. (2024). Efficient Reduction of Resources and Noise in Discrete Quantum Computing Circuits (Doctoral dissertation, Indian Statistical Institute - Kolkata). https://www.proquest.com/openview/b481def90b1cc80e6b58a77c99e8385c/1?pq-origsite=gscholar&cbl=2026366&diss=y