modularitypruning 2.0.0__tar.gz → 2.0.2__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 (31) hide show
  1. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/PKG-INFO +4 -4
  2. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/README.md +3 -3
  3. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/modularitypruning.egg-info/PKG-INFO +4 -4
  4. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/modularitypruning.egg-info/SOURCES.txt +1 -0
  5. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/pyproject.toml +12 -2
  6. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_monolayer_parameter_estimation.py +2 -0
  7. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_multiplex_parameter_estimation.py +3 -1
  8. modularitypruning-2.0.2/tests/test_prune_stable_partitions.py +55 -0
  9. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_temporal_multilevel_parameter_estimation.py +3 -1
  10. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/champ_utilities.py +62 -61
  11. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/parameter_estimation_utilities.py +26 -21
  12. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/plotting.py +1 -1
  13. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/types.py +4 -0
  14. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/LICENSE +0 -0
  15. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/modularitypruning.egg-info/dependency_links.txt +0 -0
  16. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/modularitypruning.egg-info/requires.txt +0 -0
  17. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/modularitypruning.egg-info/top_level.txt +0 -0
  18. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/setup.cfg +0 -0
  19. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_champ_coefficients_2D.py +0 -0
  20. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_champ_coefficients_3D.py +0 -0
  21. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_champ_usage_2D.py +0 -0
  22. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_champ_usage_3D.py +0 -0
  23. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_deprecated_louvain_names.py +0 -0
  24. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_documentation_examples.py +0 -0
  25. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/tests/test_parallel_leiden_performance.py +0 -0
  26. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/__init__.py +0 -0
  27. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/leiden_utilities.py +0 -0
  28. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/louvain_utilities.py +0 -0
  29. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/parameter_estimation.py +0 -0
  30. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/partition_utilities.py +0 -0
  31. {modularitypruning-2.0.0 → modularitypruning-2.0.2}/utilities/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modularitypruning
3
- Version: 2.0.0
3
+ Version: 2.0.2
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
@@ -105,6 +105,6 @@ which is the stable 4-community split of the Karate Club network.
105
105
 
106
106
  ## More Information
107
107
 
108
- The [issues (which contains some potential future work)](https://github.com/ragibson/ModularityPruning/issues)
109
- and [figure generation runtimes README](figure_generation_runtimes.md) may also
110
- be of interest.
108
+ The [issues](https://github.com/ragibson/ModularityPruning/issues) and
109
+ [figure generation runtimes README](figure_generation_runtimes.md) may also be
110
+ of interest.
@@ -74,6 +74,6 @@ which is the stable 4-community split of the Karate Club network.
74
74
 
75
75
  ## More Information
76
76
 
77
- The [issues (which contains some potential future work)](https://github.com/ragibson/ModularityPruning/issues)
78
- and [figure generation runtimes README](figure_generation_runtimes.md) may also
79
- be of interest.
77
+ The [issues](https://github.com/ragibson/ModularityPruning/issues) and
78
+ [figure generation runtimes README](figure_generation_runtimes.md) may also be
79
+ of interest.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modularitypruning
3
- Version: 2.0.0
3
+ Version: 2.0.2
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
@@ -105,6 +105,6 @@ which is the stable 4-community split of the Karate Club network.
105
105
 
106
106
  ## More Information
107
107
 
108
- The [issues (which contains some potential future work)](https://github.com/ragibson/ModularityPruning/issues)
109
- and [figure generation runtimes README](figure_generation_runtimes.md) may also
110
- be of interest.
108
+ The [issues](https://github.com/ragibson/ModularityPruning/issues) and
109
+ [figure generation runtimes README](figure_generation_runtimes.md) may also be
110
+ of interest.
@@ -15,6 +15,7 @@ tests/test_documentation_examples.py
15
15
  tests/test_monolayer_parameter_estimation.py
16
16
  tests/test_multiplex_parameter_estimation.py
17
17
  tests/test_parallel_leiden_performance.py
18
+ tests/test_prune_stable_partitions.py
18
19
  tests/test_temporal_multilevel_parameter_estimation.py
19
20
  utilities/__init__.py
20
21
  utilities/champ_utilities.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "modularitypruning"
7
- version = "2.0.0"
7
+ version = "2.0.2"
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
10
  requires-python = ">=3.10"
@@ -48,4 +48,14 @@ modularitypruning = "utilities"
48
48
  modularitypruning = ["py.typed"]
49
49
 
50
50
  [tool.autopep8]
51
- max_line_length = 120
51
+ max_line_length = 120
52
+
53
+ [tool.coverage.run]
54
+ # a bit strange to include tests, but this is recommended by coverage.py
55
+ source = ["modularitypruning", "tests"]
56
+ concurrency = ["multiprocessing"]
57
+ branch = true
58
+
59
+ [tool.coverage.report]
60
+ fail_under = 90
61
+ show_missing = true
@@ -98,6 +98,8 @@ class TestMonolayerParameterEstimation(unittest.TestCase):
98
98
  G_undirected.es['weight'] = weights
99
99
  gamma_undirected_treatment = gamma_estimate(G_undirected, membership)
100
100
 
101
+ assert gamma_directed is not None
102
+ assert gamma_undirected_treatment is not None
101
103
  self.assertGreater(abs(gamma_directed - gamma_undirected_treatment), 1e-2)
102
104
 
103
105
 
@@ -114,7 +114,7 @@ class TestMultiplexParameterEstimation(unittest.TestCase):
114
114
  one_way_edges = [(0, 1), (2, 3), (0, 2), (0, 3), (1, 2), (1, 3)]
115
115
  edge_list = one_way_edges + [(u + 4, v + 4) for (u, v) in one_way_edges]
116
116
  weights = [1.0, 2.0, 3.0, 3.0, 3.0, 3.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0]
117
- membership = [0, 0, 1, 1, 0, 0, 1, 1]
117
+ membership = (0, 0, 1, 1, 0, 0, 1, 1)
118
118
  layer_membership = [0, 0, 0, 0, 1, 1, 1, 1]
119
119
  G_interlayer = ig.Graph([(0, 4), (4, 0), (1, 5), (5, 1), (2, 6), (6, 2), (3, 7), (7, 3)], directed=True)
120
120
 
@@ -128,6 +128,8 @@ class TestMultiplexParameterEstimation(unittest.TestCase):
128
128
  G_undirected.es['weight'] = weights
129
129
  gamma_undirected_treatment, _ = gamma_omega_estimate(G_undirected, G_interlayer, layer_membership,
130
130
  membership, model='multiplex')
131
+ assert gamma_directed is not None
132
+ assert gamma_undirected_treatment is not None
131
133
  self.assertGreater(abs(gamma_directed - gamma_undirected_treatment), 1e-2)
132
134
 
133
135
  def test_directed_consistency_multiplex_SBM_leiden(self):
@@ -0,0 +1,55 @@
1
+ """Simple tests of main pruning functions beyond the other testing scripts.
2
+
3
+ These cover:
4
+ - prune_to_stable_partitions
5
+ - prune_to_multilayer_stable_partitions
6
+ """
7
+ import unittest
8
+ from random import seed
9
+
10
+ from modularitypruning import prune_to_multilayer_stable_partitions, prune_to_stable_partitions
11
+ from modularitypruning.leiden_utilities import multilayer_leiden, singlelayer_leiden
12
+
13
+ from .shared_testing_functions import generate_connected_ER, generate_temporal_SBM
14
+
15
+
16
+ class TestPruneToStablePartitions(unittest.TestCase):
17
+ def test_prune_leiden_partitions_and_membership_tuples_agree(self):
18
+ """Check that passing a list of leidenalg partitions operates the same as a list of membership tuples."""
19
+ G = generate_connected_ER(n=50, m=200, directed=False)
20
+ partitions = [singlelayer_leiden(G, gamma, return_partition=True)
21
+ for gamma in [0.25, 0.5, 0.75, 1.0, 1.25, 1.5]]
22
+ membership_tuples = [tuple(part.membership) for part in partitions]
23
+
24
+ stable_from_tuples = prune_to_stable_partitions(G, membership_tuples, 0, 2)
25
+ stable_from_partitions = prune_to_stable_partitions(G, partitions, 0, 2)
26
+
27
+ self.assertEqual(stable_from_partitions, stable_from_tuples)
28
+ self.assertTrue(stable_from_tuples) # non-empty
29
+
30
+
31
+ class TestPruneToMultilayerStablePartitions(unittest.TestCase):
32
+ def test_prune_leiden_partitions_and_membership_tuples_agree(self):
33
+ """Check that passing a list of leidenalg partitions operates the same as a list of membership tuples."""
34
+ G_intralayer, G_interlayer, layer_vec = generate_temporal_SBM(
35
+ copying_probability=0.75, p_in=0.6, p_out=0.05,
36
+ first_layer_membership=[0] * 10 + [1] * 10, num_layers=2)
37
+
38
+ partition_runs = [multilayer_leiden(G_intralayer, G_interlayer, layer_vec, gamma, omega, return_partition=True)
39
+ for gamma in [0.5, 1.0, 1.5, 2.0] for omega in [0.5, 1.0, 1.5, 2.0]]
40
+
41
+ partitions = [run[0] for run in partition_runs]
42
+ membership_tuples = [tuple(part.membership) for part in partitions]
43
+
44
+ stable_from_tuples = prune_to_multilayer_stable_partitions(
45
+ G_intralayer, G_interlayer, layer_vec, 'temporal', membership_tuples, 0, 2, 0, 2)
46
+ stable_from_partitions = prune_to_multilayer_stable_partitions(
47
+ G_intralayer, G_interlayer, layer_vec, 'temporal', partitions, 0, 2, 0, 2)
48
+
49
+ self.assertEqual(stable_from_partitions, stable_from_tuples)
50
+ self.assertTrue(stable_from_tuples) # non-empty
51
+
52
+
53
+ if __name__ == "__main__":
54
+ seed(0)
55
+ unittest.main()
@@ -159,7 +159,7 @@ class TestTemporalAndMultilevelParameterEstimation(unittest.TestCase):
159
159
  one_way_edges = [(0, 1), (2, 3), (0, 2), (0, 3), (1, 2), (1, 3)]
160
160
  edge_list = one_way_edges + [(u + 4, v + 4) for (u, v) in one_way_edges]
161
161
  weights = [1.0, 2.0, 3.0, 3.0, 3.0, 3.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0]
162
- membership = [0, 0, 1, 1, 0, 0, 1, 1]
162
+ membership = (0, 0, 1, 1, 0, 0, 1, 1)
163
163
  layer_membership = [0, 0, 0, 0, 1, 1, 1, 1]
164
164
  G_interlayer = ig.Graph([(0, 4), (1, 5), (2, 6), (3, 7)], directed=True)
165
165
 
@@ -174,6 +174,8 @@ class TestTemporalAndMultilevelParameterEstimation(unittest.TestCase):
174
174
  G_undirected.es['weight'] = weights
175
175
  gamma_undirected_treatment, _ = gamma_omega_estimate(G_undirected, G_interlayer, layer_membership,
176
176
  membership, model=model)
177
+ assert gamma_directed is not None
178
+ assert gamma_undirected_treatment is not None
177
179
  self.assertGreater(abs(gamma_directed - gamma_undirected_treatment), 1e-2)
178
180
 
179
181
  def test_directed_consistency_temporal_SBM_leiden(self):
@@ -35,68 +35,69 @@ def get_interior_point(
35
35
  # We suppress these two warnings to avoid cluttering output, some of these warnings are expected as the result is
36
36
  # converged to and we've checked the consistency of results in our own tests. Moreover, we explicitly check the
37
37
  # interior point's validity prior to returning.
38
- warnings.filterwarnings("ignore", category=LinAlgWarning)
39
- warnings.filterwarnings("ignore", category=OptimizeWarning)
40
-
41
- normals, offsets = np.split(halfspaces, [-1], axis=1)
42
-
43
- # in our singlelayer case, the last four halfspaces are boundary halfspaces (a square)
44
- # for multilayer, the last six are boundaries (a cube)
45
- if normals.shape[1] == 2:
46
- interior_hs, boundaries = np.split(halfspaces, [-4], axis=0)
47
- elif normals.shape[1] == 3:
48
- interior_hs, boundaries = np.split(halfspaces, [-6], axis=0)
49
- else:
50
- raise ValueError(f"get_interior_point received unhandled dimension {normals.shape[1]}!")
51
-
52
- num_retries: int = 0
53
-
54
- while True: # retry until success
55
- # randomly sample some of the halfspaces
56
- sample_len: int = min(initial_num_sampled, len(interior_hs)) # len(interior_hs)
57
- sampled_hs: FloatArray = np.vstack((
58
- interior_hs[choice(interior_hs.shape[0], sample_len, replace=False)], boundaries))
59
-
60
- # compute the Chebyshev center of the sampled halfspaces' intersection
61
- norm_vector: FloatArray = np.reshape(
62
- np.linalg.norm(sampled_hs[:, :-1], axis=1), (sampled_hs.shape[0], 1))
63
- c: FloatArray = np.zeros((sampled_hs.shape[1],))
64
- c[-1] = -1
65
- A: FloatArray = np.hstack((sampled_hs[:, :-1], norm_vector))
66
- b: FloatArray = -sampled_hs[:, -1:]
67
-
68
- res = linprog(c, A_ub=A, b_ub=b, bounds=(-np.inf, np.inf), method='highs-ipm')
69
-
70
- # check solution if optimization succeeded, even with difficulties
71
- # res.status codes
72
- # 0 : Optimization terminated successfully.
73
- # 1 : Iteration limit reached.
74
- # 2 : Problem appears to be infeasible.
75
- # 3 : Problem appears to be unbounded.
76
- # 4 : Numerical difficulties encountered.
77
- if res.success or res.status in {0, 1, 4}:
78
- if res.x is None:
79
- # satisfying mypy, this should never occur
80
- raise ValueError(f"Scipy's linprog claimed success but returned no solution! {res}")
81
- intpt: FloatArray = res.x[:-1] # res.x contains [interior_point, distance to enclosing polyhedron]
82
-
83
- # ensure that the computed point is actually interior to all halfspaces
84
- if (np.dot(normals, intpt) + np.transpose(offsets) < 0).all():
85
- break
86
- elif res.status in {2, 3}:
87
- # if we ever fail, the linear program seems impossible
88
- raise ValueError("get_interior_point problem is impossible or degenerate!")
89
-
90
- if num_retries > full_retry_limit:
91
- raise ValueError("get_interior_point is unable to find a well-conditioned solution, "
92
- "but the problem does not appear impossible or degenerate?")
93
-
94
- if initial_num_sampled >= len(interior_hs):
95
- num_retries += 1
38
+ with warnings.catch_warnings(): # revert our filters once we're done
39
+ warnings.filterwarnings("ignore", category=LinAlgWarning)
40
+ warnings.filterwarnings("ignore", category=OptimizeWarning)
41
+
42
+ normals, offsets = np.split(halfspaces, [-1], axis=1)
43
+
44
+ # in our singlelayer case, the last four halfspaces are boundary halfspaces (a square)
45
+ # for multilayer, the last six are boundaries (a cube)
46
+ if normals.shape[1] == 2:
47
+ interior_hs, boundaries = np.split(halfspaces, [-4], axis=0)
48
+ elif normals.shape[1] == 3:
49
+ interior_hs, boundaries = np.split(halfspaces, [-6], axis=0)
96
50
  else:
97
- initial_num_sampled *= 2 # try again and sample more halfspaces this time
98
-
99
- return intpt
51
+ raise ValueError(f"get_interior_point received unhandled dimension {normals.shape[1]}!")
52
+
53
+ num_retries: int = 0
54
+
55
+ while True: # retry until success
56
+ # randomly sample some of the halfspaces
57
+ sample_len: int = min(initial_num_sampled, len(interior_hs)) # len(interior_hs)
58
+ sampled_hs: FloatArray = np.vstack((
59
+ interior_hs[choice(interior_hs.shape[0], sample_len, replace=False)], boundaries))
60
+
61
+ # compute the Chebyshev center of the sampled halfspaces' intersection
62
+ norm_vector: FloatArray = np.reshape(
63
+ np.linalg.norm(sampled_hs[:, :-1], axis=1), (sampled_hs.shape[0], 1))
64
+ c: FloatArray = np.zeros((sampled_hs.shape[1],))
65
+ c[-1] = -1
66
+ A: FloatArray = np.hstack((sampled_hs[:, :-1], norm_vector))
67
+ b: FloatArray = -sampled_hs[:, -1:]
68
+
69
+ res = linprog(c, A_ub=A, b_ub=b, bounds=(-np.inf, np.inf), method='highs-ipm')
70
+
71
+ # check solution if optimization succeeded, even with difficulties
72
+ # res.status codes
73
+ # 0 : Optimization terminated successfully.
74
+ # 1 : Iteration limit reached.
75
+ # 2 : Problem appears to be infeasible.
76
+ # 3 : Problem appears to be unbounded.
77
+ # 4 : Numerical difficulties encountered.
78
+ if res.success or res.status in {0, 1, 4}:
79
+ if res.x is None:
80
+ # satisfying mypy, this should never occur
81
+ raise ValueError(f"Scipy's linprog claimed success but returned no solution! {res}")
82
+ intpt: FloatArray = res.x[:-1] # res.x contains [interior_point, distance to enclosing polyhedron]
83
+
84
+ # ensure that the computed point is actually interior to all halfspaces
85
+ if (np.dot(normals, intpt) + np.transpose(offsets) < 0).all():
86
+ break
87
+ elif res.status in {2, 3}:
88
+ # if we ever fail, the linear program seems impossible
89
+ raise ValueError("get_interior_point problem is impossible or degenerate!")
90
+
91
+ if num_retries > full_retry_limit:
92
+ raise ValueError("get_interior_point is unable to find a well-conditioned solution, "
93
+ "but the problem does not appear impossible or degenerate?")
94
+
95
+ if initial_num_sampled >= len(interior_hs):
96
+ num_retries += 1
97
+ else:
98
+ initial_num_sampled *= 2 # try again and sample more halfspaces this time
99
+
100
+ return intpt
100
101
 
101
102
 
102
103
  def CHAMP_2D(
@@ -13,7 +13,7 @@ from .partition_utilities import num_communities
13
13
  from .types import (Domain, DomainWithEstimates, FloatArray, GammaEstimate,
14
14
  GammaEstimateValue, GammaOmegaEstimate, GammaRange, GammaUpdateFunc, LayerVector,
15
15
  LeidenPartition, Membership, MultilayerSBMParameters, OmegaUpdateFunc,
16
- PersistenceFunc, SinglelayerSBMParameters)
16
+ PersistenceFunc, PrunablePartitions, SinglelayerSBMParameters)
17
17
 
18
18
 
19
19
  def estimate_singlelayer_SBM_parameters(
@@ -353,7 +353,7 @@ def omega_function_from_model(
353
353
  :type model: str
354
354
  :param omega_max: maximum allowed value for omega
355
355
  :type omega_max: float
356
- :param T: number of layers in input graph (automatically computed if None)
356
+ :param T: number of layers in input graph
357
357
  :type T: int
358
358
  :return: an "update_omega" function
359
359
  :rtype: function
@@ -575,7 +575,6 @@ def gamma_omega_estimates_to_stable_partitions(
575
575
 
576
576
  for polyverts, membership, gamma_est, omega_est in domains_with_estimates:
577
577
  if gamma_est is None or omega_est is None:
578
- print(gamma_est, omega_est)
579
578
  continue
580
579
 
581
580
  centroid_x: float = np.mean([x[0] for x in polyverts])
@@ -604,7 +603,7 @@ def gamma_omega_estimates_to_stable_partitions(
604
603
 
605
604
 
606
605
  def prune_to_stable_partitions(
607
- G: ig.Graph, parts: set[Membership],
606
+ G: ig.Graph, parts: PrunablePartitions,
608
607
  gamma_start: float, gamma_end: float,
609
608
  restrict_num_communities: int | None = None,
610
609
  single_threaded: bool = False
@@ -620,7 +619,7 @@ def prune_to_stable_partitions(
620
619
  :param G: graph of interest
621
620
  :type G: igraph.Graph
622
621
  :param parts: partitions to prune
623
- :type parts: set[tuple[int, ...]]
622
+ :type parts: list or set of membership tuples or leidenalg.RBConfigurationVertexPartitions
624
623
  :param gamma_start: starting gamma value for CHAMP
625
624
  :type gamma_start: float
626
625
  :param gamma_end: ending gamma value for CHAMP
@@ -633,7 +632,7 @@ def prune_to_stable_partitions(
633
632
  :rtype: list[tuple[int, ...]]
634
633
  """
635
634
 
636
- if not G.is_connected():
635
+ if not G.is_connected(mode='weak'):
637
636
  warnings.warn("The pruning pipeline has not been thoroughly tested on disconnected graphs. If you run into "
638
637
  "problems, consider using the largest connected component of your graph.")
639
638
 
@@ -643,12 +642,15 @@ def prune_to_stable_partitions(
643
642
  if sum(G.es['weight']) <= 0:
644
643
  raise ValueError("Total edge weight must be positive.")
645
644
 
646
- if isinstance(parts, leidenalg.RBConfigurationVertexPartition):
647
- # convert to (canonically represented) membership vectors if necessary
648
- parts = {sorted_tuple(part.membership) for part in parts}
649
- else:
650
- # assume parts contains membership vectors
651
- parts = {sorted_tuple(part) for part in parts}
645
+ canonical_parts: set[Membership] = set()
646
+ for part in parts:
647
+ if isinstance(part, leidenalg.RBConfigurationVertexPartition):
648
+ # convert to (canonically represented) membership vectors if necessary
649
+ canonical_parts.add(sorted_tuple(tuple(part.membership)))
650
+ else:
651
+ # assume parts contains membership vectors
652
+ canonical_parts.add(sorted_tuple(part))
653
+ parts = canonical_parts
652
654
 
653
655
  if restrict_num_communities is not None:
654
656
  parts = {part for part in parts if num_communities(part) == restrict_num_communities}
@@ -668,7 +670,7 @@ def prune_to_stable_partitions(
668
670
  def prune_to_multilayer_stable_partitions(
669
671
  G_intralayer: ig.Graph, G_interlayer: ig.Graph,
670
672
  layer_vec: LayerVector, model: str,
671
- parts: set[Membership], gamma_start: float, gamma_end: float,
673
+ parts: PrunablePartitions, gamma_start: float, gamma_end: float,
672
674
  omega_start: float, omega_end: float,
673
675
  restrict_num_communities: int | None = None, single_threaded: bool = False
674
676
  ) -> list[Membership]:
@@ -713,7 +715,7 @@ def prune_to_multilayer_stable_partitions(
713
715
  :param model: network layer topology (temporal, multilevel, multiplex)
714
716
  :type model: str
715
717
  :param parts: partitions to prune
716
- :type parts: set[tuple[int, ...]]
718
+ :type parts: list or set of membership tuples or leidenalg.RBConfigurationVertexPartitions
717
719
  :param gamma_start: starting gamma value for CHAMP
718
720
  :type gamma_start: float
719
721
  :param gamma_end: ending gamma value for CHAMP
@@ -736,7 +738,7 @@ def prune_to_multilayer_stable_partitions(
736
738
  # check graph is connected (cannot just check intra/interlayer graphs since they will both be disconnected)
737
739
  G_combined: ig.Graph = ig.Graph(edges=[(e.source, e.target) for e in G_intralayer.es] +
738
740
  [(e.source, e.target) for e in G_interlayer.es])
739
- if not G_combined.is_connected() or G_combined.vcount() != G_interlayer.vcount():
741
+ if not G_combined.is_connected(mode='weak') or G_combined.vcount() != G_interlayer.vcount():
740
742
  warnings.warn("The pruning pipeline has not been thoroughly tested on disconnected graphs. If you run into "
741
743
  "problems, consider using the largest connected component of your graph.")
742
744
 
@@ -751,12 +753,15 @@ def prune_to_multilayer_stable_partitions(
751
753
  warnings.warn("Parameter estimation treats interlayer edges as unweighted and will proceed as though all "
752
754
  "interlayer weights are 1.0, but CHAMP will still use the actual interlayer weights!")
753
755
 
754
- if isinstance(parts, leidenalg.RBConfigurationVertexPartition):
755
- # convert to (canonically represented) membership vectors if necessary
756
- parts = {sorted_tuple(part.membership) for part in parts}
757
- else:
758
- # assume parts contains membership vectors
759
- parts = {sorted_tuple(part) for part in parts}
756
+ canonical_parts: set[Membership] = set()
757
+ for part in parts:
758
+ if isinstance(part, leidenalg.RBConfigurationVertexPartition):
759
+ # convert to (canonically represented) membership vectors if necessary
760
+ canonical_parts.add(sorted_tuple(tuple(part.membership)))
761
+ else:
762
+ # assume parts contains membership vectors
763
+ canonical_parts.add(sorted_tuple(part))
764
+ parts = canonical_parts
760
765
 
761
766
  if restrict_num_communities is not None:
762
767
  parts = {part for part in parts if num_communities(part) == restrict_num_communities}
@@ -89,7 +89,7 @@ def plot_2d_domains(
89
89
  ) -> None:
90
90
  """Plot partition dominance ranges in the (gamma, omega) plane, using the domains from CHAMP_3D.
91
91
 
92
- Limits output to xlim and ylim dimensions. Note that the plotting here has x=gamma and y=omega.
92
+ Limits output to xlim and ylim dimensions. Note that the plotting here has x=omega and y=gamma by default.
93
93
 
94
94
  :param domains: CHAMP domains as returned from :meth:`~modularitypruning.champ_utilities.CHAMP_3D`.
95
95
  :param xlim: plotting x limits
@@ -16,6 +16,7 @@ IntSequence = Sequence[int] | IntArray
16
16
  FloatSequence = Sequence[float] | FloatArray
17
17
 
18
18
  # community membership vector and layer membership vector
19
+ # note: some parts of the code do actually support list[int] for membership, but this is simpler and more consistent
19
20
  Membership = tuple[int, ...]
20
21
  LayerVector = list[int]
21
22
  LayerTuple = tuple[int, ...] # needed for hashing
@@ -29,6 +30,9 @@ MultilayerLeidenParts: TypeAlias = tuple[list[LeidenPartition], leidenalg.CPMVer
29
30
  SingleLayerLeidenResult: TypeAlias = LeidenPartition | Membership
30
31
  MultilayerLeidenResult: TypeAlias = list[LeidenPartition] | Membership
31
32
 
33
+ # collections of partitions for pruning (membership vectors or leiden partitions)
34
+ PrunablePartitions = Sequence[LeidenPartition | Membership] | set[LeidenPartition | Membership]
35
+
32
36
  # CHAMP 2D domains and parameter estimates
33
37
  GammaEstimateValue = np.float64 | None
34
38
  GammaEstimate = tuple[np.float64, np.float64, Membership, GammaEstimateValue]