From 264cb7abf4f68073f4872bfc8222d4ed2d134cee Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Fri, 15 Oct 2021 10:40:53 -0400 Subject: [PATCH 01/22] Add retworkx as pl-requirements --- requirements-ci.txt | 1 + requirements.txt | 1 + setup.py | 1 + 3 files changed, 3 insertions(+) diff --git a/requirements-ci.txt b/requirements-ci.txt index 27a55d2a73e..07bec466c24 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -3,6 +3,7 @@ scipy cvxpy cvxopt networkx +retworkx tensornetwork==0.3 autograd toml diff --git a/requirements.txt b/requirements.txt index 25a996ac7cb..a87ca6aef77 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ cvxpy==1.1 cvxopt==1.2 cachetools==4.2.2 networkx==2.6 +retworkx==0.10.2 tensornetwork==0.3 autograd==1.3 toml==0.10 diff --git a/setup.py b/setup.py index e6963555c5a..fe9a03a4779 100644 --- a/setup.py +++ b/setup.py @@ -21,6 +21,7 @@ "numpy", "scipy", "networkx", + "retworkx", "autograd", "toml", "appdirs", From 5c33c3e18d9f873b87ea042815256ed27b386ce2 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Mon, 18 Oct 2021 18:52:45 -0400 Subject: [PATCH 02/22] Add RX support to qaoa/cycle.py --- pennylane/qaoa/cycle.py | 150 +++++++++++++++++++++++++++++++--------- 1 file changed, 119 insertions(+), 31 deletions(-) diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index b0d465bd327..4d5e03e6321 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -18,12 +18,13 @@ import itertools from typing import Dict, Tuple, Iterable, List import networkx as nx +from networkx.generators.expanders import paley_graph import numpy as np import pennylane as qml from pennylane.ops import Hamiltonian +import retworkx as rx - -def edges_to_wires(graph: nx.Graph) -> Dict[Tuple, int]: +def edges_to_wires(graph) -> Dict[Tuple, int]: r"""Maps the edges of a graph to corresponding wires. **Example** @@ -43,16 +44,33 @@ def edges_to_wires(graph: nx.Graph) -> Dict[Tuple, int]: (3, 1): 10, (3, 2): 11} + >>> g = rx.generators.directed_mesh_graph(4) + >>> edges_to_wires(g) + {(0, 1): 0, + (1, 0): 1, + (0, 2): 2, + (2, 0): 3, + (0, 3): 4, + (3, 0): 5, + (1, 2): 6, + (2, 1): 7, + (1, 3): 8, + (3, 1): 9, + (2, 3): 10, + (3, 2): 11} + Args: - graph (nx.Graph): the graph specifying possible edges + 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 """ - return {edge: i for i, edge in enumerate(graph.edges)} + if isinstance(graph, nx.Graph): + return {edge: i for i, edge in enumerate(graph.edges)} + elif isinstance(graph, rx.PyGraph) or isinstance(graph, rx.PyDiGraph): + return {edge: i for i, edge in enumerate(graph.edge_list())} - -def wires_to_edges(graph: nx.Graph) -> Dict[int, Tuple]: +def wires_to_edges(graph) -> Dict[int, Tuple]: r"""Maps the wires of a register of qubits to corresponding edges. **Example** @@ -72,16 +90,33 @@ def wires_to_edges(graph: nx.Graph) -> Dict[int, Tuple]: 10: (3, 1), 11: (3, 2)} + >>> g = rx.generators.directed_mesh_graph(4) + >>> wires_to_edges(g) + {0: (0, 1), + 1: (1, 0), + 2: (0, 2), + 3: (2, 0), + 4: (0, 3), + 5: (3, 0), + 6: (1, 2), + 7: (2, 1), + 8: (1, 3), + 9: (3, 1), + 10: (2, 3), + 11: (3, 2)} + Args: - graph (nx.Graph): the graph specifying possible edges + 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 """ - return {i: edge for i, edge in enumerate(graph.edges)} - + if isinstance(graph, nx.Graph): + return {i: edge for i, edge in enumerate(graph.edges)} + elif isinstance(graph, rx.PyGraph) or isinstance(graph, rx.PyDiGraph): + return {i: edge for i, edge in enumerate(graph.edge_list())} -def cycle_mixer(graph: nx.DiGraph) -> Hamiltonian: +def cycle_mixer(graph) -> Hamiltonian: r"""Calculates the cycle-mixer Hamiltonian. Following methods outlined `here `__, the @@ -128,21 +163,51 @@ def cycle_mixer(graph: nx.DiGraph) -> Hamiltonian: + (0.25) [Y5 Y4 X0] + (0.25) [Y5 X4 Y0] + >>> import retworkx as rx + >>> g = rx.generators.directed_mesh_graph(3) + >>> h_m = cycle_mixer(g) + >>> print(h_m) + (-0.25) [X0 Y2 Y5] + + (-0.25) [X1 Y4 Y3] + + (-0.25) [X2 Y0 Y4] + + (-0.25) [X3 Y5 Y1] + + (-0.25) [X4 Y1 Y2] + + (-0.25) [X5 Y3 Y0] + + (0.25) [X0 X2 X5] + + (0.25) [Y0 Y2 X5] + + (0.25) [Y0 X2 Y5] + + (0.25) [X1 X4 X3] + + (0.25) [Y1 Y4 X3] + + (0.25) [Y1 X4 Y3] + + (0.25) [X2 X0 X4] + + (0.25) [Y2 Y0 X4] + + (0.25) [Y2 X0 Y4] + + (0.25) [X3 X5 X1] + + (0.25) [Y3 Y5 X1] + + (0.25) [Y3 X5 Y1] + + (0.25) [X4 X1 X2] + + (0.25) [Y4 Y1 X2] + + (0.25) [Y4 X1 Y2] + + (0.25) [X5 X3 X0] + + (0.25) [Y5 Y3 X0] + + (0.25) [Y5 X3 Y0] + Args: - graph (nx.DiGraph): the directed graph specifying possible edges + graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges Returns: qml.Hamiltonian: the cycle-mixer Hamiltonian """ hamiltonian = Hamiltonian([], []) + graph_edges = graph.edge_list() if isinstance(graph, rx.PyDiGraph) else graph.edges - for edge in graph.edges: + for edge in graph_edges: hamiltonian += _partial_cycle_mixer(graph, edge) return hamiltonian -def _partial_cycle_mixer(graph: nx.DiGraph, edge: Tuple) -> Hamiltonian: +def _partial_cycle_mixer(graph, edge: Tuple) -> Hamiltonian: r"""Calculates the partial cycle-mixer Hamiltonian for a specific edge. For an edge :math:`(i, j)`, this function returns: @@ -153,7 +218,7 @@ def _partial_cycle_mixer(graph: nx.DiGraph, edge: Tuple) -> Hamiltonian: 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): the directed graph specifying possible edges + graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges edge (tuple): a fixed edge Returns: @@ -163,11 +228,13 @@ def _partial_cycle_mixer(graph: nx.DiGraph, edge: Tuple) -> Hamiltonian: ops = [] edges_to_qubits = edges_to_wires(graph) + graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes + graph_edges = graph.edge_list() if isinstance(graph, rx.PyDiGraph) else graph.edges - for node in graph.nodes: + 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: + if node not in edge and out_edge in graph_edges and in_edge in graph_edges: wire = edges_to_qubits[edge] out_wire = edges_to_qubits[out_edge] in_wire = edges_to_qubits[in_edge] @@ -189,7 +256,7 @@ def _partial_cycle_mixer(graph: nx.DiGraph, edge: Tuple) -> Hamiltonian: return Hamiltonian(coeffs, ops) -def loss_hamiltonian(graph: nx.Graph) -> Hamiltonian: +def loss_hamiltonian(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 @@ -243,8 +310,22 @@ def loss_hamiltonian(graph: nx.Graph) -> Hamiltonian: + (0.9162907318741551) [Z4] + (1.0986122886681098) [Z5] + >>> import retworkx as rx + >>> g = rx.generators.directed_mesh_graph(3) + >>> edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edge_list())} + >>> for k, v in edge_weight_data.items(): + g.update_edge(k[0], k[1], v) + >>> h = loss_hamiltonian(g) + >>> print(h) + (-0.6931471805599453) [Z0] + + (0.0) [Z1] + + (0.4054651081081644) [Z2] + + (0.6931471805599453) [Z3] + + (0.9162907318741551) [Z4] + + (1.0986122886681098) [Z5] + Args: - graph (nx.Graph): the graph specifying possible edges + graph (nx.Graph or rx.PyGraph): the graph specifying possible edges Returns: qml.Hamiltonian: the loss Hamiltonian @@ -257,7 +338,7 @@ def loss_hamiltonian(graph: nx.Graph) -> Hamiltonian: coeffs = [] ops = [] - edges_data = graph.edges(data=True) + edges_data = graph.weighted_edge_list() if isinstance(graph, rx.PyGraph) else graph.edges(data=True) for edge_data in edges_data: edge = edge_data[:2] @@ -315,7 +396,7 @@ def _square_hamiltonian_terms( return squared_coeffs, squared_ops -def out_flow_constraint(graph: nx.DiGraph) -> Hamiltonian: +def out_flow_constraint(graph) -> Hamiltonian: r"""Calculates the `out flow constraint `__ Hamiltonian for the maximum-weighted cycle problem. @@ -342,7 +423,7 @@ def out_flow_constraint(graph: nx.DiGraph) -> Hamiltonian: using :func:`~.edges_to_wires`. Args: - graph (nx.DiGraph): the directed graph specifying possible edges + graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges Returns: qml.Hamiltonian: the out flow constraint Hamiltonian @@ -350,18 +431,22 @@ def out_flow_constraint(graph: nx.DiGraph) -> Hamiltonian: Raises: ValueError: if the input graph is not directed """ - if not hasattr(graph, "out_edges"): + if isinstance(graph, nx.DiGraph) and not hasattr(graph, "out_edges"): raise ValueError("Input graph must be directed") + elif not isinstance(graph, rx.PyDiGraph): + raise ValueError("Input graph must be directed") + hamiltonian = Hamiltonian([], []) + graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes - for node in graph.nodes: + for node in graph_nodes: hamiltonian += _inner_out_flow_constraint_hamiltonian(graph, node) return hamiltonian -def net_flow_constraint(graph: nx.DiGraph) -> Hamiltonian: +def net_flow_constraint(graph) -> Hamiltonian: r"""Calculates the `net flow constraint `__ Hamiltonian for the maximum-weighted cycle problem. @@ -387,7 +472,7 @@ def net_flow_constraint(graph: nx.DiGraph) -> Hamiltonian: Args: - graph (nx.DiGraph): the directed graph specifying possible edges + graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges Returns: qml.Hamiltonian: the net-flow constraint Hamiltonian @@ -395,18 +480,21 @@ def net_flow_constraint(graph: nx.DiGraph) -> Hamiltonian: Raises: ValueError: if the input graph is not directed """ - if not hasattr(graph, "in_edges") or not hasattr(graph, "out_edges"): + if isinstance(graph, nx.DiGraph) and (not hasattr(graph, "in_edges") or not hasattr(graph, "out_edges")): + raise ValueError("Input graph must be directed") + elif not isinstance(graph, rx.PyDiGraph): raise ValueError("Input graph must be directed") hamiltonian = Hamiltonian([], []) + graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes - for node in 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, node) -> Hamiltonian: +def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: r"""Calculates the inner portion of the Hamiltonian in :func:`out_flow_constraint`. For a given :math:`i`, this function returns: @@ -417,7 +505,7 @@ def _inner_out_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> Hamiltoni ( \sum_{j,(i,j)\in E}\hat{Z}_{ij} )^{2} Args: - graph (nx.DiGraph): the directed graph specifying possible edges + graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges node: a fixed node Returns: @@ -453,7 +541,7 @@ def _inner_out_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> Hamiltoni return H -def _inner_net_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> Hamiltonian: +def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: r"""Calculates the squared inner portion of the Hamiltonian in :func:`net_flow_constraint`. @@ -465,7 +553,7 @@ def _inner_net_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> Hamiltoni \sum_{j, (i, j) \in E} Z_{ij} + \sum_{j, (j, i) \in E} Z_{ji} \right)^{2}. Args: - graph (nx.DiGraph): the directed graph specifying possible edges + graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges node: a fixed node Returns: From 17d0edb1cd1c9794ad851bfee10db670cde00ff7 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Tue, 19 Oct 2021 10:56:11 -0400 Subject: [PATCH 03/22] Update RX in qaoa/cycle.py --- pennylane/qaoa/cycle.py | 104 +++++++++++++++++++++++++--------------- 1 file changed, 66 insertions(+), 38 deletions(-) diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index 4d5e03e6321..8f754a86462 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -18,11 +18,10 @@ import itertools from typing import Dict, Tuple, Iterable, List import networkx as nx -from networkx.generators.expanders import paley_graph +import retworkx as rx import numpy as np import pennylane as qml from pennylane.ops import Hamiltonian -import retworkx as rx def edges_to_wires(graph) -> Dict[Tuple, int]: r"""Maps the edges of a graph to corresponding wires. @@ -47,17 +46,17 @@ def edges_to_wires(graph) -> Dict[Tuple, int]: >>> g = rx.generators.directed_mesh_graph(4) >>> edges_to_wires(g) {(0, 1): 0, - (1, 0): 1, - (0, 2): 2, - (2, 0): 3, - (0, 3): 4, - (3, 0): 5, - (1, 2): 6, - (2, 1): 7, - (1, 3): 8, - (3, 1): 9, - (2, 3): 10, - (3, 2): 11} + (1, 0): 1, + (0, 2): 2, + (2, 0): 3, + (0, 3): 4, + (3, 0): 5, + (1, 2): 6, + (2, 1): 7, + (1, 3): 8, + (3, 1): 9, + (2, 3): 10, + (3, 2): 11} Args: graph (nx.Graph or rx.PyGraph or rx.PyDiGraph): the graph specifying possible edges @@ -67,8 +66,11 @@ def edges_to_wires(graph) -> Dict[Tuple, int]: """ if isinstance(graph, nx.Graph): return {edge: i for i, edge in enumerate(graph.edges)} - elif isinstance(graph, rx.PyGraph) or isinstance(graph, rx.PyDiGraph): + elif isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): return {edge: i for i, edge in enumerate(graph.edge_list())} + else: + raise ValueError("Input graph must be a nx.Graph, rx.Py(Di)Graph, got {}".format(type(graph).__name__)) + def wires_to_edges(graph) -> Dict[int, Tuple]: r"""Maps the wires of a register of qubits to corresponding edges. @@ -93,28 +95,30 @@ def wires_to_edges(graph) -> Dict[int, Tuple]: >>> g = rx.generators.directed_mesh_graph(4) >>> wires_to_edges(g) {0: (0, 1), - 1: (1, 0), - 2: (0, 2), - 3: (2, 0), - 4: (0, 3), - 5: (3, 0), - 6: (1, 2), - 7: (2, 1), - 8: (1, 3), - 9: (3, 1), - 10: (2, 3), - 11: (3, 2)} + 1: (1, 0), + 2: (0, 2), + 3: (2, 0), + 4: (0, 3), + 5: (3, 0), + 6: (1, 2), + 7: (2, 1), + 8: (1, 3), + 9: (3, 1), + 10: (2, 3), + 11: (3, 2)} Args: - graph (nx.Graph or rx.PyGraph or rx.PyDiGraph): the graph specifying possible edges + graph (nx.Graph, rx.PyGraph, or rx.PyDiGraph): the graph specifying possible edges Returns: Dict[Tuple, int]: a mapping from wires to graph edges """ if isinstance(graph, nx.Graph): return {i: edge for i, edge in enumerate(graph.edges)} - elif isinstance(graph, rx.PyGraph) or isinstance(graph, rx.PyDiGraph): + elif isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): return {i: edge for i, edge in enumerate(graph.edge_list())} + else: + raise ValueError("Input graph must be a nx.Graph or rx.Py(Di)Graph, got {}".format(type(graph).__name__)) def cycle_mixer(graph) -> Hamiltonian: r"""Calculates the cycle-mixer Hamiltonian. @@ -167,7 +171,7 @@ def cycle_mixer(graph) -> Hamiltonian: >>> g = rx.generators.directed_mesh_graph(3) >>> h_m = cycle_mixer(g) >>> print(h_m) - (-0.25) [X0 Y2 Y5] + (-0.25) [X0 Y2 Y5] + (-0.25) [X1 Y4 Y3] + (-0.25) [X2 Y0 Y4] + (-0.25) [X3 Y5 Y1] @@ -198,6 +202,9 @@ def cycle_mixer(graph) -> Hamiltonian: Returns: qml.Hamiltonian: the cycle-mixer Hamiltonian """ + if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): + raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + hamiltonian = Hamiltonian([], []) graph_edges = graph.edge_list() if isinstance(graph, rx.PyDiGraph) else graph.edges @@ -224,6 +231,9 @@ def _partial_cycle_mixer(graph, edge: Tuple) -> Hamiltonian: Returns: qml.Hamiltonian: the partial cycle-mixer Hamiltonian """ + if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): + raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + coeffs = [] ops = [] @@ -314,7 +324,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: >>> g = rx.generators.directed_mesh_graph(3) >>> edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edge_list())} >>> for k, v in edge_weight_data.items(): - g.update_edge(k[0], k[1], v) + g.update_edge(k[0], k[1], {"weight": v}) >>> h = loss_hamiltonian(g) >>> print(h) (-0.6931471805599453) [Z0] @@ -325,7 +335,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: + (1.0986122886681098) [Z5] Args: - graph (nx.Graph or rx.PyGraph): the graph specifying possible edges + graph (nx.Graph, rx.PyGraph or rx.PyDiGraph): the graph specifying possible edges Returns: qml.Hamiltonian: the loss Hamiltonian @@ -334,11 +344,14 @@ def loss_hamiltonian(graph) -> Hamiltonian: ValueError: if the graph contains self-loops KeyError: if one or more edges do not contain weight data """ + if not isinstance(graph, (nx.Graph, rx.PyGraph, rx.PyDiGraph)): + raise ValueError("Input graph must be a nx.Graph or rx.Py(Di)Graph, got {}".format(type(graph).__name__)) + edges_to_qubits = edges_to_wires(graph) coeffs = [] ops = [] - edges_data = graph.weighted_edge_list() if isinstance(graph, rx.PyGraph) else graph.edges(data=True) + edges_data = graph.weighted_edge_list() if isinstance(graph, (rx.PyGraph, rx.PyDiGraph)) else graph.edges(data=True) for edge_data in edges_data: edge = edge_data[:2] @@ -431,11 +444,11 @@ def out_flow_constraint(graph) -> Hamiltonian: Raises: ValueError: if the input graph is not directed """ - if isinstance(graph, nx.DiGraph) and not hasattr(graph, "out_edges"): - raise ValueError("Input graph must be directed") - elif not isinstance(graph, rx.PyDiGraph): - raise ValueError("Input graph must be directed") + if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): + raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + if isinstance(graph, (nx.DiGraph, rx.PyDiGraph)) and not hasattr(graph, "out_edges"): + raise ValueError("Input graph must be directed") hamiltonian = Hamiltonian([], []) graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes @@ -480,11 +493,12 @@ def net_flow_constraint(graph) -> Hamiltonian: Raises: ValueError: if the input graph is not directed """ - if isinstance(graph, nx.DiGraph) and (not hasattr(graph, "in_edges") or not hasattr(graph, "out_edges")): - raise ValueError("Input graph must be directed") - elif not isinstance(graph, rx.PyDiGraph): + 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("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + hamiltonian = Hamiltonian([], []) graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes @@ -511,6 +525,9 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: Returns: qml.Hamiltonian: The inner part of the out-flow constraint Hamiltonian. """ + if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): + raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + coeffs = [] ops = [] @@ -519,6 +536,8 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: d = len(out_edges) for edge in out_edges: + if len(edge) > 2: + edge = tuple(edge[:2]) wire = (edges_to_qubits[edge],) coeffs.append(1) ops.append(qml.PauliZ(wire)) @@ -526,6 +545,8 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: coeffs, ops = _square_hamiltonian_terms(coeffs, ops) for edge in out_edges: + if len(edge) > 2: + edge = tuple(edge[:2]) wire = (edges_to_qubits[edge],) coeffs.append(-2 * (d - 1)) ops.append(qml.PauliZ(wire)) @@ -559,6 +580,9 @@ def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: Returns: qml.Hamiltonian: The inner part of the net-flow constraint Hamiltonian. """ + if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): + raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + edges_to_qubits = edges_to_wires(graph) coeffs = [] @@ -571,11 +595,15 @@ def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: ops.append(qml.Identity(0)) for edge in out_edges: + if len(edge) > 2: + edge = tuple(edge[:2]) wires = (edges_to_qubits[edge],) coeffs.append(-1) ops.append(qml.PauliZ(wires)) for edge in in_edges: + if len(edge) > 2: + edge = tuple(edge[:2]) wires = (edges_to_qubits[edge],) coeffs.append(1) ops.append(qml.PauliZ(wires)) From 91b1ed60aff615fc8473fb74b4ff9eee88bbf318 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Tue, 19 Oct 2021 11:15:54 -0400 Subject: [PATCH 04/22] Add RX support to qaoa/cost.py --- pennylane/qaoa/cost.py | 94 +++++++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py index 24b82186045..72e212a1dad 100644 --- a/pennylane/qaoa/cost.py +++ b/pennylane/qaoa/cost.py @@ -17,6 +17,8 @@ """ import networkx as nx +from networkx.algorithms.similarity import graph_edit_distance +import retworkx as rx import pennylane as qml from pennylane import qaoa @@ -81,7 +83,7 @@ def edge_driver(graph, reward): See usage details for more information. Args: - graph (nx.Graph): The graph on which the Hamiltonian is defined + graph (nx.Graph or nx.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: @@ -100,6 +102,19 @@ def edge_driver(graph, reward): + (0.25) [Z0 Z1] + (0.25) [Z1 Z2] + >>> import retworkx as rx + >>> g = rx.PyGraph() + >>> g.add_nodes_from([0, 1, 2]) + >>> g.add_edges_from([(0, 1,""), (1,2,"")]) + >>> hamiltonian = qaoa.edge_driver(graph, ["11", "10", "01"]) + >>> print(hamiltonian) + (0.25) [Z0] + + (0.25) [Z1] + + (0.25) [Z1] + + (0.25) [Z2] + + (0.25) [Z0 Z1] + + (0.25) [Z1 Z2] + 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: @@ -159,15 +174,17 @@ def edge_driver(graph, reward): "'reward' cannot contain either '10' or '01', must contain neither or both." ) - if not isinstance(graph, nx.Graph): - raise ValueError("Input graph must be a nx.Graph, got {}".format(type(graph).__name__)) + if not isinstance(graph, (nx.Graph, rx.PyGraph)): + raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) coeffs = [] ops = [] + graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes + if len(reward) == 0 or len(reward) == 4: - coeffs = [1 for _ in graph.nodes] - ops = [qml.Identity(v) for v in graph.nodes] + coeffs = [1 for _ in graph_nodes] + ops = [qml.Identity(v) for v in graph_nodes] else: @@ -181,19 +198,19 @@ def edge_driver(graph, reward): reward = reward[0] if reward == "00": - for e in graph.edges: + for e in graph_nodes: coeffs.extend([0.25 * sign, 0.25 * sign, 0.25 * sign]) ops.extend( [qml.PauliZ(e[0]) @ qml.PauliZ(e[1]), qml.PauliZ(e[0]), qml.PauliZ(e[1])] ) if reward == "10": - for e in graph.edges: + for e in graph_nodes: coeffs.append(-0.5 * sign) ops.append(qml.PauliZ(e[0]) @ qml.PauliZ(e[1])) if reward == "11": - for e in graph.edges: + for e in graph_nodes: coeffs.extend([0.25 * sign, -0.25 * sign, -0.25 * sign]) ops.extend( [qml.PauliZ(e[0]) @ qml.PauliZ(e[1]), qml.PauliZ(e[0]), qml.PauliZ(e[1])] @@ -230,7 +247,7 @@ def maxcut(graph): Even superposition over all basis states Args: - graph (nx.Graph): a graph defining the pairs of wires on which each term of the Hamiltonian acts + 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 @@ -250,16 +267,19 @@ def maxcut(graph): + (1) [X2] """ - if not isinstance(graph, nx.Graph): - raise ValueError("Input graph must be a nx.Graph, got {}".format(type(graph).__name__)) + if not isinstance(graph, (nx.Graph, rx.PyGraph)): + raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + + graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes + graph_edges = graph.edge_list() if isinstance(graph, rx.PyGraph) else graph.edges identity_h = qml.Hamiltonian( - [-0.5 for e in graph.edges], [qml.Identity(e[0]) @ qml.Identity(e[1]) for e in graph.edges] + [-0.5 for e in graph_edges], [qml.Identity(e[0]) @ qml.Identity(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, qaoa.x_mixer(graph.nodes)) + return (H, qaoa.x_mixer(graph_nodes)) def max_independent_set(graph, constrained=True): @@ -269,7 +289,7 @@ def max_independent_set(graph, constrained=True): share a common edge. The Maximum Independent Set problem, is the problem of finding the largest such set. Args: - graph (nx.Graph): a graph whose edges define the pairs of vertices on which each term of the Hamiltonian acts + 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: @@ -319,16 +339,18 @@ def max_independent_set(graph, constrained=True): """ - if not isinstance(graph, nx.Graph): - raise ValueError("Input graph must be a nx.Graph, got {}".format(type(graph).__name__)) + if not isinstance(graph, (nx.Graph, rx.PyGraph)): + raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + + graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes if constrained: - cost_h = bit_driver(graph.nodes, 1) + cost_h = bit_driver(graph_nodes, 1) cost_h.grouping_indices = [list(range(len(cost_h.ops)))] return (cost_h, qaoa.bit_flip_mixer(graph, 0)) - cost_h = 3 * edge_driver(graph, ["10", "01", "00"]) + bit_driver(graph.nodes, 1) - mixer_h = qaoa.x_mixer(graph.nodes) + cost_h = 3 * edge_driver(graph, ["10", "01", "00"]) + bit_driver(graph_nodes, 1) + mixer_h = qaoa.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)))] @@ -345,7 +367,7 @@ def min_vertex_cover(graph, constrained=True): every edge in the graph has one of the vertices as an endpoint. Args: - graph (nx.Graph): a graph whose edges define the pairs of vertices on which each term of the Hamiltonian acts + 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: @@ -395,16 +417,18 @@ def min_vertex_cover(graph, constrained=True): """ - if not isinstance(graph, nx.Graph): - raise ValueError("Input graph must be a nx.Graph, got {}".format(type(graph).__name__)) + if not isinstance(graph, (nx.Graph, rx.PyGraph)): + raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + + graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes if constrained: - cost_h = bit_driver(graph.nodes, 0) + cost_h = bit_driver(graph_nodes, 0) cost_h.grouping_indices = [list(range(len(cost_h.ops)))] return (cost_h, qaoa.bit_flip_mixer(graph, 1)) - cost_h = 3 * edge_driver(graph, ["11", "10", "01"]) + bit_driver(graph.nodes, 0) - mixer_h = qaoa.x_mixer(graph.nodes) + cost_h = 3 * edge_driver(graph, ["11", "10", "01"]) + bit_driver(graph_nodes, 0) + mixer_h = qaoa.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)))] @@ -420,7 +444,7 @@ def max_clique(graph, constrained=True): graph --- the largest subgraph such that all vertices are connected by an edge. Args: - graph (nx.Graph): a graph whose edges define the pairs of vertices on which each term of the Hamiltonian acts + 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: @@ -473,16 +497,18 @@ def max_clique(graph, constrained=True): """ - if not isinstance(graph, nx.Graph): - raise ValueError("Input graph must be a nx.Graph, got {}".format(type(graph).__name__)) + if not isinstance(graph, (nx.Graph, rx.PyGraph)): + raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + + graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes if constrained: - cost_h = bit_driver(graph.nodes, 1) + cost_h = bit_driver(graph_nodes, 1) cost_h.grouping_indices = [list(range(len(cost_h.ops)))] return (cost_h, qaoa.bit_flip_mixer(nx.complement(graph), 0)) - cost_h = 3 * edge_driver(nx.complement(graph), ["10", "01", "00"]) + bit_driver(graph.nodes, 1) - mixer_h = qaoa.x_mixer(graph.nodes) + cost_h = 3 * edge_driver(nx.complement(graph), ["10", "01", "00"]) + bit_driver(graph_nodes, 1) + mixer_h = qaoa.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)))] @@ -506,7 +532,7 @@ def max_weight_cycle(graph, constrained=True): our subset of edges composes a `cycle `__. Args: - graph (nx.Graph): the directed graph on which the Hamiltonians are defined + graph (nx.Graph or rx.PyGraph): the directed graph on which the Hamiltonians are defined constrained (bool): specifies the variant of QAOA that is performed (constrained or unconstrained) Returns: @@ -625,8 +651,8 @@ def max_weight_cycle(graph, constrained=True): 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__)) + if not isinstance(graph, (nx.Graph, rx.PyGraph)): + raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) mapping = qaoa.cycle.wires_to_edges(graph) From ff8a2d657a3df99e70f8777054f0418b98e3f967 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Tue, 19 Oct 2021 11:51:13 -0400 Subject: [PATCH 05/22] Update RX in qaoa/cost.py --- pennylane/qaoa/cost.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py index 72e212a1dad..bf4a44fe4b6 100644 --- a/pennylane/qaoa/cost.py +++ b/pennylane/qaoa/cost.py @@ -18,6 +18,7 @@ import networkx as nx from networkx.algorithms.similarity import graph_edit_distance +from networkx.generators.expanders import paley_graph import retworkx as rx import pennylane as qml from pennylane import qaoa @@ -501,13 +502,14 @@ def max_clique(graph, constrained=True): raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else 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, qaoa.bit_flip_mixer(nx.complement(graph), 0)) + return (cost_h, qaoa.bit_flip_mixer(graph_complement, 0)) - cost_h = 3 * edge_driver(nx.complement(graph), ["10", "01", "00"]) + bit_driver(graph_nodes, 1) + cost_h = 3 * edge_driver(graph_complement, ["10", "01", "00"]) + bit_driver(graph_nodes, 1) mixer_h = qaoa.x_mixer(graph_nodes) # store the valuable information that all observables are in one commuting group From 310b860821e55fb2e96039c336b1a5d4c1dc4477 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Tue, 19 Oct 2021 12:06:30 -0400 Subject: [PATCH 06/22] Add RX support to qaoa/mixers.py --- pennylane/qaoa/mixers.py | 45 +++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/pennylane/qaoa/mixers.py b/pennylane/qaoa/mixers.py index 36dd1f3fdb9..07c298f36b9 100644 --- a/pennylane/qaoa/mixers.py +++ b/pennylane/qaoa/mixers.py @@ -17,6 +17,7 @@ import itertools import functools import networkx as nx +import retworkx as rx import pennylane as qml from pennylane.wires import Wires @@ -79,7 +80,7 @@ def xy_mixer(graph): Eleanor G. Rieffel, Davide Venturelli, and Rupak Biswas [`arXiv:1709.03489 `__]. Args: - graph (nx.Graph): A graph defining the collections of wires on which the Hamiltonian acts. + graph (nx.Graph or rx.PyGraph): A graph defining the collections of wires on which the Hamiltonian acts. Returns: Hamiltonian: Mixer Hamiltonian @@ -97,14 +98,25 @@ def xy_mixer(graph): + (0.5) [Y0 Y1] + (0.5) [X1 X2] + (0.5) [Y1 Y2] + + >>> import retworkx 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): + if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph object, got {}".format(type(graph).__name__) + "Input graph must be a nx.Graph or rx.PyGraph object, got {}".format(type(graph).__name__) ) - edges = graph.edges + edges = graph.edge_list() if isinstance(graph, rx.PyGraph) else graph.edges coeffs = 2 * [0.5 for e in edges] obs = [] @@ -133,7 +145,7 @@ def bit_flip_mixer(graph, b): This mixer was introduced in [`arXiv:1709.03489 `__]. Args: - graph (nx.Graph): A graph defining the collections of wires on which the Hamiltonian acts. + 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 @@ -159,11 +171,26 @@ def bit_flip_mixer(graph, b): + (0.5) [X0 Z1] + (0.5) [X2 Z1] + (0.25) [X1 Z0 Z2] + + >>> import retworkx 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.25) [X1] + + (0.5) [X0] + + (0.5) [X2] + + (0.25) [X1 Z0] + + (0.25) [X1 Z2] + + (0.5) [X0 Z1] + + (0.5) [X2 Z1] + + (0.25) [X1 Z2 Z0] """ - if not isinstance(graph, nx.Graph): + if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph object, got {}".format(type(graph).__name__) + "Input graph must be a nx.Graph or rx.PyGraph object, got {}".format(type(graph).__name__) ) if b not in [0, 1]: @@ -174,7 +201,9 @@ def bit_flip_mixer(graph, b): coeffs = [] terms = [] - for i in graph.nodes: + graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes + + for i in graph_nodes: neighbours = list(graph.neighbors(i)) degree = len(neighbours) From 5c50590006dfac221d2ed3ea6455716e956060dc Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Tue, 19 Oct 2021 12:46:22 -0400 Subject: [PATCH 07/22] Update RX support in qaoa --- pennylane/circuit_graph.py | 1 + pennylane/qaoa/cost.py | 12 +++++++----- pennylane/qaoa/cycle.py | 2 ++ pennylane/qaoa/mixers.py | 2 ++ tests/test_qaoa.py | 16 ++++++++-------- 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index a44c819b4c1..03d214806f5 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -19,6 +19,7 @@ from collections import Counter, OrderedDict, namedtuple import networkx as nx +import retworkx as rx import pennylane as qml import numpy as np diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py index bf4a44fe4b6..b0127674ede 100644 --- a/pennylane/qaoa/cost.py +++ b/pennylane/qaoa/cost.py @@ -15,11 +15,12 @@ Methods for generating QAOA cost Hamiltonians corresponding to different optimization problems. """ - import networkx as nx +import retworkx as rx + from networkx.algorithms.similarity import graph_edit_distance from networkx.generators.expanders import paley_graph -import retworkx as rx + import pennylane as qml from pennylane import qaoa @@ -182,6 +183,7 @@ def edge_driver(graph, reward): ops = [] graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes + graph_edges = graph.edge_list() if isinstance(graph, rx.PyGraph) else graph.edges if len(reward) == 0 or len(reward) == 4: coeffs = [1 for _ in graph_nodes] @@ -199,19 +201,19 @@ def edge_driver(graph, reward): reward = reward[0] if reward == "00": - for e in graph_nodes: + for e in graph_edges: coeffs.extend([0.25 * sign, 0.25 * sign, 0.25 * sign]) ops.extend( [qml.PauliZ(e[0]) @ qml.PauliZ(e[1]), qml.PauliZ(e[0]), qml.PauliZ(e[1])] ) if reward == "10": - for e in graph_nodes: + for e in graph_edges: coeffs.append(-0.5 * sign) ops.append(qml.PauliZ(e[0]) @ qml.PauliZ(e[1])) if reward == "11": - for e in graph_nodes: + for e in graph_edges: coeffs.extend([0.25 * sign, -0.25 * sign, -0.25 * sign]) ops.extend( [qml.PauliZ(e[0]) @ qml.PauliZ(e[1]), qml.PauliZ(e[0]), qml.PauliZ(e[1])] diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index 8f754a86462..b4685ad0871 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -17,8 +17,10 @@ # pylint: disable=unnecessary-comprehension import itertools from typing import Dict, Tuple, Iterable, List + import networkx as nx import retworkx as rx + import numpy as np import pennylane as qml from pennylane.ops import Hamiltonian diff --git a/pennylane/qaoa/mixers.py b/pennylane/qaoa/mixers.py index 07c298f36b9..bab1ba9324c 100644 --- a/pennylane/qaoa/mixers.py +++ b/pennylane/qaoa/mixers.py @@ -16,8 +16,10 @@ """ import itertools import functools + import networkx as nx import retworkx as rx + import pennylane as qml from pennylane.wires import Wires diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index 3085ed4aaaa..b29ca57255c 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -132,7 +132,7 @@ def test_xy_mixer_type_error(self): graph = [(0, 1), (1, 2)] - with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph object, got list"): + with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph object, got list"): qaoa.xy_mixer(graph) @pytest.mark.parametrize( @@ -217,7 +217,7 @@ 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 object"): + with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph object"): qaoa.bit_flip_mixer(graph, 0) n = 2 @@ -678,7 +678,7 @@ def test_edge_driver_errors(self): ): qaoa.edge_driver(Graph([(0, 1), (1, 2)]), ["11", "00", "01"]) - with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph"): + 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"), EDGE_DRIVER) @@ -693,7 +693,7 @@ def test_edge_driver_output(self, graph, reward, hamiltonian): 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"): + with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph"): qaoa.max_weight_cycle([(0, 1), (1, 2)]) def test_cost_graph_error(self): @@ -701,13 +701,13 @@ def test_cost_graph_error(self): graph = [(0, 1), (1, 2)] - with pytest.raises(ValueError, match=r"Input graph must be a nx\.Graph"): + 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"): + 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"): + 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"): + 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"), MAXCUT) From afabefd27454a62d679e9335ea04c86ba350a3f0 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Thu, 21 Oct 2021 00:51:52 -0400 Subject: [PATCH 08/22] Add RX support to CircuitGraph and update QAOA tests --- pennylane/circuit_graph.py | 1 - pennylane/qaoa/layers.py | 758 +++++++++++++++++++++++++++++++------ tests/test_qaoa.py | 140 +++++-- 3 files changed, 767 insertions(+), 132 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 03d214806f5..a44c819b4c1 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -19,7 +19,6 @@ from collections import Counter, OrderedDict, namedtuple import networkx as nx -import retworkx as rx import pennylane as qml import numpy as np diff --git a/pennylane/qaoa/layers.py b/pennylane/qaoa/layers.py index bce194994ac..63f6fdd44d4 100644 --- a/pennylane/qaoa/layers.py +++ b/pennylane/qaoa/layers.py @@ -12,149 +12,691 @@ # 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. +This module contains the CircuitGraph class which is used to generate a DAG (directed acyclic graph) +representation of a quantum circuit from an Operator queue. """ +# pylint: disable=too-many-branches,too-many-arguments,too-many-instance-attributes +from collections import Counter, OrderedDict, namedtuple + +import networkx as nx +import retworkx as rx + import pennylane as qml -from pennylane.operation import Tensor +import numpy as np +from pennylane.wires import Wires +from .circuit_drawer import CircuitDrawer -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 +def _by_idx(x): + """Sorting key for Operators: queue index aka temporal order. + Args: + x (Operator): node in the circuit graph Returns: - bool: ``True`` if all terms are products of diagonal Pauli gates, ``False`` otherwise + int: sorting key for the node """ - val = True - - for i in hamiltonian.ops: - obs = i.obs if isinstance(i, Tensor) else [i] - for j in obs: - if j.name not in ("PauliZ", "Identity"): - val = False - break + return x.queue_idx - return val +def _is_observable(x): + """Predicate for deciding if an Operator instance is an observable. -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. + .. note:: + Currently some :class:`Observable` instances are not observables in this sense, + since they can be used as gates as well. 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 - - .. UsageDetails:: - - We first define a cost Hamiltonian: - - .. code-block:: python3 - - from pennylane import qaoa - import pennylane as qml - - cost_h = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliZ(1)]) - - We can then pass it into ``qaoa.cost_layer``, within a quantum circuit: - - .. code-block:: python - - dev = qml.device('default.qubit', wires=2) - - @qml.qnode(dev) - def circuit(gamma): - - for i in range(2): - qml.Hadamard(wires=i) - - cost_layer(gamma, cost_h) - - return [qml.expval(qml.PauliZ(wires=i)) for i in range(2)] - - which gives us a circuit of the form: - - >>> print(qml.draw(circuit)(0.5)) - 0: ──H──RZ(1)──╭RZ(1)──┤ ⟨Z⟩ - 1: ──H─────────╰RZ(1)──┤ ⟨Z⟩ - + x (Operator): node in the circuit graph + Returns: + bool: True iff x is an observable """ - if not isinstance(hamiltonian, qml.Hamiltonian): - raise ValueError( - "hamiltonian must be of type pennylane.Hamiltonian, got {}".format( - type(hamiltonian).__name__ - ) - ) + return getattr(x, "return_type", None) is not None - if not _diagonal_terms(hamiltonian): - raise ValueError("hamiltonian must be written only in terms of PauliZ and Identity gates") - qml.templates.ApproxTimeEvolution(hamiltonian, gamma, 1) +def _list_at_index_or_none(ls, idx): + """Return the element of a list at the given index if it exists, return None otherwise. + Args: + ls (list[object]): The target list + idx (int): The target index -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: + Returns: + Union[object,NoneType]: The element at the target index or None + """ + if len(ls) > idx: + return ls[idx] - .. math:: U_M \ = \ e^{-i \alpha H_M} + return None - 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 +def _is_returned_observable(op): + """Helper for the condition of having an observable or + measurement process in the return statement. - .. UsageDetails:: + Returns: + bool: whether or not the observable or measurement process is in the + return statement + """ + is_obs = isinstance(op, (qml.operation.Observable, qml.measure.MeasurementProcess)) + return is_obs and op.return_type is not None - We first define a mixer Hamiltonian: - .. code-block:: python3 +Layer = namedtuple("Layer", ["ops", "param_inds"]) +"""Parametrized layer of the circuit. - from pennylane import qaoa - import pennylane as qml +Args: - mixer_h = qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliX(0) @ qml.PauliX(1)]) + ops (list[Operator]): parametrized operators in the layer + param_inds (list[int]): corresponding free parameter indices +""" +# TODO define what a layer is - We can then pass it into ``qaoa.mixer_layer``, within a quantum circuit: +LayerData = namedtuple("LayerData", ["pre_ops", "ops", "param_inds", "post_ops"]) +"""Parametrized layer of the circuit. - .. code-block:: python +Args: + pre_ops (list[Operator]): operators that precede the layer + ops (list[Operator]): parametrized operators in the layer + param_inds (tuple[int]): corresponding free parameter indices + post_ops (list[Operator]): operators that succeed the layer +""" - dev = qml.device('default.qubit', wires=2) - @qml.qnode(dev) - def circuit(alpha): +class CircuitGraph: + """Represents a quantum circuit as a directed acyclic graph. - for i in range(2): - qml.Hadamard(wires=i) + In this representation the :class:`~.Operator` instances are the nodes of the graph, + and each directed edge represent a subsystem (or a group of subsystems) on which the two + Operators act subsequently. This representation can describe the causal relationships + between arbitrary quantum channels and measurements, not just unitary gates. - qaoa.mixer_layer(alpha, mixer_h) + Args: + ops (Iterable[.Operator]): quantum operators constituting the circuit, in temporal order + obs (Iterable[.MeasurementProcess]): terminal measurements, in temporal order + wires (.Wires): The addressable wire registers of the device that will be executing this graph + par_info (dict[int, dict[str, .Operation or int]]): Parameter information. Keys are + parameter indices (in the order they appear on the tape), and values are a + dictionary containing the corresponding operation and operation parameter index. + trainable_params (set[int]): A set containing the indices of parameters that support + differentiability. The indices provided match the order of appearence in the + quantum circuit. + """ - return [qml.expval(qml.PauliZ(wires=i)) for i in range(2)] + # pylint: disable=too-many-public-methods + + def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): + self._operations = ops + self._observables = obs + self.par_info = par_info + self.trainable_params = trainable_params + + queue = ops + obs + + self._depth = None + + self._grid = {} + """dict[int, list[Operator]]: dictionary representing the quantum circuit as a grid. + Here, the key is the wire number, and the value is a list containing the operators on that wire. + """ + self.wires = wires + """Wires: wires that are addressed in the operations. + Required to translate between wires and indices of the wires on the device.""" + self.num_wires = len(wires) + """int: number of wires the circuit contains""" + for k, op in enumerate(queue): + op.queue_idx = k # store the queue index in the Operator + + if hasattr(op, "return_type"): + if op.return_type is qml.operation.State: + # State measurements contain no wires by default, but wires are + # required for the circuit drawer, so we recreate the state + # measurement with all wires + op = qml.measure.MeasurementProcess(qml.operation.State, wires=wires) + + elif op.return_type is qml.operation.Sample and op.wires == Wires([]): + # Sampling without specifying wires is treated as sampling all wires + op = qml.measure.MeasurementProcess(qml.operation.Sample, wires=wires) + + op.queue_idx = k + + for w in op.wires: + # get the index of the wire on the device + wire = wires.index(w) + # add op to the grid, to the end of wire w + self._grid.setdefault(wire, []).append(op) + + # TODO: State preparations demolish the incoming state entirely, and therefore should have no incoming edges. + + # self._graph = nx.DiGraph() #: nx.DiGraph: DAG representation of the quantum circuit + self._graph = rx.PyDiGraph() #: rx.PyDiGraph: DAG representation of the quantum circuit + # Iterate over each (populated) wire in the grid + for wire in self._grid.values(): + # Add the first operator on the wire to the graph + # This operator does not depend on any others + self._graph.add_node(wire[0]) + + for i in range(1, len(wire)): + # For subsequent operators on the wire: + if wire[i] not in self._graph: + # Add them to the graph if they are not already + # in the graph (multi-qubit operators might already have been placed) + self._graph.add_node(wire[i]) + + # Create an edge between this and the previous operator + # self._graph.add_edge(wire[i - 1], wire[i]) + self._graph.add_edge(wire[i - 1], wire[i], '') # rx. + + # For computing depth; want only a graph with the operations, not + # including the observables + self._operation_graph = None + + # Required to keep track if we need to handle multiple returned + # observables per wire + self._max_simultaneous_measurements = None + + def print_contents(self): + """Prints the contents of the quantum circuit.""" + + print("Operations") + print("==========") + for op in self.operations: + print(repr(op)) + + print("\nObservables") + print("===========") + for op in self.observables: + print(repr(op)) + + def serialize(self): + """Serialize the quantum circuit graph based on the operations and + observables in the circuit graph and the index of the variables + used by them. + + The string that is produced can be later hashed to assign a unique value to the circuit graph. + + Returns: + string: serialized quantum circuit graph + """ + serialization_string = "" + delimiter = "!" + + for op in self.operations_in_order: + serialization_string += op.name + + for param in op.data: + serialization_string += delimiter + serialization_string += str(param) + serialization_string += delimiter + + serialization_string += str(op.wires.tolist()) + + # Adding a distinct separating string that could not occur by any combination of the + # name of the operation and wires + serialization_string += "|||" + + for obs in self.observables_in_order: + serialization_string += str(obs.return_type) + serialization_string += delimiter + serialization_string += str(obs.name) + for param in obs.data: + serialization_string += delimiter + serialization_string += str(param) + serialization_string += delimiter + + serialization_string += str(obs.wires.tolist()) + return serialization_string + + @property + def hash(self): + """Creating a hash for the circuit graph based on the string generated by serialize. + + Returns: + int: the hash of the serialized quantum circuit graph + """ + return hash(self.serialize()) + + @property + def observables_in_order(self): + """Observables in the circuit, in a fixed topological order. + + The topological order used by this method is guaranteed to be the same + as the order in which the measured observables are returned by the quantum function. + Currently the topological order is determined by the queue index. + + Returns: + list[Observable]: observables + """ + # nodes = [node for node in self._graph.nodes if _is_observable(node)] + nodes = [node for node in self._graph.nodes() if _is_observable(node)] # rx. + return sorted(nodes, key=_by_idx) + + @property + def observables(self): + """Observables in the circuit.""" + return self._observables + + @property + def operations_in_order(self): + """Operations in the circuit, in a fixed topological order. + + Currently the topological order is determined by the queue index. + + The complement of :meth:`QNode.observables`. Together they return every :class:`Operator` + instance in the circuit. + + Returns: + list[Operation]: operations + """ + # nodes = [node for node in self._graph.nodes if not _is_observable(node)] + nodes = [node for node in self._graph.nodes() if not _is_observable(node)] # rx. + return sorted(nodes, key=_by_idx) + + @property + def operations(self): + """Operations in the circuit.""" + return self._operations + + @property + def graph(self): + """The graph representation of the quantum circuit. + + The graph has nodes representing :class:`.Operator` instances, + and directed edges pointing from nodes to their immediate dependents/successors. + + Returns: + retworkx.PyDiGraph: the directed acyclic graph representing the quantum circuit + """ + return self._graph + + def wire_indices(self, wire): + """Operator indices on the given wire. + + Args: + wire (int): wire to examine + + Returns: + list[int]: indices of operators on the wire, in temporal order + """ + return [op.queue_idx for op in self._grid[wire]] + + def ancestors(self, ops): + """Ancestors of a given set of operators. + + Args: + ops (Iterable[Operator]): set of operators in the circuit + + Returns: + set[Operator]: ancestors of the given operators + """ + # return set().union(*(nx.dag.ancestors(self._graph, o) for o in ops)) - set(ops) + return set().union(*(rx.ancestors(self._graph, o) for o in ops)) - set(ops) # rx. + + def descendants(self, ops): + """Descendants of a given set of operators. + + Args: + ops (Iterable[Operator]): set of operators in the circuit + + Returns: + set[Operator]: descendants of the given operators + """ + # return set().union(*(nx.dag.descendants(self._graph, o) for o in ops)) - set(ops) + return set().union(*(rx.descendants(self._graph, o) for o in ops)) - set(ops) # rx. + + def _in_topological_order(self, ops): + """Sorts a set of operators in the circuit in a topological order. + + Args: + ops (Iterable[Operator]): set of operators in the circuit + + Returns: + Iterable[Operator]: same set of operators, topologically ordered + """ + # G = nx.DiGraph(self._graph.subgraph(ops)) + # return nx.dag.topological_sort(G) + return rx.topological_sort(self._graph.subgraph(ops)) # rx. + + def ancestors_in_order(self, ops): + """Operator ancestors in a topological order. + + Currently the topological order is determined by the queue index. + + Args: + ops (Iterable[Operator]): set of operators in the circuit + + Returns: + list[Operator]: ancestors of the given operators, topologically ordered + """ + # return self._in_topological_order(self.ancestors(ops)) # an abitrary topological order + return sorted(self.ancestors(ops), key=_by_idx) + + def descendants_in_order(self, ops): + """Operator descendants in a topological order. + + Currently the topological order is determined by the queue index. + + Args: + ops (Iterable[Operator]): set of operators in the circuit + + Returns: + list[Operator]: descendants of the given operators, topologically ordered + """ + return sorted(self.descendants(ops), key=_by_idx) + + def nodes_between(self, a, b): + r"""Nodes on all the directed paths between the two given nodes. + + Returns the set of all nodes ``s`` that fulfill :math:`a \le s \le b`. + There is a directed path from ``a`` via ``s`` to ``b`` iff the set is nonempty. + The endpoints belong to the path. + + Args: + a (Operator): initial node + b (Operator): final node + + Returns: + set[Operator]: nodes on all the directed paths between a and b + """ + A = self.descendants([a]) + A.add(a) + B = self.ancestors([b]) + B.add(b) + return A & B + + def invisible_operations(self): + """Operations that cannot affect the circuit output. + + An :class:`Operation` instance in a quantum circuit is *invisible* if is not an ancestor + of an observable. Such an operation cannot affect the circuit output, and usually indicates + there is something wrong with the circuit. + + Returns: + set[Operator]: operations that cannot affect the output + """ + visible = self.ancestors(self.observables) + invisible = set(self.operations) - visible + return invisible + + @property + def parametrized_layers(self): + """Identify the parametrized layer structure of the circuit. + + Returns: + list[Layer]: layers of the circuit + """ + # FIXME maybe layering should be greedier, for example [a0 b0 c1 d1] should layer as [a0 + # c1], [b0, d1] and not [a0], [b0 c1], [d1] keep track of the current layer + current = Layer([], []) + layers = [current] + + for idx, info in self.par_info.items(): + if idx in self.trainable_params: + op = info["op"] + + # get all predecessor ops of the op + sub = self.ancestors((op,)) + + # check if any of the dependents are in the + # currently assembled layer + if set(current.ops) & sub: + # operator depends on current layer, start a new layer + current = Layer([], []) + layers.append(current) + + # store the parameters and ops indices for the layer + current.ops.append(op) + current.param_inds.append(idx) + + return layers + + def iterate_parametrized_layers(self): + """Parametrized layers of the circuit. + + Returns: + Iterable[LayerData]: layers with extra metadata + """ + # iterate through each layer + for ops, param_inds in self.parametrized_layers: + pre_queue = self.ancestors_in_order(ops) + post_queue = self.descendants_in_order(ops) + yield LayerData(pre_queue, ops, tuple(param_inds), post_queue) + + def greedy_layers(self, wire_order=None, show_all_wires=False): + """Greedily collected layers of the circuit. Empty slots are filled with ``None``. + + Layers are built by pushing back gates in the circuit as far as possible, so that + every Gate is at the lower possible layer. + + Args: + wire_order (Wires): the order (from top to bottom) to print the wires of the circuit + show_all_wires (bool): If True, all wires, including empty wires, are printed. + + Returns: + Tuple[list[list[~.Operation]], list[list[~.Observable]]]: + Tuple of the circuits operations and the circuits observables, both indexed + by wires. + """ + l = 0 + + operations = OrderedDict() + for key in sorted(self._grid): + operations[key] = self._grid[key] + + for wire in operations: + operations[wire] = list( + filter( + lambda op: not ( + isinstance(op, (qml.operation.Observable, qml.measure.MeasurementProcess)) + and op.return_type is not None + ), + operations[wire], + ) + ) - which gives us a circuit of the form: + while True: + layer_ops = {wire: _list_at_index_or_none(operations[wire], l) for wire in operations} + num_ops = Counter(layer_ops.values()) - >>> print(qml.draw(circuit)(0.5)) - 0: ──H──RZ(1)──H──H──╭RZ(1)──H──┤ ⟨Z⟩ - 1: ──H───────────────╰RZ(1)──H──┤ ⟨Z⟩ + if None in num_ops and num_ops[None] == len(operations): + break - """ - if not isinstance(hamiltonian, qml.Hamiltonian): - raise ValueError( - "hamiltonian must be of type pennylane.Hamiltonian, got {}".format( - type(hamiltonian).__name__ - ) + for (wire, op) in layer_ops.items(): + if op is None: + operations[wire].append(None) + continue + + # push back to next layer if not all args wires are there yet + if len(op.wires) > num_ops[op]: + operations[wire].insert(l, None) + + l += 1 + + observables = OrderedDict() + + if self.max_simultaneous_measurements == 1: + + # There is a single measurement for every wire + for wire in sorted(self._grid): + observables[wire] = list( + filter( + lambda op: isinstance( + op, (qml.operation.Observable, qml.measure.MeasurementProcess) + ) + and op.return_type is not None, + self._grid[wire], + ) + ) + if not observables[wire]: + observables[wire] = [None] + else: + + # There are wire(s) with multiple measurements. + # We are creating a separate "visual block" at the end of the + # circuit for each observable and mapping observables with block + # indices. + num_observables = len(self.observables) + mp_map = dict(zip(self.observables, range(num_observables))) + + for wire in sorted(self._grid): + # Initialize to None everywhere + observables[wire] = [None] * num_observables + + for op in self._grid[wire]: + if _is_returned_observable(op): + obs_idx = mp_map[op] + observables[wire][obs_idx] = op + + if wire_order is not None: + temp_op_grid = OrderedDict() + temp_obs_grid = OrderedDict() + + if show_all_wires: + permutation = [ + self.wires.labels.index(i) if i in self.wires else None + for i in wire_order.labels + ] + else: + permutation = [ + self.wires.labels.index(i) for i in wire_order.labels if i in self.wires + ] + + for i, j in enumerate(permutation): + if j is None: + temp_op_grid[i] = [None] * len(operations[0]) + temp_obs_grid[i] = [None] * len(observables[0]) + continue + + if j in operations: + temp_op_grid[i] = operations[j] + if j in observables: + temp_obs_grid[i] = observables[j] + + operations = temp_op_grid + observables = temp_obs_grid + + op_grid = [operations[wire] for wire in operations] + obs_grid = [observables[wire] for wire in observables] + + return op_grid, obs_grid + + def update_node(self, old, new): + """Replaces the given circuit graph node with a new one. + + Args: + old (Operator): node to replace + new (Operator): replacement + + Raises: + ValueError: if the new :class:`~.Operator` does not act on the same wires as the old one + """ + # NOTE Does not alter the graph edges in any way. variable_deps is not changed, _grid is not changed. Dangerous! + if new.wires != old.wires: + raise ValueError("The new Operator must act on the same wires as the old one.") + new.queue_idx = old.queue_idx + + # nx.relabel_nodes(self._graph, {old: new}, copy=False) # change the graph in place + self._graph[old] = new # rx. + + self._operations = self.operations_in_order + self._observables = self.observables_in_order + + def draw(self, charset="unicode", wire_order=None, show_all_wires=False): + """Draw the CircuitGraph as a circuit diagram. + + Args: + charset (str, optional): The charset that should be used. Currently, "unicode" and "ascii" are supported. + wire_order (Wires or None): the order (from top to bottom) to print the wires of the circuit + show_all_wires (bool): If True, all wires, including empty wires, are printed. + + Raises: + ValueError: If the given charset is not supported + + Returns: + str: The circuit diagram representation of the ``CircuitGraph`` + """ + if wire_order is not None: + wire_order = qml.wires.Wires.all_wires([wire_order, self.wires]) + + grid, obs = self.greedy_layers(wire_order=wire_order, show_all_wires=show_all_wires) + + drawer = CircuitDrawer( + grid, + obs, + wires=wire_order or self.wires, + charset=charset, + show_all_wires=show_all_wires, ) - qml.templates.ApproxTimeEvolution(hamiltonian, alpha, 1) + return drawer.draw() + + def get_depth(self): + """Depth of the quantum circuit (longest path in the DAG).""" + # If there are no operations in the circuit, the depth is 0 + if not self.operations: + self._depth = 0 + + # If there are operations but depth is uncomputed, compute the truncated graph + # with only the operations, and return the longest path + 1 (since the path is + # expressed in terms of edges, and we want it in terms of nodes). + if self._depth is None and self.operations: + if self._operation_graph is None: + self._operation_graph = self.graph.subgraph(self.operations) + + # self._depth = nx.dag_longest_path_length(self._operation_graph) + 1 + self._depth = rx.dag_longest_path_length(self._operation_graph, weight_fn=lambda edge: edge) + 1 # rx. + + return self._depth + + def has_path(self, a, b): + """Checks if a path exists between the two given nodes. + + Args: + a (Operator): initial node + b (Operator): final node + + Returns: + bool: returns ``True`` if a path exists + """ + # return nx.has_path(self._graph, a, b) + return len(rx.dijkstra_shortest_paths(self._graph, a, b)) != 0 # rx. + + @property + def max_simultaneous_measurements(self): + """Returns the maximum number of measurements on any wire in the circuit graph. + + This method counts the number of measurements for each wire and returns + the maximum. + + **Examples** + + + >>> dev = qml.device('default.qubit', wires=3) + >>> def circuit_measure_max_once(): + ... return qml.expval(qml.PauliX(wires=0)) + >>> qnode = qml.QNode(circuit_measure_max_once, dev) + >>> qnode() + >>> qnode.qtape.graph.max_simultaneous_measurements + 1 + >>> def circuit_measure_max_twice(): + ... return qml.expval(qml.PauliX(wires=0)), qml.probs(wires=0) + >>> qnode = qml.QNode(circuit_measure_max_twice, dev) + >>> qnode() + >>> qnode.qtape.graph.max_simultaneous_measurements + 2 + + Returns: + int: the maximum number of measurements + """ + if self._max_simultaneous_measurements is None: + all_wires = [] + + for obs in self.observables: + all_wires.extend(obs.wires.tolist()) + + a = np.array(all_wires) + _, counts = np.unique(a, return_counts=True) + self._max_simultaneous_measurements = ( + counts.max() if counts.size != 0 else 1 + ) # qml.state() will result in an empty array + return self._max_simultaneous_measurements diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index b29ca57255c..e0f13fd298b 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -14,13 +14,18 @@ """ Unit tests for the :mod:`pennylane.qaoa` submodule. """ +from networkx.algorithms.similarity import graph_edit_distance import pytest import itertools import numpy as np + import networkx as nx +from networkx import Graph +import retworkx as rx + import pennylane as qml from pennylane import qaoa -from networkx import Graph + from pennylane.qaoa.cycle import ( edges_to_wires, wires_to_edges, @@ -43,20 +48,42 @@ graph.add_nodes_from([0, 1, 2]) 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, '')]) + 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) +for k, v in complete_edge_weight_data.items(): + digraph_complete_rx.update_edge(k[0], k[1], {"weight": v}) + +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, '')]) def decompose_hamiltonian(hamiltonian): coeffs = list(qml.math.toarray(hamiltonian.coeffs)) ops = [i.name for i in hamiltonian.ops] wires = [i.wires for i in hamiltonian.ops] - return [coeffs, ops, wires] @@ -139,7 +166,7 @@ def test_xy_mixer_type_error(self): ("graph", "target_hamiltonian"), [ ( - Graph([(0, 1), (1, 2), (2, 3)]), + g2, qml.Hamiltonian( [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], [ @@ -194,6 +221,48 @@ def test_xy_mixer_type_error(self): ], ), ), + ( + g2_rx, + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliY(0) @ qml.PauliY(1), + qml.PauliX(1) @ qml.PauliX(2), + qml.PauliY(1) @ qml.PauliY(2), + qml.PauliX(2) @ qml.PauliX(3), + qml.PauliY(2) @ qml.PauliY(3), + ], + ), + ), + ( + graph_rx, + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliY(0) @ qml.PauliY(1), + qml.PauliX(1) @ qml.PauliX(2), + qml.PauliY(1) @ qml.PauliY(2), + ], + ), + ), + ( + non_consecutive_graph_rx, + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(4), + qml.PauliY(0) @ qml.PauliY(4), + qml.PauliX(0) @ qml.PauliX(2), + qml.PauliY(0) @ qml.PauliY(2), + qml.PauliX(4) @ qml.PauliX(3), + qml.PauliY(4) @ qml.PauliY(3), + qml.PauliX(2) @ qml.PauliX(1), + qml.PauliY(2) @ qml.PauliY(1), + ], + ), + ), ], ) def test_xy_mixer_output(self, graph, target_hamiltonian): @@ -241,7 +310,7 @@ def test_bit_flip_mixer_errors(self): ), ), ( - Graph([(0, 1), (1, 2)]), + g1, 0, qml.Hamiltonian( [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], @@ -290,12 +359,13 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): """GENERATES CASES TO TEST THE MAXCUT PROBLEM""" GRAPHS = [ - Graph([(0, 1), (1, 2)]), + g1, Graph((np.array([0, 1]), np.array([1, 2]), np.array([0, 2]))), graph, + g1_rx, ] -COST_COEFFS = [[0.5, 0.5, -1.0], [0.5, 0.5, 0.5, -1.5], [0.5, 0.5, -1.0]] +COST_COEFFS = [[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 = [ [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], @@ -306,27 +376,29 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): qml.Identity(0), ], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], + [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], ] -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(3)] +COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(4)] -MIXER_COEFFS = [[1, 1, 1], [1, 1, 1], [1, 1, 1]] +MIXER_COEFFS = [[1, 1, 1], [1, 1, 1], [1, 1, 1], [1, 1, 1]] MIXER_TERMS = [ [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(3)] +MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(4)] MAXCUT = list(zip(GRAPHS, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) """GENERATES THE CASES TO TEST THE MAX INDEPENDENT SET PROBLEM""" -CONSTRAINED = [True, True, False] +CONSTRAINED = [True, True, False, True] -COST_COEFFS = [[1, 1, 1], [1, 1, 1], [0.75, 0.25, -0.5, 0.75, 0.25]] +COST_COEFFS = [[1, 1, 1], [1, 1, 1], [0.75, 0.25, -0.5, 0.75, 0.25], [1, 1, 1]] COST_TERMS = [ [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], @@ -338,14 +410,16 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): qml.PauliZ(1) @ qml.PauliZ(2), qml.PauliZ(2), ], + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], ] -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(3)] +COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(4)] MIXER_COEFFS = [ [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], + [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], ] MIXER_TERMS = [ @@ -374,15 +448,25 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): qml.PauliX(2) @ qml.PauliZ(1) @ qml.PauliZ(0), ], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(2) @ qml.PauliZ(0), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(1), + ], ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(3)] +MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(4)] MIS = list(zip(GRAPHS, CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) """GENERATES THE CASES TO TEST THE MIN VERTEX COVER PROBLEM""" -COST_COEFFS = [[-1, -1, -1], [-1, -1, -1], [0.75, -0.25, 0.5, 0.75, -0.25]] +COST_COEFFS = [[-1, -1, -1], [-1, -1, -1], [0.75, -0.25, 0.5, 0.75, -0.25], [-1, -1, -1]] COST_TERMS = [ [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], @@ -394,33 +478,36 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): qml.PauliZ(1) @ qml.PauliZ(2), qml.PauliZ(2), ], + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], ] -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(3)] +COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(4)] MIXER_COEFFS = [ [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], + [0.5, -0.5, 0.25, -0.25, -0.25, 0.25, 0.5, -0.5], ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(3)] +MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(4)] MVC = list(zip(GRAPHS, CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) """GENERATES THE CASES TO TEST THE MAXCLIQUE PROBLEM""" -COST_COEFFS = [[1, 1, 1], [1, 1, 1], [0.75, 0.25, 0.25, 1]] +COST_COEFFS = [[1, 1, 1], [1, 1, 1], [0.75, 0.25, 0.25, 1], [1, 1, 1]] COST_TERMS = [ [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], [qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliZ(0), qml.PauliZ(2), qml.PauliZ(1)], + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], ] -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(3)] +COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(4)] -MIXER_COEFFS = [[0.5, 0.5, 1.0, 0.5, 0.5], [1.0, 1.0, 1.0], [1, 1, 1]] +MIXER_COEFFS = [[0.5, 0.5, 1.0, 0.5, 0.5], [1.0, 1.0, 1.0], [1, 1, 1], [0.5, 0.5, 1.0, 0.5, 0.5]] MIXER_TERMS = [ [ @@ -432,14 +519,21 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): ], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(2), + qml.PauliX(1), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(0), + ], ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(3)] +MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(4)] MAXCLIQUE = list(zip(GRAPHS, CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) """GENERATES CASES TO TEST EDGE DRIVER COST HAMILTONIAN""" - +GRAPHS.pop() GRAPHS.append(graph) GRAPHS.append(Graph([("b", 1), (1, 2.3)])) REWARDS = [["00"], ["00", "11"], ["00", "01", "10"], ["00", "11", "01", "10"], ["00", "01", "10"]] @@ -670,13 +764,13 @@ def test_edge_driver_errors(self): with pytest.raises( ValueError, match=r"Encountered invalid entry in 'reward', expected 2-bit bitstrings." ): - qaoa.edge_driver(Graph([(0, 1), (1, 2)]), ["10", "11", 21, "g"]) + 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(Graph([(0, 1), (1, 2)]), ["11", "00", "01"]) + 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"]) From 8529194afe25a0d1ff5aac57a7d6b0fde55c33a9 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Thu, 21 Oct 2021 01:22:59 -0400 Subject: [PATCH 09/22] Update CircuitGraph --- pennylane/circuit_graph.py | 40 +- pennylane/qaoa/layers.py | 758 ++++++------------------------------- 2 files changed, 135 insertions(+), 663 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index a44c819b4c1..63f6fdd44d4 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -19,6 +19,7 @@ from collections import Counter, OrderedDict, namedtuple import networkx as nx +import retworkx as rx import pennylane as qml import numpy as np @@ -167,7 +168,8 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): # TODO: State preparations demolish the incoming state entirely, and therefore should have no incoming edges. - self._graph = nx.DiGraph() #: nx.DiGraph: DAG representation of the quantum circuit + # self._graph = nx.DiGraph() #: nx.DiGraph: DAG representation of the quantum circuit + self._graph = rx.PyDiGraph() #: rx.PyDiGraph: DAG representation of the quantum circuit # Iterate over each (populated) wire in the grid for wire in self._grid.values(): # Add the first operator on the wire to the graph @@ -182,8 +184,9 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): self._graph.add_node(wire[i]) # Create an edge between this and the previous operator - self._graph.add_edge(wire[i - 1], wire[i]) - + # self._graph.add_edge(wire[i - 1], wire[i]) + self._graph.add_edge(wire[i - 1], wire[i], '') # rx. + # For computing depth; want only a graph with the operations, not # including the observables self._operation_graph = None @@ -264,7 +267,8 @@ def observables_in_order(self): Returns: list[Observable]: observables """ - nodes = [node for node in self._graph.nodes if _is_observable(node)] + # nodes = [node for node in self._graph.nodes if _is_observable(node)] + nodes = [node for node in self._graph.nodes() if _is_observable(node)] # rx. return sorted(nodes, key=_by_idx) @property @@ -284,7 +288,8 @@ def operations_in_order(self): Returns: list[Operation]: operations """ - nodes = [node for node in self._graph.nodes if not _is_observable(node)] + # nodes = [node for node in self._graph.nodes if not _is_observable(node)] + nodes = [node for node in self._graph.nodes() if not _is_observable(node)] # rx. return sorted(nodes, key=_by_idx) @property @@ -300,7 +305,7 @@ def graph(self): and directed edges pointing from nodes to their immediate dependents/successors. Returns: - networkx.DiGraph: the directed acyclic graph representing the quantum circuit + retworkx.PyDiGraph: the directed acyclic graph representing the quantum circuit """ return self._graph @@ -324,7 +329,8 @@ def ancestors(self, ops): Returns: set[Operator]: ancestors of the given operators """ - return set().union(*(nx.dag.ancestors(self._graph, o) for o in ops)) - set(ops) + # return set().union(*(nx.dag.ancestors(self._graph, o) for o in ops)) - set(ops) + return set().union(*(rx.ancestors(self._graph, o) for o in ops)) - set(ops) # rx. def descendants(self, ops): """Descendants of a given set of operators. @@ -335,7 +341,8 @@ def descendants(self, ops): Returns: set[Operator]: descendants of the given operators """ - return set().union(*(nx.dag.descendants(self._graph, o) for o in ops)) - set(ops) + # return set().union(*(nx.dag.descendants(self._graph, o) for o in ops)) - set(ops) + return set().union(*(rx.descendants(self._graph, o) for o in ops)) - set(ops) # rx. def _in_topological_order(self, ops): """Sorts a set of operators in the circuit in a topological order. @@ -346,8 +353,9 @@ def _in_topological_order(self, ops): Returns: Iterable[Operator]: same set of operators, topologically ordered """ - G = nx.DiGraph(self._graph.subgraph(ops)) - return nx.dag.topological_sort(G) + # G = nx.DiGraph(self._graph.subgraph(ops)) + # return nx.dag.topological_sort(G) + return rx.topological_sort(self._graph.subgraph(ops)) # rx. def ancestors_in_order(self, ops): """Operator ancestors in a topological order. @@ -586,7 +594,10 @@ def update_node(self, old, new): if new.wires != old.wires: raise ValueError("The new Operator must act on the same wires as the old one.") new.queue_idx = old.queue_idx - nx.relabel_nodes(self._graph, {old: new}, copy=False) # change the graph in place + + # nx.relabel_nodes(self._graph, {old: new}, copy=False) # change the graph in place + self._graph[old] = new # rx. + self._operations = self.operations_in_order self._observables = self.observables_in_order @@ -631,7 +642,9 @@ def get_depth(self): if self._depth is None and self.operations: if self._operation_graph is None: self._operation_graph = self.graph.subgraph(self.operations) - self._depth = nx.dag_longest_path_length(self._operation_graph) + 1 + + # self._depth = nx.dag_longest_path_length(self._operation_graph) + 1 + self._depth = rx.dag_longest_path_length(self._operation_graph, weight_fn=lambda edge: edge) + 1 # rx. return self._depth @@ -645,7 +658,8 @@ def has_path(self, a, b): Returns: bool: returns ``True`` if a path exists """ - return nx.has_path(self._graph, a, b) + # return nx.has_path(self._graph, a, b) + return len(rx.dijkstra_shortest_paths(self._graph, a, b)) != 0 # rx. @property def max_simultaneous_measurements(self): diff --git a/pennylane/qaoa/layers.py b/pennylane/qaoa/layers.py index 63f6fdd44d4..bce194994ac 100644 --- a/pennylane/qaoa/layers.py +++ b/pennylane/qaoa/layers.py @@ -12,691 +12,149 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -This module contains the CircuitGraph class which is used to generate a DAG (directed acyclic graph) -representation of a quantum circuit from an Operator queue. +Methods that define cost and mixer layers for use in QAOA workflows. """ -# pylint: disable=too-many-branches,too-many-arguments,too-many-instance-attributes -from collections import Counter, OrderedDict, namedtuple - -import networkx as nx -import retworkx as rx - import pennylane as qml -import numpy as np - -from pennylane.wires import Wires -from .circuit_drawer import CircuitDrawer +from pennylane.operation import Tensor -def _by_idx(x): - """Sorting key for Operators: queue index aka temporal order. +def _diagonal_terms(hamiltonian): + r"""Checks if all terms in a Hamiltonian are products of diagonal Pauli gates + (:class:`~.PauliZ` and :class:`~.Identity`). Args: - x (Operator): node in the circuit graph + hamiltonian (.Hamiltonian): The Hamiltonian being checked + Returns: - int: sorting key for the node + bool: ``True`` if all terms are products of diagonal Pauli gates, ``False`` otherwise """ - return x.queue_idx + val = True + for i in hamiltonian.ops: + obs = i.obs if isinstance(i, Tensor) else [i] + for j in obs: + if j.name not in ("PauliZ", "Identity"): + val = False + break -def _is_observable(x): - """Predicate for deciding if an Operator instance is an observable. + return val - .. note:: - Currently some :class:`Observable` instances are not observables in this sense, - since they can be used as gates as well. - Args: - x (Operator): node in the circuit graph - Returns: - bool: True iff x is an observable - """ - return getattr(x, "return_type", None) is not None +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} -def _list_at_index_or_none(ls, idx): - """Return the element of a list at the given index if it exists, return None otherwise. + where :math:`\gamma` is a variational parameter. Args: - ls (list[object]): The target list - idx (int): The target index + gamma (int or float): The variational parameter passed into the cost layer + hamiltonian (.Hamiltonian): The cost Hamiltonian - Returns: - Union[object,NoneType]: The element at the target index or None - """ - if len(ls) > idx: - return ls[idx] + Raises: + ValueError: if the terms of the supplied cost Hamiltonian are not exclusively products of diagonal Pauli gates - return None + .. UsageDetails:: + We first define a cost Hamiltonian: -def _is_returned_observable(op): - """Helper for the condition of having an observable or - measurement process in the return statement. + .. code-block:: python3 - Returns: - bool: whether or not the observable or measurement process is in the - return statement - """ - is_obs = isinstance(op, (qml.operation.Observable, qml.measure.MeasurementProcess)) - return is_obs and op.return_type is not None + from pennylane import qaoa + import pennylane as qml + cost_h = qml.Hamiltonian([1, 1], [qml.PauliZ(0), qml.PauliZ(0) @ qml.PauliZ(1)]) -Layer = namedtuple("Layer", ["ops", "param_inds"]) -"""Parametrized layer of the circuit. + We can then pass it into ``qaoa.cost_layer``, within a quantum circuit: -Args: + .. code-block:: python - ops (list[Operator]): parametrized operators in the layer - param_inds (list[int]): corresponding free parameter indices -""" -# TODO define what a layer is + dev = qml.device('default.qubit', wires=2) -LayerData = namedtuple("LayerData", ["pre_ops", "ops", "param_inds", "post_ops"]) -"""Parametrized layer of the circuit. + @qml.qnode(dev) + def circuit(gamma): -Args: - pre_ops (list[Operator]): operators that precede the layer - ops (list[Operator]): parametrized operators in the layer - param_inds (tuple[int]): corresponding free parameter indices - post_ops (list[Operator]): operators that succeed the layer -""" + for i in range(2): + qml.Hadamard(wires=i) + cost_layer(gamma, cost_h) -class CircuitGraph: - """Represents a quantum circuit as a directed acyclic graph. + return [qml.expval(qml.PauliZ(wires=i)) for i in range(2)] - In this representation the :class:`~.Operator` instances are the nodes of the graph, - and each directed edge represent a subsystem (or a group of subsystems) on which the two - Operators act subsequently. This representation can describe the causal relationships - between arbitrary quantum channels and measurements, not just unitary gates. + which gives us a circuit of the form: - Args: - ops (Iterable[.Operator]): quantum operators constituting the circuit, in temporal order - obs (Iterable[.MeasurementProcess]): terminal measurements, in temporal order - wires (.Wires): The addressable wire registers of the device that will be executing this graph - par_info (dict[int, dict[str, .Operation or int]]): Parameter information. Keys are - parameter indices (in the order they appear on the tape), and values are a - dictionary containing the corresponding operation and operation parameter index. - trainable_params (set[int]): A set containing the indices of parameters that support - differentiability. The indices provided match the order of appearence in the - quantum circuit. - """ + >>> print(qml.draw(circuit)(0.5)) + 0: ──H──RZ(1)──╭RZ(1)──┤ ⟨Z⟩ + 1: ──H─────────╰RZ(1)──┤ ⟨Z⟩ - # pylint: disable=too-many-public-methods - - def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): - self._operations = ops - self._observables = obs - self.par_info = par_info - self.trainable_params = trainable_params - - queue = ops + obs - - self._depth = None - - self._grid = {} - """dict[int, list[Operator]]: dictionary representing the quantum circuit as a grid. - Here, the key is the wire number, and the value is a list containing the operators on that wire. - """ - self.wires = wires - """Wires: wires that are addressed in the operations. - Required to translate between wires and indices of the wires on the device.""" - self.num_wires = len(wires) - """int: number of wires the circuit contains""" - for k, op in enumerate(queue): - op.queue_idx = k # store the queue index in the Operator - - if hasattr(op, "return_type"): - if op.return_type is qml.operation.State: - # State measurements contain no wires by default, but wires are - # required for the circuit drawer, so we recreate the state - # measurement with all wires - op = qml.measure.MeasurementProcess(qml.operation.State, wires=wires) - - elif op.return_type is qml.operation.Sample and op.wires == Wires([]): - # Sampling without specifying wires is treated as sampling all wires - op = qml.measure.MeasurementProcess(qml.operation.Sample, wires=wires) - - op.queue_idx = k - - for w in op.wires: - # get the index of the wire on the device - wire = wires.index(w) - # add op to the grid, to the end of wire w - self._grid.setdefault(wire, []).append(op) - - # TODO: State preparations demolish the incoming state entirely, and therefore should have no incoming edges. - - # self._graph = nx.DiGraph() #: nx.DiGraph: DAG representation of the quantum circuit - self._graph = rx.PyDiGraph() #: rx.PyDiGraph: DAG representation of the quantum circuit - # Iterate over each (populated) wire in the grid - for wire in self._grid.values(): - # Add the first operator on the wire to the graph - # This operator does not depend on any others - self._graph.add_node(wire[0]) - - for i in range(1, len(wire)): - # For subsequent operators on the wire: - if wire[i] not in self._graph: - # Add them to the graph if they are not already - # in the graph (multi-qubit operators might already have been placed) - self._graph.add_node(wire[i]) - - # Create an edge between this and the previous operator - # self._graph.add_edge(wire[i - 1], wire[i]) - self._graph.add_edge(wire[i - 1], wire[i], '') # rx. - - # For computing depth; want only a graph with the operations, not - # including the observables - self._operation_graph = None - - # Required to keep track if we need to handle multiple returned - # observables per wire - self._max_simultaneous_measurements = None - - def print_contents(self): - """Prints the contents of the quantum circuit.""" - - print("Operations") - print("==========") - for op in self.operations: - print(repr(op)) - - print("\nObservables") - print("===========") - for op in self.observables: - print(repr(op)) - - def serialize(self): - """Serialize the quantum circuit graph based on the operations and - observables in the circuit graph and the index of the variables - used by them. - - The string that is produced can be later hashed to assign a unique value to the circuit graph. - - Returns: - string: serialized quantum circuit graph - """ - serialization_string = "" - delimiter = "!" - - for op in self.operations_in_order: - serialization_string += op.name - - for param in op.data: - serialization_string += delimiter - serialization_string += str(param) - serialization_string += delimiter - - serialization_string += str(op.wires.tolist()) - - # Adding a distinct separating string that could not occur by any combination of the - # name of the operation and wires - serialization_string += "|||" - - for obs in self.observables_in_order: - serialization_string += str(obs.return_type) - serialization_string += delimiter - serialization_string += str(obs.name) - for param in obs.data: - serialization_string += delimiter - serialization_string += str(param) - serialization_string += delimiter - - serialization_string += str(obs.wires.tolist()) - return serialization_string - - @property - def hash(self): - """Creating a hash for the circuit graph based on the string generated by serialize. - - Returns: - int: the hash of the serialized quantum circuit graph - """ - return hash(self.serialize()) - - @property - def observables_in_order(self): - """Observables in the circuit, in a fixed topological order. - - The topological order used by this method is guaranteed to be the same - as the order in which the measured observables are returned by the quantum function. - Currently the topological order is determined by the queue index. - - Returns: - list[Observable]: observables - """ - # nodes = [node for node in self._graph.nodes if _is_observable(node)] - nodes = [node for node in self._graph.nodes() if _is_observable(node)] # rx. - return sorted(nodes, key=_by_idx) - - @property - def observables(self): - """Observables in the circuit.""" - return self._observables - - @property - def operations_in_order(self): - """Operations in the circuit, in a fixed topological order. - - Currently the topological order is determined by the queue index. - - The complement of :meth:`QNode.observables`. Together they return every :class:`Operator` - instance in the circuit. - - Returns: - list[Operation]: operations - """ - # nodes = [node for node in self._graph.nodes if not _is_observable(node)] - nodes = [node for node in self._graph.nodes() if not _is_observable(node)] # rx. - return sorted(nodes, key=_by_idx) - - @property - def operations(self): - """Operations in the circuit.""" - return self._operations - - @property - def graph(self): - """The graph representation of the quantum circuit. - - The graph has nodes representing :class:`.Operator` instances, - and directed edges pointing from nodes to their immediate dependents/successors. - - Returns: - retworkx.PyDiGraph: the directed acyclic graph representing the quantum circuit - """ - return self._graph - - def wire_indices(self, wire): - """Operator indices on the given wire. - - Args: - wire (int): wire to examine - - Returns: - list[int]: indices of operators on the wire, in temporal order - """ - return [op.queue_idx for op in self._grid[wire]] - - def ancestors(self, ops): - """Ancestors of a given set of operators. - - Args: - ops (Iterable[Operator]): set of operators in the circuit - - Returns: - set[Operator]: ancestors of the given operators - """ - # return set().union(*(nx.dag.ancestors(self._graph, o) for o in ops)) - set(ops) - return set().union(*(rx.ancestors(self._graph, o) for o in ops)) - set(ops) # rx. - - def descendants(self, ops): - """Descendants of a given set of operators. - - Args: - ops (Iterable[Operator]): set of operators in the circuit - - Returns: - set[Operator]: descendants of the given operators - """ - # return set().union(*(nx.dag.descendants(self._graph, o) for o in ops)) - set(ops) - return set().union(*(rx.descendants(self._graph, o) for o in ops)) - set(ops) # rx. - - def _in_topological_order(self, ops): - """Sorts a set of operators in the circuit in a topological order. - - Args: - ops (Iterable[Operator]): set of operators in the circuit - - Returns: - Iterable[Operator]: same set of operators, topologically ordered - """ - # G = nx.DiGraph(self._graph.subgraph(ops)) - # return nx.dag.topological_sort(G) - return rx.topological_sort(self._graph.subgraph(ops)) # rx. - - def ancestors_in_order(self, ops): - """Operator ancestors in a topological order. - - Currently the topological order is determined by the queue index. - - Args: - ops (Iterable[Operator]): set of operators in the circuit - - Returns: - list[Operator]: ancestors of the given operators, topologically ordered - """ - # return self._in_topological_order(self.ancestors(ops)) # an abitrary topological order - return sorted(self.ancestors(ops), key=_by_idx) - - def descendants_in_order(self, ops): - """Operator descendants in a topological order. - - Currently the topological order is determined by the queue index. - - Args: - ops (Iterable[Operator]): set of operators in the circuit - - Returns: - list[Operator]: descendants of the given operators, topologically ordered - """ - return sorted(self.descendants(ops), key=_by_idx) - - def nodes_between(self, a, b): - r"""Nodes on all the directed paths between the two given nodes. - - Returns the set of all nodes ``s`` that fulfill :math:`a \le s \le b`. - There is a directed path from ``a`` via ``s`` to ``b`` iff the set is nonempty. - The endpoints belong to the path. - - Args: - a (Operator): initial node - b (Operator): final node - - Returns: - set[Operator]: nodes on all the directed paths between a and b - """ - A = self.descendants([a]) - A.add(a) - B = self.ancestors([b]) - B.add(b) - return A & B - - def invisible_operations(self): - """Operations that cannot affect the circuit output. - - An :class:`Operation` instance in a quantum circuit is *invisible* if is not an ancestor - of an observable. Such an operation cannot affect the circuit output, and usually indicates - there is something wrong with the circuit. - - Returns: - set[Operator]: operations that cannot affect the output - """ - visible = self.ancestors(self.observables) - invisible = set(self.operations) - visible - return invisible - - @property - def parametrized_layers(self): - """Identify the parametrized layer structure of the circuit. - - Returns: - list[Layer]: layers of the circuit - """ - # FIXME maybe layering should be greedier, for example [a0 b0 c1 d1] should layer as [a0 - # c1], [b0, d1] and not [a0], [b0 c1], [d1] keep track of the current layer - current = Layer([], []) - layers = [current] - - for idx, info in self.par_info.items(): - if idx in self.trainable_params: - op = info["op"] - - # get all predecessor ops of the op - sub = self.ancestors((op,)) - - # check if any of the dependents are in the - # currently assembled layer - if set(current.ops) & sub: - # operator depends on current layer, start a new layer - current = Layer([], []) - layers.append(current) - - # store the parameters and ops indices for the layer - current.ops.append(op) - current.param_inds.append(idx) - - return layers - - def iterate_parametrized_layers(self): - """Parametrized layers of the circuit. - - Returns: - Iterable[LayerData]: layers with extra metadata - """ - # iterate through each layer - for ops, param_inds in self.parametrized_layers: - pre_queue = self.ancestors_in_order(ops) - post_queue = self.descendants_in_order(ops) - yield LayerData(pre_queue, ops, tuple(param_inds), post_queue) - - def greedy_layers(self, wire_order=None, show_all_wires=False): - """Greedily collected layers of the circuit. Empty slots are filled with ``None``. - - Layers are built by pushing back gates in the circuit as far as possible, so that - every Gate is at the lower possible layer. - - Args: - wire_order (Wires): the order (from top to bottom) to print the wires of the circuit - show_all_wires (bool): If True, all wires, including empty wires, are printed. - - Returns: - Tuple[list[list[~.Operation]], list[list[~.Observable]]]: - Tuple of the circuits operations and the circuits observables, both indexed - by wires. - """ - l = 0 - - operations = OrderedDict() - for key in sorted(self._grid): - operations[key] = self._grid[key] - - for wire in operations: - operations[wire] = list( - filter( - lambda op: not ( - isinstance(op, (qml.operation.Observable, qml.measure.MeasurementProcess)) - and op.return_type is not None - ), - operations[wire], - ) + """ + if not isinstance(hamiltonian, qml.Hamiltonian): + raise ValueError( + "hamiltonian must be of type pennylane.Hamiltonian, got {}".format( + type(hamiltonian).__name__ ) + ) - while True: - layer_ops = {wire: _list_at_index_or_none(operations[wire], l) for wire in operations} - num_ops = Counter(layer_ops.values()) + if not _diagonal_terms(hamiltonian): + raise ValueError("hamiltonian must be written only in terms of PauliZ and Identity gates") - if None in num_ops and num_ops[None] == len(operations): - break + qml.templates.ApproxTimeEvolution(hamiltonian, gamma, 1) + + +def mixer_layer(alpha, hamiltonian): + r"""Applies the QAOA mixer layer corresponding to a mixer Hamiltonian. - for (wire, op) in layer_ops.items(): - if op is None: - operations[wire].append(None) - continue - - # push back to next layer if not all args wires are there yet - if len(op.wires) > num_ops[op]: - operations[wire].insert(l, None) - - l += 1 - - observables = OrderedDict() - - if self.max_simultaneous_measurements == 1: - - # There is a single measurement for every wire - for wire in sorted(self._grid): - observables[wire] = list( - filter( - lambda op: isinstance( - op, (qml.operation.Observable, qml.measure.MeasurementProcess) - ) - and op.return_type is not None, - self._grid[wire], - ) - ) - if not observables[wire]: - observables[wire] = [None] - else: - - # There are wire(s) with multiple measurements. - # We are creating a separate "visual block" at the end of the - # circuit for each observable and mapping observables with block - # indices. - num_observables = len(self.observables) - mp_map = dict(zip(self.observables, range(num_observables))) - - for wire in sorted(self._grid): - # Initialize to None everywhere - observables[wire] = [None] * num_observables - - for op in self._grid[wire]: - if _is_returned_observable(op): - obs_idx = mp_map[op] - observables[wire][obs_idx] = op - - if wire_order is not None: - temp_op_grid = OrderedDict() - temp_obs_grid = OrderedDict() - - if show_all_wires: - permutation = [ - self.wires.labels.index(i) if i in self.wires else None - for i in wire_order.labels - ] - else: - permutation = [ - self.wires.labels.index(i) for i in wire_order.labels if i in self.wires - ] - - for i, j in enumerate(permutation): - if j is None: - temp_op_grid[i] = [None] * len(operations[0]) - temp_obs_grid[i] = [None] * len(observables[0]) - continue - - if j in operations: - temp_op_grid[i] = operations[j] - if j in observables: - temp_obs_grid[i] = observables[j] - - operations = temp_op_grid - observables = temp_obs_grid - - op_grid = [operations[wire] for wire in operations] - obs_grid = [observables[wire] for wire in observables] - - return op_grid, obs_grid - - def update_node(self, old, new): - """Replaces the given circuit graph node with a new one. - - Args: - old (Operator): node to replace - new (Operator): replacement - - Raises: - ValueError: if the new :class:`~.Operator` does not act on the same wires as the old one - """ - # NOTE Does not alter the graph edges in any way. variable_deps is not changed, _grid is not changed. Dangerous! - if new.wires != old.wires: - raise ValueError("The new Operator must act on the same wires as the old one.") - new.queue_idx = old.queue_idx - - # nx.relabel_nodes(self._graph, {old: new}, copy=False) # change the graph in place - self._graph[old] = new # rx. - - self._operations = self.operations_in_order - self._observables = self.observables_in_order - - def draw(self, charset="unicode", wire_order=None, show_all_wires=False): - """Draw the CircuitGraph as a circuit diagram. - - Args: - charset (str, optional): The charset that should be used. Currently, "unicode" and "ascii" are supported. - wire_order (Wires or None): the order (from top to bottom) to print the wires of the circuit - show_all_wires (bool): If True, all wires, including empty wires, are printed. - - Raises: - ValueError: If the given charset is not supported - - Returns: - str: The circuit diagram representation of the ``CircuitGraph`` - """ - if wire_order is not None: - wire_order = qml.wires.Wires.all_wires([wire_order, self.wires]) - - grid, obs = self.greedy_layers(wire_order=wire_order, show_all_wires=show_all_wires) - - drawer = CircuitDrawer( - grid, - obs, - wires=wire_order or self.wires, - charset=charset, - show_all_wires=show_all_wires, + 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 + + .. UsageDetails:: + + We first define a mixer Hamiltonian: + + .. code-block:: python3 + + from pennylane import qaoa + import pennylane as qml + + mixer_h = qml.Hamiltonian([1, 1], [qml.PauliX(0), qml.PauliX(0) @ qml.PauliX(1)]) + + We can then pass it into ``qaoa.mixer_layer``, within a quantum circuit: + + .. code-block:: python + + dev = qml.device('default.qubit', wires=2) + + @qml.qnode(dev) + def circuit(alpha): + + for i in range(2): + qml.Hadamard(wires=i) + + qaoa.mixer_layer(alpha, mixer_h) + + return [qml.expval(qml.PauliZ(wires=i)) for i in range(2)] + + which gives us a circuit of the form: + + >>> print(qml.draw(circuit)(0.5)) + 0: ──H──RZ(1)──H──H──╭RZ(1)──H──┤ ⟨Z⟩ + 1: ──H───────────────╰RZ(1)──H──┤ ⟨Z⟩ + + """ + if not isinstance(hamiltonian, qml.Hamiltonian): + raise ValueError( + "hamiltonian must be of type pennylane.Hamiltonian, got {}".format( + type(hamiltonian).__name__ + ) ) - return drawer.draw() - - def get_depth(self): - """Depth of the quantum circuit (longest path in the DAG).""" - # If there are no operations in the circuit, the depth is 0 - if not self.operations: - self._depth = 0 - - # If there are operations but depth is uncomputed, compute the truncated graph - # with only the operations, and return the longest path + 1 (since the path is - # expressed in terms of edges, and we want it in terms of nodes). - if self._depth is None and self.operations: - if self._operation_graph is None: - self._operation_graph = self.graph.subgraph(self.operations) - - # self._depth = nx.dag_longest_path_length(self._operation_graph) + 1 - self._depth = rx.dag_longest_path_length(self._operation_graph, weight_fn=lambda edge: edge) + 1 # rx. - - return self._depth - - def has_path(self, a, b): - """Checks if a path exists between the two given nodes. - - Args: - a (Operator): initial node - b (Operator): final node - - Returns: - bool: returns ``True`` if a path exists - """ - # return nx.has_path(self._graph, a, b) - return len(rx.dijkstra_shortest_paths(self._graph, a, b)) != 0 # rx. - - @property - def max_simultaneous_measurements(self): - """Returns the maximum number of measurements on any wire in the circuit graph. - - This method counts the number of measurements for each wire and returns - the maximum. - - **Examples** - - - >>> dev = qml.device('default.qubit', wires=3) - >>> def circuit_measure_max_once(): - ... return qml.expval(qml.PauliX(wires=0)) - >>> qnode = qml.QNode(circuit_measure_max_once, dev) - >>> qnode() - >>> qnode.qtape.graph.max_simultaneous_measurements - 1 - >>> def circuit_measure_max_twice(): - ... return qml.expval(qml.PauliX(wires=0)), qml.probs(wires=0) - >>> qnode = qml.QNode(circuit_measure_max_twice, dev) - >>> qnode() - >>> qnode.qtape.graph.max_simultaneous_measurements - 2 - - Returns: - int: the maximum number of measurements - """ - if self._max_simultaneous_measurements is None: - all_wires = [] - - for obs in self.observables: - all_wires.extend(obs.wires.tolist()) - - a = np.array(all_wires) - _, counts = np.unique(a, return_counts=True) - self._max_simultaneous_measurements = ( - counts.max() if counts.size != 0 else 1 - ) # qml.state() will result in an empty array - return self._max_simultaneous_measurements + qml.templates.ApproxTimeEvolution(hamiltonian, alpha, 1) From 5e03a5907051b6cbf6a21b0a6a0dc317e0a40530 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Thu, 21 Oct 2021 17:53:47 -0400 Subject: [PATCH 10/22] Update RX in circuit_graph --- pennylane/circuit_graph.py | 16 ++++++++---- tests/circuit_graph/test_circuit_graph.py | 31 +++++++++++++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 63f6fdd44d4..d96781e463a 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -178,14 +178,17 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): for i in range(1, len(wire)): # For subsequent operators on the wire: - if wire[i] not in self._graph: + # if wire[i] not in self._graph: + if wire[i] not in self._graph.nodes(): # rx. # Add them to the graph if they are not already # in the graph (multi-qubit operators might already have been placed) self._graph.add_node(wire[i]) # Create an edge between this and the previous operator # self._graph.add_edge(wire[i - 1], wire[i]) - self._graph.add_edge(wire[i - 1], wire[i], '') # rx. + self._graph.add_edge(self._graph.nodes().index(wire[i - 1]), + self._graph.nodes().index(wire[i]), + '') # rx. # For computing depth; want only a graph with the operations, not # including the observables @@ -330,7 +333,8 @@ def ancestors(self, ops): set[Operator]: ancestors of the given operators """ # return set().union(*(nx.dag.ancestors(self._graph, o) for o in ops)) - set(ops) - return set().union(*(rx.ancestors(self._graph, o) for o in ops)) - set(ops) # rx. + anc = set(self._graph.get_node_data(n) for n in set().union(*(rx.ancestors(self._graph, self._graph.nodes().index(o)) for o in ops))) # rx. + return anc - set(ops) # rx. def descendants(self, ops): """Descendants of a given set of operators. @@ -342,7 +346,8 @@ def descendants(self, ops): set[Operator]: descendants of the given operators """ # return set().union(*(nx.dag.descendants(self._graph, o) for o in ops)) - set(ops) - return set().union(*(rx.descendants(self._graph, o) for o in ops)) - set(ops) # rx. + des = set(self._graph.get_node_data(n) for n in set().union(*(rx.descendants(self._graph, self._graph.nodes().index(o)) for o in ops))) # rx. + return des - set(ops) # rx. def _in_topological_order(self, ops): """Sorts a set of operators in the circuit in a topological order. @@ -596,7 +601,8 @@ def update_node(self, old, new): new.queue_idx = old.queue_idx # nx.relabel_nodes(self._graph, {old: new}, copy=False) # change the graph in place - self._graph[old] = new # rx. + # self._graph[old] = new # rx. + self._graph.update_edge_by_index(self._graph.nodes().index(old), new) # rx. self._operations = self.operations_in_order self._observables = self.observables_in_order diff --git a/tests/circuit_graph/test_circuit_graph.py b/tests/circuit_graph/test_circuit_graph.py index 25b3579e3b1..d214ffc0a06 100644 --- a/tests/circuit_graph/test_circuit_graph.py +++ b/tests/circuit_graph/test_circuit_graph.py @@ -146,21 +146,40 @@ def test_dependence(self, ops, obs): circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) graph = circuit.graph - assert len(graph) == 9 + # assert len(graph) == 9 + assert len(graph.node_indexes()) == 9 # rx. assert len(graph.edges()) == 9 queue = ops + obs # all ops should be nodes in the graph for k in queue: - assert k in graph.nodes + # assert k in graph.nodes + assert k in graph.nodes() # rx. # all nodes in the graph should be ops - for k in graph.nodes: + # for k in graph.nodes: + for k in graph.nodes(): # rx. assert k is queue[k.queue_idx] # Finally, checking the adjacency of the returned DAG: - assert set(graph.edges()) == set( + # assert set(graph.edges()) == set( + # (queue[a], queue[b]) + # for a, b in [ + # (0, 3), + # (1, 3), + # (2, 4), + # (3, 5), + # (3, 6), + # (4, 5), + # (5, 7), + # (5, 8), + # (6, 8), + # ] + # ) + a = set((graph.get_node_data(e[0]), graph.get_node_data(e[1])) + for e in graph.edge_list()) # rx. + b = set( (queue[a], queue[b]) for a, b in [ (0, 3), @@ -173,7 +192,9 @@ def test_dependence(self, ops, obs): (5, 8), (6, 8), ] - ) + ) # rx. + assert a == b # rx. + def test_ancestors_and_descendants_example(self, ops, obs): """ From d0cf841bbeb19aa73d847cbb3d843eaa36e78fb3 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Thu, 21 Oct 2021 23:14:14 -0400 Subject: [PATCH 11/22] Update CircuitGraph tests --- pennylane/circuit_graph.py | 13 ++++++++----- tests/circuit_graph/test_circuit_graph.py | 6 ++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index d96781e463a..28f5d935200 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -360,7 +360,9 @@ def _in_topological_order(self, ops): """ # G = nx.DiGraph(self._graph.subgraph(ops)) # return nx.dag.topological_sort(G) - return rx.topological_sort(self._graph.subgraph(ops)) # rx. + G = self._graph.subgraph( list(self._graph.nodes().index(o) for o in ops)) # rx. + indexes = rx.topological_sort(G) # rx. + return list(G[x] for x in indexes) # rx. def ancestors_in_order(self, ops): """Operator ancestors in a topological order. @@ -602,7 +604,7 @@ def update_node(self, old, new): # nx.relabel_nodes(self._graph, {old: new}, copy=False) # change the graph in place # self._graph[old] = new # rx. - self._graph.update_edge_by_index(self._graph.nodes().index(old), new) # rx. + self._graph[self._graph.nodes().index(old)] = new # rx. self._operations = self.operations_in_order self._observables = self.observables_in_order @@ -647,10 +649,11 @@ def get_depth(self): # expressed in terms of edges, and we want it in terms of nodes). if self._depth is None and self.operations: if self._operation_graph is None: - self._operation_graph = self.graph.subgraph(self.operations) + # self._operation_graph = self.graph.subgraph(self.operations) + self._operation_graph = self.graph.subgraph(list(self.graph.nodes().index(node) for node in self.operations)) # rx/ # self._depth = nx.dag_longest_path_length(self._operation_graph) + 1 - self._depth = rx.dag_longest_path_length(self._operation_graph, weight_fn=lambda edge: edge) + 1 # rx. + self._depth = rx.dag_longest_path_length(self._operation_graph) + 1 # rx. return self._depth @@ -665,7 +668,7 @@ def has_path(self, a, b): bool: returns ``True`` if a path exists """ # return nx.has_path(self._graph, a, b) - return len(rx.dijkstra_shortest_paths(self._graph, a, b)) != 0 # rx. + return len(rx.dijkstra_shortest_paths(self._graph, self._graph.nodes().index(a), self._graph.nodes().index(b))) != 0 # rx. @property def max_simultaneous_measurements(self): diff --git a/tests/circuit_graph/test_circuit_graph.py b/tests/circuit_graph/test_circuit_graph.py index d214ffc0a06..a6d8d78dca2 100644 --- a/tests/circuit_graph/test_circuit_graph.py +++ b/tests/circuit_graph/test_circuit_graph.py @@ -222,11 +222,13 @@ def test_update_node(self, ops, obs): def test_observables(self, circuit, obs): """Test that the `observables` property returns the list of observables in the circuit.""" - assert circuit.observables == obs + # assert circuit.observables == obs + assert str(circuit.observables) == str(obs) # rx. def test_operations(self, circuit, ops): """Test that the `operations` property returns the list of operations in the circuit.""" - assert circuit.operations == ops + # assert circuit.operations == ops + assert str(circuit.operations) == str(ops) # rx. def test_op_indices(self, circuit): """Test that for the given circuit, this method will fetch the correct operation indices for From cb0192799ae88d333b81acbb9820fe6eae522782 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Fri, 17 Dec 2021 01:22:34 -0500 Subject: [PATCH 12/22] Update RX @ qaoa --- pennylane/qaoa/cost.py | 87 ++-- pennylane/qaoa/cycle.py | 189 +++++---- pennylane/qaoa/mixers.py | 28 +- tests/circuit_graph/test_circuit_graph.py | 20 +- tests/test_qaoa.py | 478 +++++++++++++++++----- 5 files changed, 584 insertions(+), 218 deletions(-) diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py index 385a0d7b18a..bcb590bc3a3 100644 --- a/pennylane/qaoa/cost.py +++ b/pennylane/qaoa/cost.py @@ -105,9 +105,9 @@ def edge_driver(graph, reward): + (0.25) [Z1 Z2] >>> import retworkx as rx - >>> g = rx.PyGraph() - >>> g.add_nodes_from([0, 1, 2]) - >>> g.add_edges_from([(0, 1,""), (1,2,"")]) + >>> 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) [Z0] @@ -177,13 +177,17 @@ def edge_driver(graph, reward): ) if not isinstance(graph, (nx.Graph, rx.PyGraph)): - raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + ) coeffs = [] ops = [] - graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes - graph_edges = graph.edge_list() if isinstance(graph, rx.PyGraph) else graph.edges + is_rx = isinstance(graph, rx.PyGraph) + graph_nodes = graph.nodes() if is_rx else graph.nodes + graph_edges = sorted(graph.edge_list()) if is_rx else graph.edges + get_nvalue = lambda i: graph_nodes[i] if is_rx else i if len(reward) == 0 or len(reward) == 4: coeffs = [1 for _ in graph_nodes] @@ -204,19 +208,27 @@ def edge_driver(graph, reward): for e in graph_edges: coeffs.extend([0.25 * sign, 0.25 * sign, 0.25 * sign]) ops.extend( - [qml.PauliZ(e[0]) @ qml.PauliZ(e[1]), qml.PauliZ(e[0]), qml.PauliZ(e[1])] + [ + qml.PauliZ(get_nvalue(e[0])) @ qml.PauliZ(get_nvalue(e[1])), + qml.PauliZ(get_nvalue(e[0])), + qml.PauliZ(get_nvalue(e[1])), + ] ) if reward == "10": for e in graph_edges: coeffs.append(-0.5 * sign) - ops.append(qml.PauliZ(e[0]) @ qml.PauliZ(e[1])) + ops.append(qml.PauliZ(get_nvalue(e[0])) @ qml.PauliZ(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( - [qml.PauliZ(e[0]) @ qml.PauliZ(e[1]), qml.PauliZ(e[0]), qml.PauliZ(e[1])] + [ + qml.PauliZ(get_nvalue(e[0])) @ qml.PauliZ(get_nvalue(e[1])), + qml.PauliZ(get_nvalue(e[0])), + qml.PauliZ(get_nvalue(e[1])), + ] ) return qml.Hamiltonian(coeffs, ops) @@ -268,16 +280,34 @@ def maxcut(graph): (1) [X0] + (1) [X1] + (1) [X2] + + >>> import retworkx as rx + >>> graph = rx.PyGraph() + >>> graph.add_nodes_from([0, 1, 2]) + >>> graph.add_edges_from([(0, 1,""), (1,2,"")]) + >>> cost_h, mixer_h = qml.qaoa.maxcut(graph) + >>> print(cost_h) + (-1.0) [I0] + + (0.5) [Z0 Z1] + + (0.5) [Z1 Z2] + >>> print(mixer_h) + (1) [X0] + + (1) [X1] + + (1) [X2] """ if not isinstance(graph, (nx.Graph, rx.PyGraph)): - raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + ) - graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes - graph_edges = graph.edge_list() if isinstance(graph, rx.PyGraph) else graph.edges + is_rx = isinstance(graph, rx.PyGraph) + graph_nodes = graph.nodes() if is_rx else graph.nodes + graph_edges = sorted(graph.edge_list()) if is_rx else graph.edges + get_nvalue = lambda i: graph_nodes[i] if is_rx else i identity_h = qml.Hamiltonian( - [-0.5 for e in graph_edges], [qml.Identity(e[0]) @ qml.Identity(e[1]) for e in graph_edges] + [-0.5 for e in graph_edges], [qml.Identity(get_nvalue(e[0])) @ qml.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 @@ -343,9 +373,11 @@ def max_independent_set(graph, constrained=True): """ if not isinstance(graph, (nx.Graph, rx.PyGraph)): - raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + ) - graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes + graph_nodes = graph.nodes() if isinstance(graph, rx.PyGraph) else graph.nodes if constrained: cost_h = bit_driver(graph_nodes, 1) @@ -421,9 +453,11 @@ def min_vertex_cover(graph, constrained=True): """ if not isinstance(graph, (nx.Graph, rx.PyGraph)): - raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + ) - graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes + graph_nodes = graph.nodes() if isinstance(graph, rx.PyGraph) else graph.nodes if constrained: cost_h = bit_driver(graph_nodes, 0) @@ -501,11 +535,14 @@ def max_clique(graph, constrained=True): """ if not isinstance(graph, (nx.Graph, rx.PyGraph)): - raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + ) + + is_rx = isinstance(graph, rx.PyGraph) + graph_nodes = graph.nodes() if is_rx else graph.nodes + graph_complement = rx.complement(graph) if is_rx else nx.complement(graph) - graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else 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)))] @@ -536,7 +573,7 @@ def max_weight_cycle(graph, constrained=True): our subset of edges composes a `cycle `__. Args: - graph (nx.Graph or rx.PyGraph): the directed graph on which the Hamiltonians are defined + graph (nx.Graph or rx.Py(Di)Graph): the directed graph on which the Hamiltonians are defined constrained (bool): specifies the variant of QAOA that is performed (constrained or unconstrained) Returns: @@ -655,8 +692,10 @@ def max_weight_cycle(graph, constrained=True): can be prepared using :class:`~.BasisState` or simple :class:`~.PauliX` rotations on the ``0`` and ``3`` wires. """ - if not isinstance(graph, (nx.Graph, rx.PyGraph)): - raise ValueError("Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__)) + if not isinstance(graph, (nx.Graph, rx.PyGraph, rx.PyDiGraph)): + raise ValueError( + "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + ) mapping = qaoa.cycle.wires_to_edges(graph) diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index b4685ad0871..79799da5828 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -25,6 +25,7 @@ import pennylane as qml from pennylane.ops import Hamiltonian + def edges_to_wires(graph) -> Dict[Tuple, int]: r"""Maps the edges of a graph to corresponding wires. @@ -45,23 +46,23 @@ def edges_to_wires(graph) -> Dict[Tuple, int]: (3, 1): 10, (3, 2): 11} - >>> g = rx.generators.directed_mesh_graph(4) + >>> g = rx.generators.directed_mesh_graph(4, [0,1,2,3]) >>> edges_to_wires(g) {(0, 1): 0, - (1, 0): 1, - (0, 2): 2, - (2, 0): 3, - (0, 3): 4, - (3, 0): 5, - (1, 2): 6, + (0, 2): 1, + (0, 3): 2, + (1, 0): 3, + (1, 2): 4, + (1, 3): 5, + (2, 0): 6, (2, 1): 7, - (1, 3): 8, - (3, 1): 9, - (2, 3): 10, + (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 + graph (nx.Graph or rx.Py(Di)Graph): the graph specifying possible edges Returns: Dict[Tuple, int]: a mapping from graph edges to wires @@ -69,9 +70,12 @@ def edges_to_wires(graph) -> Dict[Tuple, int]: if isinstance(graph, nx.Graph): return {edge: i for i, edge in enumerate(graph.edges)} elif isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): - return {edge: i for i, edge in enumerate(graph.edge_list())} + gnodes = graph.nodes() + return {(gnodes[e[0]], gnodes[e[1]]): i for i, e in enumerate(sorted(graph.edge_list()))} else: - raise ValueError("Input graph must be a nx.Graph, rx.Py(Di)Graph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.Graph, rx.Py(Di)Graph, got {}".format(type(graph).__name__) + ) def wires_to_edges(graph) -> Dict[int, Tuple]: @@ -94,23 +98,23 @@ def wires_to_edges(graph) -> Dict[int, Tuple]: 10: (3, 1), 11: (3, 2)} - >>> g = rx.generators.directed_mesh_graph(4) + >>> g = rx.generators.directed_mesh_graph(4, [0,1,2,3]) >>> wires_to_edges(g) {0: (0, 1), - 1: (1, 0), - 2: (0, 2), - 3: (2, 0), - 4: (0, 3), - 5: (3, 0), - 6: (1, 2), + 1: (0, 2), + 2: (0, 3), + 3: (1, 0), + 4: (1, 2), + 5: (1, 3), + 6: (2, 0), 7: (2, 1), - 8: (1, 3), - 9: (3, 1), - 10: (2, 3), + 8: (2, 3), + 9: (3, 0), + 10: (3, 1), 11: (3, 2)} Args: - graph (nx.Graph, rx.PyGraph, or rx.PyDiGraph): the graph specifying possible edges + graph (nx.Graph or rx.Py(Di)Graph): the graph specifying possible edges Returns: Dict[Tuple, int]: a mapping from wires to graph edges @@ -118,9 +122,13 @@ def wires_to_edges(graph) -> Dict[int, Tuple]: if isinstance(graph, nx.Graph): return {i: edge for i, edge in enumerate(graph.edges)} elif isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): - return {i: edge for i, edge in enumerate(graph.edge_list())} + gnodes = graph.nodes() + return {i: (gnodes[e[0]], gnodes[e[1]]) for i, e in enumerate(sorted(graph.edge_list()))} else: - raise ValueError("Input graph must be a nx.Graph or rx.Py(Di)Graph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.Graph or rx.Py(Di)Graph, got {}".format(type(graph).__name__) + ) + def cycle_mixer(graph) -> Hamiltonian: r"""Calculates the cycle-mixer Hamiltonian. @@ -170,33 +178,33 @@ def cycle_mixer(graph) -> Hamiltonian: + (0.25) [Y5 X4 Y0] >>> import retworkx as rx - >>> g = rx.generators.directed_mesh_graph(3) + >>> g = rx.generators.directed_mesh_graph(3, [0,1,2]) >>> h_m = cycle_mixer(g) >>> print(h_m) - (-0.25) [X0 Y2 Y5] - + (-0.25) [X1 Y4 Y3] - + (-0.25) [X2 Y0 Y4] - + (-0.25) [X3 Y5 Y1] - + (-0.25) [X4 Y1 Y2] - + (-0.25) [X5 Y3 Y0] - + (0.25) [X0 X2 X5] - + (0.25) [Y0 Y2 X5] - + (0.25) [Y0 X2 Y5] - + (0.25) [X1 X4 X3] - + (0.25) [Y1 Y4 X3] - + (0.25) [Y1 X4 Y3] - + (0.25) [X2 X0 X4] - + (0.25) [Y2 Y0 X4] - + (0.25) [Y2 X0 Y4] - + (0.25) [X3 X5 X1] - + (0.25) [Y3 Y5 X1] - + (0.25) [Y3 X5 Y1] - + (0.25) [X4 X1 X2] - + (0.25) [Y4 Y1 X2] - + (0.25) [Y4 X1 Y2] - + (0.25) [X5 X3 X0] - + (0.25) [Y5 Y3 X0] - + (0.25) [Y5 X3 Y0] + (-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 @@ -205,10 +213,12 @@ def cycle_mixer(graph) -> Hamiltonian: qml.Hamiltonian: the cycle-mixer Hamiltonian """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + ) hamiltonian = Hamiltonian([], []) - graph_edges = graph.edge_list() if isinstance(graph, rx.PyDiGraph) else graph.edges + 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) @@ -234,22 +244,26 @@ def _partial_cycle_mixer(graph, edge: Tuple) -> Hamiltonian: qml.Hamiltonian: the partial cycle-mixer Hamiltonian """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + ) coeffs = [] ops = [] + is_rx = isinstance(graph, rx.PyDiGraph) edges_to_qubits = edges_to_wires(graph) - graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes - graph_edges = graph.edge_list() if isinstance(graph, rx.PyDiGraph) else graph.edges + graph_nodes = graph.node_indexes() if is_rx else graph.nodes + graph_edges = sorted(graph.edge_list()) if is_rx else graph.edges + get_nvalues = lambda T: (graph.nodes()[T[0]], graph.nodes()[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[edge] - out_wire = edges_to_qubits[out_edge] - in_wire = edges_to_qubits[in_edge] + 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 = qml.PauliX(wires=wire) @ qml.PauliX(wires=out_wire) @ qml.PauliX(wires=in_wire) ops.append(t) @@ -324,7 +338,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: >>> import retworkx as rx >>> g = rx.generators.directed_mesh_graph(3) - >>> edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edge_list())} + >>> 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) @@ -337,7 +351,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: + (1.0986122886681098) [Z5] Args: - graph (nx.Graph, rx.PyGraph or rx.PyDiGraph): the graph specifying possible edges + graph (nx.Graph or rx.Py(Di)Graph): the graph specifying possible edges Returns: qml.Hamiltonian: the loss Hamiltonian @@ -347,17 +361,22 @@ def loss_hamiltonian(graph) -> Hamiltonian: KeyError: if one or more edges do not contain weight data """ if not isinstance(graph, (nx.Graph, rx.PyGraph, rx.PyDiGraph)): - raise ValueError("Input graph must be a nx.Graph or rx.Py(Di)Graph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.Graph or rx.Py(Di)Graph, got {}".format(type(graph).__name__) + ) edges_to_qubits = edges_to_wires(graph) + coeffs = [] ops = [] - edges_data = graph.weighted_edge_list() if isinstance(graph, (rx.PyGraph, rx.PyDiGraph)) else graph.edges(data=True) + is_rx = isinstance(graph, (rx.PyGraph, rx.PyDiGraph)) + edges_data = sorted(graph.weighted_edge_list()) if is_rx else graph.edges(data=True) + get_nvalues = lambda T: (graph.nodes()[T[0]], graph.nodes()[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") @@ -365,9 +384,11 @@ def loss_hamiltonian(graph) -> Hamiltonian: weight = edge_data[2]["weight"] except KeyError as e: raise KeyError(f"Edge {edge} does not contain weight data") from e + except TypeError: + weight = 0 coeffs.append(np.log(weight)) - ops.append(qml.PauliZ(wires=edges_to_qubits[edge])) + ops.append(qml.PauliZ(wires=edges_to_qubits[get_nvalues(edge)])) H = Hamiltonian(coeffs, ops) # store the valuable information that all observables are in one commuting group @@ -447,7 +468,9 @@ def out_flow_constraint(graph) -> Hamiltonian: ValueError: if the input graph is not directed """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + ) if isinstance(graph, (nx.DiGraph, rx.PyDiGraph)) and not hasattr(graph, "out_edges"): raise ValueError("Input graph must be directed") @@ -495,11 +518,15 @@ def net_flow_constraint(graph) -> Hamiltonian: Raises: ValueError: if the input graph is not directed """ - if isinstance(graph, (nx.DiGraph, rx.PyDiGraph)) and (not hasattr(graph, "in_edges") or not hasattr(graph, "out_edges")): + 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("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + ) hamiltonian = Hamiltonian([], []) graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyDiGraph) else graph.nodes @@ -528,19 +555,23 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: qml.Hamiltonian: The inner part of the out-flow constraint Hamiltonian. """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + ) coeffs = [] ops = [] + get_nvalues = lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) if isinstance(graph, rx.PyDiGraph) else T + edges_to_qubits = edges_to_wires(graph) - out_edges = graph.out_edges(node) + out_edges = sorted(graph.out_edges(node)) d = len(out_edges) for edge in out_edges: if len(edge) > 2: edge = tuple(edge[:2]) - wire = (edges_to_qubits[edge],) + wire = (edges_to_qubits[get_nvalues(edge)],) coeffs.append(1) ops.append(qml.PauliZ(wire)) @@ -549,7 +580,7 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: for edge in out_edges: if len(edge) > 2: edge = tuple(edge[:2]) - wire = (edges_to_qubits[edge],) + wire = (edges_to_qubits[get_nvalues(edge)],) coeffs.append(-2 * (d - 1)) ops.append(qml.PauliZ(wire)) @@ -583,15 +614,19 @@ def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: qml.Hamiltonian: The inner part of the net-flow constraint Hamiltonian. """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): - raise ValueError("Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__)) + raise ValueError( + "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + ) edges_to_qubits = edges_to_wires(graph) coeffs = [] ops = [] - out_edges = graph.out_edges(node) - in_edges = graph.in_edges(node) + out_edges = sorted(graph.out_edges(node)) + in_edges = sorted(graph.in_edges(node)) + + get_nvalues = lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) if isinstance(graph, rx.PyDiGraph) else T coeffs.append(len(out_edges) - len(in_edges)) ops.append(qml.Identity(0)) @@ -599,14 +634,14 @@ def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: for edge in out_edges: if len(edge) > 2: edge = tuple(edge[:2]) - wires = (edges_to_qubits[edge],) + wires = (edges_to_qubits[get_nvalues(edge)],) coeffs.append(-1) ops.append(qml.PauliZ(wires)) for edge in in_edges: if len(edge) > 2: edge = tuple(edge[:2]) - wires = (edges_to_qubits[edge],) + wires = (edges_to_qubits[get_nvalues(edge)],) coeffs.append(1) ops.append(qml.PauliZ(wires)) diff --git a/pennylane/qaoa/mixers.py b/pennylane/qaoa/mixers.py index b303fb9e514..3ab7c9bf650 100644 --- a/pennylane/qaoa/mixers.py +++ b/pennylane/qaoa/mixers.py @@ -101,7 +101,7 @@ def xy_mixer(graph): + (0.5) [X1 X2] + (0.5) [Y1 Y2] - >>> import retworkx as rx + >>> import retworkx as rx >>> graph = rx.PyGraph() >>> graph.add_nodes_from([0, 1, 2]) >>> graph.add_edges_from([(0, 1, ""), (1, 2, "")]) @@ -115,16 +115,20 @@ def xy_mixer(graph): if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph object, got {}".format(type(graph).__name__) + "Input graph must be a nx.Graph or rx.PyGraph object, got {}".format( + type(graph).__name__ + ) ) - edges = graph.edge_list() if isinstance(graph, rx.PyGraph) else graph.edges + is_rx = isinstance(graph, rx.PyGraph) + edges = graph.edge_list() if is_rx else graph.edges + 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.PauliX(node1) @ qml.PauliX(node2)) - obs.append(qml.PauliY(node1) @ qml.PauliY(node2)) + obs.append(qml.PauliX(get_nvalue(node1)) @ qml.PauliX(get_nvalue(node2))) + obs.append(qml.PauliY(get_nvalue(node1)) @ qml.PauliY(get_nvalue(node2))) return qml.Hamiltonian(coeffs, obs) @@ -192,7 +196,9 @@ def bit_flip_mixer(graph, b): if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph object, got {}".format(type(graph).__name__) + "Input graph must be a nx.Graph or rx.PyGraph object, got {}".format( + type(graph).__name__ + ) ) if b not in [0, 1]: @@ -203,14 +209,18 @@ def bit_flip_mixer(graph, b): coeffs = [] terms = [] - graph_nodes = graph.node_indexes() if isinstance(graph, rx.PyGraph) else graph.nodes + is_rx = isinstance(graph, rx.PyGraph) + graph_nodes = graph.node_indexes() if is_rx else graph.nodes + get_nvalue = lambda i: graph.nodes()[i] if is_rx else i for i in graph_nodes: - neighbours = list(graph.neighbors(i)) + neighbours = sorted(graph.neighbors(i)) if is_rx else list(graph.neighbors(i)) degree = len(neighbours) - n_terms = [[qml.PauliX(i)]] + [[qml.Identity(n), qml.PauliZ(n)] for n in neighbours] + n_terms = [[qml.PauliX(get_nvalue(i))]] + [ + [qml.Identity(get_nvalue(n)), qml.PauliZ(get_nvalue(n))] for n in neighbours + ] n_coeffs = [[1, sign] for n in neighbours] final_terms = [qml.operation.Tensor(*list(m)).prune() for m in itertools.product(*n_terms)] diff --git a/tests/circuit_graph/test_circuit_graph.py b/tests/circuit_graph/test_circuit_graph.py index a6d8d78dca2..eb415349c85 100644 --- a/tests/circuit_graph/test_circuit_graph.py +++ b/tests/circuit_graph/test_circuit_graph.py @@ -147,7 +147,7 @@ def test_dependence(self, ops, obs): circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) graph = circuit.graph # assert len(graph) == 9 - assert len(graph.node_indexes()) == 9 # rx. + assert len(graph.node_indexes()) == 9 # rx. assert len(graph.edges()) == 9 queue = ops + obs @@ -155,11 +155,11 @@ def test_dependence(self, ops, obs): # all ops should be nodes in the graph for k in queue: # assert k in graph.nodes - assert k in graph.nodes() # rx. + assert k in graph.nodes() # rx. # all nodes in the graph should be ops # for k in graph.nodes: - for k in graph.nodes(): # rx. + for k in graph.nodes(): # rx. assert k is queue[k.queue_idx] # Finally, checking the adjacency of the returned DAG: @@ -177,8 +177,9 @@ def test_dependence(self, ops, obs): # (6, 8), # ] # ) - a = set((graph.get_node_data(e[0]), graph.get_node_data(e[1])) - for e in graph.edge_list()) # rx. + a = set( + (graph.get_node_data(e[0]), graph.get_node_data(e[1])) for e in graph.edge_list() + ) # rx. b = set( (queue[a], queue[b]) for a, b in [ @@ -192,9 +193,8 @@ def test_dependence(self, ops, obs): (5, 8), (6, 8), ] - ) # rx. - assert a == b # rx. - + ) # rx. + assert a == b # rx. def test_ancestors_and_descendants_example(self, ops, obs): """ @@ -223,12 +223,12 @@ def test_update_node(self, ops, obs): def test_observables(self, circuit, obs): """Test that the `observables` property returns the list of observables in the circuit.""" # assert circuit.observables == obs - assert str(circuit.observables) == str(obs) # rx. + assert str(circuit.observables) == str(obs) # rx. def test_operations(self, circuit, ops): """Test that the `operations` property returns the list of operations in the circuit.""" # assert circuit.operations == ops - assert str(circuit.operations) == str(ops) # rx. + assert str(circuit.operations) == str(ops) # rx. def test_op_indices(self, circuit): """Test that for the given circuit, this method will fetch the correct operation indices for diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index e0f13fd298b..e0b7bedd38b 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -50,13 +50,13 @@ graph_rx = rx.PyGraph() graph_rx.add_nodes_from([0, 1, 2]) -graph_rx.add_edges_from([(0, 1, ''), (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, '')]) +non_consecutive_graph_rx.add_edges_from([(0, 4, ""), (0, 2, ""), (4, 3, ""), (2, 1, "")]) 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)} @@ -64,6 +64,9 @@ digraph_complete[k[0]][k[1]]["weight"] = v digraph_complete_rx = rx.generators.directed_mesh_graph(3) +complete_edge_weight_data = { + edge: (i + 1) * 0.5 for i, edge in enumerate(sorted(digraph_complete.edges())) +} for k, v in complete_edge_weight_data.items(): digraph_complete_rx.update_edge(k[0], k[1], {"weight": v}) @@ -71,13 +74,18 @@ g1_rx = rx.PyGraph() g1_rx.add_nodes_from([0, 1, 2]) -g1_rx.add_edges_from([(0, 1, ''), (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, '')]) +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 decompose_hamiltonian(hamiltonian): @@ -159,7 +167,9 @@ def test_xy_mixer_type_error(self): graph = [(0, 1), (1, 2)] - with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph object, got list"): + with pytest.raises( + ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph object, got list" + ): qaoa.xy_mixer(graph) @pytest.mark.parametrize( @@ -179,20 +189,6 @@ def test_xy_mixer_type_error(self): ], ), ), - ( - Graph((np.array([0, 1]), np.array([1, 2]), np.array([2, 0]))), - qml.Hamiltonian( - [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], - [ - qml.PauliX(0) @ qml.PauliX(1), - qml.PauliY(0) @ qml.PauliY(1), - qml.PauliX(0) @ qml.PauliX(2), - qml.PauliY(0) @ qml.PauliY(2), - qml.PauliX(1) @ qml.PauliX(2), - qml.PauliY(1) @ qml.PauliY(2), - ], - ), - ), ( graph, qml.Hamiltonian( @@ -263,6 +259,20 @@ def test_xy_mixer_type_error(self): ], ), ), + ( + Graph((np.array([0, 1]), np.array([1, 2]), np.array([2, 0]))), + qml.Hamiltonian( + [0.5, 0.5, 0.5, 0.5, 0.5, 0.5], + [ + qml.PauliX(0) @ qml.PauliX(1), + qml.PauliY(0) @ qml.PauliY(1), + qml.PauliX(0) @ qml.PauliX(2), + qml.PauliY(0) @ qml.PauliY(2), + qml.PauliX(1) @ qml.PauliX(2), + qml.PauliY(1) @ qml.PauliY(2), + ], + ), + ), ], ) def test_xy_mixer_output(self, graph, target_hamiltonian): @@ -286,7 +296,9 @@ 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 object"): + with pytest.raises( + ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph object" + ): qaoa.bit_flip_mixer(graph, 0) n = 2 @@ -326,6 +338,23 @@ def test_bit_flip_mixer_errors(self): ], ), ), + ( + g1_rx, + 0, + qml.Hamiltonian( + [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], + [ + qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(2), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), + qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(1), + ], + ), + ), ( Graph([("b", 1), (1, 0.3), (0.3, "b")]), 1, @@ -347,6 +376,27 @@ def test_bit_flip_mixer_errors(self): ], ), ), + ( + b_rx, + 1, + qml.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], + [ + qml.PauliX("b"), + qml.PauliX("b") @ qml.PauliZ(0.3), + qml.PauliX("b") @ qml.PauliZ(1), + qml.PauliX("b") @ qml.PauliZ(1) @ qml.PauliZ(0.3), + qml.PauliX(1), + qml.PauliX(1) @ qml.PauliZ(0.3), + qml.PauliX(1) @ qml.PauliZ("b"), + qml.PauliX(1) @ qml.PauliZ("b") @ qml.PauliZ(0.3), + qml.PauliX(0.3), + qml.PauliX(0.3) @ qml.PauliZ(1), + qml.PauliX(0.3) @ qml.PauliZ("b"), + qml.PauliX(0.3) @ qml.PauliZ("b") @ qml.PauliZ(1), + ], + ), + ), ], ) def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): @@ -360,14 +410,22 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): GRAPHS = [ g1, + g1_rx, Graph((np.array([0, 1]), np.array([1, 2]), np.array([0, 2]))), graph, - g1_rx, + graph_rx, ] -COST_COEFFS = [[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_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 = [ + [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], [ qml.PauliZ(0) @ qml.PauliZ(1), @@ -379,28 +437,48 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): [qml.PauliZ(0) @ qml.PauliZ(1), qml.PauliZ(1) @ qml.PauliZ(2), qml.Identity(0)], ] -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(4)] +COST_HAMILTONIANS = [qml.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]] +MIXER_COEFFS = [ + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], + [1, 1, 1], +] MIXER_TERMS = [ [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(4)] +MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(5)] MAXCUT = list(zip(GRAPHS, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) """GENERATES THE CASES TO TEST THE MAX INDEPENDENT SET PROBLEM""" -CONSTRAINED = [True, True, False, True] +CONSTRAINED = [ + True, + True, + True, + False, + False, +] -COST_COEFFS = [[1, 1, 1], [1, 1, 1], [0.75, 0.25, -0.5, 0.75, 0.25], [1, 1, 1]] +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 = [ + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], [ @@ -410,16 +488,24 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): qml.PauliZ(1) @ qml.PauliZ(2), qml.PauliZ(2), ], - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + # [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(2), + ], ] -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(4)] +COST_HAMILTONIANS = [qml.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], - [0.5, 0.5, 0.25, 0.25, 0.25, 0.25, 0.5, 0.5], + [1, 1, 1], ] MIXER_TERMS = [ @@ -435,40 +521,48 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): ], [ qml.PauliX(0), - qml.PauliX(0) @ qml.PauliZ(2), qml.PauliX(0) @ qml.PauliZ(1), - qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2), qml.PauliX(1), qml.PauliX(1) @ qml.PauliZ(2), qml.PauliX(1) @ qml.PauliZ(0), qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliX(2), - qml.PauliX(2) @ qml.PauliZ(0), qml.PauliX(2) @ qml.PauliZ(1), - qml.PauliX(2) @ qml.PauliZ(1) @ qml.PauliZ(0), ], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], [ qml.PauliX(0), + qml.PauliX(0) @ qml.PauliZ(2), qml.PauliX(0) @ qml.PauliZ(1), + qml.PauliX(0) @ qml.PauliZ(1) @ qml.PauliZ(2), qml.PauliX(1), - qml.PauliX(1) @ qml.PauliZ(0), qml.PauliX(1) @ qml.PauliZ(2), - qml.PauliX(1) @ qml.PauliZ(2) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0), + qml.PauliX(1) @ qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliX(2), + qml.PauliX(2) @ qml.PauliZ(0), qml.PauliX(2) @ qml.PauliZ(1), + qml.PauliX(2) @ qml.PauliZ(1) @ qml.PauliZ(0), ], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(4)] +MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(5)] MIS = list(zip(GRAPHS, CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) """GENERATES THE CASES TO TEST THE MIN VERTEX COVER PROBLEM""" -COST_COEFFS = [[-1, -1, -1], [-1, -1, -1], [0.75, -0.25, 0.5, 0.75, -0.25], [-1, -1, -1]] +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 = [ + [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], [ @@ -478,36 +572,56 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): qml.PauliZ(1) @ qml.PauliZ(2), qml.PauliZ(2), ], - [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [ + qml.PauliZ(0) @ qml.PauliZ(1), + qml.PauliZ(0), + qml.PauliZ(1), + qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(2), + ], ] -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(4)] +COST_HAMILTONIANS = [qml.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], - [0.5, -0.5, 0.25, -0.25, -0.25, 0.25, 0.5, -0.5], + [1, 1, 1], ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(4)] +MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(5)] MVC = list(zip(GRAPHS, CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) """GENERATES THE CASES TO TEST THE MAXCLIQUE PROBLEM""" -COST_COEFFS = [[1, 1, 1], [1, 1, 1], [0.75, 0.25, 0.25, 1], [1, 1, 1]] +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 = [ [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], - [qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliZ(0), qml.PauliZ(2), qml.PauliZ(1)], [qml.PauliZ(0), qml.PauliZ(1), qml.PauliZ(2)], + [qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliZ(0), qml.PauliZ(2), qml.PauliZ(1)], + [qml.PauliZ(0) @ qml.PauliZ(2), qml.PauliZ(0), qml.PauliZ(2), qml.PauliZ(1)], ] -COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(4)] +COST_HAMILTONIANS = [qml.Hamiltonian(COST_COEFFS[i], COST_TERMS[i]) for i in range(5)] -MIXER_COEFFS = [[0.5, 0.5, 1.0, 0.5, 0.5], [1.0, 1.0, 1.0], [1, 1, 1], [0.5, 0.5, 1.0, 0.5, 0.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 = [ [ @@ -517,8 +631,6 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): qml.PauliX(2), qml.PauliX(2) @ qml.PauliZ(0), ], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], - [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], [ qml.PauliX(0), qml.PauliX(0) @ qml.PauliZ(2), @@ -526,17 +638,35 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): qml.PauliX(2), qml.PauliX(2) @ qml.PauliZ(0), ], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], + [qml.PauliX(0), qml.PauliX(1), qml.PauliX(2)], ] -MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(4)] +MIXER_HAMILTONIANS = [qml.Hamiltonian(MIXER_COEFFS[i], MIXER_TERMS[i]) for i in range(5)] MAXCLIQUE = list(zip(GRAPHS, CONSTRAINED, COST_HAMILTONIANS, MIXER_HAMILTONIANS)) """GENERATES CASES TO TEST EDGE DRIVER COST HAMILTONIAN""" -GRAPHS.pop() +GRAPHS = GRAPHS[1:-2] GRAPHS.append(graph) GRAPHS.append(Graph([("b", 1), (1, 2.3)])) -REWARDS = [["00"], ["00", "11"], ["00", "01", "10"], ["00", "11", "01", "10"], ["00", "01", "10"]] +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 = [ qml.Hamiltonian( @@ -558,15 +688,16 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): qml.PauliZ(1) @ qml.PauliZ(2), ], ), + qml.Hamiltonian([1, 1, 1], [qml.Identity(0), qml.Identity(1), qml.Identity(2)]), qml.Hamiltonian( [0.25, -0.25, -0.25, 0.25, -0.25, -0.25], [ - qml.PauliZ(0) @ qml.PauliZ(1), - qml.PauliZ(0), + qml.PauliZ("b") @ qml.PauliZ(1), + qml.PauliZ("b"), qml.PauliZ(1), - qml.PauliZ(1) @ qml.PauliZ(2), + qml.PauliZ(1) @ qml.PauliZ(2.3), qml.PauliZ(1), - qml.PauliZ(2), + qml.PauliZ(2.3), ], ), qml.Hamiltonian([1, 1, 1], [qml.Identity(0), qml.Identity(1), qml.Identity(2)]), @@ -1070,6 +1201,40 @@ def circuit(params, **kwargs): 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 + def circuit(params, **kwargs): + for w in wires: + qml.Hadamard(wires=w) + + qml.layer(qaoa_layer, 2, params[0], params[1]) + + # Defines the device and the QAOA cost function + dev = qml.device("default.qubit", wires=len(wires)) + cost_function = qml.ExpvalCost(circuit, cost_h, dev) + + res = cost_function([[1, 1], [1, 1]]) + expected = -1.8260274380964299 + + assert np.allclose(res, expected, atol=tol, rtol=0) + class TestCycles: """Tests that ``cycle`` module functions are behaving correctly""" @@ -1081,6 +1246,26 @@ def test_edges_to_wires(self): 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_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, + } + def test_wires_to_edges(self): """Test that wires_to_edges returns the correct mapping""" g = nx.lollipop_graph(4, 1) @@ -1088,10 +1273,33 @@ def test_wires_to_edges(self): 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_partial_cycle_mixer_complete(self): + 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""" - g = nx.complete_graph(4).to_directed() edge = (0, 1) h = _partial_cycle_mixer(g, edge) @@ -1133,12 +1341,16 @@ def test_partial_cycle_mixer_incomplete(self): 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)) - def test_cycle_mixer(self): + @pytest.mark.parametrize( + "g", + [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2, 3])], + ) + def test_cycle_mixer(self, g): """Test if the cycle_mixer Hamiltonian maps valid cycles to valid cycles""" + n_nodes = 3 - g = nx.complete_graph(n_nodes).to_directed() m = wires_to_edges(g) - n_wires = len(g.edges) + n_wires = len(graph.edge_list() if isinstance(graph, rx.PyDiGraph) else graph.edges) # Find Hamiltonian and its matrix representation h = cycle_mixer(g) @@ -1225,6 +1437,35 @@ def test_matrix(self): 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 = qml.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) + def test_edges_to_wires_directed(self): """Test that edges_to_wires returns the correct mapping on a directed graph""" g = nx.lollipop_graph(4, 1).to_directed() @@ -1269,13 +1510,21 @@ def test_wires_to_edges_directed(self): 13: (4, 3), } - def test_loss_hamiltonian_complete(self): + @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""" - 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 + 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}) + 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 = [ @@ -1338,14 +1587,23 @@ def test_loss_hamiltonian_incomplete(self): 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_self_loop_raises_error(self): + @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""" - 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 - g.add_edge(1, 1) # add self loop + 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) @@ -1353,7 +1611,6 @@ def test_self_loop_raises_error(self): 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) @@ -1410,11 +1667,12 @@ def test_square_hamiltonian_terms(self): ] ) - def test_inner_out_flow_constraint_hamiltonian(self): + @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""" - - g = nx.complete_graph(3).to_directed() h = _inner_out_flow_constraint_hamiltonian(g, 0) expected_ops = [ @@ -1431,10 +1689,12 @@ def test_inner_out_flow_constraint_hamiltonian(self): assert str(h.ops[i]) == str(expected_op) assert all([op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)]) - def test_inner_net_flow_constraint_hamiltonian(self): + @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""" - g = nx.complete_graph(3).to_directed() h = _inner_net_flow_constraint_hamiltonian(g, 0) expected_ops = [ @@ -1453,11 +1713,13 @@ def test_inner_net_flow_constraint_hamiltonian(self): assert str(h.ops[i]) == str(expected_op) assert all([op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)]) - def test_inner_out_flow_constraint_hamiltonian_non_complete(self): + @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 = nx.complete_graph(3).to_directed() g.remove_edge(0, 1) h = _inner_out_flow_constraint_hamiltonian(g, 0) @@ -1469,10 +1731,12 @@ def test_inner_out_flow_constraint_hamiltonian_non_complete(self): assert str(h.ops[i]) == str(expected_op) assert all([op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)]) - def test_inner_net_flow_constraint_hamiltonian_non_complete(self): + @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 = nx.complete_graph(3).to_directed() g.remove_edge(1, 0) h = _inner_net_flow_constraint_hamiltonian(g, 0) @@ -1492,14 +1756,16 @@ def test_inner_net_flow_constraint_hamiltonian_non_complete(self): assert str(h.ops[i]) == str(expected_op) assert all([op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)]) - def test_out_flow_constraint(self): + @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 """ - g = nx.complete_graph(3).to_directed() h = out_flow_constraint(g) m = wires_to_edges(g) - wires = len(g.edges) + 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 = qml.device("default.qubit", wires=wires) @@ -1522,7 +1788,10 @@ def states(basis_state, **kwargs): edges = tuple(m[w] for w in wires_) # find the number of edges leaving each node - num_edges_leaving_node = {node: 0 for node in g.nodes} + 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 @@ -1533,20 +1802,24 @@ def states(basis_state, **kwargs): elif max(num_edges_leaving_node.values()) <= 1: assert energy == min(energies_bitstrings)[0] - def test_out_flow_constraint_undirected_raises_error(self): + @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""" - g = nx.complete_graph(3) # undirected graph with pytest.raises(ValueError): h = out_flow_constraint(g) - def test_net_flow_constraint(self): + @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""" - g = nx.complete_graph(3).to_directed() h = net_flow_constraint(g) m = wires_to_edges(g) - wires = len(g.edges) + 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 = qml.device("default.qubit", wires=wires) @@ -1571,8 +1844,12 @@ def energy(basis_state, **kwargs): edges = tuple(m[w] for w in wires_) # Calculates the number of edges entering and leaving a given node - in_flows = np.zeros(len(g.nodes)) - out_flows = np.zeros(len(g.nodes)) + 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 @@ -1587,21 +1864,26 @@ def energy(basis_state, **kwargs): else: assert energy > min(energies_states)[0] - def test_net_flow_constraint_undirected_raises_error(self): + @pytest.mark.parametrize( + "g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])] + ) + def test_net_flow_constraint_undirected_raises_error(self, g): """Test `net_flow_constraint` raises ValueError if input graph is not directed""" - g = nx.complete_graph(3) # undirected graph with pytest.raises(ValueError): h = net_flow_constraint(g) - def test_net_flow_and_out_flow_constraint(self): + @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.edges) + wires = len(g.edge_list() if isinstance(g, rx.PyDiGraph) else g.edges) # Find the energies corresponding to each possible bitstring dev = qml.device("default.qubit", wires=wires) From c15fbc6ecf398d4377bab4920542db80d6ccd594 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Fri, 17 Dec 2021 01:23:01 -0500 Subject: [PATCH 13/22] Update formatting --- pennylane/qaoa/cost.py | 3 ++- pennylane/qaoa/cycle.py | 14 +++++++++++--- tests/test_qaoa.py | 10 +++------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py index bcb590bc3a3..ab7c734744a 100644 --- a/pennylane/qaoa/cost.py +++ b/pennylane/qaoa/cost.py @@ -307,7 +307,8 @@ def maxcut(graph): get_nvalue = lambda i: graph_nodes[i] if is_rx else i identity_h = qml.Hamiltonian( - [-0.5 for e in graph_edges], [qml.Identity(get_nvalue(e[0])) @ qml.Identity(get_nvalue(e[1])) for e in graph_edges] + [-0.5 for e in graph_edges], + [qml.Identity(get_nvalue(e[0])) @ qml.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 diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index 79799da5828..d2a27c0f706 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -376,7 +376,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: for edge_data in edges_data: edge = edge_data[:2] - + if edge[0] == edge[1]: raise ValueError("Graph contains self-loops") @@ -562,7 +562,11 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: coeffs = [] ops = [] - get_nvalues = lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) if isinstance(graph, rx.PyDiGraph) else T + get_nvalues = ( + lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) + if isinstance(graph, rx.PyDiGraph) + else T + ) edges_to_qubits = edges_to_wires(graph) out_edges = sorted(graph.out_edges(node)) @@ -626,7 +630,11 @@ def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: out_edges = sorted(graph.out_edges(node)) in_edges = sorted(graph.in_edges(node)) - get_nvalues = lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) if isinstance(graph, rx.PyDiGraph) else T + get_nvalues = ( + lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) + if isinstance(graph, rx.PyDiGraph) + else T + ) coeffs.append(len(out_edges) - len(in_edges)) ops.append(qml.Identity(0)) diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index e0b7bedd38b..37a7087c446 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -1802,10 +1802,8 @@ def states(basis_state, **kwargs): 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): + @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): @@ -1864,9 +1862,7 @@ def energy(basis_state, **kwargs): else: assert energy > min(energies_states)[0] - @pytest.mark.parametrize( - "g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])] - ) + @pytest.mark.parametrize("g", [nx.complete_graph(3), rx.generators.mesh_graph(3, [0, 1, 2])]) def test_net_flow_constraint_undirected_raises_error(self, g): """Test `net_flow_constraint` raises ValueError if input graph is not directed""" From dbc601187af145f9bc50c0140c8409f71bbba79c Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Fri, 17 Dec 2021 13:22:36 -0500 Subject: [PATCH 14/22] Update circuit_graph --- pennylane/circuit_graph.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index df447e39b34..7da1ab32bfb 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -169,13 +169,15 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): # TODO: State preparations demolish the incoming state entirely, and therefore should have no incoming edges. # self._graph = nx.DiGraph() #: nx.DiGraph: DAG representation of the quantum circuit - self._graph = rx.PyDiGraph() #: rx.PyDiGraph: DAG representation of the quantum circuit + self._graph = rx.PyDiGraph(multigraph=False) #: rx.PyDiGraph: DAG representation of the quantum circuit # Iterate over each (populated) wire in the grid for wire in self._grid.values(): # Add the first operator on the wire to the graph # This operator does not depend on any others - self._graph.add_node(wire[0]) + if wire[0] not in self._graph.nodes(): # rx. + self._graph.add_node(wire[0]) + for i in range(1, len(wire)): # For subsequent operators on the wire: # if wire[i] not in self._graph: @@ -360,7 +362,7 @@ def _in_topological_order(self, ops): """ # G = nx.DiGraph(self._graph.subgraph(ops)) # return nx.dag.topological_sort(G) - G = self._graph.subgraph( list(self._graph.nodes().index(o) for o in ops)) # rx. + G = self._graph.subgraph(list(self._graph.nodes().index(o) for o in ops)) # rx. indexes = rx.topological_sort(G) # rx. return list(G[x] for x in indexes) # rx. @@ -652,7 +654,7 @@ def get_depth(self): if self._depth is None and self.operations: if self._operation_graph is None: # self._operation_graph = self.graph.subgraph(self.operations) - self._operation_graph = self.graph.subgraph(list(self.graph.nodes().index(node) for node in self.operations)) # rx/ + self._operation_graph = self._graph.subgraph(list(self._graph.nodes().index(node) for node in self.operations)) # rx/ # self._depth = nx.dag_longest_path_length(self._operation_graph) + 1 self._depth = rx.dag_longest_path_length(self._operation_graph) + 1 # rx. From 85c46808a28d541c921b9951a208cfb5aa5a03d8 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Fri, 17 Dec 2021 16:42:07 -0500 Subject: [PATCH 15/22] Add RX support to CircuitGraph --- pennylane/circuit_graph.py | 89 ++++++++++++----------- pennylane/qaoa/cost.py | 18 ++--- pennylane/qaoa/cycle.py | 18 +++-- tests/circuit_graph/test_circuit_graph.py | 38 ++-------- tests/test_qaoa.py | 1 - 5 files changed, 76 insertions(+), 88 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 7da1ab32bfb..10bdc52a138 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -18,7 +18,6 @@ # pylint: disable=too-many-branches,too-many-arguments,too-many-instance-attributes from collections import Counter, OrderedDict, namedtuple -import networkx as nx import retworkx as rx import pennylane as qml @@ -168,30 +167,30 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): # TODO: State preparations demolish the incoming state entirely, and therefore should have no incoming edges. - # self._graph = nx.DiGraph() #: nx.DiGraph: DAG representation of the quantum circuit - self._graph = rx.PyDiGraph(multigraph=False) #: rx.PyDiGraph: DAG representation of the quantum circuit + self._graph = rx.PyDiGraph( + multigraph=False + ) #: rx.PyDiGraph: DAG representation of the quantum circuit + # Iterate over each (populated) wire in the grid for wire in self._grid.values(): # Add the first operator on the wire to the graph # This operator does not depend on any others - if wire[0] not in self._graph.nodes(): # rx. + if wire[0] not in self._graph.nodes(): self._graph.add_node(wire[0]) - + for i in range(1, len(wire)): # For subsequent operators on the wire: - # if wire[i] not in self._graph: - if wire[i] not in self._graph.nodes(): # rx. + if wire[i] not in self._graph.nodes(): # Add them to the graph if they are not already # in the graph (multi-qubit operators might already have been placed) self._graph.add_node(wire[i]) # Create an edge between this and the previous operator - # self._graph.add_edge(wire[i - 1], wire[i]) - self._graph.add_edge(self._graph.nodes().index(wire[i - 1]), - self._graph.nodes().index(wire[i]), - '') # rx. - + self._graph.add_edge( + self._graph.nodes().index(wire[i - 1]), self._graph.nodes().index(wire[i]), "" + ) + # For computing depth; want only a graph with the operations, not # including the observables self._operation_graph = None @@ -272,8 +271,7 @@ def observables_in_order(self): Returns: list[Observable]: observables """ - # nodes = [node for node in self._graph.nodes if _is_observable(node)] - nodes = [node for node in self._graph.nodes() if _is_observable(node)] # rx. + nodes = [node for node in self._graph.nodes() if _is_observable(node)] return sorted(nodes, key=_by_idx) @property @@ -293,8 +291,7 @@ def operations_in_order(self): Returns: list[Operation]: operations """ - # nodes = [node for node in self._graph.nodes if not _is_observable(node)] - nodes = [node for node in self._graph.nodes() if not _is_observable(node)] # rx. + nodes = [node for node in self._graph.nodes() if not _is_observable(node)] return sorted(nodes, key=_by_idx) @property @@ -334,9 +331,13 @@ def ancestors(self, ops): Returns: set[Operator]: ancestors of the given operators """ - # return set().union(*(nx.dag.ancestors(self._graph, o) for o in ops)) - set(ops) - anc = set(self._graph.get_node_data(n) for n in set().union(*(rx.ancestors(self._graph, self._graph.nodes().index(o)) for o in ops))) # rx. - return anc - set(ops) # rx. + anc = set( + self._graph.get_node_data(n) + for n in set().union( + *(rx.ancestors(self._graph, self._graph.nodes().index(o)) for o in ops) + ) + ) + return anc - set(ops) def descendants(self, ops): """Descendants of a given set of operators. @@ -347,9 +348,13 @@ def descendants(self, ops): Returns: set[Operator]: descendants of the given operators """ - # return set().union(*(nx.dag.descendants(self._graph, o) for o in ops)) - set(ops) - des = set(self._graph.get_node_data(n) for n in set().union(*(rx.descendants(self._graph, self._graph.nodes().index(o)) for o in ops))) # rx. - return des - set(ops) # rx. + des = set( + self._graph.get_node_data(n) + for n in set().union( + *(rx.descendants(self._graph, self._graph.nodes().index(o)) for o in ops) + ) + ) + return des - set(ops) def _in_topological_order(self, ops): """Sorts a set of operators in the circuit in a topological order. @@ -360,11 +365,9 @@ def _in_topological_order(self, ops): Returns: Iterable[Operator]: same set of operators, topologically ordered """ - # G = nx.DiGraph(self._graph.subgraph(ops)) - # return nx.dag.topological_sort(G) - G = self._graph.subgraph(list(self._graph.nodes().index(o) for o in ops)) # rx. - indexes = rx.topological_sort(G) # rx. - return list(G[x] for x in indexes) # rx. + G = self._graph.subgraph(list(self._graph.nodes().index(o) for o in ops)) + indexes = rx.topological_sort(G) + return list(G[x] for x in indexes) def ancestors_in_order(self, ops): """Operator ancestors in a topological order. @@ -377,8 +380,7 @@ def ancestors_in_order(self, ops): Returns: list[Operator]: ancestors of the given operators, topologically ordered """ - # return self._in_topological_order(self.ancestors(ops)) # an abitrary topological order - return sorted(self.ancestors(ops), key=_by_idx) + return sorted(self.ancestors(ops), key=_by_idx) # an abitrary topological order def descendants_in_order(self, ops): """Operator descendants in a topological order. @@ -603,10 +605,8 @@ def update_node(self, old, new): if new.wires != old.wires: raise ValueError("The new Operator must act on the same wires as the old one.") new.queue_idx = old.queue_idx - - # nx.relabel_nodes(self._graph, {old: new}, copy=False) # change the graph in place - # self._graph[old] = new # rx. - self._graph[self._graph.nodes().index(old)] = new # rx. + + self._graph[self._graph.nodes().index(old)] = new self._operations = self.operations_in_order self._observables = self.observables_in_order @@ -653,12 +653,10 @@ def get_depth(self): # expressed in terms of edges, and we want it in terms of nodes). if self._depth is None and self.operations: if self._operation_graph is None: - # self._operation_graph = self.graph.subgraph(self.operations) - self._operation_graph = self._graph.subgraph(list(self._graph.nodes().index(node) for node in self.operations)) # rx/ - - # self._depth = nx.dag_longest_path_length(self._operation_graph) + 1 - self._depth = rx.dag_longest_path_length(self._operation_graph) + 1 # rx. - + self._operation_graph = self._graph.subgraph( + list(self._graph.nodes().index(node) for node in self.operations) + ) + self._depth = rx.dag_longest_path_length(self._operation_graph) + 1 return self._depth def has_path(self, a, b): @@ -671,8 +669,17 @@ def has_path(self, a, b): Returns: bool: returns ``True`` if a path exists """ - # return nx.has_path(self._graph, a, b) - return len(rx.dijkstra_shortest_paths(self._graph, self._graph.nodes().index(a), self._graph.nodes().index(b))) != 0 # rx. + if a == b: + return True + + return ( + len( + rx.dijkstra_shortest_paths( + self._graph, self._graph.nodes().index(a), self._graph.nodes().index(b) + ) + ) + != 0 + ) @property def max_simultaneous_measurements(self): diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py index ab7c734744a..c5238870a99 100644 --- a/pennylane/qaoa/cost.py +++ b/pennylane/qaoa/cost.py @@ -18,9 +18,6 @@ import networkx as nx import retworkx as rx -from networkx.algorithms.similarity import graph_edit_distance -from networkx.generators.expanders import paley_graph - import pennylane as qml from pennylane import qaoa @@ -185,7 +182,7 @@ def edge_driver(graph, reward): ops = [] is_rx = isinstance(graph, rx.PyGraph) - graph_nodes = graph.nodes() if is_rx else graph.nodes + graph_nodes = graph.nodes() graph_edges = sorted(graph.edge_list()) if is_rx else graph.edges get_nvalue = lambda i: graph_nodes[i] if is_rx else i @@ -302,7 +299,7 @@ def maxcut(graph): ) is_rx = isinstance(graph, rx.PyGraph) - graph_nodes = graph.nodes() if is_rx else graph.nodes + graph_nodes = graph.nodes() graph_edges = sorted(graph.edge_list()) if is_rx else graph.edges get_nvalue = lambda i: graph_nodes[i] if is_rx else i @@ -378,7 +375,7 @@ def max_independent_set(graph, constrained=True): "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) ) - graph_nodes = graph.nodes() if isinstance(graph, rx.PyGraph) else graph.nodes + graph_nodes = graph.nodes() if constrained: cost_h = bit_driver(graph_nodes, 1) @@ -458,7 +455,7 @@ def min_vertex_cover(graph, constrained=True): "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) ) - graph_nodes = graph.nodes() if isinstance(graph, rx.PyGraph) else graph.nodes + graph_nodes = graph.nodes() if constrained: cost_h = bit_driver(graph_nodes, 0) @@ -540,9 +537,10 @@ def max_clique(graph, constrained=True): "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) ) - is_rx = isinstance(graph, rx.PyGraph) - graph_nodes = graph.nodes() if is_rx else graph.nodes - graph_complement = rx.complement(graph) if is_rx else nx.complement(graph) + 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) diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index d2a27c0f706..54bd7d84235 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -71,7 +71,10 @@ def edges_to_wires(graph) -> Dict[Tuple, int]: return {edge: i for i, edge in enumerate(graph.edges)} elif isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): gnodes = graph.nodes() - return {(gnodes[e[0]], gnodes[e[1]]): i for i, e in enumerate(sorted(graph.edge_list()))} + return { + (gnodes.index(e[0]), gnodes.index(e[1])): i + for i, e in enumerate(sorted(graph.edge_list())) + } else: raise ValueError( "Input graph must be a nx.Graph, rx.Py(Di)Graph, got {}".format(type(graph).__name__) @@ -123,7 +126,10 @@ def wires_to_edges(graph) -> Dict[int, Tuple]: return {i: edge for i, edge in enumerate(graph.edges)} elif isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): gnodes = graph.nodes() - return {i: (gnodes[e[0]], gnodes[e[1]]) for i, e in enumerate(sorted(graph.edge_list()))} + return { + i: (gnodes.index(e[0]), gnodes.index(e[1])) + for i, e in enumerate(sorted(graph.edge_list())) + } else: raise ValueError( "Input graph must be a nx.Graph or rx.Py(Di)Graph, got {}".format(type(graph).__name__) @@ -255,7 +261,7 @@ def _partial_cycle_mixer(graph, edge: Tuple) -> Hamiltonian: 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 - get_nvalues = lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) if is_rx else T + get_nvalues = lambda T: (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) @@ -372,7 +378,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: is_rx = isinstance(graph, (rx.PyGraph, rx.PyDiGraph)) edges_data = sorted(graph.weighted_edge_list()) if is_rx else graph.edges(data=True) - get_nvalues = lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) if is_rx else T + get_nvalues = lambda T: (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] @@ -563,7 +569,7 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: ops = [] get_nvalues = ( - lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) + lambda T: (graph.nodes().index(T[0]), graph.nodes().index(T[1])) if isinstance(graph, rx.PyDiGraph) else T ) @@ -631,7 +637,7 @@ def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: in_edges = sorted(graph.in_edges(node)) get_nvalues = ( - lambda T: (graph.nodes()[T[0]], graph.nodes()[T[1]]) + lambda T: (graph.nodes().index(T[0]), graph.nodes().index(T[1])) if isinstance(graph, rx.PyDiGraph) else T ) diff --git a/tests/circuit_graph/test_circuit_graph.py b/tests/circuit_graph/test_circuit_graph.py index eb415349c85..d6d94c18023 100644 --- a/tests/circuit_graph/test_circuit_graph.py +++ b/tests/circuit_graph/test_circuit_graph.py @@ -146,40 +146,20 @@ def test_dependence(self, ops, obs): circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) graph = circuit.graph - # assert len(graph) == 9 - assert len(graph.node_indexes()) == 9 # rx. + assert len(graph.node_indexes()) == 9 assert len(graph.edges()) == 9 queue = ops + obs # all ops should be nodes in the graph for k in queue: - # assert k in graph.nodes - assert k in graph.nodes() # rx. + assert k in graph.nodes() # all nodes in the graph should be ops # for k in graph.nodes: - for k in graph.nodes(): # rx. + for k in graph.nodes(): assert k is queue[k.queue_idx] - - # Finally, checking the adjacency of the returned DAG: - # assert set(graph.edges()) == set( - # (queue[a], queue[b]) - # for a, b in [ - # (0, 3), - # (1, 3), - # (2, 4), - # (3, 5), - # (3, 6), - # (4, 5), - # (5, 7), - # (5, 8), - # (6, 8), - # ] - # ) - a = set( - (graph.get_node_data(e[0]), graph.get_node_data(e[1])) for e in graph.edge_list() - ) # rx. + a = set((graph.get_node_data(e[0]), graph.get_node_data(e[1])) for e in graph.edge_list()) b = set( (queue[a], queue[b]) for a, b in [ @@ -193,8 +173,8 @@ def test_dependence(self, ops, obs): (5, 8), (6, 8), ] - ) # rx. - assert a == b # rx. + ) + assert a == b def test_ancestors_and_descendants_example(self, ops, obs): """ @@ -222,13 +202,11 @@ def test_update_node(self, ops, obs): def test_observables(self, circuit, obs): """Test that the `observables` property returns the list of observables in the circuit.""" - # assert circuit.observables == obs - assert str(circuit.observables) == str(obs) # rx. + assert str(circuit.observables) == str(obs) def test_operations(self, circuit, ops): """Test that the `operations` property returns the list of operations in the circuit.""" - # assert circuit.operations == ops - assert str(circuit.operations) == str(ops) # rx. + assert str(circuit.operations) == str(ops) def test_op_indices(self, circuit): """Test that for the given circuit, this method will fetch the correct operation indices for diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index 37a7087c446..8eabc1e1f02 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -14,7 +14,6 @@ """ Unit tests for the :mod:`pennylane.qaoa` submodule. """ -from networkx.algorithms.similarity import graph_edit_distance import pytest import itertools import numpy as np From e6736f8a55ff35c8388ff18306cf291904f6094e Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Fri, 17 Dec 2021 17:01:07 -0500 Subject: [PATCH 16/22] Update formatting --- pennylane/qaoa/cost.py | 12 ++++++------ pennylane/qaoa/cycle.py | 26 +++++++++++--------------- pennylane/qaoa/mixers.py | 8 ++------ 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py index c5238870a99..b4f17b6b05c 100644 --- a/pennylane/qaoa/cost.py +++ b/pennylane/qaoa/cost.py @@ -175,7 +175,7 @@ def edge_driver(graph, reward): if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.Graph or rx.PyGraph, got {type(graph).__name__}" ) coeffs = [] @@ -295,7 +295,7 @@ def maxcut(graph): if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.Graph or rx.PyGraph, got {type(graph).__name__}" ) is_rx = isinstance(graph, rx.PyGraph) @@ -372,7 +372,7 @@ def max_independent_set(graph, constrained=True): if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.Graph or rx.PyGraph, got {type(graph).__name__}" ) graph_nodes = graph.nodes() @@ -452,7 +452,7 @@ def min_vertex_cover(graph, constrained=True): if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.Graph or rx.PyGraph, got {type(graph).__name__}" ) graph_nodes = graph.nodes() @@ -534,7 +534,7 @@ def max_clique(graph, constrained=True): if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.Graph or rx.PyGraph, got {type(graph).__name__}" ) graph_nodes = graph.nodes() @@ -693,7 +693,7 @@ def max_weight_cycle(graph, constrained=True): """ if not isinstance(graph, (nx.Graph, rx.PyGraph, rx.PyDiGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.Graph or rx.PyGraph, got {type(graph).__name__}" ) mapping = qaoa.cycle.wires_to_edges(graph) diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index 54bd7d84235..dd240bd1101 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -75,10 +75,7 @@ def edges_to_wires(graph) -> Dict[Tuple, int]: (gnodes.index(e[0]), gnodes.index(e[1])): i for i, e in enumerate(sorted(graph.edge_list())) } - else: - raise ValueError( - "Input graph must be a nx.Graph, rx.Py(Di)Graph, got {}".format(type(graph).__name__) - ) + raise ValueError(f"Input graph must be a nx.Graph, rx.Py(Di)Graph, got {type(graph).__name__}") def wires_to_edges(graph) -> Dict[int, Tuple]: @@ -130,10 +127,9 @@ def wires_to_edges(graph) -> Dict[int, Tuple]: i: (gnodes.index(e[0]), gnodes.index(e[1])) for i, e in enumerate(sorted(graph.edge_list())) } - else: - raise ValueError( - "Input graph must be a nx.Graph or rx.Py(Di)Graph, got {}".format(type(graph).__name__) - ) + raise ValueError( + f"Input graph must be a nx.Graph or rx.Py(Di)Graph, got {type(graph).__name__}" + ) def cycle_mixer(graph) -> Hamiltonian: @@ -220,7 +216,7 @@ def cycle_mixer(graph) -> Hamiltonian: """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( - "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) hamiltonian = Hamiltonian([], []) @@ -251,7 +247,7 @@ def _partial_cycle_mixer(graph, edge: Tuple) -> Hamiltonian: """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( - "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) coeffs = [] @@ -368,7 +364,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: """ if not isinstance(graph, (nx.Graph, rx.PyGraph, rx.PyDiGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.Py(Di)Graph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.Graph or rx.Py(Di)Graph, got {type(graph).__name__}" ) edges_to_qubits = edges_to_wires(graph) @@ -475,7 +471,7 @@ def out_flow_constraint(graph) -> Hamiltonian: """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( - "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + 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"): @@ -531,7 +527,7 @@ def net_flow_constraint(graph) -> Hamiltonian: if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( - "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) hamiltonian = Hamiltonian([], []) @@ -562,7 +558,7 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( - "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) coeffs = [] @@ -625,7 +621,7 @@ def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( - "Input graph must be a nx.DiGraph or rx.PyDiGraph, got {}".format(type(graph).__name__) + f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) edges_to_qubits = edges_to_wires(graph) diff --git a/pennylane/qaoa/mixers.py b/pennylane/qaoa/mixers.py index 3ab7c9bf650..f744a317ccb 100644 --- a/pennylane/qaoa/mixers.py +++ b/pennylane/qaoa/mixers.py @@ -115,9 +115,7 @@ def xy_mixer(graph): if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph object, got {}".format( - type(graph).__name__ - ) + f"Input graph must be a nx.Graph or rx.PyGraph object, got {type(graph).__name__}" ) is_rx = isinstance(graph, rx.PyGraph) @@ -196,9 +194,7 @@ def bit_flip_mixer(graph, b): if not isinstance(graph, (nx.Graph, rx.PyGraph)): raise ValueError( - "Input graph must be a nx.Graph or rx.PyGraph object, got {}".format( - type(graph).__name__ - ) + f"Input graph must be a nx.Graph or rx.PyGraph object, got {type(graph).__name__}" ) if b not in [0, 1]: From ccac3199c65f7b4d66c7b14850aca30127c304a4 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Sun, 19 Dec 2021 19:06:33 -0500 Subject: [PATCH 17/22] Apply codecov suggestions --- pennylane/qaoa/cycle.py | 2 +- tests/circuit_graph/test_circuit_graph.py | 20 ++++++ tests/test_qaoa.py | 77 ++++++++++++++++++++--- 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index dd240bd1101..3528425c9b1 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -387,7 +387,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: except KeyError as e: raise KeyError(f"Edge {edge} does not contain weight data") from e except TypeError: - weight = 0 + raise TypeError(f"Edges do not contain weight data") coeffs.append(np.log(weight)) ops.append(qml.PauliZ(wires=edges_to_qubits[get_nvalues(edge)])) diff --git a/tests/circuit_graph/test_circuit_graph.py b/tests/circuit_graph/test_circuit_graph.py index d6d94c18023..8aaa993f33f 100644 --- a/tests/circuit_graph/test_circuit_graph.py +++ b/tests/circuit_graph/test_circuit_graph.py @@ -192,6 +192,26 @@ def test_ancestors_and_descendants_example(self, ops, obs): descendants = circuit.descendants([queue[6]]) assert descendants == set([queue[8]]) + def test_in_topological_order_example(self, ops, obs): + """ + Test ``_in_topological_order`` method returns the expected result. + """ + circuit = CircuitGraph(ops, obs, Wires([0, 1, 2])) + + to = circuit._in_topological_order(ops) + + to_expected = [ + qml.RZ(0.35, wires=[2]), + qml.Hadamard(wires=[2]), + qml.RY(0.35, wires=[1]), + qml.RX(0.43, wires=[0]), + qml.CNOT(wires=[0, 1]), + qml.PauliX(wires=[1]), + qml.CNOT(wires=[2, 0]), + ] + + assert str(to) == str(to_expected) + def test_update_node(self, ops, obs): """Changing nodes in the graph.""" diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index 8eabc1e1f02..52b152460e3 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -56,15 +56,15 @@ 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, "")]) - 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) +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.edges())) + 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}) @@ -1245,6 +1245,12 @@ def test_edges_to_wires(self): 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, rx.Py\(Di\)Graph"): + 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]) @@ -1272,6 +1278,12 @@ def test_wires_to_edges(self): 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.Py\(Di\)Graph"): + 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]) @@ -1319,10 +1331,13 @@ def test_partial_cycle_mixer_complete(self, g): 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)) - def test_partial_cycle_mixer_incomplete(self): + @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 = nx.complete_graph(4).to_directed() g.remove_edge(2, 1) # remove an egde to make graph incomplete edge = (0, 1) @@ -1340,9 +1355,19 @@ def test_partial_cycle_mixer_incomplete(self): 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, 3])], + [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""" @@ -1407,6 +1432,16 @@ def test_cycle_mixer(self, g): 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) + def test_matrix(self): """Test that the matrix function works as expected on a fixed example""" g = nx.lollipop_graph(3, 1) @@ -1516,8 +1551,8 @@ 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(g.edges())} - for k, v in complete_edge_weight_data.items(): + 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)} @@ -1540,6 +1575,11 @@ def test_loss_hamiltonian_complete(self, g): 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.Py\(Di\)Graph"): + loss_hamiltonian([(0, 1), (1, 2), (0, 2)]) + def test_loss_hamiltonian_incomplete(self): """Test if the loss_hamiltonian function returns the expected result on a manually-calculated example of a 4-node incomplete digraph""" @@ -1613,6 +1653,12 @@ def test_missing_edge_weight_data_raises_error(self): 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="Edges do 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""" @@ -1688,6 +1734,12 @@ def test_inner_out_flow_constraint_hamiltonian(self, g): assert str(h.ops[i]) == str(expected_op) assert all([op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)]) + @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])] ) @@ -1712,6 +1764,12 @@ def test_inner_net_flow_constraint_hamiltonian(self, g): assert str(h.ops[i]) == str(expected_op) assert all([op.wires == exp.wires for op, exp in zip(h.ops, expected_ops)]) + @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])] ) @@ -1804,9 +1862,8 @@ def states(basis_state, **kwargs): @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): - h = out_flow_constraint(g) + out_flow_constraint(g) @pytest.mark.parametrize( "g", [nx.complete_graph(3).to_directed(), rx.generators.directed_mesh_graph(3, [0, 1, 2])] From 7279a2a0f33d0829f4f771ae50d6ff453fbff880 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Sun, 19 Dec 2021 20:35:10 -0500 Subject: [PATCH 18/22] Add lollipop_graph_rx --- pennylane/qaoa/cost.py | 2 +- pennylane/qaoa/cycle.py | 4 +- tests/test_qaoa.py | 84 ++++++++++++++++++++++++++--------------- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py index b4f17b6b05c..0e89bca1bd5 100644 --- a/pennylane/qaoa/cost.py +++ b/pennylane/qaoa/cost.py @@ -82,7 +82,7 @@ def edge_driver(graph, reward): See usage details for more information. Args: - graph (nx.Graph or nx.PyGraph): The graph on which the Hamiltonian is defined + 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: diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index 3528425c9b1..2463dffa99f 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -386,8 +386,8 @@ def loss_hamiltonian(graph) -> Hamiltonian: weight = edge_data[2]["weight"] except KeyError as e: raise KeyError(f"Edge {edge} does not contain weight data") from e - except TypeError: - raise TypeError(f"Edges do not contain weight data") + except TypeError as e: + raise TypeError(f"Edge {edge} does not contain weight data") from e coeffs.append(np.log(weight)) ops.append(qml.PauliZ(wires=edges_to_qubits[get_nvalues(edge)])) diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index 52b152460e3..32deba03b87 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -52,22 +52,9 @@ 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, "")]) -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}) g1 = Graph([(0, 1), (1, 2)]) @@ -87,13 +74,28 @@ def decompose_hamiltonian(hamiltonian): - coeffs = list(qml.math.toarray(hamiltonian.coeffs)) ops = [i.name for i in hamiltonian.ops] wires = [i.wires for i in hamiltonian.ops] return [coeffs, ops, wires] +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: qml.Hamiltonian, n_wires: int) -> csc_matrix: r"""Calculates the matrix representation of an input Hamiltonian in the standard basis. @@ -716,6 +718,17 @@ def test_bit_flip_mixer_output(self, graph, n, target_hamiltonian): EDGE_DRIVER = zip(GRAPHS, REWARDS, HAMILTONIANS) """GENERATES THE CASES TO TEST 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 @@ -1238,9 +1251,9 @@ def circuit(params, **kwargs): class TestCycles: """Tests that ``cycle`` module functions are behaving correctly""" - def test_edges_to_wires(self): + @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""" - g = nx.lollipop_graph(4, 1) 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} @@ -1271,9 +1284,9 @@ def test_edges_to_wires_rx(self): (3, 2): 11, } - def test_wires_to_edges(self): + @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""" - g = nx.lollipop_graph(4, 1) 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)} @@ -1442,9 +1455,9 @@ def test_cycle_mixer_error(self, g): with pytest.raises(ValueError, match="Input graph must be a nx.DiGraph or rx.PyDiGraph"): cycle_mixer(g) - def test_matrix(self): + @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""" - g = nx.lollipop_graph(3, 1) h = qml.qaoa.bit_flip_mixer(g, 0) mat = matrix(h, 4) @@ -1500,9 +1513,11 @@ def test_matrix_rx(self): assert np.allclose(mat.toarray(), mat_expected) - def test_edges_to_wires_directed(self): + @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""" - g = nx.lollipop_graph(4, 1).to_directed() r = edges_to_wires(g) assert r == { @@ -1522,9 +1537,11 @@ def test_edges_to_wires_directed(self): (4, 3): 13, } - def test_wires_to_edges_directed(self): + @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""" - g = nx.lollipop_graph(4, 1).to_directed() r = wires_to_edges(g) assert r == { @@ -1580,13 +1597,20 @@ def test_loss_hamiltonian_error(self): with pytest.raises(ValueError, match=r"Input graph must be a nx.Graph or rx.Py\(Di\)Graph"): loss_hamiltonian([(0, 1), (1, 2), (0, 2)]) - def test_loss_hamiltonian_incomplete(self): + @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""" - g = nx.lollipop_graph(4, 1).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 + 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 = [ @@ -1656,7 +1680,7 @@ def test_missing_edge_weight_data_raises_error(self): 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="Edges do not contain weight data"): + with pytest.raises(TypeError, match="does not contain weight data"): loss_hamiltonian(g) def test_square_hamiltonian_terms(self): From ea31100b1435c39be53dc8a04544c076532b1025 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Mon, 20 Dec 2021 14:02:59 -0500 Subject: [PATCH 19/22] Apply code review suggestions --- doc/releases/changelog-dev.md | 5 +- pennylane/circuit_graph.py | 6 ++- pennylane/qaoa/cost.py | 25 ++++++---- pennylane/qaoa/cycle.py | 91 +++++++++++++++++++++++------------ pennylane/qaoa/mixers.py | 14 ++++-- tests/test_qaoa.py | 16 ++++-- 6 files changed, 107 insertions(+), 50 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 91de5f7f149..0138a47474c 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -122,6 +122,9 @@ * Interferometer is now a class with `shape` method. [(#1946)](https://github.com/PennyLaneAI/pennylane/pull/1946) +* Replace NetworkX by RetworkX in CircuitGraph and add RetworkX support to QAOA. + [(#1791)](https://github.com/PennyLaneAI/pennylane/pull/1791) +

Breaking changes

Bug fixes

@@ -151,4 +154,4 @@ This release contains contributions from (in alphabetical order): -Juan Miguel Arrazola, Esther Cruz, Olivia Di Matteo, Diego Guala, Ankit Khandelwal, Antal Száva, David Wierichs, Shaoming Zhang \ No newline at end of file +Juan Miguel Arrazola, Ali Asadi, Esther Cruz, Olivia Di Matteo, Diego Guala, Ankit Khandelwal, Antal Száva, David Wierichs, Shaoming Zhang \ No newline at end of file diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 10bdc52a138..ed4fea560fe 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -176,6 +176,8 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): # Add the first operator on the wire to the graph # This operator does not depend on any others + # Check if wire[0] in self._grid.values() + # is already added to the graph if wire[0] not in self._graph.nodes(): self._graph.add_node(wire[0]) @@ -604,8 +606,8 @@ def update_node(self, old, new): # NOTE Does not alter the graph edges in any way. variable_deps is not changed, _grid is not changed. Dangerous! if new.wires != old.wires: raise ValueError("The new Operator must act on the same wires as the old one.") - new.queue_idx = old.queue_idx + new.queue_idx = old.queue_idx self._graph[self._graph.nodes().index(old)] = new self._operations = self.operations_in_order @@ -674,7 +676,7 @@ def has_path(self, a, b): return ( len( - rx.dijkstra_shortest_paths( + rx._digraph_dijkstra_shortest_path( self._graph, self._graph.nodes().index(a), self._graph.nodes().index(b) ) ) diff --git a/pennylane/qaoa/cost.py b/pennylane/qaoa/cost.py index 0e89bca1bd5..db1ed28412f 100644 --- a/pennylane/qaoa/cost.py +++ b/pennylane/qaoa/cost.py @@ -15,6 +15,7 @@ Methods for generating QAOA cost Hamiltonians corresponding to different optimization problems. """ +from typing import Iterable, Union import networkx as nx import retworkx as rx @@ -26,7 +27,7 @@ # Hamiltonian components -def bit_driver(wires, b): +def bit_driver(wires: Union[Iterable, qaoa.Wires], b: int): r"""Returns the bit-driver cost Hamiltonian. This Hamiltonian is defined as: @@ -66,7 +67,7 @@ def bit_driver(wires, b): return qml.Hamiltonian(coeffs, ops) -def edge_driver(graph, reward): +def edge_driver(graph: Union[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 @@ -184,6 +185,9 @@ def edge_driver(graph, reward): 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. get_nvalue = lambda i: graph_nodes[i] if is_rx else i if len(reward) == 0 or len(reward) == 4: @@ -235,7 +239,7 @@ def edge_driver(graph, reward): # Optimization problems -def maxcut(graph): +def maxcut(graph: Union[nx.Graph, rx.PyGraph]): r"""Returns the QAOA cost Hamiltonian and the recommended mixer corresponding to the MaxCut problem, for a given graph. @@ -301,6 +305,9 @@ def maxcut(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. get_nvalue = lambda i: graph_nodes[i] if is_rx else i identity_h = qml.Hamiltonian( @@ -313,7 +320,7 @@ def maxcut(graph): return (H, qaoa.x_mixer(graph_nodes)) -def max_independent_set(graph, constrained=True): +def max_independent_set(graph: Union[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 @@ -391,7 +398,7 @@ def max_independent_set(graph, constrained=True): return (cost_h, mixer_h) -def min_vertex_cover(graph, constrained=True): +def min_vertex_cover(graph: Union[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. @@ -471,7 +478,7 @@ def min_vertex_cover(graph, constrained=True): return (cost_h, mixer_h) -def max_clique(graph, constrained=True): +def max_clique(graph: Union[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. @@ -556,7 +563,7 @@ def max_clique(graph, constrained=True): return (cost_h, mixer_h) -def max_weight_cycle(graph, constrained=True): +def max_weight_cycle(graph: Union[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. @@ -572,7 +579,7 @@ def max_weight_cycle(graph, constrained=True): our subset of edges composes a `cycle `__. Args: - graph (nx.Graph or rx.Py(Di)Graph): the directed graph on which the Hamiltonians are defined + 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: @@ -693,7 +700,7 @@ def max_weight_cycle(graph, constrained=True): """ if not isinstance(graph, (nx.Graph, rx.PyGraph, rx.PyDiGraph)): raise ValueError( - f"Input graph must be a nx.Graph or rx.PyGraph, got {type(graph).__name__}" + f"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph, got {type(graph).__name__}" ) mapping = qaoa.cycle.wires_to_edges(graph) diff --git a/pennylane/qaoa/cycle.py b/pennylane/qaoa/cycle.py index 2463dffa99f..ead83dc6ca2 100644 --- a/pennylane/qaoa/cycle.py +++ b/pennylane/qaoa/cycle.py @@ -16,7 +16,13 @@ """ # pylint: disable=unnecessary-comprehension import itertools -from typing import Dict, Tuple, Iterable, List +from typing import ( + Dict, + Tuple, + Iterable, + List, + Union, +) import networkx as nx import retworkx as rx @@ -26,7 +32,7 @@ from pennylane.ops import Hamiltonian -def edges_to_wires(graph) -> Dict[Tuple, int]: +def edges_to_wires(graph: Union[nx.Graph, rx.PyGraph, rx.PyDiGraph]) -> Dict[Tuple, int]: r"""Maps the edges of a graph to corresponding wires. **Example** @@ -62,23 +68,25 @@ def edges_to_wires(graph) -> Dict[Tuple, int]: (3, 2): 11} Args: - graph (nx.Graph or rx.Py(Di)Graph): the graph specifying possible edges + 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 """ if isinstance(graph, nx.Graph): return {edge: i for i, edge in enumerate(graph.edges)} - elif isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): + 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, rx.Py(Di)Graph, got {type(graph).__name__}") + 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) -> Dict[int, Tuple]: +def wires_to_edges(graph: Union[nx.Graph, rx.PyGraph, rx.PyDiGraph]) -> Dict[int, Tuple]: r"""Maps the wires of a register of qubits to corresponding edges. **Example** @@ -114,25 +122,25 @@ def wires_to_edges(graph) -> Dict[int, Tuple]: 11: (3, 2)} Args: - graph (nx.Graph or rx.Py(Di)Graph): the graph specifying possible edges + 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 """ if isinstance(graph, nx.Graph): return {i: edge for i, edge in enumerate(graph.edges)} - elif isinstance(graph, (rx.PyGraph, rx.PyDiGraph)): + 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.Py(Di)Graph, got {type(graph).__name__}" + f"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph, got {type(graph).__name__}" ) -def cycle_mixer(graph) -> Hamiltonian: +def cycle_mixer(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: r"""Calculates the cycle-mixer Hamiltonian. Following methods outlined `here `__, the @@ -228,7 +236,7 @@ def cycle_mixer(graph) -> Hamiltonian: return hamiltonian -def _partial_cycle_mixer(graph, edge: Tuple) -> Hamiltonian: +def _partial_cycle_mixer(graph: Union[nx.DiGraph, rx.PyDiGraph], edge: Tuple) -> Hamiltonian: r"""Calculates the partial cycle-mixer Hamiltonian for a specific edge. For an edge :math:`(i, j)`, this function returns: @@ -257,6 +265,9 @@ def _partial_cycle_mixer(graph, edge: Tuple) -> Hamiltonian: 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 lambda function to get node-values. get_nvalues = lambda T: (graph.nodes().index(T[0]), graph.nodes().index(T[1])) if is_rx else T for node in graph_nodes: @@ -284,7 +295,7 @@ def _partial_cycle_mixer(graph, edge: Tuple) -> Hamiltonian: return Hamiltonian(coeffs, ops) -def loss_hamiltonian(graph) -> Hamiltonian: +def loss_hamiltonian(graph: Union[nx.Graph, rx.PyGraph, rx.PyDiGraph]) -> 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 @@ -353,7 +364,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: + (1.0986122886681098) [Z5] Args: - graph (nx.Graph or rx.Py(Di)Graph): the graph specifying possible edges + graph (nx.Graph or rx.PyGraph or rx.PyDiGraph): the graph specifying possible edges Returns: qml.Hamiltonian: the loss Hamiltonian @@ -364,7 +375,7 @@ def loss_hamiltonian(graph) -> Hamiltonian: """ if not isinstance(graph, (nx.Graph, rx.PyGraph, rx.PyDiGraph)): raise ValueError( - f"Input graph must be a nx.Graph or rx.Py(Di)Graph, got {type(graph).__name__}" + 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) @@ -374,6 +385,9 @@ def loss_hamiltonian(graph) -> Hamiltonian: 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 lambda function to get node-values. get_nvalues = lambda T: (graph.nodes().index(T[0]), graph.nodes().index(T[1])) if is_rx else T for edge_data in edges_data: @@ -434,7 +448,7 @@ def _square_hamiltonian_terms( return squared_coeffs, squared_ops -def out_flow_constraint(graph) -> Hamiltonian: +def out_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: r"""Calculates the `out flow constraint `__ Hamiltonian for the maximum-weighted cycle problem. @@ -486,7 +500,7 @@ def out_flow_constraint(graph) -> Hamiltonian: return hamiltonian -def net_flow_constraint(graph) -> Hamiltonian: +def net_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: r"""Calculates the `net flow constraint `__ Hamiltonian for the maximum-weighted cycle problem. @@ -539,7 +553,9 @@ def net_flow_constraint(graph) -> Hamiltonian: return hamiltonian -def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: +def _inner_out_flow_constraint_hamiltonian( + graph: Union[nx.DiGraph, rx.PyDiGraph], node: int +) -> Hamiltonian: r"""Calculates the inner portion of the Hamiltonian in :func:`out_flow_constraint`. For a given :math:`i`, this function returns: @@ -564,16 +580,21 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: coeffs = [] ops = [] - get_nvalues = ( - lambda T: (graph.nodes().index(T[0]), graph.nodes().index(T[1])) - if isinstance(graph, rx.PyDiGraph) - else T - ) + is_rx = isinstance(graph, rx.PyDiGraph) + + # 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_nvalues = lambda T: (graph.nodes().index(T[0]), graph.nodes().index(T[1])) if is_rx else T edges_to_qubits = edges_to_wires(graph) - out_edges = sorted(graph.out_edges(node)) + 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]) @@ -601,7 +622,9 @@ def _inner_out_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: return H -def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: +def _inner_net_flow_constraint_hamiltonian( + graph: Union[nx.DiGraph, rx.PyDiGraph], node: int +) -> Hamiltonian: r"""Calculates the squared inner portion of the Hamiltonian in :func:`net_flow_constraint`. @@ -629,14 +652,20 @@ def _inner_net_flow_constraint_hamiltonian(graph, node) -> Hamiltonian: coeffs = [] ops = [] - out_edges = sorted(graph.out_edges(node)) - in_edges = sorted(graph.in_edges(node)) + is_rx = isinstance(graph, rx.PyDiGraph) - get_nvalues = ( - lambda T: (graph.nodes().index(T[0]), graph.nodes().index(T[1])) - if isinstance(graph, rx.PyDiGraph) - else T - ) + 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 lambda function to get node-values. + get_nvalues = lambda T: (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(qml.Identity(0)) diff --git a/pennylane/qaoa/mixers.py b/pennylane/qaoa/mixers.py index f744a317ccb..3cf95a3b556 100644 --- a/pennylane/qaoa/mixers.py +++ b/pennylane/qaoa/mixers.py @@ -16,6 +16,7 @@ """ import itertools import functools +from typing import Iterable, Union import networkx as nx import retworkx as rx @@ -24,7 +25,7 @@ from pennylane.wires import Wires -def x_mixer(wires): +def x_mixer(wires: Union[Iterable, Wires]): r"""Creates a basic Pauli-X mixer Hamiltonian. This Hamiltonian is defined as: @@ -67,7 +68,7 @@ def x_mixer(wires): return H -def xy_mixer(graph): +def xy_mixer(graph: Union[nx.Graph, rx.PyGraph]): r"""Creates a generalized SWAP/XY mixer Hamiltonian. This mixer Hamiltonian is defined as: @@ -120,7 +121,11 @@ def xy_mixer(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. get_nvalue = lambda i: graph.nodes()[i] if is_rx else i + coeffs = 2 * [0.5 for e in edges] obs = [] @@ -131,7 +136,7 @@ def xy_mixer(graph): return qml.Hamiltonian(coeffs, obs) -def bit_flip_mixer(graph, b): +def bit_flip_mixer(graph: Union[nx.Graph, rx.PyGraph], b: int): r"""Creates a bit-flip mixer Hamiltonian. This mixer is defined as: @@ -207,6 +212,9 @@ def bit_flip_mixer(graph, b): 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: diff --git a/tests/test_qaoa.py b/tests/test_qaoa.py index 32deba03b87..7d73fe01580 100644 --- a/tests/test_qaoa.py +++ b/tests/test_qaoa.py @@ -930,7 +930,9 @@ def test_edge_driver_output(self, graph, reward, hamiltonian): 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 or rx.PyGraph"): + with pytest.raises( + ValueError, match=r"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph" + ): qaoa.max_weight_cycle([(0, 1), (1, 2)]) def test_cost_graph_error(self): @@ -1261,7 +1263,9 @@ def test_edges_to_wires(self, g): 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, rx.Py\(Di\)Graph"): + 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): @@ -1294,7 +1298,9 @@ def test_wires_to_edges(self, g): 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.Py\(Di\)Graph"): + 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): @@ -1594,7 +1600,9 @@ def test_loss_hamiltonian_complete(self, g): 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.Py\(Di\)Graph"): + 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( From 9203c3730bd1c480ff4d3c910a51d71930175909 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Tue, 21 Dec 2021 01:46:03 -0500 Subject: [PATCH 20/22] Update doc/releases/changelog-dev.md Co-authored-by: Josh Izaac --- doc/releases/changelog-dev.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 0138a47474c..483da06aaef 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -122,7 +122,12 @@ * Interferometer is now a class with `shape` method. [(#1946)](https://github.com/PennyLaneAI/pennylane/pull/1946) -* Replace NetworkX by RetworkX in CircuitGraph and add RetworkX support to QAOA. +* The `CircuitGraph`, used to represent circuits via directed acyclic graphs, now + uses RetworkX for its internal representation. This results in significant speedup + for algorithms that rely on a directed acyclic graph representation. + [(#1791)](https://github.com/PennyLaneAI/pennylane/pull/1791) + +* The QAOA module now accepts both NetworkX and RetworkX graphs as function inputs. [(#1791)](https://github.com/PennyLaneAI/pennylane/pull/1791)

Breaking changes

From 048cf798b0929c122916cded50ee35ef43631d44 Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Tue, 21 Dec 2021 13:00:27 -0500 Subject: [PATCH 21/22] Add more comments --- pennylane/circuit_graph.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index ed4fea560fe..279d747d2f5 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -177,7 +177,9 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): # This operator does not depend on any others # Check if wire[0] in self._grid.values() - # is already added to the graph + # is already added to the graph; this + # condition avoids adding new nodes with + # the same value but different indexes if wire[0] not in self._graph.nodes(): self._graph.add_node(wire[0]) @@ -189,6 +191,8 @@ def __init__(self, ops, obs, wires, par_info=None, trainable_params=None): self._graph.add_node(wire[i]) # Create an edge between this and the previous operator + # There isn't any default value for the edge-data in + # rx.PyDiGraph.add_edge(); this is set to an empty string self._graph.add_edge( self._graph.nodes().index(wire[i - 1]), self._graph.nodes().index(wire[i]), "" ) @@ -336,6 +340,7 @@ def ancestors(self, ops): anc = set( self._graph.get_node_data(n) for n in set().union( + # rx.ancestors() returns node indexes instead of node-values *(rx.ancestors(self._graph, self._graph.nodes().index(o)) for o in ops) ) ) @@ -353,6 +358,7 @@ def descendants(self, ops): des = set( self._graph.get_node_data(n) for n in set().union( + # rx.descendants() returns node indexes instead of node-values *(rx.descendants(self._graph, self._graph.nodes().index(o)) for o in ops) ) ) From af47884d9509d7a8f9264feb0bf14a5f239ea2ea Mon Sep 17 00:00:00 2001 From: Ali Asadi Date: Tue, 21 Dec 2021 13:35:17 -0500 Subject: [PATCH 22/22] Update has_path --- pennylane/circuit_graph.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pennylane/circuit_graph.py b/pennylane/circuit_graph.py index 279d747d2f5..103240749a0 100644 --- a/pennylane/circuit_graph.py +++ b/pennylane/circuit_graph.py @@ -682,8 +682,13 @@ def has_path(self, a, b): return ( len( - rx._digraph_dijkstra_shortest_path( - self._graph, self._graph.nodes().index(a), self._graph.nodes().index(b) + rx.digraph_dijkstra_shortest_paths( + self._graph, + self._graph.nodes().index(a), + self._graph.nodes().index(b), + weight_fn=None, + default_weight=1.0, + as_undirected=False, ) ) != 0