From 9781f1151bd27104efefb1c7ec78ff8d8bb0728c Mon Sep 17 00:00:00 2001 From: Romain Moyard Date: Tue, 28 Nov 2023 13:45:24 -0500 Subject: [PATCH] [Transform doc] Separate qcut (#4819) **Context:** We want to make sure that qcut has a good place in the documentation and in PennyLane. **Description of the Change:** - qcut has its own folder - qcut appears in API doc - remove `qcut.py` file - rename `montecarlo.py` to `cutcircuit_mc.py` **Benefits:** Better visibility of qcut (both in the project and in the docs). --------- Co-authored-by: Tom Bromley <49409390+trbromley@users.noreply.github.com> Co-authored-by: Josh Izaac --- doc/code/qml_qcut.rst | 8 + doc/index.rst | 1 + doc/introduction/inspecting_circuits.rst | 4 +- doc/releases/changelog-dev.md | 7 + pennylane/__init__.py | 3 +- pennylane/qcut/__init__.py | 142 ++++++++++++++++++ .../qcut/_cut_kKaHyPar_sea20.ini | 0 pennylane/{transforms => }/qcut/cutcircuit.py | 36 ++--- .../montecarlo.py => qcut/cutcircuit_mc.py} | 57 +++---- .../{transforms => }/qcut/cutstrategy.py | 0 pennylane/{transforms => }/qcut/kahypar.py | 4 +- pennylane/{transforms => }/qcut/processing.py | 4 +- pennylane/{transforms => }/qcut/tapes.py | 18 +-- pennylane/{transforms => }/qcut/utils.py | 104 +++++++++---- pennylane/transforms/__init__.py | 44 ------ pennylane/transforms/qcut/__init__.py | 70 --------- pennylane/transforms/qcut/qcut.py | 74 --------- tests/transforms/test_qcut.py | 43 ++---- 18 files changed, 316 insertions(+), 303 deletions(-) create mode 100644 doc/code/qml_qcut.rst create mode 100644 pennylane/qcut/__init__.py rename pennylane/{transforms => }/qcut/_cut_kKaHyPar_sea20.ini (100%) rename pennylane/{transforms => }/qcut/cutcircuit.py (93%) rename pennylane/{transforms/qcut/montecarlo.py => qcut/cutcircuit_mc.py} (93%) rename pennylane/{transforms => }/qcut/cutstrategy.py (100%) rename pennylane/{transforms => }/qcut/kahypar.py (98%) rename pennylane/{transforms => }/qcut/processing.py (99%) rename pennylane/{transforms => }/qcut/tapes.py (96%) rename pennylane/{transforms => }/qcut/utils.py (91%) delete mode 100644 pennylane/transforms/qcut/__init__.py delete mode 100644 pennylane/transforms/qcut/qcut.py diff --git a/doc/code/qml_qcut.rst b/doc/code/qml_qcut.rst new file mode 100644 index 00000000000..e1ce5eaa508 --- /dev/null +++ b/doc/code/qml_qcut.rst @@ -0,0 +1,8 @@ +qml.qcut +======== + + +.. currentmodule:: pennylane.qcut + + +.. automodule:: pennylane.qcut diff --git a/doc/index.rst b/doc/index.rst index 122aa00ad04..a491e366303 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -196,6 +196,7 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve code/qml_ops_op_math code/qml_pauli code/qml_pulse + code/qml_qcut code/qml_qinfo code/qml_resource code/qml_shadows diff --git a/doc/introduction/inspecting_circuits.rst b/doc/introduction/inspecting_circuits.rst index dc93bf2bd03..507ebcd6243 100644 --- a/doc/introduction/inspecting_circuits.rst +++ b/doc/introduction/inspecting_circuits.rst @@ -218,12 +218,12 @@ False Another way to construct the "causal" DAG of a circuit is to use the -:func:`~pennylane.transforms.qcut.tape_to_graph` function used by the ``qcut`` module. This +:func:`~pennylane.qcut.tape_to_graph` function used by the ``qcut`` module. This function takes a quantum tape and creates a ``MultiDiGraph`` instance from the ``networkx`` python package. Using the above example, we get: ->>> g2 = qml.transforms.qcut.tape_to_graph(tape) +>>> g2 = qml.qcut.tape_to_graph(tape) >>> type(g2) >>> for k, v in g2.adjacency(): diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index abfe74935ea..ce0646a9821 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -216,6 +216,9 @@

Breaking changes πŸ’”

+* The transforms submodule `qml.transforms.qcut` becomes its own module `qml.qcut`. + [(#4819)](https://github.com/PennyLaneAI/pennylane/pull/4819) + * The decomposition of `GroverOperator` now has an additional global phase operation. [(#4666)](https://github.com/PennyLaneAI/pennylane/pull/4666) @@ -263,6 +266,9 @@

Documentation πŸ“

+* Documentation for QCut was move to its own API page `qml.qcut`. + [(#4819)](https://github.com/PennyLaneAI/pennylane/pull/4819) + * Documentation page for `qml.measurements` now links top-level accessible functions (e.g. `qml.expval`) to their top-level pages (rather than their module-level pages, eg. `qml.measurements.expval`). [(#4750)](https://github.com/PennyLaneAI/pennylane/pull/4750) @@ -353,6 +359,7 @@ Josh Izaac, Emiliano Godinez Ramirez, Ankit Khandelwal, Christina Lee, +Romain Moyard, Vincent Michaud-Rioux, Anurav Modak, Mudit Pandey, diff --git a/pennylane/__init__.py b/pennylane/__init__.py index e7b7d398680..48b58b1dad0 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -86,8 +86,6 @@ batch_input, batch_transform, batch_partial, - cut_circuit, - cut_circuit_mc, compile, defer_measurements, metric_tensor, @@ -119,6 +117,7 @@ from pennylane.vqe import ExpvalCost from pennylane.debugging import snapshots from pennylane.shadows import ClassicalShadow +from pennylane.qcut import cut_circuit, cut_circuit_mc import pennylane.pulse import pennylane.fourier diff --git a/pennylane/qcut/__init__.py b/pennylane/qcut/__init__.py new file mode 100644 index 00000000000..ef6e872c952 --- /dev/null +++ b/pennylane/qcut/__init__.py @@ -0,0 +1,142 @@ +# Copyright 2018-2021 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. +""" +This module contains quantum function transforms for cutting quantum circuits. + +.. currentmodule:: pennylane + +Overview +-------- + +This module defines transform functions for circuit cutting. This allows +for 'cutting' (or splitting) of large circuits into smaller circuits, to allow +them to be executed on devices that have a restricted number of qubits. + +Transforms for circuit cutting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~cut_circuit + ~cut_circuit_mc + + +Utility functions +~~~~~~~~~~~~~~~~~ + +There are also low-level functions that can be used to build up the circuit cutting functionalities: + +.. autosummary:: + :toctree: api + + ~qcut.tape_to_graph + ~qcut.replace_wire_cut_nodes + ~qcut.fragment_graph + ~qcut.graph_to_tape + ~qcut.expand_fragment_tape + ~qcut.expand_fragment_tapes_mc + ~qcut.qcut_processing_fn + ~qcut.qcut_processing_fn_sample + ~qcut.qcut_processing_fn_mc + ~qcut.CutStrategy + ~qcut.kahypar_cut + ~qcut.place_wire_cuts + ~qcut.find_and_place_cuts + +Cutting Circuits +---------------- + +Circuit cutting can allow you to replace a circuit with ``N`` wires by a set +of circuits with less than ``N`` wires (see also +`Peng et. al `_). This comes +with a cost: the smaller circuits require a greater number of device +executions to be evaluated. + +In PennyLane, circuit cutting for circuits that terminate in expectation values +can be activated by positioning :class:`~.pennylane.WireCut` operators at the +desired cut locations, and by decorating the QNode with +the :func:`~.pennylane.cut_circuit` transform. + +Cut circuits remain fully differentiable, and the resulting circuits can be +executed on parallel devices if available. Please see the +:func:`~.pennylane.cut_circuit` documentation for more details. + +.. note:: + + Simulated quantum circuits that produce samples can be cut using + the :func:`~.pennylane.cut_circuit_mc` + transform, which is based on the Monte Carlo method. + +Automatic cutting +----------------- + +PennyLane also has experimental support for automatic cutting of circuits --- +that is, the ability to determine optimum cut location without explicitly +placing :class:`~.pennylane.WireCut` operators. This can be enabled by using the +``auto_cutter`` keyword argument of :func:`~.pennylane.cut_circuit`; refer to the +function documentation for more details. +""" + +from .utils import ( + replace_wire_cut_node, + replace_wire_cut_nodes, + fragment_graph, + find_and_place_cuts, + place_wire_cuts, + _remove_existing_cuts, + _get_optim_cut, + _is_valid_cut, + MeasureNode, + PrepareNode, + _prep_one_state, + _prep_zero_state, + _prep_plus_state, + _prep_minus_state, + _prep_iplus_state, + _prep_iminus_state, +) +from .tapes import ( + graph_to_tape, + tape_to_graph, + expand_fragment_tape, + _qcut_expand_fn, + _get_measurements, + _find_new_wire, + _add_operator_node, +) +from .cutcircuit import cut_circuit, _cut_circuit_expand +from .cutcircuit_mc import ( + cut_circuit_mc, + _cut_circuit_mc_expand, + expand_fragment_tapes_mc, + MC_MEASUREMENTS, + MC_STATES, + _identity, + _pauliX, + _pauliY, + _pauliZ, +) +from .processing import ( + qcut_processing_fn, + qcut_processing_fn_sample, + qcut_processing_fn_mc, + contract_tensors, + _process_tensor, + _to_tensors, + _reshape_results, + _get_symbol, +) +from .kahypar import kahypar_cut, _graph_to_hmetis +from .cutstrategy import CutStrategy diff --git a/pennylane/transforms/qcut/_cut_kKaHyPar_sea20.ini b/pennylane/qcut/_cut_kKaHyPar_sea20.ini similarity index 100% rename from pennylane/transforms/qcut/_cut_kKaHyPar_sea20.ini rename to pennylane/qcut/_cut_kKaHyPar_sea20.ini diff --git a/pennylane/transforms/qcut/cutcircuit.py b/pennylane/qcut/cutcircuit.py similarity index 93% rename from pennylane/transforms/qcut/cutcircuit.py rename to pennylane/qcut/cutcircuit.py index 80a955927db..eec00693173 100644 --- a/pennylane/transforms/qcut/cutcircuit.py +++ b/pennylane/qcut/cutcircuit.py @@ -195,14 +195,14 @@ def circuit(x): .. autosummary:: :toctree: - ~transforms.qcut.tape_to_graph - ~transforms.qcut.find_and_place_cuts - ~transforms.qcut.replace_wire_cut_nodes - ~transforms.qcut.fragment_graph - ~transforms.qcut.graph_to_tape - ~transforms.qcut.expand_fragment_tape - ~transforms.qcut.qcut_processing_fn - ~transforms.qcut.CutStrategy + ~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. @@ -233,7 +233,7 @@ def circuit(x): To cut the circuit, we first convert it to its graph representation: - >>> graph = qml.transforms.qcut.tape_to_graph(tape) + >>> graph = qml.qcut.tape_to_graph(tape) .. figure:: ../../_static/qcut_graph.svg :align: center @@ -261,11 +261,11 @@ def circuit(x): measurements = [qml.expval(qml.pauli.string_to_pauli_word("ZZZ"))] uncut_tape = qml.tape.QuantumTape(ops, measurements) - >>> cut_graph = qml.transforms.qcut.find_and_place_cuts( - ... graph = qml.transforms.qcut.tape_to_graph(uncut_tape), - ... cut_strategy = qml.transforms.qcut.CutStrategy(max_free_wires=2), + >>> 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.transforms.qcut.graph_to_tape(cut_graph).draw()) + >>> print(qml.qcut.graph_to_tape(cut_graph).draw()) 0: ──RX─╭●──RY───── β•­ 1: ──RY─╰Z──//─╭●── β”œ 2: ──RX────────╰Z── β•° @@ -273,7 +273,7 @@ def circuit(x): Our next step is to remove the :class:`~.WireCut` nodes in the graph and replace with :class:`~.MeasureNode` and :class:`~.PrepareNode` pairs. - >>> qml.transforms.qcut.replace_wire_cut_nodes(graph) + >>> 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 @@ -282,11 +282,11 @@ def circuit(x): `communication_graph `__ detailing the connectivity between the components. - >>> fragments, communication_graph = qml.transforms.qcut.fragment_graph(graph) + >>> fragments, communication_graph = qml.qcut.fragment_graph(graph) We now convert the ``fragments`` back to :class:`~.QuantumTape` objects - >>> fragment_tapes = [qml.transforms.qcut.graph_to_tape(f) for f in fragments] + >>> fragment_tapes = [qml.qcut.graph_to_tape(f) for f in fragments] The circuit fragments can now be visualized: @@ -308,7 +308,7 @@ def circuit(x): .. code-block:: - expanded = [qml.transforms.qcut.expand_fragment_tape(t) for t in fragment_tapes] + expanded = [qml.qcut.expand_fragment_tape(t) for t in fragment_tapes] configurations = [] prepare_nodes = [] @@ -353,7 +353,7 @@ def circuit(x): output via a tensor network contraction >>> results = qml.execute(tapes, dev, gradient_fn=None) - >>> qml.transforms.qcut.qcut_processing_fn( + >>> qml.qcut.qcut_processing_fn( ... results, ... communication_graph, ... prepare_nodes, diff --git a/pennylane/transforms/qcut/montecarlo.py b/pennylane/qcut/cutcircuit_mc.py similarity index 93% rename from pennylane/transforms/qcut/montecarlo.py rename to pennylane/qcut/cutcircuit_mc.py index b860a8302e1..864e59a97a2 100644 --- a/pennylane/transforms/qcut/montecarlo.py +++ b/pennylane/qcut/cutcircuit_mc.py @@ -32,7 +32,12 @@ from .cutstrategy import CutStrategy from .kahypar import kahypar_cut from .processing import qcut_processing_fn_mc, qcut_processing_fn_sample -from .qcut import ( + +from .tapes import _qcut_expand_fn, graph_to_tape, tape_to_graph +from .utils import ( + find_and_place_cuts, + fragment_graph, + replace_wire_cut_nodes, MeasureNode, PrepareNode, _prep_iminus_state, @@ -42,8 +47,6 @@ _prep_plus_state, _prep_zero_state, ) -from .tapes import _qcut_expand_fn, graph_to_tape, tape_to_graph -from .utils import find_and_place_cuts, fragment_graph, replace_wire_cut_nodes def _cut_circuit_mc_expand( @@ -202,14 +205,14 @@ def circuit(x): .. autosummary:: :toctree: - ~transforms.qcut.tape_to_graph - ~transforms.qcut.find_and_place_cuts - ~transforms.qcut.replace_wire_cut_nodes - ~transforms.qcut.fragment_graph - ~transforms.qcut.graph_to_tape - ~transforms.qcut.expand_fragment_tapes_mc - ~transforms.qcut.qcut_processing_fn_sample - ~transforms.qcut.qcut_processing_fn_mc + ~qcut.tape_to_graph + ~qcut.find_and_place_cuts + ~qcut.replace_wire_cut_nodes + ~qcut.fragment_graph + ~qcut.graph_to_tape + ~qcut.expand_fragment_tapes_mc + ~qcut.qcut_processing_fn_sample + ~qcut.qcut_processing_fn_mc The following shows how these elementary steps are combined as part of the ``cut_circuit_mc()`` transform. @@ -237,7 +240,7 @@ def circuit(x): To cut the circuit, we first convert it to its graph representation: - >>> graph = qml.transforms.qcut.tape_to_graph(tape) + >>> graph = qml.qcut.tape_to_graph(tape) 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 @@ -256,11 +259,11 @@ def circuit(x): measurements = [qml.sample(wires=[0, 1, 2])] uncut_tape = qml.tape.QuantumTape(ops, measurements) - >>> cut_graph = qml.transforms.qcut.find_and_place_cuts( - ... graph=qml.transforms.qcut.tape_to_graph(uncut_tape), - ... cut_strategy=qml.transforms.qcut.CutStrategy(max_free_wires=2), + >>> 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.transforms.qcut.graph_to_tape(cut_graph).draw()) + >>> print(qml.qcut.graph_to_tape(cut_graph).draw()) 0: ──H─╭●──────────── Sample[|1⟩⟨1|] 1: ────╰X──//──X─╭●── Sample[|1⟩⟨1|] 2: ──────────────╰X── Sample[|1⟩⟨1|] @@ -268,7 +271,7 @@ def circuit(x): Our next step, using the original manual cut placement, is to remove the :class:`~.WireCut` nodes in the graph and replace with :class:`~.MeasureNode` and :class:`~.PrepareNode` pairs. - >>> qml.transforms.qcut.replace_wire_cut_nodes(graph) + >>> 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 randomly select measurement and preparation @@ -277,11 +280,11 @@ def circuit(x): `communication_graph `__ detailing the connectivity between the components. - >>> fragments, communication_graph = qml.transforms.qcut.fragment_graph(graph) + >>> fragments, communication_graph = qml.qcut.fragment_graph(graph) We now convert the ``fragments`` back to :class:`~.QuantumTape` objects - >>> fragment_tapes = [qml.transforms.qcut.graph_to_tape(f) for f in fragments] + >>> fragment_tapes = [qml.qcut.graph_to_tape(f) for f in fragments] The circuit fragments can now be visualized: @@ -315,7 +318,7 @@ def circuit(x): is determined by the number of shots. >>> shots = 3 - >>> configurations, settings = qml.transforms.qcut.expand_fragment_tapes_mc( + >>> configurations, settings = qml.qcut.expand_fragment_tapes_mc( ... fragment_tapes, communication_graph, shots=shots ... ) >>> tapes = tuple(tape for c in configurations for tape in c) @@ -353,7 +356,7 @@ def circuit(x): output bitstrings. >>> results = qml.execute(tapes, dev, gradient_fn=None) - >>> qml.transforms.qcut.qcut_processing_fn_sample( + >>> qml.qcut.qcut_processing_fn_sample( ... results, ... communication_graph, ... shots=shots, @@ -374,7 +377,7 @@ def fn(x): if x[0] == 1: return -1 - >>> qml.transforms.qcut.qcut_processing_fn_mc( + >>> qml.qcut.qcut_processing_fn_mc( ... results, ... communication_graph, ... settings, @@ -636,17 +639,17 @@ def expand_fragment_tapes_mc( We can generate the fragment tapes using the following workflow: - >>> g = qml.transforms.qcut.tape_to_graph(tape) - >>> qml.transforms.qcut.replace_wire_cut_nodes(g) - >>> subgraphs, communication_graph = qml.transforms.qcut.fragment_graph(g) - >>> tapes = [qml.transforms.qcut.graph_to_tape(sg) for sg in subgraphs] + >>> g = qml.qcut.tape_to_graph(tape) + >>> qml.qcut.replace_wire_cut_nodes(g) + >>> subgraphs, communication_graph = qml.qcut.fragment_graph(g) + >>> tapes = [qml.qcut.graph_to_tape(sg) for sg in subgraphs] We can then expand over the measurement and preparation nodes to generate random configurations using: .. code-block:: python - >>> configs, settings = qml.transforms.qcut.expand_fragment_tapes_mc(tapes, communication_graph, 3) + >>> configs, settings = qml.qcut.expand_fragment_tapes_mc(tapes, communication_graph, 3) >>> print(settings) [[1 6 2]] >>> for i, (c1, c2) in enumerate(zip(configs[0], configs[1])): diff --git a/pennylane/transforms/qcut/cutstrategy.py b/pennylane/qcut/cutstrategy.py similarity index 100% rename from pennylane/transforms/qcut/cutstrategy.py rename to pennylane/qcut/cutstrategy.py diff --git a/pennylane/transforms/qcut/kahypar.py b/pennylane/qcut/kahypar.py similarity index 98% rename from pennylane/transforms/qcut/kahypar.py rename to pennylane/qcut/kahypar.py index 10f9cd9a7a0..94c006fda7f 100644 --- a/pennylane/transforms/qcut/kahypar.py +++ b/pennylane/qcut/kahypar.py @@ -90,8 +90,8 @@ def kahypar_cut( We can let KaHyPar automatically find the optimal edges to place cuts: - >>> graph = qml.transforms.qcut.tape_to_graph(tape) - >>> cut_edges = qml.transforms.qcut.kahypar_cut( + >>> graph = qml.qcut.tape_to_graph(tape) + >>> cut_edges = qml.qcut.kahypar_cut( graph=graph, num_fragments=2, ) diff --git a/pennylane/transforms/qcut/processing.py b/pennylane/qcut/processing.py similarity index 99% rename from pennylane/transforms/qcut/processing.py rename to pennylane/qcut/processing.py index 9c31ec96835..d768b14c395 100644 --- a/pennylane/transforms/qcut/processing.py +++ b/pennylane/qcut/processing.py @@ -22,7 +22,7 @@ import pennylane as qml from pennylane import numpy as np -from .qcut import MeasureNode, PrepareNode +from .utils import MeasureNode, PrepareNode def qcut_processing_fn( @@ -298,7 +298,7 @@ def contract_tensors( The network can then be contracted using: - >>> qml.transforms.qcut.contract_tensors(tensors, graph, prep, meas) + >>> qml.qcut.contract_tensors(tensors, graph, prep, meas) 38 """ # pylint: disable=import-outside-toplevel diff --git a/pennylane/transforms/qcut/tapes.py b/pennylane/qcut/tapes.py similarity index 96% rename from pennylane/transforms/qcut/tapes.py rename to pennylane/qcut/tapes.py index a29836dcf35..b7c0a3588b5 100644 --- a/pennylane/transforms/qcut/tapes.py +++ b/pennylane/qcut/tapes.py @@ -31,7 +31,7 @@ from pennylane.tape import QuantumScript, QuantumTape from pennylane.wires import Wires -from .qcut import ( +from .utils import ( MeasureNode, PrepareNode, _prep_iplus_state, @@ -74,7 +74,7 @@ def tape_to_graph(tape: QuantumTape) -> MultiDiGraph: Its corresponding circuit graph can be found using - >>> qml.transforms.qcut.tape_to_graph(tape) + >>> qml.qcut.tape_to_graph(tape) """ graph = MultiDiGraph() @@ -138,8 +138,8 @@ def graph_to_tape(graph: MultiDiGraph) -> QuantumTape: qml.RX(0.4, wires=0), qml.RY(0.5, wires=1), qml.CNOT(wires=[0, 1]), - qml.transforms.qcut.MeasureNode(wires=1), - qml.transforms.qcut.PrepareNode(wires=1), + qml.qcut.MeasureNode(wires=1), + qml.qcut.PrepareNode(wires=1), qml.CNOT(wires=[1, 0]), ] measurements = [qml.expval(qml.PauliZ(0))] @@ -148,8 +148,8 @@ def graph_to_tape(graph: MultiDiGraph) -> QuantumTape: This circuit contains operations that follow a :class:`~.MeasureNode`. These operations will subsequently act on wire ``2`` instead of wire ``1``: - >>> graph = qml.transforms.qcut.tape_to_graph(tape) - >>> tape = qml.transforms.qcut.graph_to_tape(graph) + >>> graph = qml.qcut.tape_to_graph(tape) + >>> tape = qml.qcut.graph_to_tape(graph) >>> print(tape.draw()) 0: ──RX──────────╭●──────────────╭X── 1: ──RY──────────╰X──MeasureNode─│─── @@ -268,15 +268,15 @@ def expand_fragment_tape( .. code-block:: python ops = [ - qml.transforms.qcut.PrepareNode(wires=0), + qml.qcut.PrepareNode(wires=0), qml.RX(0.5, wires=0), - qml.transforms.qcut.MeasureNode(wires=0), + qml.qcut.MeasureNode(wires=0), ] tape = qml.tape.QuantumTape(ops) We can expand over the measurement and preparation nodes using: - >>> tapes, prep, meas = qml.transforms.qcut.expand_fragment_tape(tape) + >>> tapes, prep, meas = qml.qcut.expand_fragment_tape(tape) >>> for t in tapes: ... print(qml.drawer.tape_text(t, decimals=1)) 0: ──I──RX(0.5)── diff --git a/pennylane/transforms/qcut/utils.py b/pennylane/qcut/utils.py similarity index 91% rename from pennylane/transforms/qcut/utils.py rename to pennylane/qcut/utils.py index ffd2a98aaa7..bdf74269cad 100644 --- a/pennylane/transforms/qcut/utils.py +++ b/pennylane/qcut/utils.py @@ -17,19 +17,71 @@ import warnings +import uuid from typing import Any, Callable, Sequence, Tuple from networkx import MultiDiGraph, has_path, weakly_connected_components +import pennylane as qml from pennylane import numpy as np from pennylane.measurements import MeasurementProcess -from pennylane.operation import Operation from pennylane.ops.meta import WireCut from pennylane.queuing import WrappedObj +from pennylane.operation import Operation from .kahypar import kahypar_cut from .cutstrategy import CutStrategy -from .tapes import graph_to_tape -from .qcut import MeasureNode, PrepareNode + + +class MeasureNode(Operation): + """Placeholder node for measurement operations""" + + num_wires = 1 + grad_method = None + + def __init__(self, *params, wires=None, id=None): + id = id or str(uuid.uuid4()) + + super().__init__(*params, wires=wires, id=id) + + +class PrepareNode(Operation): + """Placeholder node for state preparations""" + + num_wires = 1 + grad_method = None + + def __init__(self, *params, wires=None, id=None): + id = id or str(uuid.uuid4()) + + super().__init__(*params, wires=wires, id=id) + + +def _prep_zero_state(wire): + qml.Identity(wire) + + +def _prep_one_state(wire): + qml.PauliX(wire) + + +def _prep_plus_state(wire): + qml.Hadamard(wire) + + +def _prep_minus_state(wire): + qml.PauliX(wire) + qml.Hadamard(wire) + + +def _prep_iplus_state(wire): + qml.Hadamard(wire) + qml.S(wires=wire) + + +def _prep_iminus_state(wire): + qml.PauliX(wire) + qml.Hadamard(wire) + qml.S(wires=wire) def find_and_place_cuts( @@ -99,8 +151,8 @@ def find_and_place_cuts( Since the existing :class:`~.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 = qml.qcut.tape_to_graph(tape) + >>> cut_graph = qml.qcut.find_and_place_cuts( graph=graph, num_fragments=2, imbalance=0.5, @@ -108,7 +160,7 @@ def find_and_place_cuts( Visualizing the newly-placed cut: - >>> print(qml.transforms.qcut.graph_to_tape(cut_graph).draw()) + >>> print(qml.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⟩ @@ -119,15 +171,15 @@ def find_and_place_cuts( 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( + >>> cut_graph = qml.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) + >>> frags, comm_graph = qml.qcut.fragment_graph(cut_graph) >>> for t in frags: - ... print(qml.transforms.qcut.graph_to_tape(t).draw()) + ... print(qml.qcut.graph_to_tape(t).draw()) .. code-block:: @@ -145,7 +197,7 @@ def find_and_place_cuts( simple cut strategy is to simply specify the the ``max_free_wires`` argument (or equivalently directly passing a :class:`pennylane.Device` to the ``device`` argument): - >>> cut_strategy = qml.transforms.qcut.CutStrategy(max_free_wires=2) + >>> cut_strategy = qml.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}, @@ -165,11 +217,11 @@ def find_and_place_cuts( in order to search for the optimal cut. This is done by directly passing a :class:`~.CutStrategy` to :func:`~.find_and_place_cuts`: - >>> cut_graph = qml.transforms.qcut.find_and_place_cuts( + >>> cut_graph = qml.qcut.find_and_place_cuts( graph=graph, cut_strategy=cut_strategy, ) - >>> print(qml.transforms.qcut.graph_to_tape(cut_graph).draw()) + >>> print(qml.qcut.graph_to_tape(cut_graph).draw()) 0: ──RX──//─╭●──//────────╭●──//────────── β•­ 1: ──RY──//─╰X──//─╭●──//─╰X────────────── β”‚ a: ──RX──//─╭●──//─╰X──//─╭●──//──RX──//── β”œ @@ -178,10 +230,10 @@ def find_and_place_cuts( 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) + >>> qml.qcut.replace_wire_cut_nodes(cut_graph) + >>> frags, comm_graph = qml.qcut.fragment_graph(cut_graph) >>> for t in frags: - ... print(qml.transforms.qcut.graph_to_tape(t).draw()) + ... print(qml.qcut.graph_to_tape(t).draw()) .. code-block:: @@ -315,8 +367,8 @@ def replace_wire_cut_node(node: WireCut, graph: MultiDiGraph): We can find the circuit graph and remove the wire cut node using: - >>> graph = qml.transforms.qcut.tape_to_graph(tape) - >>> qml.transforms.qcut.replace_wire_cut_node(wire_cut, graph) + >>> graph = qml.qcut.tape_to_graph(tape) + >>> qml.qcut.replace_wire_cut_node(wire_cut, graph) """ node_obj = WrappedObj(node) predecessors = graph.pred[node_obj] @@ -398,8 +450,8 @@ def replace_wire_cut_nodes(graph: MultiDiGraph): We can find the circuit graph and remove all the wire cut nodes using: - >>> graph = qml.transforms.qcut.tape_to_graph(tape) - >>> qml.transforms.qcut.replace_wire_cut_nodes(graph) + >>> graph = qml.qcut.tape_to_graph(tape) + >>> qml.qcut.replace_wire_cut_nodes(graph) """ for op in list(graph.nodes): if isinstance(op.obj, WireCut): @@ -442,7 +494,7 @@ def place_wire_cuts( ``RY(0.543, wires=["a"])`` and ``CNOT(wires=[0, 'a'])`` operations after the tape is constructed, we can first find the edge in the graph: - >>> graph = qml.transforms.qcut.tape_to_graph(tape) + >>> graph = qml.qcut.tape_to_graph(tape) >>> op0, op1 = tape.operations[1], tape.operations[2] >>> cut_edges = [e for e in graph.edges if e[0] is op0 and e[1] is op1] >>> cut_edges @@ -450,13 +502,13 @@ def place_wire_cuts( Then feed it to this function for placement: - >>> cut_graph = qml.transforms.qcut.place_wire_cuts(graph=graph, cut_edges=cut_edges) + >>> cut_graph = qml.qcut.place_wire_cuts(graph=graph, cut_edges=cut_edges) >>> cut_graph And visualize the cut by converting back to a tape: - >>> print(qml.transforms.qcut.graph_to_tape(cut_graph).draw()) + >>> print(qml.qcut.graph_to_tape(cut_graph).draw()) 0: ──RX(0.432)──────╭●─── ⟨Z⟩ a: ──RY(0.543)──//──╰X─── """ @@ -562,9 +614,9 @@ def fragment_graph(graph: MultiDiGraph) -> Tuple[Tuple[MultiDiGraph], MultiDiGra We can find the corresponding graph, remove all the wire cut nodes, and find the subgraphs and communication graph by using: - >>> graph = qml.transforms.qcut.tape_to_graph(tape) - >>> qml.transforms.qcut.replace_wire_cut_nodes(graph) - >>> qml.transforms.qcut.fragment_graph(graph) + >>> graph = qml.qcut.tape_to_graph(tape) + >>> qml.qcut.replace_wire_cut_nodes(graph) + >>> qml.qcut.fragment_graph(graph) ((, , , @@ -651,7 +703,7 @@ def _is_valid_cut( best_candidate_yet = (key not in cut_candidates) or (len(cut_candidates[key]) > num_cuts) # pylint: disable=no-member all_fragments_fit = all( - len(graph_to_tape(f).wires) <= max_free_wires for j, f in enumerate(fragments) + len(qml.qcut.graph_to_tape(f).wires) <= max_free_wires for j, f in enumerate(fragments) ) return correct_num_fragments and best_candidate_yet and all_fragments_fit diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py index d6512ed4e3c..b3dce386921 100644 --- a/pennylane/transforms/__init__.py +++ b/pennylane/transforms/__init__.py @@ -84,48 +84,6 @@ ~transforms.sk_decomposition -Transform for circuit cutting -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :func:`~.cut_circuit` transform accepts a QNode and returns a new function that cuts the original circuit, -allowing larger circuits to be split into smaller circuits that are compatible with devices that -have a restricted number of qubits. - -.. autosummary:: - :toctree: api - - ~cut_circuit - -The :func:`~.cut_circuit_mc` transform is designed to be used for cutting circuits which contain :func:`~.sample` -measurements and is implemented using a Monte Carlo method. Similarly to the :func:`~.cut_circuit` -transform, this transform accepts a QNode and returns a new function that cuts the original circuit. -This transform can also accept an optional classical processing function to calculate an -expectation value. - -.. autosummary:: - :toctree: api - - ~cut_circuit_mc - -There are also low-level functions that can be used to build up the circuit cutting functionalities: - -.. autosummary:: - :toctree: api - - ~transforms.qcut.tape_to_graph - ~transforms.qcut.replace_wire_cut_nodes - ~transforms.qcut.fragment_graph - ~transforms.qcut.graph_to_tape - ~transforms.qcut.expand_fragment_tape - ~transforms.qcut.expand_fragment_tapes_mc - ~transforms.qcut.qcut_processing_fn - ~transforms.qcut.qcut_processing_fn_sample - ~transforms.qcut.qcut_processing_fn_mc - ~transforms.qcut.CutStrategy - ~transforms.qcut.kahypar_cut - ~transforms.qcut.place_wire_cuts - ~transforms.qcut.find_and_place_cuts - Transforms for error mitigation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -281,7 +239,5 @@ set_decomposition, ) from .transpile import transpile -from . import qcut -from .qcut import cut_circuit, cut_circuit_mc from .zx import to_zx, from_zx from .broadcast_expand import broadcast_expand diff --git a/pennylane/transforms/qcut/__init__.py b/pennylane/transforms/qcut/__init__.py deleted file mode 100644 index 49729b9dd9f..00000000000 --- a/pennylane/transforms/qcut/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2018-2021 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. -""" -This subpackage contains quantum function transforms for cutting quantum circuits. -""" - -from .utils import ( - replace_wire_cut_node, - replace_wire_cut_nodes, - fragment_graph, - find_and_place_cuts, - place_wire_cuts, - _remove_existing_cuts, - _get_optim_cut, - _is_valid_cut, -) -from .tapes import ( - graph_to_tape, - tape_to_graph, - expand_fragment_tape, - _qcut_expand_fn, - _get_measurements, - _find_new_wire, - _add_operator_node, -) -from .cutcircuit import cut_circuit, _cut_circuit_expand -from .montecarlo import ( - cut_circuit_mc, - _cut_circuit_mc_expand, - expand_fragment_tapes_mc, - MC_MEASUREMENTS, - MC_STATES, - _identity, - _pauliX, - _pauliY, - _pauliZ, -) -from .processing import ( - qcut_processing_fn, - qcut_processing_fn_sample, - qcut_processing_fn_mc, - contract_tensors, - _process_tensor, - _to_tensors, - _reshape_results, - _get_symbol, -) -from .kahypar import kahypar_cut, _graph_to_hmetis -from .cutstrategy import CutStrategy -from .qcut import ( - MeasureNode, - PrepareNode, - _prep_one_state, - _prep_zero_state, - _prep_plus_state, - _prep_minus_state, - _prep_iplus_state, - _prep_iminus_state, -) diff --git a/pennylane/transforms/qcut/qcut.py b/pennylane/transforms/qcut/qcut.py deleted file mode 100644 index e32dc587295..00000000000 --- a/pennylane/transforms/qcut/qcut.py +++ /dev/null @@ -1,74 +0,0 @@ -# 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. -""" -Miscellaneous support for circuit cutting. -""" - - -import uuid - -import pennylane as qml -from pennylane.operation import Operation - - -class MeasureNode(Operation): - """Placeholder node for measurement operations""" - - num_wires = 1 - grad_method = None - - def __init__(self, *params, wires=None, id=None): - id = id or str(uuid.uuid4()) - - super().__init__(*params, wires=wires, id=id) - - -class PrepareNode(Operation): - """Placeholder node for state preparations""" - - num_wires = 1 - grad_method = None - - def __init__(self, *params, wires=None, id=None): - id = id or str(uuid.uuid4()) - - super().__init__(*params, wires=wires, id=id) - - -def _prep_zero_state(wire): - qml.Identity(wire) - - -def _prep_one_state(wire): - qml.PauliX(wire) - - -def _prep_plus_state(wire): - qml.Hadamard(wire) - - -def _prep_minus_state(wire): - qml.PauliX(wire) - qml.Hadamard(wire) - - -def _prep_iplus_state(wire): - qml.Hadamard(wire) - qml.S(wires=wire) - - -def _prep_iminus_state(wire): - qml.PauliX(wire) - qml.Hadamard(wire) - qml.S(wires=wire) diff --git a/tests/transforms/test_qcut.py b/tests/transforms/test_qcut.py index a323448addc..e0c2405bfe5 100644 --- a/tests/transforms/test_qcut.py +++ b/tests/transforms/test_qcut.py @@ -34,8 +34,8 @@ import pennylane as qml from pennylane import numpy as np +from pennylane import qcut from pennylane.queuing import WrappedObj -from pennylane.transforms import qcut from pennylane.wires import Wires pytestmark = pytest.mark.qcut @@ -3061,7 +3061,7 @@ def circuit(x): qml.CNOT(wires=[0, 1]) return qml.sample(wires=[0, 1]) - spy = mocker.spy(qcut.montecarlo, "qcut_processing_fn_mc") + spy = mocker.spy(qcut.cutcircuit_mc, "qcut_processing_fn_mc") x = np.array(0.531, requires_grad=True) res = circuit(x) @@ -3104,7 +3104,7 @@ def circuit(x): qml.CNOT(wires=[0, 1]) return qml.sample(wires=[0, 1]) - spy = mocker.spy(qcut.montecarlo, "qcut_processing_fn_mc") + spy = mocker.spy(qcut.cutcircuit_mc, "qcut_processing_fn_mc") x = 0.4 res = circuit(x) @@ -3163,7 +3163,7 @@ def circuit(params): return qml.sample(wires=[1, 2, 3]) - spy = mocker.spy(qcut.montecarlo, "qcut_processing_fn_mc") + spy = mocker.spy(qcut.cutcircuit_mc, "qcut_processing_fn_mc") params = np.array([0.4, 0.5, 0.6, 0.7, 0.8], requires_grad=True) res = circuit(params) @@ -4245,12 +4245,8 @@ def circuit(): dev_2 = qml.device("default.qubit", wires=["Alice", 3.14, "Bob"]) uncut_circuit = qml.QNode(circuit, dev_uncut) - cut_circuit_1 = qml.transforms.cut_circuit( - qml.QNode(circuit, dev_1), use_opt_einsum=use_opt_einsum - ) - cut_circuit_2 = qml.transforms.cut_circuit( - qml.QNode(circuit, dev_2), use_opt_einsum=use_opt_einsum - ) + cut_circuit_1 = qml.cut_circuit(qml.QNode(circuit, dev_1), use_opt_einsum=use_opt_einsum) + cut_circuit_2 = qml.cut_circuit(qml.QNode(circuit, dev_2), use_opt_einsum=use_opt_einsum) res_expected = uncut_circuit() res_1 = cut_circuit_1() @@ -4267,7 +4263,7 @@ def test_circuit_with_disconnected_components(self, use_opt_einsum): dev = qml.device("default.qubit", wires=3) - @partial(qml.transforms.cut_circuit, use_opt_einsum=use_opt_einsum) + @partial(qml.cut_circuit, use_opt_einsum=use_opt_einsum) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) @@ -4289,7 +4285,7 @@ def test_circuit_with_trivial_wire_cut(self, use_opt_einsum, mocker): dev = qml.device("default.qubit", wires=2) - @partial(qml.transforms.cut_circuit, use_opt_einsum=use_opt_einsum) + @partial(qml.cut_circuit, use_opt_einsum=use_opt_einsum) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) @@ -4514,7 +4510,7 @@ def circuit(): return qml.apply(measurement) spy = mocker.spy(qcut.cutcircuit, "_qcut_expand_fn") - spy_mc = mocker.spy(qcut.montecarlo, "_qcut_expand_fn") + spy_mc = mocker.spy(qcut.cutcircuit_mc, "_qcut_expand_fn") kwargs = {"shots": 10} if measurement.return_type is qml.measurements.Sample else {} cut_transform(circuit, device_wires=[0])(**kwargs) @@ -4535,7 +4531,7 @@ def test_expansion(self, mocker, cut_transform, measurement): tape = qml.tape.QuantumScript.from_queue(q) spy = mocker.spy(qcut.tapes, "_qcut_expand_fn") spy_cc = mocker.spy(qcut.cutcircuit, "_qcut_expand_fn") - spy_mc = mocker.spy(qcut.montecarlo, "_qcut_expand_fn") + spy_mc = mocker.spy(qcut.cutcircuit_mc, "_qcut_expand_fn") kwargs = {"shots": 10} if measurement.return_type is qml.measurements.Sample else {} cut_transform(tape, device_wires=[0], **kwargs) @@ -4617,7 +4613,7 @@ def circuit(template_weights): qnode_cut = qcut.cut_circuit_mc(qml.QNode(circuit, dev_cut)) spy_tapes = mocker.spy(qcut.tapes, "_qcut_expand_fn") - spy_mc = mocker.spy(qcut.montecarlo, "_qcut_expand_fn") + spy_mc = mocker.spy(qcut.cutcircuit_mc, "_qcut_expand_fn") qnode_cut(template_weights) @@ -4897,7 +4893,7 @@ class TestKaHyPar: ), ] config_path = str( - Path(__file__).parent.parent.parent / "pennylane/transforms/qcut/_cut_kKaHyPar_sea20.ini" + Path(__file__).parent.parent.parent / "pennylane/qcut/_cut_kKaHyPar_sea20.ini" ) def test_seed_in_ci(self): @@ -5282,7 +5278,7 @@ def test_circuit_with_disconnected_components(self): dev = qml.device("default.qubit", wires=3) - @partial(qml.transforms.cut_circuit, auto_cutter=True) + @partial(qml.cut_circuit, auto_cutter=True) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) @@ -5302,7 +5298,7 @@ def test_circuit_with_trivial_wire_cut(self): dev = qml.device("default.qubit", wires=2) - @partial(qml.transforms.cut_circuit, auto_cutter=True) + @partial(qml.cut_circuit, auto_cutter=True) @qml.qnode(dev) def circuit(x): qml.RX(x, wires=0) @@ -5385,7 +5381,7 @@ def block(weights, wires): template_weights = [[0.1, -0.3]] * n_blocks device_size = 2 - cut_strategy = qml.transforms.qcut.CutStrategy(max_free_wires=device_size) + cut_strategy = qml.qcut.CutStrategy(max_free_wires=device_size) with qml.queuing.AnnotatedQueue() as q0: qml.MPS(range(n_wires), n_block_wires, block, n_params_block, template_weights) @@ -5415,13 +5411,6 @@ def block(weights, wires): assert all(len(set(e[2] for e in f.edges.data("wire"))) <= device_size for f in frags) -class TestRedirect: - """Tests that redirect in qcut.__init__ works to maintain import pathways while reorganizing files""" - - def test_qcut_redirects_to_qcut_qcut(self): - assert qml.transforms.qcut._prep_one_state == qml.transforms.qcut.qcut._prep_one_state - - class TestCutCircuitWithHamiltonians: """Integration tests for `cut_circuit` transform with Hamiltonians.""" @@ -5562,7 +5551,7 @@ def block(weights, wires): template_weights = [[0.1, -0.3]] * n_blocks device_size = 2 - cut_strategy = qml.transforms.qcut.CutStrategy(max_free_wires=device_size) + cut_strategy = qml.qcut.CutStrategy(max_free_wires=device_size) hamiltonian = qml.Hamiltonian( [1.0, 1.0],