scikit-network 0.33.3__cp39-cp39-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.

Files changed (229) hide show
  1. scikit_network-0.33.3.dist-info/METADATA +122 -0
  2. scikit_network-0.33.3.dist-info/RECORD +229 -0
  3. scikit_network-0.33.3.dist-info/WHEEL +6 -0
  4. scikit_network-0.33.3.dist-info/licenses/AUTHORS.rst +43 -0
  5. scikit_network-0.33.3.dist-info/licenses/LICENSE +34 -0
  6. scikit_network-0.33.3.dist-info/top_level.txt +1 -0
  7. scikit_network.libs/libgomp-d22c30c5.so.1.0.0 +0 -0
  8. sknetwork/__init__.py +21 -0
  9. sknetwork/base.py +67 -0
  10. sknetwork/classification/__init__.py +8 -0
  11. sknetwork/classification/base.py +142 -0
  12. sknetwork/classification/base_rank.py +133 -0
  13. sknetwork/classification/diffusion.py +134 -0
  14. sknetwork/classification/knn.py +139 -0
  15. sknetwork/classification/metrics.py +205 -0
  16. sknetwork/classification/pagerank.py +66 -0
  17. sknetwork/classification/propagation.py +152 -0
  18. sknetwork/classification/tests/__init__.py +1 -0
  19. sknetwork/classification/tests/test_API.py +30 -0
  20. sknetwork/classification/tests/test_diffusion.py +77 -0
  21. sknetwork/classification/tests/test_knn.py +23 -0
  22. sknetwork/classification/tests/test_metrics.py +53 -0
  23. sknetwork/classification/tests/test_pagerank.py +20 -0
  24. sknetwork/classification/tests/test_propagation.py +24 -0
  25. sknetwork/classification/vote.cpp +27587 -0
  26. sknetwork/classification/vote.cpython-39-aarch64-linux-gnu.so +0 -0
  27. sknetwork/classification/vote.pyx +56 -0
  28. sknetwork/clustering/__init__.py +8 -0
  29. sknetwork/clustering/base.py +172 -0
  30. sknetwork/clustering/kcenters.py +253 -0
  31. sknetwork/clustering/leiden.py +242 -0
  32. sknetwork/clustering/leiden_core.cpp +31578 -0
  33. sknetwork/clustering/leiden_core.cpython-39-aarch64-linux-gnu.so +0 -0
  34. sknetwork/clustering/leiden_core.pyx +124 -0
  35. sknetwork/clustering/louvain.py +286 -0
  36. sknetwork/clustering/louvain_core.cpp +31223 -0
  37. sknetwork/clustering/louvain_core.cpython-39-aarch64-linux-gnu.so +0 -0
  38. sknetwork/clustering/louvain_core.pyx +124 -0
  39. sknetwork/clustering/metrics.py +91 -0
  40. sknetwork/clustering/postprocess.py +66 -0
  41. sknetwork/clustering/propagation_clustering.py +104 -0
  42. sknetwork/clustering/tests/__init__.py +1 -0
  43. sknetwork/clustering/tests/test_API.py +38 -0
  44. sknetwork/clustering/tests/test_kcenters.py +60 -0
  45. sknetwork/clustering/tests/test_leiden.py +34 -0
  46. sknetwork/clustering/tests/test_louvain.py +135 -0
  47. sknetwork/clustering/tests/test_metrics.py +50 -0
  48. sknetwork/clustering/tests/test_postprocess.py +39 -0
  49. sknetwork/data/__init__.py +6 -0
  50. sknetwork/data/base.py +33 -0
  51. sknetwork/data/load.py +406 -0
  52. sknetwork/data/models.py +459 -0
  53. sknetwork/data/parse.py +644 -0
  54. sknetwork/data/test_graphs.py +84 -0
  55. sknetwork/data/tests/__init__.py +1 -0
  56. sknetwork/data/tests/test_API.py +30 -0
  57. sknetwork/data/tests/test_base.py +14 -0
  58. sknetwork/data/tests/test_load.py +95 -0
  59. sknetwork/data/tests/test_models.py +52 -0
  60. sknetwork/data/tests/test_parse.py +250 -0
  61. sknetwork/data/tests/test_test_graphs.py +29 -0
  62. sknetwork/data/tests/test_toy_graphs.py +68 -0
  63. sknetwork/data/timeout.py +38 -0
  64. sknetwork/data/toy_graphs.py +611 -0
  65. sknetwork/embedding/__init__.py +8 -0
  66. sknetwork/embedding/base.py +94 -0
  67. sknetwork/embedding/force_atlas.py +198 -0
  68. sknetwork/embedding/louvain_embedding.py +148 -0
  69. sknetwork/embedding/random_projection.py +135 -0
  70. sknetwork/embedding/spectral.py +141 -0
  71. sknetwork/embedding/spring.py +198 -0
  72. sknetwork/embedding/svd.py +359 -0
  73. sknetwork/embedding/tests/__init__.py +1 -0
  74. sknetwork/embedding/tests/test_API.py +49 -0
  75. sknetwork/embedding/tests/test_force_atlas.py +35 -0
  76. sknetwork/embedding/tests/test_louvain_embedding.py +33 -0
  77. sknetwork/embedding/tests/test_random_projection.py +28 -0
  78. sknetwork/embedding/tests/test_spectral.py +81 -0
  79. sknetwork/embedding/tests/test_spring.py +50 -0
  80. sknetwork/embedding/tests/test_svd.py +43 -0
  81. sknetwork/gnn/__init__.py +10 -0
  82. sknetwork/gnn/activation.py +117 -0
  83. sknetwork/gnn/base.py +181 -0
  84. sknetwork/gnn/base_activation.py +90 -0
  85. sknetwork/gnn/base_layer.py +109 -0
  86. sknetwork/gnn/gnn_classifier.py +305 -0
  87. sknetwork/gnn/layer.py +153 -0
  88. sknetwork/gnn/loss.py +180 -0
  89. sknetwork/gnn/neighbor_sampler.py +65 -0
  90. sknetwork/gnn/optimizer.py +164 -0
  91. sknetwork/gnn/tests/__init__.py +1 -0
  92. sknetwork/gnn/tests/test_activation.py +56 -0
  93. sknetwork/gnn/tests/test_base.py +75 -0
  94. sknetwork/gnn/tests/test_base_layer.py +37 -0
  95. sknetwork/gnn/tests/test_gnn_classifier.py +130 -0
  96. sknetwork/gnn/tests/test_layers.py +80 -0
  97. sknetwork/gnn/tests/test_loss.py +33 -0
  98. sknetwork/gnn/tests/test_neigh_sampler.py +23 -0
  99. sknetwork/gnn/tests/test_optimizer.py +43 -0
  100. sknetwork/gnn/tests/test_utils.py +41 -0
  101. sknetwork/gnn/utils.py +127 -0
  102. sknetwork/hierarchy/__init__.py +6 -0
  103. sknetwork/hierarchy/base.py +96 -0
  104. sknetwork/hierarchy/louvain_hierarchy.py +272 -0
  105. sknetwork/hierarchy/metrics.py +234 -0
  106. sknetwork/hierarchy/paris.cpp +37889 -0
  107. sknetwork/hierarchy/paris.cpython-39-aarch64-linux-gnu.so +0 -0
  108. sknetwork/hierarchy/paris.pyx +316 -0
  109. sknetwork/hierarchy/postprocess.py +350 -0
  110. sknetwork/hierarchy/tests/__init__.py +1 -0
  111. sknetwork/hierarchy/tests/test_API.py +24 -0
  112. sknetwork/hierarchy/tests/test_algos.py +34 -0
  113. sknetwork/hierarchy/tests/test_metrics.py +62 -0
  114. sknetwork/hierarchy/tests/test_postprocess.py +57 -0
  115. sknetwork/linalg/__init__.py +9 -0
  116. sknetwork/linalg/basics.py +37 -0
  117. sknetwork/linalg/diteration.cpp +27403 -0
  118. sknetwork/linalg/diteration.cpython-39-aarch64-linux-gnu.so +0 -0
  119. sknetwork/linalg/diteration.pyx +47 -0
  120. sknetwork/linalg/eig_solver.py +93 -0
  121. sknetwork/linalg/laplacian.py +15 -0
  122. sknetwork/linalg/normalizer.py +86 -0
  123. sknetwork/linalg/operators.py +225 -0
  124. sknetwork/linalg/polynome.py +76 -0
  125. sknetwork/linalg/ppr_solver.py +170 -0
  126. sknetwork/linalg/push.cpp +31093 -0
  127. sknetwork/linalg/push.cpython-39-aarch64-linux-gnu.so +0 -0
  128. sknetwork/linalg/push.pyx +71 -0
  129. sknetwork/linalg/sparse_lowrank.py +142 -0
  130. sknetwork/linalg/svd_solver.py +91 -0
  131. sknetwork/linalg/tests/__init__.py +1 -0
  132. sknetwork/linalg/tests/test_eig.py +44 -0
  133. sknetwork/linalg/tests/test_laplacian.py +18 -0
  134. sknetwork/linalg/tests/test_normalization.py +34 -0
  135. sknetwork/linalg/tests/test_operators.py +66 -0
  136. sknetwork/linalg/tests/test_polynome.py +38 -0
  137. sknetwork/linalg/tests/test_ppr.py +50 -0
  138. sknetwork/linalg/tests/test_sparse_lowrank.py +61 -0
  139. sknetwork/linalg/tests/test_svd.py +38 -0
  140. sknetwork/linkpred/__init__.py +2 -0
  141. sknetwork/linkpred/base.py +46 -0
  142. sknetwork/linkpred/nn.py +126 -0
  143. sknetwork/linkpred/tests/__init__.py +1 -0
  144. sknetwork/linkpred/tests/test_nn.py +27 -0
  145. sknetwork/log.py +19 -0
  146. sknetwork/path/__init__.py +5 -0
  147. sknetwork/path/dag.py +54 -0
  148. sknetwork/path/distances.py +98 -0
  149. sknetwork/path/search.py +31 -0
  150. sknetwork/path/shortest_path.py +61 -0
  151. sknetwork/path/tests/__init__.py +1 -0
  152. sknetwork/path/tests/test_dag.py +37 -0
  153. sknetwork/path/tests/test_distances.py +62 -0
  154. sknetwork/path/tests/test_search.py +40 -0
  155. sknetwork/path/tests/test_shortest_path.py +40 -0
  156. sknetwork/ranking/__init__.py +8 -0
  157. sknetwork/ranking/base.py +61 -0
  158. sknetwork/ranking/betweenness.cpp +9710 -0
  159. sknetwork/ranking/betweenness.cpython-39-aarch64-linux-gnu.so +0 -0
  160. sknetwork/ranking/betweenness.pyx +97 -0
  161. sknetwork/ranking/closeness.py +92 -0
  162. sknetwork/ranking/hits.py +94 -0
  163. sknetwork/ranking/katz.py +83 -0
  164. sknetwork/ranking/pagerank.py +110 -0
  165. sknetwork/ranking/postprocess.py +37 -0
  166. sknetwork/ranking/tests/__init__.py +1 -0
  167. sknetwork/ranking/tests/test_API.py +32 -0
  168. sknetwork/ranking/tests/test_betweenness.py +38 -0
  169. sknetwork/ranking/tests/test_closeness.py +30 -0
  170. sknetwork/ranking/tests/test_hits.py +20 -0
  171. sknetwork/ranking/tests/test_pagerank.py +62 -0
  172. sknetwork/ranking/tests/test_postprocess.py +26 -0
  173. sknetwork/regression/__init__.py +4 -0
  174. sknetwork/regression/base.py +61 -0
  175. sknetwork/regression/diffusion.py +210 -0
  176. sknetwork/regression/tests/__init__.py +1 -0
  177. sknetwork/regression/tests/test_API.py +32 -0
  178. sknetwork/regression/tests/test_diffusion.py +56 -0
  179. sknetwork/sknetwork.py +3 -0
  180. sknetwork/test_base.py +35 -0
  181. sknetwork/test_log.py +15 -0
  182. sknetwork/topology/__init__.py +8 -0
  183. sknetwork/topology/cliques.cpp +32586 -0
  184. sknetwork/topology/cliques.cpython-39-aarch64-linux-gnu.so +0 -0
  185. sknetwork/topology/cliques.pyx +149 -0
  186. sknetwork/topology/core.cpp +30672 -0
  187. sknetwork/topology/core.cpython-39-aarch64-linux-gnu.so +0 -0
  188. sknetwork/topology/core.pyx +90 -0
  189. sknetwork/topology/cycles.py +243 -0
  190. sknetwork/topology/minheap.cpp +27335 -0
  191. sknetwork/topology/minheap.cpython-39-aarch64-linux-gnu.so +0 -0
  192. sknetwork/topology/minheap.pxd +20 -0
  193. sknetwork/topology/minheap.pyx +109 -0
  194. sknetwork/topology/structure.py +194 -0
  195. sknetwork/topology/tests/__init__.py +1 -0
  196. sknetwork/topology/tests/test_cliques.py +28 -0
  197. sknetwork/topology/tests/test_core.py +19 -0
  198. sknetwork/topology/tests/test_cycles.py +65 -0
  199. sknetwork/topology/tests/test_structure.py +85 -0
  200. sknetwork/topology/tests/test_triangles.py +38 -0
  201. sknetwork/topology/tests/test_wl.py +72 -0
  202. sknetwork/topology/triangles.cpp +8897 -0
  203. sknetwork/topology/triangles.cpython-39-aarch64-linux-gnu.so +0 -0
  204. sknetwork/topology/triangles.pyx +151 -0
  205. sknetwork/topology/weisfeiler_lehman.py +133 -0
  206. sknetwork/topology/weisfeiler_lehman_core.cpp +27638 -0
  207. sknetwork/topology/weisfeiler_lehman_core.cpython-39-aarch64-linux-gnu.so +0 -0
  208. sknetwork/topology/weisfeiler_lehman_core.pyx +114 -0
  209. sknetwork/utils/__init__.py +7 -0
  210. sknetwork/utils/check.py +355 -0
  211. sknetwork/utils/format.py +221 -0
  212. sknetwork/utils/membership.py +82 -0
  213. sknetwork/utils/neighbors.py +115 -0
  214. sknetwork/utils/tests/__init__.py +1 -0
  215. sknetwork/utils/tests/test_check.py +190 -0
  216. sknetwork/utils/tests/test_format.py +63 -0
  217. sknetwork/utils/tests/test_membership.py +24 -0
  218. sknetwork/utils/tests/test_neighbors.py +41 -0
  219. sknetwork/utils/tests/test_tfidf.py +18 -0
  220. sknetwork/utils/tests/test_values.py +66 -0
  221. sknetwork/utils/tfidf.py +37 -0
  222. sknetwork/utils/values.py +76 -0
  223. sknetwork/visualization/__init__.py +4 -0
  224. sknetwork/visualization/colors.py +34 -0
  225. sknetwork/visualization/dendrograms.py +277 -0
  226. sknetwork/visualization/graphs.py +1039 -0
  227. sknetwork/visualization/tests/__init__.py +1 -0
  228. sknetwork/visualization/tests/test_dendrograms.py +53 -0
  229. sknetwork/visualization/tests/test_graphs.py +176 -0
@@ -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