Source code for pennylane.qcut.cutcircuit

# Copyright 2022 Xanadu Quantum Technologies Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Function cut_circuit for cutting a quantum circuit into smaller circuit fragments.
"""

from collections.abc import Callable
from functools import partial
from typing import Optional, Union

import pennylane as qml
from pennylane.measurements import ExpectationMP
from pennylane.tape import QuantumScript, QuantumScriptBatch
from pennylane.transforms import transform
from pennylane.typing import PostprocessingFn
from pennylane.wires import Wires

from .cutstrategy import CutStrategy
from .kahypar import kahypar_cut
from .processing import qcut_processing_fn
from .tapes import _qcut_expand_fn, expand_fragment_tape, graph_to_tape, tape_to_graph
from .utils import find_and_place_cuts, fragment_graph, replace_wire_cut_nodes


def _cut_circuit_expand(
    tape: QuantumScript,
    use_opt_einsum: bool = False,
    device_wires: Optional[Wires] = None,
    max_depth: int = 1,
    auto_cutter: Union[bool, Callable] = False,
    **kwargs,
) -> tuple[QuantumScriptBatch, PostprocessingFn]:
    """Main entry point for expanding operations until reaching a depth that
    includes :class:`~.WireCut` operations."""
    # pylint: disable=unused-argument

    def processing_fn(res):
        return res[0]

    tapes, tapes_fn = [tape], processing_fn

    # Expand the tapes for handling Hamiltonian with two or more terms
    tape_meas_ops = tape.measurements
    if tape_meas_ops and isinstance(
        tape_meas_ops[0].obs, (qml.ops.Hamiltonian, qml.ops.LinearCombination)
    ):
        if len(tape_meas_ops) > 1:
            raise NotImplementedError(
                "Hamiltonian expansion is supported only with a single Hamiltonian"
            )

        new_meas_op = type(tape_meas_ops[0])(obs=qml.Hamiltonian(*tape_meas_ops[0].obs.terms()))
        new_tape = type(tape)(
            tape.operations, [new_meas_op], shots=tape.shots, trainable_params=tape.trainable_params
        )

        tapes, tapes_fn = qml.transforms.split_non_commuting(new_tape, grouping_strategy=None)

    return [_qcut_expand_fn(tape, max_depth, auto_cutter) for tape in tapes], tapes_fn


[docs]@partial(transform, expand_transform=_cut_circuit_expand) def cut_circuit( tape: QuantumScript, auto_cutter: Union[bool, Callable] = False, use_opt_einsum: bool = False, device_wires: Optional[Wires] = None, max_depth: int = 1, **kwargs, ) -> tuple[QuantumScriptBatch, PostprocessingFn]: """ Cut up a quantum circuit into smaller circuit fragments. Following the approach outlined in Theorem 2 of `Peng et al. <https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.125.150504>`__, strategic placement of :class:`~.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. Args: tape (QNode or QuantumTape): the quantum circuit to be cut auto_cutter (Union[bool, Callable]): Toggle for enabling automatic cutting with the default :func:`~.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 :func:`~.kahypar_cut` function requires KaHyPar to be installed using ``pip install kahypar`` for Linux and Mac users or visiting the instructions `here <https://kahypar.org>`__ to compile from source for Windows users. use_opt_einsum (bool): Determines whether to use the `opt_einsum <https://dgasmith.github.io/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 :func:`~.find_and_place_cuts` and :func:`~.kahypar_cut` for the available arguments. Returns: qnode (QNode) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`. Executing this circuit will perform a process tomography of the partitioned circuit fragments and combine the results via tensor contractions. **Example** The following :math:`3`-qubit circuit contains a :class:`~.WireCut` operation. When decorated with ``@qml.cut_circuit``, we can cut the circuit into two :math:`2`-qubit fragments: .. code-block:: python 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 :math:`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 :class:`~.WireCut` removed: .. code-block:: python 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) .. details:: :title: Usage Details Manually placing :class:`~.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 :class:`~.QuantumTape` and manipulate the tape to perform circuit cutting using the below functionality: .. autosummary:: :toctree: ~qcut.tape_to_graph ~qcut.find_and_place_cuts ~qcut.replace_wire_cut_nodes ~qcut.fragment_graph ~qcut.graph_to_tape ~qcut.expand_fragment_tape ~qcut.qcut_processing_fn ~qcut.CutStrategy The following shows how these elementary steps are combined as part of the ``cut_circuit()`` transform. Consider the circuit below: .. code-block:: python 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) .. figure:: ../../_static/qcut_graph.svg :align: center :width: 60% :target: javascript:void(0); If, however, the optimal location of the :class:`~.WireCut` is unknown, we can use :func:`~.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 :class:`~.WireCut` removed, the same (optimal) cut can be recovered with automatic cutting: .. code-block:: python 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 :class:`~.WireCut` nodes in the graph and replace with :class:`~.MeasureNode` and :class:`~.PrepareNode` pairs. >>> qml.qcut.replace_wire_cut_nodes(graph) The :class:`~.MeasureNode` and :class:`~.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 :func:`~.fragment_graph` function pulls apart the graph into disconnected components as well as returning the `communication_graph <https://en.wikipedia.org/wiki/Quotient_graph>`__ detailing the connectivity between the components. >>> fragments, communication_graph = qml.qcut.fragment_graph(graph) We now convert the ``fragments`` back to :class:`~.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(decimals=2)) 0: ──RX(0.53)─╭●──RY(-0.40)───┤ <Z> 1: ──RY(0.90)─╰Z──MeasureNode─┤ >>> print(fragment_tapes[1].draw(decimals=1)) 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 :class:`~.MeasureNode` and :class:`~.PrepareNode` configurations and a flat list of tapes is created: .. code-block:: 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)) ... print() .. code-block:: 0: ──RX(0.53)─╭●──RY(-0.40)─┤ ╭<Z@I> ╭<Z@Z> 1: ──RY(0.90)─╰Z────────────┤ ╰<Z@I> ╰<Z@Z> 0: ──RX(0.53)─╭●──RY(-0.40)─┤ ╭<Z@X> 1: ──RY(0.90)─╰Z────────────┤ ╰<Z@X> 0: ──RX(0.53)─╭●──RY(-0.40)─┤ ╭<Z@Y> 1: ──RY(0.90)─╰Z────────────┤ ╰<Z@Y> 0: ──RX(0.30)─╭Z─┤ ╭<Z@Z> 1: ──I────────╰●─┤ ╰<Z@Z> 0: ──RX(0.30)─╭Z─┤ ╭<Z@Z> 1: ──X────────╰●─┤ ╰<Z@Z> 0: ──RX(0.30)─╭Z─┤ ╭<Z@Z> 1: ──H────────╰●─┤ ╰<Z@Z> 0: ──RX(0.30)────╭Z─┤ ╭<Z@Z> 1: ──H─────────S─╰●─┤ ╰<Z@Z> The last step is to execute the tapes and postprocess the results using :func:`~.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 """ # pylint: disable=unused-argument if len(tape.measurements) != 1: raise ValueError( "The circuit cutting workflow only supports circuits with a single output " "measurement" ) if not all(isinstance(m, ExpectationMP) for m in tape.measurements): raise ValueError( "The circuit cutting workflow only supports circuits with expectation " "value measurements" ) if use_opt_einsum: try: import opt_einsum # pylint: disable=import-outside-toplevel,unused-import except ImportError as e: raise ImportError( "The opt_einsum package is required when use_opt_einsum is set to " "True in the cut_circuit function. This package can be " "installed using:\npip install opt_einsum" ) from e # convert the quantum tape to a DAG structure g = tape_to_graph(tape) # place WireCut(s) nodes in the DAG automatically if intended if auto_cutter is True or callable(auto_cutter): cut_strategy = kwargs.pop("cut_strategy", None) or CutStrategy( max_free_wires=len(device_wires) ) g = find_and_place_cuts( graph=g, cut_method=auto_cutter if callable(auto_cutter) else kahypar_cut, cut_strategy=cut_strategy, **kwargs, ) # replace the WireCut nodes in the DAG with Measure and Perpare nodes. replace_wire_cut_nodes(g) # decompose the DAG into subgraphs based on the replaced WireCut(s) # along with a quotient graph to store connections between them fragments, communication_graph = fragment_graph(g) # convert decomposed DAGs into tapes, remap their wires for device and expand them fragment_tapes = [graph_to_tape(f) for f in fragments] fragment_tapes = [ qml.map_wires(t, dict(zip(t.wires, device_wires)))[0][0] for t in fragment_tapes ] expanded = [expand_fragment_tape(t) for t in fragment_tapes] # store the data necessary for classical post processing of results configurations = [] prepare_nodes = [] measure_nodes = [] for tapes, p, m in expanded: configurations.append(tapes) prepare_nodes.append(p) measure_nodes.append(m) # flatten out the tapes to be returned tapes = tuple(tape for c in configurations for tape in c) return tapes, partial( qcut_processing_fn, communication_graph=communication_graph, prepare_nodes=prepare_nodes, measure_nodes=measure_nodes, use_opt_einsum=use_opt_einsum, )
@cut_circuit.custom_qnode_transform def _qnode_transform(self, qnode, targs, tkwargs): """Here, we overwrite the QNode execution wrapper in order to access the device wires.""" tkwargs.setdefault("device_wires", qnode.device.wires) return self.default_qnode_transform(qnode, targs, tkwargs)