simetri.helpers.graph

Graph related functions and classes. Uses NetworkX for graph operations.

  1"""Graph related functions and classes. Uses NetworkX for graph operations."""
  2
  3from dataclasses import dataclass
  4from typing import Sequence
  5
  6import networkx as nx
  7
  8from ..graphics.common import common_properties, Point
  9from ..graphics.all_enums import Types
 10from ..settings.settings import defaults
 11from ..geometry.geometry import distance, close_points2
 12
 13
 14@dataclass
 15class GraphEdge:
 16    """Edge in a graph. It has a start and end point as nodes."""
 17
 18    start: Point
 19    end: Point
 20
 21    def __post_init__(self):
 22        """Initialize the GraphEdge with start and end points."""
 23        self.length = distance(self.start.pos, self.end.pos)
 24        common_properties(self)
 25
 26    @property
 27    def nodes(self):
 28        """Return the start and end nodes of the edge."""
 29        return (self.start, self.end)
 30
 31
 32def edges2nodes(edges: Sequence[Sequence]) -> Sequence:
 33    """
 34    Given a list of edges, return a connected list of nodes.
 35
 36    Args:
 37        edges (Sequence[Sequence]): List of edges.
 38
 39    Returns:
 40        Sequence: Connected list of nodes.
 41    """
 42    chain = longest_chain(edges)
 43    closed = chain[0][0] == chain[-1][-1]
 44    if closed:
 45        nodes = [x[0] for x in chain[:-1]]
 46    else:
 47        nodes = [x[0] for x in chain] + [chain[-1][1]]
 48    if closed:
 49        last_edge = chain[-1]
 50        if last_edge[1] == nodes[-1]:
 51            nodes.extend(reversed(last_edge))
 52        elif last_edge[0] == nodes[-1]:
 53            nodes.extend(last_edge)
 54        elif last_edge[0] == nodes[0]:
 55            nodes.extend(reversed(last_edge))
 56        elif last_edge[1] == nodes[0]:
 57            nodes.extend(last_edge)
 58
 59    return nodes
 60
 61
 62def get_cycles(edges: Sequence[GraphEdge]) -> Sequence[GraphEdge]:
 63    """
 64    Computes all the cycles in a given graph of edges.
 65
 66    Args:
 67        edges (Sequence[GraphEdge]): List of graph edges.
 68
 69    Returns:
 70        Sequence[GraphEdge]: List of cycles if any cycle is found, None otherwise.
 71    """
 72    nx_graph = nx.Graph()
 73    nx_graph.add_edges_from(edges)
 74    cycles = nx.cycle_basis(nx_graph)
 75    res = None
 76    if cycles:
 77        for cycle in cycles:
 78            cycle.append(cycle[0])
 79
 80        res = cycles
 81    return res
 82
 83
 84# find all open paths starting from a given node
 85def find_all_paths(graph, node):
 86    """
 87    Find all paths starting from a given node.
 88
 89    Args:
 90        graph (nx.Graph): The graph.
 91        node: The starting node.
 92
 93    Returns:
 94        List: All paths starting from the given node.
 95    """
 96    paths = []
 97    for node_ in graph.nodes():
 98        for path in nx.all_simple_paths(graph, node, node_):
 99            if len(path) > 1:
100                paths.append(path)
101    return paths
102
103
104def is_open_walk2(graph, island):
105    """
106    Given a NetworkX Graph and an island, return True if the given island is an open walk.
107
108    Args:
109        graph (nx.Graph): The graph.
110        island: The island.
111
112    Returns:
113        bool: True if the island is an open walk, False otherwise.
114    """
115    degrees = [graph.degree(node) for node in island]
116    return set(degrees) == {1, 2} and degrees.count(1) == 2
117
118
119def longest_chain(edges: Sequence[Sequence]) -> Sequence:
120    """
121    Given a list of graph edges, return a list of connected nodes.
122
123    Args:
124        edges (Sequence[Sequence]): List of graph edges.
125
126    Returns:
127        Sequence: List of connected nodes.
128    """
129
130    def add_edge(edge, chain, index, processed):
131        nonlocal no_change
132        if index == 0:
133            chain.insert(0, edge)
134        elif index == -1:
135            chain.append(edge)
136        processed.append(set(edge))
137        no_change = False
138
139    chain = []
140    processed = []
141    while len(chain) < len(edges):
142        no_change = True
143        for edge in edges:
144            if set(edge) in processed:
145                continue
146            if not chain:
147                add_edge(edge, chain, -1, processed)
148            else:
149                if edge[0] == chain[-1][1]:
150                    add_edge(edge, chain, -1, processed)
151                elif edge[0] == chain[0][0]:
152                    add_edge((edge[1], edge[0]), chain, 0, processed)
153                elif edge[1] == chain[0][0]:
154                    add_edge(edge, chain, 0, processed)
155                elif edge[1] == chain[-1][1]:
156                    add_edge((edge[1], edge[0]), chain, -1, processed)
157        if no_change:
158            break
159    return chain
160
161
162def is_cycle(graph: nx.Graph, island: Sequence) -> bool:
163    """
164    Given a NetworkX Graph and an island, return True if the given island is a cycle.
165
166    Args:
167        graph (nx.Graph): The graph.
168        island (Sequence): The island.
169
170    Returns:
171        bool: True if the island is a cycle, False otherwise.
172    """
173    degrees = [graph.degree(node) for node in island]
174    return set(degrees) == {2}
175
176
177def is_open_walk(graph: nx.Graph, island: Sequence) -> bool:
178    """
179    Given a NetworkX Graph and an island, return True if the given island is an open walk.
180
181    Args:
182        graph (nx.Graph): The graph.
183        island (Sequence): The island.
184
185    Returns:
186        bool: True if the island is an open walk, False otherwise.
187    """
188    if len(island) == 2:
189        return True
190    degrees = [graph.degree(node) for node in island]
191    return set(degrees) == {1, 2} and degrees.count(1) == 2
192
193
194def graph_summary(graph: nx.Graph) -> str:
195    """
196    Return a summary of a graph including cycles, open walks and degenerate nodes.
197
198    Args:
199        graph (nx.Graph): The graph.
200
201    Returns:
202        str: Summary of the graph.
203    """
204    lines = []
205    for island in nx.connected_components(graph):
206        if len(island) > 8:
207            island = list(island)
208            lines.append(f"Island: {island[:4]}, ... , {island[-4:]}")
209        else:
210            lines.append(f"Island: {island}")
211        if is_cycle(graph, island):
212            lines.append(f"Cycle: {len(island)} nodes")
213        elif is_open_walk(graph, island):
214            lines.append(f"Open Walk: {len(island)} nodes")
215        else:
216            degenerates = [node for node in island if graph.degree(node) > 2]
217            degrees = f"{[(node, graph.degree(node)) for node in degenerates]}"
218            lines.append(f"Degenerate: {len(island)} nodes")
219            lines.append(f"(Node, Degree): {degrees}")
220        lines.append("-" * 40)
221    return "\n".join(lines)
222
223
224@dataclass
225class Node:
226    """
227    A Node object is a 2D point with x and y coordinates.
228    Used in graphs corresponding to shapes and batches.
229
230    Attributes:
231        x (float): X coordinate.
232        y (float): Y coordinate.
233    """
234
235    x: float
236    y: float
237
238    def __post_init__(self):
239        """Initialize the Node with x and y coordinates."""
240        common_properties(self)
241
242    @property
243    def pos(self):
244        """Return the position of the node."""
245        return (self.x, self.y)
246
247    def __eq__(self, other: object) -> bool:
248        """
249        Check if two nodes are equal.
250
251        Args:
252            other (object): The other node.
253
254        Returns:
255            bool: True if the nodes are equal, False otherwise.
256        """
257        return close_points2(self.pos, other.pos, dist2=defaults["dist_tol"] ** 2)
258
259
260@dataclass
261class Graph:
262    """
263    A Graph object is a collection of nodes and edges.
264
265    Attributes:
266        type (Types): The type of the graph.
267        subtype (Types): The subtype of the graph.
268        nx_graph (nx.Graph): The NetworkX graph object.
269    """
270
271    type: Types = "undirected"
272    subtype: Types = "none"  # this can be Types.WEIGHTED
273    nx_graph: "nx.Graph" = None
274
275    def __post_init__(self):
276        """Initialize the Graph with type and subtype."""
277        common_properties(self)
278
279    @property
280    def islands(self):
281        """
282        Return a list of all islands both cyclic and acyclic.
283
284        Returns:
285            List: List of all islands.
286        """
287        return [
288            list(island) for island in self.nx_graph.connected_components(self.nx_graph)
289        ]
290
291    @property
292    def cycles(self):
293        """
294        Return a list of cycles.
295
296        Returns:
297            List: List of cycles.
298        """
299        return nx.cycle_basis(self.nx_graph)
300
301    @property
302    def open_walks(self):
303        """
304        Return a list of open walks (aka open chains).
305
306        Returns:
307            List: List of open walks.
308        """
309        res = []
310        for island in self.islands:
311            if is_open_walk(self.nx_graph, island):
312                res.append(island)
313        return res
314
315    @property
316    def edges(self):
317        """
318        Return the edges of the graph.
319
320        Returns:
321            EdgeView: Edges of the graph.
322        """
323        return self.nx_graph.edges
324
325    @property
326    def nodes(self):
327        """
328        Return the nodes of the graph.
329
330        Returns:
331            NodeView: Nodes of the graph.
332        """
333        return self.nx_graph.nodes
@dataclass
class GraphEdge:
15@dataclass
16class GraphEdge:
17    """Edge in a graph. It has a start and end point as nodes."""
18
19    start: Point
20    end: Point
21
22    def __post_init__(self):
23        """Initialize the GraphEdge with start and end points."""
24        self.length = distance(self.start.pos, self.end.pos)
25        common_properties(self)
26
27    @property
28    def nodes(self):
29        """Return the start and end nodes of the edge."""
30        return (self.start, self.end)

Edge in a graph. It has a start and end point as nodes.

GraphEdge(start: Sequence[float], end: Sequence[float])
start: Sequence[float]
end: Sequence[float]
nodes
27    @property
28    def nodes(self):
29        """Return the start and end nodes of the edge."""
30        return (self.start, self.end)

Return the start and end nodes of the edge.

def edges2nodes(edges: Sequence[Sequence]) -> Sequence:
33def edges2nodes(edges: Sequence[Sequence]) -> Sequence:
34    """
35    Given a list of edges, return a connected list of nodes.
36
37    Args:
38        edges (Sequence[Sequence]): List of edges.
39
40    Returns:
41        Sequence: Connected list of nodes.
42    """
43    chain = longest_chain(edges)
44    closed = chain[0][0] == chain[-1][-1]
45    if closed:
46        nodes = [x[0] for x in chain[:-1]]
47    else:
48        nodes = [x[0] for x in chain] + [chain[-1][1]]
49    if closed:
50        last_edge = chain[-1]
51        if last_edge[1] == nodes[-1]:
52            nodes.extend(reversed(last_edge))
53        elif last_edge[0] == nodes[-1]:
54            nodes.extend(last_edge)
55        elif last_edge[0] == nodes[0]:
56            nodes.extend(reversed(last_edge))
57        elif last_edge[1] == nodes[0]:
58            nodes.extend(last_edge)
59
60    return nodes

Given a list of edges, return a connected list of nodes.

Arguments:
  • edges (Sequence[Sequence]): List of edges.
Returns:

Sequence: Connected list of nodes.

def get_cycles( edges: Sequence[GraphEdge]) -> Sequence[GraphEdge]:
63def get_cycles(edges: Sequence[GraphEdge]) -> Sequence[GraphEdge]:
64    """
65    Computes all the cycles in a given graph of edges.
66
67    Args:
68        edges (Sequence[GraphEdge]): List of graph edges.
69
70    Returns:
71        Sequence[GraphEdge]: List of cycles if any cycle is found, None otherwise.
72    """
73    nx_graph = nx.Graph()
74    nx_graph.add_edges_from(edges)
75    cycles = nx.cycle_basis(nx_graph)
76    res = None
77    if cycles:
78        for cycle in cycles:
79            cycle.append(cycle[0])
80
81        res = cycles
82    return res

Computes all the cycles in a given graph of edges.

Arguments:
  • edges (Sequence[GraphEdge]): List of graph edges.
Returns:

Sequence[GraphEdge]: List of cycles if any cycle is found, None otherwise.

def find_all_paths(graph, node):
 86def find_all_paths(graph, node):
 87    """
 88    Find all paths starting from a given node.
 89
 90    Args:
 91        graph (nx.Graph): The graph.
 92        node: The starting node.
 93
 94    Returns:
 95        List: All paths starting from the given node.
 96    """
 97    paths = []
 98    for node_ in graph.nodes():
 99        for path in nx.all_simple_paths(graph, node, node_):
100            if len(path) > 1:
101                paths.append(path)
102    return paths

Find all paths starting from a given node.

Arguments:
  • graph (nx.Graph): The graph.
  • node: The starting node.
Returns:

List: All paths starting from the given node.

def is_open_walk2(graph, island):
105def is_open_walk2(graph, island):
106    """
107    Given a NetworkX Graph and an island, return True if the given island is an open walk.
108
109    Args:
110        graph (nx.Graph): The graph.
111        island: The island.
112
113    Returns:
114        bool: True if the island is an open walk, False otherwise.
115    """
116    degrees = [graph.degree(node) for node in island]
117    return set(degrees) == {1, 2} and degrees.count(1) == 2

Given a NetworkX Graph and an island, return True if the given island is an open walk.

Arguments:
  • graph (nx.Graph): The graph.
  • island: The island.
Returns:

bool: True if the island is an open walk, False otherwise.

def longest_chain(edges: Sequence[Sequence]) -> Sequence:
120def longest_chain(edges: Sequence[Sequence]) -> Sequence:
121    """
122    Given a list of graph edges, return a list of connected nodes.
123
124    Args:
125        edges (Sequence[Sequence]): List of graph edges.
126
127    Returns:
128        Sequence: List of connected nodes.
129    """
130
131    def add_edge(edge, chain, index, processed):
132        nonlocal no_change
133        if index == 0:
134            chain.insert(0, edge)
135        elif index == -1:
136            chain.append(edge)
137        processed.append(set(edge))
138        no_change = False
139
140    chain = []
141    processed = []
142    while len(chain) < len(edges):
143        no_change = True
144        for edge in edges:
145            if set(edge) in processed:
146                continue
147            if not chain:
148                add_edge(edge, chain, -1, processed)
149            else:
150                if edge[0] == chain[-1][1]:
151                    add_edge(edge, chain, -1, processed)
152                elif edge[0] == chain[0][0]:
153                    add_edge((edge[1], edge[0]), chain, 0, processed)
154                elif edge[1] == chain[0][0]:
155                    add_edge(edge, chain, 0, processed)
156                elif edge[1] == chain[-1][1]:
157                    add_edge((edge[1], edge[0]), chain, -1, processed)
158        if no_change:
159            break
160    return chain

Given a list of graph edges, return a list of connected nodes.

Arguments:
  • edges (Sequence[Sequence]): List of graph edges.
Returns:

Sequence: List of connected nodes.

def is_cycle(graph: networkx.classes.graph.Graph, island: Sequence) -> bool:
163def is_cycle(graph: nx.Graph, island: Sequence) -> bool:
164    """
165    Given a NetworkX Graph and an island, return True if the given island is a cycle.
166
167    Args:
168        graph (nx.Graph): The graph.
169        island (Sequence): The island.
170
171    Returns:
172        bool: True if the island is a cycle, False otherwise.
173    """
174    degrees = [graph.degree(node) for node in island]
175    return set(degrees) == {2}

Given a NetworkX Graph and an island, return True if the given island is a cycle.

Arguments:
  • graph (nx.Graph): The graph.
  • island (Sequence): The island.
Returns:

bool: True if the island is a cycle, False otherwise.

def is_open_walk(graph: networkx.classes.graph.Graph, island: Sequence) -> bool:
178def is_open_walk(graph: nx.Graph, island: Sequence) -> bool:
179    """
180    Given a NetworkX Graph and an island, return True if the given island is an open walk.
181
182    Args:
183        graph (nx.Graph): The graph.
184        island (Sequence): The island.
185
186    Returns:
187        bool: True if the island is an open walk, False otherwise.
188    """
189    if len(island) == 2:
190        return True
191    degrees = [graph.degree(node) for node in island]
192    return set(degrees) == {1, 2} and degrees.count(1) == 2

Given a NetworkX Graph and an island, return True if the given island is an open walk.

Arguments:
  • graph (nx.Graph): The graph.
  • island (Sequence): The island.
Returns:

bool: True if the island is an open walk, False otherwise.

def graph_summary(graph: networkx.classes.graph.Graph) -> str:
195def graph_summary(graph: nx.Graph) -> str:
196    """
197    Return a summary of a graph including cycles, open walks and degenerate nodes.
198
199    Args:
200        graph (nx.Graph): The graph.
201
202    Returns:
203        str: Summary of the graph.
204    """
205    lines = []
206    for island in nx.connected_components(graph):
207        if len(island) > 8:
208            island = list(island)
209            lines.append(f"Island: {island[:4]}, ... , {island[-4:]}")
210        else:
211            lines.append(f"Island: {island}")
212        if is_cycle(graph, island):
213            lines.append(f"Cycle: {len(island)} nodes")
214        elif is_open_walk(graph, island):
215            lines.append(f"Open Walk: {len(island)} nodes")
216        else:
217            degenerates = [node for node in island if graph.degree(node) > 2]
218            degrees = f"{[(node, graph.degree(node)) for node in degenerates]}"
219            lines.append(f"Degenerate: {len(island)} nodes")
220            lines.append(f"(Node, Degree): {degrees}")
221        lines.append("-" * 40)
222    return "\n".join(lines)

Return a summary of a graph including cycles, open walks and degenerate nodes.

Arguments:
  • graph (nx.Graph): The graph.
Returns:

str: Summary of the graph.

@dataclass
class Node:
225@dataclass
226class Node:
227    """
228    A Node object is a 2D point with x and y coordinates.
229    Used in graphs corresponding to shapes and batches.
230
231    Attributes:
232        x (float): X coordinate.
233        y (float): Y coordinate.
234    """
235
236    x: float
237    y: float
238
239    def __post_init__(self):
240        """Initialize the Node with x and y coordinates."""
241        common_properties(self)
242
243    @property
244    def pos(self):
245        """Return the position of the node."""
246        return (self.x, self.y)
247
248    def __eq__(self, other: object) -> bool:
249        """
250        Check if two nodes are equal.
251
252        Args:
253            other (object): The other node.
254
255        Returns:
256            bool: True if the nodes are equal, False otherwise.
257        """
258        return close_points2(self.pos, other.pos, dist2=defaults["dist_tol"] ** 2)

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.
Node(x: float, y: float)
x: float
y: float
pos
243    @property
244    def pos(self):
245        """Return the position of the node."""
246        return (self.x, self.y)

Return the position of the node.

@dataclass
class Graph:
261@dataclass
262class Graph:
263    """
264    A Graph object is a collection of nodes and edges.
265
266    Attributes:
267        type (Types): The type of the graph.
268        subtype (Types): The subtype of the graph.
269        nx_graph (nx.Graph): The NetworkX graph object.
270    """
271
272    type: Types = "undirected"
273    subtype: Types = "none"  # this can be Types.WEIGHTED
274    nx_graph: "nx.Graph" = None
275
276    def __post_init__(self):
277        """Initialize the Graph with type and subtype."""
278        common_properties(self)
279
280    @property
281    def islands(self):
282        """
283        Return a list of all islands both cyclic and acyclic.
284
285        Returns:
286            List: List of all islands.
287        """
288        return [
289            list(island) for island in self.nx_graph.connected_components(self.nx_graph)
290        ]
291
292    @property
293    def cycles(self):
294        """
295        Return a list of cycles.
296
297        Returns:
298            List: List of cycles.
299        """
300        return nx.cycle_basis(self.nx_graph)
301
302    @property
303    def open_walks(self):
304        """
305        Return a list of open walks (aka open chains).
306
307        Returns:
308            List: List of open walks.
309        """
310        res = []
311        for island in self.islands:
312            if is_open_walk(self.nx_graph, island):
313                res.append(island)
314        return res
315
316    @property
317    def edges(self):
318        """
319        Return the edges of the graph.
320
321        Returns:
322            EdgeView: Edges of the graph.
323        """
324        return self.nx_graph.edges
325
326    @property
327    def nodes(self):
328        """
329        Return the nodes of the graph.
330
331        Returns:
332            NodeView: Nodes of the graph.
333        """
334        return self.nx_graph.nodes

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.
Graph( type: simetri.graphics.all_enums.Types = 'undirected', subtype: simetri.graphics.all_enums.Types = 'none', nx_graph: networkx.classes.graph.Graph = None)
type: simetri.graphics.all_enums.Types = 'undirected'
nx_graph: networkx.classes.graph.Graph = None
islands
280    @property
281    def islands(self):
282        """
283        Return a list of all islands both cyclic and acyclic.
284
285        Returns:
286            List: List of all islands.
287        """
288        return [
289            list(island) for island in self.nx_graph.connected_components(self.nx_graph)
290        ]

Return a list of all islands both cyclic and acyclic.

Returns:

List: List of all islands.

cycles
292    @property
293    def cycles(self):
294        """
295        Return a list of cycles.
296
297        Returns:
298            List: List of cycles.
299        """
300        return nx.cycle_basis(self.nx_graph)

Return a list of cycles.

Returns:

List: List of cycles.

open_walks
302    @property
303    def open_walks(self):
304        """
305        Return a list of open walks (aka open chains).
306
307        Returns:
308            List: List of open walks.
309        """
310        res = []
311        for island in self.islands:
312            if is_open_walk(self.nx_graph, island):
313                res.append(island)
314        return res

Return a list of open walks (aka open chains).

Returns:

List: List of open walks.

edges
316    @property
317    def edges(self):
318        """
319        Return the edges of the graph.
320
321        Returns:
322            EdgeView: Edges of the graph.
323        """
324        return self.nx_graph.edges

Return the edges of the graph.

Returns:

EdgeView: Edges of the graph.

nodes
326    @property
327    def nodes(self):
328        """
329        Return the nodes of the graph.
330
331        Returns:
332            NodeView: Nodes of the graph.
333        """
334        return self.nx_graph.nodes

Return the nodes of the graph.

Returns:

NodeView: Nodes of the graph.