def cycle_mixer(graph: nx.DiGraph) -> Hamiltonian: r"""Calculates the cycle-mixer Hamiltonian. Following methods outlined `here <https://arxiv.org/pdf/1709.03489.pdf>`__, the cycle-mixer Hamiltonian preserves the set of valid cycles: .. math:: \frac{1}{4}\sum_{(i, j)\in E} \left(\sum_{k \in V, k\neq i, k\neq j, (i, k) \in E, (k, j) \in E} \left[X_{ij}X_{ik}X_{kj} +Y_{ij}Y_{ik}X_{kj} + Y_{ij}X_{ik}Y_{kj} - X_{ij}Y_{ik}Y_{kj}\right] \right) where :math:`E` are the edges of the directed graph. A valid cycle is defined as a subset of edges in :math:`E` such that all of the graph's nodes :math:`V` have zero net flow (see the :func:`~.net_flow_constraint` function). **Example** >>> import networkx as nx >>> g = nx.complete_graph(3).to_directed() >>> h_m = cycle_mixer(g) >>> print(h_m) (-0.25) [X0 Y1 Y5] + (-0.25) [X1 Y0 Y3] + (-0.25) [X2 Y3 Y4] + (-0.25) [X3 Y2 Y1] + (-0.25) [X4 Y5 Y2] + (-0.25) [X5 Y4 Y0] + (0.25) [X0 X1 X5] + (0.25) [Y0 Y1 X5] + (0.25) [Y0 X1 Y5] + (0.25) [X1 X0 X3] + (0.25) [Y1 Y0 X3] + (0.25) [Y1 X0 Y3] + (0.25) [X2 X3 X4] + (0.25) [Y2 Y3 X4] + (0.25) [Y2 X3 Y4] + (0.25) [X3 X2 X1] + (0.25) [Y3 Y2 X1] + (0.25) [Y3 X2 Y1] + (0.25) [X4 X5 X2] + (0.25) [Y4 Y5 X2] + (0.25) [Y4 X5 Y2] + (0.25) [X5 X4 X0] + (0.25) [Y5 Y4 X0] + (0.25) [Y5 X4 Y0] Args: graph (nx.DiGraph): the directed graph specifying possible edges Returns: qml.Hamiltonian: the cycle-mixer Hamiltonian """ hamiltonian = Hamiltonian([], []) for edge in graph.edges: hamiltonian += _partial_cycle_mixer(graph, edge) return hamiltonian
def _inner_net_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> Hamiltonian: r"""Calculates the squared inner portion of the Hamiltonian in :func:`net_flow_constraint`. For a given :math:`i`, this function returns: .. math:: \left((d_{i}^{\rm out} - d_{i}^{\rm in})\mathbb{I} - \sum_{j, (i, j) \in E} Z_{ij} + \sum_{j, (j, i) \in E} Z_{ji} \right)^{2}. Args: graph (nx.DiGraph): the directed graph specifying possible edges node: a fixed node Returns: qml.Hamiltonian: The inner part of the net-flow constraint Hamiltonian. """ edges_to_qubits = edges_to_wires(graph) coeffs = [] ops = [] out_edges = graph.out_edges(node) in_edges = graph.in_edges(node) coeffs.append(len(out_edges) - len(in_edges)) ops.append(qml.Identity(0)) for edge in out_edges: wires = (edges_to_qubits[edge], ) coeffs.append(-1) ops.append(qml.PauliZ(wires)) for edge in in_edges: wires = (edges_to_qubits[edge], ) coeffs.append(1) ops.append(qml.PauliZ(wires)) coeffs, ops = _square_hamiltonian_terms(coeffs, ops) H = Hamiltonian(coeffs, ops) H.simplify() # store the valuable information that all observables are in one commuting group H.grouping_indices = [list(range(len(H.ops)))] return H
def _inner_out_flow_constraint_hamiltonian(graph: nx.DiGraph, node) -> Hamiltonian: r"""Calculates the inner portion of the Hamiltonian in :func:`out_flow_constraint`. For a given :math:`i`, this function returns: .. math:: d_{i}^{out}(d_{i}^{out} - 2)\mathbb{I} - 2(d_{i}^{out}-1)\sum_{j,(i,j)\in E}\hat{Z}_{ij} + ( \sum_{j,(i,j)\in E}\hat{Z}_{ij} )^{2} Args: graph (nx.DiGraph): the directed graph specifying possible edges node: a fixed node Returns: qml.Hamiltonian: The inner part of the out-flow constraint Hamiltonian. """ coeffs = [] ops = [] edges_to_qubits = edges_to_wires(graph) out_edges = graph.out_edges(node) d = len(out_edges) for edge in out_edges: wire = (edges_to_qubits[edge], ) coeffs.append(1) ops.append(qml.PauliZ(wire)) coeffs, ops = _square_hamiltonian_terms(coeffs, ops) for edge in out_edges: wire = (edges_to_qubits[edge], ) coeffs.append(-2 * (d - 1)) ops.append(qml.PauliZ(wire)) coeffs.append(d * (d - 2)) ops.append(qml.Identity(0)) H = Hamiltonian(coeffs, ops) H.simplify() # store the valuable information that all observables are in one commuting group H.grouping_indices = [list(range(len(H.ops)))] return H
def net_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: r"""Calculates the `net flow constraint <https://doi.org/10.1080/0020739X.2010.526248>`__ Hamiltonian for the maximum-weighted cycle problem. Given a subset of edges in a directed graph, the net-flow constraint imposes that the number of edges leaving any given node is equal to the number of edges entering the node, i.e., .. math:: \sum_{j, (i, j) \in E} x_{ij} = \sum_{j, (j, i) \in E} x_{ji}, for all nodes :math:`i`, where :math:`E` are the edges of the graph and :math:`x_{ij}` is a binary number that selects whether to include the edge :math:`(i, j)`. A set of edges has zero net flow whenever the following Hamiltonian is minimized: .. math:: \sum_{i \in V} \left((d_{i}^{\rm out} - d_{i}^{\rm in})\mathbb{I} - \sum_{j, (i, j) \in E} Z_{ij} + \sum_{j, (j, i) \in E} Z_{ji} \right)^{2}, where :math:`V` are the graph vertices, :math:`d_{i}^{\rm out}` and :math:`d_{i}^{\rm in}` are the outdegree and indegree, respectively, of node :math:`i` and :math:`Z_{ij}` is a qubit Pauli-Z matrix acting upon the wire specified by the pair :math:`(i, j)`. Mapping from edges to wires can be achieved using :func:`~.edges_to_wires`. Args: graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges Returns: qml.Hamiltonian: the net-flow constraint 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")): raise ValueError("Input graph must be directed") if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) hamiltonian = Hamiltonian([], []) graph_nodes = graph.node_indexes() if isinstance( graph, rx.PyDiGraph) else graph.nodes for node in graph_nodes: hamiltonian += _inner_net_flow_constraint_hamiltonian(graph, node) return hamiltonian
def out_flow_constraint(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: r"""Calculates the `out flow constraint <https://1qbit.com/whitepaper/arbitrage/>`__ Hamiltonian for the maximum-weighted cycle problem. Given a subset of edges in a directed graph, the out-flow constraint imposes that at most one edge can leave any given node, i.e., for all :math:`i`: .. math:: \sum_{j,(i,j)\in E}x_{ij} \leq 1, where :math:`E` are the edges of the graph and :math:`x_{ij}` is a binary number that selects whether to include the edge :math:`(i, j)`. A set of edges satisfies the out-flow constraint whenever the following Hamiltonian is minimized: .. math:: \sum_{i\in V}\left(d_{i}^{out}(d_{i}^{out} - 2)\mathbb{I} - 2(d_{i}^{out}-1)\sum_{j,(i,j)\in E}\hat{Z}_{ij} + \left( \sum_{j,(i,j)\in E}\hat{Z}_{ij} \right)^{2}\right) where :math:`V` are the graph vertices, :math:`d_{i}^{\rm out}` is the outdegree of node :math:`i`, and :math:`Z_{ij}` is a qubit Pauli-Z matrix acting upon the qubit specified by the pair :math:`(i, j)`. Mapping from edges to wires can be achieved using :func:`~.edges_to_wires`. Args: graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges Returns: qml.Hamiltonian: the out flow constraint Hamiltonian Raises: ValueError: if the input graph is not directed """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) 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 for node in graph_nodes: hamiltonian += _inner_out_flow_constraint_hamiltonian(graph, node) return hamiltonian
def _partial_cycle_mixer(graph: nx.DiGraph, edge: Tuple) -> Hamiltonian: r"""Calculates the partial cycle-mixer Hamiltonian for a specific edge. For an edge :math:`(i, j)`, this function returns: .. math:: \sum_{k \in V, k\neq i, k\neq j, (i, k) \in E, (k, j) \in E}\left[ X_{ij}X_{ik}X_{kj} + Y_{ij}Y_{ik}X_{kj} + Y_{ij}X_{ik}Y_{kj} - X_{ij}Y_{ik}Y_{kj}\right] Args: graph (nx.DiGraph): the directed graph specifying possible edges edge (tuple): a fixed edge Returns: qml.Hamiltonian: the partial cycle-mixer Hamiltonian """ coeffs = [] ops = [] edges_to_qubits = edges_to_wires(graph) 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] t = qml.PauliX(wires=wire) @ qml.PauliX( wires=out_wire) @ qml.PauliX(wires=in_wire) ops.append(t) t = qml.PauliY(wires=wire) @ qml.PauliY( wires=out_wire) @ qml.PauliX(wires=in_wire) ops.append(t) t = qml.PauliY(wires=wire) @ qml.PauliX( wires=out_wire) @ qml.PauliY(wires=in_wire) ops.append(t) t = qml.PauliX(wires=wire) @ qml.PauliY( wires=out_wire) @ qml.PauliY(wires=in_wire) ops.append(t) coeffs.extend([0.25, 0.25, 0.25, -0.25]) return Hamiltonian(coeffs, ops)
def loss_hamiltonian(graph: nx.Graph) -> Hamiltonian: r"""Calculates the loss Hamiltonian for the maximum-weighted cycle problem. We consider the problem of selecting a cycle from a graph that has the greatest product of edge weights, as outlined `here <https://1qbit.com/whitepaper/arbitrage/>`__. The product of weights of a subset of edges in a graph is given by .. math:: P = \prod_{(i, j) \in E} [(c_{ij} - 1)x_{ij} + 1] where :math:`E` are the edges of the graph, :math:`x_{ij}` is a binary number that selects whether to include the edge :math:`(i, j)` and :math:`c_{ij}` is the corresponding edge weight. Our objective is to maximimize :math:`P`, subject to selecting the :math:`x_{ij}` so that our subset of edges composes a cycle. The product of edge weights is maximized by equivalently considering .. math:: \sum_{(i, j) \in E} x_{ij}\log c_{ij}, assuming :math:`c_{ij} > 0`. This can be restated as a minimization of the expectation value of the following qubit Hamiltonian: .. math:: H = \sum_{(i, j) \in E} Z_{ij}\log c_{ij}. where :math:`Z_{ij}` is a qubit Pauli-Z matrix acting upon the wire specified by the edge :math:`(i, j)`. Mapping from edges to wires can be achieved using :func:`~.edges_to_wires`. .. note:: The expectation value of the returned Hamiltonian :math:`H` is not equal to :math:`P`, but minimizing the expectation value of :math:`H` is equivalent to maximizing :math:`P`. Also note that the returned Hamiltonian does not impose that the selected set of edges is a cycle. This constraint can be enforced using a penalty term or by selecting a QAOA mixer Hamiltonian that only transitions between states that correspond to cycles. **Example** >>> import networkx as nx >>> g = nx.complete_graph(3).to_directed() >>> edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edges)} >>> for k, v in edge_weight_data.items(): g[k[0]][k[1]]["weight"] = v >>> h = loss_hamiltonian(g) >>> 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 Returns: qml.Hamiltonian: the loss Hamiltonian Raises: ValueError: if the graph contains self-loops KeyError: if one or more edges do not contain weight data """ edges_to_qubits = edges_to_wires(graph) coeffs = [] ops = [] edges_data = graph.edges(data=True) for edge_data in edges_data: edge = edge_data[:2] if edge[0] == edge[1]: raise ValueError("Graph contains self-loops") try: weight = edge_data[2]["weight"] except KeyError as e: raise KeyError(f"Edge {edge} does not contain weight data") from e coeffs.append(np.log(weight)) ops.append(qml.PauliZ(wires=edges_to_qubits[edge])) return Hamiltonian(coeffs, ops)
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`. For a given :math:`i`, this function returns: .. math:: \left((d_{i}^{\rm out} - d_{i}^{\rm in})\mathbb{I} - \sum_{j, (i, j) \in E} Z_{ij} + \sum_{j, (j, i) \in E} Z_{ji} \right)^{2}. Args: graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges node: a fixed node Returns: qml.Hamiltonian: The inner part of the net-flow constraint Hamiltonian. """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) edges_to_qubits = edges_to_wires(graph) coeffs = [] ops = [] is_rx = isinstance(graph, rx.PyDiGraph) out_edges = graph.out_edges(node) in_edges = graph.in_edges(node) # To ensure out_edges and in_edges methods in both RX and NX return # the lists of edges in the same order, we sort results. if is_rx: out_edges = sorted(out_edges) in_edges = sorted(in_edges) # In RX each node is assigned to an integer index starting from 0; # thus, we use the following 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)) for edge in out_edges: if len(edge) > 2: edge = tuple(edge[:2]) wires = (edges_to_qubits[get_nvalues(edge)], ) coeffs.append(-1) ops.append(qml.PauliZ(wires)) for edge in in_edges: if len(edge) > 2: edge = tuple(edge[:2]) wires = (edges_to_qubits[get_nvalues(edge)], ) coeffs.append(1) ops.append(qml.PauliZ(wires)) coeffs, ops = _square_hamiltonian_terms(coeffs, ops) H = Hamiltonian(coeffs, ops) H.simplify() # store the valuable information that all observables are in one commuting group H.grouping_indices = [list(range(len(H.ops)))] return H
def 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 weights, as outlined `here <https://1qbit.com/whitepaper/arbitrage/>`__. The product of weights of a subset of edges in a graph is given by .. math:: P = \prod_{(i, j) \in E} [(c_{ij} - 1)x_{ij} + 1] where :math:`E` are the edges of the graph, :math:`x_{ij}` is a binary number that selects whether to include the edge :math:`(i, j)` and :math:`c_{ij}` is the corresponding edge weight. Our objective is to maximimize :math:`P`, subject to selecting the :math:`x_{ij}` so that our subset of edges composes a cycle. The product of edge weights is maximized by equivalently considering .. math:: \sum_{(i, j) \in E} x_{ij}\log c_{ij}, assuming :math:`c_{ij} > 0`. This can be restated as a minimization of the expectation value of the following qubit Hamiltonian: .. math:: H = \sum_{(i, j) \in E} Z_{ij}\log c_{ij}. where :math:`Z_{ij}` is a qubit Pauli-Z matrix acting upon the wire specified by the edge :math:`(i, j)`. Mapping from edges to wires can be achieved using :func:`~.edges_to_wires`. .. note:: The expectation value of the returned Hamiltonian :math:`H` is not equal to :math:`P`, but minimizing the expectation value of :math:`H` is equivalent to maximizing :math:`P`. Also note that the returned Hamiltonian does not impose that the selected set of edges is a cycle. This constraint can be enforced using a penalty term or by selecting a QAOA mixer Hamiltonian that only transitions between states that correspond to cycles. **Example** >>> import networkx as nx >>> g = nx.complete_graph(3).to_directed() >>> edge_weight_data = {edge: (i + 1) * 0.5 for i, edge in enumerate(g.edges)} >>> for k, v in edge_weight_data.items(): g[k[0]][k[1]]["weight"] = v >>> h = loss_hamiltonian(g) >>> print(h) (-0.6931471805599453) [Z0] + (0.0) [Z1] + (0.4054651081081644) [Z2] + (0.6931471805599453) [Z3] + (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(sorted(g.edge_list()))} >>> for k, v in edge_weight_data.items(): g.update_edge(k[0], k[1], {"weight": v}) >>> h = loss_hamiltonian(g) >>> print(h) (-0.6931471805599453) [Z0] + (0.0) [Z1] + (0.4054651081081644) [Z2] + (0.6931471805599453) [Z3] + (0.9162907318741551) [Z4] + (1.0986122886681098) [Z5] Args: graph (nx.Graph or rx.PyGraph or rx.PyDiGraph): the graph specifying possible edges Returns: qml.Hamiltonian: the loss Hamiltonian Raises: 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( f"Input graph must be a nx.Graph or rx.PyGraph or rx.PyDiGraph, got {type(graph).__name__}" ) edges_to_qubits = edges_to_wires(graph) coeffs = [] ops = [] is_rx = isinstance(graph, (rx.PyGraph, rx.PyDiGraph)) edges_data = sorted(graph.weighted_edge_list()) if is_rx else graph.edges( data=True) # In RX each node is assigned to an integer index starting from 0; # thus, we use the following 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: edge = edge_data[:2] if edge[0] == edge[1]: raise ValueError("Graph contains self-loops") try: weight = edge_data[2]["weight"] except KeyError as e: raise KeyError(f"Edge {edge} does not contain weight data") from e except TypeError as e: raise TypeError(f"Edge {edge} does not contain weight data") from e coeffs.append(np.log(weight)) ops.append(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 H.grouping_indices = [list(range(len(H.ops)))] return H
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: .. math:: \sum_{k \in V, k\neq i, k\neq j, (i, k) \in E, (k, j) \in E}\left[ X_{ij}X_{ik}X_{kj} + Y_{ij}Y_{ik}X_{kj} + Y_{ij}X_{ik}Y_{kj} - X_{ij}Y_{ik}Y_{kj}\right] Args: graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges edge (tuple): a fixed edge Returns: qml.Hamiltonian: the partial cycle-mixer Hamiltonian """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) coeffs = [] ops = [] is_rx = isinstance(graph, rx.PyDiGraph) edges_to_qubits = edges_to_wires(graph) graph_nodes = graph.node_indexes() if is_rx else graph.nodes graph_edges = sorted(graph.edge_list()) if is_rx else graph.edges # In RX each node is assigned to an integer index starting from 0; # thus, we use the following 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: out_edge = (edge[0], node) in_edge = (node, edge[1]) if node not in edge and out_edge in graph_edges and in_edge in graph_edges: wire = edges_to_qubits[get_nvalues(edge)] out_wire = edges_to_qubits[get_nvalues(out_edge)] in_wire = edges_to_qubits[get_nvalues(in_edge)] t = qml.PauliX(wires=wire) @ qml.PauliX( wires=out_wire) @ qml.PauliX(wires=in_wire) ops.append(t) t = qml.PauliY(wires=wire) @ qml.PauliY( wires=out_wire) @ qml.PauliX(wires=in_wire) ops.append(t) t = qml.PauliY(wires=wire) @ qml.PauliX( wires=out_wire) @ qml.PauliY(wires=in_wire) ops.append(t) t = qml.PauliX(wires=wire) @ qml.PauliY( wires=out_wire) @ qml.PauliY(wires=in_wire) ops.append(t) coeffs.extend([0.25, 0.25, 0.25, -0.25]) return Hamiltonian(coeffs, ops)
def cycle_mixer(graph: Union[nx.DiGraph, rx.PyDiGraph]) -> Hamiltonian: r"""Calculates the cycle-mixer Hamiltonian. Following methods outlined `here <https://arxiv.org/pdf/1709.03489.pdf>`__, the cycle-mixer Hamiltonian preserves the set of valid cycles: .. math:: \frac{1}{4}\sum_{(i, j)\in E} \left(\sum_{k \in V, k\neq i, k\neq j, (i, k) \in E, (k, j) \in E} \left[X_{ij}X_{ik}X_{kj} +Y_{ij}Y_{ik}X_{kj} + Y_{ij}X_{ik}Y_{kj} - X_{ij}Y_{ik}Y_{kj}\right] \right) where :math:`E` are the edges of the directed graph. A valid cycle is defined as a subset of edges in :math:`E` such that all of the graph's nodes :math:`V` have zero net flow (see the :func:`~.net_flow_constraint` function). **Example** >>> import networkx as nx >>> g = nx.complete_graph(3).to_directed() >>> h_m = cycle_mixer(g) >>> print(h_m) (-0.25) [X0 Y1 Y5] + (-0.25) [X1 Y0 Y3] + (-0.25) [X2 Y3 Y4] + (-0.25) [X3 Y2 Y1] + (-0.25) [X4 Y5 Y2] + (-0.25) [X5 Y4 Y0] + (0.25) [X0 X1 X5] + (0.25) [Y0 Y1 X5] + (0.25) [Y0 X1 Y5] + (0.25) [X1 X0 X3] + (0.25) [Y1 Y0 X3] + (0.25) [Y1 X0 Y3] + (0.25) [X2 X3 X4] + (0.25) [Y2 Y3 X4] + (0.25) [Y2 X3 Y4] + (0.25) [X3 X2 X1] + (0.25) [Y3 Y2 X1] + (0.25) [Y3 X2 Y1] + (0.25) [X4 X5 X2] + (0.25) [Y4 Y5 X2] + (0.25) [Y4 X5 Y2] + (0.25) [X5 X4 X0] + (0.25) [Y5 Y4 X0] + (0.25) [Y5 X4 Y0] >>> import retworkx as rx >>> g = rx.generators.directed_mesh_graph(3, [0,1,2]) >>> h_m = cycle_mixer(g) >>> print(h_m) (-0.25) [X0 Y1 Y5] + (-0.25) [X1 Y0 Y3] + (-0.25) [X2 Y3 Y4] + (-0.25) [X3 Y2 Y1] + (-0.25) [X4 Y5 Y2] + (-0.25) [X5 Y4 Y0] + (0.25) [X0 X1 X5] + (0.25) [Y0 Y1 X5] + (0.25) [Y0 X1 Y5] + (0.25) [X1 X0 X3] + (0.25) [Y1 Y0 X3] + (0.25) [Y1 X0 Y3] + (0.25) [X2 X3 X4] + (0.25) [Y2 Y3 X4] + (0.25) [Y2 X3 Y4] + (0.25) [X3 X2 X1] + (0.25) [Y3 Y2 X1] + (0.25) [Y3 X2 Y1] + (0.25) [X4 X5 X2] + (0.25) [Y4 Y5 X2] + (0.25) [Y4 X5 Y2] + (0.25) [X5 X4 X0] + (0.25) [Y5 Y4 X0] + (0.25) [Y5 X4 Y0] Args: graph (nx.DiGraph or rx.PyDiGraph): the directed graph specifying possible edges Returns: qml.Hamiltonian: the cycle-mixer Hamiltonian """ if not isinstance(graph, (nx.DiGraph, rx.PyDiGraph)): raise ValueError( f"Input graph must be a nx.DiGraph or rx.PyDiGraph, got {type(graph).__name__}" ) hamiltonian = Hamiltonian([], []) graph_edges = sorted(graph.edge_list()) if isinstance( graph, rx.PyDiGraph) else graph.edges for edge in graph_edges: hamiltonian += _partial_cycle_mixer(graph, edge) return hamiltonian