From baaf959540757f3aa72c76b016ffcc9c100f24af Mon Sep 17 00:00:00 2001 From: JerryChen97 Date: Thu, 1 Oct 2026 16:29:40 -0400 Subject: [PATCH 1/2] Remove the qp.qaoa module Removes `pennylane/qaoa` (cost, cycle, layers, mixers) along with its tests, API docs page, `tach.toml` module entry and the documentation-tests ignore rule. `QAOAEmbedding` is a template and is unaffected. Authored with assistance from an AI coding assistant. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/documentation-tests.yml | 1 - doc/code/qp_qaoa.rst | 73 - doc/development/deprecations.rst | 8 + doc/index.rst | 1 - doc/releases/changelog-dev.md | 7 + pennylane/__init__.py | 1 - pennylane/qaoa/__init__.py | 119 -- pennylane/qaoa/cost.py | 710 ------- pennylane/qaoa/cycle.py | 714 ------- pennylane/qaoa/layers.py | 156 -- pennylane/qaoa/mixers.py | 250 --- tach.toml | 5 - tests/test_qaoa.py | 2077 --------------------- 13 files changed, 15 insertions(+), 4107 deletions(-) delete mode 100644 doc/code/qp_qaoa.rst delete mode 100644 pennylane/qaoa/__init__.py delete mode 100644 pennylane/qaoa/cost.py delete mode 100644 pennylane/qaoa/cycle.py delete mode 100644 pennylane/qaoa/layers.py delete mode 100644 pennylane/qaoa/mixers.py delete mode 100644 tests/test_qaoa.py diff --git a/.github/workflows/documentation-tests.yml b/.github/workflows/documentation-tests.yml index d7cec1154fa..def2464eea8 100644 --- a/.github/workflows/documentation-tests.yml +++ b/.github/workflows/documentation-tests.yml @@ -92,7 +92,6 @@ jobs: --ignore=pennylane/pulse --ignore=pennylane/io --ignore=pennylane/qnn - --ignore=pennylane/qaoa --ignore=pennylane/data --ignore=pennylane/debugging --ignore=pennylane/qchem diff --git a/doc/code/qp_qaoa.rst b/doc/code/qp_qaoa.rst deleted file mode 100644 index 278d8ce4257..00000000000 --- a/doc/code/qp_qaoa.rst +++ /dev/null @@ -1,73 +0,0 @@ -qp.qaoa -======== - -.. currentmodule:: pennylane.qaoa - -.. automodule:: pennylane.qaoa - -Solving the MaxCut problem using QAOA -------------------------------------- - -We can demonstrate the PennyLane QAOA functionality with a basic application of QAOA: -solving the `MaxCut `__ problem. -We begin by defining the set of wires on which QAOA is executed, as well as the graph -on which we will perform MaxCut. The node labels of the graph are the index of the wire to which they -correspond: - -.. code-block:: python3 - - import pennylane as qp - from pennylane import qaoa - from networkx import Graph - - # Defines the wires and the graph on which MaxCut is being performed - wires = range(3) - graph = Graph([(0, 1), (1, 2), (2, 0)]) - -We now obtain the QAOA cost and mixer Hamiltonians for MaxCut on the graph that we defined: - -.. code-block:: python3 - - # Defines the QAOA cost and mixer Hamiltonians - cost_h, mixer_h = qaoa.maxcut(graph) - -These cost and mixer Hamiltonians are then used to define layers of the variational QAOA ansatz, -which we implement as the following function: - -.. code-block:: python3 - - # Defines a layer of the QAOA ansatz from the cost and mixer Hamiltonians - def qaoa_layer(gamma, alpha): - qaoa.cost_layer(gamma, cost_h) - qaoa.mixer_layer(alpha, mixer_h) - -Finally, the full QAOA circuit is built. We begin by initializing the wires in an even superposition over -computational basis states, and then repeatedly apply QAOA layers with the -``qp.layer`` method. In this case we repeat the circuit twice: - -.. code-block:: python3 - - # Repeatedly applies layers of the QAOA ansatz - def circuit(params): - for w in wires: - qp.Hadamard(wires=w) - qp.layer(qaoa_layer, 2, params[0], params[1]) - -With the circuit defined, we call the device on which QAOA will be executed and use ``qp.expval()`` to -create the QAOA cost function: the expected value of the cost Hamiltonian with respect to the parametrized output -of the QAOA circuit. - -.. code-block:: python3 - - # Defines the device and the QAOA cost function - dev = qp.device('default.qubit', wires=len(wires)) - @qp.qnode(dev) - def cost_function(params): - circuit(params) - return qp.expval(cost_h) - ->>> print(cost_function([[1, 1], [1, 1]])) --1.8260274380964299 - -The QAOA cost function can then be optimized in the usual way, by calling one of the built-in PennyLane optimizers -and updating the variational parameters until the expected value of the cost Hamiltonian is minimized. diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst index 715cfd810e6..538ae0f14e0 100644 --- a/doc/development/deprecations.rst +++ b/doc/development/deprecations.rst @@ -107,6 +107,14 @@ for details on how to port your legacy code to the new system. The following fun Completed deprecation cycles ---------------------------- +* The ``pennylane.qaoa`` module has been removed, including the mixer Hamiltonians + (``x_mixer``, ``xy_mixer``, ``bit_flip_mixer``), the cost Hamiltonians (``maxcut``, + ``max_independent_set``, ``min_vertex_cover``, ``max_clique``, ``max_weight_cycle``, + ``bit_driver``, ``edge_driver``), the ansatz layers (``cost_layer``, ``mixer_layer``) + and the ``pennylane.qaoa.cycle`` helpers. :class:`~.QAOAEmbedding` is unaffected. + + - Removed in releases after v0.45 + * The ``pennylane.qcut`` module, including ``pennylane.cut_circuit``, ``pennylane.cut_circuit_mc``, and the ``WireCut`` operator, has been removed. diff --git a/doc/index.rst b/doc/index.rst index 64ec92d7cf4..5158712bb1b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -255,7 +255,6 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve code/qp_ops_op_math code/qp_pauli code/qp_pulse - code/qp_qaoa code/qp_qchem code/qp_qnn code/qp_resource diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index f7554128b02..fd3eeb34e04 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -1040,6 +1040,13 @@ [(#10209)](https://github.com/PennyLaneAI/pennylane/pull/10209) +* The ``pennylane.qaoa`` module has been removed. This includes the mixer Hamiltonians + (``x_mixer``, ``xy_mixer``, ``bit_flip_mixer``), the cost Hamiltonians (``maxcut``, + ``max_independent_set``, ``min_vertex_cover``, ``max_clique``, ``max_weight_cycle``, + ``bit_driver``, ``edge_driver``), the ansatz layers (``cost_layer``, ``mixer_layer``) and the + ``pennylane.qaoa.cycle`` helpers. :class:`~.QAOAEmbedding` is unaffected. + [(#10248)](https://github.com/PennyLaneAI/pennylane/pull/10248) + * The ``pennylane.noise`` module has been removed, including ``NoiseModel``, ``add_noise``, ``insert``, noise mitigation transforms (``mitigate_with_zne``, ``fold_global``, ``poly_extrapolate``, ``richardson_extrapolate``, diff --git a/pennylane/__init__.py b/pennylane/__init__.py index 61a2845c930..18a65f0c1ba 100644 --- a/pennylane/__init__.py +++ b/pennylane/__init__.py @@ -111,7 +111,6 @@ from pennylane.templates.swapnetworks import * from pennylane.templates.state_preparations import * from pennylane.templates.subroutines import * -from pennylane import qaoa from pennylane.workflow import QNode, qnode, execute, set_shots, marker from pennylane import workflow diff --git a/pennylane/qaoa/__init__.py b/pennylane/qaoa/__init__.py deleted file mode 100644 index 8ed7d56d488..00000000000 --- a/pennylane/qaoa/__init__.py +++ /dev/null @@ -1,119 +0,0 @@ -# Copyright 2018-2025 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. -""" -Overview --------- - -This module provides a collection of methods that help in the construction of -QAOA workflows. - -.. currentmodule:: pennylane.qaoa - -Mixer Hamiltonians -~~~~~~~~~~~~~~~~~~ - -Methods for constructing QAOA mixer Hamiltonians. - -.. autosummary:: - :toctree: api - - mixers.bit_flip_mixer - mixers.x_mixer - mixers.xy_mixer - -Cost Hamiltonians -~~~~~~~~~~~~~~~~~ - -Methods for generating QAOA cost Hamiltonians corresponding to -different optimization problems. - -.. autosummary:: - :toctree: api - - cost.bit_driver - cost.edge_driver - cost.max_clique - cost.max_independent_set - cost.max_weight_cycle - cost.maxcut - cost.min_vertex_cover - -QAOA Layers -~~~~~~~~~~~ - -Methods that define cost and mixer layers for use in QAOA workflows. - -.. autosummary:: - :toctree: api - - layers.cost_layer - layers.mixer_layer - -Cycle Optimization -~~~~~~~~~~~~~~~~~~ - -Functionality for finding the maximum weighted cycle of directed graphs. - -.. autosummary:: - :toctree: api - - cycle.cycle_mixer - cycle.edges_to_wires - cycle.loss_hamiltonian - cycle.net_flow_constraint - cycle.out_flow_constraint - cycle.wires_to_edges - -""" - -from .mixers import x_mixer, xy_mixer, bit_flip_mixer -from .cycle import ( - edges_to_wires, - wires_to_edges, - cycle_mixer, - loss_hamiltonian, - out_flow_constraint, - net_flow_constraint, -) -from .cost import ( - bit_driver, - edge_driver, - maxcut, - max_independent_set, - min_vertex_cover, - max_clique, - max_weight_cycle, -) -from .layers import cost_layer, mixer_layer - -__all__ = [ - "x_mixer", - "xy_mixer", - "bit_flip_mixer", - "edges_to_wires", - "wires_to_edges", - "cycle_mixer", - "loss_hamiltonian", - "out_flow_constraint", - "net_flow_constraint", - "bit_driver", - "edge_driver", - "maxcut", - "max_independent_set", - "min_vertex_cover", - "max_clique", - "max_weight_cycle", - "cost_layer", - "mixer_layer", -] diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py deleted file mode 100644 index e56a8a506af..00000000000 --- a/pennylane/qaoa/cost.py +++ /dev/null @@ -1,710 +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. -r""" -Methods for generating QAOA cost Hamiltonians corresponding to -different optimization problems. -""" - -from collections.abc import Iterable -from typing import TYPE_CHECKING - -import rustworkx as rx - -from pennylane.ops import Identity, LinearCombination, Z -from pennylane.wires import Wires - -from .cycle import ( - cycle_mixer, - loss_hamiltonian, - net_flow_constraint, - out_flow_constraint, - wires_to_edges, -) -from .mixers import bit_flip_mixer, x_mixer - -if TYPE_CHECKING: - from networkx import Graph as nx_Graph -else: - nx_Graph = None # keep pylint happy - - -def _validate_graph(graph): - import networkx as nx # pylint: disable=import-outside-toplevel - - if not isinstance(graph, (nx.Graph, rx.PyGraph)): - raise ValueError( - f"Input graph must be a nx.Graph or rx.PyGraph, got {type(graph).__name__}" - ) - - -######################## -# Hamiltonian components - - -def bit_driver(wires: Iterable | Wires, b: int): - r"""Returns the bit-driver cost Hamiltonian. - - This Hamiltonian is defined as: - - .. math:: H \ = \ (-1)^{b + 1} \displaystyle\sum_{i} Z_i - - where :math:`Z_i` is the Pauli-Z operator acting on the - :math:`i`-th wire and :math:`b \ \in \ \{0, \ 1\}`. This Hamiltonian is often used when - constructing larger QAOA cost Hamiltonians. - - Args: - wires (Iterable or Wires): The wires on which the Hamiltonian acts - b (int): Either :math:`0` or :math:`1`. Determines whether the Hamiltonian assigns - lower energies to bitstrings with a majority of bits being :math:`0` or - a majority of bits being :math:`1`, respectively. - - Returns: - .Hamiltonian: - - **Example** - - >>> wires = range(3) - >>> hamiltonian = qaoa.bit_driver(wires, 1) - >>> print(hamiltonian) - 1 * Z(0) + 1 * Z(1) + 1 * Z(2) - """ - if b == 0: - coeffs = [-1 for _ in wires] - elif b == 1: - coeffs = [1 for _ in wires] - else: - raise ValueError(f"'b' must be either 0 or 1, got {b}") - - ops = [Z(w) for w in wires] - return LinearCombination(coeffs, ops) - - -def edge_driver(graph: nx_Graph | rx.PyGraph, reward: list): - r"""Returns the edge-driver cost Hamiltonian. - - Given some graph, :math:`G` with each node representing a wire, and a binary - colouring where each node/wire is assigned either :math:`|0\rangle` or :math:`|1\rangle`, the edge driver - cost Hamiltonian will assign a lower energy to edges represented by qubit states with endpoint colourings - supplied in ``reward``. - - For instance, if ``reward`` is ``["11"]``, then edges - with both endpoints coloured as ``1`` (the state :math:`|11\rangle`) will be assigned a lower energy, while - the other colourings (``"00"``, ``"10"``, and ``"01"`` corresponding to states - :math:`|00\rangle`, :math:`|10\rangle`, and :math:`|10\rangle`, respectively) will be assigned a higher energy. - - See usage details for more information. - - Args: - graph (nx.Graph or rx.PyGraph): The graph on which the Hamiltonian is defined - reward (list[str]): The list of two-bit bitstrings that are assigned a lower energy by the Hamiltonian - - Returns: - .Hamiltonian: - - **Example** - - >>> import networkx as nx - >>> graph = nx.Graph([(0, 1), (1, 2)]) - >>> hamiltonian = qaoa.edge_driver(graph, ["11", "10", "01"]) - >>> print(hamiltonian) - 0.25 * (Z(0) @ Z(1)) + 0.25 * Z(0) + 0.25 * Z(1) + 0.25 * (Z(1) @ Z(2)) + 0.25 * Z(1) + 0.25 * Z(2) - - >>> import rustworkx as rx - >>> graph = rx.PyGraph() - >>> graph.add_nodes_from([0, 1, 2]) - >>> graph.add_edges_from([(0, 1,""), (1,2,"")]) - >>> hamiltonian = qaoa.edge_driver(graph, ["11", "10", "01"]) - >>> print(hamiltonian) - 0.25 * (Z(0) @ Z(1)) + 0.25 * Z(0) + 0.25 * Z(1) + 0.25 * (Z(1) @ Z(2)) + 0.25 * Z(1) + 0.25 * Z(2) - - In the above example, ``"11"``, ``"10"``, and ``"01"`` are assigned a lower - energy than ``"00"``. For example, a quick calculation of expectation values gives us: - - .. math:: \langle 000 | H | 000 \rangle \ = \ 1.5 - .. math:: \langle 100 | H | 100 \rangle \ = \ 0.5 - .. math:: \langle 110 | H | 110\rangle \ = \ -0.5 - - In the first example, both vertex pairs are not in ``reward``. In the second example, one pair is in ``reward`` and - the other is not. Finally, in the third example, both pairs are in ``reward``. - - .. details:: - :title: Usage Details - - The goal of many combinatorial problems that can be solved with QAOA is to - find a `Graph colouring `__ of some supplied - graph :math:`G`, that minimizes some cost function. With QAOA, it is natural to consider the class - of graph colouring problems that only admit two colours, as we can easily encode these two colours - using the :math:`|1\rangle` and :math:`|0\rangle` states of qubits. Therefore, given - some graph :math:`G`, each edge of the graph can be described by a pair of qubits, :math:`|00\rangle`, - :math:`|01\rangle`, :math:`|10\rangle`, or :math:`|11\rangle`, corresponding to the colourings of its endpoints. - - When constructing QAOA cost functions, one must "penalize" certain states of the graph, and "reward" - others, by assigning higher and lower energies to these respective configurations. Given a set of vertex-colour - pairs (which each describe a possible state of a graph edge), the ``edge_driver()`` - function outputs a Hamiltonian that rewards the pairs in the set, and penalizes the others. - - For example, given the reward set: :math:`\{|00\rangle, \ |01\rangle, \ |10\rangle\}` and the graph :math:`G`, - the ``edge_driver()`` function will output the following Hamiltonian: - - .. math:: H \ = \ \frac{1}{4} \displaystyle\sum_{(i, j) \in E(G)} \big( Z_{i} Z_{j} \ - \ Z_{i} \ - \ Z_{j} \big) - - where :math:`E(G)` is the set of edges of :math:`G`, and :math:`Z_i` is the Pauli-Z operator acting on the - :math:`i`-th wire. As can be checked, this Hamiltonian assigns an energy of :math:`-1/4` to the states - :math:`|00\rangle`, :math:`|01\rangle` and :math:`|10\rangle`, and an energy of :math:`3/4` to the state - :math:`|11\rangle`. - - .. Note:: - - ``reward`` must always contain both :math:`|01\rangle` and :math:`|10\rangle`, or neither of the two. - Within an undirected graph, there is no notion of "order" - of edge endpoints, so these two states are effectively the same. Therefore, there is no well-defined way to - penalize one and reward the other. - - .. Note:: - - The absolute difference in energy between colourings in ``reward`` and colourings in its - complement is always :math:`1`. - - """ - - allowed = ["00", "01", "10", "11"] - - if not all(e in allowed for e in reward): - raise ValueError("Encountered invalid entry in 'reward', expected 2-bit bitstrings.") - - if "01" in reward and "10" not in reward or "10" in reward and "01" not in reward: - raise ValueError( - "'reward' cannot contain either '10' or '01', must contain neither or both." - ) - - _validate_graph(graph) - - coeffs = [] - ops = [] - - is_rx = isinstance(graph, rx.PyGraph) - graph_nodes = graph.nodes() - graph_edges = sorted(graph.edge_list()) if is_rx else graph.edges - - # In RX each node is assigned to an integer index starting from 0; - # thus, we use the following lambda function to get node-values. - def get_nvalue(i): - return graph_nodes[i] if is_rx else i - - if len(reward) == 0 or len(reward) == 4: - coeffs = [1 for _ in graph_nodes] - ops = [Identity(v) for v in graph_nodes] - - else: - reward = list(set(reward) - {"01"}) - sign = -1 - - if len(reward) == 2: - reward = list({"00", "10", "11"} - set(reward)) - sign = 1 - - reward = reward[0] - - if reward == "00": - for e in graph_edges: - coeffs.extend([0.25 * sign, 0.25 * sign, 0.25 * sign]) - ops.extend( - [ - Z(get_nvalue(e[0])) @ Z(get_nvalue(e[1])), - Z(get_nvalue(e[0])), - Z(get_nvalue(e[1])), - ] - ) - - if reward == "10": - for e in graph_edges: - coeffs.append(-0.5 * sign) - ops.append(Z(get_nvalue(e[0])) @ Z(get_nvalue(e[1]))) - - if reward == "11": - for e in graph_edges: - coeffs.extend([0.25 * sign, -0.25 * sign, -0.25 * sign]) - ops.extend( - [ - Z(get_nvalue(e[0])) @ Z(get_nvalue(e[1])), - Z(get_nvalue(e[0])), - Z(get_nvalue(e[1])), - ] - ) - - return LinearCombination(coeffs, ops) - - -####################### -# Optimization problems - - -def maxcut(graph: nx_Graph | rx.PyGraph): - r"""Returns the QAOA cost Hamiltonian and the recommended mixer corresponding to the - MaxCut problem, for a given graph. - - The goal of the MaxCut problem for a particular graph is to find a partition of nodes into two sets, - such that the number of edges in the graph with endpoints in different sets is maximized. Formally, - we wish to find the `cut of the graph `__ such - that the number of edges crossing the cut is maximized. - - The MaxCut cost Hamiltonian is defined as: - - .. math:: H_C \ = \ \frac{1}{2} \displaystyle\sum_{(i, j) \in E(G)} \big( Z_i Z_j \ - \ \mathbb{I} \big), - - where :math:`G` is a graph, :math:`\mathbb{I}` is the identity, and :math:`Z_i` and :math:`Z_j` are - the Pauli-Z operators on the :math:`i`-th and :math:`j`-th wire respectively. - - The mixer Hamiltonian returned from :func:`~qaoa.maxcut` is :func:`~qaoa.x_mixer` applied to all wires. - - .. note:: - - **Recommended initialization circuit:** - Even superposition over all basis states - - Args: - graph (nx.Graph or rx.PyGraph): a graph defining the pairs of wires on which each term of the Hamiltonian acts - - Returns: - (.Hamiltonian, .Hamiltonian): The cost and mixer Hamiltonians - - **Example** - - >>> import networkx as nx - >>> graph = nx.Graph([(0, 1), (1, 2)]) - >>> cost_h, mixer_h = qp.qaoa.maxcut(graph) - >>> print(cost_h) - 0.5 * (Z(0) @ Z(1)) + 0.5 * (Z(1) @ Z(2)) + -0.5 * (I(0) @ I(1)) + -0.5 * (I(1) @ I(2)) - >>> print(mixer_h) - 1 * X(0) + 1 * X(1) + 1 * X(2) - - >>> import rustworkx as rx - >>> graph = rx.PyGraph() - >>> graph.add_nodes_from([0, 1, 2]) - >>> graph.add_edges_from([(0, 1,""), (1,2,"")]) - >>> cost_h, mixer_h = qp.qaoa.maxcut(graph) - >>> print(cost_h) - 0.5 * (Z(0) @ Z(1)) + 0.5 * (Z(1) @ Z(2)) + -0.5 * (I(0) @ I(1)) + -0.5 * (I(1) @ I(2)) - >>> print(mixer_h) - 1 * X(0) + 1 * X(1) + 1 * X(2) - """ - _validate_graph(graph) - - is_rx = isinstance(graph, rx.PyGraph) - graph_nodes = graph.nodes() - graph_edges = sorted(graph.edge_list()) if is_rx else graph.edges - - # In RX each node is assigned to an integer index starting from 0; - # thus, we use the following lambda function to get node-values. - def get_nvalue(i): - return graph_nodes[i] if is_rx else i - - identity_h = LinearCombination( - [-0.5 for e in graph_edges], - [Identity(get_nvalue(e[0])) @ Identity(get_nvalue(e[1])) for e in graph_edges], - ) - H = edge_driver(graph, ["10", "01"]) + identity_h - # store the valuable information that all observables are in one commuting group - H.grouping_indices = [list(range(len(H.ops)))] - return (H, x_mixer(graph_nodes)) - - -def max_independent_set(graph: nx_Graph | rx.PyGraph, constrained: bool = True): - r"""For a given graph, returns the QAOA cost Hamiltonian and the recommended mixer corresponding to the Maximum Independent Set problem. - - Given some graph :math:`G`, an independent set is a set of vertices such that no pair of vertices in the set - share a common edge. The Maximum Independent Set problem, is the problem of finding the largest such set. - - Args: - graph (nx.Graph or rx.PyGraph): a graph whose edges define the pairs of vertices on which each term of the Hamiltonian acts - constrained (bool): specifies the variant of QAOA that is performed (constrained or unconstrained) - - Returns: - (.Hamiltonian, .Hamiltonian): The cost and mixer Hamiltonians - - .. details:: - :title: Usage Details - - There are two variations of QAOA for this problem, constrained and unconstrained: - - **Constrained** - - .. note:: - - This method of constrained QAOA was introduced by - `Hadfield, Wang, Gorman, Rieffel, Venturelli, and Biswas (2019) `__. - - The Maximum Independent Set cost Hamiltonian for constrained QAOA is defined as: - - .. math:: H_C \ = \ \displaystyle\sum_{v \in V(G)} Z_{v}, - - where :math:`V(G)` is the set of vertices of the input graph, and :math:`Z_i` is the Pauli-Z - operator applied to the :math:`i`-th vertex. - - The returned mixer Hamiltonian is :func:`~qaoa.bit_flip_mixer` applied to :math:`G`. - - .. note:: - - **Recommended initialization circuit:** - Each wire in the :math:`|0\rangle` state. - - **Unconstrained** - - The Maximum Independent Set cost Hamiltonian for unconstrained QAOA is defined as: - - .. math:: H_C \ = \ 3 \sum_{(i, j) \in E(G)} (Z_i Z_j \ - \ Z_i \ - \ Z_j) \ + \ - \displaystyle\sum_{i \in V(G)} Z_i - - where :math:`E(G)` is the set of edges of :math:`G`, :math:`V(G)` is the set of vertices, - and :math:`Z_i` is the Pauli-Z operator acting on the :math:`i`-th vertex. - - The returned mixer Hamiltonian is :func:`~qaoa.x_mixer` applied to all wires. - - .. note:: - - **Recommended initialization circuit:** - Even superposition over all basis states. - - """ - _validate_graph(graph) - - graph_nodes = graph.nodes() - - if constrained: - cost_h = bit_driver(graph_nodes, 1) - cost_h.grouping_indices = [list(range(len(cost_h.ops)))] - return (cost_h, bit_flip_mixer(graph, 0)) - - cost_h = 3 * edge_driver(graph, ["10", "01", "00"]) + bit_driver(graph_nodes, 1) - mixer_h = x_mixer(graph_nodes) - - # store the valuable information that all observables are in one commuting group - cost_h.grouping_indices = [list(range(len(cost_h.ops)))] - - return (cost_h, mixer_h) - - -def min_vertex_cover(graph: nx_Graph | rx.PyGraph, constrained: bool = True): - r"""Returns the QAOA cost Hamiltonian and the recommended mixer corresponding to the Minimum Vertex Cover problem, - for a given graph. - - To solve the Minimum Vertex Cover problem, we attempt to find the smallest - `vertex cover `__ of a graph --- a collection of vertices such that - every edge in the graph has one of the vertices as an endpoint. - - Args: - graph (nx.Graph or rx.PyGraph): a graph whose edges define the pairs of vertices on which each term of the Hamiltonian acts - constrained (bool): specifies the variant of QAOA that is performed (constrained or unconstrained) - - Returns: - (.Hamiltonian, .Hamiltonian): The cost and mixer Hamiltonians - - .. details:: - :title: Usage Details - - There are two variations of QAOA for this problem, constrained and unconstrained: - - **Constrained** - - .. note:: - - This method of constrained QAOA was introduced by Hadfield, Wang, Gorman, Rieffel, Venturelli, and Biswas - in arXiv:1709.03489. - - The Minimum Vertex Cover cost Hamiltonian for constrained QAOA is defined as: - - .. math:: H_C \ = \ - \displaystyle\sum_{v \in V(G)} Z_{v}, - - where :math:`V(G)` is the set of vertices of the input graph, and :math:`Z_i` is the Pauli-Z operator - applied to the :math:`i`-th vertex. - - The returned mixer Hamiltonian is :func:`~qaoa.bit_flip_mixer` applied to :math:`G`. - - .. note:: - - **Recommended initialization circuit:** - Each wire in the :math:`|1\rangle` state. - - **Unconstrained** - - The Minimum Vertex Cover cost Hamiltonian for unconstrained QAOA is defined as: - - .. math:: H_C \ = \ 3 \sum_{(i, j) \in E(G)} (Z_i Z_j \ + \ Z_i \ + \ Z_j) \ - \ - \displaystyle\sum_{i \in V(G)} Z_i - - where :math:`E(G)` is the set of edges of :math:`G`, :math:`V(G)` is the set of vertices, - and :math:`Z_i` is the Pauli-Z operator acting on the :math:`i`-th vertex. - - The returned mixer Hamiltonian is :func:`~qaoa.x_mixer` applied to all wires. - - .. note:: - - **Recommended initialization circuit:** - Even superposition over all basis states. - - """ - _validate_graph(graph) - graph_nodes = graph.nodes() - - if constrained: - cost_h = bit_driver(graph_nodes, 0) - cost_h.grouping_indices = [list(range(len(cost_h.ops)))] - return (cost_h, bit_flip_mixer(graph, 1)) - - cost_h = 3 * edge_driver(graph, ["11", "10", "01"]) + bit_driver(graph_nodes, 0) - mixer_h = x_mixer(graph_nodes) - - # store the valuable information that all observables are in one commuting group - cost_h.grouping_indices = [list(range(len(cost_h.ops)))] - - return (cost_h, mixer_h) - - -def max_clique(graph: nx_Graph | rx.PyGraph, constrained: bool = True): - r"""Returns the QAOA cost Hamiltonian and the recommended mixer corresponding to the Maximum Clique problem, - for a given graph. - - The goal of Maximum Clique is to find the largest `clique `__ of a - graph --- the largest subgraph such that all vertices are connected by an edge. - - Args: - graph (nx.Graph or rx.PyGraph): a graph whose edges define the pairs of vertices on which each term of the Hamiltonian acts - constrained (bool): specifies the variant of QAOA that is performed (constrained or unconstrained) - - Returns: - (.Hamiltonian, .Hamiltonian): The cost and mixer Hamiltonians - - .. details:: - :title: Usage Details - - There are two variations of QAOA for this problem, constrained and unconstrained: - - **Constrained** - - .. note:: - - This method of constrained QAOA was introduced by Hadfield, Wang, Gorman, Rieffel, Venturelli, and Biswas - in arXiv:1709.03489. - - The Maximum Clique cost Hamiltonian for constrained QAOA is defined as: - - .. math:: H_C \ = \ \displaystyle\sum_{v \in V(G)} Z_{v}, - - where :math:`V(G)` is the set of vertices of the input graph, and :math:`Z_i` is the Pauli-Z operator - applied to the :math:`i`-th - vertex. - - The returned mixer Hamiltonian is :func:`~qaoa.bit_flip_mixer` applied to :math:`\bar{G}`, - the complement of the graph. - - .. note:: - - **Recommended initialization circuit:** - Each wire in the :math:`|0\rangle` state. - - **Unconstrained** - - The Maximum Clique cost Hamiltonian for unconstrained QAOA is defined as: - - .. math:: H_C \ = \ 3 \sum_{(i, j) \in E(\bar{G})} - (Z_i Z_j \ - \ Z_i \ - \ Z_j) \ + \ \displaystyle\sum_{i \in V(G)} Z_i - - where :math:`V(G)` is the set of vertices of the input graph :math:`G`, :math:`E(\bar{G})` is the set of - edges of the complement of :math:`G`, and :math:`Z_i` is the Pauli-Z operator applied to the - :math:`i`-th vertex. - - The returned mixer Hamiltonian is :func:`~qaoa.x_mixer` applied to all wires. - - .. note:: - - **Recommended initialization circuit:** - Even superposition over all basis states. - - """ - _validate_graph(graph) - - import networkx as nx # pylint: disable=import-outside-toplevel - - graph_nodes = graph.nodes() - graph_complement = ( - rx.complement(graph) if isinstance(graph, rx.PyGraph) else nx.complement(graph) - ) - - if constrained: - cost_h = bit_driver(graph_nodes, 1) - cost_h.grouping_indices = [list(range(len(cost_h.ops)))] - return (cost_h, bit_flip_mixer(graph_complement, 0)) - - cost_h = 3 * edge_driver(graph_complement, ["10", "01", "00"]) + bit_driver(graph_nodes, 1) - mixer_h = x_mixer(graph_nodes) - - # store the valuable information that all observables are in one commuting group - cost_h.grouping_indices = [list(range(len(cost_h.ops)))] - - return (cost_h, mixer_h) - - -def max_weight_cycle(graph: nx_Graph | rx.PyGraph | rx.PyDiGraph, constrained: bool = True): - r"""Returns the QAOA cost Hamiltonian and the recommended mixer corresponding to the - maximum-weighted cycle problem, for a given graph. - - The maximum-weighted cycle problem is defined in the following way (see - `here `__ for more details). - The product of weights of a subset of edges in a graph is given by - - .. math:: P = \prod_{(i, j) \in E} [(c_{ij} - 1)x_{ij} + 1] - - where :math:`E` are the edges of the graph, :math:`x_{ij}` is a binary number that selects - whether to include the edge :math:`(i, j)` and :math:`c_{ij}` is the corresponding edge weight. - Our objective is to maximize :math:`P`, subject to selecting the :math:`x_{ij}` so that - our subset of edges composes a `cycle `__. - - Args: - graph (nx.Graph or rx.PyGraph or rx.PyDiGraph): the directed graph on which the Hamiltonians are defined - constrained (bool): specifies the variant of QAOA that is performed (constrained or unconstrained) - - Returns: - (.Hamiltonian, .Hamiltonian, dict): The cost and mixer Hamiltonians, as well as a dictionary - mapping from wires to the graph's edges - - .. details:: - :title: Usage Details - - There are two variations of QAOA for this problem, constrained and unconstrained: - - **Constrained** - - .. note:: - - This method of constrained QAOA was introduced by Hadfield, Wang, Gorman, Rieffel, - Venturelli, and Biswas in `arXiv:1709.03489 `__. - - The maximum weighted cycle cost Hamiltonian for unconstrained QAOA is - - .. math:: H_C = H_{\rm loss}. - - Here, :math:`H_{\rm loss}` is a loss Hamiltonian: - - .. math:: H_{\rm loss} = \sum_{(i, j) \in E} Z_{ij}\log c_{ij} - - where :math:`E` are the edges of the graph and :math:`Z_{ij}` is a qubit Pauli-Z matrix - acting upon the wire specified by the edge :math:`(i, j)` (see :func:`~.loss_hamiltonian` - for more details). - - The returned mixer Hamiltonian is :func:`~.cycle_mixer` given by - - .. math:: H_M = \frac{1}{4}\sum_{(i, j)\in E} - \left(\sum_{k \in V, k\neq i, k\neq j, (i, k) \in E, (k, j) \in E} - \left[X_{ij}X_{ik}X_{kj} +Y_{ij}Y_{ik}X_{kj} + Y_{ij}X_{ik}Y_{kj} - X_{ij}Y_{ik}Y_{kj}\right] - \right). - - This mixer provides transitions between collections of cycles, i.e., any subset of edges - in :math:`E` such that all the graph's nodes :math:`V` have zero net flow - (see the :func:`~.net_flow_constraint` function). - - .. note:: - - **Recommended initialization circuit:** - Your circuit must prepare a state that corresponds to a cycle (or a superposition - of cycles). Follow the example code below to see how this is done. - - **Unconstrained** - - The maximum weighted cycle cost Hamiltonian for constrained QAOA is defined as: - - .. math:: H_C \ = H_{\rm loss} + 3 H_{\rm netflow} + 3 H_{\rm outflow}. - - The netflow constraint Hamiltonian :func:`~.net_flow_constraint` is given by - - .. math:: H_{\rm netflow} = \sum_{i \in V} \left((d_{i}^{\rm out} - d_{i}^{\rm in})\mathbb{I} - - \sum_{j, (i, j) \in E} Z_{ij} + \sum_{j, (j, i) \in E} Z_{ji} \right)^{2}, - - where :math:`d_{i}^{\rm out}` and :math:`d_{i}^{\rm in}` are - the outdegree and indegree, respectively, of node :math:`i`. It is minimized whenever a - subset of edges in :math:`E` results in zero net flow from each node in :math:`V`. - - The outflow constraint Hamiltonian :func:`~.out_flow_constraint` is given by - - .. math:: H_{\rm outflow} = \sum_{i\in V}\left(d_{i}^{out}(d_{i}^{out} - 2)\mathbb{I} - - 2(d_{i}^{out}-1)\sum_{j,(i,j)\in E}\hat{Z}_{ij} + - \left( \sum_{j,(i,j)\in E}\hat{Z}_{ij} \right)^{2}\right). - - It is minimized whenever a subset of edges in :math:`E` results in an outflow of at most one - from each node in :math:`V`. - - The returned mixer Hamiltonian is :func:`~.x_mixer` applied to all wires. - - .. note:: - - **Recommended initialization circuit:** - Even superposition over all basis states. - - **Example** - - First set up a simple graph: - - .. code-block:: python - - import pennylane as qp - import numpy as np - import networkx as nx - - a = np.random.random((4, 4)) - np.fill_diagonal(a, 0) - g = nx.DiGraph(a) - - The cost and mixer Hamiltonian as well as the mapping from wires to edges can be loaded - using: - - >>> cost, mixer, mapping = qp.qaoa.max_weight_cycle(g, constrained=True) - - Since we are using ``constrained=True``, we must ensure that the input state to the QAOA - algorithm corresponds to a cycle. Consider the mapping: - - >>> mapping - {0: (0, 1), - 1: (0, 2), - 2: (0, 3), - 3: (1, 0), - 4: (1, 2), - 5: (1, 3), - 6: (2, 0), - 7: (2, 1), - 8: (2, 3), - 9: (3, 0), - 10: (3, 1), - 11: (3, 2)} - - A simple cycle is given by the edges ``(0, 1)`` and ``(1, 0)`` and corresponding wires - ``0`` and ``3``. Hence, the state :math:`|100100000000\rangle` corresponds to a cycle and - can be prepared using :class:`~.BasisState` or simple :class:`~.PauliX` rotations on the - ``0`` and ``3`` wires. - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if not isinstance(graph, (nx.Graph, rx.PyGraph, rx.PyDiGraph)): - raise ValueError( - f"Input graph must be a nx.Graph, rx.PyGraph, or rx.PyDiGraph, got {type(graph).__name__}" - ) - mapping = wires_to_edges(graph) - - if constrained: - cost_h = loss_hamiltonian(graph) - cost_h.grouping_indices = [list(range(len(cost_h.ops)))] - return (cost_h, cycle_mixer(graph), mapping) - - cost_h = loss_hamiltonian(graph) + 3 * (net_flow_constraint(graph) + out_flow_constraint(graph)) - mixer_h = x_mixer(mapping.keys()) - - return (cost_h, mixer_h, mapping) diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py deleted file mode 100644 index efa62e336d7..00000000000 --- a/pennylane/qaoa/cycle.py +++ /dev/null @@ -1,714 +0,0 @@ -# Copyright 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. -r""" -Functionality for finding the maximum weighted cycle of directed graphs. -""" - -# pylint: disable=unnecessary-comprehension -import itertools -from collections.abc import Iterable -from typing import TYPE_CHECKING - -import numpy as np -import rustworkx as rx - -from pennylane.core.operator import Operator -from pennylane.ops import Identity, LinearCombination, X, Y, Z - -if TYPE_CHECKING: - from networkx import DiGraph as nx_DiGraph - from networkx import Graph as nx_Graph -else: - nx_DiGraph = None - nx_Graph = None - - -def edges_to_wires(graph: nx_Graph | rx.PyGraph | rx.PyDiGraph) -> dict[tuple, int]: - r"""Maps the edges of a graph to corresponding wires. - - **Example** - - >>> g = nx.complete_graph(4).to_directed() - >>> edges_to_wires(g) - {(0, 1): 0, - (0, 2): 1, - (0, 3): 2, - (1, 0): 3, - (1, 2): 4, - (1, 3): 5, - (2, 0): 6, - (2, 1): 7, - (2, 3): 8, - (3, 0): 9, - (3, 1): 10, - (3, 2): 11} - - >>> g = rx.generators.directed_mesh_graph(4, [0,1,2,3]) - >>> edges_to_wires(g) - {(0, 1): 0, - (0, 2): 1, - (0, 3): 2, - (1, 0): 3, - (1, 2): 4, - (1, 3): 5, - (2, 0): 6, - (2, 1): 7, - (2, 3): 8, - (3, 0): 9, - (3, 1): 10, - (3, 2): 11} - - Args: - graph (nx.Graph or rx.PyGraph or rx.PyDiGraph): the graph specifying possible edges - - Returns: - Dict[Tuple, int]: a mapping from graph edges to wires - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if isinstance(graph, nx.Graph): - return {edge: i for i, edge in enumerate(graph.edges)} - if isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): - gnodes = graph.nodes() - return { - (gnodes.index(e[0]), gnodes.index(e[1])): i - for i, e in enumerate(sorted(graph.edge_list())) - } - raise ValueError( - f"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph, got {type(graph).__name__}" - ) - - -def wires_to_edges(graph: nx_Graph | rx.PyGraph | rx.PyDiGraph) -> dict[int, tuple]: - r"""Maps the wires of a register of qubits to corresponding edges. - - **Example** - - >>> g = nx.complete_graph(4).to_directed() - >>> wires_to_edges(g) - {0: (0, 1), - 1: (0, 2), - 2: (0, 3), - 3: (1, 0), - 4: (1, 2), - 5: (1, 3), - 6: (2, 0), - 7: (2, 1), - 8: (2, 3), - 9: (3, 0), - 10: (3, 1), - 11: (3, 2)} - - >>> g = rx.generators.directed_mesh_graph(4, [0,1,2,3]) - >>> wires_to_edges(g) - {0: (0, 1), - 1: (0, 2), - 2: (0, 3), - 3: (1, 0), - 4: (1, 2), - 5: (1, 3), - 6: (2, 0), - 7: (2, 1), - 8: (2, 3), - 9: (3, 0), - 10: (3, 1), - 11: (3, 2)} - - Args: - graph (nx.Graph or rx.PyGraph or rx.PyDiGraph): the graph specifying possible edges - - Returns: - Dict[Tuple, int]: a mapping from wires to graph edges - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if isinstance(graph, nx.Graph): - return {i: edge for i, edge in enumerate(graph.edges)} - if isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): - gnodes = graph.nodes() - return { - i: (gnodes.index(e[0]), gnodes.index(e[1])) - for i, e in enumerate(sorted(graph.edge_list())) - } - raise ValueError( - f"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph, got {type(graph).__name__}" - ) - - -def cycle_mixer(graph: nx_DiGraph | rx.PyDiGraph) -> Operator: - r"""Calculates the cycle-mixer Hamiltonian. - - Following methods outlined `here `__, the - cycle-mixer Hamiltonian preserves the set of valid cycles: - - .. math:: - \frac{1}{4}\sum_{(i, j)\in E} - \left(\sum_{k \in V, k\neq i, k\neq j, (i, k) \in E, (k, j) \in E} - \left[X_{ij}X_{ik}X_{kj} +Y_{ij}Y_{ik}X_{kj} + Y_{ij}X_{ik}Y_{kj} - X_{ij}Y_{ik}Y_{kj}\right] - \right) - - where :math:`E` are the edges of the directed graph. A valid cycle is defined as a subset of - edges in :math:`E` such that all of the graph's nodes :math:`V` have zero net flow (see the - :func:`~.net_flow_constraint` function). - - **Example** - - >>> import networkx as nx - >>> g = nx.complete_graph(3).to_directed() - >>> h_m = cycle_mixer(g) - >>> print(h_m) - (-0.25) [X0 Y1 Y5] - + (-0.25) [X1 Y0 Y3] - + (-0.25) [X2 Y3 Y4] - + (-0.25) [X3 Y2 Y1] - + (-0.25) [X4 Y5 Y2] - + (-0.25) [X5 Y4 Y0] - + (0.25) [X0 X1 X5] - + (0.25) [Y0 Y1 X5] - + (0.25) [Y0 X1 Y5] - + (0.25) [X1 X0 X3] - + (0.25) [Y1 Y0 X3] - + (0.25) [Y1 X0 Y3] - + (0.25) [X2 X3 X4] - + (0.25) [Y2 Y3 X4] - + (0.25) [Y2 X3 Y4] - + (0.25) [X3 X2 X1] - + (0.25) [Y3 Y2 X1] - + (0.25) [Y3 X2 Y1] - + (0.25) [X4 X5 X2] - + (0.25) [Y4 Y5 X2] - + (0.25) [Y4 X5 Y2] - + (0.25) [X5 X4 X0] - + (0.25) [Y5 Y4 X0] - + (0.25) [Y5 X4 Y0] - - >>> import rustworkx as rx - >>> g = rx.generators.directed_mesh_graph(3, [0,1,2]) - >>> h_m = cycle_mixer(g) - >>> print(h_m) - (-0.25) [X0 Y1 Y5] - + (-0.25) [X1 Y0 Y3] - + (-0.25) [X2 Y3 Y4] - + (-0.25) [X3 Y2 Y1] - + (-0.25) [X4 Y5 Y2] - + (-0.25) [X5 Y4 Y0] - + (0.25) [X0 X1 X5] - + (0.25) [Y0 Y1 X5] - + (0.25) [Y0 X1 Y5] - + (0.25) [X1 X0 X3] - + (0.25) [Y1 Y0 X3] - + (0.25) [Y1 X0 Y3] - + (0.25) [X2 X3 X4] - + (0.25) [Y2 Y3 X4] - + (0.25) [Y2 X3 Y4] - + (0.25) [X3 X2 X1] - + (0.25) [Y3 Y2 X1] - + (0.25) [Y3 X2 Y1] - + (0.25) [X4 X5 X2] - + (0.25) [Y4 Y5 X2] - + (0.25) [Y4 X5 Y2] - + (0.25) [X5 X4 X0] - + (0.25) [Y5 Y4 X0] - + (0.25) [Y5 X4 Y0] - - Args: - graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges - - Returns: - qp.Hamiltonian: the cycle-mixer Hamiltonian - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError( - f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" - ) - - hamiltonian = LinearCombination([], []) - graph_edges = sorted(graph.edge_list()) if isinstance(graph, rx.PyDiGraph) else graph.edges - - for edge in graph_edges: - hamiltonian += _partial_cycle_mixer(graph, edge) - - return hamiltonian - - -def _partial_cycle_mixer(graph: nx_DiGraph | rx.PyDiGraph, edge: tuple) -> Operator: - r"""Calculates the partial cycle-mixer Hamiltonian for a specific edge. - - For an edge :math:`(i, j)`, this function returns: - - .. math:: - - \sum_{k \in V, k\neq i, k\neq j, (i, k) \in E, (k, j) \in E}\left[ - X_{ij}X_{ik}X_{kj} + Y_{ij}Y_{ik}X_{kj} + Y_{ij}X_{ik}Y_{kj} - X_{ij}Y_{ik}Y_{kj}\right] - - Args: - graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges - edge (tuple): a fixed edge - - Returns: - qp.Hamiltonian: the partial cycle-mixer Hamiltonian - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError( - f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" - ) - - coeffs = [] - ops = [] - - is_rx = isinstance(graph, rx.PyDiGraph) - edges_to_qubits = edges_to_wires(graph) - graph_nodes = graph.node_indexes() if is_rx else graph.nodes - graph_edges = sorted(graph.edge_list()) if is_rx else graph.edges - - # In RX each node is assigned to an integer index starting from 0; - # thus, we use the following function to get node-values. - def get_nvalues(T): - return (graph.nodes().index(T[0]), graph.nodes().index(T[1])) if is_rx else T - - for node in graph_nodes: - out_edge = (edge[0], node) - in_edge = (node, edge[1]) - if node not in edge and out_edge in graph_edges and in_edge in graph_edges: - wire = edges_to_qubits[get_nvalues(edge)] - out_wire = edges_to_qubits[get_nvalues(out_edge)] - in_wire = edges_to_qubits[get_nvalues(in_edge)] - - t = X(wire) @ X(out_wire) @ X(in_wire) - ops.append(t) - - t = Y(wire) @ Y(out_wire) @ X(in_wire) - ops.append(t) - - t = Y(wire) @ X(out_wire) @ Y(in_wire) - ops.append(t) - - t = X(wire) @ Y(out_wire) @ Y(in_wire) - ops.append(t) - - coeffs.extend([0.25, 0.25, 0.25, -0.25]) - - return LinearCombination(coeffs, ops) - - -def loss_hamiltonian(graph: nx_Graph | rx.PyGraph | rx.PyDiGraph) -> Operator: - r"""Calculates the loss Hamiltonian for the maximum-weighted cycle problem. - - We consider the problem of selecting a cycle from a graph that has the greatest product of edge - weights, as outlined `here `__. The product of weights - of a subset of edges in a graph is given by - - .. math:: P = \prod_{(i, j) \in E} [(c_{ij} - 1)x_{ij} + 1] - - where :math:`E` are the edges of the graph, :math:`x_{ij}` is a binary number that selects - whether to include the edge :math:`(i, j)` and :math:`c_{ij}` is the corresponding edge weight. - Our objective is to maximize :math:`P`, subject to selecting the :math:`x_{ij}` so that - our subset of edges composes a cycle. - - The product of edge weights is maximized by equivalently considering - - .. math:: \sum_{(i, j) \in E} x_{ij}\log c_{ij}, - - assuming :math:`c_{ij} > 0`. - - This can be restated as a minimization of the expectation value of the following qubit - Hamiltonian: - - .. math:: - - H = \sum_{(i, j) \in E} Z_{ij}\log c_{ij}. - - where :math:`Z_{ij}` is a qubit Pauli-Z matrix acting upon the wire specified by the edge - :math:`(i, j)`. Mapping from edges to wires can be achieved using :func:`~.edges_to_wires`. - - .. note:: - The expectation value of the returned Hamiltonian :math:`H` is not equal to :math:`P`, but - minimizing the expectation value of :math:`H` is equivalent to maximizing :math:`P`. - - Also note that the returned Hamiltonian does not impose that the selected set of edges is - a cycle. This constraint can be enforced using a penalty term or by selecting a QAOA - mixer Hamiltonian that only transitions between states that correspond to cycles. - - **Example** - - >>> import networkx as nx - >>> g = nx.complete_graph(3).to_directed() - >>> edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edges)} - >>> for k, v in edge_weight_data.items(): - g[k[0]][k[1]]["weight"] = v - >>> h = loss_hamiltonian(g) - >>> h - ( - -0.6931471805599453 * Z(0) - + 0.0 * Z(1) - + 0.4054651081081644 * Z(2) - + 0.6931471805599453 * Z(3) - + 0.9162907318741551 * Z(4) - + 1.0986122886681098 * Z(5) - ) - - >>> import rustworkx as rx - >>> g = rx.generators.directed_mesh_graph(3, [0, 1, 2]) - >>> edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(sorted(g.edge_list()))} - >>> for k, v in edge_weight_data.items(): - g.update_edge(k[0], k[1], {"weight": v}) - >>> h = loss_hamiltonian(g) - >>> print(h) - ( - -0.6931471805599453 * Z(0) - + 0.0 * Z(1) - + 0.4054651081081644 * Z(2) - + 0.6931471805599453 * Z(3) - + 0.9162907318741551 * Z(4) - + 1.0986122886681098 * Z(5) - ) - - Args: - graph (nx.Graph or rx.PyGraph or rx.PyDiGraph): the graph specifying possible edges - - Returns: - qp.Hamiltonian: the loss Hamiltonian - - Raises: - ValueError: if the graph contains self-loops - KeyError: if one or more edges do not contain weight data - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if not isinstance(graph, (nx.Graph, rx.PyGraph, rx.PyDiGraph)): - raise ValueError( - f"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph, got {type(graph).__name__}" - ) - - edges_to_qubits = edges_to_wires(graph) - - coeffs = [] - ops = [] - - is_rx = isinstance(graph, (rx.PyGraph, rx.PyDiGraph)) - edges_data = sorted(graph.weighted_edge_list()) if is_rx else graph.edges(data=True) - - # In RX each node is assigned to an integer index starting from 0; - # thus, we use the following function to get node-values. - def get_nvalues(T): - return (graph.nodes().index(T[0]), graph.nodes().index(T[1])) if is_rx else T - - for edge_data in edges_data: - edge = edge_data[:2] - - if edge[0] == edge[1]: - raise ValueError("Graph contains self-loops") - - try: - weight = edge_data[2]["weight"] - except KeyError as e: - raise KeyError(f"Edge {edge} does not contain weight data") from e - except TypeError as e: - raise TypeError(f"Edge {edge} does not contain weight data") from e - - coeffs.append(np.log(weight)) - ops.append(Z(edges_to_qubits[get_nvalues(edge)])) - - H = LinearCombination(coeffs, ops) - # store the valuable information that all observables are in one commuting group - H.grouping_indices = [list(range(len(H.ops)))] - - return H - - -def _square_hamiltonian_terms( - coeffs: Iterable[float], ops: Iterable[Operator] -) -> tuple[list[float], list[Operator]]: - """Calculates the coefficients and observables that compose the squared Hamiltonian. - - Args: - coeffs (Iterable[float]): coeffients of the input Hamiltonian - ops (Iterable[qp.operation.Operator]): observables of the input Hamiltonian - - Returns: - Tuple[List[float], List[qp.operation.Operator]]: The list of coefficients and list of observables - of the squared Hamiltonian. - """ - combs = itertools.combinations(zip(coeffs, ops, strict=True), r=2) - - # Initialize with diagonal terms - squared_coeffs = [sum(c**2 for c in coeffs)] - squared_ops = [Identity(0)] - for (coeff1, op1), (coeff2, op2) in combs: - squared_coeffs.append(2 * coeff1 * coeff2) - - if isinstance(op1, Identity): - squared_ops.append(op2) - elif isinstance(op2, Identity): - squared_ops.append(op1) - elif op2.wires[0] < op1.wires[0]: - squared_ops.append(op2 @ op1) - else: - squared_ops.append(op1 @ op2) - - return squared_coeffs, squared_ops - - -def out_flow_constraint(graph: nx_DiGraph | rx.PyDiGraph) -> Operator: - r"""Calculates the `out flow constraint `__ - Hamiltonian for the maximum-weighted cycle problem. - - Given a subset of edges in a directed graph, the out-flow constraint imposes that at most one - edge can leave any given node, i.e., for all :math:`i`: - - .. math:: \sum_{j,(i,j)\in E}x_{ij} \leq 1, - - where :math:`E` are the edges of the graph and :math:`x_{ij}` is a binary number that selects - whether to include the edge :math:`(i, j)`. - - A set of edges satisfies the out-flow constraint whenever the following Hamiltonian is minimized: - - .. math:: - - \sum_{i\in V}\left(d_{i}^{out}(d_{i}^{out} - 2)\mathbb{I} - - 2(d_{i}^{out}-1)\sum_{j,(i,j)\in E}\hat{Z}_{ij} + - \left( \sum_{j,(i,j)\in E}\hat{Z}_{ij} \right)^{2}\right) - - - where :math:`V` are the graph vertices, :math:`d_{i}^{\rm out}` is the outdegree of node - :math:`i`, and :math:`Z_{ij}` is a qubit Pauli-Z matrix acting - upon the qubit specified by the pair :math:`(i, j)`. Mapping from edges to wires can be achieved - using :func:`~.edges_to_wires`. - - Args: - graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges - - Returns: - qp.Hamiltonian: the out flow constraint Hamiltonian - - Raises: - ValueError: if the input graph is not directed - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError( - f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" - ) - - if isinstance(graph, (nx.DiGraph, rx.PyDiGraph)) and not hasattr(graph, "out_edges"): - raise ValueError("Input graph must be directed") - - hamiltonian = LinearCombination([], []) - graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes - - for node in graph_nodes: - hamiltonian += _inner_out_flow_constraint_hamiltonian(graph, node) - - return hamiltonian - - -def net_flow_constraint(graph: nx_DiGraph | rx.PyDiGraph) -> Operator: - r"""Calculates the `net flow constraint `__ - Hamiltonian for the maximum-weighted cycle problem. - - Given a subset of edges in a directed graph, the net-flow constraint imposes that the number of - edges leaving any given node is equal to the number of edges entering the node, i.e., - - .. math:: \sum_{j, (i, j) \in E} x_{ij} = \sum_{j, (j, i) \in E} x_{ji}, - - for all nodes :math:`i`, where :math:`E` are the edges of the graph and :math:`x_{ij}` is a - binary number that selects whether to include the edge :math:`(i, j)`. - - A set of edges has zero net flow whenever the following Hamiltonian is minimized: - - .. math:: - - \sum_{i \in V} \left((d_{i}^{\rm out} - d_{i}^{\rm in})\mathbb{I} - - \sum_{j, (i, j) \in E} Z_{ij} + \sum_{j, (j, i) \in E} Z_{ji} \right)^{2}, - - where :math:`V` are the graph vertices, :math:`d_{i}^{\rm out}` and :math:`d_{i}^{\rm in}` are - the outdegree and indegree, respectively, of node :math:`i` and :math:`Z_{ij}` is a qubit - Pauli-Z matrix acting upon the wire specified by the pair :math:`(i, j)`. Mapping from edges to - wires can be achieved using :func:`~.edges_to_wires`. - - - Args: - graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges - - Returns: - qp.Hamiltonian: the net-flow constraint Hamiltonian - - Raises: - ValueError: if the input graph is not directed - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if isinstance(graph, (nx.DiGraph, rx.PyDiGraph)) and ( - not hasattr(graph, "in_edges") or not hasattr(graph, "out_edges") - ): - raise ValueError("Input graph must be directed") - - if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError( - f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" - ) - - hamiltonian = LinearCombination([], []) - graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes - - for node in graph_nodes: - hamiltonian += _inner_net_flow_constraint_hamiltonian(graph, node) - - return hamiltonian - - -def _inner_out_flow_constraint_hamiltonian(graph: nx_DiGraph | rx.PyDiGraph, node: int) -> Operator: - r"""Calculates the inner portion of the Hamiltonian in :func:`out_flow_constraint`. - For a given :math:`i`, this function returns: - - .. math:: - - d_{i}^{out}(d_{i}^{out} - 2)\mathbb{I} - - 2(d_{i}^{out}-1)\sum_{j,(i,j)\in E}\hat{Z}_{ij} + - ( \sum_{j,(i,j)\in E}\hat{Z}_{ij} )^{2} - - Args: - graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges - node: a fixed node - - Returns: - qp.Hamiltonian: The inner part of the out-flow constraint Hamiltonian. - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError( - f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" - ) - - coeffs = [] - ops = [] - - is_rx = isinstance(graph, rx.PyDiGraph) - - # In RX each node is assigned to an integer index starting from 0; - # thus, we use the following function to get node-values. - def get_nvalues(T): - return (graph.nodes().index(T[0]), graph.nodes().index(T[1])) if is_rx else T - - edges_to_qubits = edges_to_wires(graph) - out_edges = graph.out_edges(node) - d = len(out_edges) - - # To ensure the out_edges method in both RX and NX returns - # the list of edges in the same order, we sort results. - if is_rx: - out_edges = sorted(out_edges) - - for edge in out_edges: - if len(edge) > 2: - edge = tuple(edge[:2]) - wire = (edges_to_qubits[get_nvalues(edge)],) - coeffs.append(1) - ops.append(Z(wire)) - - coeffs, ops = _square_hamiltonian_terms(coeffs, ops) - - for edge in out_edges: - if len(edge) > 2: - edge = tuple(edge[:2]) - wire = (edges_to_qubits[get_nvalues(edge)],) - coeffs.append(-2 * (d - 1)) - ops.append(Z(wire)) - - coeffs.append(d * (d - 2)) - ops.append(Identity(0)) - - H = LinearCombination(coeffs, ops) - H.simplify() - # store the valuable information that all observables are in one commuting group - H.grouping_indices = [list(range(len(H.ops)))] - - return H - - -def _inner_net_flow_constraint_hamiltonian(graph: nx_DiGraph | rx.PyDiGraph, node: int) -> Operator: - r"""Calculates the squared inner portion of the Hamiltonian in :func:`net_flow_constraint`. - - - For a given :math:`i`, this function returns: - - .. math:: - - \left((d_{i}^{\rm out} - d_{i}^{\rm in})\mathbb{I} - - \sum_{j, (i, j) \in E} Z_{ij} + \sum_{j, (j, i) \in E} Z_{ji} \right)^{2}. - - Args: - graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges - node: a fixed node - - Returns: - qp.Hamiltonian: The inner part of the net-flow constraint Hamiltonian. - """ - import networkx as nx # pylint: disable=import-outside-toplevel - - if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError( - f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" - ) - - edges_to_qubits = edges_to_wires(graph) - - coeffs = [] - ops = [] - - is_rx = isinstance(graph, rx.PyDiGraph) - - out_edges = graph.out_edges(node) - in_edges = graph.in_edges(node) - - # To ensure out_edges and in_edges methods in both RX and NX return - # the lists of edges in the same order, we sort results. - if is_rx: - out_edges = sorted(out_edges) - in_edges = sorted(in_edges) - - # In RX each node is assigned to an integer index starting from 0; - # thus, we use the following function to get node-values. - def get_nvalues(T): - return (graph.nodes().index(T[0]), graph.nodes().index(T[1])) if is_rx else T - - coeffs.append(len(out_edges) - len(in_edges)) - ops.append(Identity(0)) - - for edge in out_edges: - if len(edge) > 2: - edge = tuple(edge[:2]) - wires = (edges_to_qubits[get_nvalues(edge)],) - coeffs.append(-1) - ops.append(Z(wires)) - - for edge in in_edges: - if len(edge) > 2: - edge = tuple(edge[:2]) - wires = (edges_to_qubits[get_nvalues(edge)],) - coeffs.append(1) - ops.append(Z(wires)) - - coeffs, ops = _square_hamiltonian_terms(coeffs, ops) - H = LinearCombination(coeffs, ops) - H = H.simplify() - # store the valuable information that all observables are in one commuting group - H.grouping_indices = [list(range(len(H.ops)))] - return H diff --git a/pennylane/qaoa/layers.py b/pennylane/qaoa/layers.py deleted file mode 100644 index 767bb45064c..00000000000 --- a/pennylane/qaoa/layers.py +++ /dev/null @@ -1,156 +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. -""" -Methods that define cost and mixer layers for use in QAOA workflows. -""" - -from pennylane.ops import Prod -from pennylane.templates import ApproxTimeEvolution - - -def _diagonal_terms(hamiltonian): - r"""Checks if all terms in a Hamiltonian are products of diagonal Pauli gates - (:class:`~.PauliZ` and :class:`~.Identity`). - - Args: - hamiltonian (.Hamiltonian): The Hamiltonian being checked - - Returns: - bool: ``True`` if all terms are products of diagonal Pauli gates, ``False`` otherwise - """ - - for op in hamiltonian.terms()[1]: - if isinstance(op, Prod): - obs = op.operands - else: - obs = [op] - for j in obs: - if j.name not in ("PauliZ", "Identity"): - return False - return True - - -def cost_layer(gamma, hamiltonian): - r"""Applies the QAOA cost layer corresponding to a cost Hamiltonian. - - For the cost Hamiltonian :math:`H_C`, this is defined as the following unitary: - - .. math:: U_C \ = \ e^{-i \gamma H_C} - - where :math:`\gamma` is a variational parameter. - - Args: - gamma (int or float): The variational parameter passed into the cost layer - hamiltonian (.Hamiltonian): The cost Hamiltonian - - Raises: - ValueError: if the terms of the supplied cost Hamiltonian are not exclusively products of diagonal Pauli gates - - .. details:: - :title: Usage Details - - We first define a cost Hamiltonian: - - .. code-block:: python3 - - from pennylane import qaoa - import pennylane as qp - - cost_h = qp.Hamiltonian([1, 1], [qp.Z(0), qp.Z(0) @ qp.Z(1)]) - - We can then pass it into ``qaoa.cost_layer``, within a quantum circuit: - - .. code-block:: python - - dev = qp.device('default.qubit', wires=2) - - @qp.qnode(dev) - def circuit(gamma): - - for i in range(2): - qp.Hadamard(wires=i) - - qaoa.cost_layer(gamma, cost_h) - - return [qp.expval(qp.Z(i)) for i in range(2)] - - which gives us a circuit of the form: - - >>> print(qp.draw(circuit)(0.5)) - 0: ──H─╭ApproxTimeEvolution(1.00,1.00,0.50)─┤ - 1: ──H─╰ApproxTimeEvolution(1.00,1.00,0.50)─┤ - >>> print(qp.draw(circuit, level="device")(0.5)) - 0: ──H──RZ(1.00)─╭RZZ(1.00)─┤ - 1: ──H───────────╰RZZ(1.00)─┤ - - """ - # NOTE: op is defined explicitly as validation inside ApproxTimeEvolution needs to be called before checking Hamiltonian - op = ApproxTimeEvolution(hamiltonian, gamma, 1) - if not _diagonal_terms(hamiltonian): - raise ValueError("hamiltonian must be written only in terms of PauliZ and Identity gates") - return op - - -def mixer_layer(alpha, hamiltonian): - r"""Applies the QAOA mixer layer corresponding to a mixer Hamiltonian. - - For a mixer Hamiltonian :math:`H_M`, this is defined as the following unitary: - - .. math:: U_M \ = \ e^{-i \alpha H_M} - - where :math:`\alpha` is a variational parameter. - - Args: - alpha (int or float): The variational parameter passed into the mixer layer - hamiltonian (.Hamiltonian): The mixer Hamiltonian - - .. details:: - :title: Usage Details - - We first define a mixer Hamiltonian: - - .. code-block:: python3 - - from pennylane import qaoa - import pennylane as qp - - mixer_h = qp.Hamiltonian([1, 1], [qp.X(0), qp.X(0) @ qp.X(1)]) - - We can then pass it into ``qaoa.mixer_layer``, within a quantum circuit: - - .. code-block:: python - - dev = qp.device('default.qubit', wires=2) - - @qp.qnode(dev) - def circuit(alpha): - - for i in range(2): - qp.Hadamard(wires=i) - - qaoa.mixer_layer(alpha, mixer_h) - - return [qp.expval(qp.Z(i)) for i in range(2)] - - which gives us a circuit of the form: - - >>> print(qp.draw(circuit)(0.5)) - 0: ──H─╭ApproxTimeEvolution(1.00,1.00,0.50)─┤ - 1: ──H─╰ApproxTimeEvolution(1.00,1.00,0.50)─┤ - >>> print(qp.draw(circuit, level="device")(0.5)) - 0: ──H──RX(1.00)─╭RXX(1.00)─┤ - 1: ──H───────────╰RXX(1.00)─┤ - - """ - return ApproxTimeEvolution(hamiltonian, alpha, 1) diff --git a/pennylane/qaoa/mixers.py b/pennylane/qaoa/mixers.py deleted file mode 100644 index 97d064590c2..00000000000 --- a/pennylane/qaoa/mixers.py +++ /dev/null @@ -1,250 +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. -r""" -Methods for constructing QAOA mixer Hamiltonians. -""" - -import functools -import itertools -from collections.abc import Iterable -from typing import TYPE_CHECKING - -import rustworkx as rx - -from pennylane.ops import Identity, LinearCombination, X, Y, Z, prod -from pennylane.wires import Wires - -if TYPE_CHECKING: - from networkx import Graph as nx_Graph -else: - nx_Graph = None - - -def _validate_graph(graph): - import networkx as nx # pylint: disable=import-outside-toplevel - - if not isinstance(graph, (nx.Graph, rx.PyGraph)): - raise ValueError( - f"Input graph must be a nx.Graph or rx.PyGraph, got {type(graph).__name__}" - ) - - -def x_mixer(wires: Iterable | Wires): - r"""Creates a basic Pauli-X mixer Hamiltonian. - - This Hamiltonian is defined as: - - .. math:: H_M \ = \ \displaystyle\sum_{i} X_{i}, - - where :math:`i` ranges over all wires, and :math:`X_i` - denotes the Pauli-X operator on the :math:`i`-th wire. - - This is mixer is used in *A Quantum Approximate Optimization Algorithm* - by Edward Farhi, Jeffrey Goldstone, Sam Gutmann [`arXiv:1411.4028 `__]. - - Args: - wires (Iterable or Wires): The wires on which the Hamiltonian is applied - - Returns: - Hamiltonian: Mixer Hamiltonian - - **Example** - - The mixer Hamiltonian can be called as follows: - - >>> from pennylane import qaoa - >>> wires = range(3) - >>> mixer_h = qaoa.x_mixer(wires) - >>> print(mixer_h) - 1 * X(0) + 1 * X(1) + 1 * X(2) - """ - - wires = Wires(wires) - - coeffs = [1 for w in wires] - obs = [X(w) for w in wires] - - H = LinearCombination(coeffs, obs) - # store the valuable information that all observables are in one commuting group - H.grouping_indices = [list(range(len(H.ops)))] - return H - - -def xy_mixer(graph: nx_Graph | rx.PyGraph): - r"""Creates a generalized SWAP/XY mixer Hamiltonian. - - This mixer Hamiltonian is defined as: - - .. math:: H_M \ = \ \frac{1}{2} \displaystyle\sum_{(i, j) \in E(G)} X_i X_j \ + \ Y_i Y_j, - - for some graph :math:`G`. :math:`X_i` and :math:`Y_i` denote the Pauli-X and Pauli-Y operators on the :math:`i`-th - wire respectively. - - This mixer was introduced in *From the Quantum Approximate Optimization Algorithm - to a Quantum Alternating Operator Ansatz* by Stuart Hadfield, Zhihui Wang, Bryan O'Gorman, - Eleanor G. Rieffel, Davide Venturelli, and Rupak Biswas `Algorithms 12.2 (2019) `__. - - Args: - graph (nx.Graph or rx.PyGraph): A graph defining the collections of wires on which the Hamiltonian acts. - - Returns: - Hamiltonian: Mixer Hamiltonian - - **Example** - - The mixer Hamiltonian can be called as follows: - - >>> from pennylane import qaoa - >>> from networkx import Graph - >>> graph = Graph([(0, 1), (1, 2)]) - >>> mixer_h = qaoa.xy_mixer(graph) - >>> print(mixer_h) - (0.5) [X0 X1] - + (0.5) [Y0 Y1] - + (0.5) [X1 X2] - + (0.5) [Y1 Y2] - - >>> import rustworkx as rx - >>> graph = rx.PyGraph() - >>> graph.add_nodes_from([0, 1, 2]) - >>> graph.add_edges_from([(0, 1, ""), (1, 2, "")]) - >>> mixer_h = xy_mixer(graph) - >>> print(mixer_h) - (0.5) [X0 X1] - + (0.5) [Y0 Y1] - + (0.5) [X1 X2] - + (0.5) [Y1 Y2] - """ - _validate_graph(graph) - - is_rx = isinstance(graph, rx.PyGraph) - edges = graph.edge_list() if is_rx else graph.edges - - # In RX each node is assigned to an integer index starting from 0; - # thus, we use the following lambda function to get node-values. - def get_nvalue(i): - return graph.nodes()[i] if is_rx else i - - coeffs = 2 * [0.5 for e in edges] - - obs = [] - for node1, node2 in edges: - obs.append(X(get_nvalue(node1)) @ X(get_nvalue(node2))) - obs.append(Y(get_nvalue(node1)) @ Y(get_nvalue(node2))) - - return LinearCombination(coeffs, obs) - - -def bit_flip_mixer(graph: nx_Graph | rx.PyGraph, b: int): - r"""Creates a bit-flip mixer Hamiltonian. - - This mixer is defined as: - - .. math:: H_M \ = \ \displaystyle\sum_{v \in V(G)} \frac{1}{2^{d(v)}} X_{v} - \displaystyle\prod_{w \in N(v)} (\mathbb{I} \ + \ (-1)^b Z_w) - - where :math:`V(G)` is the set of vertices of some graph :math:`G`, :math:`d(v)` is the - `degree `__ of vertex :math:`v`, and - :math:`N(v)` is the `neighbourhood `__ - of vertex :math:`v`. In addition, :math:`Z_v` and :math:`X_v` - are the Pauli-Z and Pauli-X operators on vertex :math:`v`, respectively, - and :math:`\mathbb{I}` is the identity operator. - - This mixer was introduced in `Hadfield et al. (2019) `__. - - Args: - graph (nx.Graph or rx.PyGraph): A graph defining the collections of wires on which the Hamiltonian acts. - b (int): Either :math:`0` or :math:`1`. When :math:`b=0`, a bit flip is performed on - vertex :math:`v` only when all neighbouring nodes are in state :math:`|0\rangle`. - Alternatively, for :math:`b=1`, a bit flip is performed only when all the neighbours of - :math:`v` are in the state :math:`|1\rangle`. - - Returns: - Hamiltonian: Mixer Hamiltonian - - **Example** - - The mixer Hamiltonian can be called as follows: - - >>> from pennylane import qaoa - >>> from networkx import Graph - >>> graph = Graph([(0, 1), (1, 2)]) - >>> mixer_h = qaoa.bit_flip_mixer(graph, 0) - >>> mixer_h - ( - 0.5 * X(0) - + 0.5 * (X(0) @ Z(1)) - + 0.25 * X(1) - + 0.25 * (X(1) @ Z(2)) - + 0.25 * (X(1) @ Z(0)) - + 0.25 * (X(1) @ Z(0) @ Z(2)) - + 0.5 * X(2) - + 0.5 * (X(2) @ Z(1)) - ) - - >>> import rustworkx as rx - >>> graph = rx.PyGraph() - >>> graph.add_nodes_from([0, 1, 2]) - >>> graph.add_edges_from([(0, 1, ""), (1, 2, "")]) - >>> mixer_h = qaoa.bit_flip_mixer(graph, 0) - >>> print(mixer_h) - ( - 0.5 * X(0) - + 0.5 * (X(0) @ Z(1)) - + 0.25 * X(1) - + 0.25 * (X(1) @ Z(2)) - + 0.25 * (X(1) @ Z(0)) - + 0.25 * (X(1) @ Z(0) @ Z(2)) - + 0.5 * X(2) - + 0.5 * (X(2) @ Z(1)) - ) - """ - _validate_graph(graph) - - if b not in [0, 1]: - raise ValueError(f"'b' must be either 0 or 1, got {b}") - - sign = 1 if b == 0 else -1 - - coeffs = [] - terms = [] - - is_rx = isinstance(graph, rx.PyGraph) - graph_nodes = graph.node_indexes() if is_rx else graph.nodes - - # In RX each node is assigned to an integer index starting from 0; - # thus, we use the following lambda function to get node-values. - def get_nvalue(i): - return graph.nodes()[i] if is_rx else i - - for i in graph_nodes: - neighbours = sorted(graph.neighbors(i)) if is_rx else list(graph.neighbors(i)) - degree = len(neighbours) - - n_terms = [[X(get_nvalue(i))]] + [ - [Identity(get_nvalue(n)), Z(get_nvalue(n))] for n in neighbours - ] - n_coeffs = [[1, sign] for _ in neighbours] - - final_terms = [prod(*list(m)).simplify() for m in itertools.product(*n_terms)] - - final_coeffs = [ - (0.5**degree) * functools.reduce(lambda x, y: x * y, list(m), 1) - for m in itertools.product(*n_coeffs) - ] - - coeffs.extend(final_coeffs) - terms.extend(final_terms) - - return LinearCombination(coeffs, terms) diff --git a/tach.toml b/tach.toml index 7f8c3398fb4..8035fd8a7c1 100644 --- a/tach.toml +++ b/tach.toml @@ -302,11 +302,6 @@ path = "pennylane.qnn" layer = "tertiary" cannot_depend_on = ["pennylane.ftqc", "pennylane.labs"] -[[modules]] -path = "pennylane.qaoa" -layer = "tertiary" -cannot_depend_on = ["pennylane.ftqc", "pennylane.labs"] - [[modules]] path = "pennylane.kernels" cannot_depend_on = ["pennylane.ftqc", "pennylane.labs"] diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py deleted file mode 100644 index 3c8e4da7dc6..00000000000 --- a/tests/test_qaoa.py +++ /dev/null @@ -1,2077 +0,0 @@ -# Copyright 2018-2020 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. -""" -Unit tests for the :mod:`pennylane.qaoa` submodule. -""" - -import itertools - -import networkx as nx -import numpy as np -import pytest -import rustworkx as rx -from networkx import Graph -from scipy.linalg import expm -from scipy.sparse import csc_matrix, kron - -import pennylane as qp -from pennylane import qaoa -from pennylane.qaoa.cycle import ( - _inner_net_flow_constraint_hamiltonian, - _inner_out_flow_constraint_hamiltonian, - _partial_cycle_mixer, - _square_hamiltonian_terms, - cycle_mixer, - edges_to_wires, - loss_hamiltonian, - net_flow_constraint, - out_flow_constraint, - wires_to_edges, -) - -##################################################### - -line_graph = Graph() -line_graph.add_nodes_from([0, 1, 2]) -line_graph.add_edges_from([(0, 1), (1, 2)]) - -graph_rx = rx.PyGraph() -graph_rx.add_nodes_from([0, 1, 2]) -graph_rx.add_edges_from([(0, 1, ""), (1, 2, "")]) - -non_consecutive_graph = Graph([(0, 4), (3, 4), (2, 1), (2, 0)]) -non_consecutive_graph_rx = rx.PyGraph() -non_consecutive_graph_rx.add_nodes_from([0, 1, 2, 3, 4]) -non_consecutive_graph_rx.add_edges_from([(0, 4, ""), (0, 2, ""), (4, 3, ""), (2, 1, "")]) - -g1 = Graph([(0, 1), (1, 2)]) - -g1_rx = rx.PyGraph() -g1_rx.add_nodes_from([0, 1, 2]) -g1_rx.add_edges_from([(0, 1, ""), (1, 2, "")]) - -g2 = nx.Graph([(0, 1), (1, 2), (2, 3)]) - -g2_rx = rx.PyGraph() -g2_rx.add_nodes_from([0, 1, 2, 3]) -g2_rx.add_edges_from([(0, 1, ""), (1, 2, ""), (2, 3, "")]) - -b_rx = rx.PyGraph() -b_rx.add_nodes_from(["b", 1, 0.3]) -b_rx.add_edges_from([(0, 1, ""), (1, 2, ""), (0, 2, "")]) - - -def lollipop_graph_rx(mesh_nodes: int, path_nodes: int, to_directed: bool = False): - if to_directed: - g = rx.generators.directed_mesh_graph(weights=[*range(mesh_nodes)]) - else: - g = rx.generators.mesh_graph(weights=[*range(mesh_nodes)]) - if path_nodes < 1: - return g - - for i in range(path_nodes): - g.add_node(mesh_nodes + i) - g.add_edges_from([(mesh_nodes + i - 1, mesh_nodes + i, "")]) - if to_directed: - g.add_edges_from([(mesh_nodes + i, mesh_nodes + i - 1, "")]) - return g - - -def matrix(hamiltonian: qp.Hamiltonian, n_wires: int) -> csc_matrix: - r"""Calculates the matrix representation of an input Hamiltonian in the standard basis. - - Args: - hamiltonian (qp.Hamiltonian): the input Hamiltonian - n_wires (int): the total number of wires - - Returns: - csc_matrix: a sparse matrix representation - """ - ops_matrices = [] - - for op in hamiltonian.ops: - - if isinstance(op, qp.ops.Prod): - op_matrix = op.sparse_matrix(wire_order=list(range(n_wires))) - else: - op_wires = np.array(op.wires.tolist()) - op_list = [op] - op_matrices = [] - - for wire in range(n_wires): - loc = np.argwhere(op_wires == wire).flatten() - mat = np.eye(2) if len(loc) == 0 else op_list[loc[0]].matrix() - mat = csc_matrix(mat) - op_matrices.append(mat) - - op_matrix = op_matrices.pop(0) - - for mat in op_matrices: - op_matrix = kron(op_matrix, mat) - - ops_matrices.append(op_matrix) - - mat = sum(coeff * op_mat for coeff, op_mat in zip(hamiltonian.coeffs, ops_matrices)) - return csc_matrix(mat) - - -def make_xy_mixer_test_cases(): - return [ - ( - g2, - qp.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qp.PauliX(0) @ qp.PauliX(1), - qp.PauliY(0) @ qp.PauliY(1), - qp.PauliX(1) @ qp.PauliX(2), - qp.PauliY(1) @ qp.PauliY(2), - qp.PauliX(2) @ qp.PauliX(3), - qp.PauliY(2) @ qp.PauliY(3), - ], - ), - ), - ( - line_graph, - qp.Hamiltonian( - [0.5, 0.5, 0.5, 0.5], - [ - qp.PauliX(0) @ qp.PauliX(1), - qp.PauliY(0) @ qp.PauliY(1), - qp.PauliX(1) @ qp.PauliX(2), - qp.PauliY(1) @ qp.PauliY(2), - ], - ), - ), - ( - non_consecutive_graph, - qp.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qp.PauliX(0) @ qp.PauliX(4), - qp.PauliY(0) @ qp.PauliY(4), - qp.PauliX(0) @ qp.PauliX(2), - qp.PauliY(0) @ qp.PauliY(2), - qp.PauliX(4) @ qp.PauliX(3), - qp.PauliY(4) @ qp.PauliY(3), - qp.PauliX(2) @ qp.PauliX(1), - qp.PauliY(2) @ qp.PauliY(1), - ], - ), - ), - ( - g2_rx, - qp.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qp.PauliX(0) @ qp.PauliX(1), - qp.PauliY(0) @ qp.PauliY(1), - qp.PauliX(1) @ qp.PauliX(2), - qp.PauliY(1) @ qp.PauliY(2), - qp.PauliX(2) @ qp.PauliX(3), - qp.PauliY(2) @ qp.PauliY(3), - ], - ), - ), - ( - graph_rx, - qp.Hamiltonian( - [0.5, 0.5, 0.5, 0.5], - [ - qp.PauliX(0) @ qp.PauliX(1), - qp.PauliY(0) @ qp.PauliY(1), - qp.PauliX(1) @ qp.PauliX(2), - qp.PauliY(1) @ qp.PauliY(2), - ], - ), - ), - ( - non_consecutive_graph_rx, - qp.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qp.PauliX(0) @ qp.PauliX(4), - qp.PauliY(0) @ qp.PauliY(4), - qp.PauliX(0) @ qp.PauliX(2), - qp.PauliY(0) @ qp.PauliY(2), - qp.PauliX(4) @ qp.PauliX(3), - qp.PauliY(4) @ qp.PauliY(3), - qp.PauliX(2) @ qp.PauliX(1), - qp.PauliY(2) @ qp.PauliY(1), - ], - ), - ), - ( - Graph((np.array([0, 1]), np.array([1, 2]), np.array([2, 0]))), - qp.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qp.PauliX(0) @ qp.PauliX(1), - qp.PauliY(0) @ qp.PauliY(1), - qp.PauliX(0) @ qp.PauliX(2), - qp.PauliY(0) @ qp.PauliY(2), - qp.PauliX(1) @ qp.PauliX(2), - qp.PauliY(1) @ qp.PauliY(2), - ], - ), - ), - ] - - -def make_bit_flip_mixer_test_cases(): - return [ - ( - Graph([(0, 1)]), - 1, - qp.Hamiltonian( - [0.5, -0.5, 0.5, -0.5], - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(1), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(0), - ], - ), - ), - ( - g1, - 0, - qp.Hamiltonian( - [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(1), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(2), - qp.PauliX(1) @ qp.PauliZ(0), - qp.PauliX(1) @ qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(1), - ], - ), - ), - ( - g1_rx, - 0, - qp.Hamiltonian( - [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(1), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(2), - qp.PauliX(1) @ qp.PauliZ(0), - qp.PauliX(1) @ qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(1), - ], - ), - ), - ( - Graph([("b", 1), (1, 0.3), (0.3, "b")]), - 1, - qp.Hamiltonian( - [0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25], - [ - qp.PauliX("b"), - qp.PauliX("b") @ qp.PauliZ(0.3), - qp.PauliX("b") @ qp.PauliZ(1), - qp.PauliX("b") @ qp.PauliZ(1) @ qp.PauliZ(0.3), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(0.3), - qp.PauliX(1) @ qp.PauliZ("b"), - qp.PauliX(1) @ qp.PauliZ("b") @ qp.PauliZ(0.3), - qp.PauliX(0.3), - qp.PauliX(0.3) @ qp.PauliZ("b"), - qp.PauliX(0.3) @ qp.PauliZ(1), - qp.PauliX(0.3) @ qp.PauliZ(1) @ qp.PauliZ("b"), - ], - ), - ), - ( - b_rx, - 1, - qp.Hamiltonian( - [0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25], - [ - qp.PauliX("b"), - qp.PauliX("b") @ qp.PauliZ(0.3), - qp.PauliX("b") @ qp.PauliZ(1), - qp.PauliX("b") @ qp.PauliZ(1) @ qp.PauliZ(0.3), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(0.3), - qp.PauliX(1) @ qp.PauliZ("b"), - qp.PauliX(1) @ qp.PauliZ("b") @ qp.PauliZ(0.3), - qp.PauliX(0.3), - qp.PauliX(0.3) @ qp.PauliZ(1), - qp.PauliX(0.3) @ qp.PauliZ("b"), - qp.PauliX(0.3) @ qp.PauliZ("b") @ qp.PauliZ(1), - ], - ), - ), - ] - - -class TestMixerHamiltonians: - """Tests that the mixer Hamiltonians are being generated correctly""" - - def test_x_mixer_output(self): - """Tests that the output of the Pauli-X mixer is correct""" - - wires = range(4) - mixer_hamiltonian = qaoa.x_mixer(wires) - expected_hamiltonian = qp.Hamiltonian( - [1, 1, 1, 1], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2), qp.PauliX(3)], - ) - assert mixer_hamiltonian == expected_hamiltonian - - def test_x_mixer_grouping(self): - """Tests that the grouping information is set and correct""" - - wires = range(4) - mixer_hamiltonian = qaoa.x_mixer(wires) - - # check that all observables commute - assert all(qp.is_commuting(o, mixer_hamiltonian.ops[0]) for o in mixer_hamiltonian.ops[1:]) - # check that the 1-group grouping information was set - assert mixer_hamiltonian.grouping_indices is not None - assert mixer_hamiltonian.grouping_indices == ((0, 1, 2, 3),) - - def test_xy_mixer_type_error(self): - """Tests that the XY mixer throws the correct error""" - - graph = [(0, 1), (1, 2)] - - with pytest.raises( - ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph, got list" - ): - qaoa.xy_mixer(graph) - - @pytest.mark.parametrize(("graph", "target_hamiltonian"), make_xy_mixer_test_cases()) - def test_xy_mixer_output(self, graph, target_hamiltonian): - """Tests that the output of the XY mixer is correct""" - hamiltonian = qaoa.xy_mixer(graph) - assert hamiltonian == target_hamiltonian - - def test_bit_flip_mixer_errors(self): - """Tests that the bit-flip mixer throws the correct errors""" - - graph = [(0, 1), (1, 2)] - with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph"): - qaoa.bit_flip_mixer(graph, 0) - - n = 2 - with pytest.raises(ValueError, match=r"'b' must be either 0 or 1"): - qaoa.bit_flip_mixer(Graph(graph), n) - - @pytest.mark.parametrize( - ("graph", "n", "target_hamiltonian"), - make_bit_flip_mixer_test_cases(), - ) - def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): - """Tests that the output of the bit-flip mixer is correct""" - hamiltonian = qaoa.bit_flip_mixer(graph, n) - assert hamiltonian == target_hamiltonian - - -GRAPHS = [ - g1, - g1_rx, - Graph((np.array([0, 1]), np.array([1, 2]), np.array([0, 2]))), - line_graph, - graph_rx, -] - - -def make_max_cut_test_cases(): - """Generates test cases for the maxcut problem""" - - cost_coeffs = [ - [0.5, 0.5, -1.0], - [0.5, 0.5, -1.0], - [0.5, 0.5, 0.5, -1.5], - [0.5, 0.5, -1.0], - [0.5, 0.5, -1.0], - ] - - cost_terms = [ - [qp.PauliZ(0) @ qp.PauliZ(1), qp.PauliZ(1) @ qp.PauliZ(2), qp.Identity(0)], - [qp.PauliZ(0) @ qp.PauliZ(1), qp.PauliZ(1) @ qp.PauliZ(2), qp.Identity(0)], - [ - qp.PauliZ(0) @ qp.PauliZ(1), - qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliZ(1) @ qp.PauliZ(2), - qp.Identity(0), - ], - [qp.PauliZ(0) @ qp.PauliZ(1), qp.PauliZ(1) @ qp.PauliZ(2), qp.Identity(0)], - [qp.PauliZ(0) @ qp.PauliZ(1), qp.PauliZ(1) @ qp.PauliZ(2), qp.Identity(0)], - ] - - cost_hamiltonians = [qp.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(5)] - - mixer_coeffs = [ - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - ] - - mixer_terms = [ - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - ] - - mixer_hamiltonians = [qp.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(5)] - - return list(zip(GRAPHS, cost_hamiltonians, mixer_hamiltonians)) - - -CONSTRAINED = [ - True, - True, - True, - False, - False, -] - - -def make_max_independent_test_cases(): - """Generates test cases for the max independent set problem""" - - cost_coeffs = [ - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - [0.75, 0.25, -0.5, 0.75, 0.25], - [0.75, 0.25, -0.5, 0.75, 0.25], - ] - - cost_terms = [ - [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [ - qp.PauliZ(0) @ qp.PauliZ(1), - qp.PauliZ(0), - qp.PauliZ(1), - qp.PauliZ(1) @ qp.PauliZ(2), - qp.PauliZ(2), - ], - # [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [ - qp.PauliZ(0) @ qp.PauliZ(1), - qp.PauliZ(0), - qp.PauliZ(1), - qp.PauliZ(1) @ qp.PauliZ(2), - qp.PauliZ(2), - ], - ] - - cost_hamiltonians = [qp.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(5)] - - mixer_coeffs = [ - [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], - [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], - [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], - [1, 1, 1], - [1, 1, 1], - ] - - mixer_terms = [ - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(1), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(2), - qp.PauliX(1) @ qp.PauliZ(0), - qp.PauliX(1) @ qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(1), - ], - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(1), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(2), - qp.PauliX(1) @ qp.PauliZ(0), - qp.PauliX(1) @ qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(1), - ], - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(2), - qp.PauliX(0) @ qp.PauliZ(1), - qp.PauliX(0) @ qp.PauliZ(1) @ qp.PauliZ(2), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(2), - qp.PauliX(1) @ qp.PauliZ(0), - qp.PauliX(1) @ qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(0), - qp.PauliX(2) @ qp.PauliZ(1), - qp.PauliX(2) @ qp.PauliZ(1) @ qp.PauliZ(0), - ], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - ] - - mixer_hamiltonians = [qp.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(5)] - - return list(zip(GRAPHS, CONSTRAINED, cost_hamiltonians, mixer_hamiltonians)) - - -def make_min_vertex_cover_test_cases(): - """Generates the test cases for the min vertex cover problem""" - - cost_coeffs = [ - [-1, -1, -1], - [-1, -1, -1], - [-1, -1, -1], - [0.75, -0.25, 0.5, 0.75, -0.25], - [0.75, -0.25, 0.5, 0.75, -0.25], - ] - - cost_terms = [ - [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [ - qp.PauliZ(0) @ qp.PauliZ(1), - qp.PauliZ(0), - qp.PauliZ(1), - qp.PauliZ(1) @ qp.PauliZ(2), - qp.PauliZ(2), - ], - [ - qp.PauliZ(0) @ qp.PauliZ(1), - qp.PauliZ(0), - qp.PauliZ(1), - qp.PauliZ(1) @ qp.PauliZ(2), - qp.PauliZ(2), - ], - ] - - cost_hamiltonians = [qp.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(5)] - - mixer_coeffs = [ - [0.5, -0.5, 0.25, -0.25, -0.25, 0.25, 0.5, -0.5], - [0.5, -0.5, 0.25, -0.25, -0.25, 0.25, 0.5, -0.5], - [0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25, 0.25, -0.25, -0.25, 0.25], - [1, 1, 1], - [1, 1, 1], - ] - - mixer_terms = [ - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(1), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(2), - qp.PauliX(1) @ qp.PauliZ(0), - qp.PauliX(1) @ qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(1), - ], - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(1), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(2), - qp.PauliX(1) @ qp.PauliZ(0), - qp.PauliX(1) @ qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(1), - ], - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(2), - qp.PauliX(0) @ qp.PauliZ(1), - qp.PauliX(0) @ qp.PauliZ(1) @ qp.PauliZ(2), - qp.PauliX(1), - qp.PauliX(1) @ qp.PauliZ(2), - qp.PauliX(1) @ qp.PauliZ(0), - qp.PauliX(1) @ qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(0), - qp.PauliX(2) @ qp.PauliZ(1), - qp.PauliX(2) @ qp.PauliZ(1) @ qp.PauliZ(0), - ], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - ] - - mixer_hamiltonians = [qp.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(5)] - - return list(zip(GRAPHS, CONSTRAINED, cost_hamiltonians, mixer_hamiltonians)) - - -def make_max_clique_test_cases(): - """Generates the test cases for the max clique problem""" - - cost_coeffs = [ - [1, 1, 1], - [1, 1, 1], - [1, 1, 1], - [0.75, 0.25, 0.25, 1], - [0.75, 0.25, 0.25, 1], - ] - - cost_terms = [ - [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)], - [qp.PauliZ(0) @ qp.PauliZ(2), qp.PauliZ(0), qp.PauliZ(2), qp.PauliZ(1)], - [qp.PauliZ(0) @ qp.PauliZ(2), qp.PauliZ(0), qp.PauliZ(2), qp.PauliZ(1)], - ] - - cost_hamiltonians = [qp.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(5)] - - mixer_coeffs = [ - [0.5, 0.5, 1.0, 0.5, 0.5], - [0.5, 0.5, 1.0, 0.5, 0.5], - [1.0, 1.0, 1.0], - [1, 1, 1], - [1, 1, 1], - ] - - mixer_terms = [ - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(2), - qp.PauliX(1), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(0), - ], - [ - qp.PauliX(0), - qp.PauliX(0) @ qp.PauliZ(2), - qp.PauliX(1), - qp.PauliX(2), - qp.PauliX(2) @ qp.PauliZ(0), - ], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - [qp.PauliX(0), qp.PauliX(1), qp.PauliX(2)], - ] - - mixer_hamiltonians = [qp.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(5)] - - return list(zip(GRAPHS, CONSTRAINED, cost_hamiltonians, mixer_hamiltonians)) - - -def make_edge_driver_cost_test_cases(): - """Generates the test cases for the edge driver cost Hamiltonian""" - - graphs = GRAPHS[1:-2] - graphs.append(line_graph) - graphs.append(Graph([("b", 1), (1, 2.3)])) - graphs.append(graph_rx) - - b1_rx = rx.PyGraph() - b1_rx.add_nodes_from(["b", 1, 2.3]) - b1_rx.add_edges_from([(0, 1, ""), (1, 2, "")]) - - graphs.append(b1_rx) - - rewards = [ - ["00"], - ["00", "11"], - ["00", "11", "01", "10"], - ["00", "01", "10"], - ["00", "11", "01", "10"], - ["00", "01", "10"], - ] - - hamiltonians = [ - qp.Hamiltonian( - [-0.25, -0.25, -0.25, -0.25, -0.25, -0.25], - [ - qp.PauliZ(0) @ qp.PauliZ(1), - qp.PauliZ(0), - qp.PauliZ(1), - qp.PauliZ(1) @ qp.PauliZ(2), - qp.PauliZ(1), - qp.PauliZ(2), - ], - ), - qp.Hamiltonian( - [-0.5, -0.5, -0.5], - [ - qp.PauliZ(0) @ qp.PauliZ(1), - qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliZ(1) @ qp.PauliZ(2), - ], - ), - qp.Hamiltonian([1, 1, 1], [qp.Identity(0), qp.Identity(1), qp.Identity(2)]), - qp.Hamiltonian( - [0.25, -0.25, -0.25, 0.25, -0.25, -0.25], - [ - qp.PauliZ("b") @ qp.PauliZ(1), - qp.PauliZ("b"), - qp.PauliZ(1), - qp.PauliZ(1) @ qp.PauliZ(2.3), - qp.PauliZ(1), - qp.PauliZ(2.3), - ], - ), - qp.Hamiltonian([1, 1, 1], [qp.Identity(0), qp.Identity(1), qp.Identity(2)]), - qp.Hamiltonian( - [0.25, -0.25, -0.25, 0.25, -0.25, -0.25], - [ - qp.PauliZ("b") @ qp.PauliZ(1), - qp.PauliZ("b"), - qp.PauliZ(1), - qp.PauliZ(1) @ qp.PauliZ(2.3), - qp.PauliZ(1), - qp.PauliZ(2.3), - ], - ), - ] - - return list(zip(graphs, rewards, hamiltonians)) - - -def make_max_weighted_cycle_test_cases(): - """Generates the test cases for the maximum weighted cycle problem""" - - digraph_complete = nx.complete_graph(3).to_directed() - complete_edge_weight_data = { - edge: (i + 1) * 0.5 for i, edge in enumerate(digraph_complete.edges) - } - for _k, _v in complete_edge_weight_data.items(): - digraph_complete[_k[0]][_k[1]]["weight"] = _v - - digraph_complete_rx = rx.generators.directed_mesh_graph(3, [0, 1, 2]) - complete_edge_weight_data = { - edge: (i + 1) * 0.5 for i, edge in enumerate(sorted(digraph_complete_rx.edge_list())) - } - for _k, _v in complete_edge_weight_data.items(): - digraph_complete_rx.update_edge(_k[0], _k[1], {"weight": _v}) - - digraphs = [digraph_complete] * 2 - - mwc_constrained = [True, False] - - cost_coeffs = [ - [ - -0.6931471805599453, - 0.0, - 0.4054651081081644, - 0.6931471805599453, - 0.9162907318741551, - 1.0986122886681098, - ], - [ - -6.693147180559945, - -6.0, - -5.594534891891835, - -5.306852819440055, - -5.083709268125845, - -4.90138771133189, - 54, - 12, - -12, - -6, - -6, - -12, - 6, - 12, - -6, - -6, - -12, - 6, - 12, - -6, - -6, - 6, - ], - ] - - cost_terms = [ - [ - qp.PauliZ(wires=[0]), - qp.PauliZ(wires=[1]), - qp.PauliZ(wires=[2]), - qp.PauliZ(wires=[3]), - qp.PauliZ(wires=[4]), - qp.PauliZ(wires=[5]), - ], - [ - qp.PauliZ(wires=[0]), - qp.PauliZ(wires=[1]), - qp.PauliZ(wires=[2]), - qp.PauliZ(wires=[3]), - qp.PauliZ(wires=[4]), - qp.PauliZ(wires=[5]), - qp.Identity(wires=[0]), - qp.PauliZ(wires=[0]) @ qp.PauliZ(wires=[1]), - qp.PauliZ(wires=[0]) @ qp.PauliZ(wires=[2]), - qp.PauliZ(wires=[0]) @ qp.PauliZ(wires=[4]), - qp.PauliZ(wires=[1]) @ qp.PauliZ(wires=[2]), - qp.PauliZ(wires=[1]) @ qp.PauliZ(wires=[4]), - qp.PauliZ(wires=[2]) @ qp.PauliZ(wires=[4]), - qp.PauliZ(wires=[2]) @ qp.PauliZ(wires=[3]), - qp.PauliZ(wires=[2]) @ qp.PauliZ(wires=[5]), - qp.PauliZ(wires=[0]) @ qp.PauliZ(wires=[3]), - qp.PauliZ(wires=[3]) @ qp.PauliZ(wires=[5]), - qp.PauliZ(wires=[0]) @ qp.PauliZ(wires=[5]), - qp.PauliZ(wires=[4]) @ qp.PauliZ(wires=[5]), - qp.PauliZ(wires=[3]) @ qp.PauliZ(wires=[4]), - qp.PauliZ(wires=[1]) @ qp.PauliZ(wires=[5]), - qp.PauliZ(wires=[1]) @ qp.PauliZ(wires=[3]), - ], - ] - - cost_hamiltonians = [qp.Hamiltonian(cost_coeffs[i], cost_terms[i]) for i in range(2)] - - mixer_coeffs = [ - [ - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - 0.25, - 0.25, - 0.25, - -0.25, - ], - [1] * 6, - ] - - mixer_terms = [ - [ - qp.PauliX(wires=[0]) @ qp.PauliX(wires=[1]) @ qp.PauliX(wires=[5]), - qp.PauliY(wires=[0]) @ qp.PauliY(wires=[1]) @ qp.PauliX(wires=[5]), - qp.PauliY(wires=[0]) @ qp.PauliX(wires=[1]) @ qp.PauliY(wires=[5]), - qp.PauliX(wires=[0]) @ qp.PauliY(wires=[1]) @ qp.PauliY(wires=[5]), - qp.PauliX(wires=[1]) @ qp.PauliX(wires=[0]) @ qp.PauliX(wires=[3]), - qp.PauliY(wires=[1]) @ qp.PauliY(wires=[0]) @ qp.PauliX(wires=[3]), - qp.PauliY(wires=[1]) @ qp.PauliX(wires=[0]) @ qp.PauliY(wires=[3]), - qp.PauliX(wires=[1]) @ qp.PauliY(wires=[0]) @ qp.PauliY(wires=[3]), - qp.PauliX(wires=[2]) @ qp.PauliX(wires=[3]) @ qp.PauliX(wires=[4]), - qp.PauliY(wires=[2]) @ qp.PauliY(wires=[3]) @ qp.PauliX(wires=[4]), - qp.PauliY(wires=[2]) @ qp.PauliX(wires=[3]) @ qp.PauliY(wires=[4]), - qp.PauliX(wires=[2]) @ qp.PauliY(wires=[3]) @ qp.PauliY(wires=[4]), - qp.PauliX(wires=[3]) @ qp.PauliX(wires=[2]) @ qp.PauliX(wires=[1]), - qp.PauliY(wires=[3]) @ qp.PauliY(wires=[2]) @ qp.PauliX(wires=[1]), - qp.PauliY(wires=[3]) @ qp.PauliX(wires=[2]) @ qp.PauliY(wires=[1]), - qp.PauliX(wires=[3]) @ qp.PauliY(wires=[2]) @ qp.PauliY(wires=[1]), - qp.PauliX(wires=[4]) @ qp.PauliX(wires=[5]) @ qp.PauliX(wires=[2]), - qp.PauliY(wires=[4]) @ qp.PauliY(wires=[5]) @ qp.PauliX(wires=[2]), - qp.PauliY(wires=[4]) @ qp.PauliX(wires=[5]) @ qp.PauliY(wires=[2]), - qp.PauliX(wires=[4]) @ qp.PauliY(wires=[5]) @ qp.PauliY(wires=[2]), - qp.PauliX(wires=[5]) @ qp.PauliX(wires=[4]) @ qp.PauliX(wires=[0]), - qp.PauliY(wires=[5]) @ qp.PauliY(wires=[4]) @ qp.PauliX(wires=[0]), - qp.PauliY(wires=[5]) @ qp.PauliX(wires=[4]) @ qp.PauliY(wires=[0]), - qp.PauliX(wires=[5]) @ qp.PauliY(wires=[4]) @ qp.PauliY(wires=[0]), - ], - [qp.PauliX(wires=i) for i in range(6)], - ] - - mixer_hamiltonians = [qp.Hamiltonian(mixer_coeffs[i], mixer_terms[i]) for i in range(2)] - - mappings = [qaoa.cycle.wires_to_edges(digraph_complete)] * 2 - - return list(zip(digraphs, mwc_constrained, cost_hamiltonians, mixer_hamiltonians, mappings)) - - -class TestCostHamiltonians: - """Tests that the cost Hamiltonians are being generated correctly""" - - def test_bit_driver_error(self): - """Tests that the bit driver Hamiltonian throws the correct error""" - - with pytest.raises(ValueError, match=r"'b' must be either 0 or 1"): - qaoa.bit_driver(range(3), 2) - - def test_bit_driver_output(self): - """Tests that the bit driver Hamiltonian has the correct output""" - - H = qaoa.bit_driver(range(3), 1) - hamiltonian = qp.Hamiltonian([1, 1, 1], [qp.PauliZ(0), qp.PauliZ(1), qp.PauliZ(2)]) - assert hamiltonian == H - - def test_edge_driver_errors(self): - """Tests that the edge driver Hamiltonian throws the correct errors""" - - with pytest.raises( - ValueError, match=r"Encountered invalid entry in 'reward', expected 2-bit bitstrings." - ): - qaoa.edge_driver(g1, ["10", "11", 21, "g"]) - - with pytest.raises( - ValueError, - match=r"'reward' cannot contain either '10' or '01', must contain neither or both.", - ): - qaoa.edge_driver(g1, ["11", "00", "01"]) - - with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph"): - qaoa.edge_driver([(0, 1), (1, 2)], ["00", "11"]) - - @pytest.mark.parametrize(("graph", "reward", "hamiltonian"), make_edge_driver_cost_test_cases()) - def test_edge_driver_output(self, graph, reward, hamiltonian): - """Tests that the edge driver Hamiltonian throws the correct errors""" - H = qaoa.edge_driver(graph, reward) - assert hamiltonian == H - - def test_max_weight_cycle_errors(self): - """Tests that the max weight cycle Hamiltonian throws the correct errors""" - - with pytest.raises( - ValueError, match=r"Input graph must be a nx.Graph, rx.PyGraph, or rx.PyDiGraph" - ): - qaoa.max_weight_cycle([(0, 1), (1, 2)]) - - def test_cost_graph_error(self): - """Tests that the cost Hamiltonians throw the correct error""" - - graph = [(0, 1), (1, 2)] - - with pytest.raises(ValueError, match=r"Input graph must be a nx\.Graph or rx\.PyGraph"): - qaoa.maxcut(graph) - with pytest.raises(ValueError, match=r"Input graph must be a nx\.Graph or rx\.PyGraph"): - qaoa.max_independent_set(graph) - with pytest.raises(ValueError, match=r"Input graph must be a nx\.Graph or rx\.PyGraph"): - qaoa.min_vertex_cover(graph) - with pytest.raises(ValueError, match=r"Input graph must be a nx\.Graph or rx\.PyGraph"): - qaoa.max_clique(graph) - - @pytest.mark.parametrize( - ("graph", "cost_hamiltonian", "mixer_hamiltonian"), make_max_cut_test_cases() - ) - def test_maxcut_output(self, graph, cost_hamiltonian, mixer_hamiltonian): - """Tests that the output of the MaxCut method is correct""" - cost_h, mixer_h = qaoa.maxcut(graph) - assert cost_h == cost_hamiltonian - assert mixer_h == mixer_hamiltonian - - def test_maxcut_grouping(self): - """Tests that the grouping information is set and correct""" - - maxcut = make_max_cut_test_cases() - graph = maxcut[0][0] - cost_h, _ = qaoa.maxcut(graph) - - # check that all observables commute - assert all(qp.is_commuting(o, cost_h.ops[0]) for o in cost_h.ops[1:]) - # check that the 1-group grouping information was set - assert cost_h.grouping_indices is not None - assert cost_h.grouping_indices == (tuple(range(len(cost_h.ops))),) - - @pytest.mark.parametrize( - ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), - make_max_independent_test_cases(), - ) - def test_mis_output(self, graph, constrained, cost_hamiltonian, mixer_hamiltonian): - """Tests that the output of the Max Indepenent Set method is correct""" - cost_h, mixer_h = qaoa.max_independent_set(graph, constrained=constrained) - assert cost_h == cost_hamiltonian - assert mixer_h == mixer_hamiltonian - - def test_mis_grouping(self): - """Tests that the grouping information is set and correct""" - - mis = make_max_independent_test_cases() - graph = mis[0][0] - cost_h, _ = qaoa.max_independent_set(graph) - - # check that all observables commute - assert all(qp.is_commuting(o, cost_h.ops[0]) for o in cost_h.ops[1:]) - # check that the 1-group grouping information was set - assert cost_h.grouping_indices is not None - assert cost_h.grouping_indices == (tuple(range(len(cost_h.ops))),) - - @pytest.mark.parametrize( - ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), - make_min_vertex_cover_test_cases(), - ) - def test_mvc_output(self, graph, constrained, cost_hamiltonian, mixer_hamiltonian): - """Tests that the output of the Min Vertex Cover method is correct""" - cost_h, mixer_h = qaoa.min_vertex_cover(graph, constrained=constrained) - assert cost_h == cost_hamiltonian - assert mixer_h == mixer_hamiltonian - - def test_mvc_grouping(self): - """Tests that the grouping information is set and correct""" - - mvc = make_min_vertex_cover_test_cases() - graph = mvc[0][0] - cost_h, _ = qaoa.min_vertex_cover(graph) - - # check that all observables commute - assert all(qp.is_commuting(o, cost_h.ops[0]) for o in cost_h.ops[1:]) - # check that the 1-group grouping information was set - assert cost_h.grouping_indices is not None - assert cost_h.grouping_indices == (tuple(range(len(cost_h.ops))),) - - @pytest.mark.parametrize( - ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian"), - make_max_clique_test_cases(), - ) - def test_max_clique_output(self, graph, constrained, cost_hamiltonian, mixer_hamiltonian): - """Tests that the output of the Maximum Clique method is correct""" - cost_h, mixer_h = qaoa.max_clique(graph, constrained=constrained) - assert cost_h == cost_hamiltonian - assert mixer_h == mixer_hamiltonian - - def test_max_clique_grouping(self): - """Tests that the grouping information is set and correct""" - - maxclique = make_max_clique_test_cases() - graph = maxclique[0][0] - cost_h, _ = qaoa.max_clique(graph) - - # check that all observables commute - assert all(qp.is_commuting(o, cost_h.ops[0]) for o in cost_h.ops[1:]) - # check that the 1-group grouping information was set - assert cost_h.grouping_indices is not None - assert cost_h.grouping_indices == (tuple(range(len(cost_h.ops))),) - - # pylint: disable=too-many-arguments - @pytest.mark.parametrize( - ("graph", "constrained", "cost_hamiltonian", "mixer_hamiltonian", "mapping"), - make_max_weighted_cycle_test_cases(), - ) - def test_max_weight_cycle_output( - self, graph, constrained, cost_hamiltonian, mixer_hamiltonian, mapping - ): - """Tests that the output of the maximum weighted cycle method is correct""" - cost_h, mixer_h, m = qaoa.max_weight_cycle(graph, constrained=constrained) - assert cost_h == cost_hamiltonian - assert mixer_h == mixer_hamiltonian - assert mapping == m - - def test_max_weight_cycle_grouping(self): - """Tests that the grouping information is set and correct""" - - mwc = make_max_weighted_cycle_test_cases() - graph = mwc[0][0] - cost_h, _, _ = qaoa.max_weight_cycle(graph) - - # check that all observables commute - assert all(qp.is_commuting(o, cost_h.ops[0]) for o in cost_h.ops[1:]) - # check that the 1-group grouping information was set - assert cost_h.grouping_indices is not None - assert cost_h.grouping_indices == (tuple(range(len(cost_h.ops))),) - - -# pylint: disable=too-few-public-methods -class TestUtils: - """Tests that the utility functions are working properly""" - - # pylint: disable=protected-access - @pytest.mark.parametrize( - ("hamiltonian", "value"), - ( - (qp.Hamiltonian([1, 1], [qp.PauliZ(0), qp.PauliZ(1)]), True), - (qp.Hamiltonian([1, 1], [qp.PauliX(0), qp.PauliZ(1)]), False), - (qp.Hamiltonian([1, 1], [qp.PauliZ(0) @ qp.Identity(1), qp.PauliZ(1)]), True), - (qp.Hamiltonian([1, 1], [qp.PauliZ(0), qp.PauliX(0) @ qp.PauliZ(1)]), False), - ), - ) - def test_diagonal_terms(self, hamiltonian, value): - assert qaoa.layers._diagonal_terms(hamiltonian) == value - - -class TestLayers: - """Tests that the cost and mixer layers are being constructed properly""" - - def test_mixer_layer_errors(self): - """Tests that the mixer layer is throwing the correct errors""" - - hamiltonian = [[1, 1], [1, 1]] - - with pytest.raises( - ValueError, match=r"hamiltonian must be a linear combination of pauli words" - ): - qaoa.mixer_layer(0.1, hamiltonian) - - def test_cost_layer_errors(self): - """Tests that the cost layer is throwing the correct errors""" - - hamiltonian = [[1, 1], [1, 1]] - - with pytest.raises( - ValueError, match=r"hamiltonian must be a linear combination of pauli words" - ): - qaoa.cost_layer(0.1, hamiltonian) - - hamiltonian = qp.Hamiltonian([1, 1], [qp.PauliZ(0), qp.PauliX(1)]) - - with pytest.raises( - ValueError, - match=r"hamiltonian must be written only in terms of PauliZ and Identity gates", - ): - qaoa.cost_layer(0.1, hamiltonian) - - mixer_layer_test_cases = [ - [ - qp.Hamiltonian([1, 1], [qp.PauliX(0), qp.PauliX(1)]), - [qp.PauliRot(2, "X", wires=[0]), qp.PauliRot(2, "X", wires=[1])], - ], - [ - qp.X(0) + qp.X(1), - [qp.PauliRot(2, "X", wires=[0]), qp.PauliRot(2, "X", wires=[1])], - ], - [ - qaoa.xy_mixer(Graph([(0, 1), (1, 2), (2, 0)])), - [ - qp.PauliRot(1.0, "XX", wires=[0, 1]), - qp.PauliRot(1.0, "YY", wires=[0, 1]), - qp.PauliRot(1.0, "XX", wires=[0, 2]), - qp.PauliRot(1.0, "YY", wires=[0, 2]), - qp.PauliRot(1.0, "XX", wires=[1, 2]), - qp.PauliRot(1.0, "YY", wires=[1, 2]), - ], - ], - ] - - @pytest.mark.parametrize(("mixer", "gates"), mixer_layer_test_cases) - def test_mixer_layer_output(self, mixer, gates): - """Tests that the gates of the mixer layer are correct""" - - alpha = 1 - with qp.queuing.AnnotatedQueue() as q: - out = qaoa.mixer_layer(alpha, mixer) - - expected = qp.ApproxTimeEvolution(mixer, alpha, 1) - qp.assert_equal(out, expected) - - assert q.queue[0] is out - assert len(q) == 1 - decomp = out.decomposition() - - for i, j in zip(decomp, gates): - qp.assert_equal(i, j) - - cost_layer_test_cases = [ - [ - qp.Hamiltonian([1, 1], [qp.PauliZ(0), qp.PauliZ(1)]), - [qp.PauliRot(2, "Z", wires=[0]), qp.PauliRot(2, "Z", wires=[1])], - ], - [ - qp.Z(0) + qp.Z(1), - [qp.PauliRot(2, "Z", wires=[0]), qp.PauliRot(2, "Z", wires=[1])], - ], - [ - qaoa.maxcut(Graph([(0, 1), (1, 2), (2, 0)]))[0], - [ - qp.PauliRot(1.0, "ZZ", wires=[0, 1]), - qp.PauliRot(1.0, "ZZ", wires=[0, 2]), - qp.PauliRot(1.0, "ZZ", wires=[1, 2]), - ], - ], - ] - - @pytest.mark.parametrize( - ("cost", "gates"), - cost_layer_test_cases, - ) - def test_cost_layer_output(self, cost, gates): - """Tests that the gates of the cost layer is correct""" - - gamma = 1 - - with qp.queuing.AnnotatedQueue() as q: - out = qaoa.cost_layer(gamma, cost) - - expected = qp.ApproxTimeEvolution(cost, gamma, 1) - qp.assert_equal(out, expected) - - assert q.queue[0] is out - assert len(q) == 1 - decomp = out.decomposition() - - for i, j in zip(decomp, gates): - qp.assert_equal(i, j) - - -class TestIntegration: - """Test integration of the QAOA module with PennyLane""" - - def test_module_example(self, tol): - """Test the example in the QAOA module docstring""" - - # Defines the wires and the graph on which MaxCut is being performed - wires = range(3) - graph = Graph([(0, 1), (1, 2), (2, 0)]) - - # Defines the QAOA cost and mixer Hamiltonians - cost_h, mixer_h = qaoa.maxcut(graph) - - # Defines a layer of the QAOA ansatz from the cost and mixer Hamiltonians - def qaoa_layer(gamma, alpha): - qaoa.cost_layer(gamma, cost_h) - qaoa.mixer_layer(alpha, mixer_h) - - # Repeatedly applies layers of the QAOA ansatz - # pylint: disable=unused-argument - def circuit(params, **kwargs): - for w in wires: - qp.Hadamard(wires=w) - - qaoa_layer(params[0][0], params[1][0]) - qaoa_layer(params[0][1], params[1][1]) - - # Defines the device and the QAOA cost function - dev = qp.device("default.qubit", wires=len(wires)) - - @qp.qnode(dev) - def cost_function(params): - circuit(params) - return qp.expval(cost_h) - - res = cost_function([[1, 1], [1, 1]]) - expected = -1.8260274380964299 - - assert np.allclose(res, expected, atol=tol, rtol=0) - - def test_module_example_rx(self, tol): - """Test the example in the QAOA module docstring""" - - # Defines the wires and the graph on which MaxCut is being performed - wires = range(3) - - graph = rx.PyGraph() - graph.add_nodes_from([0, 1, 2]) - graph.add_edges_from([(0, 1, ""), (1, 2, ""), (2, 0, "")]) - - # Defines the QAOA cost and mixer Hamiltonians - cost_h, mixer_h = qaoa.maxcut(graph) - - # Defines a layer of the QAOA ansatz from the cost and mixer Hamiltonians - def qaoa_layer(gamma, alpha): - qaoa.cost_layer(gamma, cost_h) - qaoa.mixer_layer(alpha, mixer_h) - - # Repeatedly applies layers of the QAOA ansatz - # pylint: disable=unused-argument - def circuit(params, **kwargs): - for w in wires: - qp.Hadamard(wires=w) - - qaoa_layer(params[0][0], params[1][0]) - qaoa_layer(params[0][1], params[1][1]) - - # Defines the device and the QAOA cost function - dev = qp.device("default.qubit", wires=len(wires)) - - @qp.qnode(dev) - def cost_function(params): - circuit(params) - return qp.expval(cost_h) - - res = cost_function([[1, 1], [1, 1]]) - expected = -1.8260274380964299 - - assert np.allclose(res, expected, atol=tol, rtol=0) - - -# pylint: disable=too-many-public-methods -class TestCycles: - """Tests that ``cycle`` module functions are behaving correctly""" - - @pytest.mark.parametrize("g", [nx.lollipop_graph(4, 1), lollipop_graph_rx(4, 1)]) - def test_edges_to_wires(self, g): - """Test that edges_to_wires returns the correct mapping""" - r = edges_to_wires(g) - assert r == {(0, 1): 0, (0, 2): 1, (0, 3): 2, (1, 2): 3, (1, 3): 4, (2, 3): 5, (3, 4): 6} - - def test_edges_to_wires_error(self): - """Test that edges_to_wires raises ValueError""" - g = [1, 1, 1, 1] - with pytest.raises( - ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph" - ): - edges_to_wires(g) - - def test_edges_to_wires_rx(self): - """Test that edges_to_wires returns the correct mapping""" - g = rx.generators.directed_mesh_graph(4, [0, 1, 2, 3]) - r = edges_to_wires(g) - - assert r == { - (0, 1): 0, - (0, 2): 1, - (0, 3): 2, - (1, 0): 3, - (1, 2): 4, - (1, 3): 5, - (2, 0): 6, - (2, 1): 7, - (2, 3): 8, - (3, 0): 9, - (3, 1): 10, - (3, 2): 11, - } - - @pytest.mark.parametrize("g", [nx.lollipop_graph(4, 1), lollipop_graph_rx(4, 1)]) - def test_wires_to_edges(self, g): - """Test that wires_to_edges returns the correct mapping""" - r = wires_to_edges(g) - - assert r == {0: (0, 1), 1: (0, 2), 2: (0, 3), 3: (1, 2), 4: (1, 3), 5: (2, 3), 6: (3, 4)} - - def test_wires_to_edges_error(self): - """Test that wires_to_edges raises ValueError""" - g = [1, 1, 1, 1] - with pytest.raises( - ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph" - ): - wires_to_edges(g) - - def test_wires_to_edges_rx(self): - """Test that wires_to_edges returns the correct mapping""" - g = rx.generators.directed_mesh_graph(4, [0, 1, 2, 3]) - r = wires_to_edges(g) - - assert r == { - 0: (0, 1), - 1: (0, 2), - 2: (0, 3), - 3: (1, 0), - 4: (1, 2), - 5: (1, 3), - 6: (2, 0), - 7: (2, 1), - 8: (2, 3), - 9: (3, 0), - 10: (3, 1), - 11: (3, 2), - } - - @pytest.mark.parametrize( - "g", - [nx.complete_graph(4).to_directed(), rx.generators.directed_mesh_graph(4, [0, 1, 2, 3])], - ) - def test_partial_cycle_mixer_complete(self, g): - """Test if the _partial_cycle_mixer function returns the expected Hamiltonian for a fixed - example""" - edge = (0, 1) - - h = _partial_cycle_mixer(g, edge) - - ops_expected = [ - qp.PauliX(0) @ qp.PauliX(1) @ qp.PauliX(7), - qp.PauliY(0) @ qp.PauliY(1) @ qp.PauliX(7), - qp.PauliY(0) @ qp.PauliX(1) @ qp.PauliY(7), - qp.PauliX(0) @ qp.PauliY(1) @ qp.PauliY(7), - qp.PauliX(0) @ qp.PauliX(2) @ qp.PauliX(10), - qp.PauliY(0) @ qp.PauliY(2) @ qp.PauliX(10), - qp.PauliY(0) @ qp.PauliX(2) @ qp.PauliY(10), - qp.PauliX(0) @ qp.PauliY(2) @ qp.PauliY(10), - ] - coeffs_expected = [0.25, 0.25, 0.25, -0.25, 0.25, 0.25, 0.25, -0.25] - - assert h.coeffs == coeffs_expected - assert all(op.wires == op_e.wires for op, op_e in zip(h.ops, ops_expected)) - assert all(op.name == op_e.name for op, op_e in zip(h.ops, ops_expected)) - - @pytest.mark.parametrize( - "g", - [nx.complete_graph(4).to_directed(), rx.generators.directed_mesh_graph(4, [0, 1, 2, 3])], - ) - def test_partial_cycle_mixer_incomplete(self, g): - """Test if the _partial_cycle_mixer function returns the expected Hamiltonian for a fixed - example""" - g.remove_edge(2, 1) # remove an egde to make graph incomplete - edge = (0, 1) - - h = _partial_cycle_mixer(g, edge) - - ops_expected = [ - qp.PauliX(0) @ qp.PauliX(2) @ qp.PauliX(9), - qp.PauliY(0) @ qp.PauliY(2) @ qp.PauliX(9), - qp.PauliY(0) @ qp.PauliX(2) @ qp.PauliY(9), - qp.PauliX(0) @ qp.PauliY(2) @ qp.PauliY(9), - ] - coeffs_expected = [0.25, 0.25, 0.25, -0.25] - - assert h.coeffs == coeffs_expected - assert all(op.wires == op_e.wires for op, op_e in zip(h.ops, ops_expected)) - assert all(op.name == op_e.name for op, op_e in zip(h.ops, ops_expected)) - - @pytest.mark.parametrize("g", [nx.complete_graph(4), rx.generators.mesh_graph(4, [0, 1, 2, 3])]) - def test_partial_cycle_mixer_error(self, g): - """Test if the _partial_cycle_mixer raises ValueError""" - g.remove_edge(2, 1) # remove an egde to make graph incomplete - edge = (0, 1) - - # Find Hamiltonian and its matrix representation - with pytest.raises(ValueError, match="Input graph must be a nx.DiGraph or rx.PyDiGraph"): - _partial_cycle_mixer(g, edge) - - @pytest.mark.parametrize( - "g", - [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])], - ) - def test_cycle_mixer(self, g): - """Test if the cycle_mixer Hamiltonian maps valid cycles to valid cycles""" - - n_nodes = 3 - m = wires_to_edges(g) - n_wires = len(g.edge_list() if isinstance(g, rx.PyDiGraph) else g.edges) - - # Find Hamiltonian and its matrix representation - h = cycle_mixer(g) - h_matrix = np.real_if_close(matrix(h, n_wires).toarray()) - - # Decide which bitstrings are valid and which are invalid - valid_bitstrings_indx = [] - invalid_bitstrings_indx = [] - - for indx, bitstring in enumerate(itertools.product([0, 1], repeat=n_wires)): - wires = [i for i, bit in enumerate(bitstring) if bit == 1] - edges = [m[wire] for wire in wires] - - flows = [0 for i in range(n_nodes)] - - for start, end in edges: - flows[start] += 1 - flows[end] -= 1 - - # A bitstring is valid if the net flow is zero and we aren't the empty set or the set - # of all edges. Note that the max out-flow constraint is not imposed, which means we can - # pass through nodes more than once - if sum(np.abs(flows)) == 0 and 0 < len(edges) < n_wires: - valid_bitstrings_indx.append(indx) - else: - invalid_bitstrings_indx.append(indx) - - # Check that valid bitstrings map to a subset of the valid bitstrings - for indx in valid_bitstrings_indx: - column = h_matrix[:, indx] - destination_indxs = set(np.argwhere(column != 0).flatten()) - - assert destination_indxs.issubset(valid_bitstrings_indx) - - # Check that invalid bitstrings map to a subset of the invalid bitstrings - for indx in invalid_bitstrings_indx: - column = h_matrix[:, indx] - destination_indxs = set(np.argwhere(column != 0).flatten()) - - assert destination_indxs.issubset(invalid_bitstrings_indx) - - # Now consider a unitary generated by the Hamiltonian - h_matrix_e = expm(1j * h_matrix) - - # We expect non-zero transitions among the set of valid bitstrings, and no transitions - # outside - for indx in valid_bitstrings_indx: - column = h_matrix_e[:, indx] - destination_indxs = np.argwhere(column != 0).flatten().tolist() - assert destination_indxs == valid_bitstrings_indx - - # Check that invalid bitstrings transition within the set of invalid bitstrings - for indx in invalid_bitstrings_indx: - column = h_matrix_e[:, indx] - destination_indxs = set(np.argwhere(column != 0).flatten().tolist()) - assert destination_indxs.issubset(invalid_bitstrings_indx) - - @pytest.mark.parametrize( - "g", - [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])], - ) - def test_cycle_mixer_error(self, g): - """Test if the cycle_mixer raises ValueError""" - # Find Hamiltonian and its matrix representation - with pytest.raises(ValueError, match="Input graph must be a nx.DiGraph or rx.PyDiGraph"): - cycle_mixer(g) - - @pytest.mark.parametrize("g", [nx.lollipop_graph(3, 1), lollipop_graph_rx(3, 1)]) - def test_matrix(self, g): - """Test that the matrix function works as expected on a fixed example""" - h = qp.qaoa.bit_flip_mixer(g, 0) - - mat = matrix(h, 4) - mat_expected = np.array( - [ - [0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - ] - ) - - assert np.allclose(mat.toarray(), mat_expected) - - def test_matrix_rx(self): - """Test that the matrix function works as expected on a fixed example""" - g = rx.generators.star_graph(4, [0, 1, 2, 3]) - h = qp.qaoa.bit_flip_mixer(g, 0) - - mat = matrix(h, 4) - mat_expected = np.array( - [ - [0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - ] - ) - - assert np.allclose(mat.toarray(), mat_expected) - - @pytest.mark.parametrize( - "g", [nx.lollipop_graph(4, 1).to_directed(), lollipop_graph_rx(4, 1, to_directed=True)] - ) - def test_edges_to_wires_directed(self, g): - """Test that edges_to_wires returns the correct mapping on a directed graph""" - r = edges_to_wires(g) - - assert r == { - (0, 1): 0, - (0, 2): 1, - (0, 3): 2, - (1, 0): 3, - (1, 2): 4, - (1, 3): 5, - (2, 0): 6, - (2, 1): 7, - (2, 3): 8, - (3, 0): 9, - (3, 1): 10, - (3, 2): 11, - (3, 4): 12, - (4, 3): 13, - } - - @pytest.mark.parametrize( - "g", [nx.lollipop_graph(4, 1).to_directed(), lollipop_graph_rx(4, 1, to_directed=True)] - ) - def test_wires_to_edges_directed(self, g): - """Test that wires_to_edges returns the correct mapping on a directed graph""" - r = wires_to_edges(g) - - assert r == { - 0: (0, 1), - 1: (0, 2), - 2: (0, 3), - 3: (1, 0), - 4: (1, 2), - 5: (1, 3), - 6: (2, 0), - 7: (2, 1), - 8: (2, 3), - 9: (3, 0), - 10: (3, 1), - 11: (3, 2), - 12: (3, 4), - 13: (4, 3), - } - - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - def test_loss_hamiltonian_complete(self, g): - """Test if the loss_hamiltonian function returns the expected result on a - manually-calculated example of a 3-node complete digraph""" - if isinstance(g, rx.PyDiGraph): - edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(sorted(g.edge_list()))} - for k, v in edge_weight_data.items(): - g.update_edge(k[0], k[1], {"weight": v}) - else: - edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edges)} - for k, v in edge_weight_data.items(): - g[k[0]][k[1]]["weight"] = v - - h = loss_hamiltonian(g) - - expected_ops = [ - qp.PauliZ(0), - qp.PauliZ(1), - qp.PauliZ(2), - qp.PauliZ(3), - qp.PauliZ(4), - qp.PauliZ(5), - ] - expected_coeffs = [np.log(0.5), np.log(1), np.log(1.5), np.log(2), np.log(2.5), np.log(3)] - - assert np.allclose(expected_coeffs, h.coeffs) - assert all(op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)) - assert all(type(op) is type(exp) for op, exp in zip(h.ops, expected_ops)) - - def test_loss_hamiltonian_error(self): - """Test if the loss_hamiltonian function raises ValueError""" - with pytest.raises( - ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph" - ): - loss_hamiltonian([(0, 1), (1, 2), (0, 2)]) - - @pytest.mark.parametrize( - "g", [nx.lollipop_graph(4, 1).to_directed(), lollipop_graph_rx(4, 1, to_directed=True)] - ) - def test_loss_hamiltonian_incomplete(self, g): - """Test if the loss_hamiltonian function returns the expected result on a - manually-calculated example of a 4-node incomplete digraph""" - if isinstance(g, rx.PyDiGraph): - edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(sorted(g.edge_list()))} - for k, v in edge_weight_data.items(): - g.update_edge(k[0], k[1], {"weight": v}) - else: - edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edges)} - for k, v in edge_weight_data.items(): - g[k[0]][k[1]]["weight"] = v - h = loss_hamiltonian(g) - - expected_ops = [ - qp.PauliZ(0), - qp.PauliZ(1), - qp.PauliZ(2), - qp.PauliZ(3), - qp.PauliZ(4), - qp.PauliZ(5), - qp.PauliZ(6), - qp.PauliZ(7), - qp.PauliZ(8), - qp.PauliZ(9), - qp.PauliZ(10), - qp.PauliZ(11), - qp.PauliZ(12), - qp.PauliZ(13), - ] - expected_coeffs = [ - np.log(0.5), - np.log(1), - np.log(1.5), - np.log(2), - np.log(2.5), - np.log(3), - np.log(3.5), - np.log(4), - np.log(4.5), - np.log(5), - np.log(5.5), - np.log(6), - np.log(6.5), - np.log(7), - ] - - assert np.allclose(expected_coeffs, h.coeffs) - assert all(op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)) - assert all(type(op) is type(exp) for op, exp in zip(h.ops, expected_ops)) - - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - def test_self_loop_raises_error(self, g): - """Test graphs with self loop raises ValueError""" - - digraph_complete = nx.complete_graph(3).to_directed() - complete_edge_weight_data = { - edge: (i + 1) * 0.5 for i, edge in enumerate(digraph_complete.edges) - } - for _k, _v in complete_edge_weight_data.items(): - digraph_complete[_k[0]][_k[1]]["weight"] = _v - digraph_complete_rx = rx.generators.directed_mesh_graph(3, [0, 1, 2]) - complete_edge_weight_data = { - edge: (i + 1) * 0.5 for i, edge in enumerate(sorted(digraph_complete_rx.edge_list())) - } - - if isinstance(g, rx.PyDiGraph): - edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edges())} - for k, v in complete_edge_weight_data.items(): - g.update_edge(k[0], k[1], {"weight": v}) - g.add_edge(1, 1, "") # add self loop - - else: - edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edges)} - for k, v in edge_weight_data.items(): - g[k[0]][k[1]]["weight"] = v - g.add_edge(1, 1) # add self loop - - with pytest.raises(ValueError, match="Graph contains self-loops"): - loss_hamiltonian(g) - - def test_missing_edge_weight_data_raises_error(self): - """Test graphs with no edge weight data raises `KeyError`""" - g = nx.complete_graph(3).to_directed() - with pytest.raises(KeyError, match="does not contain weight data"): - loss_hamiltonian(g) - - def test_missing_edge_weight_data_without_weights(self): - """Test graphs with no edge weight data raises `KeyError`""" - g = rx.generators.mesh_graph(3, [0, 1, 2]) - with pytest.raises(TypeError, match="does not contain weight data"): - loss_hamiltonian(g) - - def test_square_hamiltonian_terms(self): - """Test if the _square_hamiltonian_terms function returns the expected result on a fixed - example""" - coeffs = [1, -1, -1, 1] - ops = [qp.I(0), qp.Z(0), qp.Z(1), qp.Z(3)] - - expected_coeffs = [4, -2, -2, 2, 2, -2, -2] - expected_ops = [ - qp.I(0), - qp.Z(0), - qp.Z(1), - qp.Z(3), - qp.Z(0) @ qp.Z(1), - qp.Z(0) @ qp.Z(3), - qp.Z(1) @ qp.Z(3), - ] - - squared_coeffs, squared_ops = _square_hamiltonian_terms(coeffs, ops) - - assert squared_coeffs == expected_coeffs - assert squared_ops == expected_ops - - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - def test_inner_out_flow_constraint_hamiltonian(self, g): - """Test if the _inner_out_flow_constraint_hamiltonian function returns the expected result - on a manually-calculated example of a 3-node complete digraph relative to the 0 node""" - h = _inner_out_flow_constraint_hamiltonian(g, 0) - expected_ops = [ - qp.Identity(0), - qp.PauliZ(1) @ qp.PauliZ(0), - qp.PauliZ(0), - qp.PauliZ(1), - ] - expected_coeffs = [2, 2, -2, -2] - - expected_hamiltonian = qp.Hamiltonian(expected_coeffs, expected_ops) - assert h == expected_hamiltonian - - @pytest.mark.parametrize("g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])]) - def test_inner_out_flow_constraint_hamiltonian_error(self, g): - """Test if the _inner_out_flow_constraint_hamiltonian function raises ValueError""" - with pytest.raises(ValueError, match=r"Input graph must be a nx.DiGraph or rx.PyDiGraph"): - _inner_out_flow_constraint_hamiltonian(g, 0) - - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - def test_inner_net_flow_constraint_hamiltonian(self, g): - """Test if the _inner_net_flow_constraint_hamiltonian function returns the expected result on a manually-calculated - example of a 3-node complete digraph relative to the 0 node""" - h = _inner_net_flow_constraint_hamiltonian(g, 0) - expected_ops = [ - qp.Identity(0), - qp.PauliZ(0) @ qp.PauliZ(1), - qp.PauliZ(0) @ qp.PauliZ(2), - qp.PauliZ(0) @ qp.PauliZ(4), - qp.PauliZ(1) @ qp.PauliZ(2), - qp.PauliZ(1) @ qp.PauliZ(4), - qp.PauliZ(2) @ qp.PauliZ(4), - ] - expected_coeffs = [4, 2, -2, -2, -2, -2, 2] - _, ops = h.terms() - non_zero_terms = [(coeff, op) for coeff, op in zip(h.coeffs, ops) if coeff != 0] - coeffs = [term[0] for term in non_zero_terms] - assert qp.math.allclose(coeffs, expected_coeffs) - non_zero_ops = [term[1] for term in non_zero_terms] - for op, expected_op in zip(non_zero_ops, expected_ops): - assert op.pauli_rep == expected_op.pauli_rep - - @pytest.mark.parametrize("g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])]) - def test_inner_net_flow_constraint_hamiltonian_error(self, g): - """Test if the _inner_net_flow_constraint_hamiltonian function returns raises ValueError""" - with pytest.raises(ValueError, match=r"Input graph must be a nx.DiGraph or rx.PyDiGraph"): - _inner_net_flow_constraint_hamiltonian(g, 0) - - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - def test_inner_out_flow_constraint_hamiltonian_non_complete(self, g): - """Test if the _inner_out_flow_constraint_hamiltonian function returns the expected result - on a manually-calculated example of a 3-node complete digraph relative to the 0 node, with - the (0, 1) edge removed""" - g.remove_edge(0, 1) - h = _inner_out_flow_constraint_hamiltonian(g, 0) - h = h.simplify() - expected_ops = [qp.Identity(0), qp.PauliZ(wires=[0])] - expected_coeffs = [0, 0] - - coeffs, ops = h.terms() - assert qp.math.allclose(expected_coeffs, coeffs) - for op, expected_op in zip(ops, expected_ops): - assert op.pauli_rep == expected_op.pauli_rep - - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - def test_inner_net_flow_constraint_hamiltonian_non_complete(self, g): - """Test if the _inner_net_flow_constraint_hamiltonian function returns the expected result on a manually-calculated - example of a 3-node complete digraph relative to the 0 node, with the (1, 0) edge removed""" - g.remove_edge(1, 0) - h = _inner_net_flow_constraint_hamiltonian(g, 0) - h = h.simplify() - expected_ops = [ - qp.Identity(0), - qp.PauliZ(0), - qp.PauliZ(1), - qp.PauliZ(3), - qp.PauliZ(0) @ qp.PauliZ(1), - qp.PauliZ(0) @ qp.PauliZ(3), - qp.PauliZ(1) @ qp.PauliZ(3), - ] - expected_coeffs = [4, -2, -2, 2, 2, -2, -2] - coeffs, ops = h.terms() - assert qp.math.allclose(coeffs, expected_coeffs) - for op, expected_op in zip(ops, expected_ops): - assert op.pauli_rep == expected_op.pauli_rep - - def test_out_flow_constraint_raises(self): - """Test the out-flow constraint function may raise an error.""" - - # pylint: disable=super-init-not-called - class OtherDirectedGraph(nx.DiGraph): - def __init__(self, *args, **kwargs): - pass - - g = OtherDirectedGraph() - with pytest.raises(ValueError, match="must be directed"): - out_flow_constraint(g) - - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - def test_out_flow_constraint(self, g): - """Test the out-flow constraint Hamiltonian is minimised by states that correspond to - subgraphs that only ever have 0 or 1 edge leaving each node - """ - h = out_flow_constraint(g) - m = wires_to_edges(g) - wires = len(g.edge_list() if isinstance(g, rx.PyDiGraph) else g.edges) - - # We use PL to find the energies corresponding to each possible bitstring - dev = qp.device("default.qubit", wires=wires) - - # pylint: disable=unused-argument - def states(basis_state, **kwargs): - qp.BasisState(basis_state, wires=range(wires)) - - @qp.qnode(dev) - def cost(params): - states(params) - return qp.expval(h) - - # Calculate the set of all bitstrings - bitstrings = itertools.product([0, 1], repeat=wires) - - # Calculate the corresponding energies - energies_bitstrings = ((cost(np.array(bitstring)), bitstring) for bitstring in bitstrings) - - for energy, bs in energies_bitstrings: - # convert binary string to wires then wires to edges - wires_ = tuple(i for i, s in enumerate(bs) if s != 0) - edges = tuple(m[w] for w in wires_) - - # find the number of edges leaving each node - if isinstance(g, rx.PyDiGraph): - num_edges_leaving_node = {node: 0 for node in g.nodes()} - else: - num_edges_leaving_node = {node: 0 for node in g.nodes} - for e in edges: - num_edges_leaving_node[e[0]] += 1 - - # check that if the max number of edges is <=1 it corresponds to a state that minimizes - # the out_flow_constraint Hamiltonian - if max(num_edges_leaving_node.values()) > 1: - assert energy > min(energies_bitstrings)[0] - elif max(num_edges_leaving_node.values()) <= 1: - assert energy == min(energies_bitstrings)[0] - - @pytest.mark.parametrize("g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])]) - def test_out_flow_constraint_undirected_raises_error(self, g): - """Test `out_flow_constraint` raises ValueError if input graph is not directed""" - with pytest.raises(ValueError): - out_flow_constraint(g) - - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - def test_net_flow_constraint(self, g): - """Test if the net_flow_constraint Hamiltonian is minimized by states that correspond to a - collection of edges with zero flow""" - h = net_flow_constraint(g) - m = wires_to_edges(g) - wires = len(g.edge_list() if isinstance(g, rx.PyDiGraph) else g.edges) - - # We use PL to find the energies corresponding to each possible bitstring - dev = qp.device("default.qubit", wires=wires) - - @qp.qnode(dev) - def cost(basis_state): - qp.BasisState(basis_state, wires=range(wires)) - return qp.expval(h) - - # Calculate the set of all bitstrings - states = itertools.product([0, 1], repeat=wires) - - # Calculate the corresponding energies - energies_states = ((cost(np.array(state)), state) for state in states) - - # We now have the energies of each bitstring/state. We also want to calculate the net flow of - # the corresponding edges - for energy, state in energies_states: - # This part converts from a binary string of wires selected to graph edges - wires_ = tuple(i for i, s in enumerate(state) if s != 0) - edges = tuple(m[w] for w in wires_) - - # Calculates the number of edges entering and leaving a given node - if isinstance(g, rx.PyDiGraph): - in_flows = np.zeros(len(g.nodes())) - out_flows = np.zeros(len(g.nodes())) - else: - in_flows = np.zeros(len(g.nodes)) - out_flows = np.zeros(len(g.nodes)) - - for e in edges: - in_flows[e[0]] += 1 - out_flows[e[1]] += 1 - - net_flow = np.sum(np.abs(in_flows - out_flows)) - - # The test requires that a set of edges with zero net flow must have a corresponding - # bitstring that minimized the energy of the Hamiltonian - if net_flow == 0: - assert energy == min(energies_states)[0] - else: - assert energy > min(energies_states)[0] - - @pytest.mark.parametrize("g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])]) - def test_net_flow_constraint_wrong_graph_type_raises_error(self, g): - """Test `net_flow_constraint` raises ValueError if input graph is not - the correct graph type""" - - with pytest.raises(ValueError, match="Input graph must be"): - net_flow_constraint(g) - - def test_net_flow_constraint_undirected_raises_error(self): - """Test the net-flow constraint function may raise an error.""" - - # pylint: disable=super-init-not-called - class OtherDirectedGraph(nx.DiGraph): - def __init__(self, *args, **kwargs): - pass - - g = OtherDirectedGraph() - with pytest.raises(ValueError, match="must be directed"): - net_flow_constraint(g) - - @pytest.mark.parametrize( - "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] - ) - def test_net_flow_and_out_flow_constraint(self, g): - """Test the combined net-flow and out-flow constraint Hamiltonian is minimised by states that correspond to subgraphs - that qualify as simple_cycles - """ - g = nx.complete_graph(3).to_directed() - h = net_flow_constraint(g) + out_flow_constraint(g) - m = wires_to_edges(g) - wires = len(g.edge_list() if isinstance(g, rx.PyDiGraph) else g.edges) - - # Find the energies corresponding to each possible bitstring - dev = qp.device("default.qubit", wires=wires) - - # pylint: disable=unused-argument - def states(basis_state, **kwargs): - qp.BasisState(basis_state, wires=range(wires)) - - @qp.qnode(dev) - def cost(params): - states(params) - return qp.expval(h) - - # Calculate the set of all bitstrings - bitstrings = itertools.product([0, 1], repeat=wires) - - # Calculate the corresponding energies - energies_bitstrings = ((cost(np.array(bitstring)), bitstring) for bitstring in bitstrings) - - def find_simple_cycle(list_of_edges): - """Returns True if list_of_edges contains a permutation corresponding to a simple cycle""" - permutations = list(itertools.permutations(list_of_edges)) - - for edges in permutations: - if edges[0][0] != edges[-1][-1]: # check first node is equal to last node - continue - all_nodes = [] - for edge in edges: - for n in edge: - all_nodes.append(n) - inner_nodes = all_nodes[ - 1:-1 - ] # find all nodes in all edges excluding the first and last nodes - nodes_out = [ - inner_nodes[i] for i in range(len(inner_nodes)) if i % 2 == 0 - ] # find the nodes each edge is leaving - node_in = [ - inner_nodes[i] for i in range(len(inner_nodes)) if i % 2 != 0 - ] # find the nodes each edge is entering - if nodes_out == node_in and ( - len([all_nodes[0]] + nodes_out) == len(set([all_nodes[0]] + nodes_out)) - ): # check that each edge connect to the next via a common node and that no node is crossed more than once - return True - return False - - for energy, bs in energies_bitstrings: - # convert binary string to wires then wires to edges - wires_ = tuple(i for i, s in enumerate(bs) if s != 0) - edges = tuple(m[w] for w in wires_) - - if len(edges) > 0 and find_simple_cycle(edges): - assert energy == min(energies_bitstrings)[0] - elif len(edges) > 0 and not find_simple_cycle(edges): - assert energy > min(energies_bitstrings)[0] From 14fbfcc12a6180337a473d3f23a71422006b7909 Mon Sep 17 00:00:00 2001 From: JerryChen97 Date: Thu, 1 Oct 2026 16:35:20 -0400 Subject: [PATCH 2/2] Point changelog entry at the actual PR number Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- doc/releases/changelog-dev.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index fd3eeb34e04..db246c0efba 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -1045,7 +1045,7 @@ ``max_independent_set``, ``min_vertex_cover``, ``max_clique``, ``max_weight_cycle``, ``bit_driver``, ``edge_driver``), the ansatz layers (``cost_layer``, ``mixer_layer``) and the ``pennylane.qaoa.cycle`` helpers. :class:`~.QAOAEmbedding` is unaffected. - [(#10248)](https://github.com/PennyLaneAI/pennylane/pull/10248) + [(#10249)](https://github.com/PennyLaneAI/pennylane/pull/10249) * The ``pennylane.noise`` module has been removed, including ``NoiseModel``, ``add_noise``, ``insert``, noise mitigation transforms (``mitigate_with_zne``,