Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add max_weight_cycle helper function #1283

Merged
merged 25 commits into from
May 10, 2021
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
[(#1213)](https://github.com/PennyLaneAI/pennylane/pull/1213)
[(#1220)](https://github.com/PennyLaneAI/pennylane/pull/1220)
[(#1214)](https://github.com/PennyLaneAI/pennylane/pull/1214)

[(#1283)](https://github.com/PennyLaneAI/pennylane/pull/1283)

* Adds `QubitCarry` and `QubitSum` operations for basic arithmetic.
[(#1169)](https://github.com/PennyLaneAI/pennylane/pull/1169)

Expand Down
1 change: 1 addition & 0 deletions pennylane/qaoa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
from .mixers import *
from .cost import *
from .layers import *
import pennylane.qaoa.cycle
151 changes: 151 additions & 0 deletions pennylane/qaoa/cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,3 +471,154 @@ def max_clique(graph, constrained=True):
mixer_h = qaoa.x_mixer(graph.nodes)

return (cost_h, mixer_h)


def max_weight_cycle(graph, constrained=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 <https://1qbit.com/whitepaper/arbitrage/>`__ 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} x_{ij} c_{ij}

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 maximimize :math:`P`, subject to selecting the :math:`x_{ij}` so that
our subset of edges composes a `cycle <https://en.wikipedia.org/wiki/Cycle_(graph_theory)>`__.

Args:
graph (nx.Graph): 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

.. UsageDetails::

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 <https://arxiv.org/abs/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 qml
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 = qml.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.
"""
if not isinstance(graph, nx.Graph):
raise ValueError("Input graph must be a nx.Graph, got {}".format(type(graph).__name__))

mapping = qaoa.cycle.wires_to_edges(graph)

if constrained:
return (qaoa.cycle.loss_hamiltonian(graph), qaoa.cycle.cycle_mixer(graph), mapping)

cost_h = qaoa.cycle.loss_hamiltonian(graph) + 3 * (
qaoa.cycle.net_flow_constraint(graph) + qaoa.cycle.out_flow_constraint(graph)
)
mixer_h = qaoa.x_mixer(mapping.keys())

return (cost_h, mixer_h, mapping)
29 changes: 15 additions & 14 deletions pennylane/qaoa/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import networkx as nx
import numpy as np
import pennylane as qml
from pennylane.vqe import Hamiltonian
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doing this prevents problems with importing into PL.



def edges_to_wires(graph: nx.Graph) -> Dict[Tuple, int]:
Expand Down Expand Up @@ -79,7 +80,7 @@ def wires_to_edges(graph: nx.Graph) -> Dict[int, Tuple]:
return {i: edge for i, edge in enumerate(graph.edges)}


def cycle_mixer(graph: nx.DiGraph) -> qml.Hamiltonian:
def cycle_mixer(graph: nx.DiGraph) -> Hamiltonian:
r"""Calculates the cycle-mixer Hamiltonian.

Following methods outlined `here <https://arxiv.org/pdf/1709.03489.pdf>`__, the
Expand Down Expand Up @@ -132,15 +133,15 @@ def cycle_mixer(graph: nx.DiGraph) -> qml.Hamiltonian:
Returns:
qml.Hamiltonian: the cycle-mixer Hamiltonian
"""
hamiltonian = qml.Hamiltonian([], [])
hamiltonian = Hamiltonian([], [])

for edge in graph.edges:
hamiltonian += _partial_cycle_mixer(graph, edge)

return hamiltonian


def _partial_cycle_mixer(graph: nx.DiGraph, edge: Tuple) -> qml.Hamiltonian:
def _partial_cycle_mixer(graph: nx.DiGraph, edge: Tuple) -> Hamiltonian:
r"""Calculates the partial cycle-mixer Hamiltonian for a specific edge.

For an edge :math:`(i, j)`, this function returns:
Expand Down Expand Up @@ -184,10 +185,10 @@ def _partial_cycle_mixer(graph: nx.DiGraph, edge: Tuple) -> qml.Hamiltonian:

coeffs.extend([0.25, 0.25, 0.25, -0.25])

return qml.Hamiltonian(coeffs, ops)
return Hamiltonian(coeffs, ops)


def loss_hamiltonian(graph: nx.Graph) -> qml.Hamiltonian:
def loss_hamiltonian(graph: nx.Graph) -> Hamiltonian:
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
Expand Down Expand Up @@ -271,7 +272,7 @@ def loss_hamiltonian(graph: nx.Graph) -> qml.Hamiltonian:
coeffs.append(np.log(weight))
ops.append(qml.PauliZ(wires=edges_to_qubits[edge]))

return qml.Hamiltonian(coeffs, ops)
return Hamiltonian(coeffs, ops)


def _square_hamiltonian_terms(
Expand Down Expand Up @@ -308,7 +309,7 @@ def _square_hamiltonian_terms(
return squared_coeffs, squared_ops


def out_flow_constraint(graph: nx.DiGraph) -> qml.Hamiltonian:
def out_flow_constraint(graph: nx.DiGraph) -> Hamiltonian:
r"""Calculates the `out flow constraint <https://1qbit.com/whitepaper/arbitrage/>`__
Hamiltonian for the maximum-weighted cycle problem.

Expand Down Expand Up @@ -346,15 +347,15 @@ def out_flow_constraint(graph: nx.DiGraph) -> qml.Hamiltonian:
if not hasattr(graph, "out_edges"):
raise ValueError("Input graph must be directed")

hamiltonian = qml.Hamiltonian([], [])
hamiltonian = Hamiltonian([], [])

for node in graph.nodes:
hamiltonian += _inner_out_flow_constraint_hamiltonian(graph, node)

return hamiltonian


def net_flow_constraint(graph: nx.DiGraph) -> qml.Hamiltonian:
def net_flow_constraint(graph: nx.DiGraph) -> Hamiltonian:
r"""Calculates the `net flow constraint <https://doi.org/10.1080/0020739X.2010.526248>`__
Hamiltonian for the maximum-weighted cycle problem.

Expand Down Expand Up @@ -391,15 +392,15 @@ def net_flow_constraint(graph: nx.DiGraph) -> qml.Hamiltonian:
if not hasattr(graph, "in_edges") or not hasattr(graph, "out_edges"):
raise ValueError("Input graph must be directed")

hamiltonian = qml.Hamiltonian([], [])
hamiltonian = Hamiltonian([], [])

for node in graph.nodes:
hamiltonian += _inner_net_flow_constraint_hamiltonian(graph, node)

return hamiltonian


def _inner_out_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> qml.Hamiltonian:
def _inner_out_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> Hamiltonian:
r"""Calculates the inner portion of the Hamiltonian in :func:`out_flow_constraint`.
For a given :math:`i`, this function returns:

Expand Down Expand Up @@ -438,13 +439,13 @@ def _inner_out_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> qml.Hamil
coeffs.append(d * (d - 2))
ops.append(qml.Identity(0))

H = qml.Hamiltonian(coeffs, ops)
H = Hamiltonian(coeffs, ops)
H.simplify()

return H


def _inner_net_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> qml.Hamiltonian:
def _inner_net_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> Hamiltonian:
r"""Calculates the squared inner portion of the Hamiltonian in :func:`net_flow_constraint`.


Expand Down Expand Up @@ -484,7 +485,7 @@ def _inner_net_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> qml.Hamil
ops.append(qml.PauliZ(wires))

coeffs, ops = _square_hamiltonian_terms(coeffs, ops)
H = qml.Hamiltonian(coeffs, ops)
H = Hamiltonian(coeffs, ops)
H.simplify()

return H
Loading