scikit-network 0.33.0__cp312-cp312-macosx_10_9_x86_64.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.0.dist-info/AUTHORS.rst +43 -0
- scikit_network-0.33.0.dist-info/LICENSE +34 -0
- scikit_network-0.33.0.dist-info/METADATA +517 -0
- scikit_network-0.33.0.dist-info/RECORD +216 -0
- scikit_network-0.33.0.dist-info/WHEEL +5 -0
- scikit_network-0.33.0.dist-info/top_level.txt +1 -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.cpython-312-darwin.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.cpython-312-darwin.so +0 -0
- sknetwork/clustering/leiden_core.pyx +124 -0
- sknetwork/clustering/louvain.py +286 -0
- sknetwork/clustering/louvain_core.cpython-312-darwin.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 +129 -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 +89 -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.cpython-312-darwin.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.cpython-312-darwin.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.cpython-312-darwin.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.cpython-312-darwin.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.cpython-312-darwin.so +0 -0
- sknetwork/topology/cliques.pyx +149 -0
- sknetwork/topology/core.cpython-312-darwin.so +0 -0
- sknetwork/topology/core.pyx +90 -0
- sknetwork/topology/cycles.py +243 -0
- sknetwork/topology/minheap.cpython-312-darwin.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.cpython-312-darwin.so +0 -0
- sknetwork/topology/triangles.pyx +151 -0
- sknetwork/topology/weisfeiler_lehman.py +133 -0
- sknetwork/topology/weisfeiler_lehman_core.cpython-312-darwin.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
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""tests for diffusion.py"""
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from sknetwork.data.test_graphs import *
|
|
8
|
+
from sknetwork.regression import Diffusion, Dirichlet
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# noinspection DuplicatedCode
|
|
12
|
+
class TestDiffusion(unittest.TestCase):
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
self.algos = [Diffusion(), Dirichlet()]
|
|
16
|
+
|
|
17
|
+
def test_predict(self):
|
|
18
|
+
adjacency = test_graph()
|
|
19
|
+
for algo in self.algos:
|
|
20
|
+
values = algo.fit_predict(adjacency, {0: 0, 1: 1, 2: 0.5})
|
|
21
|
+
values_ = algo.predict()
|
|
22
|
+
self.assertAlmostEqual(np.linalg.norm(values - values_), 0)
|
|
23
|
+
|
|
24
|
+
def test_no_iter(self):
|
|
25
|
+
with self.assertRaises(ValueError):
|
|
26
|
+
Diffusion(n_iter=-1)
|
|
27
|
+
|
|
28
|
+
def test_single_node_graph(self):
|
|
29
|
+
for algo in self.algos:
|
|
30
|
+
algo.fit(sparse.identity(1, format='csr'), {0: 1})
|
|
31
|
+
self.assertEqual(algo.values_, [1])
|
|
32
|
+
|
|
33
|
+
def test_range(self):
|
|
34
|
+
for adjacency in [test_graph(), test_digraph()]:
|
|
35
|
+
for algo in self.algos:
|
|
36
|
+
values = algo.fit_predict(adjacency, {0: 0, 1: 1, 2: 0.5})
|
|
37
|
+
self.assertTrue(np.all(values <= 1) and np.all(values >= 0))
|
|
38
|
+
|
|
39
|
+
biadjacency = test_bigraph()
|
|
40
|
+
for algo in [Diffusion(), Dirichlet()]:
|
|
41
|
+
values = algo.fit_predict(biadjacency, values_row={0: 1})
|
|
42
|
+
self.assertTrue(np.all(values <= 1) and np.all(values >= 0))
|
|
43
|
+
values = algo.fit_predict(biadjacency, values_row={0: 0.1}, values_col={1: 2}, init=0.3)
|
|
44
|
+
self.assertTrue(np.all(values <= 2) and np.all(values >= 0.1))
|
|
45
|
+
self.assertAlmostEqual(np.linalg.norm(algo.values_col_ - algo.predict(columns=True)), 0)
|
|
46
|
+
|
|
47
|
+
def test_initial_state(self):
|
|
48
|
+
for adjacency in [test_graph(), test_digraph()]:
|
|
49
|
+
for algo in self.algos:
|
|
50
|
+
values = algo.fit_predict(adjacency, {0: 0, 1: 1, 2: 0.5}, 0.3)
|
|
51
|
+
self.assertTrue(np.all(values <= 1) and np.all(values >= 0))
|
|
52
|
+
|
|
53
|
+
def test_n_iter(self):
|
|
54
|
+
with self.assertRaises(ValueError):
|
|
55
|
+
Dirichlet(n_iter=0)
|
|
56
|
+
|
sknetwork/sknetwork.py
ADDED
sknetwork/test_base.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""tests for base.py"""
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
from sknetwork.base import Algorithm
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestBase(unittest.TestCase):
|
|
10
|
+
|
|
11
|
+
def setUp(self):
|
|
12
|
+
class NewAlgo(Algorithm):
|
|
13
|
+
"""Docstring"""
|
|
14
|
+
def __init__(self, param: int, name: str):
|
|
15
|
+
self.param = param
|
|
16
|
+
self.name = name
|
|
17
|
+
|
|
18
|
+
def fit(self):
|
|
19
|
+
"""Docstring"""
|
|
20
|
+
pass
|
|
21
|
+
self.algo = NewAlgo(1, 'abc')
|
|
22
|
+
|
|
23
|
+
def test_repr(self):
|
|
24
|
+
self.assertEqual(repr(self.algo), "NewAlgo(param=1, name='abc')")
|
|
25
|
+
|
|
26
|
+
def test_get_params(self):
|
|
27
|
+
self.assertEqual(len(self.algo.get_params()), 2)
|
|
28
|
+
|
|
29
|
+
def test_set_params(self):
|
|
30
|
+
self.algo.set_params({'param': 3})
|
|
31
|
+
self.assertEqual(self.algo.param, 3)
|
|
32
|
+
|
|
33
|
+
def test_fit(self):
|
|
34
|
+
stub = Algorithm()
|
|
35
|
+
self.assertRaises(NotImplementedError, stub.fit, None)
|
sknetwork/test_log.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""tests for verbose.py"""
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from sknetwork.log import Log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TestVerbose(unittest.TestCase):
|
|
11
|
+
|
|
12
|
+
def test_prints(self):
|
|
13
|
+
logger = Log(verbose=True)
|
|
14
|
+
logger.print_log('Hello', 42)
|
|
15
|
+
self.assertEqual(str(logger.log), 'Hello 42\n')
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Module on topology."""
|
|
2
|
+
from sknetwork.topology.cliques import count_cliques
|
|
3
|
+
from sknetwork.topology.core import get_core_decomposition
|
|
4
|
+
from sknetwork.topology.triangles import count_triangles, get_clustering_coefficient
|
|
5
|
+
from sknetwork.topology.structure import is_connected, is_bipartite, is_symmetric, get_connected_components, \
|
|
6
|
+
get_largest_connected_component
|
|
7
|
+
from sknetwork.topology.cycles import is_acyclic, get_cycles, break_cycles
|
|
8
|
+
from sknetwork.topology.weisfeiler_lehman import color_weisfeiler_lehman, are_isomorphic
|
|
Binary file
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
from libcpp.vector cimport vector
|
|
9
|
+
import numpy as np
|
|
10
|
+
cimport numpy as np
|
|
11
|
+
from scipy import sparse
|
|
12
|
+
|
|
13
|
+
cimport cython
|
|
14
|
+
|
|
15
|
+
from sknetwork.path.dag import get_dag
|
|
16
|
+
from sknetwork.topology.core import get_core_decomposition
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@cython.boundscheck(False)
|
|
20
|
+
@cython.wraparound(False)
|
|
21
|
+
cdef class ListingBox:
|
|
22
|
+
cdef int[:] ns
|
|
23
|
+
cdef np.ndarray degrees
|
|
24
|
+
cdef np.ndarray subs
|
|
25
|
+
cdef short[:] lab
|
|
26
|
+
|
|
27
|
+
def __cinit__(self, vector[int] indptr, int k):
|
|
28
|
+
cdef int n = indptr.size() - 1
|
|
29
|
+
cdef int i
|
|
30
|
+
cdef int max_deg = 0
|
|
31
|
+
|
|
32
|
+
cdef np.ndarray[int, ndim=1] ns = np.empty((k+1,), dtype=np.int32)
|
|
33
|
+
ns[k] = n
|
|
34
|
+
self.ns = ns
|
|
35
|
+
|
|
36
|
+
cdef np.ndarray[short, ndim=1] lab = np.full((n,), k, dtype=np.int16)
|
|
37
|
+
self.lab = lab
|
|
38
|
+
|
|
39
|
+
cdef np.ndarray[int, ndim=1] deg = np.zeros(n, dtype=np.int32)
|
|
40
|
+
cdef np.ndarray[int, ndim=1] sub = np.zeros(n, dtype=np.int32)
|
|
41
|
+
|
|
42
|
+
for i in range(n):
|
|
43
|
+
deg[i] = indptr[i+1] - indptr[i]
|
|
44
|
+
max_deg = max(deg[i], max_deg)
|
|
45
|
+
sub[i] = i
|
|
46
|
+
|
|
47
|
+
self.degrees = np.empty((k+1,), dtype=object)
|
|
48
|
+
self.subs = np.empty((k+1,), dtype=object)
|
|
49
|
+
|
|
50
|
+
self.degrees[k] = deg
|
|
51
|
+
self.subs[k] = sub
|
|
52
|
+
|
|
53
|
+
for i in range(2, k):
|
|
54
|
+
deg = np.zeros(n, dtype=np.int32)
|
|
55
|
+
sub = np.zeros(max_deg, dtype=np.int32)
|
|
56
|
+
self.degrees[i] = deg
|
|
57
|
+
self.subs[i] = sub
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@cython.boundscheck(False)
|
|
61
|
+
@cython.wraparound(False)
|
|
62
|
+
cdef long count_cliques_from_dag(vector[int] indptr, vector[int] indices, int clique_size, ListingBox box):
|
|
63
|
+
cdef int n = indptr.size() - 1
|
|
64
|
+
cdef long n_cliques = 0
|
|
65
|
+
cdef int i, j, k, k_max
|
|
66
|
+
cdef int u, v, w
|
|
67
|
+
|
|
68
|
+
if clique_size == 2:
|
|
69
|
+
degree_ = box.degrees[2]
|
|
70
|
+
sub_ = box.subs[2]
|
|
71
|
+
for i in range(box.ns[2]):
|
|
72
|
+
j = sub_[i]
|
|
73
|
+
n_cliques += degree_[j]
|
|
74
|
+
return n_cliques
|
|
75
|
+
|
|
76
|
+
sub_ = box.subs[clique_size]
|
|
77
|
+
sub_prev = box.subs[clique_size - 1]
|
|
78
|
+
degree_ = box.degrees[clique_size]
|
|
79
|
+
deg_prev = box.degrees[clique_size - 1]
|
|
80
|
+
for i in range(box.ns[clique_size]):
|
|
81
|
+
u = sub_[i]
|
|
82
|
+
box.ns[clique_size - 1] = 0
|
|
83
|
+
for j in range(indptr[u], indptr[u] + degree_[u]):
|
|
84
|
+
v = indices[j]
|
|
85
|
+
if box.lab[v] == clique_size:
|
|
86
|
+
box.lab[v] = clique_size - 1
|
|
87
|
+
sub_prev[box.ns[clique_size - 1]] = v
|
|
88
|
+
box.ns[clique_size - 1] += 1
|
|
89
|
+
deg_prev[v] = 0
|
|
90
|
+
for j in range(box.ns[clique_size - 1]):
|
|
91
|
+
v = sub_prev[j]
|
|
92
|
+
k = indptr[v]
|
|
93
|
+
k_max = indptr[v] + degree_[v]
|
|
94
|
+
while k < k_max:
|
|
95
|
+
w = indices[k]
|
|
96
|
+
if box.lab[w] == clique_size - 1:
|
|
97
|
+
deg_prev[v] += 1
|
|
98
|
+
else:
|
|
99
|
+
k_max -= 1
|
|
100
|
+
indices[k] = indices[k_max]
|
|
101
|
+
k -= 1
|
|
102
|
+
indices[k_max] = w
|
|
103
|
+
k += 1
|
|
104
|
+
n_cliques += count_cliques_from_dag(indptr, indices, clique_size - 1, box)
|
|
105
|
+
for j in range(box.ns[clique_size - 1]):
|
|
106
|
+
v = sub_prev[j]
|
|
107
|
+
box.lab[v] = clique_size
|
|
108
|
+
return n_cliques
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def count_cliques(adjacency: sparse.csr_matrix, clique_size: int = 3) -> int:
|
|
112
|
+
"""Count the number of cliques of some size.
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
adjacency :
|
|
117
|
+
Adjacency matrix of the graph.
|
|
118
|
+
clique_size : int
|
|
119
|
+
Clique size (default = 3, corresponding to triangles.
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
n_cliques : int
|
|
124
|
+
Number of cliques.
|
|
125
|
+
|
|
126
|
+
Example
|
|
127
|
+
-------
|
|
128
|
+
>>> from sknetwork.data import karate_club
|
|
129
|
+
>>> adjacency = karate_club()
|
|
130
|
+
>>> count_cliques(adjacency, 3)
|
|
131
|
+
45
|
|
132
|
+
|
|
133
|
+
References
|
|
134
|
+
----------
|
|
135
|
+
Danisch, M., Balalau, O., & Sozio, M. (2018, April).
|
|
136
|
+
`Listing k-cliques in sparse real-world graphs.
|
|
137
|
+
<https://dl.acm.org/doi/pdf/10.1145/3178876.3186125>`_
|
|
138
|
+
In Proceedings of the 2018 World Wide Web Conference (pp. 589-598).
|
|
139
|
+
"""
|
|
140
|
+
if clique_size < 2:
|
|
141
|
+
raise ValueError("The clique size must be at least 2.")
|
|
142
|
+
|
|
143
|
+
values = get_core_decomposition(adjacency)
|
|
144
|
+
dag = get_dag(adjacency, order=np.argsort(values))
|
|
145
|
+
indptr = dag.indptr
|
|
146
|
+
indices = dag.indices
|
|
147
|
+
box = ListingBox.__new__(ListingBox, indptr, clique_size)
|
|
148
|
+
n_cliques = count_cliques_from_dag(indptr, indices, clique_size, box)
|
|
149
|
+
return n_cliques
|
|
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
|
+
>>> get_cycles(graph.adjacency, directed=True)
|
|
82
|
+
[[0, 1, 2, 3]]
|
|
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
|
|
Binary file
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
from libcpp.vector cimport vector
|
|
9
|
+
|
|
10
|
+
cdef class MinHeap:
|
|
11
|
+
|
|
12
|
+
cdef vector[int] val, pos
|
|
13
|
+
cdef int size
|
|
14
|
+
|
|
15
|
+
cdef int pop_min(self, int[:] scores)
|
|
16
|
+
cdef bint empty(self)
|
|
17
|
+
cdef void swap(self, int x, int y)
|
|
18
|
+
cdef void insert_key(self, int k, int[:] scores)
|
|
19
|
+
cdef void decrease_key(self, int i, int[:] scores)
|
|
20
|
+
cdef void min_heapify(self, int i, int[:] scores)
|