Source code for pennylane.qaoa.mixers

# 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

# pylint: disable=unnecessary-lambda-assignment
import itertools
from collections.abc import Iterable
from typing import Union

import networkx as nx
import rustworkx as rx

import pennylane as qml
from pennylane.wires import Wires


[docs]def x_mixer(wires: Union[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 <https://arxiv.org/abs/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 = [qml.X(w) for w in wires] H = qml.Hamiltonian(coeffs, obs) # store the valuable information that all observables are in one commuting group H.grouping_indices = [list(range(len(H.ops)))] return H
[docs]def xy_mixer(graph: Union[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) <https://doi.org/10.3390/a12020034>`__. 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] """ if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( f"Input graph must be a nx.Graph or rx.PyGraph object, got {type(graph).__name__}" ) 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. get_nvalue = lambda i: graph.nodes()[i] if is_rx else i coeffs = 2 * [0.5 for e in edges] obs = [] for node1, node2 in edges: obs.append(qml.X(get_nvalue(node1)) @ qml.X(get_nvalue(node2))) obs.append(qml.Y(get_nvalue(node1)) @ qml.Y(get_nvalue(node2))) return qml.Hamiltonian(coeffs, obs)
[docs]def bit_flip_mixer(graph: Union[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 <https://en.wikipedia.org/wiki/Degree_(graph_theory)>`__ of vertex :math:`v`, and :math:`N(v)` is the `neighbourhood <https://en.wikipedia.org/wiki/Neighbourhood_(graph_theory)>`__ 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) <https://doi.org/10.3390/a12020034>`__. 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)) ) """ if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( f"Input graph must be a nx.Graph or rx.PyGraph object, got {type(graph).__name__}" ) 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. get_nvalue = lambda i: 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 = [[qml.X(get_nvalue(i))]] + [ [qml.Identity(get_nvalue(n)), qml.Z(get_nvalue(n))] for n in neighbours ] n_coeffs = [[1, sign] for _ in neighbours] final_terms = ( [qml.prod(*list(m)).simplify() for m in itertools.product(*n_terms)] if qml.operation.active_new_opmath() else [qml.operation.Tensor(*list(m)).prune() 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 qml.Hamiltonian(coeffs, terms)