From 0c865cba4331ce6b83b5101c4e4ee7c398e37a22 Mon Sep 17 00:00:00 2001 From: Andreas Tille <tille@debian.org> Date: Thu, 18 Feb 2021 20:13:48 +0100 Subject: [PATCH] New upstream version 0.5.2 --- PKG-INFO | 5 +- README.rst | 3 + pynndescent.egg-info/PKG-INFO | 5 +- pynndescent.egg-info/SOURCES.txt | 5 + pynndescent/distances.py | 73 +++++- pynndescent/graph_utils.py | 242 ++++++++++++++++++ pynndescent/pynndescent_.py | 60 +++-- pynndescent/rp_trees.py | 9 +- pynndescent/sparse.py | 35 ++- .../tests/__pycache__/__init__.cpython-38.pyc | Bin 0 -> 162 bytes ...test_distances.cpython-38-pytest-6.2.2.pyc | Bin 0 -> 13744 bytes ...t_pynndescent_.cpython-38-pytest-6.2.2.pyc | Bin 0 -> 12971 bytes .../test_rank.cpython-38-pytest-6.2.2.pyc | Bin 0 -> 6286 bytes pynndescent/tests/test_distances.py | 7 +- pynndescent/tests/test_pynndescent_.py | 45 ++++ pynndescent/utils.py | 10 +- setup.py | 2 +- 17 files changed, 450 insertions(+), 51 deletions(-) create mode 100644 pynndescent/graph_utils.py create mode 100644 pynndescent/tests/__pycache__/__init__.cpython-38.pyc create mode 100644 pynndescent/tests/__pycache__/test_distances.cpython-38-pytest-6.2.2.pyc create mode 100644 pynndescent/tests/__pycache__/test_pynndescent_.cpython-38-pytest-6.2.2.pyc create mode 100644 pynndescent/tests/__pycache__/test_rank.cpython-38-pytest-6.2.2.pyc diff --git a/PKG-INFO b/PKG-INFO index 55e1224..1703a98 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: pynndescent -Version: 0.5.1 +Version: 0.5.2 Summary: Nearest Neighbor Descent Home-page: http://github.com/lmcinnes/pynndescent Author: Leland McInnes @@ -91,8 +91,11 @@ Description: .. image:: https://travis-ci.org/lmcinnes/pynndescent.svg **Angular and correlation metrics** - cosine + - dot - correlation - spearmanr + - tsss + - true_angular **Probability metrics** diff --git a/README.rst b/README.rst index ef5c322..cd0349b 100644 --- a/README.rst +++ b/README.rst @@ -81,8 +81,11 @@ supporting a wide variety of distance metrics by default: **Angular and correlation metrics** - cosine +- dot - correlation - spearmanr +- tsss +- true_angular **Probability metrics** diff --git a/pynndescent.egg-info/PKG-INFO b/pynndescent.egg-info/PKG-INFO index 55e1224..1703a98 100644 --- a/pynndescent.egg-info/PKG-INFO +++ b/pynndescent.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 1.2 Name: pynndescent -Version: 0.5.1 +Version: 0.5.2 Summary: Nearest Neighbor Descent Home-page: http://github.com/lmcinnes/pynndescent Author: Leland McInnes @@ -91,8 +91,11 @@ Description: .. image:: https://travis-ci.org/lmcinnes/pynndescent.svg **Angular and correlation metrics** - cosine + - dot - correlation - spearmanr + - tsss + - true_angular **Probability metrics** diff --git a/pynndescent.egg-info/SOURCES.txt b/pynndescent.egg-info/SOURCES.txt index fbe0a29..5499a38 100644 --- a/pynndescent.egg-info/SOURCES.txt +++ b/pynndescent.egg-info/SOURCES.txt @@ -7,6 +7,7 @@ requirements.txt setup.py pynndescent/__init__.py pynndescent/distances.py +pynndescent/graph_utils.py pynndescent/optimal_transport.py pynndescent/pynndescent_.py pynndescent/rp_trees.py @@ -25,7 +26,11 @@ pynndescent/tests/test_distances.py pynndescent/tests/test_pynndescent_.py pynndescent/tests/test_rank.py pynndescent/tests/__pycache__/__init__.cpython-37.pyc +pynndescent/tests/__pycache__/__init__.cpython-38.pyc pynndescent/tests/__pycache__/test_distances.cpython-37.pyc +pynndescent/tests/__pycache__/test_distances.cpython-38-pytest-6.2.2.pyc pynndescent/tests/__pycache__/test_pynndescent_.cpython-37.pyc +pynndescent/tests/__pycache__/test_pynndescent_.cpython-38-pytest-6.2.2.pyc pynndescent/tests/__pycache__/test_rank.cpython-37.pyc +pynndescent/tests/__pycache__/test_rank.cpython-38-pytest-6.2.2.pyc pynndescent/tests/test_data/cosine_hang.npy \ No newline at end of file diff --git a/pynndescent/distances.py b/pynndescent/distances.py index e645cc3..3d40480 100644 --- a/pynndescent/distances.py +++ b/pynndescent/distances.py @@ -47,7 +47,7 @@ def euclidean(x, y): locals={ "result": numba.types.float32, "diff": numba.types.float32, - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -230,7 +230,7 @@ def jaccard(x, y): "num_equal": numba.types.float32, "x_true": numba.types.uint8, "y_true": numba.types.uint8, - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -415,7 +415,7 @@ def cosine(x, y): "result": numba.types.float32, "norm_x": numba.types.float32, "norm_y": numba.types.float32, - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -445,7 +445,7 @@ def alternative_cosine(x, y): fastmath=True, locals={ "result": numba.types.float32, - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -472,7 +472,7 @@ def dot(x, y): fastmath=True, locals={ "result": numba.types.float32, - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -493,6 +493,59 @@ def correct_alternative_cosine(d): return 1.0 - pow(2.0, -d) +@numba.njit(fastmath=True) +def tsss(x, y): + d_euc_squared = 0.0 + d_cos = 0.0 + norm_x = 0.0 + norm_y = 0.0 + dim = x.shape[0] + + for i in range(dim): + diff = x[i] - y[i] + d_euc_squared += diff * diff + d_cos += x[i] * y[i] + norm_x += x[i] * x[i] + norm_y += y[i] * y[i] + + norm_x = np.sqrt(norm_x) + norm_y = np.sqrt(norm_y) + magnitude_difference = np.abs(norm_x - norm_y) + d_cos /= norm_x * norm_y + theta = np.arccos(d_cos) + np.radians(10) # Add 10 degrees as an "epsilon" to + # avoid problems + sector = ((np.sqrt(d_euc_squared) + magnitude_difference) ** 2) * theta + triangle = norm_x * norm_y * np.sin(theta) / 2.0 + return triangle * sector + + +@numba.njit(fastmath=True) +def true_angular(x, y): + result = 0.0 + norm_x = 0.0 + norm_y = 0.0 + dim = x.shape[0] + for i in range(dim): + result += x[i] * y[i] + norm_x += x[i] * x[i] + norm_y += y[i] * y[i] + + if norm_x == 0.0 and norm_y == 0.0: + return 0.0 + elif norm_x == 0.0 or norm_y == 0.0: + return FLOAT32_MAX + elif result <= 0.0: + return FLOAT32_MAX + else: + result = result / np.sqrt(norm_x * norm_y) + return 1.0 - (np.arccos(result) / np.pi) + + +@numba.vectorize(fastmath=True) +def true_angular_from_alt_cosine(d): + return 1.0 - (np.arccos(pow(2.0, -d)) / np.pi) + + @numba.njit(fastmath=True, cache=True) def correlation(x, y): mu_x = 0.0 @@ -536,7 +589,7 @@ def correlation(x, y): "result": numba.types.float32, "l1_norm_x": numba.types.float32, "l1_norm_y": numba.types.float32, - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -572,7 +625,7 @@ def hellinger(x, y): "result": numba.types.float32, "l1_norm_x": numba.types.float32, "l1_norm_y": numba.types.float32, - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -738,6 +791,8 @@ named_distances = { "spearmanr": spearmanr, "kantorovich": kantorovich, "wasserstein": kantorovich, + "tsss": tsss, + "true_angular": true_angular, # Binary distances "hamming": hamming, "jaccard": jaccard, @@ -762,6 +817,10 @@ fast_distance_alternatives = { "l2": {"dist": squared_euclidean, "correction": np.sqrt}, "cosine": {"dist": alternative_cosine, "correction": correct_alternative_cosine}, "dot": {"dist": alternative_dot, "correction": correct_alternative_cosine}, + "true_angular": { + "dist": alternative_cosine, + "correction": true_angular_from_alt_cosine, + }, "hellinger": { "dist": alternative_hellinger, "correction": correct_alternative_hellinger, diff --git a/pynndescent/graph_utils.py b/pynndescent/graph_utils.py new file mode 100644 index 0000000..337a925 --- /dev/null +++ b/pynndescent/graph_utils.py @@ -0,0 +1,242 @@ +import numba +import numpy as np +import heapq + +from scipy.sparse import coo_matrix +from scipy.sparse.csgraph import connected_components +from itertools import combinations + +import pynndescent.distances as pynnd_dist +import joblib + +from pynndescent.utils import ( + rejection_sample, + make_heap, + deheap_sort, + simple_heap_push, + has_been_visited, + mark_visited, +) + +FLOAT32_EPS = np.finfo(np.float32).eps + + +def create_component_search(index): + alternative_dot = pynnd_dist.alternative_dot + alternative_cosine = pynnd_dist.alternative_cosine + + data = index._raw_data + indptr = index._search_graph.indptr + indices = index._search_graph.indices + dist = index._distance_func + + @numba.njit( + fastmath=True, + nogil=True, + locals={ + "current_query": numba.types.float32[::1], + "i": numba.types.uint32, + "j": numba.types.uint32, + "heap_priorities": numba.types.float32[::1], + "heap_indices": numba.types.int32[::1], + "candidate": numba.types.int32, + "vertex": numba.types.int32, + "d": numba.types.float32, + "d_vertex": numba.types.float32, + "visited": numba.types.uint8[::1], + "indices": numba.types.int32[::1], + "indptr": numba.types.int32[::1], + "data": numba.types.float32[:, ::1], + "heap_size": numba.types.int16, + "distance_scale": numba.types.float32, + "distance_bound": numba.types.float32, + "seed_scale": numba.types.float32, + }, + ) + def custom_search_closure( + query_points, + candidate_indices, + k, + epsilon, + visited, + ): + result = make_heap(query_points.shape[0], k) + distance_scale = 1.0 + epsilon + + for i in range(query_points.shape[0]): + visited[:] = 0 + if dist == alternative_dot or dist == alternative_cosine: + norm = np.sqrt((query_points[i] ** 2).sum()) + if norm > 0.0: + current_query = query_points[i] / norm + else: + continue + else: + current_query = query_points[i] + + heap_priorities = result[1][i] + heap_indices = result[0][i] + seed_set = [(np.float32(np.inf), np.int32(-1)) for j in range(0)] + + ############ Init ################ + n_initial_points = candidate_indices.shape[0] + + for j in range(n_initial_points): + candidate = np.int32(candidate_indices[j]) + d = dist(data[candidate], current_query) + # indices are guaranteed different + simple_heap_push(heap_priorities, heap_indices, d, candidate) + heapq.heappush(seed_set, (d, candidate)) + mark_visited(visited, candidate) + + ############ Search ############## + distance_bound = distance_scale * heap_priorities[0] + + # Find smallest seed point + d_vertex, vertex = heapq.heappop(seed_set) + + while d_vertex < distance_bound: + + for j in range(indptr[vertex], indptr[vertex + 1]): + + candidate = indices[j] + + if has_been_visited(visited, candidate) == 0: + mark_visited(visited, candidate) + + d = dist(data[candidate], current_query) + + if d < distance_bound: + simple_heap_push( + heap_priorities, heap_indices, d, candidate + ) + heapq.heappush(seed_set, (d, candidate)) + # Update bound + distance_bound = distance_scale * heap_priorities[0] + + # find new smallest seed point + if len(seed_set) == 0: + break + else: + d_vertex, vertex = heapq.heappop(seed_set) + + return result + + return custom_search_closure + + +# @numba.njit(nogil=True) +def find_component_connection_edge( + component1, + component2, + search_closure, + raw_data, + visited, + rng_state, + search_size=10, + epsilon=0.0, +): + indices = [np.zeros(1, dtype=np.int64) for i in range(2)] + indices[0] = component1[ + rejection_sample(np.int64(search_size), component1.shape[0], rng_state) + ] + indices[1] = component2[ + rejection_sample(np.int64(search_size), component2.shape[0], rng_state) + ] + query_side = 0 + query_points = raw_data[indices[query_side]] + candidate_indices = indices[1 - query_side].copy() + changed = [True, True] + best_dist = np.inf + best_edge = (indices[0][0], indices[1][0]) + + while changed[0] or changed[1]: + result = search_closure( + query_points, candidate_indices, search_size, epsilon, visited + ) + inds, dists = deheap_sort(result) + for i in range(dists.shape[0]): + for j in range(dists.shape[1]): + if dists[i, j] < best_dist: + best_dist = dists[i, j] + best_edge = (indices[query_side][i], inds[i, j]) + candidate_indices = indices[query_side] + new_indices = np.unique(inds[:, 0]) + if indices[1 - query_side].shape[0] == new_indices.shape[0]: + changed[1 - query_side] = np.any(indices[1 - query_side] != new_indices) + indices[1 - query_side] = new_indices + query_points = raw_data[indices[1 - query_side]] + query_side = 1 - query_side + + return best_edge[0], best_edge[1], best_dist + + +def adjacency_matrix_representation(neighbor_indices, neighbor_distances): + result = coo_matrix( + (neighbor_indices.shape[0], neighbor_indices.shape[0]), dtype=np.float32 + ) + + # Preserve any distance 0 points + neighbor_distances[neighbor_distances == 0.0] = FLOAT32_EPS + + result.row = np.repeat( + np.arange(neighbor_indices.shape[0], dtype=np.int32), + neighbor_indices.shape[1], + ) + result.col = neighbor_indices.ravel() + result.data = neighbor_distances.ravel() + + # Get rid of any -1 index entries + result = result.tocsr() + result.data[result.indices == -1] = 0.0 + result.eliminate_zeros() + + # Symmetrize + result = result.maximum(result.T) + + return result + + +def connect_graph(graph, index, search_size=10, n_jobs=None): + + search_closure = create_component_search(index) + n_components, component_ids = connected_components(graph) + result = graph.tolil() + + # Translate component ids into internal vertex order + component_ids = component_ids[index._vertex_order] + + def new_edge(c1, c2): + component1 = np.where(component_ids == c1)[0] + component2 = np.where(component_ids == c2)[0] + + i, j, d = find_component_connection_edge( + component1, + component2, + search_closure, + index._raw_data, + index._visited, + index.rng_state, + search_size=search_size, + ) + + # Correct the distance if required + if index._distance_correction is not None: + d = index._distance_correction(d) + + # Convert indices to original data order + i = index._vertex_order[i] + j = index._vertex_order[j] + + return i, j, d + + new_edges = joblib.Parallel(n_jobs=n_jobs, prefer="threads")( + joblib.delayed(new_edge)(c1, c2) + for c1, c2 in combinations(range(n_components), 2) + ) + + for i, j, d in new_edges: + result[i, j] = d + result[j, i] = d + + return result.tocsr() diff --git a/pynndescent/pynndescent_.py b/pynndescent/pynndescent_.py index ad7f067..96b357c 100644 --- a/pynndescent/pynndescent_.py +++ b/pynndescent/pynndescent_.py @@ -855,7 +855,9 @@ class NNDescent(object): if init_graph is None: _init_graph = EMPTY_GRAPH else: - _init_graph = make_heap(init_graph.shape[0], init_graph.shape[1]) + if init_graph.shape[0] != self._raw_data.shape[0]: + raise ValueError("Init graph size does not match dataset size!") + _init_graph = make_heap(init_graph.shape[0], self.n_neighbors) _init_graph = sparse_initalize_heap_from_graph_indices( _init_graph, init_graph, @@ -892,7 +894,9 @@ class NNDescent(object): if init_graph is None: _init_graph = EMPTY_GRAPH else: - _init_graph = make_heap(init_graph.shape[0], init_graph.shape[1]) + if init_graph.shape[0] != self._raw_data.shape[0]: + raise ValueError("Init graph size does not match dataset size!") + _init_graph = make_heap(init_graph.shape[0], self.n_neighbors) _init_graph = initalize_heap_from_graph_indices( _init_graph, init_graph, data, self._distance_func ) @@ -952,21 +956,40 @@ class NNDescent(object): numba.set_num_threads(self.n_jobs) if not hasattr(self, "_search_forest"): - tree_scores = [ - score_linked_tree(tree, self._neighbor_graph[0]) - for tree in self._rp_forest - ] - if self.verbose: - print(ts(), "Worst tree score: {:.8f}".format(np.min(tree_scores))) - print(ts(), "Mean tree score: {:.8f}".format(np.mean(tree_scores))) - print(ts(), "Best tree score: {:.8f}".format(np.max(tree_scores))) - best_tree_indices = np.argsort(tree_scores)[: self.n_search_trees] - best_trees = [self._rp_forest[idx] for idx in best_tree_indices] - del self._rp_forest - self._search_forest = [ - convert_tree_format(tree, self._raw_data.shape[0]) - for tree in best_trees - ] + if self._rp_forest is None: + # We don't have a forest, so make a small search forest + current_random_state = check_random_state(self.random_state) + rp_forest = make_forest( + self._raw_data, + self.n_neighbors, + self.n_search_trees, + self.leaf_size, + self.rng_state, + current_random_state, + self.n_jobs, + self._angular_trees, + ) + self._search_forest = [ + convert_tree_format(tree, self._raw_data.shape[0]) + for tree in rp_forest + ] + else: + # convert the best trees into a search forest + tree_scores = [ + score_linked_tree(tree, self._neighbor_graph[0]) + for tree in self._rp_forest + ] + if self.verbose: + print(ts(), "Worst tree score: {:.8f}".format(np.min(tree_scores))) + print(ts(), "Mean tree score: {:.8f}".format(np.mean(tree_scores))) + print(ts(), "Best tree score: {:.8f}".format(np.max(tree_scores))) + best_tree_indices = np.argsort(tree_scores)[: self.n_search_trees] + best_trees = [self._rp_forest[idx] for idx in best_tree_indices] + del self._rp_forest + self._search_forest = [ + convert_tree_format(tree, self._raw_data.shape[0]) + for tree in best_trees + ] nnz_pre_diversify = np.sum(self._neighbor_graph[0] >= 0) if self._is_sparse: @@ -1079,7 +1102,7 @@ class NNDescent(object): self._search_graph.sort_indices() self._search_graph = self._search_graph.maximum(reverse_graph).tocsr() - # Eliminate the diagonal0] + # Eliminate the diagonal self._search_graph.setdiag(0.0) self._search_graph.eliminate_zeros() @@ -1531,6 +1554,7 @@ class NNDescent(object): def compress_index(self): import gc + self.prepare() self.compressed = True if hasattr(self, "_rp_forest"): diff --git a/pynndescent/rp_trees.py b/pynndescent/rp_trees.py index a2a729f..55aff6e 100644 --- a/pynndescent/rp_trees.py +++ b/pynndescent/rp_trees.py @@ -853,7 +853,7 @@ def make_sparse_tree(inds, indptr, spdata, rng_state, leaf_size=30, angular=Fals fastmath=True, locals={ "margin": numba.types.float32, - "dim": numba.types.uint16, + "dim": numba.types.intp, "d": numba.types.uint16, }, ) @@ -984,7 +984,7 @@ def make_forest( ) try: if scipy.sparse.isspmatrix_csr(data): - result = joblib.Parallel(n_jobs=n_jobs, prefer="threads")( + result = joblib.Parallel(n_jobs=n_jobs, require="sharedmem")( joblib.delayed(make_sparse_tree)( data.indices, data.indptr, @@ -996,7 +996,7 @@ def make_forest( for i in range(n_trees) ) else: - result = joblib.Parallel(n_jobs=n_jobs, prefer="threads")( + result = joblib.Parallel(n_jobs=n_jobs, require="sharedmem")( joblib.delayed(make_dense_tree)(data, rng_states[i], leaf_size, angular) for i in range(n_trees) ) @@ -1029,10 +1029,9 @@ def get_leaves_from_tree(tree): def rptree_leaf_array_parallel(rp_forest): - result = joblib.Parallel(n_jobs=-1, prefer="threads")( + result = joblib.Parallel(n_jobs=-1, require="sharedmem")( joblib.delayed(get_leaves_from_tree)(rp_tree) for rp_tree in rp_forest ) - # result = [get_leaves_from_tree(rp_tree) for rp_tree in rp_forest] return result diff --git a/pynndescent/sparse.py b/pynndescent/sparse.py index 301e99f..8d91bb5 100644 --- a/pynndescent/sparse.py +++ b/pynndescent/sparse.py @@ -218,7 +218,7 @@ def sparse_euclidean(ind1, data1, ind2, data2): "aux_data": numba.types.float32[::1], "result": numba.types.float32, "diff": numba.types.float32, - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -281,17 +281,30 @@ def sparse_canberra(ind1, data1, ind2, data2): return result -@numba.njit() +@numba.njit( + [ + "f4(i4[::1],f4[::1],i4[::1],f4[::1])", + numba.types.float32( + numba.types.Array(numba.types.int32, 1, "C", readonly=True), + numba.types.Array(numba.types.float32, 1, "C", readonly=True), + numba.types.Array(numba.types.int32, 1, "C", readonly=True), + numba.types.Array(numba.types.float32, 1, "C", readonly=True), + ), + ], + fastmath=True, +) def sparse_bray_curtis(ind1, data1, ind2, data2): # pragma: no cover - abs_data1 = np.abs(data1) - abs_data2 = np.abs(data2) - _, denom_data = sparse_sum(ind1, abs_data1, ind2, abs_data2) + _, denom_data = sparse_sum(ind1, data1, ind2, data2) + denom_data = np.abs(denom_data) if denom_data.shape[0] == 0: return 0.0 denominator = np.sum(denom_data) + if denominator == 0.0: + return 0.0 + _, numer_data = sparse_diff(ind1, data1, ind2, data2) numer_data = np.abs(numer_data) @@ -323,8 +336,8 @@ def sparse_jaccard(ind1, data1, ind2, data2): ], fastmath=True, locals={ - "num_non_zero": numba.types.float32, - "num_equal": numba.types.float32, + "num_non_zero": numba.types.intp, + "num_equal": numba.types.intp, }, ) def sparse_alternative_jaccard(ind1, data1, ind2, data2): @@ -445,7 +458,7 @@ def sparse_cosine(ind1, data1, ind2, data2): "result": numba.types.float32, "norm_x": numba.types.float32, "norm_y": numba.types.float32, - "dim": numba.types.int32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -492,7 +505,7 @@ def sparse_dot(ind1, data1, ind2, data2): fastmath=True, locals={ "result": numba.types.float32, - "dim": numba.types.int32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -598,7 +611,7 @@ def sparse_hellinger(ind1, data1, ind2, data2): "result": numba.types.float32, "l1_norm_x": numba.types.float32, "l1_norm_y": numba.types.float32, - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint16, }, ) @@ -810,7 +823,7 @@ sparse_named_distances = { "canberra": sparse_canberra, "kantorovich": sparse_kantorovich, "wasserstein": sparse_kantorovich, - # 'braycurtis': sparse_bray_curtis, + "braycurtis": sparse_bray_curtis, # Binary distances "hamming": sparse_hamming, "jaccard": sparse_jaccard, diff --git a/pynndescent/tests/__pycache__/__init__.cpython-38.pyc b/pynndescent/tests/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fac04a06e41948fd60c54410e7328ef04d387d4 GIT binary patch literal 162 zcmWIL<>g`k0#iYTSP=afL?8o3AjbiSi&=m~3PUi1CZpd<h9ZzKg81dGA6lGRRIHzq znv<B9q90J1oRL_R8&H&=m6}{qtY1)>mzR=SoSd3hg2FCIEe6WQ$7kkcmc+;F6;$5h Su*uC&Da}c>16lAHh#3HULnpZa literal 0 HcmV?d00001 diff --git a/pynndescent/tests/__pycache__/test_distances.cpython-38-pytest-6.2.2.pyc b/pynndescent/tests/__pycache__/test_distances.cpython-38-pytest-6.2.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..737be21baf662888c3cfe14da58af8874571dcb4 GIT binary patch literal 13744 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 Ye=xp2c`!Z{5C2O3t%=jWo_OMa01iM@W&i*H literal 0 HcmV?d00001 diff --git a/pynndescent/tests/__pycache__/test_pynndescent_.cpython-38-pytest-6.2.2.pyc b/pynndescent/tests/__pycache__/test_pynndescent_.cpython-38-pytest-6.2.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fcc558db4928153de3967e57ee1e413f0dd5db39 GIT binary patch literal 12971 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 aGmhFiXAABooH1uRu5oA78F4n@KlVS}Znc*H literal 0 HcmV?d00001 diff --git a/pynndescent/tests/__pycache__/test_rank.cpython-38-pytest-6.2.2.pyc b/pynndescent/tests/__pycache__/test_rank.cpython-38-pytest-6.2.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d5cb61c799f9b03c7a18f57edd33bb51cfee4fb GIT binary patch literal 6286 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 C4op`7 literal 0 HcmV?d00001 diff --git a/pynndescent/tests/test_distances.py b/pynndescent/tests/test_distances.py index 91a0e99..ab6056e 100644 --- a/pynndescent/tests/test_distances.py +++ b/pynndescent/tests/test_distances.py @@ -1,11 +1,10 @@ import numpy as np -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_equal, assert_array_almost_equal import pynndescent.distances as dist import pynndescent.sparse as spdist from scipy import sparse, stats from sklearn.metrics import pairwise_distances from sklearn.neighbors import BallTree -from sklearn.utils.testing import assert_array_almost_equal np.random.seed(42) spatial_data = np.random.randn(10, 20) @@ -315,6 +314,10 @@ def test_sparse_sokalsneath(): sparse_binary_check("sokalsneath") +def test_sparse_braycurtis(): + sparse_spatial_check("braycurtis") + + def test_seuclidean(): v = np.abs(np.random.randn(spatial_data.shape[1])) dist_matrix = pairwise_distances(spatial_data, metric="seuclidean", V=v) diff --git a/pynndescent/tests/test_pynndescent_.py b/pynndescent/tests/test_pynndescent_.py index c58fc24..83d7e7f 100644 --- a/pynndescent/tests/test_pynndescent_.py +++ b/pynndescent/tests/test_pynndescent_.py @@ -413,6 +413,51 @@ def test_pickle_unpickle(): np.testing.assert_equal(distances1, distances2) +def test_compressed_pickle_unpickle(): + seed = np.random.RandomState(42) + + x1 = seed.normal(0, 100, (1000, 50)) + x2 = seed.normal(0, 100, (1000, 50)) + + index1 = NNDescent( + x1, + "euclidean", + {}, + 10, + random_state=None, + compressed=True, + ) + neighbors1, distances1 = index1.query(x2) + + pickle.dump(index1, open("test_tmp.pkl", "wb")) + index2 = pickle.load(open("test_tmp.pkl", "rb")) + os.remove("test_tmp.pkl") + + neighbors2, distances2 = index2.query(x2) + + np.testing.assert_equal(neighbors1, neighbors2) + np.testing.assert_equal(distances1, distances2) + + +def test_transformer_pickle_unpickle(): + seed = np.random.RandomState(42) + + x1 = seed.normal(0, 100, (1000, 50)) + x2 = seed.normal(0, 100, (1000, 50)) + + index1 = PyNNDescentTransformer(n_neighbors=10).fit(x1) + result1 = index1.transform(x2) + + pickle.dump(index1, open("test_tmp.pkl", "wb")) + index2 = pickle.load(open("test_tmp.pkl", "rb")) + os.remove("test_tmp.pkl") + + result2 = index2.transform(x2) + + np.testing.assert_equal(result1.indices, result2.indices) + np.testing.assert_equal(result1.data, result2.data) + + def test_joblib_dump(): seed = np.random.RandomState(42) diff --git a/pynndescent/utils.py b/pynndescent/utils.py index 23ce2e0..befe4bd 100644 --- a/pynndescent/utils.py +++ b/pynndescent/utils.py @@ -67,7 +67,7 @@ def tau_rand(state): ), ], locals={ - "dim": numba.types.uint32, + "dim": numba.types.intp, "i": numba.types.uint32, "result": numba.types.float32, }, @@ -620,7 +620,7 @@ def mark_visited(table, candidate): "i4(f4[::1],i4[::1],f4,i4)", fastmath=True, locals={ - "size": numba.types.uint16, + "size": numba.types.intp, "i": numba.types.uint16, "ic1": numba.types.uint16, "ic2": numba.types.uint16, @@ -676,7 +676,7 @@ def simple_heap_push(priorities, indices, p, n): "i4(f4[::1],i4[::1],f4,i4)", fastmath=True, locals={ - "size": numba.types.uint16, + "size": numba.types.intp, "i": numba.types.uint16, "ic1": numba.types.uint16, "ic2": numba.types.uint16, @@ -737,7 +737,7 @@ def checked_heap_push(priorities, indices, p, n): "i4(f4[::1],i4[::1],u1[::1],f4,i4,u1)", fastmath=True, locals={ - "size": numba.types.uint16, + "size": numba.types.intp, "i": numba.types.uint16, "ic1": numba.types.uint16, "ic2": numba.types.uint16, @@ -796,7 +796,7 @@ def flagged_heap_push(priorities, indices, flags, p, n, f): "i4(f4[::1],i4[::1],u1[::1],f4,i4,u1)", fastmath=True, locals={ - "size": numba.types.uint16, + "size": numba.types.intp, "i": numba.types.uint16, "ic1": numba.types.uint16, "ic2": numba.types.uint16, diff --git a/setup.py b/setup.py index 9b15d5d..193b4e0 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ def readme(): configuration = { "name": "pynndescent", - "version": "0.5.1", + "version": "0.5.2", "description": "Nearest Neighbor Descent", "long_description": readme(), "classifiers": [ -- GitLab