import networkx as nx
import copy
from typing import Tuple, Any, Optional, Sequence, Set, Union
OrderPair = Tuple[float, float]
MissingOrder = Tuple[Set[float], Set[float]]
GraphType = Union[nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph]
TypesGHTuple = Tuple[Any, ...] # e.g., ('H', 0, 0, 0, ['C'])
def _extract_leaf_candidates(orig_th: Tuple[Any, ...]) -> Tuple[TypesGHTuple, ...]:
"""
Flatten one level: if an element is a sequence whose elements are themselves sequences,
treat those inner sequences as candidates; otherwise the element itself is a candidate.
"""
candidates: list[TypesGHTuple] = []
for item in orig_th:
if (
isinstance(item, (list, tuple))
and item
and all(isinstance(inner, (list, tuple)) for inner in item)
):
# e.g., (('H',...), ('H',...)) -> take inner tuples
for inner in item:
if isinstance(inner, (list, tuple)):
candidates.append(tuple(inner))
else:
if isinstance(item, (list, tuple)):
candidates.append(tuple(item))
else:
# non-sequence fallback, wrap to tuple for uniformity
candidates.append((item,))
return tuple(candidates)
[docs]
def normalize_hcount_and_typesGH(G: GraphType) -> GraphType:
"""
Return a fresh copy of G where:
* each node's `hcount` attribute is set to 0
* each node's `typesGH` is processed as follows:
1. Flatten one level so that nested tuples-of-tuples are expanded.
2. Drop any tuple that contains a `set` anywhere.
3. From the remaining tuples, keep only the first and last (if more than two).
4. Zero indices 1 and 2 in each kept tuple (if they exist).
5. If nothing remains after dropping, result is an empty tuple.
:param G: input NetworkX graph
:type G: nx.Graph or nx.DiGraph or nx.MultiGraph or nx.MultiDiGraph
:returns: a new graph with normalized hcount and typesGH
:rtype: same type as G
:raises: TypeError if G is not a supported NetworkX graph or if typesGH is malformed.
"""
if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
raise TypeError(f"Expected a NetworkX graph, got {type(G)!r}")
H = G.__class__()
H.graph.update(copy.deepcopy(G.graph))
for node, data in G.nodes(data=True):
new_data = data.copy()
new_data["hcount"] = 0
orig_th = data.get("typesGH", ())
if not isinstance(orig_th, (list, tuple)):
raise TypeError(
f"Node {node} has typesGH of unexpected type {type(orig_th)}; expected sequence"
)
# Step 1: flatten one level of nested tuples-of-tuples
candidates = _extract_leaf_candidates(tuple(orig_th))
# Step 2: drop any tuple containing a set
filtered = [
t for t in candidates if not any(isinstance(elem, set) for elem in t)
]
# Step 3: select first and last (or whatever remains)
if not filtered:
selected: Tuple[TypesGHTuple, ...] = ()
elif len(filtered) > 2:
selected = (filtered[0], filtered[-1])
else:
selected = tuple(filtered) # 1 or 2 elements
# Step 4: zero indices 1 and 2
normalized: list[TypesGHTuple] = []
for inner in selected:
if not isinstance(inner, (list, tuple)):
raise TypeError(
f"Inner element of typesGH for node {node} is not tuple-like: {inner!r}"
)
inner_list = list(inner)
if len(inner_list) > 1:
inner_list[1] = 0
if len(inner_list) > 2:
inner_list[2] = 0
normalized.append(tuple(inner_list))
new_data["typesGH"] = tuple(normalized)
H.add_node(node, **new_data)
# Copy edges appropriately
if G.is_multigraph():
for u, v, key, edata in G.edges(keys=True, data=True):
H.add_edge(u, v, key=key, **copy.deepcopy(edata))
else:
for u, v, edata in G.edges(data=True):
H.add_edge(u, v, **copy.deepcopy(edata))
return H
[docs]
def normalize_order(G: nx.Graph) -> nx.Graph:
"""
Return a copy of the graph with each edge's 'order' attribute normalized.
If an edge has an 'order' attribute that is a sequence of length >= 2,
it is replaced by the 2-tuple returned by :func:`extract_order_norm`,
if that function returns a non-None result.
:param G: Input NetworkX graph
:type G: nx.Graph, nx.DiGraph, nx.MultiGraph, or nx.MultiDiGraph
:returns: A new graph of the same type with normalized edge 'order'
:rtype: same as G
:raises TypeError: If G is not a NetworkX graph
:example:
>>> import networkx as nx
>>> G = nx.Graph()
>>> G.add_edge(1, 2, order=[(1,2), ({3},{4}), (5,6)])
>>> H = normalize_order(G)
>>> H.edges[1,2]['order']
(1, 6)
"""
from copy import deepcopy
if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
raise TypeError(f"Expected a NetworkX graph, got {type(G)}")
H = deepcopy(G)
# Iterate edges appropriately
if H.is_multigraph():
edge_iter = H.edges(keys=True, data=True)
for _, _, _, attr in edge_iter:
order = attr.get("order")
if isinstance(order, (list, tuple)) and len(order) >= 2:
norm = extract_order_norm(order)
if norm is not None:
attr["order"] = norm
else:
for _, _, attr in H.edges(data=True):
order = attr.get("order")
if isinstance(order, (list, tuple)) and len(order) >= 2:
norm = extract_order_norm(order)
if norm is not None:
attr["order"] = norm
return H
# def normalize_hcount_and_typesGH(G):
# """
# Return a fresh copy of G where:
# - each node's `hcount` attribute is set to 0
# - in each tuple of `typesGH`, indices 1 and 2 are set to 0
# :param G: input NetworkX graph
# :type G: nx.Graph or nx.DiGraph or nx.MultiGraph or nx.MultiDiGraph
# :returns: a new graph with normalized hcount and typesGH
# :rtype: same type as G
# :raises: TypeError if G is not a NetworkX graph
# :example:
# >>> G = nx.Graph()
# >>> G.add_node(1, hcount=2, typesGH=(("C", 1, 2), ("O", 0, 1)))
# >>> H = normalize_hcount_and_typesGH(G)
# >>> H.nodes[1]['hcount']
# 0
# >>> H.nodes[1]['typesGH']
# (('C', 0, 0), ('O', 0, 0))
# """
# if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
# raise TypeError(f"Expected NetworkX graph, got {type(G)}")
# # Create empty graph of same class and copy graph-level attributes
# H = G.__class__()
# H.graph.update(copy.deepcopy(G.graph))
# # Copy and normalize node data
# for node, data in G.nodes(data=True):
# new_data = data.copy()
# new_data["hcount"] = 0
# orig_th = data.get("typesGH", ())
# new_th = []
# for inner in orig_th:
# inner_list = list(inner)
# # Zero the aromatic slot (index 1) and the hcountGH slot (index 2)
# if len(inner_list) > 1:
# inner_list[1] = 0
# if len(inner_list) > 2:
# inner_list[2] = 0
# new_th.append(tuple(inner_list))
# new_data["typesGH"] = tuple(new_th)
# H.add_node(node, **new_data)
# # Copy edges (with keys for multigraphs)
# if G.is_multigraph():
# for u, v, key, edata in G.edges(keys=True, data=True):
# H.add_edge(u, v, key=key, **copy.deepcopy(edata))
# else:
# for u, v, edata in G.edges(data=True):
# H.add_edge(u, v, **copy.deepcopy(edata))
# return H
[docs]
def label_mtg_edges(G: nx.Graph, inplace: bool = False) -> nx.Graph:
"""
Label each edge in the MTG graph with a boolean 'is_mtg' attribute based on two criteria:
1. There are at least two steps where the standard order (order[0] - order[1]) is non-zero.
2. The sum of all non-None standard orders is zero.
:param G: Input MTG graph with 'order' history per edge
:type G: nx.Graph or nx.DiGraph
:param inplace: If True, modify G in place; otherwise work on a copy
:type inplace: bool
:returns: Graph with 'is_mtg' boolean attribute on each edge
:rtype: same type as G
:raises TypeError: If G is not a NetworkX Graph
:example:
>>> import networkx as nx
>>> G = nx.Graph()
>>> # Single change only -> less than 2 non-zero steps => False
>>> G.add_edge(7,3, order=((1.0,1.0),(1.0,0)))
>>> H = label_mtg_edges(G)
>>> H.edges[7,3]['is_mtg']
False
>>> # Two-step equal but opposite changes -> sum zero and count>=2 => True
>>> G = nx.Graph()
>>> G.add_edge(2,1, order=((1.0,2.0),(2.0,1.0)))
>>> H = label_mtg_edges(G)
>>> H.edges[2,1]['is_mtg']
True
"""
if not isinstance(G, (nx.Graph, nx.DiGraph)):
raise TypeError(f"Expected a NetworkX Graph, got {type(G)}")
graph = G if inplace else G.copy()
for u, v, attr in graph.edges(data=True):
history = attr.get("order")
# Extract numeric standard orders
std_vals: list[float] = []
if isinstance(history, (list, tuple)) and len(history) >= 2:
for entry in history:
if (
isinstance(entry, tuple)
and len(entry) == 2
and all(isinstance(x, (int, float)) for x in entry)
):
std_vals.append(entry[0] - entry[1])
# Apply criteria
non_zero_count = sum(1 for v in std_vals if v != 0)
total = sum(std_vals)
attr["is_mtg"] = non_zero_count >= 2 and total == 0
return graph
[docs]
def compute_standard_order(G: nx.Graph, inplace: bool = False) -> nx.Graph:
"""
Compute and assign the 'standard_order' attribute for each edge in the graph.
'standard_order' is defined as the difference order[0] - order[1]
for edges whose 'order' attribute is a 2-tuple of numeric values.
:param G: Input NetworkX graph
:type G: nx.Graph, nx.DiGraph, nx.MultiGraph, or nx.MultiDiGraph
:param inplace: If True, modify G in-place; otherwise operate on a copy
:type inplace: bool
:returns: Graph with 'standard_order' attributes set
:rtype: same type as G
:raises TypeError: If G is not a NetworkX graph
:example:
>>> import networkx as nx
>>> G = nx.Graph()
>>> G.add_edge(7, 3, order=(1.0, 0))
>>> H = compute_standard_order(G)
>>> H.edges[7,3]['standard_order']
1.0
"""
if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
raise TypeError(f"Expected a NetworkX graph, got {type(G)}")
graph = G if inplace else G.copy()
if graph.is_multigraph():
for u, v, key, attr in graph.edges(keys=True, data=True):
order = attr.get("order")
if isinstance(order, tuple) and len(order) == 2:
a, b = order
try:
attr["standard_order"] = a - b
except Exception:
attr["standard_order"] = None
else:
for u, v, attr in graph.edges(data=True):
order = attr.get("order")
if isinstance(order, tuple) and len(order) == 2:
a, b = order
try:
attr["standard_order"] = a - b
except Exception:
attr["standard_order"] = None
return graph
# def extract_order_norm(order_tuple):
# """
# Given a sequence of four 2-element tuples (order data), return the normalized order:
# - left: first element of the first tuple that is not both sets
# - right: second element of the last tuple that is not both sets
# :param order_tuple: tuple of four 2-element tuples
# :type order_tuple: tuple(tuple, tuple, tuple, tuple)
# :returns: normalized (left, right) or None if not found
# :rtype: tuple or None
# :raises: ValueError if order_tuple is not length 4
# :example:
# >>> ot = (({1}, {2}), (3, 4), ({5}, {6}), (7, 8))
# >>> extract_order_norm(ot)
# (3, 8)
# """
# if not (isinstance(order_tuple, tuple) and len(order_tuple) == 4):
# raise ValueError("order_tuple must be a tuple of length 4")
# left = None
# right = None
# # Find first non-all-set tuple for left
# for a, b in order_tuple:
# if not (isinstance(a, set) and isinstance(b, set)):
# left = a
# break
# # Find last non-all-set tuple for right
# for a, b in reversed(order_tuple):
# if not (isinstance(a, set) and isinstance(b, set)):
# right = b
# break
# return (left, right) if (left is not None and right is not None) else None
# def normalize_order(G):
# """
# Return a copy of G with edge attribute 'order' normalized.
# For each edge, if the 'order' attribute is a 4-tuple, replace it with the
# normalized 2-tuple returned by extract_order_norm.
# :param G: input NetworkX graph
# :type G: nx.Graph or nx.DiGraph or nx.MultiGraph or nx.MultiDiGraph
# :returns: a new graph with normalized edge orders
# :rtype: same type as G
# :raises: TypeError if G is not a NetworkX graph
# :example:
# >>> G = nx.Graph()
# >>> G.add_edge(1, 2, order=((1,2), ({3},{4}), ({5},{6}), (7,8)))
# >>> H = copy_and_normalize_order(G)
# >>> H.edges[1,2]['order']
# (1, 8)
# """
# if not isinstance(G, (nx.Graph, nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph)):
# raise TypeError(f"Expected NetworkX graph, got {type(G)}")
# H = G.copy()
# for _, _, _, attr in (
# H.edges(keys=True, data=True)
# if H.is_multigraph()
# else [(u, v, None, attr) for u, v, attr in H.edges(data=True)]
# ):
# order = attr.get("order")
# if isinstance(order, tuple) and len(order) == 4:
# norm = extract_order_norm(order)
# if norm is not None:
# attr["order"] = norm
# return H