Source code for qarp.graphs._utils
import random
from collections import defaultdict
from itertools import combinations
from typing import Mapping, Optional
from qarp.operators import QubitOperator
from qarp.operators.functions import count_qubits
from ._graph import Graph, _check_qubit_label
from ._simplicial_complex import SimplicialComplex
[docs]
def graph_to_cost_hamiltonian(graph, *, linear: Optional[Mapping] = None) -> QubitOperator:
"""Ising cost Hamiltonian ``Σ_e w_e Z_i Z_j + Σ_i c_i Z_i`` of any networkx graph.
This is QAOA's Ising form, **not** the MaxCut objective
``Σ_e w_e (1 − Z_i Z_j) / 2``: the two differ by a sign and an offset, so
minimising this Hamiltonian maximises the cut, with
``cut = (Σ_e w_e − ⟨H⟩) / 2``.
Node labels are used directly as qubit indices and are validated as
non-negative integers; non-contiguous labels are allowed. The edge weight is
the ``weight`` attribute, defaulting to 1.0 for an edge without one (whatever
other attributes it carries).
Args:
graph: A networkx graph (``qarp.graphs.Graph`` or plain ``nx.Graph``).
linear: Mapping node → coefficient ``c_i`` of its ``Z_i`` term. ``None``
reads the per-node ``linear`` attributes, which
:func:`qubit_operator_to_graph` sets; an explicit mapping replaces them.
Returns:
QubitOperator: The cost Hamiltonian.
Raises:
TypeError: If a node label (or a ``linear`` key) is not a non-negative integer.
ValueError: On a self-loop ``(i, i)``: ``Z_i Z_i`` is the identity, so the
edge has no ZZ term — pass a linear term through ``linear=`` instead.
"""
for node in graph.nodes:
_check_qubit_label(node)
ham = QubitOperator()
for u, v, data in graph.edges(data=True):
_reject_self_loop(u, v)
ham += QubitOperator(f"Z{int(u)} Z{int(v)}", data.get("weight", 1.0))
if linear is None:
linear = {node: data["linear"] for node, data in graph.nodes(data=True) if "linear" in data}
for node, coeff in linear.items():
ham += QubitOperator(f"Z{_check_qubit_label(node)}", coeff)
return ham
def _reject_self_loop(u, v) -> None:
if u == v:
raise ValueError(
f"self-loop ({u!r}, {v!r}) has no two-qubit term: Z_i Z_i is the identity. "
"Encode a linear term with the `linear` node attribute or the linear= argument."
)
[docs]
def qubit_operator_to_graph(qubit_op) -> Graph:
"""Graph of a ``QubitOperator`` with only ``Z`` and ``Z Z`` terms.
Nodes are the qubits ``0 … count_qubits − 1``; each ``Z_i Z_j`` term becomes
the edge ``(i, j)`` with attribute ``weight`` and each ``Z_i`` term the node
attribute ``linear`` on ``i``, so :func:`graph_to_cost_hamiltonian` inverts
this up to the constant term, which has no home on a graph and is dropped.
Args:
qubit_op: QubitOperator with only Z or ZZ terms (plus a constant).
Returns:
Graph: The native graph.
Raises:
ValueError: On a non-Z Pauli or a term acting on more than two qubits.
"""
G = Graph()
G.add_nodes_from(range(count_qubits(qubit_op)))
for term, coeff in qubit_op.terms.items():
if not all(op == "Z" for _, op in term):
raise ValueError("Only Z Paulis are accepted")
qubits = [q for q, _ in term]
# Cost-Hamiltonian coefficients are physically real; qarpx operators
# store them as complex, so realify at this boundary (graph weights feed
# cut values, losses and rotation angles).
if len(qubits) == 1:
G.nodes[qubits[0]]["linear"] = complex(coeff).real
elif len(qubits) == 2:
G.add_edge(qubits[0], qubits[1], weight=complex(coeff).real)
elif len(qubits) > 2:
raise ValueError("Only linear and quadratic terms are accepted")
return G
[docs]
def lists_to_tuples(lst: list) -> list:
"""
Converts a list of lists describing edges or simplices into a list of tuples.
Parameters:
lst (list of lists): A list of edges or simplices, where each element is a list of nodes.
Returns:
list of tuples: The same elements represented as tuples.
"""
return [tuple(ele) for ele in lst]
[docs]
def generate_complete_simplicial_complex(n: int) -> SimplicialComplex:
"""
Generates a complete simplicial complex.
Parameters:
n (int): The number of nodes.
Returns:
SimplicialComplex: A complete simplicial complex with all possible simplices.
"""
return SimplicialComplex([tuple(range(n))])
[docs]
def generate_random_simplicial_complex(n: int, p: float) -> SimplicialComplex:
"""
Generates a random simplicial complex.
Every non-empty subset of ``range(n)`` is offered with probability ``p``;
closure then adds the faces of whatever was accepted. Draws come from the
global ``random`` module, so seed with ``random.seed`` for reproducibility.
Parameters:
n (int): The number of nodes.
p (float): The probability (0 <= p <= 1) for a simplex to be included.
Returns:
SimplicialComplex: A random simplicial complex with simplices included based on probability p.
"""
simpl = []
for k in range(1, n + 1):
for simplex in combinations(range(n), k):
if random.random() <= p: # Include simplex with probability p
simpl.append(list(simplex))
return SimplicialComplex(simpl)