fast-hdbscan 0.1.3__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.
- fast_hdbscan-0.1.3/LICENSE +24 -0
- fast_hdbscan-0.1.3/PKG-INFO +122 -0
- fast_hdbscan-0.1.3/README.rst +103 -0
- fast_hdbscan-0.1.3/fast_hdbscan/__init__.py +10 -0
- fast_hdbscan-0.1.3/fast_hdbscan/boruvka.py +277 -0
- fast_hdbscan-0.1.3/fast_hdbscan/cluster_trees.py +457 -0
- fast_hdbscan-0.1.3/fast_hdbscan/disjoint_set.py +56 -0
- fast_hdbscan-0.1.3/fast_hdbscan/hdbscan.py +302 -0
- fast_hdbscan-0.1.3/fast_hdbscan/numba_kdtree.py +220 -0
- fast_hdbscan-0.1.3/fast_hdbscan.egg-info/PKG-INFO +122 -0
- fast_hdbscan-0.1.3/fast_hdbscan.egg-info/SOURCES.txt +17 -0
- fast_hdbscan-0.1.3/fast_hdbscan.egg-info/dependency_links.txt +1 -0
- fast_hdbscan-0.1.3/fast_hdbscan.egg-info/not-zip-safe +1 -0
- fast_hdbscan-0.1.3/fast_hdbscan.egg-info/requires.txt +3 -0
- fast_hdbscan-0.1.3/fast_hdbscan.egg-info/top_level.txt +1 -0
- fast_hdbscan-0.1.3/pyproject.toml +3 -0
- fast_hdbscan-0.1.3/setup.cfg +33 -0
- fast_hdbscan-0.1.3/setup.py +4 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023, Tutte Institute for Mathematics and Computing
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
19
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
20
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
21
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
22
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
23
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
24
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: fast_hdbscan
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: A fast multicore version of hdbscan for low dimensional euclidean spaces
|
|
5
|
+
Home-page: https://github.com/TutteInstitute/fast_hdbscan
|
|
6
|
+
Author: Leland McInnes
|
|
7
|
+
Author-email: leland.mcinnes@gmail.com
|
|
8
|
+
Maintainer: Leland McInnes
|
|
9
|
+
Maintainer-email: leland.mcinnes@gmail.com
|
|
10
|
+
License: BSD 2-Clause License
|
|
11
|
+
Keywords: cluster,clustering,hdbscan,dbscan
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Development Status :: 4 - Beta
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
|
|
20
|
+
.. -*- mode: rst -*-
|
|
21
|
+
|
|
22
|
+
.. image:: doc/hdbscan_logo.png
|
|
23
|
+
:width: 600
|
|
24
|
+
:alt: HDBSCAN logo
|
|
25
|
+
:align: center
|
|
26
|
+
|
|
27
|
+
======================
|
|
28
|
+
Fast Multicore HDBSCAN
|
|
29
|
+
======================
|
|
30
|
+
|
|
31
|
+
Ahe ``fast_hdbscan`` library provides a simple implementation of the HDBSCAN clustering algorithm designed specifically
|
|
32
|
+
for high performance on multicore machine with low dimensional data (2D to about 20D). The algorithm runs in parallel and can make
|
|
33
|
+
effective use of as many cores as you wish to throw at a problem. It is thus ideal for large SMP systems, and even
|
|
34
|
+
modern multicore laptops.
|
|
35
|
+
|
|
36
|
+
This library provides a
|
|
37
|
+
re-implementation of a subset of the HDBSCAN algorithm that is compatible with the
|
|
38
|
+
`hdbscan <https://github.com/scikit-learn-contrib/hdbscan>`_ library for data that is Euclidean and
|
|
39
|
+
low dimensional. The primary advantages of this library over the standard ``hdbscan`` library are:
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
* this library can easily use all available cores to speed up computation;
|
|
43
|
+
* this library has much faster implementations of tree condensing and cluster extraction;
|
|
44
|
+
* this library is much simpler and more approachable for extending or using components from;
|
|
45
|
+
* this library is built on numba and has less issues with binaries and compilation.
|
|
46
|
+
|
|
47
|
+
This library does not support all the features and input formats available in the hdbscan library.
|
|
48
|
+
|
|
49
|
+
-----------
|
|
50
|
+
Basic Usage
|
|
51
|
+
-----------
|
|
52
|
+
|
|
53
|
+
The ``fast_hdbscan`` library follows the ``hdbscan`` library in using the sklearn API. You can use the ``fast_hdbscan``
|
|
54
|
+
class ``HDBSCAN`` exactly as you would that of the ``hdbscan`` library with the caveat that ``fast_hdbscan`` only
|
|
55
|
+
supports a subset of the parameters and options of ``hdbscan``. Nonetheless, if you have low-dimensional
|
|
56
|
+
Euclidean data (e.g. the output of UMAP), you can use this library as a straightforward drop in replacement for
|
|
57
|
+
``hdbscan``:
|
|
58
|
+
|
|
59
|
+
.. code:: python
|
|
60
|
+
|
|
61
|
+
import fast_hdbscan
|
|
62
|
+
from sklearn.datasets import make_blobs
|
|
63
|
+
|
|
64
|
+
data, _ = make_blobs(1000)
|
|
65
|
+
|
|
66
|
+
clusterer = fast_hdbscan.HDBSCAN(min_cluster_size=10)
|
|
67
|
+
cluster_labels = clusterer.fit_predict(data)
|
|
68
|
+
|
|
69
|
+
------------
|
|
70
|
+
Installation
|
|
71
|
+
------------
|
|
72
|
+
fast_hdbscan requires:
|
|
73
|
+
|
|
74
|
+
* numba
|
|
75
|
+
* numpy
|
|
76
|
+
* scikit-learn
|
|
77
|
+
|
|
78
|
+
fast_hdbscan can be installed via pip:
|
|
79
|
+
|
|
80
|
+
.. code:: bash
|
|
81
|
+
|
|
82
|
+
pip install fast_hdbscan
|
|
83
|
+
|
|
84
|
+
To manually install this package:
|
|
85
|
+
|
|
86
|
+
.. code:: bash
|
|
87
|
+
|
|
88
|
+
wget https://github.com/TutteInstitute/fast_hdbscan/archive/main.zip
|
|
89
|
+
unzip main.zip
|
|
90
|
+
rm main.zip
|
|
91
|
+
cd fast_hdbscan-main
|
|
92
|
+
python setup.py install
|
|
93
|
+
|
|
94
|
+
----------
|
|
95
|
+
References
|
|
96
|
+
----------
|
|
97
|
+
|
|
98
|
+
The algorithm used here is an adaptation of the algorithms described in the papers:
|
|
99
|
+
|
|
100
|
+
McInnes L, Healy J. *Accelerated Hierarchical Density Based Clustering*
|
|
101
|
+
In: 2017 IEEE International Conference on Data Mining Workshops (ICDMW), IEEE, pp 33-42.
|
|
102
|
+
2017 `[pdf] <http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8215642>`_
|
|
103
|
+
|
|
104
|
+
R. Campello, D. Moulavi, and J. Sander, *Density-Based Clustering Based on
|
|
105
|
+
Hierarchical Density Estimates*
|
|
106
|
+
In: Advances in Knowledge Discovery and Data Mining, Springer, pp 160-172.
|
|
107
|
+
2013
|
|
108
|
+
|
|
109
|
+
-------
|
|
110
|
+
License
|
|
111
|
+
-------
|
|
112
|
+
|
|
113
|
+
fast_hdbscan is BSD (2-clause) licensed. See the LICENSE file for details.
|
|
114
|
+
|
|
115
|
+
------------
|
|
116
|
+
Contributing
|
|
117
|
+
------------
|
|
118
|
+
|
|
119
|
+
Contributions are more than welcome! If you have ideas for features of projects please get in touch. Everything from
|
|
120
|
+
code to notebooks to examples and documentation are all *equally valuable* so please don't feel you can't contribute.
|
|
121
|
+
To contribute please `fork the project <https://github.com/TutteInstitute/fast_hdbscan/issues#fork-destination-box>`_ make your
|
|
122
|
+
changes and submit a pull request. We will do our best to work through any issues with you and get your code merged in.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
.. -*- mode: rst -*-
|
|
2
|
+
|
|
3
|
+
.. image:: doc/hdbscan_logo.png
|
|
4
|
+
:width: 600
|
|
5
|
+
:alt: HDBSCAN logo
|
|
6
|
+
:align: center
|
|
7
|
+
|
|
8
|
+
======================
|
|
9
|
+
Fast Multicore HDBSCAN
|
|
10
|
+
======================
|
|
11
|
+
|
|
12
|
+
Ahe ``fast_hdbscan`` library provides a simple implementation of the HDBSCAN clustering algorithm designed specifically
|
|
13
|
+
for high performance on multicore machine with low dimensional data (2D to about 20D). The algorithm runs in parallel and can make
|
|
14
|
+
effective use of as many cores as you wish to throw at a problem. It is thus ideal for large SMP systems, and even
|
|
15
|
+
modern multicore laptops.
|
|
16
|
+
|
|
17
|
+
This library provides a
|
|
18
|
+
re-implementation of a subset of the HDBSCAN algorithm that is compatible with the
|
|
19
|
+
`hdbscan <https://github.com/scikit-learn-contrib/hdbscan>`_ library for data that is Euclidean and
|
|
20
|
+
low dimensional. The primary advantages of this library over the standard ``hdbscan`` library are:
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
* this library can easily use all available cores to speed up computation;
|
|
24
|
+
* this library has much faster implementations of tree condensing and cluster extraction;
|
|
25
|
+
* this library is much simpler and more approachable for extending or using components from;
|
|
26
|
+
* this library is built on numba and has less issues with binaries and compilation.
|
|
27
|
+
|
|
28
|
+
This library does not support all the features and input formats available in the hdbscan library.
|
|
29
|
+
|
|
30
|
+
-----------
|
|
31
|
+
Basic Usage
|
|
32
|
+
-----------
|
|
33
|
+
|
|
34
|
+
The ``fast_hdbscan`` library follows the ``hdbscan`` library in using the sklearn API. You can use the ``fast_hdbscan``
|
|
35
|
+
class ``HDBSCAN`` exactly as you would that of the ``hdbscan`` library with the caveat that ``fast_hdbscan`` only
|
|
36
|
+
supports a subset of the parameters and options of ``hdbscan``. Nonetheless, if you have low-dimensional
|
|
37
|
+
Euclidean data (e.g. the output of UMAP), you can use this library as a straightforward drop in replacement for
|
|
38
|
+
``hdbscan``:
|
|
39
|
+
|
|
40
|
+
.. code:: python
|
|
41
|
+
|
|
42
|
+
import fast_hdbscan
|
|
43
|
+
from sklearn.datasets import make_blobs
|
|
44
|
+
|
|
45
|
+
data, _ = make_blobs(1000)
|
|
46
|
+
|
|
47
|
+
clusterer = fast_hdbscan.HDBSCAN(min_cluster_size=10)
|
|
48
|
+
cluster_labels = clusterer.fit_predict(data)
|
|
49
|
+
|
|
50
|
+
------------
|
|
51
|
+
Installation
|
|
52
|
+
------------
|
|
53
|
+
fast_hdbscan requires:
|
|
54
|
+
|
|
55
|
+
* numba
|
|
56
|
+
* numpy
|
|
57
|
+
* scikit-learn
|
|
58
|
+
|
|
59
|
+
fast_hdbscan can be installed via pip:
|
|
60
|
+
|
|
61
|
+
.. code:: bash
|
|
62
|
+
|
|
63
|
+
pip install fast_hdbscan
|
|
64
|
+
|
|
65
|
+
To manually install this package:
|
|
66
|
+
|
|
67
|
+
.. code:: bash
|
|
68
|
+
|
|
69
|
+
wget https://github.com/TutteInstitute/fast_hdbscan/archive/main.zip
|
|
70
|
+
unzip main.zip
|
|
71
|
+
rm main.zip
|
|
72
|
+
cd fast_hdbscan-main
|
|
73
|
+
python setup.py install
|
|
74
|
+
|
|
75
|
+
----------
|
|
76
|
+
References
|
|
77
|
+
----------
|
|
78
|
+
|
|
79
|
+
The algorithm used here is an adaptation of the algorithms described in the papers:
|
|
80
|
+
|
|
81
|
+
McInnes L, Healy J. *Accelerated Hierarchical Density Based Clustering*
|
|
82
|
+
In: 2017 IEEE International Conference on Data Mining Workshops (ICDMW), IEEE, pp 33-42.
|
|
83
|
+
2017 `[pdf] <http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8215642>`_
|
|
84
|
+
|
|
85
|
+
R. Campello, D. Moulavi, and J. Sander, *Density-Based Clustering Based on
|
|
86
|
+
Hierarchical Density Estimates*
|
|
87
|
+
In: Advances in Knowledge Discovery and Data Mining, Springer, pp 160-172.
|
|
88
|
+
2013
|
|
89
|
+
|
|
90
|
+
-------
|
|
91
|
+
License
|
|
92
|
+
-------
|
|
93
|
+
|
|
94
|
+
fast_hdbscan is BSD (2-clause) licensed. See the LICENSE file for details.
|
|
95
|
+
|
|
96
|
+
------------
|
|
97
|
+
Contributing
|
|
98
|
+
------------
|
|
99
|
+
|
|
100
|
+
Contributions are more than welcome! If you have ideas for features of projects please get in touch. Everything from
|
|
101
|
+
code to notebooks to examples and documentation are all *equally valuable* so please don't feel you can't contribute.
|
|
102
|
+
To contribute please `fork the project <https://github.com/TutteInstitute/fast_hdbscan/issues#fork-destination-box>`_ make your
|
|
103
|
+
changes and submit a pull request. We will do our best to work through any issues with you and get your code merged in.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .hdbscan import HDBSCAN, fast_hdbscan
|
|
2
|
+
|
|
3
|
+
# Force JIT compilation on import
|
|
4
|
+
import numpy as np
|
|
5
|
+
random_state = np.random.RandomState(42)
|
|
6
|
+
random_data = random_state.random(size=(100, 3))
|
|
7
|
+
HDBSCAN(allow_single_cluster=True).fit(random_data)
|
|
8
|
+
HDBSCAN(cluster_selection_method="leaf").fit(random_data)
|
|
9
|
+
|
|
10
|
+
__all__ = ["HDBSCAN", "fast_hdbscan"]
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import numba
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from .disjoint_set import ds_rank_create, ds_find, ds_union_by_rank
|
|
5
|
+
from .numba_kdtree import parallel_tree_query, rdist, point_to_node_lower_bound_rdist
|
|
6
|
+
|
|
7
|
+
@numba.njit(locals={"i": numba.types.int64})
|
|
8
|
+
def merge_components(disjoint_set, candidate_neighbors, candidate_neighbor_distances, point_components):
|
|
9
|
+
component_edges = {np.int64(0): (np.int64(0), np.int64(1), np.float32(0.0)) for i in range(0)}
|
|
10
|
+
|
|
11
|
+
# Find the best edges from each component
|
|
12
|
+
for i in range(candidate_neighbors.shape[0]):
|
|
13
|
+
from_component = np.int64(point_components[i])
|
|
14
|
+
if from_component in component_edges:
|
|
15
|
+
if candidate_neighbor_distances[i] < component_edges[from_component][2]:
|
|
16
|
+
component_edges[from_component] = (np.int64(i), np.int64(candidate_neighbors[i]), candidate_neighbor_distances[i])
|
|
17
|
+
else:
|
|
18
|
+
component_edges[from_component] = (np.int64(i), np.int64(candidate_neighbors[i]), candidate_neighbor_distances[i])
|
|
19
|
+
|
|
20
|
+
result = np.empty((len(component_edges), 3), dtype=np.float64)
|
|
21
|
+
result_idx = 0
|
|
22
|
+
|
|
23
|
+
# Add the best edges to the edge set and merge the relevant components
|
|
24
|
+
for edge in component_edges.values():
|
|
25
|
+
from_component = ds_find(disjoint_set, np.int32(edge[0]))
|
|
26
|
+
to_component = ds_find(disjoint_set, np.int32(edge[1]))
|
|
27
|
+
if from_component != to_component:
|
|
28
|
+
result[result_idx] = (np.float64(edge[0]), np.float64(edge[1]), np.float64(edge[2]))
|
|
29
|
+
result_idx += 1
|
|
30
|
+
|
|
31
|
+
ds_union_by_rank(disjoint_set, from_component, to_component)
|
|
32
|
+
|
|
33
|
+
return result[:result_idx]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@numba.njit(parallel=True)
|
|
37
|
+
def update_component_vectors(tree, disjoint_set, node_components, point_components):
|
|
38
|
+
for i in numba.prange(point_components.shape[0]):
|
|
39
|
+
point_components[i] = ds_find(disjoint_set, np.int32(i))
|
|
40
|
+
|
|
41
|
+
for i in range(tree.node_data.shape[0] - 1, -1, -1):
|
|
42
|
+
node_info = tree.node_data[i]
|
|
43
|
+
|
|
44
|
+
# Case 1:
|
|
45
|
+
# If the node is a leaf we need to check that every point
|
|
46
|
+
# in the node is of the same component
|
|
47
|
+
if node_info.is_leaf:
|
|
48
|
+
candidate_component = point_components[tree.idx_array[node_info.idx_start]]
|
|
49
|
+
for j in range(node_info.idx_start + 1, node_info.idx_end):
|
|
50
|
+
idx = tree.idx_array[j]
|
|
51
|
+
if point_components[idx] != candidate_component:
|
|
52
|
+
break
|
|
53
|
+
else:
|
|
54
|
+
node_components[i] = candidate_component
|
|
55
|
+
|
|
56
|
+
# Case 2:
|
|
57
|
+
# If the node is not a leaf we only need to check
|
|
58
|
+
# that both child nodes are in the same component
|
|
59
|
+
else:
|
|
60
|
+
left = 2 * i + 1
|
|
61
|
+
right = left + 1
|
|
62
|
+
|
|
63
|
+
if node_components[left] == node_components[right]:
|
|
64
|
+
node_components[i] = node_components[left]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@numba.njit()
|
|
68
|
+
def component_aware_query_recursion(
|
|
69
|
+
tree,
|
|
70
|
+
node,
|
|
71
|
+
point,
|
|
72
|
+
heap_p,
|
|
73
|
+
heap_i,
|
|
74
|
+
current_core_distance,
|
|
75
|
+
core_distances,
|
|
76
|
+
current_component,
|
|
77
|
+
node_components,
|
|
78
|
+
point_components,
|
|
79
|
+
dist_lower_bound,
|
|
80
|
+
component_nearest_neighbor_dist,
|
|
81
|
+
):
|
|
82
|
+
node_info = tree.node_data[node]
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------
|
|
85
|
+
# Case 1a: query point is outside node radius:
|
|
86
|
+
# trim it from the query
|
|
87
|
+
if dist_lower_bound > heap_p[0]:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
# ------------------------------------------------------------
|
|
91
|
+
# Case 1b: we can't improve on the best distance for this component
|
|
92
|
+
# trim it from the query
|
|
93
|
+
elif dist_lower_bound > component_nearest_neighbor_dist[0] or current_core_distance > \
|
|
94
|
+
component_nearest_neighbor_dist[0]:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
# ------------------------------------------------------------
|
|
98
|
+
# Case 1c: node contains only points in same component as query
|
|
99
|
+
# trim it from the query
|
|
100
|
+
elif node_components[node] == current_component:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------
|
|
104
|
+
# Case 2: this is a leaf node. Update set of nearby points
|
|
105
|
+
elif node_info.is_leaf:
|
|
106
|
+
for i in range(node_info.idx_start, node_info.idx_end):
|
|
107
|
+
idx = tree.idx_array[i]
|
|
108
|
+
if point_components[idx] != current_component and core_distances[idx] < component_nearest_neighbor_dist[0]:
|
|
109
|
+
d = max(rdist(point, tree.data[idx]), current_core_distance, core_distances[idx])
|
|
110
|
+
if d < heap_p[0]:
|
|
111
|
+
heap_p[0] = d
|
|
112
|
+
heap_i[0] = idx
|
|
113
|
+
if d < component_nearest_neighbor_dist[0]:
|
|
114
|
+
component_nearest_neighbor_dist[0] = d
|
|
115
|
+
|
|
116
|
+
# ------------------------------------------------------------
|
|
117
|
+
# Case 3: Node is not a leaf. Recursively query subnodes
|
|
118
|
+
# starting with the closest
|
|
119
|
+
else:
|
|
120
|
+
left = 2 * node + 1
|
|
121
|
+
right = left + 1
|
|
122
|
+
dist_lower_bound_left = point_to_node_lower_bound_rdist(tree.node_bounds[0, left], tree.node_bounds[1, left],
|
|
123
|
+
point)
|
|
124
|
+
dist_lower_bound_right = point_to_node_lower_bound_rdist(tree.node_bounds[0, right], tree.node_bounds[1, right],
|
|
125
|
+
point)
|
|
126
|
+
|
|
127
|
+
# recursively query subnodes
|
|
128
|
+
if dist_lower_bound_left <= dist_lower_bound_right:
|
|
129
|
+
component_aware_query_recursion(
|
|
130
|
+
tree,
|
|
131
|
+
left,
|
|
132
|
+
point,
|
|
133
|
+
heap_p,
|
|
134
|
+
heap_i,
|
|
135
|
+
current_core_distance,
|
|
136
|
+
core_distances,
|
|
137
|
+
current_component,
|
|
138
|
+
node_components,
|
|
139
|
+
point_components,
|
|
140
|
+
dist_lower_bound_left,
|
|
141
|
+
component_nearest_neighbor_dist
|
|
142
|
+
)
|
|
143
|
+
component_aware_query_recursion(
|
|
144
|
+
tree,
|
|
145
|
+
right,
|
|
146
|
+
point,
|
|
147
|
+
heap_p,
|
|
148
|
+
heap_i,
|
|
149
|
+
current_core_distance,
|
|
150
|
+
core_distances,
|
|
151
|
+
current_component,
|
|
152
|
+
node_components,
|
|
153
|
+
point_components,
|
|
154
|
+
dist_lower_bound_right,
|
|
155
|
+
component_nearest_neighbor_dist
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
component_aware_query_recursion(
|
|
159
|
+
tree,
|
|
160
|
+
right,
|
|
161
|
+
point,
|
|
162
|
+
heap_p,
|
|
163
|
+
heap_i,
|
|
164
|
+
current_core_distance,
|
|
165
|
+
core_distances,
|
|
166
|
+
current_component,
|
|
167
|
+
node_components,
|
|
168
|
+
point_components,
|
|
169
|
+
dist_lower_bound_right,
|
|
170
|
+
component_nearest_neighbor_dist
|
|
171
|
+
)
|
|
172
|
+
component_aware_query_recursion(
|
|
173
|
+
tree,
|
|
174
|
+
left,
|
|
175
|
+
point,
|
|
176
|
+
heap_p,
|
|
177
|
+
heap_i,
|
|
178
|
+
current_core_distance,
|
|
179
|
+
core_distances,
|
|
180
|
+
current_component,
|
|
181
|
+
node_components,
|
|
182
|
+
point_components,
|
|
183
|
+
dist_lower_bound_left,
|
|
184
|
+
component_nearest_neighbor_dist
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@numba.njit(parallel=True)
|
|
191
|
+
def boruvka_tree_query(tree, node_components, point_components, core_distances):
|
|
192
|
+
candidate_distances = np.full(tree.data.shape[0], np.inf, dtype=np.float32)
|
|
193
|
+
candidate_indices = np.full(tree.data.shape[0], -1, dtype=np.int32)
|
|
194
|
+
component_nearest_neighbor_dist = np.full(tree.data.shape[0], np.inf, dtype=np.float32)
|
|
195
|
+
|
|
196
|
+
data = np.asarray(tree.data.astype(np.float32))
|
|
197
|
+
|
|
198
|
+
for i in numba.prange(tree.data.shape[0]):
|
|
199
|
+
distance_lower_bound = point_to_node_lower_bound_rdist(tree.node_bounds[0, 0], tree.node_bounds[1, 0],
|
|
200
|
+
tree.data[i])
|
|
201
|
+
heap_p, heap_i = candidate_distances[i:i + 1], candidate_indices[i:i + 1]
|
|
202
|
+
component_aware_query_recursion(
|
|
203
|
+
tree,
|
|
204
|
+
0,
|
|
205
|
+
data[i],
|
|
206
|
+
heap_p,
|
|
207
|
+
heap_i,
|
|
208
|
+
core_distances[i],
|
|
209
|
+
core_distances,
|
|
210
|
+
point_components[i],
|
|
211
|
+
node_components,
|
|
212
|
+
point_components,
|
|
213
|
+
distance_lower_bound,
|
|
214
|
+
component_nearest_neighbor_dist[point_components[i]:point_components[i] + 1]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return candidate_distances, candidate_indices
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@numba.njit(parallel=True)
|
|
221
|
+
def initialize_boruvka_from_knn(knn_indices, knn_distances, core_distances, disjoint_set):
|
|
222
|
+
# component_edges = {0:(np.int32(0), np.int32(1), np.float32(0.0)) for i in range(0)}
|
|
223
|
+
component_edges = np.full((knn_indices.shape[0], 3), -1, dtype=np.float64)
|
|
224
|
+
|
|
225
|
+
for i in numba.prange(knn_indices.shape[0]):
|
|
226
|
+
for j in range(1, knn_indices.shape[1]):
|
|
227
|
+
k = np.int32(knn_indices[i, j])
|
|
228
|
+
if core_distances[i] >= core_distances[k]:
|
|
229
|
+
component_edges[i] = (np.float64(i), np.float64(k), np.float64(core_distances[i]))
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
result = np.empty((len(component_edges), 3), dtype=np.float64)
|
|
233
|
+
result_idx = 0
|
|
234
|
+
|
|
235
|
+
# Add the best edges to the edge set and merge the relevant components
|
|
236
|
+
for edge in component_edges:
|
|
237
|
+
if edge[0] < 0:
|
|
238
|
+
continue
|
|
239
|
+
from_component = ds_find(disjoint_set, np.int32(edge[0]))
|
|
240
|
+
to_component = ds_find(disjoint_set, np.int32(edge[1]))
|
|
241
|
+
if from_component != to_component:
|
|
242
|
+
result[result_idx] = (np.float64(edge[0]), np.float64(edge[1]), np.float64(edge[2]))
|
|
243
|
+
result_idx += 1
|
|
244
|
+
|
|
245
|
+
ds_union_by_rank(disjoint_set, from_component, to_component)
|
|
246
|
+
|
|
247
|
+
return result[:result_idx]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def parallel_boruvka(tree, min_samples=10):
|
|
251
|
+
components_disjoint_set = ds_rank_create(tree.data.shape[0])
|
|
252
|
+
point_components = np.arange(tree.data.shape[0])
|
|
253
|
+
node_components = np.full(tree.node_data.shape[0], -1)
|
|
254
|
+
n_components = point_components.shape[0]
|
|
255
|
+
|
|
256
|
+
if min_samples > 1:
|
|
257
|
+
distances, neighbors = parallel_tree_query(tree, tree.data, k=min_samples + 1, output_rdist=True)
|
|
258
|
+
core_distances = distances.T[-1]
|
|
259
|
+
edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
|
|
260
|
+
update_component_vectors(tree, components_disjoint_set, node_components, point_components)
|
|
261
|
+
else:
|
|
262
|
+
core_distances = np.zeros(tree.data.shape[0], dtype=np.float32)
|
|
263
|
+
distances, neighbors = parallel_tree_query(tree, tree.data, k=2)
|
|
264
|
+
edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
|
|
265
|
+
update_component_vectors(tree, components_disjoint_set, node_components, point_components)
|
|
266
|
+
|
|
267
|
+
while n_components > 1:
|
|
268
|
+
candidate_distances, candidate_indices = boruvka_tree_query(tree, node_components, point_components,
|
|
269
|
+
core_distances)
|
|
270
|
+
new_edges = merge_components(components_disjoint_set, candidate_indices, candidate_distances, point_components)
|
|
271
|
+
update_component_vectors(tree, components_disjoint_set, node_components, point_components)
|
|
272
|
+
|
|
273
|
+
edges = np.vstack((edges, new_edges))
|
|
274
|
+
n_components = np.unique(point_components).shape[0]
|
|
275
|
+
|
|
276
|
+
edges[:, 2] = np.sqrt(edges.T[2])
|
|
277
|
+
return edges
|