fast-hdbscan 0.2.0__py3-none-any.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.
@@ -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,299 @@
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
+ @numba.njit(parallel=True)
251
+ def sample_weight_core_distance(distances, neighbors, sample_weights, min_samples):
252
+ core_distances = np.zeros(distances.shape[0], dtype=np.float32)
253
+ for i in numba.prange(distances.shape[0]):
254
+ total_weight = 0.0
255
+ j = 0
256
+ while total_weight < min_samples and j < neighbors.shape[1]:
257
+ total_weight += sample_weights[neighbors[i, j]]
258
+ j += 1
259
+
260
+ core_distances[i] = distances[i, j - 1]
261
+
262
+ return core_distances
263
+
264
+ def parallel_boruvka(tree, min_samples=10, sample_weights=None):
265
+ components_disjoint_set = ds_rank_create(tree.data.shape[0])
266
+ point_components = np.arange(tree.data.shape[0])
267
+ node_components = np.full(tree.node_data.shape[0], -1)
268
+ n_components = point_components.shape[0]
269
+
270
+ if sample_weights is not None:
271
+ mean_sample_weight = np.mean(sample_weights)
272
+ expected_neighbors = min_samples / mean_sample_weight
273
+ distances, neighbors = parallel_tree_query(tree, tree.data, k=int(2 * expected_neighbors))
274
+ core_distances = sample_weight_core_distance(distances, neighbors, sample_weights, min_samples)
275
+ edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
276
+ update_component_vectors(tree, components_disjoint_set, node_components, point_components)
277
+ else:
278
+ if min_samples > 1:
279
+ distances, neighbors = parallel_tree_query(tree, tree.data, k=min_samples + 1, output_rdist=True)
280
+ core_distances = distances.T[-1]
281
+ edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
282
+ update_component_vectors(tree, components_disjoint_set, node_components, point_components)
283
+ else:
284
+ core_distances = np.zeros(tree.data.shape[0], dtype=np.float32)
285
+ distances, neighbors = parallel_tree_query(tree, tree.data, k=2)
286
+ edges = initialize_boruvka_from_knn(neighbors, distances, core_distances, components_disjoint_set)
287
+ update_component_vectors(tree, components_disjoint_set, node_components, point_components)
288
+
289
+ while n_components > 1:
290
+ candidate_distances, candidate_indices = boruvka_tree_query(tree, node_components, point_components,
291
+ core_distances)
292
+ new_edges = merge_components(components_disjoint_set, candidate_indices, candidate_distances, point_components)
293
+ update_component_vectors(tree, components_disjoint_set, node_components, point_components)
294
+
295
+ edges = np.vstack((edges, new_edges))
296
+ n_components = np.unique(point_components).shape[0]
297
+
298
+ edges[:, 2] = np.sqrt(edges.T[2])
299
+ return edges