Skip to content

Commit 8da1f27

Browse files
committed
Add documentation's examples into automated tests
1 parent ea262f7 commit 8da1f27

File tree

1 file changed

+239
-0
lines changed

1 file changed

+239
-0
lines changed

tests/test_documentation_examples.py

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
"""
2+
This set of tests checks that the examples from the documentation still work correctly.
3+
4+
Sometimes this is simply checking that the code produces the intended output or runs without errors.
5+
"""
6+
from modularitypruning import prune_to_stable_partitions, prune_to_multilayer_stable_partitions
7+
from modularitypruning.champ_utilities import CHAMP_2D, CHAMP_3D
8+
from modularitypruning.leiden_utilities import (repeated_parallel_leiden_from_gammas,
9+
repeated_parallel_leiden_from_gammas_omegas)
10+
from modularitypruning.parameter_estimation_utilities import domains_to_gamma_omega_estimates, ranges_to_gamma_estimates
11+
from modularitypruning.partition_utilities import num_communities
12+
from modularitypruning.plotting import (plot_2d_domains_with_estimates, plot_2d_domains, plot_2d_domains_with_ami,
13+
plot_2d_domains_with_num_communities, plot_estimates)
14+
from random import seed, random
15+
import igraph as ig
16+
import matplotlib.pyplot as plt
17+
import numpy as np
18+
import unittest
19+
20+
21+
class TestDocumentationExamples(unittest.TestCase):
22+
def test_basic_singlelayer_example(self):
23+
"""
24+
Taken verbatim from basic_example.rst.
25+
26+
Like a lot of our other tests, this is stochastic but appears incredibly stable.
27+
"""
28+
# get Karate Club graph in igraph
29+
G = ig.Graph.Famous("Zachary")
30+
31+
# run leiden 1000 times on this graph from gamma=0 to gamma=2
32+
partitions = repeated_parallel_leiden_from_gammas(G, np.linspace(0, 2, 1000))
33+
34+
# prune to the stable partitions from gamma=0 to gamma=2
35+
stable_partitions = prune_to_stable_partitions(G, partitions, 0, 2)
36+
37+
intended_stable_partition = [(0, 0, 0, 0, 1, 1, 1, 0, 2, 2, 1, 0, 0, 0, 2, 2, 1,
38+
0, 2, 0, 2, 0, 2, 3, 3, 3, 2, 3, 3, 2, 2, 3, 2, 2)]
39+
self.assertEqual(stable_partitions, intended_stable_partition)
40+
41+
@staticmethod
42+
def generate_basic_multilayer_network():
43+
"""This is taken verbatim from basic_multilayer_example.rst."""
44+
num_layers = 3
45+
n_per_layer = 30
46+
p_in = 0.5
47+
p_out = 0.05
48+
K = 3
49+
50+
# layer_vec holds the layer membership of each node
51+
# e.g. layer_vec[5] = 2 means that node 5 resides in layer 2 (the third layer)
52+
layer_vec = [i // n_per_layer for i in range(n_per_layer * num_layers)]
53+
interlayer_edges = [(n_per_layer * layer + v, n_per_layer * layer + v + n_per_layer)
54+
for layer in range(num_layers - 1)
55+
for v in range(n_per_layer)]
56+
57+
# set up a community vector with
58+
# three communities in layer 0 (each of size 10)
59+
# three communities in layer 1 (each of size 10)
60+
# one community in layer 2 (of size 30)
61+
comm_per_layer = [[i // (n_per_layer // K) if layer < num_layers - 1 else 0
62+
for i in range(n_per_layer)] for layer in range(num_layers)]
63+
comm_vec = [item for sublist in comm_per_layer for item in sublist]
64+
65+
# randomly connect nodes inside each layer with undirected edges according to
66+
# within-community probability p_in and between-community probability p_out
67+
intralayer_edges = [(u, v) for v in range(len(comm_vec)) for u in range(v + 1, len(comm_vec))
68+
if layer_vec[v] == layer_vec[u] and (
69+
(comm_vec[v] == comm_vec[u] and random() < p_in) or
70+
(comm_vec[v] != comm_vec[u] and random() < p_out)
71+
)]
72+
73+
# create the networks in igraph. By Pamfil et al.'s convention, the interlayer edges
74+
# of a temporal network are directed (representing the "one-way" nature of time)
75+
G_intralayer = ig.Graph(intralayer_edges)
76+
G_interlayer = ig.Graph(interlayer_edges, directed=True)
77+
78+
return G_intralayer, G_interlayer, layer_vec
79+
80+
def test_basic_multilayer_example(self):
81+
"""
82+
This is taken verbatim from basic_multilayer_example.rst.
83+
84+
For simplicity and re-use, the network generation is encapsulated in generate_basic_multilayer_network().
85+
"""
86+
n_per_layer = 30 # from network generation code
87+
G_intralayer, G_interlayer, layer_vec = self.generate_basic_multilayer_network()
88+
89+
# run leidenalg on a uniform 32x32 grid (1024 samples) of gamma and omega in [0, 2]
90+
gamma_range = (0, 2)
91+
omega_range = (0, 2)
92+
leiden_gammas = np.linspace(*gamma_range, 32)
93+
leiden_omegas = np.linspace(*omega_range, 32)
94+
95+
parts = repeated_parallel_leiden_from_gammas_omegas(G_intralayer, G_interlayer, layer_vec,
96+
gammas=leiden_gammas, omegas=leiden_omegas)
97+
98+
# prune to the stable partitions from (gamma=0, omega=0) to (gamma=2, omega=2)
99+
stable_parts = prune_to_multilayer_stable_partitions(G_intralayer, G_interlayer, layer_vec,
100+
"temporal", parts,
101+
*gamma_range, *omega_range)
102+
103+
# check all 3-partition stable partitions closely match ground truth communities
104+
for membership in stable_parts:
105+
if num_communities(membership) != 3:
106+
continue
107+
108+
most_common_label = []
109+
for chunk_idx in range(6): # check most common label of each community (10 nodes each)
110+
counts = {i: 0 for i in range(max(membership) + 1)}
111+
for chunk_label in membership[10 * chunk_idx:10 * (chunk_idx + 1)]:
112+
counts[chunk_label] += 1
113+
most_common_label.append(max(counts.items(), key=lambda x: x[1])[0])
114+
115+
# check these communities look like the intended ground truth communities for the first layer
116+
# [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
117+
self.assertNotEqual(most_common_label[0], most_common_label[1])
118+
self.assertNotEqual(most_common_label[1], most_common_label[2])
119+
120+
# at least one partition has the last layer mostly in one community and another splits it into multiple
121+
unified_final_layer_count = 0
122+
split_final_layer_count = 0
123+
for membership in stable_parts:
124+
count_final_layer = {i: 0 for i in range(max(membership) + 1)}
125+
for label in membership[-n_per_layer:]:
126+
count_final_layer[label] += 1
127+
most_common_label_final_layer, most_common_label_count = max(count_final_layer.items(),
128+
key=lambda x: x[1])
129+
proportion_final_layer_having_same_label = most_common_label_count / n_per_layer
130+
131+
if proportion_final_layer_having_same_label > 0.9:
132+
unified_final_layer_count += 1
133+
elif proportion_final_layer_having_same_label < 0.5:
134+
split_final_layer_count += 1
135+
136+
self.assertGreater(unified_final_layer_count, 0)
137+
self.assertGreater(split_final_layer_count, 0)
138+
139+
def test_plot_estimates_example(self):
140+
"""
141+
This is taken (almost) verbatim from plotting_examples.rst.
142+
143+
The first call to plt.rc() has usetex=False (instead of True) to avoid requiring a full LaTeX installation.
144+
"""
145+
# get Karate Club graph in igraph
146+
G = ig.Graph.Famous("Zachary")
147+
148+
# run leiden 100K times on this graph from gamma=0 to gamma=2 (takes ~2-3 seconds)
149+
partitions = repeated_parallel_leiden_from_gammas(G, np.linspace(0, 2, 10 ** 5))
150+
151+
# run CHAMP to obtain the dominant partitions along with their regions of optimality
152+
ranges = CHAMP_2D(G, partitions, gamma_0=0.0, gamma_f=2.0)
153+
154+
# append gamma estimate for each dominant partition onto the CHAMP domains
155+
gamma_estimates = ranges_to_gamma_estimates(G, ranges)
156+
157+
# plot gamma estimates and domains of optimality
158+
plt.rc('text', usetex=False)
159+
plt.rc('font', family='serif')
160+
plot_estimates(gamma_estimates)
161+
plt.title(r"Karate Club CHAMP Domains of Optimality and $\gamma$ Estimates", fontsize=14)
162+
plt.xlabel(r"$\gamma$", fontsize=14)
163+
plt.ylabel("Number of communities", fontsize=14)
164+
165+
def test_plot_2d_domains_examples(self):
166+
"""
167+
This is taken (almost) verbatim from plotting_examples.rst.
168+
169+
The first call to plt.rc() has usetex=False (instead of True) to avoid requiring a full LaTeX installation.
170+
171+
The documentation explicitly shows plot_2d_domains_with_estimates() and describes other, similar functions
172+
* plot_2d_domains()
173+
* plot_2d_domains_with_ami()
174+
* plot_2d_domains_with_num_communities()
175+
As such, we test them all here.
176+
"""
177+
G_intralayer, G_interlayer, layer_vec = self.generate_basic_multilayer_network()
178+
# run leiden on a uniform grid (10K samples) of gamma and omega (takes ~3 seconds)
179+
gamma_range = (0.5, 1.5)
180+
omega_range = (0, 2)
181+
parts = repeated_parallel_leiden_from_gammas_omegas(G_intralayer, G_interlayer, layer_vec,
182+
gammas=np.linspace(*gamma_range, 100),
183+
omegas=np.linspace(*omega_range, 100))
184+
185+
# run CHAMP to obtain the dominant partitions along with their regions of optimality
186+
domains = CHAMP_3D(G_intralayer, G_interlayer, layer_vec, parts,
187+
gamma_0=gamma_range[0], gamma_f=gamma_range[1],
188+
omega_0=omega_range[0], omega_f=omega_range[1])
189+
190+
# append resolution parameter estimates for each dominant partition onto the CHAMP domains
191+
domains_with_estimates = domains_to_gamma_omega_estimates(G_intralayer, G_interlayer, layer_vec,
192+
domains, model='temporal')
193+
194+
# plot resolution parameter estimates and domains of optimality
195+
plt.rc('text', usetex=False)
196+
plt.rc('font', family='serif')
197+
plot_2d_domains_with_estimates(domains_with_estimates, xlim=omega_range, ylim=gamma_range)
198+
plt.title(r"CHAMP Domains and ($\omega$, $\gamma$) Estimates", fontsize=16)
199+
plt.xlabel(r"$\omega$", fontsize=20)
200+
plt.ylabel(r"$\gamma$", fontsize=20)
201+
plt.gca().tick_params(axis='both', labelsize=12)
202+
plt.tight_layout()
203+
204+
# same plotting code, but with plot_2d_domains()
205+
plt.rc('text', usetex=False)
206+
plt.rc('font', family='serif')
207+
plot_2d_domains(domains, xlim=omega_range, ylim=gamma_range)
208+
plt.title(r"CHAMP Domains", fontsize=16)
209+
plt.xlabel(r"$\omega$", fontsize=20)
210+
plt.ylabel(r"$\gamma$", fontsize=20)
211+
plt.gca().tick_params(axis='both', labelsize=12)
212+
plt.tight_layout()
213+
214+
# same plotting code, but with plot_2d_domains_with_ami()
215+
plt.rc('text', usetex=False)
216+
plt.rc('font', family='serif')
217+
ground_truth_partition = ([0] * 10 + [1] * 10 + [2] * 10) * 2 + [0] * 30
218+
plot_2d_domains_with_ami(domains_with_estimates, ground_truth=ground_truth_partition,
219+
xlim=omega_range, ylim=gamma_range)
220+
plt.title(r"CHAMP Domains, Colored by AMI with Ground Truth", fontsize=16)
221+
plt.xlabel(r"$\omega$", fontsize=20)
222+
plt.ylabel(r"$\gamma$", fontsize=20)
223+
plt.gca().tick_params(axis='both', labelsize=12)
224+
plt.tight_layout()
225+
226+
# same plotting code, but with plot_2d_domains_with_num_communities()
227+
plt.rc('text', usetex=False)
228+
plt.rc('font', family='serif')
229+
plot_2d_domains_with_num_communities(domains_with_estimates, xlim=omega_range, ylim=gamma_range)
230+
plt.title(r"CHAMP Domains, Colored by Number of Communities", fontsize=16)
231+
plt.xlabel(r"$\omega$", fontsize=20)
232+
plt.ylabel(r"$\gamma$", fontsize=20)
233+
plt.gca().tick_params(axis='both', labelsize=12)
234+
plt.tight_layout()
235+
236+
237+
if __name__ == "__main__":
238+
seed(0)
239+
unittest.main()

0 commit comments

Comments
 (0)