Skip to content

Cut and Recombine ​

Circuit cutting lets MENOS simulate a circuit that is too large for any single engine by splitting it into smaller fragments, simulating each separately, and recombining the results exactly.

Why it exists ​

Suppose two groups of qubits are densely connected internally but linked to each other through a single qubit. The whole circuit may not fit as a dense state, but each group does. Cutting the wire that carries the link turns one large problem into several small ones, at the price of running each fragment for several different boundary conditions.

How it works: wire cutting ​

MENOS cuts wires (a qubit's timeline between two operations), not gates. Each cut wire is replaced by an exact decomposition into eight terms: an observable measured on one side and a state prepared on the other, with a coefficient. A plan with K cuts has 8^K branches in principle, but fragments that share boundary conditions are evaluated once, so the number of distinct fragment runs is far smaller. For one cut the example below needs 10 distinct fragment evaluations, where running both fragments once for each of the 8 branches would need 16.

The expectation value is then reconstructed as a signed, weighted sum over the branches.

Scope ​

AspectWhat is implemented
OutputExpectation values only. Counts and statevector requests never enumerate cutting
AccuracyExact. Every branch is enumerated; nothing is sampled or approximated
Number of cutsAt most 2
FragmentsRun on the cheapest engine for each fragment and boundary assignment
BackendCPU

Probability reconstruction and full-state reconstruction are not implemented. A cut plan refuses a non-exact accuracy policy outright.

When the planner chooses it ​

Cutting is one candidate among the others and competes on work units. It is enumerated for an expectation request when a cut exists, and it wins only when it is genuinely cheaper. A circuit that has no good cut simply never lists it. In a four-qubit example where running the circuit directly is cheaper, the planner declines to cut.

Overhead ​

Cutting trades memory for time. Each fragment needs far less memory than the whole circuit, but the number of fragment runs grows with the cuts (8^K branches, fewer distinct evaluations). The plan report shows it:

text
cut        cuts=1 fragments=2 branches=8 distinct_evals=10 actual_peak_bytes=512

Example ​

Two four-qubit clusters joined through one qubit. MENOS cuts the joining wire, and the result agrees with a dense statevector to about 1e-16:

py
"""Circuit cutting: an expectation value recombined from two smaller fragments.

Two four-qubit clusters are coupled through one qubit. MENOS cuts that wire,
simulates each fragment separately, and recombines the results exactly. A dense
statevector of the whole circuit is used here only as an independent check.
"""

import numpy as np

from menos import Circuit, Opcode, RunOptions, run
from menos._core import Observable, PauliString

per_cluster = 4
n = 2 * per_cluster

circuit = Circuit(n, 0)
for q in range(n):
    circuit.append(Opcode.RY, [q], [0.21 + 0.07 * q])
for a in range(per_cluster):                     # cluster A: qubits 0..3
    for b in range(a + 1, per_cluster):
        circuit.append(Opcode.CZ, [a, b])
for a in range(per_cluster, n):                  # cluster B: qubits 4..7
    for b in range(a + 1, n):
        circuit.append(Opcode.CZ, [a, b])
circuit.append(Opcode.T, [per_cluster])
for b in range(per_cluster, n):                  # qubit 0 migrates into cluster B
    circuit.append(Opcode.CZ, [0, b])

# The observable Z on every qubit.
pauli = PauliString()
pauli.num_qubits = n
pauli.x_words = [0]
pauli.z_words = [(1 << n) - 1]
observable = Observable()
observable.terms = [(pauli, complex(1.0))]

options = RunOptions()
options.expectation = observable
cut = run(circuit, options)

report = cut.explain()
print(next(line for line in report.splitlines() if line.startswith("strategy")))
print(next(line for line in report.splitlines() if line.startswith("cut")))
print("expectation:", cut.expectation_value.real)

# Independent check: the dense statevector, contracted with the same observable.
dense_options = RunOptions()
dense_options.want_statevector = True
state = run(circuit, dense_options).statevector
parity = np.array([bin(i).count("1") & 1 for i in range(state.size)])
reference = float(np.sum(np.abs(state) ** 2 * (1 - 2 * parity)))

assert "cut_and_recombine" in report
assert abs(cut.expectation_value.real - reference) < 1e-12
print("agrees with the dense statevector to", abs(cut.expectation_value.real - reference))
text
strategy   cut_and_recombine (exact)
cut        cuts=1 fragments=2 branches=8 distinct_evals=10 actual_peak_bytes=512
expectation: 0.37247461285251077
agrees with the dense statevector to 1.1102230246251565e-16

Limits ​

  • Expectation values only.
  • At most two cuts.
  • Fragments are simulated on the CPU.
  • A fragment's engine is chosen per assignment, so a plan's regions in the report show the fragments, not the final engine of every evaluation.

Execution Plans · Expectation Values · Analyze

Released under the Apache-2.0 license. GitHub · PyPI