Skip to content

Commit aa0374a

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

File tree

1 file changed

+265
-0
lines changed

1 file changed

+265
-0
lines changed

tests/test_documentation_examples.py

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
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, plot_multiplex_community)
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+
plt.close() # closing all these figures instead of showing
236+
237+
def test_plot_multiplex_community(self):
238+
"""
239+
This is taken (almost) verbatim from plotting_examples.rst.
240+
241+
The first call to plt.rc() has usetex=False (instead of True) to avoid requiring a full LaTeX installation.
242+
"""
243+
num_layers = 3
244+
layer_vec = [i // 71 for i in range(num_layers * 71)]
245+
membership = [1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1,
246+
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
247+
2, 2, 2, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
248+
0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,
249+
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1,
250+
1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 2, 2, 2,
251+
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
252+
253+
plt.rc('text', usetex=False)
254+
plt.rc('font', family='serif')
255+
ax = plot_multiplex_community(np.array(membership), np.array(layer_vec))
256+
ax.set_xticks(np.linspace(0, num_layers, 2 * num_layers + 1))
257+
ax.set_xticklabels(["", "Advice", "", "Coworker", "", "Friend", ""], fontsize=14)
258+
plt.title(f"Multiplex Communities", fontsize=14)
259+
plt.ylabel("Node ID", fontsize=14)
260+
plt.close() # closing this these figures instead of showing
261+
262+
263+
if __name__ == "__main__":
264+
seed(0)
265+
unittest.main()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy