Skip to content

Commit e5313ae

Browse files
committed
Docs for fomin.py
1 parent 704e304 commit e5313ae

1 file changed

Lines changed: 171 additions & 6 deletions

File tree

fomin.py

Lines changed: 171 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,33 @@
33

44

55
def sort_nodes(node):
6+
"""
7+
Function to sort nodes for consistent ordering.
8+
9+
Args:
10+
node: A node in the graph, which can be of any hashable type.
11+
12+
Returns:
13+
tuple: A tuple where the first element indicates if the node is a tuple,
14+
and the second element is the string representation of the node.
15+
"""
616
is_tuple = isinstance(node, tuple)
7-
safe_val = str(node)
8-
return (is_tuple, safe_val)
17+
string_rep = str(node)
18+
# False comes before True when sorting so non-tuple nodes are prioritized
19+
# If two nodes are of the same type, sort by their string representation
20+
return (is_tuple, string_rep)
921

1022

1123
def get_non_trivial_components(G):
24+
"""
25+
Retrieves the connected components of G that contain more than one node.
26+
27+
Args:
28+
G (networkx.Graph): A networkx graph.
29+
30+
Returns:
31+
list: A list of sets reprenting the non-trivial components of G.
32+
"""
1233
components = []
1334
for c in nx.connected_components(G):
1435
if len(c) > 1:
@@ -18,6 +39,18 @@ def get_non_trivial_components(G):
1839

1940

2041
def construct_H(G, F, nb):
42+
"""
43+
Constructs a subgraph H from the neighbors of a given set of nodes and adding edges
44+
between nodes that share a common neighbor in F.
45+
46+
Args:
47+
G (networkx.Graph): A networkx graph.
48+
F (set): A set of nodes in G.
49+
nb (set): A set of neighbors in G.
50+
51+
Returns:
52+
networkx.Graph: The constructed subgraph H.
53+
"""
2154
H = nx.subgraph(G, nb).copy()
2255
for u, v in itertools.combinations(nb, 2):
2356
if not set(nx.common_neighbors(G, u, v)).isdisjoint(F):
@@ -27,6 +60,17 @@ def construct_H(G, F, nb):
2760

2861

2962
def is_foldable(G, v):
63+
"""
64+
Checks if a node v in the graph is foldable. A node is foldable if none of its neighbors
65+
form an anti-triangle. An anti-triangle is a set of three nodes with no edges between them.
66+
67+
Args:
68+
G (networkx.Graph): A networkx graph.
69+
v: A node in G.
70+
71+
Returns:
72+
bool: True if the node is foldable, False otherwise.
73+
"""
3074
nb = sorted(nx.neighbors(G, v), key=sort_nodes)
3175
for x, y, z in itertools.combinations(nb, 3):
3276
if not (G.has_edge(x, y) or G.has_edge(y, z) or G.has_edge(z, x)):
@@ -37,6 +81,17 @@ def is_foldable(G, v):
3781

3882

3983
def get_anti_edges(G, v):
84+
"""
85+
Finds all anti-edges among the neighbors of a node v. An anti-edge is a pair of
86+
nodes that are not connected by an edge.
87+
88+
Args:
89+
G (networkx.Graph): A networkx graph.
90+
v: A node in G.
91+
92+
Returns:
93+
set: A set of tuples representing anti-edges.
94+
"""
4095
nb = nx.neighbors(G, v)
4196
anti_edges = set()
4297
for x, y in itertools.combinations(nb, 2):
@@ -47,25 +102,50 @@ def get_anti_edges(G, v):
47102

48103

49104
def fold_graph(G, v, anti_edges):
105+
"""
106+
Folds the graph by contracting anti-edges of a node v and removing its neighbors.
107+
108+
Args:
109+
G (networkx.Graph): A networkx graph.
110+
v: A node in G.
111+
anti_edges: A set of anti-edges to be contracted.
112+
113+
Returns:
114+
networkx.Graph: The folded graph.
115+
"""
50116
nb_v = set(nx.neighbors(G, v))
51117
folded_G = G.copy()
52118
added_nodes = set()
53119

54120
for i, j in anti_edges:
121+
# Create a new node representing the contraction of i and j
55122
n = (i, j)
56123
folded_G.add_node(n)
57124
added_nodes.add(n)
58125
for u in set(nx.neighbors(G, i)) | set(nx.neighbors(G, j)):
126+
# Add edges between the new node and the neighbors of i and j except v and those in nb_v
127+
# (these nodes will be removed later)
59128
if u != v and u not in nb_v:
60129
folded_G.add_edge(n, u)
61130

62131
for a, b in itertools.combinations(added_nodes, 2):
132+
# Add edges between each newly created node
63133
folded_G.add_edge(a, b)
64134

65135
return nx.subgraph(folded_G, set(folded_G.nodes) - nb_v - {v})
66136

67137

68138
def get_2_hop_neighbors(G, v):
139+
"""
140+
Finds all nodes that are two hops away from a given node v.
141+
142+
Args:
143+
G (networkx.Graph): A networkx graph.
144+
v: A node in G.
145+
146+
Returns:
147+
set: A set of nodes that are two hops away from v.
148+
"""
69149
first_neighbors = set(G.neighbors(v))
70150
second_neighbors = set()
71151
for u in first_neighbors:
@@ -77,10 +157,31 @@ def get_2_hop_neighbors(G, v):
77157

78158

79159
def is_complete(G):
160+
"""
161+
Checks if a graph is complete. A graph is complete if every pair of nodes is connected.
162+
163+
Args:
164+
G (networkx.Graph): A networkx graph.
165+
166+
Returns:
167+
bool: True if the graph is complete, False otherwise.
168+
"""
80169
return len(G.edges) == len(G.nodes) * (len(G.nodes) - 1) / 2
81170

82171

83172
def get_mirrors(G, v):
173+
"""
174+
Finds all mirror nodes of a given node v. A node u is a mirror of v if their
175+
neighborhoods are identical or if the subgraph induced by the difference of their
176+
neighborhoods is complete.
177+
178+
Args:
179+
G (networkx.Graph): A networkx graph.
180+
v: A node in G.
181+
182+
Returns:
183+
set: A set of mirror nodes of v.
184+
"""
84185
mirrors = set()
85186

86187
for u in get_2_hop_neighbors(G, v):
@@ -93,6 +194,15 @@ def get_mirrors(G, v):
93194

94195

95196
def get_max_indep_set(G):
197+
"""
198+
Computes the size of the maximum independent set of a graph.
199+
200+
Args:
201+
G (networkx.Graph): A networkx graph.
202+
203+
Returns:
204+
int: The size of the maximum independent set.
205+
"""
96206
if len(G.nodes) == 0:
97207
return 0
98208

@@ -128,6 +238,19 @@ def get_max_indep_set(G):
128238

129239

130240
def get_generalized_neighbors(G, F, active_v, v):
241+
"""
242+
Finds the generalized neighbors of a node v considering a set of nodes F
243+
and an active node.
244+
245+
Args:
246+
G (networkx.Graph): A networkx graph.
247+
F: A set of fixed nodes in G.
248+
active_v: The active node in G.
249+
v: The node for which generalized neighbors are computed.
250+
251+
Returns:
252+
set: A set of generalized neighbors of v.
253+
"""
131254
K = {u for u in (F - {active_v}) if G.has_edge(u, v)}
132255
new_G = G.copy()
133256

@@ -139,7 +262,19 @@ def get_generalized_neighbors(G, F, active_v, v):
139262

140263

141264
def get_mif_len(G, F, active_v):
265+
"""
266+
Computes the length of a maximum induced forest (MIF) of a graph. This MIF must include all nodes in F.
267+
268+
Args:
269+
G (networkx.Graph): A networkx graph.
270+
F: A set of fixed nodes in G.
271+
active_v: The active node in G.
272+
273+
Returns:
274+
int: The length of a MIF of G.
275+
"""
142276
if nx.number_connected_components(G) > 1:
277+
# If G is disconnected, compute MIF length for each component separately
143278
res = 0
144279
for c in nx.connected_components(G):
145280
if nx.is_forest(nx.subgraph(G, c)):
@@ -152,7 +287,7 @@ def get_mif_len(G, F, active_v):
152287
return res
153288

154289
sg_F = nx.subgraph(G, F)
155-
# Verify is F is acyclic
290+
# Verify if F is acyclic
156291
if len(sg_F.nodes) > 0 and not nx.is_forest(sg_F):
157292
return 0
158293

@@ -176,6 +311,7 @@ def get_mif_len(G, F, active_v):
176311

177312
v_T = list(sorted(T, key=sort_nodes))[0]
178313

314+
# Contract all nodes in T into v_T
179315
for n in T - {v_T}:
180316
new_G = nx.contracted_nodes(new_G, v_T, n, self_loops=False)
181317

@@ -194,7 +330,18 @@ def get_mif_len(G, F, active_v):
194330

195331

196332
def main_procedure(G, F, active_v):
197-
333+
"""
334+
Main procedure for computing the length of a maximum induced forest (MIF) of G.
335+
This MIF must include all nodes in F.
336+
337+
Args:
338+
G (networkx.Graph): A networkx graph.
339+
F: A set of fixed nodes in G.
340+
active_v: The active node in G.
341+
342+
Returns:
343+
int: The length of a MIF in G.
344+
"""
198345
if F == set(G.nodes):
199346
return len(G.nodes)
200347

@@ -204,6 +351,8 @@ def main_procedure(G, F, active_v):
204351
if max_degree < 2:
205352
return len(G.nodes)
206353
else:
354+
# Choose a node t with degree at least 2
355+
# t is either contained in a MIF or not, thus we explore both subproblems
207356
t = list(sorted([n for n, d in G.degree() if d >= 2], key=sort_nodes))[0]
208357
new_G = nx.subgraph(G, G.nodes - {t})
209358
return max(
@@ -215,16 +364,19 @@ def main_procedure(G, F, active_v):
215364

216365
nb = set(nx.neighbors(G, active_v))
217366
if set(G.nodes) - F == nb:
367+
# Get a maximum independent set of the constructed graph H
218368
return len(F) + get_max_indep_set(construct_H(G, F - {active_v}, nb))
219369

220370
for v in sorted(nb, key=sort_nodes):
221371
gen_nb = get_generalized_neighbors(G, F, active_v, v)
222372
if len(gen_nb) < 2:
373+
# Include v in the MIF
223374
return get_mif_len(G, F | {v}, active_v)
224375

225376
for v in sorted(nb, key=sort_nodes):
226377
gen_nb = get_generalized_neighbors(G, F, active_v, v)
227378
if len(gen_nb) > 3:
379+
# Explore both subproblems of including and excluding v from the MIF
228380
return max(
229381
get_mif_len(G, F | {v}, active_v),
230382
get_mif_len(nx.subgraph(G, G.nodes - {v}), F, active_v),
@@ -233,6 +385,7 @@ def main_procedure(G, F, active_v):
233385
for v in sorted(nb, key=sort_nodes):
234386
gen_nb = get_generalized_neighbors(G, F, active_v, v)
235387
if len(gen_nb) == 2:
388+
# Either include v in the MIF or exclude v and include its two generalized neighbors in the MIF
236389
return max(
237390
get_mif_len(G, F | {v}, active_v),
238391
get_mif_len(
@@ -242,7 +395,8 @@ def main_procedure(G, F, active_v):
242395
),
243396
)
244397

245-
# If every v in nb has exactly 3 generalized neighbors, find a v that has at least one generalized neighbor outside nb
398+
# If every v in nb has exactly 3 generalized neighbors, find a v that has at least
399+
# one generalized neighbor outside nb
246400
v = None
247401
good_gen_nb = None
248402
for n in nb:
@@ -265,6 +419,8 @@ def main_procedure(G, F, active_v):
265419
else:
266420
print("Problem")
267421

422+
# Either include v in the MIF, or exclude v and include w1 (which is not in the neighborhood of
423+
# the active vertex), or exclude v and w1 and include w2 and w3 in the MIF
268424
return max(
269425
get_mif_len(G, F | {v}, active_v),
270426
get_mif_len(
@@ -280,12 +436,21 @@ def main_procedure(G, F, active_v):
280436
)
281437

282438
else:
439+
# Should not happen but for safety
283440
print("Problem")
284441
return Exception("No suitable v found")
285442

286443

287444
def get_decycling_number_fomin(G):
445+
"""
446+
Computes the decycling number of a graph using Fomin's algorithm.
447+
448+
Args:
449+
G (networkx.Graph): A networkx graph.
450+
451+
Returns:
452+
int: The decycling number of the graph.
453+
"""
288454
if nx.is_forest(G):
289455
return 0
290-
291456
return len(G.nodes) - get_mif_len(G, set(), None)

0 commit comments

Comments
 (0)