scikit-network 0.33.3__cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
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.
Potentially problematic release.
This version of scikit-network might be problematic. Click here for more details.
- scikit_network-0.33.3.dist-info/METADATA +122 -0
- scikit_network-0.33.3.dist-info/RECORD +229 -0
- scikit_network-0.33.3.dist-info/WHEEL +6 -0
- scikit_network-0.33.3.dist-info/licenses/AUTHORS.rst +43 -0
- scikit_network-0.33.3.dist-info/licenses/LICENSE +34 -0
- scikit_network-0.33.3.dist-info/top_level.txt +1 -0
- scikit_network.libs/libgomp-d22c30c5.so.1.0.0 +0 -0
- sknetwork/__init__.py +21 -0
- sknetwork/base.py +67 -0
- sknetwork/classification/__init__.py +8 -0
- sknetwork/classification/base.py +142 -0
- sknetwork/classification/base_rank.py +133 -0
- sknetwork/classification/diffusion.py +134 -0
- sknetwork/classification/knn.py +139 -0
- sknetwork/classification/metrics.py +205 -0
- sknetwork/classification/pagerank.py +66 -0
- sknetwork/classification/propagation.py +152 -0
- sknetwork/classification/tests/__init__.py +1 -0
- sknetwork/classification/tests/test_API.py +30 -0
- sknetwork/classification/tests/test_diffusion.py +77 -0
- sknetwork/classification/tests/test_knn.py +23 -0
- sknetwork/classification/tests/test_metrics.py +53 -0
- sknetwork/classification/tests/test_pagerank.py +20 -0
- sknetwork/classification/tests/test_propagation.py +24 -0
- sknetwork/classification/vote.cpp +27587 -0
- sknetwork/classification/vote.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/classification/vote.pyx +56 -0
- sknetwork/clustering/__init__.py +8 -0
- sknetwork/clustering/base.py +172 -0
- sknetwork/clustering/kcenters.py +253 -0
- sknetwork/clustering/leiden.py +242 -0
- sknetwork/clustering/leiden_core.cpp +31578 -0
- sknetwork/clustering/leiden_core.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/clustering/leiden_core.pyx +124 -0
- sknetwork/clustering/louvain.py +286 -0
- sknetwork/clustering/louvain_core.cpp +31223 -0
- sknetwork/clustering/louvain_core.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/clustering/louvain_core.pyx +124 -0
- sknetwork/clustering/metrics.py +91 -0
- sknetwork/clustering/postprocess.py +66 -0
- sknetwork/clustering/propagation_clustering.py +104 -0
- sknetwork/clustering/tests/__init__.py +1 -0
- sknetwork/clustering/tests/test_API.py +38 -0
- sknetwork/clustering/tests/test_kcenters.py +60 -0
- sknetwork/clustering/tests/test_leiden.py +34 -0
- sknetwork/clustering/tests/test_louvain.py +135 -0
- sknetwork/clustering/tests/test_metrics.py +50 -0
- sknetwork/clustering/tests/test_postprocess.py +39 -0
- sknetwork/data/__init__.py +6 -0
- sknetwork/data/base.py +33 -0
- sknetwork/data/load.py +406 -0
- sknetwork/data/models.py +459 -0
- sknetwork/data/parse.py +644 -0
- sknetwork/data/test_graphs.py +84 -0
- sknetwork/data/tests/__init__.py +1 -0
- sknetwork/data/tests/test_API.py +30 -0
- sknetwork/data/tests/test_base.py +14 -0
- sknetwork/data/tests/test_load.py +95 -0
- sknetwork/data/tests/test_models.py +52 -0
- sknetwork/data/tests/test_parse.py +250 -0
- sknetwork/data/tests/test_test_graphs.py +29 -0
- sknetwork/data/tests/test_toy_graphs.py +68 -0
- sknetwork/data/timeout.py +38 -0
- sknetwork/data/toy_graphs.py +611 -0
- sknetwork/embedding/__init__.py +8 -0
- sknetwork/embedding/base.py +94 -0
- sknetwork/embedding/force_atlas.py +198 -0
- sknetwork/embedding/louvain_embedding.py +148 -0
- sknetwork/embedding/random_projection.py +135 -0
- sknetwork/embedding/spectral.py +141 -0
- sknetwork/embedding/spring.py +198 -0
- sknetwork/embedding/svd.py +359 -0
- sknetwork/embedding/tests/__init__.py +1 -0
- sknetwork/embedding/tests/test_API.py +49 -0
- sknetwork/embedding/tests/test_force_atlas.py +35 -0
- sknetwork/embedding/tests/test_louvain_embedding.py +33 -0
- sknetwork/embedding/tests/test_random_projection.py +28 -0
- sknetwork/embedding/tests/test_spectral.py +81 -0
- sknetwork/embedding/tests/test_spring.py +50 -0
- sknetwork/embedding/tests/test_svd.py +43 -0
- sknetwork/gnn/__init__.py +10 -0
- sknetwork/gnn/activation.py +117 -0
- sknetwork/gnn/base.py +181 -0
- sknetwork/gnn/base_activation.py +90 -0
- sknetwork/gnn/base_layer.py +109 -0
- sknetwork/gnn/gnn_classifier.py +305 -0
- sknetwork/gnn/layer.py +153 -0
- sknetwork/gnn/loss.py +180 -0
- sknetwork/gnn/neighbor_sampler.py +65 -0
- sknetwork/gnn/optimizer.py +164 -0
- sknetwork/gnn/tests/__init__.py +1 -0
- sknetwork/gnn/tests/test_activation.py +56 -0
- sknetwork/gnn/tests/test_base.py +75 -0
- sknetwork/gnn/tests/test_base_layer.py +37 -0
- sknetwork/gnn/tests/test_gnn_classifier.py +130 -0
- sknetwork/gnn/tests/test_layers.py +80 -0
- sknetwork/gnn/tests/test_loss.py +33 -0
- sknetwork/gnn/tests/test_neigh_sampler.py +23 -0
- sknetwork/gnn/tests/test_optimizer.py +43 -0
- sknetwork/gnn/tests/test_utils.py +41 -0
- sknetwork/gnn/utils.py +127 -0
- sknetwork/hierarchy/__init__.py +6 -0
- sknetwork/hierarchy/base.py +96 -0
- sknetwork/hierarchy/louvain_hierarchy.py +272 -0
- sknetwork/hierarchy/metrics.py +234 -0
- sknetwork/hierarchy/paris.cpp +37871 -0
- sknetwork/hierarchy/paris.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/hierarchy/paris.pyx +316 -0
- sknetwork/hierarchy/postprocess.py +350 -0
- sknetwork/hierarchy/tests/__init__.py +1 -0
- sknetwork/hierarchy/tests/test_API.py +24 -0
- sknetwork/hierarchy/tests/test_algos.py +34 -0
- sknetwork/hierarchy/tests/test_metrics.py +62 -0
- sknetwork/hierarchy/tests/test_postprocess.py +57 -0
- sknetwork/linalg/__init__.py +9 -0
- sknetwork/linalg/basics.py +37 -0
- sknetwork/linalg/diteration.cpp +27403 -0
- sknetwork/linalg/diteration.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/linalg/diteration.pyx +47 -0
- sknetwork/linalg/eig_solver.py +93 -0
- sknetwork/linalg/laplacian.py +15 -0
- sknetwork/linalg/normalizer.py +86 -0
- sknetwork/linalg/operators.py +225 -0
- sknetwork/linalg/polynome.py +76 -0
- sknetwork/linalg/ppr_solver.py +170 -0
- sknetwork/linalg/push.cpp +31075 -0
- sknetwork/linalg/push.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/linalg/push.pyx +71 -0
- sknetwork/linalg/sparse_lowrank.py +142 -0
- sknetwork/linalg/svd_solver.py +91 -0
- sknetwork/linalg/tests/__init__.py +1 -0
- sknetwork/linalg/tests/test_eig.py +44 -0
- sknetwork/linalg/tests/test_laplacian.py +18 -0
- sknetwork/linalg/tests/test_normalization.py +34 -0
- sknetwork/linalg/tests/test_operators.py +66 -0
- sknetwork/linalg/tests/test_polynome.py +38 -0
- sknetwork/linalg/tests/test_ppr.py +50 -0
- sknetwork/linalg/tests/test_sparse_lowrank.py +61 -0
- sknetwork/linalg/tests/test_svd.py +38 -0
- sknetwork/linkpred/__init__.py +2 -0
- sknetwork/linkpred/base.py +46 -0
- sknetwork/linkpred/nn.py +126 -0
- sknetwork/linkpred/tests/__init__.py +1 -0
- sknetwork/linkpred/tests/test_nn.py +27 -0
- sknetwork/log.py +19 -0
- sknetwork/path/__init__.py +5 -0
- sknetwork/path/dag.py +54 -0
- sknetwork/path/distances.py +98 -0
- sknetwork/path/search.py +31 -0
- sknetwork/path/shortest_path.py +61 -0
- sknetwork/path/tests/__init__.py +1 -0
- sknetwork/path/tests/test_dag.py +37 -0
- sknetwork/path/tests/test_distances.py +62 -0
- sknetwork/path/tests/test_search.py +40 -0
- sknetwork/path/tests/test_shortest_path.py +40 -0
- sknetwork/ranking/__init__.py +8 -0
- sknetwork/ranking/base.py +61 -0
- sknetwork/ranking/betweenness.cpp +9710 -0
- sknetwork/ranking/betweenness.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/ranking/betweenness.pyx +97 -0
- sknetwork/ranking/closeness.py +92 -0
- sknetwork/ranking/hits.py +94 -0
- sknetwork/ranking/katz.py +83 -0
- sknetwork/ranking/pagerank.py +110 -0
- sknetwork/ranking/postprocess.py +37 -0
- sknetwork/ranking/tests/__init__.py +1 -0
- sknetwork/ranking/tests/test_API.py +32 -0
- sknetwork/ranking/tests/test_betweenness.py +38 -0
- sknetwork/ranking/tests/test_closeness.py +30 -0
- sknetwork/ranking/tests/test_hits.py +20 -0
- sknetwork/ranking/tests/test_pagerank.py +62 -0
- sknetwork/ranking/tests/test_postprocess.py +26 -0
- sknetwork/regression/__init__.py +4 -0
- sknetwork/regression/base.py +61 -0
- sknetwork/regression/diffusion.py +210 -0
- sknetwork/regression/tests/__init__.py +1 -0
- sknetwork/regression/tests/test_API.py +32 -0
- sknetwork/regression/tests/test_diffusion.py +56 -0
- sknetwork/sknetwork.py +3 -0
- sknetwork/test_base.py +35 -0
- sknetwork/test_log.py +15 -0
- sknetwork/topology/__init__.py +8 -0
- sknetwork/topology/cliques.cpp +32568 -0
- sknetwork/topology/cliques.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/topology/cliques.pyx +149 -0
- sknetwork/topology/core.cpp +30654 -0
- sknetwork/topology/core.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/topology/core.pyx +90 -0
- sknetwork/topology/cycles.py +243 -0
- sknetwork/topology/minheap.cpp +27335 -0
- sknetwork/topology/minheap.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/topology/minheap.pxd +20 -0
- sknetwork/topology/minheap.pyx +109 -0
- sknetwork/topology/structure.py +194 -0
- sknetwork/topology/tests/__init__.py +1 -0
- sknetwork/topology/tests/test_cliques.py +28 -0
- sknetwork/topology/tests/test_core.py +19 -0
- sknetwork/topology/tests/test_cycles.py +65 -0
- sknetwork/topology/tests/test_structure.py +85 -0
- sknetwork/topology/tests/test_triangles.py +38 -0
- sknetwork/topology/tests/test_wl.py +72 -0
- sknetwork/topology/triangles.cpp +8897 -0
- sknetwork/topology/triangles.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/topology/triangles.pyx +151 -0
- sknetwork/topology/weisfeiler_lehman.py +133 -0
- sknetwork/topology/weisfeiler_lehman_core.cpp +27638 -0
- sknetwork/topology/weisfeiler_lehman_core.cpython-313-aarch64-linux-gnu.so +0 -0
- sknetwork/topology/weisfeiler_lehman_core.pyx +114 -0
- sknetwork/utils/__init__.py +7 -0
- sknetwork/utils/check.py +355 -0
- sknetwork/utils/format.py +221 -0
- sknetwork/utils/membership.py +82 -0
- sknetwork/utils/neighbors.py +115 -0
- sknetwork/utils/tests/__init__.py +1 -0
- sknetwork/utils/tests/test_check.py +190 -0
- sknetwork/utils/tests/test_format.py +63 -0
- sknetwork/utils/tests/test_membership.py +24 -0
- sknetwork/utils/tests/test_neighbors.py +41 -0
- sknetwork/utils/tests/test_tfidf.py +18 -0
- sknetwork/utils/tests/test_values.py +66 -0
- sknetwork/utils/tfidf.py +37 -0
- sknetwork/utils/values.py +76 -0
- sknetwork/visualization/__init__.py +4 -0
- sknetwork/visualization/colors.py +34 -0
- sknetwork/visualization/dendrograms.py +277 -0
- sknetwork/visualization/graphs.py +1039 -0
- sknetwork/visualization/tests/__init__.py +1 -0
- sknetwork/visualization/tests/test_dendrograms.py +53 -0
- sknetwork/visualization/tests/test_graphs.py +176 -0
|
Binary file
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# distutils: language = c++
|
|
2
|
+
# cython: language_level=3
|
|
3
|
+
"""
|
|
4
|
+
Created in June 2020
|
|
5
|
+
@author: Julien Simonnet <julien.simonnet@etu.upmc.fr>
|
|
6
|
+
@author: Yohann Robert <yohann.robert@etu.upmc.fr>
|
|
7
|
+
"""
|
|
8
|
+
cimport cython
|
|
9
|
+
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
cimport numpy as np
|
|
14
|
+
from scipy import sparse
|
|
15
|
+
|
|
16
|
+
from sknetwork.utils.check import check_format
|
|
17
|
+
from sknetwork.topology.minheap cimport MinHeap
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cython.boundscheck(False)
|
|
21
|
+
@cython.wraparound(False)
|
|
22
|
+
cdef compute_core(int[:] indptr, int[:] indices):
|
|
23
|
+
"""Compute the core value of each node.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
indptr :
|
|
28
|
+
CSR format index array of the adjacency matrix.
|
|
29
|
+
indices :
|
|
30
|
+
CSR format index pointer array of the adjacency matrix.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
labels :
|
|
35
|
+
Core value of each node.
|
|
36
|
+
"""
|
|
37
|
+
cdef int n = indptr.shape[0] - 1
|
|
38
|
+
cdef int core_value = 0 # current/max core value of the graph
|
|
39
|
+
cdef int min_node # current node of minimum degree
|
|
40
|
+
cdef int i, j, k
|
|
41
|
+
cdef int[:] degrees = np.asarray(indptr)[1:] - np.asarray(indptr)[:n]
|
|
42
|
+
cdef np.ndarray[int, ndim=1] labels = np.empty((n,), dtype=np.int32)
|
|
43
|
+
cdef MinHeap mh = MinHeap.__new__(MinHeap, n) # minimum heap with an update system
|
|
44
|
+
|
|
45
|
+
# insert all nodes in the heap
|
|
46
|
+
for i in range(n):
|
|
47
|
+
mh.insert_key(i, degrees)
|
|
48
|
+
|
|
49
|
+
i = n - 1
|
|
50
|
+
while not mh.empty():
|
|
51
|
+
min_node = mh.pop_min(degrees)
|
|
52
|
+
core_value = max(core_value, degrees[min_node])
|
|
53
|
+
|
|
54
|
+
# decrease the degree of each neighbor of min_node
|
|
55
|
+
for k in range(indptr[min_node], indptr[min_node+1]):
|
|
56
|
+
j = indices[k]
|
|
57
|
+
degrees[j] -= 1
|
|
58
|
+
mh.decrease_key(j, degrees) # update the heap to take the new degree into account
|
|
59
|
+
|
|
60
|
+
labels[min_node] = core_value
|
|
61
|
+
i -= 1
|
|
62
|
+
|
|
63
|
+
return np.asarray(labels)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_core_decomposition(adjacency: Union[np.ndarray, sparse.csr_matrix]) -> np.ndarray:
|
|
67
|
+
"""Get the k-core decomposition of a graph.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
adjacency :
|
|
72
|
+
Adjacency matrix of the graph.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
core_values :
|
|
77
|
+
Core value of each node.
|
|
78
|
+
|
|
79
|
+
Example
|
|
80
|
+
-------
|
|
81
|
+
>>> from sknetwork.data import karate_club
|
|
82
|
+
>>> adjacency = karate_club()
|
|
83
|
+
>>> core_values = get_core_decomposition(adjacency)
|
|
84
|
+
>>> len(core_values)
|
|
85
|
+
34
|
|
86
|
+
"""
|
|
87
|
+
adjacency = check_format(adjacency, allow_empty=True)
|
|
88
|
+
indptr = adjacency.indptr
|
|
89
|
+
indices = adjacency.indices
|
|
90
|
+
return compute_core(indptr, indices)
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Created in February 2024
|
|
5
|
+
@author: Thomas Bonald <thomas.bonald@telecom-paris.fr>
|
|
6
|
+
@author: Yiwen Peng <yiwen.peng@telecom-paris.fr>
|
|
7
|
+
"""
|
|
8
|
+
from typing import Tuple, Optional, Union, List
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from scipy import sparse
|
|
12
|
+
|
|
13
|
+
from sknetwork.utils.check import is_symmetric, check_format
|
|
14
|
+
from sknetwork.utils.format import get_adjacency
|
|
15
|
+
from sknetwork.path import get_distances
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def is_acyclic(adjacency: sparse.csr_matrix, directed: Optional[bool] = None) -> bool:
|
|
19
|
+
"""Check whether a graph has no cycle.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
adjacency:
|
|
24
|
+
Adjacency matrix of the graph.
|
|
25
|
+
directed:
|
|
26
|
+
Whether to consider the graph as directed (inferred if not specified).
|
|
27
|
+
Returns
|
|
28
|
+
-------
|
|
29
|
+
is_acyclic : bool
|
|
30
|
+
A boolean with value True if the graph has no cycle and False otherwise.
|
|
31
|
+
|
|
32
|
+
Example
|
|
33
|
+
-------
|
|
34
|
+
>>> from sknetwork.topology import is_acyclic
|
|
35
|
+
>>> from sknetwork.data import star, grid
|
|
36
|
+
>>> is_acyclic(star())
|
|
37
|
+
True
|
|
38
|
+
>>> is_acyclic(grid())
|
|
39
|
+
False
|
|
40
|
+
"""
|
|
41
|
+
if directed is False:
|
|
42
|
+
# the graph must be undirected
|
|
43
|
+
if not is_symmetric(adjacency):
|
|
44
|
+
raise ValueError("The adjacency matrix is not symmetric. The parameter 'directed' must be True.")
|
|
45
|
+
elif directed is None:
|
|
46
|
+
# if not specified, infer from the graph
|
|
47
|
+
directed = not is_symmetric(adjacency)
|
|
48
|
+
has_loops = (adjacency.diagonal() > 0).any()
|
|
49
|
+
if has_loops:
|
|
50
|
+
return False
|
|
51
|
+
else:
|
|
52
|
+
n_cc = sparse.csgraph.connected_components(adjacency, directed, connection='strong', return_labels=False)
|
|
53
|
+
n_nodes = adjacency.shape[0]
|
|
54
|
+
if directed:
|
|
55
|
+
return n_cc == n_nodes
|
|
56
|
+
else:
|
|
57
|
+
n_edges = adjacency.nnz // 2
|
|
58
|
+
return n_cc == n_nodes - n_edges
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_cycles(adjacency: sparse.csr_matrix, directed: Optional[bool] = None) -> List[List[int]]:
|
|
62
|
+
"""Get all possible cycles of a graph.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
adjacency :
|
|
67
|
+
Adjacency matrix of the graph.
|
|
68
|
+
directed :
|
|
69
|
+
Whether to consider the graph as directed (inferred if not specified).
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
cycles : list
|
|
74
|
+
List of cycles, each cycle represented as a list of nodes.
|
|
75
|
+
|
|
76
|
+
Example
|
|
77
|
+
-------
|
|
78
|
+
>>> from sknetwork.topology import get_cycles
|
|
79
|
+
>>> from sknetwork.data import cyclic_digraph
|
|
80
|
+
>>> graph = cyclic_digraph(4, metadata=True)
|
|
81
|
+
>>> len(get_cycles(graph.adjacency, directed=True))
|
|
82
|
+
1
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
if directed is False:
|
|
86
|
+
# the graph must be undirected
|
|
87
|
+
if not is_symmetric(adjacency):
|
|
88
|
+
raise ValueError("The adjacency matrix is not symmetric. The parameter 'directed' must be True.")
|
|
89
|
+
elif directed is None:
|
|
90
|
+
# if not specified, infer from the graph
|
|
91
|
+
directed = not is_symmetric(adjacency)
|
|
92
|
+
|
|
93
|
+
cycles = []
|
|
94
|
+
n_nodes = adjacency.shape[0]
|
|
95
|
+
self_loops = np.argwhere(adjacency.diagonal() > 0).ravel()
|
|
96
|
+
if len(self_loops) > 0:
|
|
97
|
+
# add self-loops as cycles
|
|
98
|
+
for node in self_loops:
|
|
99
|
+
cycles.append([node])
|
|
100
|
+
|
|
101
|
+
# check if the graph is acyclic
|
|
102
|
+
n_cc, cc_labels = sparse.csgraph.connected_components(adjacency, directed, connection='strong', return_labels=True)
|
|
103
|
+
if directed and n_cc == n_nodes:
|
|
104
|
+
return cycles
|
|
105
|
+
elif not directed:
|
|
106
|
+
# acyclic undirected graph
|
|
107
|
+
n_edges = adjacency.nnz // 2
|
|
108
|
+
if n_cc == n_nodes - n_edges:
|
|
109
|
+
return cycles
|
|
110
|
+
|
|
111
|
+
# locate possible cycles
|
|
112
|
+
labels, counts = np.unique(cc_labels, return_counts=True)
|
|
113
|
+
if directed:
|
|
114
|
+
labels = labels[counts > 1]
|
|
115
|
+
cycle_starts = [np.argwhere(cc_labels == label).ravel()[0] for label in labels]
|
|
116
|
+
|
|
117
|
+
# find cycles for indicated nodes (depth-first traversal)
|
|
118
|
+
for start_node in cycle_starts:
|
|
119
|
+
stack = [(start_node, [start_node])]
|
|
120
|
+
while stack:
|
|
121
|
+
current_node, path = stack.pop()
|
|
122
|
+
for neighbor in adjacency.indices[adjacency.indptr[current_node]:adjacency.indptr[current_node+1]]:
|
|
123
|
+
if not directed and len(path) > 1 and neighbor == path[-2]:
|
|
124
|
+
# ignore the inverse edge (back move) in undirected graph
|
|
125
|
+
continue
|
|
126
|
+
if neighbor in path:
|
|
127
|
+
cycles.append(path[path.index(neighbor):])
|
|
128
|
+
else:
|
|
129
|
+
stack.append((neighbor, path + [neighbor]))
|
|
130
|
+
|
|
131
|
+
# remove duplicates
|
|
132
|
+
visited_cycles, unique_cycles = set(), []
|
|
133
|
+
for cycle in cycles:
|
|
134
|
+
candidate = np.roll(cycle, -cycle.index(min(cycle)))
|
|
135
|
+
current_cycle = tuple(candidate)
|
|
136
|
+
if not directed:
|
|
137
|
+
current_cycle = tuple(np.sort(candidate))
|
|
138
|
+
if current_cycle not in visited_cycles:
|
|
139
|
+
unique_cycles.append(list(candidate))
|
|
140
|
+
visited_cycles.add(current_cycle)
|
|
141
|
+
|
|
142
|
+
return unique_cycles
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def break_cycles(adjacency: sparse.csr_matrix, root: Union[int, List[int]],
|
|
146
|
+
directed: Optional[bool] = None) -> sparse.csr_matrix:
|
|
147
|
+
"""Break cycles of a graph from given roots.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
adjacency :
|
|
152
|
+
Adjacency matrix of the graph.
|
|
153
|
+
root :
|
|
154
|
+
The root node or list of root nodes to break cycles from.
|
|
155
|
+
directed :
|
|
156
|
+
Whether to consider the graph as directed (inferred if not specified).
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
adjacency : sparse.csr_matrix
|
|
161
|
+
Adjacency matrix of the acyclic graph.
|
|
162
|
+
|
|
163
|
+
Example
|
|
164
|
+
-------
|
|
165
|
+
>>> from sknetwork.topology import break_cycles, is_acyclic
|
|
166
|
+
>>> from sknetwork.data import cyclic_digraph
|
|
167
|
+
>>> adjacency = cyclic_digraph(4)
|
|
168
|
+
>>> dag = break_cycles(adjacency, root=0, directed=True)
|
|
169
|
+
>>> is_acyclic(dag, directed=True)
|
|
170
|
+
True
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
if is_acyclic(adjacency, directed):
|
|
174
|
+
return adjacency
|
|
175
|
+
|
|
176
|
+
if root is None:
|
|
177
|
+
raise ValueError("The parameter root must be specified.")
|
|
178
|
+
else:
|
|
179
|
+
out_degree = len(adjacency[root].indices)
|
|
180
|
+
if out_degree == 0:
|
|
181
|
+
raise ValueError("Invalid root node. The root must have at least one outgoing edge.")
|
|
182
|
+
if isinstance(root, int):
|
|
183
|
+
root = [root]
|
|
184
|
+
|
|
185
|
+
if directed is False:
|
|
186
|
+
# the graph must be undirected
|
|
187
|
+
if not is_symmetric(adjacency):
|
|
188
|
+
raise ValueError("The adjacency matrix is not symmetric. The parameter 'directed' must be True.")
|
|
189
|
+
elif directed is None:
|
|
190
|
+
# if not specified, infer from the graph
|
|
191
|
+
directed = not is_symmetric(adjacency)
|
|
192
|
+
|
|
193
|
+
# break self-loops
|
|
194
|
+
adjacency = (adjacency - sparse.diags(adjacency.diagonal().astype(int), format='csr')).astype(int)
|
|
195
|
+
|
|
196
|
+
if directed:
|
|
197
|
+
# break cycles from the cycle node closest to the root
|
|
198
|
+
_, cc_labels = sparse.csgraph.connected_components(adjacency, directed, connection='strong', return_labels=True)
|
|
199
|
+
labels, counts = np.unique(cc_labels, return_counts=True)
|
|
200
|
+
cycle_labels = labels[counts > 1]
|
|
201
|
+
distances = get_distances(adjacency, source=root)
|
|
202
|
+
|
|
203
|
+
for label in cycle_labels:
|
|
204
|
+
cycle_nodes = np.argwhere(cc_labels == label).ravel()
|
|
205
|
+
roots_ix = np.argwhere(distances[cycle_nodes] == min(distances[cycle_nodes])).ravel()
|
|
206
|
+
subroots = set(cycle_nodes[roots_ix])
|
|
207
|
+
stack = [(subroot, [subroot]) for subroot in subroots]
|
|
208
|
+
# break cycles using depth-first traversal
|
|
209
|
+
while stack:
|
|
210
|
+
current_node, path = stack.pop()
|
|
211
|
+
# check if the edge still exists
|
|
212
|
+
if len(path) > 1 and adjacency[path[-2], path[-1]] <= 0:
|
|
213
|
+
continue
|
|
214
|
+
neighbors = adjacency.indices[adjacency.indptr[current_node]:adjacency.indptr[current_node+1]]
|
|
215
|
+
cycle_neighbors = set(neighbors) & set(cycle_nodes)
|
|
216
|
+
for neighbor in cycle_neighbors:
|
|
217
|
+
if neighbor in path:
|
|
218
|
+
adjacency[current_node, neighbor] = 0
|
|
219
|
+
adjacency.eliminate_zeros()
|
|
220
|
+
else:
|
|
221
|
+
stack.append((neighbor, path + [neighbor]))
|
|
222
|
+
else:
|
|
223
|
+
# break cycles from given roots for undirected graphs
|
|
224
|
+
for start_node in root:
|
|
225
|
+
stack = [(start_node, [start_node])]
|
|
226
|
+
# break cycles using depth-first traversal
|
|
227
|
+
while stack:
|
|
228
|
+
current_node, path = stack.pop()
|
|
229
|
+
# check if the edge still exists
|
|
230
|
+
if len(path) > 1 and adjacency[path[-2], path[-1]] <= 0:
|
|
231
|
+
continue
|
|
232
|
+
neighbors = list(adjacency.indices[adjacency.indptr[current_node]:adjacency.indptr[current_node+1]])
|
|
233
|
+
for neighbor in neighbors:
|
|
234
|
+
if len(path) > 1 and neighbor == path[-2]:
|
|
235
|
+
continue
|
|
236
|
+
if neighbor in path:
|
|
237
|
+
adjacency[current_node, neighbor] = 0
|
|
238
|
+
adjacency[neighbor, current_node] = 0
|
|
239
|
+
adjacency.eliminate_zeros()
|
|
240
|
+
else:
|
|
241
|
+
stack.append((neighbor, path + [neighbor]))
|
|
242
|
+
|
|
243
|
+
return adjacency
|