# Tutorial 2: Circuit Cutting with Manual Wire Cutting¶

Circuit cutting is a technique to decompose a quantum circuit into smaller circuits, whose results can be knitted together to reconstruct the original circuit output.

The circuit knitting toolbox implements a wire cutting method presented in CutQC (Tang et al.). This method allows a circuit wire to be cut such that the generated subcircuits are amended by measurements in the Pauli bases and by state preparation of four Pauli eigenstates (see Fig. 4 of CutQC).

This wire cutting technique is comprised of the following basic steps:

1. Decompose: Cut a circuit into multiple subcircuits. Here, we’ll use a manual method to specify the cut(s). See tutorial 1 to automatically cut a circuit.

2. Evaluate: Execute those subcircuits on quantum backend(s).

3. Reconstruct: Knit the subcircuit results together to reconstruct the original circuit output (in this case, the full probability distribution).

## Create a quantum circuit with Qiskit¶

In this tutorial, we’ll use the example circuit shown in CutQC.

[4]:

import numpy as np
from qiskit import QuantumCircuit

num_qubits = 5

circuit = QuantumCircuit(num_qubits)
for i in range(num_qubits):
circuit.h(i)
circuit.cx(0, 1)
for i in range(2, num_qubits):
circuit.t(i)
circuit.cx(0, 2)
circuit.rx(np.pi / 2, 4)
circuit.rx(np.pi / 2, 0)
circuit.rx(np.pi / 2, 1)
circuit.cx(2, 4)
circuit.t(0)
circuit.t(1)
circuit.cx(2, 3)
circuit.ry(np.pi / 2, 4)
for i in range(num_qubits):
circuit.h(i)

circuit.draw("mpl", fold=-1, scale=0.75)

[4]:


## Set up the Qiskit Runtime Service¶

The Qiskit Runtime Service provides access to IBM Runtime Primitives and quantum backends. Alternatively, a local statevector simulator can be used with the Qiskit primitives.

[5]:

from qiskit_ibm_runtime import (
QiskitRuntimeService,
Options,
)

# Use local versions of the primitives by default.
service = None

# Uncomment the following line to instead use Qiskit Runtime.
# service = QiskitRuntimeService()


The wire cutter tool uses a Sampler primitive to evaluate the probabilities of each subcircuit. Here, we configure the options for the Runtime Sampler and specify the backend(s) to be used to evaluate the subcircuits.

If no service was set up, the backend_names argument will be ignored, and Qiskit primitives will be used with statevector simulator.

[6]:

# Set the Sampler and runtime options
options = Options(execution={"shots": 4000})

# Run 2 parallel qasm simulator threads
backend_names = ["ibmq_qasm_simulator"] * 2


## Decompose the circuit with wire cutting¶

In this example, we will use a manual method to specify the wire cuts. See tutorial 1 for how to automatically cut a circuit. The figure below shows the steps for producing the subcircuit_vertices argument for the cut_circuit_wires function.

• method='manual': Manually specify the wire cuts

• subcircuit_vertices: A list of lists containing the two-qubit gate indices appearing on either side of the cut(s)

[7]:

%%capture

from circuit_knitting_toolbox.circuit_cutting.wire_cutting import cut_circuit_wires

cuts = cut_circuit_wires(
circuit=circuit, method="manual", subcircuit_vertices=[[0, 1], [2, 3]]
)


The two subcircuits produced

[8]:

# visualize the first subcircuit
cuts["subcircuits"][0].draw("mpl", fold=-1, scale=0.6)

[8]:

[9]:

# visualize the second subcircuit
cuts["subcircuits"][1].draw("mpl", fold=-1, scale=0.6)

[9]:


## Evaluate the subcircuits¶

Set up the Qiskit Runtime Service

The Qiskit Runtime Service provides access to Qiskit Runtime Primitives and quantum backends. See the Qiskit Runtime documentation for more information. Alternatively, if a Qiskit Runtime Service is not passed, then a local statevector simulator will be used with the Qiskit Primitives.

[10]:

from qiskit_ibm_runtime import (
QiskitRuntimeService,
Options,
)

# Use local versions of the primitives by default.
service = None

# Uncomment the following line to instead use Qiskit Runtime Service.
# service = QiskitRuntimeService()


Configure the Qiskit Runtime Primitive

The wire cutter tool uses a Sampler primitive to evaluate the probabilities of each subcircuit. Here, we configure the options for the Qiskit Runtime Sampler and specify the backend(s) to be used to evaluate the subcircuits. Backends could be simulator(s) and/or quantum device(s). In this tutorial, two local cores will be used to support each of the parallel backend threads we’ll specify below.

If no service was set up, the backend_names argument will be ignored, and Qiskit Primitives will be used with statevector simulator.

[11]:

# Set the Sampler and runtime options
options = Options(execution={"shots": 4000})

# Run 2 parallel qasm simulator threads
backend_names = ["ibmq_qasm_simulator"] * 2


Evaluate the subcircuits on the backend(s)

[12]:

from circuit_knitting_toolbox.circuit_cutting.wire_cutting import evaluate_subcircuits

subcircuit_instance_probabilities = evaluate_subcircuits(cuts)

# Uncomment the following lines to instead use Qiskit Runtime Service as configured above.
# subcircuit_instance_probabilities = evaluate_subcircuits(cuts,
#                                                          service_args=service.active_account(),
#                                                          backend_names=backend_names,
#                                                          options=options,
#                                                         )


## Reconstruct the full circuit output¶

Next, the results of the subcircuit experiments are classically postprocessed to reconstruct the original circuit’s full probability distribution.

[13]:

%%capture

from circuit_knitting_toolbox.circuit_cutting.wire_cutting import (
reconstruct_full_distribution,
)

reconstructed_probabilities = reconstruct_full_distribution(
circuit, subcircuit_instance_probabilities, cuts
)


## Verify the results¶

If the original circuit is small enough, we can use a statevector simulator to check the results of cutting against the original circuit’s exact probability distribution (ground truth).

[14]:

from circuit_knitting_toolbox.circuit_cutting.wire_cutting import verify

metrics, exact_probabilities = verify(circuit, reconstructed_probabilities)


The verify step includes several metrics

For example, the chi square loss is computed. Since we’re using the Qiskit Sampler with statevector simulator, we expect the reconstructed distributed to exactly match the ground truth. More info about each metric can be found in the utils metrics file.

[15]:

metrics

[15]:

{'nearest': {'chi2': 0,
'Mean Squared Error': 1.554441697306333e-32,
'Mean Absolute Percentage Error': 4.748059826691265e-14,
'Cross Entropy': 2.599681088367844,
'HOP': 0.9004283905932716},
'naive': {'chi2': 0,
'Mean Squared Error': 6.5760386594463524e-34,
'Mean Absolute Percentage Error': 5.260720927719812e-14,
'Cross Entropy': 2.5996810883678423,
'HOP': 0.9004283905932736}}


Visualize both distributions

If we calculated the ground truth above, we can visualize a comparison to the reconstructed probabilities

[16]:

from qiskit.visualization import plot_histogram
from qiskit.result import ProbDistribution

# Create a dict for the reconstructed distribution
reconstructed_distribution = {
i: prob for i, prob in enumerate(reconstructed_probabilities)
}

# Represent states as bitstrings (instead of ints)
reconstructed_dict_bitstring = ProbDistribution(
data=reconstructed_distribution
).binary_probabilities(num_bits=num_qubits)

# Create the ground truth distribution dict
exact_distribution = {i: prob for i, prob in enumerate(exact_probabilities)}

# Represent states as bitstrings (instead of ints)
exact_dict_bitstring = ProbDistribution(data=exact_distribution).binary_probabilities(
num_bits=num_qubits
)

# plot a histogram of the distributions
plot_histogram(
[exact_dict_bitstring, reconstructed_dict_bitstring],
number_to_keep=8,
figsize=(16, 6),
sort="asc",
legend=["Exact", "Reconstructed"],
)

[16]:

[17]:

import qiskit.tools.jupyter

%qiskit_version_table


### Version Information

Qiskit SoftwareVersion
qiskit-terra0.22.0
qiskit-aer0.11.0
qiskit-ibmq-provider0.19.2
qiskit-nature0.4.5
System information
Python version3.10.6
Python compilerClang 13.1.6 (clang-1316.0.21.2.5)
Python buildmain, Aug 11 2022 13:49:25
OSDarwin
CPUs8
Memory (Gb)32.0
Wed Oct 26 11:28:28 2022 CDT