|
| 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