modularitypruning 1.5.1__tar.gz → 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/PKG-INFO +21 -10
  2. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/README.md +18 -7
  3. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/modularitypruning.egg-info/PKG-INFO +21 -10
  4. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/modularitypruning.egg-info/SOURCES.txt +2 -1
  5. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/pyproject.toml +7 -4
  6. modularitypruning-2.0.0/tests/test_champ_coefficients_2D.py +122 -0
  7. modularitypruning-2.0.0/tests/test_champ_coefficients_3D.py +177 -0
  8. modularitypruning-2.0.0/tests/test_champ_usage_2D.py +125 -0
  9. modularitypruning-2.0.0/tests/test_champ_usage_3D.py +184 -0
  10. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/tests/test_documentation_examples.py +1 -1
  11. modularitypruning-2.0.0/tests/test_monolayer_parameter_estimation.py +106 -0
  12. modularitypruning-2.0.0/tests/test_multiplex_parameter_estimation.py +161 -0
  13. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/tests/test_parallel_leiden_performance.py +10 -11
  14. modularitypruning-2.0.0/tests/test_temporal_multilevel_parameter_estimation.py +220 -0
  15. modularitypruning-2.0.0/utilities/__init__.py +7 -0
  16. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/utilities/champ_utilities.py +136 -97
  17. modularitypruning-2.0.0/utilities/leiden_utilities.py +338 -0
  18. modularitypruning-2.0.0/utilities/louvain_utilities.py +25 -0
  19. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/utilities/parameter_estimation.py +92 -66
  20. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/utilities/parameter_estimation_utilities.py +286 -126
  21. modularitypruning-2.0.0/utilities/partition_utilities.py +61 -0
  22. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/utilities/plotting.py +78 -48
  23. modularitypruning-2.0.0/utilities/py.typed +0 -0
  24. modularitypruning-2.0.0/utilities/types.py +62 -0
  25. modularitypruning-1.5.1/tests/test_champ_coefficients_2D.py +0 -89
  26. modularitypruning-1.5.1/tests/test_champ_coefficients_3D.py +0 -126
  27. modularitypruning-1.5.1/tests/test_champ_usage_2D.py +0 -100
  28. modularitypruning-1.5.1/tests/test_champ_usage_3D.py +0 -147
  29. modularitypruning-1.5.1/tests/test_monolayer_parameter_estimation.py +0 -56
  30. modularitypruning-1.5.1/tests/test_multiplex_parameter_estimation.py +0 -112
  31. modularitypruning-1.5.1/tests/test_temporal_multilevel_parameter_estimation.py +0 -163
  32. modularitypruning-1.5.1/utilities/__init__.py +0 -2
  33. modularitypruning-1.5.1/utilities/leiden_utilities.py +0 -255
  34. modularitypruning-1.5.1/utilities/louvain_utilities.py +0 -185
  35. modularitypruning-1.5.1/utilities/partition_utilities.py +0 -43
  36. modularitypruning-1.5.1/utilities/progress.py +0 -31
  37. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/LICENSE +0 -0
  38. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/modularitypruning.egg-info/dependency_links.txt +0 -0
  39. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/modularitypruning.egg-info/requires.txt +0 -0
  40. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/modularitypruning.egg-info/top_level.txt +0 -0
  41. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/setup.cfg +0 -0
  42. {modularitypruning-1.5.1 → modularitypruning-2.0.0}/tests/test_deprecated_louvain_names.py +0 -0
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modularitypruning
3
- Version: 1.5.1
3
+ Version: 2.0.0
4
4
  Summary: Pruning tool to identify small subsets of network partitions that are significant from the perspective of stochastic block model inference.
5
5
  Author-email: Ryan Gibson <ryan.alex.gibson@gmail.com>
6
6
  License-Expression: MIT
7
7
  Classifier: Development Status :: 5 - Production/Stable
8
8
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
9
9
  Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.9
11
10
  Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
@@ -15,7 +14,8 @@ Classifier: Programming Language :: Python :: 3.13
15
14
  Classifier: Programming Language :: Python :: 3.14
16
15
  Classifier: Programming Language :: Python :: 3.15
17
16
  Classifier: Programming Language :: Python :: 3 :: Only
18
- Requires-Python: >=3.9
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: leidenalg
@@ -35,17 +35,23 @@ Dynamic: license-file
35
35
 
36
36
  ModularityPruning is a pruning tool to identify small subsets of network
37
37
  partitions that are significant from the perspective of stochastic block model
38
- inference. This method works for single-layer and multi-layer networks, as well
39
- as for restricting focus to a fixed number of communities when desired.
38
+ inference. It:
39
+
40
+ - Works for single-layer and multi-layer networks
41
+ - Supports both directed and undirected networks
42
+ - Supports weighted networks via the `weight` edge attribute
43
+ - Restricts focus to a fixed number of communities when desired
44
+ - Contains a faster and more robust implementation of the CHAMP (Convex Hull of
45
+ Admissible Modularity Partitions) algorithm (see `CHAMP_2D` and `CHAMP_3D`)
40
46
 
41
47
  See the [documentation](https://modularitypruning.readthedocs.io/en/latest/)
42
48
  or the journal article at https://doi.org/10.1038/s41598-022-20142-6 for more
43
49
  information.
44
50
 
45
51
  Significantly more details can be found in the article's
46
- [Supplementary Information](
47
- https://static-content.springer.com/esm/art%3A10.1038%2Fs41598-022-20142-6/MediaObjects/41598_2022_20142_MOESM1_ESM.pdf
48
- ).
52
+ [Supplementary Information](https://static-content.springer.com/esm/art%3A10.1038%2Fs41598-022-20142-6/MediaObjects/41598_2022_20142_MOESM1_ESM.pdf).
53
+
54
+ Note: support for weighted and directed networks is an extension beyond the original publication.
49
55
 
50
56
  ## Installation
51
57
 
@@ -60,7 +66,12 @@ Alternatively, you can install it from this repository directly:
60
66
 
61
67
  git clone https://github.com/ragibson/ModularityPruning
62
68
  cd ModularityPruning
63
- python3 setup.py install
69
+ pip install .
70
+
71
+ For modularitypruning development, it is best to use a strict editable install
72
+ so that `pytest` and `mypy` can pick up changes immediately.
73
+
74
+ pip install --editable . --config-settings editable_mode=strict
64
75
 
65
76
  <a name = "Basic Usage"></a>
66
77
 
@@ -84,7 +95,7 @@ partitions = repeated_leiden_from_gammas(G, np.linspace(0, 2, 1000))
84
95
  # prune to the stable partitions from gamma=0 to gamma=2
85
96
  stable_partitions = prune_to_stable_partitions(G, partitions, 0, 2)
86
97
  print(stable_partitions)
87
- ```
98
+ ```
88
99
 
89
100
  This prints
90
101
 
@@ -4,17 +4,23 @@
4
4
 
5
5
  ModularityPruning is a pruning tool to identify small subsets of network
6
6
  partitions that are significant from the perspective of stochastic block model
7
- inference. This method works for single-layer and multi-layer networks, as well
8
- as for restricting focus to a fixed number of communities when desired.
7
+ inference. It:
8
+
9
+ - Works for single-layer and multi-layer networks
10
+ - Supports both directed and undirected networks
11
+ - Supports weighted networks via the `weight` edge attribute
12
+ - Restricts focus to a fixed number of communities when desired
13
+ - Contains a faster and more robust implementation of the CHAMP (Convex Hull of
14
+ Admissible Modularity Partitions) algorithm (see `CHAMP_2D` and `CHAMP_3D`)
9
15
 
10
16
  See the [documentation](https://modularitypruning.readthedocs.io/en/latest/)
11
17
  or the journal article at https://doi.org/10.1038/s41598-022-20142-6 for more
12
18
  information.
13
19
 
14
20
  Significantly more details can be found in the article's
15
- [Supplementary Information](
16
- https://static-content.springer.com/esm/art%3A10.1038%2Fs41598-022-20142-6/MediaObjects/41598_2022_20142_MOESM1_ESM.pdf
17
- ).
21
+ [Supplementary Information](https://static-content.springer.com/esm/art%3A10.1038%2Fs41598-022-20142-6/MediaObjects/41598_2022_20142_MOESM1_ESM.pdf).
22
+
23
+ Note: support for weighted and directed networks is an extension beyond the original publication.
18
24
 
19
25
  ## Installation
20
26
 
@@ -29,7 +35,12 @@ Alternatively, you can install it from this repository directly:
29
35
 
30
36
  git clone https://github.com/ragibson/ModularityPruning
31
37
  cd ModularityPruning
32
- python3 setup.py install
38
+ pip install .
39
+
40
+ For modularitypruning development, it is best to use a strict editable install
41
+ so that `pytest` and `mypy` can pick up changes immediately.
42
+
43
+ pip install --editable . --config-settings editable_mode=strict
33
44
 
34
45
  <a name = "Basic Usage"></a>
35
46
 
@@ -53,7 +64,7 @@ partitions = repeated_leiden_from_gammas(G, np.linspace(0, 2, 1000))
53
64
  # prune to the stable partitions from gamma=0 to gamma=2
54
65
  stable_partitions = prune_to_stable_partitions(G, partitions, 0, 2)
55
66
  print(stable_partitions)
56
- ```
67
+ ```
57
68
 
58
69
  This prints
59
70
 
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modularitypruning
3
- Version: 1.5.1
3
+ Version: 2.0.0
4
4
  Summary: Pruning tool to identify small subsets of network partitions that are significant from the perspective of stochastic block model inference.
5
5
  Author-email: Ryan Gibson <ryan.alex.gibson@gmail.com>
6
6
  License-Expression: MIT
7
7
  Classifier: Development Status :: 5 - Production/Stable
8
8
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
9
9
  Classifier: Programming Language :: Python :: 3
10
- Classifier: Programming Language :: Python :: 3.9
11
10
  Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
@@ -15,7 +14,8 @@ Classifier: Programming Language :: Python :: 3.13
15
14
  Classifier: Programming Language :: Python :: 3.14
16
15
  Classifier: Programming Language :: Python :: 3.15
17
16
  Classifier: Programming Language :: Python :: 3 :: Only
18
- Requires-Python: >=3.9
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.10
19
19
  Description-Content-Type: text/markdown
20
20
  License-File: LICENSE
21
21
  Requires-Dist: leidenalg
@@ -35,17 +35,23 @@ Dynamic: license-file
35
35
 
36
36
  ModularityPruning is a pruning tool to identify small subsets of network
37
37
  partitions that are significant from the perspective of stochastic block model
38
- inference. This method works for single-layer and multi-layer networks, as well
39
- as for restricting focus to a fixed number of communities when desired.
38
+ inference. It:
39
+
40
+ - Works for single-layer and multi-layer networks
41
+ - Supports both directed and undirected networks
42
+ - Supports weighted networks via the `weight` edge attribute
43
+ - Restricts focus to a fixed number of communities when desired
44
+ - Contains a faster and more robust implementation of the CHAMP (Convex Hull of
45
+ Admissible Modularity Partitions) algorithm (see `CHAMP_2D` and `CHAMP_3D`)
40
46
 
41
47
  See the [documentation](https://modularitypruning.readthedocs.io/en/latest/)
42
48
  or the journal article at https://doi.org/10.1038/s41598-022-20142-6 for more
43
49
  information.
44
50
 
45
51
  Significantly more details can be found in the article's
46
- [Supplementary Information](
47
- https://static-content.springer.com/esm/art%3A10.1038%2Fs41598-022-20142-6/MediaObjects/41598_2022_20142_MOESM1_ESM.pdf
48
- ).
52
+ [Supplementary Information](https://static-content.springer.com/esm/art%3A10.1038%2Fs41598-022-20142-6/MediaObjects/41598_2022_20142_MOESM1_ESM.pdf).
53
+
54
+ Note: support for weighted and directed networks is an extension beyond the original publication.
49
55
 
50
56
  ## Installation
51
57
 
@@ -60,7 +66,12 @@ Alternatively, you can install it from this repository directly:
60
66
 
61
67
  git clone https://github.com/ragibson/ModularityPruning
62
68
  cd ModularityPruning
63
- python3 setup.py install
69
+ pip install .
70
+
71
+ For modularitypruning development, it is best to use a strict editable install
72
+ so that `pytest` and `mypy` can pick up changes immediately.
73
+
74
+ pip install --editable . --config-settings editable_mode=strict
64
75
 
65
76
  <a name = "Basic Usage"></a>
66
77
 
@@ -84,7 +95,7 @@ partitions = repeated_leiden_from_gammas(G, np.linspace(0, 2, 1000))
84
95
  # prune to the stable partitions from gamma=0 to gamma=2
85
96
  stable_partitions = prune_to_stable_partitions(G, partitions, 0, 2)
86
97
  print(stable_partitions)
87
- ```
98
+ ```
88
99
 
89
100
  This prints
90
101
 
@@ -24,4 +24,5 @@ utilities/parameter_estimation.py
24
24
  utilities/parameter_estimation_utilities.py
25
25
  utilities/partition_utilities.py
26
26
  utilities/plotting.py
27
- utilities/progress.py
27
+ utilities/py.typed
28
+ utilities/types.py
@@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "modularitypruning"
7
- version = "1.5.1"
7
+ version = "2.0.0"
8
8
  description = "Pruning tool to identify small subsets of network partitions that are significant from the perspective of stochastic block model inference."
9
9
  readme = "README.md"
10
- requires-python = ">=3.9"
10
+ requires-python = ">=3.10"
11
11
  license = "MIT"
12
12
  authors = [
13
13
  { name = "Ryan Gibson", email = "ryan.alex.gibson@gmail.com" }
@@ -17,14 +17,14 @@ classifiers = [
17
17
  "Development Status :: 5 - Production/Stable",
18
18
  "Topic :: Scientific/Engineering :: Information Analysis",
19
19
  "Programming Language :: Python :: 3",
20
- "Programming Language :: Python :: 3.9",
21
20
  "Programming Language :: Python :: 3.10",
22
21
  "Programming Language :: Python :: 3.11",
23
22
  "Programming Language :: Python :: 3.12",
24
23
  "Programming Language :: Python :: 3.13",
25
24
  "Programming Language :: Python :: 3.14",
26
25
  "Programming Language :: Python :: 3.15",
27
- "Programming Language :: Python :: 3 :: Only"
26
+ "Programming Language :: Python :: 3 :: Only",
27
+ "Typing :: Typed"
28
28
  ]
29
29
  dependencies = [
30
30
  "leidenalg",
@@ -44,5 +44,8 @@ packages = ["modularitypruning"]
44
44
  [tool.setuptools.package-dir]
45
45
  modularitypruning = "utilities"
46
46
 
47
+ [tool.setuptools.package-data]
48
+ modularitypruning = ["py.typed"]
49
+
47
50
  [tool.autopep8]
48
51
  max_line_length = 120
@@ -0,0 +1,122 @@
1
+ import unittest
2
+ from random import seed
3
+
4
+ from modularitypruning.champ_utilities import partition_coefficients_2D
5
+ from modularitypruning.leiden_utilities import leiden_part_with_membership, repeated_leiden_from_gammas
6
+
7
+ from .shared_testing_functions import generate_connected_ER, generate_SBM, \
8
+ generate_random_values, generate_random_partitions, generate_igraph_famous
9
+
10
+
11
+ class TestCHAMPCoefficients2D(unittest.TestCase):
12
+ def assert_partition_coefficient_correctness(self, G, partitions, coefficients):
13
+ A_hats, P_hats = coefficients
14
+
15
+ for membership, A_hat, P_hat in zip(partitions, A_hats, P_hats):
16
+ leiden_part = leiden_part_with_membership(G, membership)
17
+
18
+ # Q(gamma=0) = sum_{ij} A_{ij} delta(c_i, c_j) = A_hat
19
+ leiden_A_hat = leiden_part.quality(resolution_parameter=0)
20
+
21
+ # Q(gamma=0) - Q(gamma=1)
22
+ # = sum_{ij} (A_{ij} - gamma*P_{ij} - A_{ij}) delta(c_i, c_j)
23
+ # = sum_{ij} P_{ij} delta(c_i, c_j)
24
+ # = P_hat
25
+ leiden_P_hat = leiden_A_hat - leiden_part.quality(resolution_parameter=1)
26
+
27
+ self.assertAlmostEqual(A_hat, leiden_A_hat, places=8)
28
+ self.assertAlmostEqual(P_hat, leiden_P_hat, places=8)
29
+
30
+ def assert_partition_coefficient_correctness_ER(self, n=100, m=500, directed=False,
31
+ num_partitions=10, K_max=5, weighted=False):
32
+ G = generate_connected_ER(n=n, m=m, directed=directed, weighted=weighted)
33
+ partitions = generate_random_partitions(num_nodes=n, num_partitions=num_partitions, K_max=K_max)
34
+ coefficients = partition_coefficients_2D(G, partitions)
35
+ self.assert_partition_coefficient_correctness(G, partitions, coefficients)
36
+
37
+ def test_partition_coefficient_correctness_undirected_varying_n(self):
38
+ for n in [50, 100, 250, 500]:
39
+ for weighted in [False, True]:
40
+ with self.subTest(n=n, weighted=weighted):
41
+ self.assert_partition_coefficient_correctness_ER(n=n, m=5 * n, weighted=weighted)
42
+
43
+ def test_partition_coefficient_correctness_undirected_varying_m(self):
44
+ for m in [200, 500, 1000]:
45
+ for weighted in [False, True]:
46
+ with self.subTest(m=m, weighted=weighted):
47
+ self.assert_partition_coefficient_correctness_ER(m=m, weighted=weighted)
48
+
49
+ def test_partition_coefficient_correctness_undirected_varying_num_partitions(self):
50
+ for num_partitions in [10, 100, 1000]:
51
+ for weighted in [False, True]:
52
+ with self.subTest(num_partitions=num_partitions, weighted=weighted):
53
+ self.assert_partition_coefficient_correctness_ER(num_partitions=num_partitions,
54
+ weighted=weighted)
55
+
56
+ def test_partition_coefficient_correctness_undirected_varying_K_max(self):
57
+ for K_max in [2, 5, 10, 20]:
58
+ for weighted in [False, True]:
59
+ with self.subTest(K_max=K_max, weighted=weighted):
60
+ self.assert_partition_coefficient_correctness_ER(num_partitions=100, K_max=K_max,
61
+ weighted=weighted)
62
+
63
+ def test_partition_coefficient_correctness_directed_varying_n(self):
64
+ for n in [50, 100, 250, 500]:
65
+ for weighted in [False, True]:
66
+ with self.subTest(n=n, weighted=weighted):
67
+ self.assert_partition_coefficient_correctness_ER(n=n, m=10 * n, directed=True,
68
+ weighted=weighted)
69
+
70
+ def test_partition_coefficient_correctness_directed_varying_m(self):
71
+ for m in [400, 1000, 2000]:
72
+ for weighted in [False, True]:
73
+ with self.subTest(m=m, weighted=weighted):
74
+ self.assert_partition_coefficient_correctness_ER(m=m, directed=True, weighted=weighted)
75
+
76
+ def test_partition_coefficient_correctness_directed_varying_num_partitions(self):
77
+ for num_partitions in [10, 100, 1000]:
78
+ for weighted in [False, True]:
79
+ with self.subTest(num_partitions=num_partitions, weighted=weighted):
80
+ self.assert_partition_coefficient_correctness_ER(directed=True, num_partitions=num_partitions,
81
+ weighted=weighted)
82
+
83
+ def test_partition_coefficient_correctness_directed_varying_K_max(self):
84
+ for K_max in [2, 5, 10, 20]:
85
+ for weighted in [False, True]:
86
+ with self.subTest(K_max=K_max, weighted=weighted):
87
+ self.assert_partition_coefficient_correctness_ER(directed=True, num_partitions=100, K_max=K_max,
88
+ weighted=weighted)
89
+
90
+ def test_partition_coefficient_correctness_directed_SBM(self):
91
+ """Test on asymmetric directed SBMs, unweighted and weighted."""
92
+ for q in [2, 3, 5]:
93
+ for weighted in [False, True]:
94
+ with self.subTest(q=q, weighted=weighted):
95
+ G, _, _ = generate_SBM(q, directed=True, weighted=weighted)
96
+ partitions = generate_random_partitions(num_nodes=G.vcount(), num_partitions=100, K_max=q)
97
+ coefficients = partition_coefficients_2D(G, partitions)
98
+ self.assert_partition_coefficient_correctness(G, partitions, coefficients)
99
+
100
+ def test_partition_coefficient_correctness_igraph_famous_leiden(self):
101
+ """Test partition coefficient correctness on various famous graphs while obtaining partitions via Leiden.
102
+
103
+ The correctness is checked for the original undirected and (symmetric) directed variants.
104
+ """
105
+
106
+ for name, G in generate_igraph_famous():
107
+ gammas = generate_random_values(100, start_value=0, end_value=5)
108
+ with self.subTest(graph=name, directed=False):
109
+ partitions = repeated_leiden_from_gammas(G, gammas)
110
+ coefficients = partition_coefficients_2D(G, partitions)
111
+ self.assert_partition_coefficient_correctness(G, partitions, coefficients)
112
+
113
+ G.to_directed() # check the directed version of the graph as well
114
+ with self.subTest(graph=name, directed=True):
115
+ partitions = repeated_leiden_from_gammas(G, gammas)
116
+ coefficients = partition_coefficients_2D(G, partitions)
117
+ self.assert_partition_coefficient_correctness(G, partitions, coefficients)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ seed(0)
122
+ unittest.main()
@@ -0,0 +1,177 @@
1
+ import unittest
2
+ from random import seed
3
+
4
+ from modularitypruning.champ_utilities import partition_coefficients_3D
5
+ from modularitypruning.leiden_utilities import multilayer_leiden_part_with_membership, leiden_part_with_membership
6
+
7
+ from .shared_testing_functions import generate_connected_multilayer_ER, generate_random_partitions, \
8
+ generate_temporal_SBM
9
+
10
+
11
+ class TestCHAMPCoefficients3D(unittest.TestCase):
12
+ def assert_partition_coefficient_correctness(self, G_intralayer, G_interlayer, layer_membership,
13
+ partitions, coefficients):
14
+ A_hats, P_hats, C_hats = coefficients
15
+
16
+ for membership, A_hat, P_hat, C_hat in zip(partitions, A_hats, P_hats, C_hats):
17
+ intralayer_parts, interlayer_part = multilayer_leiden_part_with_membership(G_intralayer, G_interlayer,
18
+ layer_membership,
19
+ community_membership=membership)
20
+
21
+ # Q_intralayer(gamma=0) = sum_{ij} A_{ij} delta(c_i, c_j) = A_hat
22
+ leiden_A_hat = sum(p.quality(resolution_parameter=0) for p in intralayer_parts)
23
+
24
+ # Q_intralayer(gamma=0) - Q_intralayer(gamma=1)
25
+ # = sum_{ij} (A_{ij} - gamma*P_{ij} - A_{ij}) delta(c_i, c_j)
26
+ # = sum_{ij} P_{ij} delta(c_i, c_j)
27
+ # = P_hat
28
+ leiden_P_hat = leiden_A_hat - sum(p.quality(resolution_parameter=1) for p in intralayer_parts)
29
+
30
+ # Q_interlayer(omega=0)
31
+ # = sum_{ij} (C_{ij} - omega*P{ij}) delta(c_i, c_j)
32
+ # = sum_{ij} C_{ij} delta(c_i, c_j)
33
+ # = C_hat
34
+ leiden_C_hat = interlayer_part.quality(resolution_parameter=0)
35
+
36
+ self.assertAlmostEqual(A_hat, leiden_A_hat, places=8)
37
+ self.assertAlmostEqual(P_hat, leiden_P_hat, places=8)
38
+ self.assertAlmostEqual(C_hat, leiden_C_hat, places=8)
39
+
40
+ # Also test against an alternate, per-layer calculation of P_hat
41
+ alternate_P_hat = 0
42
+ for layer in set(layer_membership):
43
+ this_layer_indices = [i for i, l in enumerate(layer_membership) if layer == l]
44
+ layer_subgraph = G_intralayer.subgraph(this_layer_indices)
45
+ layer_community_membership = [community for i, community in enumerate(membership)
46
+ if layer_membership[i] == layer]
47
+ layer_part = leiden_part_with_membership(layer_subgraph, layer_community_membership)
48
+ alternate_P_hat += (layer_part.quality(resolution_parameter=0.0) -
49
+ layer_part.quality(resolution_parameter=1.0))
50
+ self.assertAlmostEqual(alternate_P_hat, P_hat, places=8)
51
+
52
+ def assert_partition_coefficient_correctness_ER(self, num_nodes_per_layer=100, m=25000, num_layers=10,
53
+ directed=False, num_partitions=10, K_max=10, weighted=False):
54
+ G_intralayer, G_interlayer, layer_membership = generate_connected_multilayer_ER(
55
+ num_nodes_per_layer=num_nodes_per_layer, m=m, num_layers=num_layers, directed=directed,
56
+ weighted=weighted)
57
+ partitions = generate_random_partitions(num_nodes=G_intralayer.vcount(), num_partitions=num_partitions,
58
+ K_max=K_max)
59
+ coefficients = partition_coefficients_3D(G_intralayer, G_interlayer, layer_membership, partitions)
60
+ self.assert_partition_coefficient_correctness(G_intralayer, G_interlayer, layer_membership, partitions,
61
+ coefficients)
62
+
63
+ def test_partition_coefficient_correctness_undirected_varying_num_nodes_per_layer(self):
64
+ for num_nodes_per_layer in [50, 100, 250, 500]:
65
+ for weighted in [False, True]:
66
+ with self.subTest(num_nodes_per_layer=num_nodes_per_layer, weighted=weighted):
67
+ self.assert_partition_coefficient_correctness_ER(num_nodes_per_layer=num_nodes_per_layer,
68
+ m=50 * num_nodes_per_layer,
69
+ weighted=weighted)
70
+
71
+ def test_partition_coefficient_correctness_undirected_varying_m(self):
72
+ for m in [5000, 10000, 15000, 20000]:
73
+ for weighted in [False, True]:
74
+ with self.subTest(m=m, weighted=weighted):
75
+ self.assert_partition_coefficient_correctness_ER(m=m, weighted=weighted)
76
+
77
+ def test_partition_coefficient_correctness_undirected_varying_num_layers(self):
78
+ for num_layers in [5, 10, 20, 30]:
79
+ for weighted in [False, True]:
80
+ with self.subTest(num_layers=num_layers, weighted=weighted):
81
+ self.assert_partition_coefficient_correctness_ER(num_layers=num_layers, weighted=weighted)
82
+
83
+ def test_partition_coefficient_correctness_undirected_varying_num_partitions(self):
84
+ for num_partitions in [5, 10, 100, 250]:
85
+ for weighted in [False, True]:
86
+ with self.subTest(num_partitions=num_partitions, weighted=weighted):
87
+ self.assert_partition_coefficient_correctness_ER(num_partitions=num_partitions,
88
+ weighted=weighted)
89
+
90
+ def test_partition_coefficient_correctness_undirected_varying_K_max(self):
91
+ for K_max in [2, 5, 10, 25]:
92
+ for weighted in [False, True]:
93
+ with self.subTest(K_max=K_max, weighted=weighted):
94
+ self.assert_partition_coefficient_correctness_ER(K_max=K_max, weighted=weighted)
95
+
96
+ def test_partition_coefficient_correctness_directed_varying_num_nodes_per_layer(self):
97
+ for num_nodes_per_layer in [50, 100, 250, 500]:
98
+ for weighted in [False, True]:
99
+ with self.subTest(num_nodes_per_layer=num_nodes_per_layer, weighted=weighted):
100
+ self.assert_partition_coefficient_correctness_ER(num_nodes_per_layer=num_nodes_per_layer,
101
+ m=100 * num_nodes_per_layer, directed=True,
102
+ weighted=weighted)
103
+
104
+ def test_partition_coefficient_correctness_directed_varying_m(self):
105
+ for m in [10000, 20000, 30000, 40000]:
106
+ for weighted in [False, True]:
107
+ with self.subTest(m=m, weighted=weighted):
108
+ self.assert_partition_coefficient_correctness_ER(m=m, directed=True, weighted=weighted)
109
+
110
+ def test_partition_coefficient_correctness_directed_varying_num_layers(self):
111
+ for num_layers in [5, 10, 20, 30]:
112
+ for weighted in [False, True]:
113
+ with self.subTest(num_layers=num_layers, weighted=weighted):
114
+ self.assert_partition_coefficient_correctness_ER(num_layers=num_layers, directed=True,
115
+ weighted=weighted)
116
+
117
+ def test_partition_coefficient_correctness_directed_varying_num_partitions(self):
118
+ for num_partitions in [5, 10, 100, 250]:
119
+ for weighted in [False, True]:
120
+ with self.subTest(num_partitions=num_partitions, weighted=weighted):
121
+ self.assert_partition_coefficient_correctness_ER(directed=True, num_partitions=num_partitions,
122
+ weighted=weighted)
123
+
124
+ def test_partition_coefficient_correctness_directed_varying_K_max(self):
125
+ for K_max in [2, 5, 10, 25]:
126
+ for weighted in [False, True]:
127
+ with self.subTest(K_max=K_max, weighted=weighted):
128
+ self.assert_partition_coefficient_correctness_ER(directed=True, K_max=K_max, weighted=weighted)
129
+
130
+ def assert_partition_coefficient_correctness_SBM(self, num_layers, weighted):
131
+ num_nodes_per_layer = 100
132
+ first_layer_membership = [i // (num_nodes_per_layer // 3) for i in range(num_nodes_per_layer)]
133
+
134
+ G_intralayer, G_interlayer, layer_membership = generate_temporal_SBM(
135
+ copying_probability=0.9, p_in=0.25, p_out=0.05, first_layer_membership=first_layer_membership,
136
+ num_layers=num_layers, weighted=weighted, directed=True)
137
+
138
+ partitions = generate_random_partitions(num_nodes=G_intralayer.vcount(), num_partitions=100, K_max=3)
139
+ coefficients = partition_coefficients_3D(G_intralayer, G_interlayer, layer_membership, partitions)
140
+
141
+ self.assert_partition_coefficient_correctness(G_intralayer, G_interlayer, layer_membership, partitions,
142
+ coefficients)
143
+
144
+ def test_partition_coefficient_correctness_directed_SBM(self):
145
+ """Test on asymmetric directed SBMs, unweighted and weighted."""
146
+ for num_layers in [4, 5]:
147
+ for weighted in [False, True]:
148
+ with self.subTest(num_layers=num_layers, weighted=weighted):
149
+ self.assert_partition_coefficient_correctness_SBM(num_layers=num_layers, weighted=weighted)
150
+
151
+ def test_partition_coefficient_correctness_interleaved_directedness(self):
152
+ """Test partition coefficient correctness when directedness of interlayer and intralayer edges do not match."""
153
+ for weighted in [False, True]:
154
+ # Intralayer directed edges, but interlayer undirected ones
155
+ with self.subTest(intralayer_directed=True, weighted=weighted):
156
+ G_intralayer, G_interlayer, layer_membership = generate_connected_multilayer_ER(
157
+ num_nodes_per_layer=100, m=5000, num_layers=10, directed=False, weighted=weighted)
158
+ G_intralayer.to_directed()
159
+ partitions = generate_random_partitions(num_nodes=G_intralayer.vcount(), num_partitions=10, K_max=10)
160
+ coefficients = partition_coefficients_3D(G_intralayer, G_interlayer, layer_membership, partitions)
161
+ self.assert_partition_coefficient_correctness(G_intralayer, G_interlayer, layer_membership, partitions,
162
+ coefficients)
163
+
164
+ # Interlayer directed edges, but intralayer undirected ones
165
+ with self.subTest(intralayer_directed=False, weighted=weighted):
166
+ G_intralayer, G_interlayer, layer_membership = generate_connected_multilayer_ER(
167
+ num_nodes_per_layer=100, m=5000, num_layers=10, directed=False, weighted=weighted)
168
+ G_interlayer.to_directed()
169
+ partitions = generate_random_partitions(num_nodes=G_intralayer.vcount(), num_partitions=10, K_max=10)
170
+ coefficients = partition_coefficients_3D(G_intralayer, G_interlayer, layer_membership, partitions)
171
+ self.assert_partition_coefficient_correctness(G_intralayer, G_interlayer, layer_membership, partitions,
172
+ coefficients)
173
+
174
+
175
+ if __name__ == "__main__":
176
+ seed(0)
177
+ unittest.main()