"""Graph related functions and classes. Uses NetworkX for graph operations."""
from dataclasses import dataclass
from typing import Sequence
import networkx as nx
from ..graphics.common import common_properties, Point
from ..graphics.all_enums import Types
from ..settings.settings import defaults
from ..geometry.geometry import distance, close_points2
[docs]
@dataclass
class GraphEdge:
"""Edge in a graph. It has a start and end point as nodes."""
start: Point
end: Point
def __post_init__(self):
"""Initialize the GraphEdge with start and end points."""
self.length = distance(self.start.pos, self.end.pos)
common_properties(self)
@property
def nodes(self):
"""Return the start and end nodes of the edge."""
return (self.start, self.end)
[docs]
def edges2nodes(edges: Sequence[Sequence]) -> Sequence:
"""
Given a list of edges, return a connected list of nodes.
Args:
edges (Sequence[Sequence]): List of edges.
Returns:
Sequence: Connected list of nodes.
"""
chain = longest_chain(edges)
closed = chain[0][0] == chain[-1][-1]
if closed:
nodes = [x[0] for x in chain[:-1]]
else:
nodes = [x[0] for x in chain] + [chain[-1][1]]
if closed:
last_edge = chain[-1]
if last_edge[1] == nodes[-1]:
nodes.extend(reversed(last_edge))
elif last_edge[0] == nodes[-1]:
nodes.extend(last_edge)
elif last_edge[0] == nodes[0]:
nodes.extend(reversed(last_edge))
elif last_edge[1] == nodes[0]:
nodes.extend(last_edge)
return nodes
[docs]
def get_cycles(edges: Sequence[GraphEdge]) -> Sequence[GraphEdge]:
"""
Computes all the cycles in a given graph of edges.
Args:
edges (Sequence[GraphEdge]): List of graph edges.
Returns:
Sequence[GraphEdge]: List of cycles if any cycle is found, None otherwise.
"""
nx_graph = nx.Graph()
nx_graph.add_edges_from(edges)
cycles = nx.cycle_basis(nx_graph)
res = None
if cycles:
for cycle in cycles:
cycle.append(cycle[0])
res = cycles
return res
# find all open paths starting from a given node
[docs]
def find_all_paths(graph, node):
"""
Find all paths starting from a given node.
Args:
graph (nx.Graph): The graph.
node: The starting node.
Returns:
List: All paths starting from the given node.
"""
paths = []
for node_ in graph.nodes():
for path in nx.all_simple_paths(graph, node, node_):
if len(path) > 1:
paths.append(path)
return paths
[docs]
def is_open_walk2(graph, island):
"""
Given a NetworkX Graph and an island, return True if the given island is an open walk.
Args:
graph (nx.Graph): The graph.
island: The island.
Returns:
bool: True if the island is an open walk, False otherwise.
"""
degrees = [graph.degree(node) for node in island]
return set(degrees) == {1, 2} and degrees.count(1) == 2
[docs]
def longest_chain(edges: Sequence[Sequence]) -> Sequence:
"""
Given a list of graph edges, return a list of connected nodes.
Args:
edges (Sequence[Sequence]): List of graph edges.
Returns:
Sequence: List of connected nodes.
"""
def add_edge(edge, chain, index, processed):
nonlocal no_change
if index == 0:
chain.insert(0, edge)
elif index == -1:
chain.append(edge)
processed.append(set(edge))
no_change = False
chain = []
processed = []
while len(chain) < len(edges):
no_change = True
for edge in edges:
if set(edge) in processed:
continue
if not chain:
add_edge(edge, chain, -1, processed)
else:
if edge[0] == chain[-1][1]:
add_edge(edge, chain, -1, processed)
elif edge[0] == chain[0][0]:
add_edge((edge[1], edge[0]), chain, 0, processed)
elif edge[1] == chain[0][0]:
add_edge(edge, chain, 0, processed)
elif edge[1] == chain[-1][1]:
add_edge((edge[1], edge[0]), chain, -1, processed)
if no_change:
break
return chain
[docs]
def is_cycle(graph: nx.Graph, island: Sequence) -> bool:
"""
Given a NetworkX Graph and an island, return True if the given island is a cycle.
Args:
graph (nx.Graph): The graph.
island (Sequence): The island.
Returns:
bool: True if the island is a cycle, False otherwise.
"""
degrees = [graph.degree(node) for node in island]
return set(degrees) == {2}
[docs]
def is_open_walk(graph: nx.Graph, island: Sequence) -> bool:
"""
Given a NetworkX Graph and an island, return True if the given island is an open walk.
Args:
graph (nx.Graph): The graph.
island (Sequence): The island.
Returns:
bool: True if the island is an open walk, False otherwise.
"""
if len(island) == 2:
return True
degrees = [graph.degree(node) for node in island]
return set(degrees) == {1, 2} and degrees.count(1) == 2
[docs]
def graph_summary(graph: nx.Graph) -> str:
"""
Return a summary of a graph including cycles, open walks and degenerate nodes.
Args:
graph (nx.Graph): The graph.
Returns:
str: Summary of the graph.
"""
lines = []
for island in nx.connected_components(graph):
if len(island) > 8:
island = list(island)
lines.append(f"Island: {island[:4]}, ... , {island[-4:]}")
else:
lines.append(f"Island: {island}")
if is_cycle(graph, island):
lines.append(f"Cycle: {len(island)} nodes")
elif is_open_walk(graph, island):
lines.append(f"Open Walk: {len(island)} nodes")
else:
degenerates = [node for node in island if graph.degree(node) > 2]
degrees = f"{[(node, graph.degree(node)) for node in degenerates]}"
lines.append(f"Degenerate: {len(island)} nodes")
lines.append(f"(Node, Degree): {degrees}")
lines.append("-" * 40)
return "\n".join(lines)
[docs]
@dataclass
class Node:
"""
A Node object is a 2D point with x and y coordinates.
Used in graphs corresponding to shapes and batches.
Attributes:
x (float): X coordinate.
y (float): Y coordinate.
"""
x: float
y: float
def __post_init__(self):
"""Initialize the Node with x and y coordinates."""
common_properties(self)
@property
def pos(self):
"""Return the position of the node."""
return (self.x, self.y)
def __eq__(self, other: object) -> bool:
"""
Check if two nodes are equal.
Args:
other (object): The other node.
Returns:
bool: True if the nodes are equal, False otherwise.
"""
return close_points2(self.pos, other.pos, dist2=defaults["dist_tol"] ** 2)
[docs]
@dataclass
class Graph:
"""
A Graph object is a collection of nodes and edges.
Attributes:
type (Types): The type of the graph.
subtype (Types): The subtype of the graph.
nx_graph (nx.Graph): The NetworkX graph object.
"""
type: Types = "undirected"
subtype: Types = "none" # this can be Types.WEIGHTED
nx_graph: "nx.Graph" = None
def __post_init__(self):
"""Initialize the Graph with type and subtype."""
common_properties(self)
@property
def islands(self):
"""
Return a list of all islands both cyclic and acyclic.
Returns:
List: List of all islands.
"""
return [
list(island) for island in self.nx_graph.connected_components(self.nx_graph)
]
@property
def cycles(self):
"""
Return a list of cycles.
Returns:
List: List of cycles.
"""
return nx.cycle_basis(self.nx_graph)
@property
def open_walks(self):
"""
Return a list of open walks (aka open chains).
Returns:
List: List of open walks.
"""
res = []
for island in self.islands:
if is_open_walk(self.nx_graph, island):
res.append(island)
return res
@property
def edges(self):
"""
Return the edges of the graph.
Returns:
EdgeView: Edges of the graph.
"""
return self.nx_graph.edges
@property
def nodes(self):
"""
Return the nodes of the graph.
Returns:
NodeView: Nodes of the graph.
"""
return self.nx_graph.nodes