qml.cut_circuit

cut_circuit(tape, auto_cutter=False, use_opt_einsum=False, device_wires=None, max_depth=1, **kwargs)[source]

Cut up a quantum circuit into smaller circuit fragments.

Following the approach outlined in Theorem 2 of Peng et al., strategic placement of WireCut operations can allow a quantum circuit to be split into disconnected circuit fragments. Each circuit fragment is then executed multiple times by varying the state preparations and measurements at incoming and outgoing cut locations, respectively, resulting in a process tensor describing the action of the fragment. The process tensors are then contracted to provide the result of the original uncut circuit.

Note

Only circuits that return a single expectation value are supported.

Parameters
  • tape (QNode or QuantumTape) – the quantum circuit to be cut

  • auto_cutter (Union[bool, Callable]) – Toggle for enabling automatic cutting with the default kahypar_cut() partition method. Can also pass a graph partitioning function that takes an input graph and returns a list of edges to be cut based on a given set of constraints and objective. The default kahypar_cut() function requires KaHyPar to be installed using pip install kahypar for Linux and Mac users or visiting the instructions here to compile from source for Windows users.

  • use_opt_einsum (bool) – Determines whether to use the opt_einsum package. This package is useful for faster tensor contractions of large networks but must be installed separately using, e.g., pip install opt_einsum. Both settings for use_opt_einsum result in a differentiable contraction.

  • device_wires (Wires) – Wires of the device that the cut circuits are to be run on. When transforming a QNode, this argument is optional and will be set to the QNode’s device wires. Required when transforming a tape.

  • max_depth (int) – The maximum depth used to expand the circuit while searching for wire cuts. Only applicable when transforming a QNode.

  • kwargs – Additional keyword arguments to be passed to a callable auto_cutter argument. For the default KaHyPar cutter, please refer to the docstring of functions find_and_place_cuts() and kahypar_cut() for the available arguments.

Returns

The transformed circuit as described in qml.transform. Executing this circuit will perform a process tomography of the partitioned circuit fragments and combine the results via tensor contractions.

Return type

qnode (QNode) or tuple[List[QuantumTape], function]

Example

The following \(3\)-qubit circuit contains a WireCut operation. When decorated with @qml.cut_circuit, we can cut the circuit into two \(2\)-qubit fragments:

dev = qml.device("default.qubit", wires=2)

@qml.cut_circuit
@qml.qnode(dev)
def circuit(x):
    qml.RX(x, wires=0)
    qml.RY(0.9, wires=1)
    qml.RX(0.3, wires=2)

    qml.CZ(wires=[0, 1])
    qml.RY(-0.4, wires=0)

    qml.WireCut(wires=1)

    qml.CZ(wires=[1, 2])

    return qml.expval(qml.pauli.string_to_pauli_word("ZZZ"))

Executing circuit will run multiple configurations of the \(2\)-qubit fragments which are then postprocessed to give the result of the original circuit:

>>> x = np.array(0.531, requires_grad=True)
>>> circuit(x)
0.47165198882111165

Futhermore, the output of the cut circuit is also differentiable:

>>> qml.grad(circuit)(x)
tensor(-0.27698287, requires_grad=True)

Alternatively, if the optimal wire-cut placement is unknown for an arbitrary circuit, the auto_cutter option can be enabled to make attempts in finding such an optimal cut. The following examples shows this capability on the same circuit as above but with the WireCut removed:

from functools import partial

@partial(qml.cut_circuit, auto_cutter=True)
@qml.qnode(dev)
def circuit(x):
    qml.RX(x, wires=0)
    qml.RY(0.9, wires=1)
    qml.RX(0.3, wires=2)

    qml.CZ(wires=[0, 1])
    qml.RY(-0.4, wires=0)

    qml.CZ(wires=[1, 2])

    return qml.expval(qml.pauli.string_to_pauli_word("ZZZ"))
>>> x = np.array(0.531, requires_grad=True)
>>> circuit(x)
0.47165198882111165
>>> qml.grad(circuit)(x)
tensor(-0.27698287, requires_grad=True)

Manually placing WireCut operations and decorating the QNode with the cut_circuit() batch transform is the suggested entrypoint into circuit cutting. However, advanced users also have the option to work directly with a QuantumTape and manipulate the tape to perform circuit cutting using the below functionality:

tape_to_graph(tape)

Converts a quantum tape to a directed multigraph.

find_and_place_cuts(graph[, cut_method, …])

Automatically finds and places optimal WireCut nodes into a given tape-converted graph using a customizable graph partitioning function.

replace_wire_cut_nodes(graph)

Replace each WireCut node in the graph with a MeasureNode and PrepareNode.

fragment_graph(graph)

Fragments a graph into a collection of subgraphs as well as returning the communication (quotient) graph.

graph_to_tape(graph)

Converts a directed multigraph to the corresponding QuantumTape.

expand_fragment_tape(tape)

Expands a fragment tape into a sequence of tapes for each configuration of the contained MeasureNode and PrepareNode operations.

qcut_processing_fn(results, …[, …])

Processing function for the cut_circuit() transform.

CutStrategy([devices, max_free_wires, …])

A circuit-cutting distribution policy for executing (large) circuits on available (comparably smaller) devices.

The following shows how these elementary steps are combined as part of the cut_circuit() transform.

Consider the circuit below:

ops = [
    qml.RX(0.531, wires=0),
    qml.RY(0.9, wires=1),
    qml.RX(0.3, wires=2),

    qml.CZ(wires=(0,1)),
    qml.RY(-0.4, wires=0),

    qml.WireCut(wires=1),

    qml.CZ(wires=[1, 2]),
]
measurements = [qml.expval(qml.pauli.string_to_pauli_word("ZZZ"))]
tape = qml.tape.QuantumTape(ops, measurements)
>>> print(qml.drawer.tape_text(tape))
0: ──RX─╭●──RY────┤ ╭<Z@Z@Z>
1: ──RY─╰Z──//─╭●─┤ ├<Z@Z@Z>
2: ──RX────────╰Z─┤ ╰<Z@Z@Z>

To cut the circuit, we first convert it to its graph representation:

>>> graph = qml.qcut.tape_to_graph(tape)
../../_images/qcut_graph.svg

If, however, the optimal location of the WireCut is unknown, we can use find_and_place_cuts() to make attempts in automatically finding such a cut given the device constraints. Using the same circuit as above but with the WireCut removed, the same (optimal) cut can be recovered with automatic cutting:

ops = [
    qml.RX(0.531, wires=0),
    qml.RY(0.9, wires=1),
    qml.RX(0.3, wires=2),

    qml.CZ(wires=(0,1)),
    qml.RY(-0.4, wires=0),

    qml.CZ(wires=[1, 2]),
]
measurements = [qml.expval(qml.pauli.string_to_pauli_word("ZZZ"))]
uncut_tape = qml.tape.QuantumTape(ops, measurements)
>>> cut_graph = qml.qcut.find_and_place_cuts(
...     graph = qml.qcut.tape_to_graph(uncut_tape),
...     cut_strategy = qml.qcut.CutStrategy(max_free_wires=2),
... )
>>> print(qml.qcut.graph_to_tape(cut_graph).draw())
0: ──RX─╭●──RY────┤ ╭<Z@Z@Z>
1: ──RY─╰Z──//─╭●─┤ ├<Z@Z@Z>
2: ──RX────────╰Z─┤ ╰<Z@Z@Z>

Our next step is to remove the WireCut nodes in the graph and replace with MeasureNode and PrepareNode pairs.

>>> qml.qcut.replace_wire_cut_nodes(graph)

The MeasureNode and PrepareNode pairs are placeholder operations that allow us to cut the circuit graph and then iterate over measurement and preparation configurations at cut locations. First, the fragment_graph() function pulls apart the graph into disconnected components as well as returning the communication_graph detailing the connectivity between the components.

>>> fragments, communication_graph = qml.qcut.fragment_graph(graph)

We now convert the fragments back to QuantumTape objects

>>> fragment_tapes = [qml.qcut.graph_to_tape(f) for f in fragments]

The circuit fragments can now be visualized:

>>> print(fragment_tapes[0].draw())
 0: ──RX(0.531)──╭●──RY(-0.4)─────┤ ⟨Z⟩
 1: ──RY(0.9)────╰Z──MeasureNode──┤
>>> print(fragment_tapes[1].draw())
 2: ──RX(0.3)──────╭Z──╭┤ ⟨Z ⊗ Z⟩
 1: ──PrepareNode──╰●──╰┤ ⟨Z ⊗ Z⟩

Additionally, we must remap the tape wires to match those available on our device.

>>> dev = qml.device("default.qubit", wires=2)
>>> fragment_tapes = [qml.map_wires(t, dict(zip(t.wires, dev.wires)))[0][0] for t in fragment_tapes]

Next, each circuit fragment is expanded over MeasureNode and PrepareNode configurations and a flat list of tapes is created:

expanded = [qml.qcut.expand_fragment_tape(t) for t in fragment_tapes]

configurations = []
prepare_nodes = []
measure_nodes = []
for tapes, p, m in expanded:
    configurations.append(tapes)
    prepare_nodes.append(p)
    measure_nodes.append(m)

tapes = tuple(tape for c in configurations for tape in c)

Each configuration is drawn below:

>>> for t in tapes:
...     print(qml.drawer.tape_text(t))
0: ──RX(0.531)──╭●──RY(-0.4)──╭┤ ⟨Z ⊗ I⟩ ╭┤ ⟨Z ⊗ Z⟩
1: ──RY(0.9)────╰Z────────────╰┤ ⟨Z ⊗ I⟩ ╰┤ ⟨Z ⊗ Z⟩

0: ──RX(0.531)──╭●──RY(-0.4)──╭┤ ⟨Z ⊗ X⟩
1: ──RY(0.9)────╰Z────────────╰┤ ⟨Z ⊗ X⟩

0: ──RX(0.531)──╭●──RY(-0.4)──╭┤ ⟨Z ⊗ Y⟩
1: ──RY(0.9)────╰Z────────────╰┤ ⟨Z ⊗ Y⟩

0: ──RX(0.3)──╭Z──╭┤ ⟨Z ⊗ Z⟩
1: ──I────────╰●──╰┤ ⟨Z ⊗ Z⟩

0: ──RX(0.3)──╭Z──╭┤ ⟨Z ⊗ Z⟩
1: ──X────────╰●──╰┤ ⟨Z ⊗ Z⟩

0: ──RX(0.3)──╭Z──╭┤ ⟨Z ⊗ Z⟩
1: ──H────────╰●──╰┤ ⟨Z ⊗ Z⟩

0: ──RX(0.3)─────╭Z──╭┤ ⟨Z ⊗ Z⟩
1: ──H────────S──╰●──╰┤ ⟨Z ⊗ Z⟩

The last step is to execute the tapes and postprocess the results using qcut_processing_fn(), which processes the results to the original full circuit output via a tensor network contraction

>>> results = qml.execute(tapes, dev, gradient_fn=None)
>>> qml.qcut.qcut_processing_fn(
...     results,
...     communication_graph,
...     prepare_nodes,
...     measure_nodes,
... )
0.47165198882111165