qml.transforms.qcut.find_and_place_cuts

find_and_place_cuts(graph: networkx.classes.multidigraph.MultiDiGraph, cut_method: Callable = <function kahypar_cut>, cut_strategy: Optional[pennylane.transforms.qcut.qcut.CutStrategy] = None, replace_wire_cuts=False, local_measurement=False, **kwargs)networkx.classes.multidigraph.MultiDiGraph[source]

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

Parameters
  • graph (MultiDiGraph) – The original (tape-converted) graph to be cut.

  • cut_method (Callable) – 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. Defaults to kahypar_cut() which 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.

  • cut_strategy (CutStrategy) – Strategy for optimizing cutting parameters based on device constraints. Defaults to None in which case kwargs must be fully specified for passing to the cut_method.

  • replace_wire_cuts (bool) – Whether to replace WireCut nodes with MeasureNode and PrepareNode pairs. Defaults to False.

  • local_measurement (bool) – Whether to use the local-measurement circuit-cutting objective, i.e. the maximum node-degree of the communication graph, for cut evaluation. Defaults to False which assumes global measurement and uses the total number of cuts as the cutting objective.

  • kwargs – Additional keyword arguments to be passed to the callable cut_method.

Returns

Copy of the input graph with WireCut nodes inserted.

Return type

nx.MultiDiGraph

Example

Consider the following 4-wire circuit with a single CNOT gate connecting the top (wires [0, 1]) and bottom (wires ["a", "b"]) halves of the circuit. Note there’s a WireCut manually placed into the circuit already.

with qml.tape.QuantumTape() as tape:
    qml.RX(0.1, wires=0)
    qml.RY(0.2, wires=1)
    qml.RX(0.3, wires="a")
    qml.RY(0.4, wires="b")
    qml.CNOT(wires=[0, 1])
    qml.WireCut(wires=1)
    qml.CNOT(wires=["a", "b"])
    qml.CNOT(wires=[1, "a"])
    qml.CNOT(wires=[0, 1])
    qml.CNOT(wires=["a", "b"])
    qml.RX(0.5, wires="a")
    qml.RY(0.6, wires="b")
    qml.expval(qml.PauliX(wires=[0]) @ qml.PauliY(wires=["a"]) @ qml.PauliZ(wires=["b"]))
>>> print(qml.drawer.tape.text(tape))
 0: ──RX(0.1)──╭●──────────╭●───────────╭┤ ⟨X ⊗ Y ⊗ Z⟩
 1: ──RY(0.2)──╰X──//──╭●──╰X───────────│┤
 a: ──RX(0.3)──╭●──────╰X──╭●──RX(0.5)──├┤ ⟨X ⊗ Y ⊗ Z⟩
 b: ──RY(0.4)──╰X──────────╰X──RY(0.6)──╰┤ ⟨X ⊗ Y ⊗ Z⟩

Since the existing WireCut doesn’t sufficiently fragment the circuit, we can find the remaining cuts using the default KaHyPar partitioner:

>>> graph = qml.transforms.qcut.tape_to_graph(tape)
>>> cut_graph = qml.transforms.qcut.find_and_place_cuts(
        graph=graph,
        num_fragments=2,
        imbalance=0.5,
    )

Visualizing the newly-placed cut:

>>> print(qml.transforms.qcut.graph_to_tape(cut_graph).draw())
 0: ──RX(0.1)──╭●───────────────╭●────────╭┤ ⟨X ⊗ Y ⊗ Z⟩
 1: ──RY(0.2)──╰X──//──╭●───//──╰X────────│┤
 a: ──RX(0.3)──╭●──────╰X──╭●────RX(0.5)──├┤ ⟨X ⊗ Y ⊗ Z⟩
 b: ──RY(0.4)──╰X──────────╰X────RY(0.6)──╰┤ ⟨X ⊗ Y ⊗ Z⟩

We can then proceed with the usual process of replacing WireCut nodes with pairs of MeasureNode and PrepareNode, and then break the graph into fragments. Or, alternatively, we can directly get such processed graph by passing replace_wire_cuts=True:

>>> cut_graph = qml.transforms.qcut.find_and_place_cuts(
        graph=graph,
        num_fragments=2,
        imbalance=0.5,
        replace_wire_cuts=True,
    )
>>> frags, comm_graph = qml.transforms.qcut.fragment_graph(cut_graph)
>>> for t in frags:
...     print(qml.transforms.qcut.graph_to_tape(t).draw())
0: ──RX(0.1)──────╭●───────────────╭●──┤ ⟨X⟩
1: ──RY(0.2)──────╰X──MeasureNode──│───┤
2: ──PrepareNode───────────────────╰X──┤

a: ──RX(0.3)──────╭●──╭X──╭●────────────RX(0.5)──╭┤ ⟨Y ⊗ Z⟩
b: ──RY(0.4)──────╰X──│───╰X────────────RY(0.6)──╰┤ ⟨Y ⊗ Z⟩
1: ──PrepareNode──────╰●───MeasureNode────────────┤

Alternatively, if all we want to do is to find the optimal way to fit a circuit onto a smaller device, a CutStrategy can be used to populate the necessary explorations of cutting parameters. As an extreme example, if the only device at our disposal is a 2-qubit device, a simple cut strategy is to simply specify the the max_free_wires argument (or equivalently directly passing a Device to the device argument):

>>> cut_strategy = qml.transforms.qcut.CutStrategy(max_free_wires=2)
>>> print(cut_strategy.get_cut_kwargs(graph))
 [{'num_fragments': 2, 'imbalance': 0.5714285714285714},
  {'num_fragments': 3, 'imbalance': 1.4},
  {'num_fragments': 4, 'imbalance': 1.75},
  {'num_fragments': 5, 'imbalance': 2.3333333333333335},
  {'num_fragments': 6, 'imbalance': 2.0},
  {'num_fragments': 7, 'imbalance': 3.0},
  {'num_fragments': 8, 'imbalance': 2.5},
  {'num_fragments': 9, 'imbalance': 2.0},
  {'num_fragments': 10, 'imbalance': 1.5},
  {'num_fragments': 11, 'imbalance': 1.0},
  {'num_fragments': 12, 'imbalance': 0.5},
  {'num_fragments': 13, 'imbalance': 0.05},
  {'num_fragments': 14, 'imbalance': 0.1}]

The printed list above shows all the possible cutting configurations one can attempt to perform in order to search for the optimal cut. This is done by directly passing a CutStrategy to find_and_place_cuts():

>>> cut_graph = qml.transforms.qcut.find_and_place_cuts(
        graph=graph,
        cut_strategy=cut_strategy,
    )
>>> print(qml.transforms.qcut.graph_to_tape(cut_graph).draw())
 0: ──RX──//─╭●──//────────╭●──//─────────┤ ╭<[email protected]@Z>
 1: ──RY──//─╰X──//─╭●──//─╰X─────────────┤ │
 a: ──RX──//─╭●──//─╰X──//─╭●──//──RX──//─┤ ├<[email protected]@Z>
 b: ──RY──//─╰X──//────────╰X──//──RY─────┤ ╰<[email protected]@Z>

As one can tell, quite a few cuts have to be made in order to execute the circuit on solely 2-qubit devices. To verify, let’s print the fragments:

>>> qml.transforms.qcut.replace_wire_cut_nodes(cut_graph)
>>> frags, comm_graph = qml.transforms.qcut.fragment_graph(cut_graph)
>>> for t in frags:
...     print(qml.transforms.qcut.graph_to_tape(t).draw())
0: ──RX──MeasureNode─┤

1: ──RY──MeasureNode─┤

a: ──RX──MeasureNode─┤

b: ──RY──MeasureNode─┤

0: ──PrepareNode─╭●──MeasureNode─┤
1: ──PrepareNode─╰X──MeasureNode─┤

a: ──PrepareNode─╭●──MeasureNode─┤
b: ──PrepareNode─╰X──MeasureNode─┤

1: ──PrepareNode─╭●──MeasureNode─┤
a: ──PrepareNode─╰X──MeasureNode─┤

0: ──PrepareNode─╭●──MeasureNode─┤
1: ──PrepareNode─╰X──────────────┤

b: ──PrepareNode─╭X──MeasureNode─┤
a: ──PrepareNode─╰●──MeasureNode─┤

a: ──PrepareNode──RX──MeasureNode─┤

b: ──PrepareNode──RY─┤  <Z>

0: ──PrepareNode─┤  <X>

a: ──PrepareNode─┤  <Y>