OSCR

Convergence-divergence circuits for multimodal integration of innate and learned opponent valences.

Code ↔ Paper

8 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 8 matches
  1. [1] § Materials and methods › UMAP › UMAP generation ↔ umap/umap_.py, lines 1485–1737 · score 0.97 · uniform manifold approximation, fuzzy simplicial, min_dist, UMAP algorithm, UMAP embedding, n_neighbors
  2. [2] § Materials and methods › UMAP › Clustering on UMAP ↔ umap/umap_.py, lines 1485–1737 · score 0.84 · Local density, UMAP embedding, nearest neighbors, high dimensional, NN, inverse
  3. [3] § Materials and methods › UMAP › UMAP generation ↔ umap/layouts.py, lines 527–658 · score 0.81 · stochastic gradient descent, fuzzy simplicial, low dimensional, metric, optimized, embedding
  4. [4] § Materials and methods › Geometry ↔ navis/nbl/nblast_funcs.py, lines 878–1016 · score 0.64 · dot product, nearest neighbor, NBLAST, straight, score, query
  5. [5] § Materials and methods › Geometry ↔ navis/nbl/synblast_funcs.py, lines 230–360 · score 0.61 · target neuron, nearest neighbor, product, NBLAST, score, query
  6. [6] § Materials and methods › Subcellular topology › Ranked dendrogram ↔ navis/graph/converters.py, lines 484–592 · score 0.56 · adding temporary, edge lengths, branches, tree, node, skeleton
  7. [7] § Materials and methods › Geometry ↔ navis/nbl/nblast_funcs.py, lines 878–1016 · score 0.55 · score matrix, nearest neighbors, NBLAST, vector, neurons
  8. [8] § Materials and methods › Geometry ↔ navis/nbl/synblast_funcs.py, lines 230–360 · score 0.54 · score matrix, nearest neighbors, NBLAST, morphological, vector, neurons

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 3,762 lines · 142 KB · BSD-3-Clause · 2 matches

  1. # Author: Leland McInnes <[email hidden]>
  2. #
  3. # License: BSD 3 clause
  4. from __future__ import print_function
  5. import locale
  6. from collections import deque
  7. from warnings import warn
  8. import time
  9. from scipy.optimize import curve_fit
  10. from sklearn.base import BaseEstimator, ClassNamePrefixFeaturesOutMixin
  11. from sklearn.utils import check_array, check_random_state
  12. from sklearn.utils.validation import check_is_fitted
  13. from sklearn.metrics import pairwise_distances
  14. from sklearn.preprocessing import normalize
  15. from sklearn.neighbors import KDTree
  16. from sklearn.decomposition import PCA, TruncatedSVD
  17. try:
  18. import joblib
  19. except ImportError:
  20. # sklearn.externals.joblib is deprecated in 0.21, will be removed in 0.23
  21. from sklearn.externals import joblib
  22. import numpy as np
  23. import scipy.sparse
  24. from scipy.sparse import tril as sparse_tril, triu as sparse_triu
  25. import scipy.sparse.csgraph
  26. import numba
  27. import umap.distances as dist
  28. import umap.sparse as sparse
  29. from umap.utils import (
  30. ts,
  31. csr_unique,
  32. fast_knn_indices,
  33. )
  34. from umap.spectral import spectral_layout, tswspectral_layout
  35. from umap.layouts import (
  36. optimize_layout_euclidean,
  37. optimize_layout_generic,
  38. optimize_layout_inverse,
  39. )
  40. from pynndescent import NNDescent
  41. from pynndescent.distances import named_distances as pynn_named_distances
  42. from pynndescent.sparse import sparse_named_distances as pynn_sparse_named_distances
  43. locale.setlocale(locale.LC_NUMERIC, "C")
  44. INT32_MIN = np.iinfo(np.int32).min + 1
  45. INT32_MAX = np.iinfo(np.int32).max - 1
  46. SMOOTH_K_TOLERANCE = 1e-5
  47. MIN_K_DIST_SCALE = 1e-3
  48. NPY_INFINITY = np.inf
  49. NPY_FLOATMAX = np.finfo(np.float32).max
  50. DISCONNECTION_DISTANCES = {
  51. "correlation": 2,
  52. "cosine": 2,
  53. "hellinger": 1,
  54. "jaccard": 1,
  55. "bit_jaccard": 1,
  56. "dice": 1,
  57. }
  58. def flatten_iter(container):
  59. for i in container:
  60. if isinstance(i, (list, tuple)):
  61. for j in flatten_iter(i):
  62. yield j
  63. else:
  64. yield i
  65. def flattened(container):
  66. return tuple(flatten_iter(container))
  67. def breadth_first_search(adjmat, start, min_vertices):
  68. explored = []
  69. queue = deque([start])
  70. levels = {start: 0}
  71. max_level = np.inf
  72. visited = np.zeros(adjmat.shape[0], dtype=np.bool_)
  73. visited[start] = True
  74. while queue:
  75. node = queue.popleft()
  76. explored.append(node)
  77. if max_level == np.inf and len(explored) > min_vertices:
  78. max_level = max(levels.values())
  79. if levels[node] + 1 < max_level:
  80. neighbors = adjmat[node].indices
  81. for neighbour in neighbors:
  82. if not visited[neighbour]:
  83. queue.append(neighbour)
  84. visited[neighbour] = True
  85. levels[neighbour] = levels[node] + 1
  86. return np.array(explored)
  87. def raise_disconnected_warning(
  88. edges_removed,
  89. vertices_disconnected,
  90. disconnection_distance,
  91. total_rows,
  92. threshold=0.1,
  93. verbose=False,
  94. ):
  95. """A simple wrapper function to avoid large amounts of code repetition."""
  96. if verbose & (vertices_disconnected == 0) & (edges_removed > 0):
  97. print(
  98. f"Disconnection_distance = {disconnection_distance} has removed {edges_removed} edges. "
  99. f"This is not a problem as no vertices were disconnected."
  100. )
  101. elif (vertices_disconnected > 0) & (
  102. vertices_disconnected <= threshold * total_rows
  103. ):
  104. warn(
  105. f"A few of your vertices were disconnected from the manifold. This shouldn't cause problems.\n"
  106. f"Disconnection_distance = {disconnection_distance} has removed {edges_removed} edges.\n"
  107. f"It has only fully disconnected {vertices_disconnected} vertices.\n"
  108. f"Use umap.utils.disconnected_vertices() to identify them.",
  109. )
  110. elif vertices_disconnected > threshold * total_rows:
  111. warn(
  112. f"A large number of your vertices were disconnected from the manifold.\n"
  113. f"Disconnection_distance = {disconnection_distance} has removed {edges_removed} edges.\n"
  114. f"It has fully disconnected {vertices_disconnected} vertices.\n"
  115. f"You might consider using find_disconnected_points() to find and remove these points from your data.\n"
  116. f"Use umap.utils.disconnected_vertices() to identify them.",
  117. )
  118. @numba.njit(
  119. locals={
  120. "psum": numba.types.float32,
  121. "lo": numba.types.float32,
  122. "mid": numba.types.float32,
  123. "hi": numba.types.float32,
  124. },
  125. parallel=True,
  126. )
  127. def smooth_knn_dist(distances, k, n_iter=64, local_connectivity=1.0, bandwidth=1.0):
  128. """Compute a continuous version of the distance to the kth nearest
  129. neighbor. That is, this is similar to knn-distance but allows continuous
  130. k values rather than requiring an integral k. In essence we are simply
  131. computing the distance such that the cardinality of fuzzy set we generate
  132. is k.
  133. Parameters
  134. ----------
  135. distances: array of shape (n_samples, n_neighbors)
  136. Distances to nearest neighbors for each sample. Each row should be a
  137. sorted list of distances to a given samples nearest neighbors.
  138. k: float
  139. The number of nearest neighbors to approximate for.
  140. n_iter: int (optional, default 64)
  141. We need to binary search for the correct distance value. This is the
  142. max number of iterations to use in such a search.
  143. local_connectivity: int (optional, default 1)
  144. The local connectivity required -- i.e. the number of nearest
  145. neighbors that should be assumed to be connected at a local level.
  146. The higher this value the more connected the manifold becomes
  147. locally. In practice this should be not more than the local intrinsic
  148. dimension of the manifold.
  149. bandwidth: float (optional, default 1)
  150. The target bandwidth of the kernel, larger values will produce
  151. larger return values.
  152. Returns
  153. -------
  154. knn_dist: array of shape (n_samples,)
  155. The distance to kth nearest neighbor, as suitably approximated.
  156. nn_dist: array of shape (n_samples,)
  157. The distance to the 1st nearest neighbor for each point.
  158. """
  159. target = np.log2(k) * bandwidth
  160. rho = np.zeros(distances.shape[0], dtype=np.float32)
  161. result = np.zeros(distances.shape[0], dtype=np.float32)
  162. # Neighbours pruned by disconnection_distance (or inf entries of a
  163. # precomputed distance matrix) arrive here as inf. They must be excluded
  164. # from rho and from the MIN_K_DIST_SCALE floor: the mean of a row that
  165. # contains inf is inf, which made sigma inf for every point with a pruned
  166. # neighbour and gave all of its remaining edges membership strength 1.0.
  167. flat_distances = distances.ravel()
  168. finite_distances = flat_distances[np.isfinite(flat_distances)]
  169. if finite_distances.shape[0] > 0:
  170. mean_distances = np.mean(finite_distances)
  171. else:
  172. mean_distances = 0.0
  173. for i in numba.prange(distances.shape[0]):
  174. lo = 0.0
  175. hi = NPY_FLOATMAX
  176. mid = 1.0
  177. # TODO: This is very inefficient, but will do for now. FIXME
  178. ith_distances = distances[i]
  179. finite_ith_distances = ith_distances[np.isfinite(ith_distances)]
  180. non_zero_dists = finite_ith_distances[finite_ith_distances > 0.0]
  181. if non_zero_dists.shape[0] >= local_connectivity:
  182. index = int(np.floor(local_connectivity))
  183. interpolation = local_connectivity - index
  184. if index > 0:
  185. rho[i] = non_zero_dists[index - 1]
  186. if interpolation > SMOOTH_K_TOLERANCE:
  187. rho[i] += interpolation * (
  188. non_zero_dists[index] - non_zero_dists[index - 1]
  189. )
  190. else:
  191. rho[i] = interpolation * non_zero_dists[0]
  192. elif non_zero_dists.shape[0] > 0:
  193. rho[i] = np.max(non_zero_dists)
  194. for n in range(n_iter):
  195. psum = 0.0
  196. for j in range(1, distances.shape[1]):
  197. d = distances[i, j] - rho[i]
  198. if d > 0:
  199. psum += np.exp(-(d / mid))
  200. else:
  201. psum += 1.0
  202. if np.fabs(psum - target) < SMOOTH_K_TOLERANCE:
  203. break
  204. if psum > target:
  205. hi = mid
  206. mid = (lo + hi) / 2.0
  207. else:
  208. lo = mid
  209. if hi >= NPY_FLOATMAX:
  210. mid *= 2
  211. else:
  212. mid = (lo + hi) / 2.0
  213. result[i] = mid
  214. # TODO: This is very inefficient, but will do for now. FIXME
  215. if rho[i] > 0.0:
  216. mean_ith_distances = np.mean(finite_ith_distances)
  217. if result[i] < MIN_K_DIST_SCALE * mean_ith_distances:
  218. result[i] = MIN_K_DIST_SCALE * mean_ith_distances
  219. else:
  220. if result[i] < MIN_K_DIST_SCALE * mean_distances:
  221. result[i] = MIN_K_DIST_SCALE * mean_distances
  222. return result, rho
  223. def nearest_neighbors(
  224. X,
  225. n_neighbors,
  226. metric,
  227. metric_kwds,
  228. angular,
  229. random_state,
  230. low_memory=True,
  231. use_pynndescent=True,
  232. n_jobs=-1,
  233. verbose=False,
  234. ):
  235. """Compute the ``n_neighbors`` nearest points for each data point in ``X``
  236. under ``metric``. This may be exact, but more likely is approximated via
  237. nearest neighbor descent.
  238. Parameters
  239. ----------
  240. X: array of shape (n_samples, n_features)
  241. The input data to compute the k-neighbor graph of.
  242. n_neighbors: int
  243. The number of nearest neighbors to compute for each sample in ``X``.
  244. metric: string or callable
  245. The metric to use for the computation.
  246. metric_kwds: dict
  247. Any arguments to pass to the metric computation function.
  248. angular: bool
  249. Whether to use angular rp trees in NN approximation.
  250. random_state: np.random state
  251. The random state to use for approximate NN computations.
  252. low_memory: bool (optional, default True)
  253. Whether to pursue lower memory NNdescent.
  254. verbose: bool (optional, default False)
  255. Whether to print status data during the computation.
  256. Returns
  257. -------
  258. knn_indices: array of shape (n_samples, n_neighbors)
  259. The indices on the ``n_neighbors`` closest points in the dataset.
  260. knn_dists: array of shape (n_samples, n_neighbors)
  261. The distances to the ``n_neighbors`` closest points in the dataset.
  262. rp_forest: list of trees
  263. The random projection forest used for searching (if used, None otherwise).
  264. """
  265. if verbose:
  266. print(ts(), "Finding Nearest Neighbors")
  267. if metric == "precomputed":
  268. # Note that this does not support sparse distance matrices yet ...
  269. # Compute indices of n nearest neighbors
  270. knn_indices = fast_knn_indices(X, n_neighbors)
  271. # knn_indices = np.argsort(X)[:, :n_neighbors]
  272. # Compute the nearest neighbor distances
  273. # (equivalent to np.sort(X)[:,:n_neighbors])
  274. # Advanced indexing already returns a fresh contiguous array, so no
  275. # extra .copy() is needed here.
  276. knn_dists = X[np.arange(X.shape[0])[:, None], knn_indices]
  277. # Prune any nearest neighbours that are infinite distance apart.
  278. disconnected_index = knn_dists == np.inf
  279. knn_indices[disconnected_index] = -1
  280. knn_search_index = None
  281. else:
  282. # TODO: Hacked values for now
  283. n_trees = min(64, 5 + int(round((X.shape[0]) ** 0.5 / 20.0)))
  284. n_iters = max(5, int(round(np.log2(X.shape[0]))))
  285. knn_search_index = NNDescent(
  286. X,
  287. n_neighbors=n_neighbors,
  288. metric=metric,
  289. metric_kwds=metric_kwds,
  290. random_state=random_state,
  291. n_trees=n_trees,
  292. n_iters=n_iters,
  293. max_candidates=60,
  294. low_memory=low_memory,
  295. n_jobs=n_jobs,
  296. verbose=verbose,
  297. compressed=False,
  298. )
  299. knn_indices, knn_dists = knn_search_index.neighbor_graph
  300. if verbose:
  301. print(ts(), "Finished Nearest Neighbor Search")
  302. return knn_indices, knn_dists, knn_search_index
  303. @numba.njit(
  304. locals={
  305. "knn_dists": numba.types.float32[:, ::1],
  306. "sigmas": numba.types.float32[::1],
  307. "rhos": numba.types.float32[::1],
  308. "val": numba.types.float32,
  309. },
  310. parallel=True,
  311. )
  312. def compute_membership_strengths(
  313. knn_indices,
  314. knn_dists,
  315. sigmas,
  316. rhos,
  317. return_dists=False,
  318. bipartite=False,
  319. ):
  320. """Construct the membership strength data for the 1-skeleton of each local
  321. fuzzy simplicial set -- this is formed as a sparse matrix where each row is
  322. a local fuzzy simplicial set, with a membership strength for the
  323. 1-simplex to each other data point.
  324. Parameters
  325. ----------
  326. knn_indices: array of shape (n_samples, n_neighbors)
  327. The indices on the ``n_neighbors`` closest points in the dataset.
  328. knn_dists: array of shape (n_samples, n_neighbors)
  329. The distances to the ``n_neighbors`` closest points in the dataset.
  330. sigmas: array of shape(n_samples)
  331. The normalization factor derived from the metric tensor approximation.
  332. rhos: array of shape(n_samples)
  333. The local connectivity adjustment.
  334. return_dists: bool (optional, default False)
  335. Whether to return the pairwise distance associated with each edge.
  336. bipartite: bool (optional, default False)
  337. Does the nearest neighbour set represent a bipartite graph? That is, are the
  338. nearest neighbour indices from the same point set as the row indices?
  339. Returns
  340. -------
  341. rows: array of shape (n_samples * n_neighbors)
  342. Row data for the resulting sparse matrix (coo format)
  343. cols: array of shape (n_samples * n_neighbors)
  344. Column data for the resulting sparse matrix (coo format)
  345. vals: array of shape (n_samples * n_neighbors)
  346. Entries for the resulting sparse matrix (coo format)
  347. dists: array of shape (n_samples * n_neighbors)
  348. Distance associated with each entry in the resulting sparse matrix
  349. """
  350. n_samples = knn_indices.shape[0]
  351. n_neighbors = knn_indices.shape[1]
  352. rows = np.zeros(knn_indices.size, dtype=np.int32)
  353. cols = np.zeros(knn_indices.size, dtype=np.int32)
  354. vals = np.zeros(knn_indices.size, dtype=np.float32)
  355. if return_dists:
  356. dists = np.zeros(knn_indices.size, dtype=np.float32)
  357. else:
  358. dists = None
  359. for i in range(n_samples):
  360. for j in range(n_neighbors):
  361. if knn_indices[i, j] == -1:
  362. continue # We didn't get the full knn for i
  363. # If applied to an adjacency matrix points shouldn't be similar to themselves.
  364. # If applied to an incidence matrix (or bipartite) then the row and column indices are different.
  365. if (bipartite == False) & (knn_indices[i, j] == i):
  366. val = 0.0
  367. elif knn_dists[i, j] - rhos[i] <= 0.0 or sigmas[i] == 0.0:
  368. val = 1.0
  369. else:
  370. val = np.exp(-((knn_dists[i, j] - rhos[i]) / (sigmas[i])))
  371. rows[i * n_neighbors + j] = i
  372. cols[i * n_neighbors + j] = knn_indices[i, j]
  373. vals[i * n_neighbors + j] = val
  374. if return_dists:
  375. dists[i * n_neighbors + j] = knn_dists[i, j]
  376. return rows, cols, vals, dists
  377. def fuzzy_simplicial_set(
  378. X,
  379. n_neighbors,
  380. random_state,
  381. metric,
  382. metric_kwds={},
  383. knn_indices=None,
  384. knn_dists=None,
  385. angular=False,
  386. set_op_mix_ratio=1.0,
  387. local_connectivity=1.0,
  388. apply_set_operations=True,
  389. verbose=False,
  390. return_dists=None,
  391. ):
  392. """Given a set of data X, a neighborhood size, and a measure of distance
  393. compute the fuzzy simplicial set (here represented as a fuzzy graph in
  394. the form of a sparse matrix) associated to the data. This is done by
  395. locally approximating geodesic distance at each point, creating a fuzzy
  396. simplicial set for each such point, and then combining all the local
  397. fuzzy simplicial sets into a global one via a fuzzy union.
  398. Parameters
  399. ----------
  400. X: array of shape (n_samples, n_features)
  401. The data to be modelled as a fuzzy simplicial set.
  402. n_neighbors: int
  403. The number of neighbors to use to approximate geodesic distance.
  404. Larger numbers induce more global estimates of the manifold that can
  405. miss finer detail, while smaller values will focus on fine manifold
  406. structure to the detriment of the larger picture.
  407. random_state: numpy RandomState or equivalent
  408. A state capable being used as a numpy random state.
  409. metric: string or function (optional, default 'euclidean')
  410. The metric to use to compute distances in high dimensional space.
  411. If a string is passed it must match a valid predefined metric. If
  412. a general metric is required a function that takes two 1d arrays and
  413. returns a float can be provided. For performance purposes it is
  414. required that this be a numba jit'd function. Valid string metrics
  415. include:
  416. * euclidean (or l2)
  417. * manhattan (or l1)
  418. * cityblock
  419. * braycurtis
  420. * canberra
  421. * chebyshev
  422. * correlation
  423. * cosine
  424. * dice
  425. * hamming
  426. * jaccard
  427. * kulsinski
  428. * ll_dirichlet
  429. * mahalanobis
  430. * matching
  431. * minkowski
  432. * rogerstanimoto
  433. * russellrao
  434. * seuclidean
  435. * sokalmichener
  436. * sokalsneath
  437. * sqeuclidean
  438. * yule
  439. * wminkowski
  440. Metrics that take arguments (such as minkowski, mahalanobis etc.)
  441. can have arguments passed via the metric_kwds dictionary. At this
  442. time care must be taken and dictionary elements must be ordered
  443. appropriately; this will hopefully be fixed in the future.
  444. metric_kwds: dict (optional, default {})
  445. Arguments to pass on to the metric, such as the ``p`` value for
  446. Minkowski distance.
  447. knn_indices: array of shape (n_samples, n_neighbors) (optional)
  448. If the k-nearest neighbors of each point has already been calculated
  449. you can pass them in here to save computation time. This should be
  450. an array with the indices of the k-nearest neighbors as a row for
  451. each data point.
  452. knn_dists: array of shape (n_samples, n_neighbors) (optional)
  453. If the k-nearest neighbors of each point has already been calculated
  454. you can pass them in here to save computation time. This should be
  455. an array with the distances of the k-nearest neighbors as a row for
  456. each data point.
  457. angular: bool (optional, default False)
  458. Whether to use angular/cosine distance for the random projection
  459. forest for seeding NN-descent to determine approximate nearest
  460. neighbors.
  461. set_op_mix_ratio: float (optional, default 1.0)
  462. Interpolate between (fuzzy) union and intersection as the set operation
  463. used to combine local fuzzy simplicial sets to obtain a global fuzzy
  464. simplicial sets. Both fuzzy set operations use the product t-norm.
  465. The value of this parameter should be between 0.0 and 1.0; a value of
  466. 1.0 will use a pure fuzzy union, while 0.0 will use a pure fuzzy
  467. intersection.
  468. local_connectivity: int (optional, default 1)
  469. The local connectivity required -- i.e. the number of nearest
  470. neighbors that should be assumed to be connected at a local level.
  471. The higher this value the more connected the manifold becomes
  472. locally. In practice this should be not more than the local intrinsic
  473. dimension of the manifold.
  474. verbose: bool (optional, default False)
  475. Whether to report information on the current progress of the algorithm.
  476. return_dists: bool or None (optional, default None)
  477. Whether to return the pairwise distance associated with each edge.
  478. Returns
  479. -------
  480. fuzzy_simplicial_set: coo_matrix
  481. A fuzzy simplicial set represented as a sparse matrix. The (i,
  482. j) entry of the matrix represents the membership strength of the
  483. 1-simplex between the ith and jth sample points.
  484. """
  485. if knn_indices is None or knn_dists is None:
  486. knn_indices, knn_dists, _ = nearest_neighbors(
  487. X,
  488. n_neighbors,
  489. metric,
  490. metric_kwds,
  491. angular,
  492. random_state,
  493. verbose=verbose,
  494. )
  495. knn_dists = knn_dists.astype(np.float32, copy=False)
  496. sigmas, rhos = smooth_knn_dist(
  497. knn_dists,
  498. float(n_neighbors),
  499. local_connectivity=float(local_connectivity),
  500. )
  501. rows, cols, vals, dists = compute_membership_strengths(
  502. knn_indices, knn_dists, sigmas, rhos, return_dists
  503. )
  504. result = scipy.sparse.coo_matrix(
  505. (vals, (rows, cols)), shape=(X.shape[0], X.shape[0])
  506. )
  507. result.eliminate_zeros()
  508. if apply_set_operations:
  509. transpose = result.transpose()
  510. prod_matrix = result.multiply(transpose)
  511. if set_op_mix_ratio == 1.0:
  512. # Default fuzzy union: the (1 - ratio) * prod_matrix term is zero, so
  513. # skip building/scaling/adding it (saves redundant sparse temporaries).
  514. result = result + transpose - prod_matrix
  515. elif set_op_mix_ratio == 0.0:
  516. # Pure fuzzy intersection.
  517. result = prod_matrix
  518. else:
  519. result = (
  520. set_op_mix_ratio * (result + transpose - prod_matrix)
  521. + (1.0 - set_op_mix_ratio) * prod_matrix
  522. )
  523. result.eliminate_zeros()
  524. if return_dists is None:
  525. return result, sigmas, rhos
  526. else:
  527. if return_dists:
  528. dmat = scipy.sparse.coo_matrix(
  529. (dists, (rows, cols)), shape=(X.shape[0], X.shape[0])
  530. )
  531. dists = dmat.maximum(dmat.transpose()).todok()
  532. else:
  533. dists = None
  534. return result, sigmas, rhos, dists
  535. @numba.njit()
  536. def fast_intersection(rows, cols, values, target, unknown_dist=1.0, far_dist=5.0):
  537. """Under the assumption of categorical distance for the intersecting
  538. simplicial set perform a fast intersection.
  539. Parameters
  540. ----------
  541. rows: array
  542. An array of the row of each non-zero in the sparse matrix
  543. representation.
  544. cols: array
  545. An array of the column of each non-zero in the sparse matrix
  546. representation.
  547. values: array
  548. An array of the value of each non-zero in the sparse matrix
  549. representation.
  550. target: array of shape (n_samples)
  551. The categorical labels to use in the intersection.
  552. unknown_dist: float (optional, default 1.0)
  553. The distance an unknown label (-1) is assumed to be from any point.
  554. far_dist float (optional, default 5.0)
  555. The distance between unmatched labels.
  556. Returns
  557. -------
  558. None
  559. """
  560. for nz in range(rows.shape[0]):
  561. i = rows[nz]
  562. j = cols[nz]
  563. if (target[i] == -1) or (target[j] == -1):
  564. values[nz] *= np.exp(-unknown_dist)
  565. elif target[i] != target[j]:
  566. values[nz] *= np.exp(-far_dist)
  567. return
  568. @numba.njit()
  569. def fast_metric_intersection(
  570. rows, cols, values, discrete_space, metric, metric_args, scale
  571. ):
  572. """Under the assumption of categorical distance for the intersecting
  573. simplicial set perform a fast intersection.
  574. Parameters
  575. ----------
  576. rows: array
  577. An array of the row of each non-zero in the sparse matrix
  578. representation.
  579. cols: array
  580. An array of the column of each non-zero in the sparse matrix
  581. representation.
  582. values: array of shape
  583. An array of the values of each non-zero in the sparse matrix
  584. representation.
  585. discrete_space: array of shape (n_samples, n_features)
  586. The vectors of categorical labels to use in the intersection.
  587. metric: numba function
  588. The function used to calculate distance over the target array.
  589. scale: float
  590. A scaling to apply to the metric.
  591. Returns
  592. -------
  593. None
  594. """
  595. for nz in range(rows.shape[0]):
  596. i = rows[nz]
  597. j = cols[nz]
  598. dist = metric(discrete_space[i], discrete_space[j], *metric_args)
  599. values[nz] *= np.exp(-(scale * dist))
  600. return
  601. @numba.njit()
  602. def reprocess_row(probabilities, k=15, n_iters=32):
  603. target = np.log2(k)
  604. lo = 0.0
  605. hi = NPY_INFINITY
  606. mid = 1.0
  607. for n in range(n_iters):
  608. psum = 0.0
  609. for j in range(probabilities.shape[0]):
  610. psum += pow(probabilities[j], mid)
  611. if np.fabs(psum - target) < SMOOTH_K_TOLERANCE:
  612. break
  613. if psum < target:
  614. hi = mid
  615. mid = (lo + hi) / 2.0
  616. else:
  617. lo = mid
  618. if hi == NPY_INFINITY:
  619. mid *= 2
  620. else:
  621. mid = (lo + hi) / 2.0
  622. return np.power(probabilities, mid)
  623. @numba.njit()
  624. def reset_local_metrics(simplicial_set_indptr, simplicial_set_data):
  625. for i in range(simplicial_set_indptr.shape[0] - 1):
  626. simplicial_set_data[simplicial_set_indptr[i] : simplicial_set_indptr[i + 1]] = (
  627. reprocess_row(
  628. simplicial_set_data[
  629. simplicial_set_indptr[i] : simplicial_set_indptr[i + 1]
  630. ]
  631. )
  632. )
  633. return
  634. def reset_local_connectivity(simplicial_set, reset_local_metric=False):
  635. """Reset the local connectivity requirement -- each data sample should
  636. have complete confidence in at least one 1-simplex in the simplicial set.
  637. We can enforce this by locally rescaling confidences, and then remerging the
  638. different local simplicial sets together.
  639. Parameters
  640. ----------
  641. simplicial_set: sparse matrix
  642. The simplicial set for which to recalculate with respect to local
  643. connectivity.
  644. Returns
  645. -------
  646. simplicial_set: sparse_matrix
  647. The recalculated simplicial set, now with the local connectivity
  648. assumption restored.
  649. """
  650. simplicial_set = normalize(simplicial_set, norm="max")
  651. if reset_local_metric:
  652. simplicial_set = simplicial_set.tocsr()
  653. reset_local_metrics(simplicial_set.indptr, simplicial_set.data)
  654. simplicial_set = simplicial_set.tocoo()
  655. transpose = simplicial_set.transpose()
  656. prod_matrix = simplicial_set.multiply(transpose)
  657. simplicial_set = simplicial_set + transpose - prod_matrix
  658. simplicial_set.eliminate_zeros()
  659. return simplicial_set
  660. def discrete_metric_simplicial_set_intersection(
  661. simplicial_set,
  662. discrete_space,
  663. unknown_dist=1.0,
  664. far_dist=5.0,
  665. metric=None,
  666. metric_kws={},
  667. metric_scale=1.0,
  668. ):
  669. """Combine a fuzzy simplicial set with another fuzzy simplicial set
  670. generated from discrete metric data using discrete distances. The target
  671. data is assumed to be categorical label data (a vector of labels),
  672. and this will update the fuzzy simplicial set to respect that label data.
  673. TODO: optional category cardinality based weighting of distance
  674. Parameters
  675. ----------
  676. simplicial_set: sparse matrix
  677. The input fuzzy simplicial set.
  678. discrete_space: array of shape (n_samples)
  679. The categorical labels to use in the intersection.
  680. unknown_dist: float (optional, default 1.0)
  681. The distance an unknown label (-1) is assumed to be from any point.
  682. far_dist: float (optional, default 5.0)
  683. The distance between unmatched labels.
  684. metric: str (optional, default None)
  685. If not None, then use this metric to determine the
  686. distance between values.
  687. metric_scale: float (optional, default 1.0)
  688. If using a custom metric scale the distance values by
  689. this value -- this controls the weighting of the
  690. intersection. Larger values weight more toward target.
  691. Returns
  692. -------
  693. simplicial_set: sparse matrix
  694. The resulting intersected fuzzy simplicial set.
  695. """
  696. simplicial_set = simplicial_set.tocoo()
  697. if metric is not None:
  698. # We presume target is now a 2d array, with each row being a
  699. # vector of target info
  700. if metric in dist.named_distances:
  701. metric_func = dist.named_distances[metric]
  702. else:
  703. raise ValueError("Discrete intersection metric is not recognized")
  704. fast_metric_intersection(
  705. simplicial_set.row,
  706. simplicial_set.col,
  707. simplicial_set.data,
  708. discrete_space,
  709. metric_func,
  710. tuple(metric_kws.values()),
  711. metric_scale,
  712. )
  713. else:
  714. fast_intersection(
  715. simplicial_set.row,
  716. simplicial_set.col,
  717. simplicial_set.data,
  718. discrete_space,
  719. unknown_dist,
  720. far_dist,
  721. )
  722. simplicial_set.eliminate_zeros()
  723. return reset_local_connectivity(simplicial_set)
  724. def general_simplicial_set_intersection(
  725. simplicial_set1, simplicial_set2, weight=0.5, right_complement=False
  726. ):
  727. if right_complement:
  728. result = simplicial_set1.tocoo()
  729. else:
  730. result = (simplicial_set1 + simplicial_set2).tocoo()
  731. left = simplicial_set1.tocsr()
  732. right = simplicial_set2.tocsr()
  733. sparse.general_sset_intersection(
  734. left.indptr,
  735. left.indices,
  736. left.data,
  737. right.indptr,
  738. right.indices,
  739. right.data,
  740. result.row,
  741. result.col,
  742. result.data,
  743. mix_weight=weight,
  744. right_complement=right_complement,
  745. )
  746. return result
  747. def general_simplicial_set_union(simplicial_set1, simplicial_set2):
  748. result = (simplicial_set1 + simplicial_set2).tocoo()
  749. left = simplicial_set1.tocsr()
  750. right = simplicial_set2.tocsr()
  751. sparse.general_sset_union(
  752. left.indptr,
  753. left.indices,
  754. left.data,
  755. right.indptr,
  756. right.indices,
  757. right.data,
  758. result.row,
  759. result.col,
  760. result.data,
  761. )
  762. return result
  763. def make_epochs_per_sample(weights, n_epochs):
  764. """Given a set of weights and number of epochs generate the number of
  765. epochs per sample for each weight.
  766. Parameters
  767. ----------
  768. weights: array of shape (n_1_simplices)
  769. The weights of how much we wish to sample each 1-simplex.
  770. n_epochs: int
  771. The total number of epochs we want to train for.
  772. Returns
  773. -------
  774. An array of number of epochs per sample, one for each 1-simplex.
  775. """
  776. result = np.full(weights.shape[0], -1.0, dtype=np.float64)
  777. n_samples = n_epochs * (weights / weights.max())
  778. positive = n_samples > 0 # compute the mask once instead of twice
  779. result[positive] = float(n_epochs) / np.float64(n_samples[positive])
  780. return result
  781. # scale coords so that the largest coordinate is max_coords, then add normal-distributed
  782. # noise with standard deviation noise
  783. def noisy_scale_coords(coords, random_state, max_coord=10.0, noise=0.0001):
  784. expansion = max_coord / np.abs(coords).max()
  785. coords = (coords * expansion).astype(np.float32)
  786. return coords + random_state.normal(scale=noise, size=coords.shape).astype(
  787. np.float32
  788. )
  789. @numba.njit()
  790. def _densmap_original_densities(
  791. head, tail, graph_data, dists_indptr, dists_indices, dists_data, ro, mu_sum
  792. ):
  793. for i in range(len(head)):
  794. j = head[i]
  795. k = tail[i]
  796. d_val = 0.0
  797. for idx in range(dists_indptr[j], dists_indptr[j + 1]):
  798. if dists_indices[idx] == k:
  799. d_val = dists_data[idx]
  800. break
  801. D = d_val * d_val
  802. mu = graph_data[i]
  803. ro[j] += mu * D
  804. ro[k] += mu * D
  805. mu_sum[j] += mu
  806. mu_sum[k] += mu
  807. @numba.njit()
  808. def _densmap_embedding_densities(
  809. head, tail, graph_data, dists_indptr, dists_indices, dists_data, re, mu_sum
  810. ):
  811. for i in range(len(head)):
  812. j = head[i]
  813. k = tail[i]
  814. d_val = 0.0
  815. for idx in range(dists_indptr[j], dists_indptr[j + 1]):
  816. if dists_indices[idx] == k:
  817. d_val = dists_data[idx]
  818. break
  819. mu = graph_data[i]
  820. weighted = mu * d_val
  821. re[j] += weighted
  822. re[k] += weighted
  823. mu_sum[j] += mu
  824. mu_sum[k] += mu
  825. def simplicial_set_embedding(
  826. data,
  827. graph,
  828. n_components,
  829. initial_alpha,
  830. a,
  831. b,
  832. gamma,
  833. negative_sample_rate,
  834. n_epochs,
  835. init,
  836. random_state,
  837. metric,
  838. metric_kwds,
  839. densmap,
  840. densmap_kwds,
  841. output_dens,
  842. output_metric=dist.named_distances_with_gradients["euclidean"],
  843. output_metric_kwds={},
  844. euclidean_output=True,
  845. parallel=False,
  846. verbose=False,
  847. tqdm_kwds=None,
  848. ):
  849. """Perform a fuzzy simplicial set embedding, using a specified
  850. initialisation method and then minimizing the fuzzy set cross entropy
  851. between the 1-skeletons of the high and low dimensional fuzzy simplicial
  852. sets.
  853. Parameters
  854. ----------
  855. data: array of shape (n_samples, n_features)
  856. The source data to be embedded by UMAP.
  857. graph: sparse matrix
  858. The 1-skeleton of the high dimensional fuzzy simplicial set as
  859. represented by a graph for which we require a sparse matrix for the
  860. (weighted) adjacency matrix.
  861. n_components: int
  862. The dimensionality of the euclidean space into which to embed the data.
  863. initial_alpha: float
  864. Initial learning rate for the SGD.
  865. a: float
  866. Parameter of differentiable approximation of right adjoint functor
  867. b: float
  868. Parameter of differentiable approximation of right adjoint functor
  869. gamma: float
  870. Weight to apply to negative samples.
  871. negative_sample_rate: int (optional, default 5)
  872. The number of negative samples to select per positive sample
  873. in the optimization process. Increasing this value will result
  874. in greater repulsive force being applied, greater optimization
  875. cost, but slightly more accuracy.
  876. n_epochs: int (optional, default 0), or list of int
  877. The number of training epochs to be used in optimizing the
  878. low dimensional embedding. Larger values result in more accurate
  879. embeddings. If 0 is specified a value will be selected based on
  880. the size of the input dataset (200 for large datasets, 500 for small).
  881. If a list of int is specified, then the intermediate embeddings at the
  882. different epochs specified in that list are returned in
  883. ``aux_data["embedding_list"]``.
  884. init: string
  885. How to initialize the low dimensional embedding. Options are:
  886. * 'spectral': use a spectral embedding of the fuzzy 1-skeleton
  887. * 'random': assign initial embedding positions at random.
  888. * 'pca': use the first n_components from PCA applied to the input data.
  889. * A numpy array of initial embedding positions.
  890. random_state: numpy RandomState or equivalent
  891. A state capable being used as a numpy random state.
  892. metric: string or callable
  893. The metric used to measure distance in high dimensional space; used if
  894. multiple connected components need to be layed out.
  895. metric_kwds: dict
  896. Key word arguments to be passed to the metric function; used if
  897. multiple connected components need to be layed out.
  898. densmap: bool
  899. Whether to use the density-augmented objective function to optimize
  900. the embedding according to the densMAP algorithm.
  901. densmap_kwds: dict
  902. Key word arguments to be used by the densMAP optimization.
  903. output_dens: bool
  904. Whether to output local radii in the original data and the embedding.
  905. output_metric: function
  906. Function returning the distance between two points in embedding space and
  907. the gradient of the distance wrt the first argument.
  908. output_metric_kwds: dict
  909. Key word arguments to be passed to the output_metric function.
  910. euclidean_output: bool
  911. Whether to use the faster code specialised for euclidean output metrics
  912. parallel: bool (optional, default False)
  913. Whether to run the computation using numba parallel.
  914. Running in parallel is non-deterministic, and is not used
  915. if a random seed has been set, to ensure reproducibility.
  916. verbose: bool (optional, default False)
  917. Whether to report information on the current progress of the algorithm.
  918. tqdm_kwds: dict
  919. Key word arguments to be used by the tqdm progress bar.
  920. Returns
  921. -------
  922. embedding: array of shape (n_samples, n_components)
  923. The optimized of ``graph`` into an ``n_components`` dimensional
  924. euclidean space.
  925. aux_data: dict
  926. Auxiliary output returned with the embedding. When densMAP extension
  927. is turned on, this dictionary includes local radii in the original
  928. data (``rad_orig``) and in the embedding (``rad_emb``).
  929. """
  930. graph = graph.tocoo()
  931. graph.sum_duplicates()
  932. n_vertices = graph.shape[1]
  933. # For smaller datasets we can use more epochs
  934. if graph.shape[0] <= 10000:
  935. default_epochs = 500
  936. else:
  937. default_epochs = 200
  938. # Use more epochs for densMAP
  939. if densmap:
  940. default_epochs += 200
  941. if n_epochs is None:
  942. n_epochs = default_epochs
  943. # If n_epoch is a list, get the maximum epoch to reach
  944. n_epochs_max = max(n_epochs) if isinstance(n_epochs, list) else n_epochs
  945. if n_epochs_max > 10:
  946. graph.data[graph.data < (graph.data.max() / float(n_epochs_max))] = 0.0
  947. else:
  948. graph.data[graph.data < (graph.data.max() / float(default_epochs))] = 0.0
  949. graph.eliminate_zeros()
  950. if isinstance(init, str) and init == "random":
  951. embedding = random_state.uniform(
  952. low=-10.0, high=10.0, size=(graph.shape[0], n_components)
  953. ).astype(np.float32)
  954. elif isinstance(init, str) and init == "pca":
  955. if scipy.sparse.issparse(data):
  956. pca = TruncatedSVD(n_components=n_components, random_state=random_state)
  957. else:
  958. pca = PCA(n_components=n_components, random_state=random_state)
  959. embedding = pca.fit_transform(data).astype(np.float32)
  960. embedding = noisy_scale_coords(
  961. embedding, random_state, max_coord=10, noise=0.0001
  962. )
  963. elif isinstance(init, str) and init == "spectral":
  964. embedding = spectral_layout(
  965. data,
  966. graph,
  967. n_components,
  968. random_state,
  969. metric=metric,
  970. metric_kwds=metric_kwds,
  971. )
  972. # We add a little noise to avoid local minima for optimization to come
  973. embedding = noisy_scale_coords(
  974. embedding, random_state, max_coord=10, noise=0.0001
  975. )
  976. elif isinstance(init, str) and init == "tswspectral":
  977. embedding = tswspectral_layout(
  978. data,
  979. graph,
  980. n_components,
  981. random_state,
  982. metric=metric,
  983. metric_kwds=metric_kwds,
  984. )
  985. embedding = noisy_scale_coords(
  986. embedding, random_state, max_coord=10, noise=0.0001
  987. )
  988. else:
  989. init_data = np.array(init)
  990. if len(init_data.shape) == 2:
  991. if np.unique(init_data, axis=0).shape[0] < init_data.shape[0]:
  992. tree = KDTree(init_data)
  993. dist, ind = tree.query(init_data, k=2)
  994. nndist = np.mean(dist[:, 1])
  995. embedding = init_data + random_state.normal(
  996. scale=0.001 * nndist, size=init_data.shape
  997. ).astype(np.float32)
  998. else:
  999. embedding = init_data
  1000. epochs_per_sample = make_epochs_per_sample(graph.data, n_epochs_max)
  1001. head = graph.row
  1002. tail = graph.col
  1003. weight = graph.data
  1004. rng_state = random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)
  1005. aux_data = {}
  1006. if densmap or output_dens:
  1007. if verbose:
  1008. print(ts() + " Computing original densities")
  1009. dists = densmap_kwds["graph_dists"]
  1010. mu_sum = np.zeros(n_vertices, dtype=np.float32)
  1011. ro = np.zeros(n_vertices, dtype=np.float32)
  1012. dists_csr = dists.tocsr()
  1013. _densmap_original_densities(
  1014. head,
  1015. tail,
  1016. graph.data,
  1017. dists_csr.indptr,
  1018. dists_csr.indices,
  1019. dists_csr.data,
  1020. ro,
  1021. mu_sum,
  1022. )
  1023. epsilon = 1e-8
  1024. ro = np.log(epsilon + (ro / mu_sum))
  1025. if densmap:
  1026. R = (ro - np.mean(ro)) / np.std(ro)
  1027. densmap_kwds["mu"] = graph.data
  1028. densmap_kwds["mu_sum"] = mu_sum
  1029. densmap_kwds["R"] = R
  1030. if output_dens:
  1031. aux_data["rad_orig"] = ro
  1032. embedding = (
  1033. 10.0
  1034. * (embedding - np.min(embedding, 0))
  1035. / (np.max(embedding, 0) - np.min(embedding, 0))
  1036. ).astype(np.float32, order="C")
  1037. if euclidean_output:
  1038. embedding = optimize_layout_euclidean(
  1039. embedding,
  1040. embedding,
  1041. head,
  1042. tail,
  1043. n_epochs,
  1044. n_vertices,
  1045. epochs_per_sample,
  1046. a,
  1047. b,
  1048. rng_state,
  1049. gamma,
  1050. initial_alpha,
  1051. negative_sample_rate,
  1052. parallel=parallel,
  1053. verbose=verbose,
  1054. densmap=densmap,
  1055. densmap_kwds=densmap_kwds,
  1056. tqdm_kwds=tqdm_kwds,
  1057. move_other=True,
  1058. )
  1059. else:
  1060. embedding = optimize_layout_generic(
  1061. embedding,
  1062. embedding,
  1063. head,
  1064. tail,
  1065. n_epochs,
  1066. n_vertices,
  1067. epochs_per_sample,
  1068. a,
  1069. b,
  1070. rng_state,
  1071. gamma,
  1072. initial_alpha,
  1073. negative_sample_rate,
  1074. output_metric,
  1075. tuple(output_metric_kwds.values()),
  1076. verbose=verbose,
  1077. tqdm_kwds=tqdm_kwds,
  1078. move_other=True,
  1079. )
  1080. if isinstance(embedding, list):
  1081. aux_data["embedding_list"] = embedding
  1082. embedding = embedding[-1].copy()
  1083. if output_dens:
  1084. if verbose:
  1085. print(ts() + " Computing embedding densities")
  1086. # Compute graph in embedding
  1087. (
  1088. knn_indices,
  1089. knn_dists,
  1090. rp_forest,
  1091. ) = nearest_neighbors(
  1092. embedding,
  1093. densmap_kwds["n_neighbors"],
  1094. "euclidean",
  1095. {},
  1096. False,
  1097. random_state,
  1098. verbose=verbose,
  1099. )
  1100. emb_graph, emb_sigmas, emb_rhos, emb_dists = fuzzy_simplicial_set(
  1101. embedding,
  1102. densmap_kwds["n_neighbors"],
  1103. random_state,
  1104. "euclidean",
  1105. {},
  1106. knn_indices,
  1107. knn_dists,
  1108. verbose=verbose,
  1109. return_dists=True,
  1110. )
  1111. emb_graph = emb_graph.tocoo()
  1112. emb_graph.sum_duplicates()
  1113. emb_graph.eliminate_zeros()
  1114. n_vertices = emb_graph.shape[1]
  1115. mu_sum = np.zeros(n_vertices, dtype=np.float32)
  1116. re = np.zeros(n_vertices, dtype=np.float32)
  1117. head = emb_graph.row
  1118. tail = emb_graph.col
  1119. emb_dists_csr = emb_dists.tocsr()
  1120. _densmap_embedding_densities(
  1121. head,
  1122. tail,
  1123. emb_graph.data,
  1124. emb_dists_csr.indptr,
  1125. emb_dists_csr.indices,
  1126. emb_dists_csr.data,
  1127. re,
  1128. mu_sum,
  1129. )
  1130. epsilon = 1e-8
  1131. re = np.log(epsilon + (re / mu_sum))
  1132. aux_data["rad_emb"] = re
  1133. return embedding, aux_data
  1134. @numba.njit()
  1135. def init_transform(indices, weights, embedding):
  1136. """Given indices and weights and an original embeddings
  1137. initialize the positions of new points relative to the
  1138. indices and weights (of their neighbors in the source data).
  1139. Parameters
  1140. ----------
  1141. indices: array of shape (n_new_samples, n_neighbors)
  1142. The indices of the neighbors of each new sample
  1143. weights: array of shape (n_new_samples, n_neighbors)
  1144. The membership strengths of associated 1-simplices
  1145. for each of the new samples.
  1146. embedding: array of shape (n_samples, dim)
  1147. The original embedding of the source data.
  1148. Returns
  1149. -------
  1150. new_embedding: array of shape (n_new_samples, dim)
  1151. An initial embedding of the new sample points.
  1152. """
  1153. result = np.zeros((indices.shape[0], embedding.shape[1]), dtype=np.float32)
  1154. for i in range(indices.shape[0]):
  1155. for j in range(indices.shape[1]):
  1156. for d in range(embedding.shape[1]):
  1157. result[i, d] += weights[i, j] * embedding[indices[i, j], d]
  1158. return result
  1159. def init_graph_transform(graph, embedding):
  1160. """Given a bipartite graph representing the 1-simplices and strengths between the
  1161. new points and the original data set along with an embedding of the original points
  1162. initialize the positions of new points relative to the strengths (of their neighbors in the source data).
  1163. If a point is in our original data set it embeds at the original points coordinates.
  1164. If a point has no neighbours in our original dataset it embeds as the np.nan vector.
  1165. Otherwise a point is the weighted average of it's neighbours embedding locations.
  1166. Parameters
  1167. ----------
  1168. graph: csr_matrix (n_new_samples, n_samples)
  1169. A matrix indicating the 1-simplices and their associated strengths. These strengths should
  1170. be values between zero and one and not normalized. One indicating that the new point was identical
  1171. to one of our original points.
  1172. embedding: array of shape (n_samples, dim)
  1173. The original embedding of the source data.
  1174. Returns
  1175. -------
  1176. new_embedding: array of shape (n_new_samples, dim)
  1177. An initial embedding of the new sample points.
  1178. """
  1179. n_new = graph.shape[0]
  1180. result = np.zeros((n_new, embedding.shape[1]), dtype=np.float32)
  1181. row_nnz = np.diff(graph.indptr)
  1182. empty_mask = row_nnz == 0
  1183. result[empty_mask] = np.nan
  1184. has_exact = np.zeros(n_new, dtype=bool)
  1185. exact_data_mask = graph.data == 1.0
  1186. if exact_data_mask.any():
  1187. exact_positions = np.where(exact_data_mask)[0]
  1188. exact_rows = np.searchsorted(graph.indptr, exact_positions, side="right") - 1
  1189. _, first_idx = np.unique(exact_rows, return_index=True)
  1190. unique_rows = exact_rows[first_idx]
  1191. exact_cols = graph.indices[exact_positions[first_idx]]
  1192. has_exact[unique_rows] = True
  1193. result[unique_rows] = embedding[exact_cols]
  1194. avg_mask = ~empty_mask & ~has_exact
  1195. if np.any(avg_mask):
  1196. avg_graph = graph[avg_mask]
  1197. row_sums = np.array(avg_graph.sum(axis=1)).flatten()
  1198. inv_sums = scipy.sparse.diags(1.0 / row_sums)
  1199. normalized = inv_sums @ avg_graph
  1200. result[avg_mask] = (normalized @ embedding).astype(np.float32)
  1201. return result
  1202. @numba.njit()
  1203. def init_update(current_init, n_original_samples, indices):
  1204. for i in range(n_original_samples, indices.shape[0]):
  1205. n = 0
  1206. for j in range(indices.shape[1]):
  1207. for d in range(current_init.shape[1]):
  1208. if indices[i, j] < n_original_samples:
  1209. n += 1
  1210. current_init[i, d] += current_init[indices[i, j], d]
  1211. for d in range(current_init.shape[1]):
  1212. current_init[i, d] /= n
  1213. return
  1214. def find_ab_params(spread, min_dist):
  1215. """Fit a, b params for the differentiable curve used in lower
  1216. dimensional fuzzy simplicial complex construction. We want the
  1217. smooth curve (from a pre-defined family with simple gradient) that
  1218. best matches an offset exponential decay.
  1219. """
  1220. def curve(x, a, b):
  1221. return 1.0 / (1.0 + a * x ** (2 * b))
  1222. xv = np.linspace(0, spread * 3, 300)
  1223. yv = np.zeros(xv.shape)
  1224. yv[xv < min_dist] = 1.0
  1225. yv[xv >= min_dist] = np.exp(-(xv[xv >= min_dist] - min_dist) / spread)
  1226. params, covar = curve_fit(curve, xv, yv)
  1227. return params[0], params[1]
  1228. class UMAP(BaseEstimator, ClassNamePrefixFeaturesOutMixin):
  1229. """Uniform Manifold Approximation and Projection
  1230. Finds a low dimensional embedding of the data that approximates
  1231. an underlying manifold.
  1232. Parameters
  1233. ----------
  1234. n_neighbors: float (optional, default 15)
  1235. The size of local neighborhood (in terms of number of neighboring
  1236. sample points) used for manifold approximation. Larger values
  1237. result in more global views of the manifold, while smaller
  1238. values result in more local data being preserved. In general
  1239. values should be in the range 2 to 100.
  1240. n_components: int (optional, default 2)
  1241. The dimension of the space to embed into. This defaults to 2 to
  1242. provide easy visualization, but can reasonably be set to any
  1243. integer value in the range 2 to 100.
  1244. metric: string or function (optional, default 'euclidean')
  1245. The metric to use to compute distances in high dimensional space.
  1246. If a string is passed it must match a valid predefined metric. If
  1247. a general metric is required a function that takes two 1d arrays and
  1248. returns a float can be provided. For performance purposes it is
  1249. required that this be a numba jit'd function. Valid string metrics
  1250. include:
  1251. * euclidean
  1252. * manhattan
  1253. * chebyshev
  1254. * minkowski
  1255. * canberra
  1256. * braycurtis
  1257. * mahalanobis
  1258. * wminkowski
  1259. * seuclidean
  1260. * cosine
  1261. * correlation
  1262. * haversine
  1263. * hamming
  1264. * jaccard
  1265. * dice
  1266. * russelrao
  1267. * kulsinski
  1268. * ll_dirichlet
  1269. * hellinger
  1270. * rogerstanimoto
  1271. * sokalmichener
  1272. * sokalsneath
  1273. * yule
  1274. Metrics that take arguments (such as minkowski, mahalanobis etc.)
  1275. can have arguments passed via the metric_kwds dictionary. At this
  1276. time care must be taken and dictionary elements must be ordered
  1277. appropriately; this will hopefully be fixed in the future.
  1278. n_epochs: int (optional, default None)
  1279. The number of training epochs to be used in optimizing the
  1280. low dimensional embedding. Larger values result in more accurate
  1281. embeddings. If None is specified a value will be selected based on
  1282. the size of the input dataset (200 for large datasets, 500 for small).
  1283. learning_rate: float (optional, default 1.0)
  1284. The initial learning rate for the embedding optimization.
  1285. init: string (optional, default 'spectral')
  1286. How to initialize the low dimensional embedding. Options are:
  1287. * 'spectral': use a spectral embedding of the fuzzy 1-skeleton
  1288. * 'random': assign initial embedding positions at random.
  1289. * 'pca': use the first n_components from PCA applied to the
  1290. input data.
  1291. * 'tswspectral': use a spectral embedding of the fuzzy
  1292. 1-skeleton, using a truncated singular value decomposition to
  1293. "warm" up the eigensolver. This is intended as an alternative
  1294. to the 'spectral' method, if that takes an excessively long
  1295. time to complete initialization (or fails to complete).
  1296. * A numpy array of initial embedding positions.
  1297. min_dist: float (optional, default 0.1)
  1298. The effective minimum distance between embedded points. Smaller values
  1299. will result in a more clustered/clumped embedding where nearby points
  1300. on the manifold are drawn closer together, while larger values will
  1301. result on a more even dispersal of points. The value should be set
  1302. relative to the ``spread`` value, which determines the scale at which
  1303. embedded points will be spread out.
  1304. spread: float (optional, default 1.0)
  1305. The effective scale of embedded points. In combination with ``min_dist``
  1306. this determines how clustered/clumped the embedded points are.
  1307. low_memory: bool (optional, default True)
  1308. For some datasets the nearest neighbor computation can consume a lot of
  1309. memory. If you find that UMAP is failing due to memory constraints
  1310. consider setting this option to True. This approach is more
  1311. computationally expensive, but avoids excessive memory use.
  1312. set_op_mix_ratio: float (optional, default 1.0)
  1313. Interpolate between (fuzzy) union and intersection as the set operation
  1314. used to combine local fuzzy simplicial sets to obtain a global fuzzy
  1315. simplicial sets. Both fuzzy set operations use the product t-norm.
  1316. The value of this parameter should be between 0.0 and 1.0; a value of
  1317. 1.0 will use a pure fuzzy union, while 0.0 will use a pure fuzzy
  1318. intersection.
  1319. local_connectivity: int (optional, default 1)
  1320. The local connectivity required -- i.e. the number of nearest
  1321. neighbors that should be assumed to be connected at a local level.
  1322. The higher this value the more connected the manifold becomes
  1323. locally. In practice this should be not more than the local intrinsic
  1324. dimension of the manifold.
  1325. repulsion_strength: float (optional, default 1.0)
  1326. Weighting applied to negative samples in low dimensional embedding
  1327. optimization. Values higher than one will result in greater weight
  1328. being given to negative samples.
  1329. negative_sample_rate: int (optional, default 5)
  1330. The number of negative samples to select per positive sample
  1331. in the optimization process. Increasing this value will result
  1332. in greater repulsive force being applied, greater optimization
  1333. cost, but slightly more accuracy.
  1334. transform_queue_size: float (optional, default 4.0)
  1335. For transform operations (embedding new points using a trained model
  1336. this will control how aggressively to search for nearest neighbors.
  1337. Larger values will result in slower performance but more accurate
  1338. nearest neighbor evaluation.
  1339. a: float (optional, default None)
  1340. More specific parameters controlling the embedding. If None these
  1341. values are set automatically as determined by ``min_dist`` and
  1342. ``spread``.
  1343. b: float (optional, default None)
  1344. More specific parameters controlling the embedding. If None these
  1345. values are set automatically as determined by ``min_dist`` and
  1346. ``spread``.
  1347. random_state: int, RandomState instance or None, optional (default: None)
  1348. If int, random_state is the seed used by the random number generator;
  1349. If RandomState instance, random_state is the random number generator;
  1350. If None, the random number generator is the RandomState instance used
  1351. by `np.random`.
  1352. metric_kwds: dict (optional, default None)
  1353. Arguments to pass on to the metric, such as the ``p`` value for
  1354. Minkowski distance. If None then no arguments are passed on.
  1355. angular_rp_forest: bool (optional, default False)
  1356. Whether to use an angular random projection forest to initialise
  1357. the approximate nearest neighbor search. This can be faster, but is
  1358. mostly only useful for a metric that uses an angular style distance such
  1359. as cosine, correlation etc. In the case of those metrics angular forests
  1360. will be chosen automatically.
  1361. target_n_neighbors: int (optional, default -1)
  1362. The number of nearest neighbors to use to construct the target simplicial
  1363. set. If set to -1 use the ``n_neighbors`` value.
  1364. target_metric: string or callable (optional, default 'categorical')
  1365. The metric used to measure distance for a target array is using supervised
  1366. dimension reduction. By default this is 'categorical' which will measure
  1367. distance in terms of whether categories match or are different. Furthermore,
  1368. if semi-supervised is required target values of -1 will be trated as
  1369. unlabelled under the 'categorical' metric. If the target array takes
  1370. continuous values (e.g. for a regression problem) then metric of 'l1'
  1371. or 'l2' is probably more appropriate.
  1372. target_metric_kwds: dict (optional, default None)
  1373. Keyword argument to pass to the target metric when performing
  1374. supervised dimension reduction. If None then no arguments are passed on.
  1375. target_weight: float (optional, default 0.5)
  1376. weighting factor between data topology and target topology. A value of
  1377. 0.0 weights predominantly on data, a value of 1.0 places a strong emphasis on
  1378. target. The default of 0.5 balances the weighting equally between data and
  1379. target.
  1380. transform_seed: int (optional, default 42)
  1381. Random seed used for the stochastic aspects of the transform operation.
  1382. This ensures consistency in transform operations.
  1383. verbose: bool (optional, default False)
  1384. Controls verbosity of logging.
  1385. tqdm_kwds: dict (optional, defaul None)
  1386. Key word arguments to be used by the tqdm progress bar.
  1387. unique: bool (optional, default False)
  1388. Controls if the rows of your data should be uniqued before being
  1389. embedded. If you have more duplicates than you have ``n_neighbors``
  1390. you can have the identical data points lying in different regions of
  1391. your space. It also violates the definition of a metric.
  1392. For to map from internal structures back to your data use the variable
  1393. _unique_inverse_.
  1394. densmap: bool (optional, default False)
  1395. Specifies whether the density-augmented objective of densMAP
  1396. should be used for optimization. Turning on this option generates
  1397. an embedding where the local densities are encouraged to be correlated
  1398. with those in the original space. Parameters below with the prefix 'dens'
  1399. further control the behavior of this extension.
  1400. dens_lambda: float (optional, default 2.0)
  1401. Controls the regularization weight of the density correlation term
  1402. in densMAP. Higher values prioritize density preservation over the
  1403. UMAP objective, and vice versa for values closer to zero. Setting this
  1404. parameter to zero is equivalent to running the original UMAP algorithm.
  1405. dens_frac: float (optional, default 0.3)
  1406. Controls the fraction of epochs (between 0 and 1) where the
  1407. density-augmented objective is used in densMAP. The first
  1408. (1 - dens_frac) fraction of epochs optimize the original UMAP objective
  1409. before introducing the density correlation term.
  1410. dens_var_shift: float (optional, default 0.1)
  1411. A small constant added to the variance of local radii in the
  1412. embedding when calculating the density correlation objective to
  1413. prevent numerical instability from dividing by a small number
  1414. output_dens: float (optional, default False)
  1415. Determines whether the local radii of the final embedding (an inverse
  1416. measure of local density) are computed and returned in addition to
  1417. the embedding. If set to True, local radii of the original data
  1418. are also included in the output for comparison; the output is a tuple
  1419. (embedding, original local radii, embedding local radii). This option
  1420. can also be used when densmap=False to calculate the densities for
  1421. UMAP embeddings.
  1422. disconnection_distance: float (optional, default np.inf or maximal value for bounded distances)
  1423. Disconnect any vertices of distance greater than or equal to disconnection_distance when approximating the
  1424. manifold via our k-nn graph. This is particularly useful in the case that you have a bounded metric. The
  1425. UMAP assumption that we have a connected manifold can be problematic when you have points that are maximally
  1426. different from all the rest of your data. The connected manifold assumption will make such points have perfect
  1427. similarity to a random set of other points. Too many such points will artificially connect your space.
  1428. precomputed_knn: tuple (optional, default (None,None,None))
  1429. If the k-nearest neighbors of each point has already been calculated you
  1430. can pass them in here to save computation time. The number of nearest
  1431. neighbors in the precomputed_knn must be greater or equal to the
  1432. n_neighbors parameter. This should be a tuple containing the output
  1433. of the nearest_neighbors() function or attributes from a previously fit
  1434. UMAP object; (knn_indices, knn_dists, knn_search_index). If you wish to use
  1435. k-nearest neighbors data calculated by another package then provide a tuple of
  1436. the form (knn_indices, knn_dists). The contents of the tuple should be two numpy
  1437. arrays of shape (N, n_neighbors) where N is the number of items in the
  1438. input data. The first array should be the integer indices of the nearest
  1439. neighbors, and the second array should be the corresponding distances. The
  1440. nearest neighbor of each item should be itself, e.g. the nearest neighbor of
  1441. item 0 should be 0, the nearest neighbor of item 1 is 1 and so on. Please note
  1442. that you will *not* be able to transform new data in this case.
  1443. """
  1444. def __init__(
  1445. self,
  1446. n_neighbors=15,
  1447. n_components=2,
  1448. metric="euclidean",
  1449. metric_kwds=None,
  1450. output_metric="euclidean",
  1451. output_metric_kwds=None,
  1452. n_epochs=None,
  1453. learning_rate=1.0,
  1454. init="spectral",
  1455. min_dist=0.1,
  1456. spread=1.0,
  1457. low_memory=True,
  1458. n_jobs=-1,
  1459. set_op_mix_ratio=1.0,
  1460. local_connectivity=1.0,
  1461. repulsion_strength=1.0,
  1462. negative_sample_rate=5,
  1463. transform_queue_size=4.0,
  1464. a=None,
  1465. b=None,
  1466. random_state=None,
  1467. angular_rp_forest=False,
  1468. target_n_neighbors=-1,
  1469. target_metric="categorical",
  1470. target_metric_kwds=None,
  1471. target_weight=0.5,
  1472. transform_seed=42,
  1473. transform_mode="embedding",
  1474. force_approximation_algorithm=False,
  1475. verbose=False,
  1476. tqdm_kwds=None,
  1477. unique=False,
  1478. densmap=False,
  1479. dens_lambda=2.0,
  1480. dens_frac=0.3,
  1481. dens_var_shift=0.1,
  1482. output_dens=False,
  1483. disconnection_distance=None,
  1484. precomputed_knn=(None, None, None),
  1485. ):
  1486. self.n_neighbors = n_neighbors
  1487. self.metric = metric
  1488. self.output_metric = output_metric
  1489. self.target_metric = target_metric
  1490. self.metric_kwds = metric_kwds
  1491. self.output_metric_kwds = output_metric_kwds
  1492. self.n_epochs = n_epochs
  1493. self.init = init
  1494. self.n_components = n_components
  1495. self.repulsion_strength = repulsion_strength
  1496. self.learning_rate = learning_rate
  1497. self.spread = spread
  1498. self.min_dist = min_dist
  1499. self.low_memory = low_memory
  1500. self.set_op_mix_ratio = set_op_mix_ratio
  1501. self.local_connectivity = local_connectivity
  1502. self.negative_sample_rate = negative_sample_rate
  1503. self.random_state = random_state
  1504. self.angular_rp_forest = angular_rp_forest
  1505. self.transform_queue_size = transform_queue_size
  1506. self.target_n_neighbors = target_n_neighbors
  1507. self.target_metric = target_metric
  1508. self.target_metric_kwds = target_metric_kwds
  1509. self.target_weight = target_weight
  1510. self.transform_seed = transform_seed
  1511. self.transform_mode = transform_mode
  1512. self.force_approximation_algorithm = force_approximation_algorithm
  1513. self.verbose = verbose
  1514. self.tqdm_kwds = tqdm_kwds
  1515. self.unique = unique
  1516. self.densmap = densmap
  1517. self.dens_lambda = dens_lambda
  1518. self.dens_frac = dens_frac
  1519. self.dens_var_shift = dens_var_shift
  1520. self.output_dens = output_dens
  1521. self.disconnection_distance = disconnection_distance
  1522. self.precomputed_knn = precomputed_knn
  1523. self.n_jobs = n_jobs
  1524. self.a = a
  1525. self.b = b
  1526. def _validate_parameters(self):
  1527. if self.set_op_mix_ratio < 0.0 or self.set_op_mix_ratio > 1.0:
  1528. raise ValueError("set_op_mix_ratio must be between 0.0 and 1.0")
  1529. if self.repulsion_strength < 0.0:
  1530. raise ValueError("repulsion_strength cannot be negative")
  1531. if self.min_dist > self.spread:
  1532. raise ValueError("min_dist must be less than or equal to spread")
  1533. if self.min_dist < 0.0:
  1534. raise ValueError("min_dist cannot be negative")
  1535. if not isinstance(self.init, str) and not isinstance(self.init, np.ndarray):
  1536. raise ValueError("init must be a string or ndarray")
  1537. if isinstance(self.init, str) and self.init not in (
  1538. "pca",
  1539. "spectral",
  1540. "random",
  1541. "tswspectral",
  1542. ):
  1543. raise ValueError(
  1544. 'string init values must be one of: "pca", "tswspectral",'
  1545. ' "spectral" or "random"'
  1546. )
  1547. if (
  1548. isinstance(self.init, np.ndarray)
  1549. and self.init.shape[1] != self.n_components
  1550. ):
  1551. raise ValueError("init ndarray must match n_components value")
  1552. if not isinstance(self.metric, str) and not callable(self.metric):
  1553. raise ValueError("metric must be string or callable")
  1554. if self.negative_sample_rate < 0:
  1555. raise ValueError("negative sample rate must be positive")
  1556. if self._initial_alpha < 0.0:
  1557. raise ValueError("learning_rate must be positive")
  1558. if self.n_neighbors < 2:
  1559. raise ValueError("n_neighbors must be greater than 1")
  1560. if self.target_n_neighbors < 2 and self.target_n_neighbors != -1:
  1561. raise ValueError("target_n_neighbors must be greater than 1")
  1562. if not isinstance(self.n_components, int):
  1563. if isinstance(self.n_components, str):
  1564. raise ValueError("n_components must be an int")
  1565. if self.n_components % 1 != 0:
  1566. raise ValueError("n_components must be a whole number")
  1567. try:
  1568. # this will convert other types of int (eg. numpy int64)
  1569. # to Python int
  1570. self.n_components = int(self.n_components)
  1571. except ValueError:
  1572. raise ValueError("n_components must be an int")
  1573. if self.n_components < 1:
  1574. raise ValueError("n_components must be greater than 0")
  1575. self.n_epochs_list = None
  1576. if (
  1577. isinstance(self.n_epochs, list)
  1578. or isinstance(self.n_epochs, tuple)
  1579. or isinstance(self.n_epochs, np.ndarray)
  1580. ):
  1581. if not issubclass(
  1582. np.array(self.n_epochs).dtype.type, np.integer
  1583. ) or not np.all(np.array(self.n_epochs) >= 0):
  1584. raise ValueError(
  1585. "n_epochs must be a nonnegative integer "
  1586. "or a list of nonnegative integers"
  1587. )
  1588. self.n_epochs_list = list(self.n_epochs)
  1589. elif self.n_epochs is not None and (
  1590. self.n_epochs < 0 or not isinstance(self.n_epochs, int)
  1591. ):
  1592. raise ValueError(
  1593. "n_epochs must be a nonnegative integer "
  1594. "or a list of nonnegative integers"
  1595. )
  1596. if self.metric_kwds is None:
  1597. self._metric_kwds = {}
  1598. else:
  1599. self._metric_kwds = self.metric_kwds
  1600. if self.output_metric_kwds is None:
  1601. self._output_metric_kwds = {}
  1602. else:
  1603. self._output_metric_kwds = self.output_metric_kwds
  1604. if self.target_metric_kwds is None:
  1605. self._target_metric_kwds = {}
  1606. else:
  1607. self._target_metric_kwds = self.target_metric_kwds
  1608. # check sparsity of data upfront to set proper _input_distance_func &
  1609. # save repeated checks later on
  1610. if scipy.sparse.isspmatrix_csr(self._raw_data):
  1611. self._sparse_data = True
  1612. else:
  1613. self._sparse_data = False
  1614. # set input distance metric & inverse_transform distance metric
  1615. if callable(self.metric):
  1616. in_returns_grad = self._check_custom_metric(
  1617. self.metric, self._metric_kwds, self._raw_data
  1618. )
  1619. if in_returns_grad:
  1620. _m = self.metric
  1621. @numba.njit(fastmath=True)
  1622. def _dist_only(x, y, *kwds):
  1623. return _m(x, y, *kwds)[0]
  1624. self._input_distance_func = _dist_only
  1625. self._inverse_distance_func = self.metric
  1626. else:
  1627. self._input_distance_func = self.metric
  1628. self._inverse_distance_func = None
  1629. warn(
  1630. "custom distance metric does not return gradient; inverse_transform will be unavailable. "
  1631. "To enable using inverse_transform method, define a distance function that returns a tuple "
  1632. "of (distance [float], gradient [np.array])"
  1633. )
  1634. elif self.metric == "precomputed":
  1635. if self.unique:
  1636. raise ValueError("unique is poorly defined on a precomputed metric")
  1637. warn("using precomputed metric; inverse_transform will be unavailable")
  1638. self._input_distance_func = self.metric
  1639. self._inverse_distance_func = None
  1640. elif self.metric == "hellinger" and self._raw_data.min() < 0:
  1641. raise ValueError("Metric 'hellinger' does not support negative values")
  1642. elif self.metric in dist.named_distances:
  1643. if self._sparse_data:
  1644. if self.metric in sparse.sparse_named_distances:
  1645. self._input_distance_func = sparse.sparse_named_distances[
  1646. self.metric
  1647. ]
  1648. else:
  1649. raise ValueError(
  1650. "Metric {} is not supported for sparse data".format(self.metric)
  1651. )
  1652. else:
  1653. self._input_distance_func = dist.named_distances[self.metric]
  1654. try:
  1655. self._inverse_distance_func = dist.named_distances_with_gradients[
  1656. self.metric
  1657. ]
  1658. except KeyError:
  1659. warn(
  1660. "gradient function is not yet implemented for {} distance metric; "
  1661. "inverse_transform will be unavailable".format(self.metric)
  1662. )
  1663. self._inverse_distance_func = None
  1664. elif self.metric in pynn_named_distances:
  1665. if self._sparse_data:
  1666. if self.metric in pynn_sparse_named_distances:
  1667. self._input_distance_func = pynn_sparse_named_distances[self.metric]
  1668. else:
  1669. raise ValueError(
  1670. "Metric {} is not supported for sparse data".format(self.metric)
  1671. )
  1672. else:
  1673. self._input_distance_func = pynn_named_distances[self.metric]
  1674. warn(
  1675. "gradient function is not yet implemented for {} distance metric; "
  1676. "inverse_transform will be unavailable".format(self.metric)
  1677. )
  1678. self._inverse_distance_func = None
  1679. else:
  1680. raise ValueError("metric is neither callable nor a recognised string")
  1681. # set output distance metric
  1682. if callable(self.output_metric):
  1683. out_returns_grad = self._check_custom_metric(
  1684. self.output_metric, self._output_metric_kwds
  1685. )
  1686. if out_returns_grad:
  1687. self._output_distance_func = self.output_metric
  1688. else:
  1689. raise ValueError(
  1690. "custom output_metric must return a tuple of (distance [float], gradient [np.array])"
  1691. )
  1692. elif self.output_metric == "precomputed":
  1693. raise ValueError("output_metric cannnot be 'precomputed'")
  1694. elif self.output_metric in dist.named_distances_with_gradients:
  1695. self._output_distance_func = dist.named_distances_with_gradients[
  1696. self.output_metric
  1697. ]
  1698. elif self.output_metric in dist.named_distances:
  1699. raise ValueError(
  1700. "gradient function is not yet implemented for {}.".format(
  1701. self.output_metric
  1702. )
  1703. )
  1704. else:
  1705. raise ValueError(
  1706. "output_metric is neither callable nor a recognised string"
  1707. )
  1708. # set angularity for NN search based on metric
  1709. if self.metric in (
  1710. "cosine",
  1711. "correlation",
  1712. "dice",
  1713. "jaccard",
  1714. "ll_dirichlet",
  1715. "hellinger",
  1716. ):
  1717. self.angular_rp_forest = True
  1718. if self.n_jobs < -1 or self.n_jobs == 0:
  1719. raise ValueError("n_jobs must be a postive integer, or -1 (for all cores)")
  1720. if self.n_jobs != 1 and self.random_state is not None:
  1721. self.n_jobs = 1
  1722. warn(
  1723. f"n_jobs value {self.n_jobs} overridden to 1 by setting random_state. Use no seed for parallelism."
  1724. )
  1725. if self.dens_lambda < 0.0:
  1726. raise ValueError("dens_lambda cannot be negative")
  1727. if self.dens_frac < 0.0 or self.dens_frac > 1.0:
  1728. raise ValueError("dens_frac must be between 0.0 and 1.0")
  1729. if self.dens_var_shift < 0.0:
  1730. raise ValueError("dens_var_shift cannot be negative")
  1731. self._densmap_kwds = {
  1732. "lambda": self.dens_lambda if self.densmap else 0.0,
  1733. "frac": self.dens_frac if self.densmap else 0.0,
  1734. "var_shift": self.dens_var_shift,
  1735. "n_neighbors": self.n_neighbors,
  1736. }
  1737. if self.densmap:
  1738. if self.output_metric not in ("euclidean", "l2"):
  1739. raise ValueError(
  1740. "Non-Euclidean output metric not supported for densMAP."
  1741. )
  1742. # This will be used to prune all edges of greater than a fixed value from our knn graph.
  1743. # We have preset defaults described in DISCONNECTION_DISTANCES for our bounded measures.
  1744. # Otherwise a user can pass in their own value.
  1745. if self.disconnection_distance is None:
  1746. self._disconnection_distance = DISCONNECTION_DISTANCES.get(
  1747. self.metric, np.inf
  1748. )
  1749. elif isinstance(self.disconnection_distance, int) or isinstance(
  1750. self.disconnection_distance, float
  1751. ):
  1752. self._disconnection_distance = self.disconnection_distance
  1753. else:
  1754. raise ValueError("disconnection_distance must either be None or a numeric.")
  1755. if self.tqdm_kwds is None:
  1756. self.tqdm_kwds = {}
  1757. else:
  1758. if isinstance(self.tqdm_kwds, dict) is False:
  1759. raise ValueError(
  1760. "tqdm_kwds must be a dictionary. Please provide valid tqdm "
  1761. "parameters as key value pairs. Valid tqdm parameters can be "
  1762. "found here: https://github.com/tqdm/tqdm#parameters"
  1763. )
  1764. if "desc" not in self.tqdm_kwds:
  1765. self.tqdm_kwds["desc"] = "Epochs completed"
  1766. if "bar_format" not in self.tqdm_kwds:
  1767. bar_f = "{desc}: {percentage:3.0f}%| {bar} {n_fmt}/{total_fmt} [{elapsed}]"
  1768. self.tqdm_kwds["bar_format"] = bar_f
  1769. if hasattr(self, "knn_dists") and self.knn_dists is not None:
  1770. if self.unique:
  1771. raise ValueError(
  1772. "unique is not currently available for " "precomputed_knn."
  1773. )
  1774. if not isinstance(self.knn_indices, np.ndarray):
  1775. raise ValueError("precomputed_knn[0] must be ndarray object.")
  1776. if not isinstance(self.knn_dists, np.ndarray):
  1777. raise ValueError("precomputed_knn[1] must be ndarray object.")
  1778. if self.knn_dists.shape != self.knn_indices.shape:
  1779. raise ValueError(
  1780. "precomputed_knn[0] and precomputed_knn[1]"
  1781. " must be numpy arrays of the same size."
  1782. )
  1783. # #848: warn but proceed if no search index is present
  1784. if not isinstance(self.knn_search_index, NNDescent):
  1785. warn(
  1786. "precomputed_knn[2] (knn_search_index) "
  1787. "is not an NNDescent object: transforming new data with transform "
  1788. "will be unavailable."
  1789. )
  1790. if self.knn_dists.shape[1] < self.n_neighbors:
  1791. warn(
  1792. "precomputed_knn has a lower number of neighbors than "
  1793. "n_neighbors parameter. precomputed_knn will be ignored"
  1794. " and the k-nn will be computed normally."
  1795. )
  1796. self.knn_indices = None
  1797. self.knn_dists = None
  1798. self.knn_search_index = None
  1799. elif self.knn_dists.shape[0] != self._raw_data.shape[0]:
  1800. warn(
  1801. "precomputed_knn has a different number of samples than the"
  1802. " data you are fitting. precomputed_knn will be ignored and"
  1803. "the k-nn will be computed normally."
  1804. )
  1805. self.knn_indices = None
  1806. self.knn_dists = None
  1807. self.knn_search_index = None
  1808. elif (
  1809. self.knn_dists.shape[0] < 4096
  1810. and not self.force_approximation_algorithm
  1811. ):
  1812. # force_approximation_algorithm is irrelevant for pre-computed knn
  1813. # always set it to True which keeps downstream code paths working
  1814. self.force_approximation_algorithm = True
  1815. elif self.knn_dists.shape[1] > self.n_neighbors:
  1816. # if k for precomputed_knn larger than n_neighbors we simply prune it
  1817. self.knn_indices = self.knn_indices[:, : self.n_neighbors]
  1818. self.knn_dists = self.knn_dists[:, : self.n_neighbors]
  1819. def _check_custom_metric(self, metric, kwds, data=None):
  1820. # quickly check to determine whether user-defined
  1821. # self.metric/self.output_metric returns both distance and gradient
  1822. if data is not None:
  1823. # if checking the high-dimensional distance metric, test directly on
  1824. # input data so we don't risk violating any assumptions potentially
  1825. # hard-coded in the metric (e.g., bounded; non-negative)
  1826. x, y = data[np.random.randint(0, data.shape[0], 2)]
  1827. else:
  1828. # if checking the manifold distance metric, simulate some data on a
  1829. # reasonable interval with output dimensionality
  1830. x, y = np.random.uniform(low=-10, high=10, size=(2, self.n_components))
  1831. if scipy.sparse.issparse(data):
  1832. metric_out = metric(x.indices, x.data, y.indices, y.data, **kwds)
  1833. else:
  1834. metric_out = metric(x, y, **kwds)
  1835. # True if metric returns iterable of length 2, False otherwise
  1836. return hasattr(metric_out, "__iter__") and len(metric_out) == 2
  1837. def _populate_combined_params(self, *models):
  1838. self.n_neighbors = flattened([m.n_neighbors for m in models])
  1839. self.metric = flattened([m.metric for m in models])
  1840. self.metric_kwds = flattened([m.metric_kwds for m in models])
  1841. self.output_metric = flattened([m.output_metric for m in models])
  1842. self.n_epochs = flattened(
  1843. [m.n_epochs if m.n_epochs is not None else -1 for m in models]
  1844. )
  1845. if all([x == -1 for x in self.n_epochs]):
  1846. self.n_epochs = None
  1847. self.init = flattened([m.init for m in models])
  1848. self.n_components = flattened([m.n_components for m in models])
  1849. self.repulsion_strength = flattened([m.repulsion_strength for m in models])
  1850. self.learning_rate = flattened([m.learning_rate for m in models])
  1851. self.spread = flattened([m.spread for m in models])
  1852. self.min_dist = flattened([m.min_dist for m in models])
  1853. self.low_memory = flattened([m.low_memory for m in models])
  1854. self.set_op_mix_ratio = flattened([m.set_op_mix_ratio for m in models])
  1855. self.local_connectivity = flattened([m.local_connectivity for m in models])
  1856. self.negative_sample_rate = flattened([m.negative_sample_rate for m in models])
  1857. self.random_state = flattened([m.random_state for m in models])
  1858. self.angular_rp_forest = flattened([m.angular_rp_forest for m in models])
  1859. self.transform_queue_size = flattened([m.transform_queue_size for m in models])
  1860. self.target_n_neighbors = flattened([m.target_n_neighbors for m in models])
  1861. self.target_metric = flattened([m.target_metric for m in models])
  1862. self.target_metric_kwds = flattened([m.target_metric_kwds for m in models])
  1863. self.target_weight = flattened([m.target_weight for m in models])
  1864. self.transform_seed = flattened([m.transform_seed for m in models])
  1865. self.force_approximation_algorithm = flattened(
  1866. [m.force_approximation_algorithm for m in models]
  1867. )
  1868. self.verbose = flattened([m.verbose for m in models])
  1869. self.unique = flattened([m.unique for m in models])
  1870. self.densmap = flattened([m.densmap for m in models])
  1871. self.dens_lambda = flattened([m.dens_lambda for m in models])
  1872. self.dens_frac = flattened([m.dens_frac for m in models])
  1873. self.dens_var_shift = flattened([m.dens_var_shift for m in models])
  1874. self.output_dens = flattened([m.output_dens for m in models])
  1875. self.a = flattened([m.a for m in models])
  1876. self.b = flattened([m.b for m in models])
  1877. self._a = flattened([m._a for m in models])
  1878. self._b = flattened([m._b for m in models])
  1879. def __mul__(self, other):
  1880. check_is_fitted(
  1881. self, attributes=["graph_"], msg="Only fitted UMAP models can be combined"
  1882. )
  1883. check_is_fitted(
  1884. other, attributes=["graph_"], msg="Only fitted UMAP models can be combined"
  1885. )
  1886. if self.graph_.shape[0] != other.graph_.shape[0]:
  1887. raise ValueError("Only models with the equivalent samples can be combined")
  1888. result = UMAP()
  1889. result._populate_combined_params(self, other)
  1890. result.graph_ = general_simplicial_set_intersection(
  1891. self.graph_, other.graph_, 0.5
  1892. )
  1893. result.graph_ = reset_local_connectivity(result.graph_, True)
  1894. if scipy.sparse.csgraph.connected_components(result.graph_)[0] > 1:
  1895. warn(
  1896. "Combined graph is not connected but multi-component layout is unsupported. "
  1897. "Falling back to random initialization."
  1898. )
  1899. init = "random"
  1900. else:
  1901. init = "spectral"
  1902. result.densmap = np.any(result.densmap)
  1903. result.output_dens = np.any(result.output_dens)
  1904. result._densmap_kwds = {
  1905. "lambda": np.max(result.dens_lambda),
  1906. "frac": np.max(result.dens_frac),
  1907. "var_shift": np.max(result.dens_var_shift),
  1908. "n_neighbors": np.max(result.n_neighbors),
  1909. }
  1910. if result.n_epochs is None:
  1911. n_epochs = None
  1912. else:
  1913. n_epochs = np.max(result.n_epochs)
  1914. result.embedding_, aux_data = simplicial_set_embedding(
  1915. None,
  1916. result.graph_,
  1917. np.min(result.n_components),
  1918. np.min(result.learning_rate),
  1919. np.mean(result._a),
  1920. np.mean(result._b),
  1921. np.mean(result.repulsion_strength),
  1922. np.mean(result.negative_sample_rate),
  1923. n_epochs,
  1924. init,
  1925. check_random_state(42),
  1926. "euclidean",
  1927. {},
  1928. result.densmap,
  1929. result._densmap_kwds,
  1930. result.output_dens,
  1931. parallel=False,
  1932. verbose=bool(np.max(result.verbose)),
  1933. tqdm_kwds=self.tqdm_kwds,
  1934. )
  1935. if result.output_dens:
  1936. result.rad_orig_ = aux_data["rad_orig"]
  1937. result.rad_emb_ = aux_data["rad_emb"]
  1938. return result
  1939. def __add__(self, other):
  1940. check_is_fitted(
  1941. self, attributes=["graph_"], msg="Only fitted UMAP models can be combined"
  1942. )
  1943. check_is_fitted(
  1944. other, attributes=["graph_"], msg="Only fitted UMAP models can be combined"
  1945. )
  1946. if self.graph_.shape[0] != other.graph_.shape[0]:
  1947. raise ValueError("Only models with the equivalent samples can be combined")
  1948. result = UMAP()
  1949. result._populate_combined_params(self, other)
  1950. result.graph_ = general_simplicial_set_union(self.graph_, other.graph_)
  1951. result.graph_ = reset_local_connectivity(result.graph_, True)
  1952. if scipy.sparse.csgraph.connected_components(result.graph_)[0] > 1:
  1953. warn(
  1954. "Combined graph is not connected but mult-component layout is unsupported. "
  1955. "Falling back to random initialization."
  1956. )
  1957. init = "random"
  1958. else:
  1959. init = "spectral"
  1960. result.densmap = np.any(result.densmap)
  1961. result.output_dens = np.any(result.output_dens)
  1962. result._densmap_kwds = {
  1963. "lambda": np.max(result.dens_lambda),
  1964. "frac": np.max(result.dens_frac),
  1965. "var_shift": np.max(result.dens_var_shift),
  1966. "n_neighbors": np.max(result.n_neighbors),
  1967. }
  1968. if result.n_epochs is None:
  1969. n_epochs = None
  1970. else:
  1971. n_epochs = np.max(result.n_epochs)
  1972. result.embedding_, aux_data = simplicial_set_embedding(
  1973. None,
  1974. result.graph_,
  1975. np.min(result.n_components),
  1976. np.min(result.learning_rate),
  1977. np.mean(result._a),
  1978. np.mean(result._b),
  1979. np.mean(result.repulsion_strength),
  1980. np.mean(result.negative_sample_rate),
  1981. n_epochs,
  1982. init,
  1983. check_random_state(42),
  1984. "euclidean",
  1985. {},
  1986. result.densmap,
  1987. result._densmap_kwds,
  1988. result.output_dens,
  1989. parallel=False,
  1990. verbose=bool(np.max(result.verbose)),
  1991. tqdm_kwds=self.tqdm_kwds,
  1992. )
  1993. if result.output_dens:
  1994. result.rad_orig_ = aux_data["rad_orig"]
  1995. result.rad_emb_ = aux_data["rad_emb"]
  1996. return result
  1997. def __sub__(self, other):
  1998. check_is_fitted(
  1999. self, attributes=["graph_"], msg="Only fitted UMAP models can be combined"
  2000. )
  2001. check_is_fitted(
  2002. other, attributes=["graph_"], msg="Only fitted UMAP models can be combined"
  2003. )
  2004. if self.graph_.shape[0] != other.graph_.shape[0]:
  2005. raise ValueError("Only models with the equivalent samples can be combined")
  2006. result = UMAP()
  2007. result._populate_combined_params(self, other)
  2008. result.graph_ = general_simplicial_set_intersection(
  2009. self.graph_, other.graph_, weight=0.5, right_complement=True
  2010. )
  2011. result.graph_ = reset_local_connectivity(result.graph_, False)
  2012. if scipy.sparse.csgraph.connected_components(result.graph_)[0] > 1:
  2013. warn(
  2014. "Combined graph is not connected but mult-component layout is unsupported. "
  2015. "Falling back to random initialization."
  2016. )
  2017. init = "random"
  2018. else:
  2019. init = "spectral"
  2020. result.densmap = np.any(result.densmap)
  2021. result.output_dens = np.any(result.output_dens)
  2022. result._densmap_kwds = {
  2023. "lambda": np.max(result.dens_lambda),
  2024. "frac": np.max(result.dens_frac),
  2025. "var_shift": np.max(result.dens_var_shift),
  2026. "n_neighbors": np.max(result.n_neighbors),
  2027. }
  2028. if result.n_epochs is None:
  2029. n_epochs = None
  2030. else:
  2031. n_epochs = np.max(result.n_epochs)
  2032. result.embedding_, aux_data = simplicial_set_embedding(
  2033. None,
  2034. result.graph_,
  2035. np.min(result.n_components),
  2036. np.min(result.learning_rate),
  2037. np.mean(result._a),
  2038. np.mean(result._b),
  2039. np.mean(result.repulsion_strength),
  2040. np.mean(result.negative_sample_rate),
  2041. n_epochs,
  2042. init,
  2043. check_random_state(42),
  2044. "euclidean",
  2045. {},
  2046. result.densmap,
  2047. result._densmap_kwds,
  2048. result.output_dens,
  2049. parallel=False,
  2050. verbose=bool(np.max(result.verbose)),
  2051. tqdm_kwds=self.tqdm_kwds,
  2052. )
  2053. if result.output_dens:
  2054. result.rad_orig_ = aux_data["rad_orig"]
  2055. result.rad_emb_ = aux_data["rad_emb"]
  2056. return result
  2057. def fit(self, X, y=None, ensure_all_finite=True, **kwargs):
  2058. """Fit X into an embedded space.
  2059. Optionally use y for supervised dimension reduction.
  2060. Parameters
  2061. ----------
  2062. X : array, shape (n_samples, n_features) or (n_samples, n_samples)
  2063. If the metric is 'precomputed' X must be a square distance
  2064. matrix. Otherwise it contains a sample per row. If the method
  2065. is 'exact', X may be a sparse matrix of type 'csr', 'csc'
  2066. or 'coo'.
  2067. y : array, shape (n_samples)
  2068. A target array for supervised dimension reduction. How this is
  2069. handled is determined by parameters UMAP was instantiated with.
  2070. The relevant attributes are ``target_metric`` and
  2071. ``target_metric_kwds``.
  2072. ensure_all_finite : Whether to raise an error on np.inf, np.nan, pd.NA in array.
  2073. The possibilities are: - True: Force all values of array to be finite.
  2074. - False: accepts np.inf, np.nan, pd.NA in array.
  2075. - 'allow-nan': accepts only np.nan and pd.NA values in array.
  2076. Values cannot be infinite.
  2077. **kwargs : optional
  2078. Any additional keyword arguments are passed to _fit_embed_data.
  2079. """
  2080. if self.metric in ("bit_hamming", "bit_jaccard"):
  2081. X = check_array(
  2082. X, dtype=np.uint8, order="C", ensure_all_finite=ensure_all_finite
  2083. )
  2084. else:
  2085. X = check_array(
  2086. X,
  2087. dtype=np.float32,
  2088. accept_sparse="csr",
  2089. order="C",
  2090. ensure_all_finite=ensure_all_finite,
  2091. )
  2092. self._raw_data = X
  2093. # Handle all the optional arguments, setting default
  2094. if self.a is None or self.b is None:
  2095. self._a, self._b = find_ab_params(self.spread, self.min_dist)
  2096. else:
  2097. self._a = self.a
  2098. self._b = self.b
  2099. if isinstance(self.init, np.ndarray):
  2100. init = check_array(
  2101. self.init,
  2102. dtype=np.float32,
  2103. accept_sparse=False,
  2104. ensure_all_finite=ensure_all_finite,
  2105. )
  2106. else:
  2107. init = self.init
  2108. self._initial_alpha = self.learning_rate
  2109. self.knn_indices = self.precomputed_knn[0]
  2110. self.knn_dists = self.precomputed_knn[1]
  2111. # #848: allow precomputed knn to not have a search index
  2112. if len(self.precomputed_knn) == 2:
  2113. self.knn_search_index = None
  2114. else:
  2115. self.knn_search_index = self.precomputed_knn[2]
  2116. self._validate_parameters()
  2117. if self.verbose:
  2118. print(str(self))
  2119. self._original_n_threads = numba.get_num_threads()
  2120. if self.n_jobs > 0 and self.n_jobs is not None:
  2121. numba.set_num_threads(self.n_jobs)
  2122. # Check if we should unique the data
  2123. # We've already ensured that we aren't in the precomputed case
  2124. if self.unique:
  2125. # check if the matrix is dense
  2126. if self._sparse_data:
  2127. # Call a sparse unique function
  2128. index, inverse, counts = csr_unique(X)
  2129. else:
  2130. index, inverse, counts = np.unique(
  2131. X,
  2132. return_index=True,
  2133. return_inverse=True,
  2134. return_counts=True,
  2135. axis=0,
  2136. )[1:4]
  2137. if self.verbose:
  2138. print(
  2139. "Unique=True -> Number of data points reduced from ",
  2140. X.shape[0],
  2141. " to ",
  2142. X[index].shape[0],
  2143. )
  2144. most_common = np.argmax(counts)
  2145. print(
  2146. "Most common duplicate is",
  2147. index[most_common],
  2148. " with a count of ",
  2149. counts[most_common],
  2150. )
  2151. # We'll expose an inverse map when unique=True for users to map from our internal structures to their data
  2152. self._unique_inverse_ = inverse
  2153. # If we aren't asking for unique use the full index.
  2154. # This will save special cases later.
  2155. else:
  2156. index = np.arange(X.shape[0])
  2157. inverse = np.arange(X.shape[0])
  2158. # Compute the indexed copy of X exactly once and reuse it everywhere
  2159. # below. Previously X[index] was re-materialized at each call site (and
  2160. # several times just to read .shape[0]), allocating the full N x D matrix
  2161. # repeatedly. A single copy is kept: it also preserves UMAP's contract of
  2162. # never mutating the caller's data (and self._raw_data) when a distance
  2163. # metric writes into the array it is handed.
  2164. n_index_samples = index.shape[0]
  2165. X_indexed = X[index]
  2166. # Error check n_neighbors based on data size
  2167. if n_index_samples <= self.n_neighbors:
  2168. if n_index_samples == 1:
  2169. self.embedding_ = np.zeros(
  2170. (1, self.n_components)
  2171. ) # needed to sklearn comparability
  2172. return self
  2173. warn(
  2174. "n_neighbors is larger than the dataset size; truncating to "
  2175. "X.shape[0] - 1"
  2176. )
  2177. self._n_neighbors = n_index_samples - 1
  2178. if self.densmap:
  2179. self._densmap_kwds["n_neighbors"] = self._n_neighbors
  2180. else:
  2181. self._n_neighbors = self.n_neighbors
  2182. # Note: unless it causes issues for setting 'index', could move this to
  2183. # initial sparsity check above
  2184. if self._sparse_data and not X.has_sorted_indices:
  2185. X.sort_indices()
  2186. random_state = check_random_state(self.random_state)
  2187. if self.verbose:
  2188. print(ts(), "Construct fuzzy simplicial set")
  2189. if self.metric == "precomputed" and self._sparse_data:
  2190. # For sparse precomputed distance matrices, we just argsort the rows to find
  2191. # nearest neighbors. To make this easier, we expect matrices that are
  2192. # symmetrical (so we can find neighbors by looking at rows in isolation,
  2193. # rather than also having to consider that sample's column too).
  2194. # print("Computing KNNs for sparse precomputed distances...")
  2195. if sparse_tril(X).getnnz() != sparse_triu(X).getnnz():
  2196. raise ValueError(
  2197. "Sparse precomputed distance matrices should be symmetrical!"
  2198. )
  2199. if not np.all(X.diagonal() == 0):
  2200. raise ValueError("Non-zero distances from samples to themselves!")
  2201. if self.knn_dists is None:
  2202. self._knn_indices = np.zeros((X.shape[0], self.n_neighbors), dtype=int)
  2203. self._knn_dists = np.zeros(self._knn_indices.shape, dtype=float)
  2204. # X is CSR (check_array(accept_sparse="csr")), so slice its backing
  2205. # arrays directly instead of materializing a temporary matrix per row.
  2206. X_indptr, X_indices, X_data = X.indptr, X.indices, X.data
  2207. for row_id in range(X.shape[0]):
  2208. # Find KNNs row-by-row
  2209. row_start, row_end = X_indptr[row_id], X_indptr[row_id + 1]
  2210. row_data = X_data[row_start:row_end]
  2211. row_indices = X_indices[row_start:row_end]
  2212. if len(row_data) < self._n_neighbors:
  2213. raise ValueError(
  2214. "Some rows contain fewer than n_neighbors distances!"
  2215. )
  2216. # argpartition selects the k smallest in O(d) vs O(d·log d) for argsort
  2217. row_nn_data_indices = np.argpartition(row_data, self._n_neighbors)[
  2218. : self._n_neighbors
  2219. ]
  2220. row_nn_data_indices = row_nn_data_indices[
  2221. np.argsort(row_data[row_nn_data_indices])
  2222. ]
  2223. self._knn_indices[row_id] = row_indices[row_nn_data_indices]
  2224. self._knn_dists[row_id] = row_data[row_nn_data_indices]
  2225. else:
  2226. self._knn_indices = self.knn_indices
  2227. self._knn_dists = self.knn_dists
  2228. # Disconnect any vertices farther apart than _disconnection_distance
  2229. disconnected_index = self._knn_dists >= self._disconnection_distance
  2230. self._knn_indices[disconnected_index] = -1
  2231. self._knn_dists[disconnected_index] = np.inf
  2232. edges_removed = disconnected_index.sum()
  2233. (
  2234. self.graph_,
  2235. self._sigmas,
  2236. self._rhos,
  2237. self.graph_dists_,
  2238. ) = fuzzy_simplicial_set(
  2239. X_indexed,
  2240. self.n_neighbors,
  2241. random_state,
  2242. "precomputed",
  2243. self._metric_kwds,
  2244. self._knn_indices,
  2245. self._knn_dists,
  2246. self.angular_rp_forest,
  2247. self.set_op_mix_ratio,
  2248. self.local_connectivity,
  2249. True,
  2250. self.verbose,
  2251. self.densmap or self.output_dens,
  2252. )
  2253. # Report the number of vertices with degree 0 in our our umap.graph_
  2254. # This ensures that they were properly disconnected.
  2255. vertices_disconnected = np.sum(
  2256. np.array(self.graph_.sum(axis=1)).flatten() == 0
  2257. )
  2258. raise_disconnected_warning(
  2259. edges_removed,
  2260. vertices_disconnected,
  2261. self._disconnection_distance,
  2262. self._raw_data.shape[0],
  2263. verbose=self.verbose,
  2264. )
  2265. # Handle small cases efficiently by computing all distances
  2266. elif n_index_samples < 4096 and not self.force_approximation_algorithm:
  2267. self._small_data = True
  2268. try:
  2269. # sklearn pairwise_distances fails for callable metric on sparse data
  2270. _m = self.metric if self._sparse_data else self._input_distance_func
  2271. dmat = dist.numba_aware_pairwise_distances(
  2272. X_indexed, metric=_m, **self._metric_kwds
  2273. )
  2274. except (ValueError, TypeError) as e:
  2275. # metric is numba.jit'd or not supported by sklearn,
  2276. # fallback to pairwise special
  2277. if self._sparse_data:
  2278. # Get a fresh metric since we are casting to dense
  2279. if not callable(self.metric):
  2280. _m = dist.named_distances[self.metric]
  2281. dmat = dist.pairwise_special_metric(
  2282. X_indexed.toarray(),
  2283. metric=_m,
  2284. kwds=self._metric_kwds,
  2285. ensure_all_finite=ensure_all_finite,
  2286. )
  2287. else:
  2288. dmat = dist.pairwise_special_metric(
  2289. X_indexed,
  2290. metric=self._input_distance_func,
  2291. kwds=self._metric_kwds,
  2292. ensure_all_finite=ensure_all_finite,
  2293. )
  2294. else:
  2295. dmat = dist.pairwise_special_metric(
  2296. X_indexed,
  2297. metric=self._input_distance_func,
  2298. kwds=self._metric_kwds,
  2299. ensure_all_finite=ensure_all_finite,
  2300. )
  2301. # set any values greater than disconnection_distance to be np.inf.
  2302. # This will have no effect when _disconnection_distance is not set since it defaults to np.inf.
  2303. # The default (inf) makes the comparison an all-False no-op, so skip
  2304. # the two N x N boolean temporaries entirely; otherwise compute the
  2305. # mask once and reuse it for both the count and the in-place fill.
  2306. if np.isfinite(self._disconnection_distance):
  2307. disconnected_mask = dmat >= self._disconnection_distance
  2308. edges_removed = disconnected_mask.sum()
  2309. dmat[disconnected_mask] = np.inf
  2310. else:
  2311. edges_removed = 0
  2312. (
  2313. self.graph_,
  2314. self._sigmas,
  2315. self._rhos,
  2316. self.graph_dists_,
  2317. ) = fuzzy_simplicial_set(
  2318. dmat,
  2319. self._n_neighbors,
  2320. random_state,
  2321. "precomputed",
  2322. self._metric_kwds,
  2323. None,
  2324. None,
  2325. self.angular_rp_forest,
  2326. self.set_op_mix_ratio,
  2327. self.local_connectivity,
  2328. True,
  2329. self.verbose,
  2330. self.densmap or self.output_dens,
  2331. )
  2332. # Report the number of vertices with degree 0 in our umap.graph_
  2333. # This ensures that they were properly disconnected.
  2334. vertices_disconnected = np.sum(
  2335. np.array(self.graph_.sum(axis=1)).flatten() == 0
  2336. )
  2337. raise_disconnected_warning(
  2338. edges_removed,
  2339. vertices_disconnected,
  2340. self._disconnection_distance,
  2341. self._raw_data.shape[0],
  2342. verbose=self.verbose,
  2343. )
  2344. else:
  2345. # Standard case
  2346. self._small_data = False
  2347. # Standard case
  2348. if self._sparse_data and self.metric in pynn_sparse_named_distances:
  2349. nn_metric = self.metric
  2350. elif not self._sparse_data and self.metric in pynn_named_distances:
  2351. nn_metric = self.metric
  2352. else:
  2353. nn_metric = self._input_distance_func
  2354. if self.knn_dists is None:
  2355. (
  2356. self._knn_indices,
  2357. self._knn_dists,
  2358. self._knn_search_index,
  2359. ) = nearest_neighbors(
  2360. X_indexed,
  2361. self._n_neighbors,
  2362. nn_metric,
  2363. self._metric_kwds,
  2364. self.angular_rp_forest,
  2365. random_state,
  2366. self.low_memory,
  2367. use_pynndescent=True,
  2368. n_jobs=self.n_jobs,
  2369. verbose=self.verbose,
  2370. )
  2371. else:
  2372. self._knn_indices = self.knn_indices
  2373. self._knn_dists = self.knn_dists
  2374. self._knn_search_index = self.knn_search_index
  2375. # Disconnect any vertices farther apart than _disconnection_distance
  2376. disconnected_index = self._knn_dists >= self._disconnection_distance
  2377. self._knn_indices[disconnected_index] = -1
  2378. self._knn_dists[disconnected_index] = np.inf
  2379. edges_removed = disconnected_index.sum()
  2380. (
  2381. self.graph_,
  2382. self._sigmas,
  2383. self._rhos,
  2384. self.graph_dists_,
  2385. ) = fuzzy_simplicial_set(
  2386. X_indexed,
  2387. self.n_neighbors,
  2388. random_state,
  2389. nn_metric,
  2390. self._metric_kwds,
  2391. self._knn_indices,
  2392. self._knn_dists,
  2393. self.angular_rp_forest,
  2394. self.set_op_mix_ratio,
  2395. self.local_connectivity,
  2396. True,
  2397. self.verbose,
  2398. self.densmap or self.output_dens,
  2399. )
  2400. # Report the number of vertices with degree 0 in our umap.graph_
  2401. # This ensures that they were properly disconnected.
  2402. vertices_disconnected = np.sum(
  2403. np.array(self.graph_.sum(axis=1)).flatten() == 0
  2404. )
  2405. raise_disconnected_warning(
  2406. edges_removed,
  2407. vertices_disconnected,
  2408. self._disconnection_distance,
  2409. self._raw_data.shape[0],
  2410. verbose=self.verbose,
  2411. )
  2412. # Currently not checking if any duplicate points have differing labels
  2413. # Might be worth throwing a warning...
  2414. if y is not None:
  2415. len_X = len(X) if not self._sparse_data else X.shape[0]
  2416. if len_X != len(y):
  2417. raise ValueError(
  2418. "Length of x = {len_x}, length of y = {len_y}, while it must be equal.".format(
  2419. len_x=len_X, len_y=len(y)
  2420. )
  2421. )
  2422. if self.target_metric == "string":
  2423. y_ = y[index]
  2424. else:
  2425. y_ = check_array(
  2426. y, ensure_2d=False, ensure_all_finite=ensure_all_finite
  2427. )[index]
  2428. if self.target_metric == "categorical":
  2429. if self.target_weight < 1.0:
  2430. far_dist = 2.5 * (1.0 / (1.0 - self.target_weight))
  2431. else:
  2432. far_dist = 1.0e12
  2433. self.graph_ = discrete_metric_simplicial_set_intersection(
  2434. self.graph_, y_, far_dist=far_dist
  2435. )
  2436. elif self.target_metric in dist.DISCRETE_METRICS:
  2437. if self.target_weight < 1.0:
  2438. scale = 2.5 * (1.0 / (1.0 - self.target_weight))
  2439. else:
  2440. scale = 1.0e12
  2441. # self.graph_ = discrete_metric_simplicial_set_intersection(
  2442. # self.graph_,
  2443. # y_,
  2444. # metric=self.target_metric,
  2445. # metric_kws=self.target_metric_kwds,
  2446. # metric_scale=scale
  2447. # )
  2448. metric_kws = dist.get_discrete_params(y_, self.target_metric)
  2449. self.graph_ = discrete_metric_simplicial_set_intersection(
  2450. self.graph_,
  2451. y_,
  2452. metric=self.target_metric,
  2453. metric_kws=metric_kws,
  2454. metric_scale=scale,
  2455. )
  2456. else:
  2457. if len(y_.shape) == 1:
  2458. y_ = y_.reshape(-1, 1)
  2459. if self.target_n_neighbors == -1:
  2460. target_n_neighbors = self._n_neighbors
  2461. else:
  2462. target_n_neighbors = self.target_n_neighbors
  2463. # Handle the small case as precomputed as before
  2464. if y.shape[0] < 4096:
  2465. try:
  2466. ydmat = pairwise_distances(
  2467. y_, metric=self.target_metric, **self._target_metric_kwds
  2468. )
  2469. except (TypeError, ValueError):
  2470. ydmat = dist.pairwise_special_metric(
  2471. y_,
  2472. metric=self.target_metric,
  2473. kwds=self._target_metric_kwds,
  2474. ensure_all_finite=ensure_all_finite,
  2475. )
  2476. (
  2477. target_graph,
  2478. target_sigmas,
  2479. target_rhos,
  2480. ) = fuzzy_simplicial_set(
  2481. ydmat,
  2482. target_n_neighbors,
  2483. random_state,
  2484. "precomputed",
  2485. self._target_metric_kwds,
  2486. None,
  2487. None,
  2488. False,
  2489. 1.0,
  2490. 1.0,
  2491. False,
  2492. )
  2493. else:
  2494. # Standard case
  2495. (
  2496. target_graph,
  2497. target_sigmas,
  2498. target_rhos,
  2499. ) = fuzzy_simplicial_set(
  2500. y_,
  2501. target_n_neighbors,
  2502. random_state,
  2503. self.target_metric,
  2504. self._target_metric_kwds,
  2505. None,
  2506. None,
  2507. False,
  2508. 1.0,
  2509. 1.0,
  2510. False,
  2511. )
  2512. # product = self.graph_.multiply(target_graph)
  2513. # # self.graph_ = 0.99 * product + 0.01 * (self.graph_ +
  2514. # # target_graph -
  2515. # # product)
  2516. # self.graph_ = product
  2517. self.graph_ = general_simplicial_set_intersection(
  2518. self.graph_, target_graph, self.target_weight
  2519. )
  2520. self.graph_ = reset_local_connectivity(self.graph_)
  2521. self._supervised = True
  2522. else:
  2523. self._supervised = False
  2524. if self.densmap or self.output_dens:
  2525. self._densmap_kwds["graph_dists"] = self.graph_dists_
  2526. if self.verbose:
  2527. print(ts(), "Construct embedding")
  2528. if self.transform_mode == "embedding":
  2529. epochs = (
  2530. self.n_epochs_list if self.n_epochs_list is not None else self.n_epochs
  2531. )
  2532. # Use a fresh copy of the (unmutated) raw data here, independent of
  2533. # X_indexed: a distance metric that writes into the array it is
  2534. # handed could otherwise corrupt the data PCA-init / densMAP read.
  2535. self.embedding_, aux_data = self._fit_embed_data(
  2536. self._raw_data[index],
  2537. epochs,
  2538. init,
  2539. random_state, # JH why raw data?
  2540. **kwargs,
  2541. )
  2542. if self.n_epochs_list is not None:
  2543. if "embedding_list" not in aux_data:
  2544. raise KeyError(
  2545. "No list of embedding were found in 'aux_data'. "
  2546. "It is likely the layout optimization function "
  2547. "doesn't support the list of int for 'n_epochs'."
  2548. )
  2549. else:
  2550. self.embedding_list_ = [
  2551. e[inverse] for e in aux_data["embedding_list"]
  2552. ]
  2553. # Assign any points that are fully disconnected from our manifold(s) to have embedding
  2554. # coordinates of np.nan. These will be filtered by our plotting functions automatically.
  2555. # They also prevent users from being deceived a distance query to one of these points.
  2556. # Might be worth moving this into simplicial_set_embedding or _fit_embed_data
  2557. disconnected_vertices = np.array(self.graph_.sum(axis=1)).flatten() == 0
  2558. if len(disconnected_vertices) > 0:
  2559. self.embedding_[disconnected_vertices] = np.full(
  2560. self.n_components, np.nan
  2561. )
  2562. self.embedding_ = self.embedding_[inverse]
  2563. if self.output_dens:
  2564. self.rad_orig_ = aux_data["rad_orig"][inverse]
  2565. self.rad_emb_ = aux_data["rad_emb"][inverse]
  2566. if self.verbose:
  2567. print(ts() + " Finished embedding")
  2568. numba.set_num_threads(self._original_n_threads)
  2569. self._input_hash = joblib.hash(self._raw_data)
  2570. if self.transform_mode == "embedding":
  2571. # Set number of features out for sklearn API
  2572. self._n_features_out = self.embedding_.shape[1]
  2573. else:
  2574. self._n_features_out = self.graph_.shape[1]
  2575. return self
  2576. def _fit_embed_data(self, X, n_epochs, init, random_state, **kwargs):
  2577. """A method wrapper for simplicial_set_embedding that can be
  2578. replaced by subclasses. Arbitrary keyword arguments can be passed
  2579. through .fit() and .fit_transform().
  2580. """
  2581. return simplicial_set_embedding(
  2582. X,
  2583. self.graph_,
  2584. self.n_components,
  2585. self._initial_alpha,
  2586. self._a,
  2587. self._b,
  2588. self.repulsion_strength,
  2589. self.negative_sample_rate,
  2590. n_epochs,
  2591. init,
  2592. random_state,
  2593. self._input_distance_func,
  2594. self._metric_kwds,
  2595. self.densmap,
  2596. self._densmap_kwds,
  2597. self.output_dens,
  2598. self._output_distance_func,
  2599. self._output_metric_kwds,
  2600. self.output_metric in ("euclidean", "l2"),
  2601. self.random_state is None,
  2602. self.verbose,
  2603. tqdm_kwds=self.tqdm_kwds,
  2604. )
  2605. def fit_transform(self, X, y=None, ensure_all_finite=True, **kwargs):
  2606. """Fit X into an embedded space and return that transformed
  2607. output.
  2608. Parameters
  2609. ----------
  2610. X : array, shape (n_samples, n_features) or (n_samples, n_samples)
  2611. If the metric is 'precomputed' X must be a square distance
  2612. matrix. Otherwise it contains a sample per row.
  2613. y : array, shape (n_samples)
  2614. A target array for supervised dimension reduction. How this is
  2615. handled is determined by parameters UMAP was instantiated with.
  2616. The relevant attributes are ``target_metric`` and
  2617. ``target_metric_kwds``.
  2618. ensure_all_finite : Whether to raise an error on np.inf, np.nan, pd.NA in array.
  2619. The possibilities are: - True: Force all values of array to be finite.
  2620. - False: accepts np.inf, np.nan, pd.NA in array.
  2621. - 'allow-nan': accepts only np.nan and pd.NA values in array.
  2622. Values cannot be infinite.
  2623. **kwargs : Any additional keyword arguments are passed to _fit_embed_data.
  2624. Returns
  2625. -------
  2626. X_new : array, shape (n_samples, n_components)
  2627. Embedding of the training data in low-dimensional space.
  2628. or a tuple (X_new, r_orig, r_emb) if ``output_dens`` flag is set,
  2629. which additionally includes:
  2630. r_orig: array, shape (n_samples)
  2631. Local radii of data points in the original data space (log-transformed).
  2632. r_emb: array, shape (n_samples)
  2633. Local radii of data points in the embedding (log-transformed).
  2634. """
  2635. _input_dtype = getattr(X, "dtype", None)
  2636. self.fit(X, y, ensure_all_finite, **kwargs)
  2637. if self.transform_mode == "embedding":
  2638. embedding = self.embedding_
  2639. if _input_dtype is not None and np.issubdtype(_input_dtype, np.floating):
  2640. embedding = embedding.astype(_input_dtype, copy=False)
  2641. if self.output_dens:
  2642. return embedding, self.rad_orig_, self.rad_emb_
  2643. else:
  2644. return embedding
  2645. elif self.transform_mode == "graph":
  2646. return self.graph_
  2647. else:
  2648. raise ValueError(
  2649. "Unrecognized transform mode {}; should be one of 'embedding' or 'graph'".format(
  2650. self.transform_mode
  2651. )
  2652. )
  2653. def transform(self, X, ensure_all_finite=True):
  2654. """Transform X into the existing embedded space and return that
  2655. transformed output.
  2656. Parameters
  2657. ----------
  2658. X : array, shape (n_samples, n_features)
  2659. New data to be transformed.
  2660. ensure_all_finite : Whether to raise an error on np.inf, np.nan, pd.NA in array.
  2661. The possibilities are: - True: Force all values of array to be finite.
  2662. - False: accepts np.inf, np.nan, pd.NA in array.
  2663. - 'allow-nan': accepts only np.nan and pd.NA values in array.
  2664. Values cannot be infinite.
  2665. Returns
  2666. -------
  2667. X_new : array, shape (n_samples, n_components)
  2668. Embedding of the new data in low-dimensional space.
  2669. """
  2670. _input_dtype = getattr(X, "dtype", None)
  2671. # If we fit just a single instance then error
  2672. if self._raw_data.shape[0] == 1:
  2673. raise ValueError(
  2674. "Transform unavailable when model was fit with only a single data sample."
  2675. )
  2676. # If we just have the original input then short circuit things
  2677. if self.metric in ("bit_hamming", "bit_jaccard"):
  2678. X = check_array(
  2679. X, dtype=np.uint8, order="C", ensure_all_finite=ensure_all_finite
  2680. )
  2681. else:
  2682. X = check_array(
  2683. X,
  2684. dtype=np.float32,
  2685. accept_sparse="csr",
  2686. order="C",
  2687. ensure_all_finite=ensure_all_finite,
  2688. )
  2689. x_hash = joblib.hash(X)
  2690. if x_hash == self._input_hash:
  2691. if self.transform_mode == "embedding":
  2692. embedding = self.embedding_
  2693. if _input_dtype is not None and np.issubdtype(
  2694. _input_dtype, np.floating
  2695. ):
  2696. embedding = embedding.astype(_input_dtype, copy=False)
  2697. return embedding
  2698. elif self.transform_mode == "graph":
  2699. return self.graph_
  2700. else:
  2701. raise ValueError(
  2702. "Unrecognized transform mode {}; should be one of 'embedding' or 'graph'".format(
  2703. self.transform_mode
  2704. )
  2705. )
  2706. if self.densmap:
  2707. raise NotImplementedError(
  2708. "Transforming data into an existing embedding not supported for densMAP."
  2709. )
  2710. # #848: knn_search_index is allowed to be None if not transforming new data,
  2711. # so now we must validate that if it exists it is not None.
  2712. # #1194: a precomputed metric never has a search index (nearest_neighbors
  2713. # returns None for it), and the precomputed branch below does not need
  2714. # one, so only enforce this for the metrics that query the index.
  2715. if (
  2716. self.metric != "precomputed"
  2717. and hasattr(self, "_knn_search_index")
  2718. and self._knn_search_index is None
  2719. ):
  2720. raise NotImplementedError(
  2721. "No search index available: transforming data"
  2722. " into an existing embedding is not supported"
  2723. )
  2724. # X = check_array(X, dtype=np.float32, order="C", accept_sparse="csr")
  2725. random_state = check_random_state(self.transform_seed)
  2726. rng_state = random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)
  2727. if self.metric == "precomputed":
  2728. warn(
  2729. "Transforming new data with precomputed metric. "
  2730. "We are assuming the input data is a matrix of distances from the new points "
  2731. "to the points in the training set. If the input matrix is sparse, it should "
  2732. "contain distances from the new points to their nearest neighbours "
  2733. "or approximate nearest neighbours in the training set."
  2734. )
  2735. assert X.shape[1] == self._raw_data.shape[0]
  2736. if scipy.sparse.issparse(X):
  2737. indices = np.full(
  2738. (X.shape[0], self._n_neighbors), dtype=np.int32, fill_value=-1
  2739. )
  2740. dists = np.full_like(indices, dtype=np.float32, fill_value=-1)
  2741. # X is CSR (check_array(accept_sparse="csr")), so slice its backing
  2742. # arrays directly instead of materializing a temporary matrix per row.
  2743. X_indptr, X_indices, X_data = X.indptr, X.indices, X.data
  2744. for i in range(X.shape[0]):
  2745. row_start, row_end = X_indptr[i], X_indptr[i + 1]
  2746. row_data = X_data[row_start:row_end]
  2747. row_indices = X_indices[row_start:row_end]
  2748. if len(row_data) < self._n_neighbors:
  2749. raise ValueError(
  2750. f"Need at least n_neighbors ({self.n_neighbors}) distances for each row!"
  2751. )
  2752. row_nn_data_indices = np.argpartition(row_data, self._n_neighbors)[
  2753. : self._n_neighbors
  2754. ]
  2755. row_nn_data_indices = row_nn_data_indices[
  2756. np.argsort(row_data[row_nn_data_indices])
  2757. ]
  2758. indices[i] = row_indices[row_nn_data_indices]
  2759. dists[i] = row_data[row_nn_data_indices]
  2760. else:
  2761. indices = np.argpartition(X, self._n_neighbors, axis=1)[
  2762. :, : self._n_neighbors
  2763. ]
  2764. dists = np.take_along_axis(X, indices, axis=1)
  2765. sorted_idx = np.argsort(dists, axis=1)
  2766. indices = np.take_along_axis(indices, sorted_idx, axis=1).astype(
  2767. np.int32
  2768. )
  2769. dists = np.take_along_axis(dists, sorted_idx, axis=1)
  2770. assert np.min(indices) >= 0 and np.min(dists) >= 0.0
  2771. elif self._small_data:
  2772. try:
  2773. # sklearn pairwise_distances fails for callable metric on sparse data
  2774. _m = self.metric if self._sparse_data else self._input_distance_func
  2775. dmat = pairwise_distances(
  2776. X, self._raw_data, metric=_m, **self._metric_kwds
  2777. )
  2778. except (TypeError, ValueError):
  2779. # metric is numba.jit'd or not supported by sklearn,
  2780. # fallback to pairwise special
  2781. if self._sparse_data:
  2782. # Get a fresh metric since we are casting to dense
  2783. if not callable(self.metric):
  2784. _m = dist.named_distances[self.metric]
  2785. dmat = dist.pairwise_special_metric(
  2786. X.toarray(),
  2787. self._raw_data.toarray(),
  2788. metric=_m,
  2789. kwds=self._metric_kwds,
  2790. ensure_all_finite=ensure_all_finite,
  2791. )
  2792. else:
  2793. dmat = dist.pairwise_special_metric(
  2794. X,
  2795. self._raw_data,
  2796. metric=self._input_distance_func,
  2797. kwds=self._metric_kwds,
  2798. ensure_all_finite=ensure_all_finite,
  2799. )
  2800. else:
  2801. dmat = dist.pairwise_special_metric(
  2802. X,
  2803. self._raw_data,
  2804. metric=self._input_distance_func,
  2805. kwds=self._metric_kwds,
  2806. ensure_all_finite=ensure_all_finite,
  2807. )
  2808. # Select the k nearest in row-blocks so the argpartition index
  2809. # temporary is (block x N) rather than a full (M x N) int64 array
  2810. # kept alive by the [:, :k] view. Per-row results are identical to
  2811. # the full-matrix argpartition/argsort (rows are independent).
  2812. k = self._n_neighbors
  2813. n_t = dmat.shape[0]
  2814. indices = np.empty((n_t, k), dtype=np.int64)
  2815. dists = np.empty((n_t, k), dtype=dmat.dtype)
  2816. block = 1024
  2817. for s in range(0, n_t, block):
  2818. e = min(s + block, n_t)
  2819. d_block = dmat[s:e]
  2820. part = np.argpartition(d_block, k, axis=1)[:, :k]
  2821. r = np.arange(e - s)[:, None]
  2822. bd = d_block[r, part]
  2823. order = np.argsort(bd, axis=1)
  2824. indices[s:e] = part[r, order]
  2825. dists[s:e] = bd[r, order]
  2826. else:
  2827. epsilon = 0.24 if self._knn_search_index._angular_trees else 0.12
  2828. indices, dists = self._knn_search_index.query(
  2829. X, self.n_neighbors, epsilon=epsilon
  2830. )
  2831. dists = dists.astype(np.float32, order="C")
  2832. # Remove any nearest neighbours who's distances are greater than our disconnection_distance
  2833. indices[dists >= self._disconnection_distance] = -1
  2834. adjusted_local_connectivity = max(0.0, self.local_connectivity - 1.0)
  2835. sigmas, rhos = smooth_knn_dist(
  2836. dists,
  2837. float(self._n_neighbors),
  2838. local_connectivity=float(adjusted_local_connectivity),
  2839. )
  2840. rows, cols, vals, dists = compute_membership_strengths(
  2841. indices, dists, sigmas, rhos, bipartite=True
  2842. )
  2843. graph = scipy.sparse.coo_matrix(
  2844. (vals, (rows, cols)), shape=(X.shape[0], self._raw_data.shape[0])
  2845. )
  2846. if self.transform_mode == "graph":
  2847. return graph
  2848. # This was a very specially constructed graph with constant degree.
  2849. # That lets us do fancy unpacking by reshaping the csr matrix indices
  2850. # and data. Doing so relies on the constant degree assumption!
  2851. # csr_graph = normalize(graph.tocsr(), norm="l1")
  2852. # inds = csr_graph.indices.reshape(X.shape[0], self._n_neighbors)
  2853. # weights = csr_graph.data.reshape(X.shape[0], self._n_neighbors)
  2854. # embedding = init_transform(inds, weights, self.embedding_)
  2855. # This is less fast code than the above numba.jit'd code.
  2856. # It handles the fact that our nearest neighbour graph can now contain variable numbers of vertices.
  2857. csr_graph = graph.tocsr()
  2858. csr_graph.eliminate_zeros()
  2859. embedding = init_graph_transform(csr_graph, self.embedding_)
  2860. if self.n_epochs is None:
  2861. # For smaller datasets we can use more epochs
  2862. if graph.shape[0] <= 10000:
  2863. n_epochs = 100
  2864. else:
  2865. n_epochs = 30
  2866. else:
  2867. n_epochs = int(self.n_epochs // 3.0)
  2868. graph.data[graph.data < (graph.data.max() / float(n_epochs))] = 0.0
  2869. graph.eliminate_zeros()
  2870. epochs_per_sample = make_epochs_per_sample(graph.data, n_epochs)
  2871. head = graph.row
  2872. tail = graph.col
  2873. weight = graph.data
  2874. # optimize_layout = make_optimize_layout(
  2875. # self._output_distance_func,
  2876. # tuple(self.output_metric_kwds.values()),
  2877. # )
  2878. if self.output_metric == "euclidean":
  2879. embedding = optimize_layout_euclidean(
  2880. embedding,
  2881. self.embedding_.astype(np.float32, copy=True), # Fixes #179 & #217,
  2882. head,
  2883. tail,
  2884. n_epochs,
  2885. graph.shape[1],
  2886. epochs_per_sample,
  2887. self._a,
  2888. self._b,
  2889. rng_state,
  2890. self.repulsion_strength,
  2891. self._initial_alpha / 4.0,
  2892. self.negative_sample_rate,
  2893. self.random_state is None,
  2894. verbose=self.verbose,
  2895. tqdm_kwds=self.tqdm_kwds,
  2896. )
  2897. else:
  2898. embedding = optimize_layout_generic(
  2899. embedding,
  2900. self.embedding_.astype(np.float32, copy=True), # Fixes #179 & #217
  2901. head,
  2902. tail,
  2903. n_epochs,
  2904. graph.shape[1],
  2905. epochs_per_sample,
  2906. self._a,
  2907. self._b,
  2908. rng_state,
  2909. self.repulsion_strength,
  2910. self._initial_alpha / 4.0,
  2911. self.negative_sample_rate,
  2912. self._output_distance_func,
  2913. tuple(self._output_metric_kwds.values()),
  2914. verbose=self.verbose,
  2915. tqdm_kwds=self.tqdm_kwds,
  2916. )
  2917. if _input_dtype is not None and np.issubdtype(_input_dtype, np.floating):
  2918. embedding = embedding.astype(_input_dtype, copy=False)
  2919. return embedding
  2920. def inverse_transform(self, X):
  2921. """Transform X in the existing embedded space back into the input
  2922. data space and return that transformed output.
  2923. Parameters
  2924. ----------
  2925. X : array, shape (n_samples, n_components)
  2926. New points to be inverse transformed.
  2927. Returns
  2928. -------
  2929. X_new : array, shape (n_samples, n_features)
  2930. Generated data points new data in data space.
  2931. """
  2932. if self._sparse_data:
  2933. raise ValueError("Inverse transform not available for sparse input.")
  2934. elif self._inverse_distance_func is None:
  2935. raise ValueError("Inverse transform not available for given metric.")
  2936. elif self.densmap:
  2937. raise ValueError("Inverse transform not available for densMAP.")
  2938. elif self.n_components >= 8:
  2939. warn(
  2940. "Inverse transform works best with low dimensional embeddings."
  2941. " Results may be poor, or this approach to inverse transform"
  2942. " may fail altogether! If you need a high dimensional latent"
  2943. " space and inverse transform operations consider using an"
  2944. " autoencoder."
  2945. )
  2946. elif self.transform_mode == "graph":
  2947. raise ValueError(
  2948. "Inverse transform not available for transform_mode = 'graph'"
  2949. )
  2950. X = check_array(X, dtype=np.float32, order="C")
  2951. random_state = check_random_state(self.transform_seed)
  2952. rng_state = random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)
  2953. # build Delaunay complex (Does this not assume a roughly euclidean output metric)?
  2954. deltri = scipy.spatial.Delaunay(
  2955. self.embedding_, incremental=True, qhull_options="QJ"
  2956. )
  2957. neighbors = deltri.simplices[deltri.find_simplex(X)]
  2958. n_embed = self.embedding_.shape[0]
  2959. simplices = deltri.simplices
  2960. k = simplices.shape[1]
  2961. n_s = simplices.shape[0]
  2962. # All (vertex_a, vertex_b) pairs within each simplex — k² pairs × n_simplices
  2963. rows = np.empty(n_s * k * k, dtype=np.int32)
  2964. cols = np.empty(n_s * k * k, dtype=np.int32)
  2965. for a in range(k):
  2966. for b in range(k):
  2967. off = (a * k + b) * n_s
  2968. rows[off : off + n_s] = simplices[:, a]
  2969. cols[off : off + n_s] = simplices[:, b]
  2970. adjmat = scipy.sparse.csr_matrix(
  2971. (np.ones(n_s * k * k, dtype=np.int8), (rows, cols)),
  2972. shape=(n_embed, n_embed),
  2973. )
  2974. min_vertices = min(self._raw_data.shape[-1], self._raw_data.shape[0])
  2975. neighborhood = [
  2976. breadth_first_search(adjmat, v[0], min_vertices=min_vertices)
  2977. for v in neighbors
  2978. ]
  2979. if callable(self.output_metric):
  2980. # need to create another numba.jit-able wrapper for callable
  2981. # output_metrics that return a tuple (already checked that it does
  2982. # during param validation in `fit` method)
  2983. _out_m = self.output_metric
  2984. @numba.njit(fastmath=True)
  2985. def _output_dist_only(x, y, *kwds):
  2986. return _out_m(x, y, *kwds)[0]
  2987. dist_only_func = _output_dist_only
  2988. elif self.output_metric in dist.named_distances.keys():
  2989. dist_only_func = dist.named_distances[self.output_metric]
  2990. else:
  2991. # shouldn't really ever get here because of checks already performed,
  2992. # but works as a failsafe in case attr was altered manually after fitting
  2993. raise ValueError(
  2994. "Unrecognized output metric: {}".format(self.output_metric)
  2995. )
  2996. dist_args = tuple(self._output_metric_kwds.values())
  2997. distances = [
  2998. np.array(
  2999. [
  3000. dist_only_func(X[i], self.embedding_[nb], *dist_args)
  3001. for nb in neighborhood[i]
  3002. ]
  3003. )
  3004. for i in range(X.shape[0])
  3005. ]
  3006. idx = np.array([np.argsort(e)[:min_vertices] for e in distances])
  3007. dists_output_space = np.array(
  3008. [distances[i][idx[i]] for i in range(len(distances))]
  3009. )
  3010. indices = np.array([neighborhood[i][idx[i]] for i in range(len(neighborhood))])
  3011. rows, cols, distances = np.array(
  3012. [
  3013. [i, indices[i, j], dists_output_space[i, j]]
  3014. for i in range(indices.shape[0])
  3015. for j in range(min_vertices)
  3016. ]
  3017. ).T
  3018. # calculate membership strength of each edge
  3019. weights = 1 / (1 + self._a * distances ** (2 * self._b))
  3020. # compute 1-skeleton
  3021. # convert 1-skeleton into coo_matrix adjacency matrix
  3022. graph = scipy.sparse.coo_matrix(
  3023. (weights, (rows, cols)), shape=(X.shape[0], self._raw_data.shape[0])
  3024. )
  3025. # That lets us do fancy unpacking by reshaping the csr matrix indices
  3026. # and data. Doing so relies on the constant degree assumption!
  3027. # csr_graph = graph.tocsr()
  3028. csr_graph = normalize(graph.tocsr(), norm="l1")
  3029. inds = csr_graph.indices.reshape(X.shape[0], min_vertices)
  3030. weights = csr_graph.data.reshape(X.shape[0], min_vertices)
  3031. inv_transformed_points = init_transform(inds, weights, self._raw_data)
  3032. if self.n_epochs is None:
  3033. # For smaller datasets we can use more epochs
  3034. if graph.shape[0] <= 10000:
  3035. n_epochs = 100
  3036. else:
  3037. n_epochs = 30
  3038. else:
  3039. n_epochs = int(self.n_epochs // 3.0)
  3040. # graph.data[graph.data < (graph.data.max() / float(n_epochs))] = 0.0
  3041. # graph.eliminate_zeros()
  3042. epochs_per_sample = make_epochs_per_sample(graph.data, n_epochs)
  3043. head = graph.row
  3044. tail = graph.col
  3045. weight = graph.data
  3046. inv_transformed_points = optimize_layout_inverse(
  3047. inv_transformed_points,
  3048. self._raw_data,
  3049. head,
  3050. tail,
  3051. weight,
  3052. self._sigmas,
  3053. self._rhos,
  3054. n_epochs,
  3055. graph.shape[1],
  3056. epochs_per_sample,
  3057. self._a,
  3058. self._b,
  3059. rng_state,
  3060. self.repulsion_strength,
  3061. self._initial_alpha / 4.0,
  3062. self.negative_sample_rate,
  3063. self._inverse_distance_func,
  3064. tuple(self._metric_kwds.values()),
  3065. verbose=self.verbose,
  3066. tqdm_kwds=self.tqdm_kwds,
  3067. )
  3068. return inv_transformed_points
  3069. def update(self, X, ensure_all_finite=True):
  3070. if self.metric in ("bit_hamming", "bit_jaccard"):
  3071. X = check_array(
  3072. X, dtype=np.uint8, order="C", ensure_all_finite=ensure_all_finite
  3073. )
  3074. else:
  3075. X = check_array(
  3076. X,
  3077. dtype=np.float32,
  3078. accept_sparse="csr",
  3079. order="C",
  3080. ensure_all_finite=ensure_all_finite,
  3081. )
  3082. random_state = check_random_state(self.transform_seed)
  3083. rng_state = random_state.randint(INT32_MIN, INT32_MAX, 3).astype(np.int64)
  3084. original_size = self._raw_data.shape[0]
  3085. if self.metric == "precomputed":
  3086. raise ValueError("Update does not currently support precomputed metrics")
  3087. if self._supervised:
  3088. raise ValueError("Updating supervised models is not currently " "supported")
  3089. if self._small_data:
  3090. if self._sparse_data:
  3091. self._raw_data = scipy.sparse.vstack([self._raw_data, X])
  3092. else:
  3093. self._raw_data = np.vstack([self._raw_data, X])
  3094. if self._raw_data.shape[0] < 4096:
  3095. # still small data
  3096. try:
  3097. # sklearn pairwise_distances fails for callable metric on sparse data
  3098. _m = self.metric if self._sparse_data else self._input_distance_func
  3099. dmat = dist.numba_aware_pairwise_distances(
  3100. self._raw_data, metric=_m, **self._metric_kwds
  3101. )
  3102. except (ValueError, TypeError) as e:
  3103. # metric is numba.jit'd or not supported by sklearn,
  3104. # fallback to pairwise special
  3105. if self._sparse_data:
  3106. # Get a fresh metric since we are casting to dense
  3107. if not callable(self.metric):
  3108. _m = dist.named_distances[self.metric]
  3109. dmat = dist.pairwise_special_metric(
  3110. self._raw_data.toarray(),
  3111. metric=_m,
  3112. kwds=self._metric_kwds,
  3113. ensure_all_finite=ensure_all_finite,
  3114. )
  3115. else:
  3116. dmat = dist.pairwise_special_metric(
  3117. self._raw_data,
  3118. metric=self._input_distance_func,
  3119. kwds=self._metric_kwds,
  3120. ensure_all_finite=ensure_all_finite,
  3121. )
  3122. else:
  3123. dmat = dist.pairwise_special_metric(
  3124. self._raw_data,
  3125. metric=self._input_distance_func,
  3126. kwds=self._metric_kwds,
  3127. ensure_all_finite=ensure_all_finite,
  3128. )
  3129. self.graph_, self._sigmas, self._rhos = fuzzy_simplicial_set(
  3130. dmat,
  3131. self._n_neighbors,
  3132. random_state,
  3133. "precomputed",
  3134. self._metric_kwds,
  3135. None,
  3136. None,
  3137. self.angular_rp_forest,
  3138. self.set_op_mix_ratio,
  3139. self.local_connectivity,
  3140. True,
  3141. self.verbose,
  3142. )
  3143. knn_indices = np.argsort(dmat)[:, : self.n_neighbors]
  3144. else:
  3145. # now large data
  3146. self._small_data = False
  3147. if self._sparse_data and self.metric in pynn_sparse_named_distances:
  3148. nn_metric = self.metric
  3149. elif not self._sparse_data and self.metric in pynn_named_distances:
  3150. nn_metric = self.metric
  3151. else:
  3152. nn_metric = self._input_distance_func
  3153. (
  3154. self._knn_indices,
  3155. self._knn_dists,
  3156. self._knn_search_index,
  3157. ) = nearest_neighbors(
  3158. self._raw_data,
  3159. self._n_neighbors,
  3160. nn_metric,
  3161. self._metric_kwds,
  3162. self.angular_rp_forest,
  3163. random_state,
  3164. self.low_memory,
  3165. use_pynndescent=True,
  3166. n_jobs=self.n_jobs,
  3167. verbose=self.verbose,
  3168. )
  3169. self.graph_, self._sigmas, self._rhos = fuzzy_simplicial_set(
  3170. self._raw_data,
  3171. self.n_neighbors,
  3172. random_state,
  3173. nn_metric,
  3174. self._metric_kwds,
  3175. self._knn_indices,
  3176. self._knn_dists,
  3177. self.angular_rp_forest,
  3178. self.set_op_mix_ratio,
  3179. self.local_connectivity,
  3180. True,
  3181. self.verbose,
  3182. )
  3183. knn_indices = self._knn_indices
  3184. init = np.zeros(
  3185. (self._raw_data.shape[0], self.n_components), dtype=np.float32
  3186. )
  3187. init[:original_size] = self.embedding_
  3188. init_update(init, original_size, knn_indices)
  3189. if self.n_epochs is None:
  3190. n_epochs = 0
  3191. else:
  3192. n_epochs = self.n_epochs
  3193. self.embedding_, aux_data = simplicial_set_embedding(
  3194. self._raw_data,
  3195. self.graph_,
  3196. self.n_components,
  3197. self._initial_alpha,
  3198. self._a,
  3199. self._b,
  3200. self.repulsion_strength,
  3201. self.negative_sample_rate,
  3202. n_epochs,
  3203. init,
  3204. random_state,
  3205. self._input_distance_func,
  3206. self._metric_kwds,
  3207. self.densmap,
  3208. self._densmap_kwds,
  3209. self.output_dens,
  3210. self._output_distance_func,
  3211. self._output_metric_kwds,
  3212. self.output_metric in ("euclidean", "l2"),
  3213. self.random_state is None,
  3214. self.verbose,
  3215. tqdm_kwds=self.tqdm_kwds,
  3216. )
  3217. else:
  3218. self._knn_search_index.prepare()
  3219. self._knn_search_index.update(X)
  3220. self._raw_data = self._knn_search_index._raw_data
  3221. (
  3222. self._knn_indices,
  3223. self._knn_dists,
  3224. ) = self._knn_search_index.neighbor_graph
  3225. if self._sparse_data and self.metric in pynn_sparse_named_distances:
  3226. nn_metric = self.metric
  3227. elif not self._sparse_data and self.metric in pynn_named_distances:
  3228. nn_metric = self.metric
  3229. else:
  3230. nn_metric = self._input_distance_func
  3231. self.graph_, self._sigmas, self._rhos = fuzzy_simplicial_set(
  3232. self._raw_data,
  3233. self.n_neighbors,
  3234. random_state,
  3235. nn_metric,
  3236. self._metric_kwds,
  3237. self._knn_indices,
  3238. self._knn_dists,
  3239. self.angular_rp_forest,
  3240. self.set_op_mix_ratio,
  3241. self.local_connectivity,
  3242. True,
  3243. self.verbose,
  3244. )
  3245. init = np.zeros(
  3246. (self._raw_data.shape[0], self.n_components), dtype=np.float32
  3247. )
  3248. init[:original_size] = self.embedding_
  3249. init_update(init, original_size, self._knn_indices)
  3250. if self.n_epochs is None:
  3251. n_epochs = 0
  3252. else:
  3253. n_epochs = self.n_epochs
  3254. self.embedding_, aux_data = simplicial_set_embedding(
  3255. self._raw_data,
  3256. self.graph_,
  3257. self.n_components,
  3258. self._initial_alpha,
  3259. self._a,
  3260. self._b,
  3261. self.repulsion_strength,
  3262. self.negative_sample_rate,
  3263. n_epochs,
  3264. init,
  3265. random_state,
  3266. self._input_distance_func,
  3267. self._metric_kwds,
  3268. self.densmap,
  3269. self._densmap_kwds,
  3270. self.output_dens,
  3271. self._output_distance_func,
  3272. self._output_metric_kwds,
  3273. self.output_metric in ("euclidean", "l2"),
  3274. self.random_state is None,
  3275. self.verbose,
  3276. tqdm_kwds=self.tqdm_kwds,
  3277. )
  3278. if self.output_dens:
  3279. self.rad_orig_ = aux_data["rad_orig"]
  3280. self.rad_emb_ = aux_data["rad_emb"]
  3281. def __repr__(self):
  3282. from sklearn.utils._pprint import _EstimatorPrettyPrinter
  3283. import re
  3284. pp = _EstimatorPrettyPrinter(
  3285. compact=True,
  3286. indent=1,
  3287. indent_at_name=True,
  3288. n_max_elements_to_show=50,
  3289. )
  3290. pp._changed_only = True
  3291. repr_ = pp.pformat(self)
  3292. repr_ = re.sub("tqdm_kwds={.*},", "", repr_, flags=re.S)
  3293. # remove empty lines
  3294. repr_ = re.sub("\n *\n", "\n", repr_, flags=re.S)
  3295. # remove extra whitespaces after a comma
  3296. repr_ = re.sub(", +", ", ", repr_)
  3297. return repr_

umap_.py at commit 1180b78, under BSD-3-Clause · at the source

Overview

Authors: Wenjing Wang1,2,3,4, Yaokai Yang2,3,4, Qiong Liu1,2,3,4, Yunming Gao2,3,4, Qiantao Lv2,3,4,5, Kaiqi Zhang2,3,4, Jing Ning2,3,4, Yi Sun2,3,4
ORCID iDs: Wenjing Wang
  1. Fudan University, Shanghai, China
  2. Key Laboratory of Growth Regulation and Translational Research of Zhejiang Province, Research Center for Industries of the Future, School of Life Sciences, Westlake University, Hangzhou, China
  3. Westlake Laboratory of Life Sciences and Biomedicine, Hangzhou, China
  4. Institute of Basic Medical Sciences, Westlake Institute for Advanced Study, Hangzhou, China
  5. Zhejiang University, Hangzhou, China
Institutions: Fudan University (China); Westlake University (China); Zhejiang University (China)
Journal: Frontiers in systems neuroscience, volume 20, article 1822122
Dates: received 3 March 2026; accepted 13 April 2026; published online 7 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3389/fnsys.2026.1822122 · PMID 42181586 · PMCID PMC13190573 · OpenAlex W7160525263
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: systems (subfield)
Methods: Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing
Keywords: connectomics, convergence-divergence circuit, dendritic computation, multimodal integration, superior protocerebrum, valence
Topic: Neurobiology and Insect Physiology Research (Cellular and Molecular Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 195 references in the paper

Abstract

Valence detection in complex environment is critical for natural behaviors like foraging. Previous studies have explored valence processing in brain regions like lateral horn (LH) and mushroom body (MB) using simple synthetic stimuli in Drosophila. However, the neural basis for valence detection of natural objects in complex contexts remains unclear. Here, by brain-wide connectome analysis, we identified the evolutionarily conserved superior protocerebrum (SP) that integrates brain-wide multimodal inputs mainly via LH and MB, and sends widespread outputs particularly to the central complex (CX). This forms a convergence-divergence circuit resembling an autoencoder architecture, with SP as the bottleneck integrating multimodal information into low-dimensional valence signals. Specifically, SP input LH neurons integrate ethologically related innate valences for robust valence detection in natural environments, and the integration can be unimodal, such as that of diverse odors signaling food, or multimodal, such as that of wind and temperature signaling lousy weather. Opponent valences of attraction and aversion are further integrated into SP for complex valence detection. MB learned valences are also integrated into SP to update LH innate valences with recent experience for flexible valence detection. Attractive and aversive valences, either innate or learned, are integrated via excitatory and inhibitory synapses, respectively to form complex valence signals in a single SP neuron. Organized synaptic compartments support dendritic computation, with SP neurons exhibiting opposite synaptic organizations for opponent valences, indicating dendritic integration for complex valence detection. Our study highlights the importance of SP in multimodal opponent valence integration and suggests generalizable network and dendritic structures for complex valence processing.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repositories

Its files are read in the Code ↔ Paper reader above, with 8 matches between paragraphs and lines of code.

lmcinnes/umap

License: BSD-3-Clause
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 1180b785023ff8e8eb071de56c581d1dc8bec04e, 24 September 2026
Languages: Python (48), Jupyter (11), Shell (3)
Size: 319 files, 62 scripts
Software Heritage: archived
Found in: the end of the paper
Holds: README, license file, environment (pyproject.toml, setup.py), tests, continuous integration, documentation, 11 notebooks
Not found: CITATION.cff
Tools: UMAP (50 files), NumPy (44 files), scikit-learn (38 files), Matplotlib (20 files), Numba (20 files), SciPy (15 files), TensorFlow (10 files), Keras (9 files), pandas (6 files), seaborn (6 files), h5py (1 file), Pillow (1 file), PyTorch (1 file), scikit-image (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
64 files

navis-org/navis

License: GPL-3.0
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Commit: cb9a5915b6b3587cb81154f4f77ffc62fe12b03a, 6 September 2026
Languages: Python (295), JavaScript (2), Shell (2), Jupyter (1), Java (1)
Size: 470 files, 301 scripts
Software Heritage: archived
Found in: the end of the paper
Holds: README, license file, environment (pyproject.toml, requirements.txt, setup.py, binder/postBuild, binder/requirements.txt), tests, continuous integration, documentation, 1 notebook
Not found: CITATION.cff
Tools: NumPy (181 files), pandas (129 files), Matplotlib (57 files), SciPy (48 files), NetworkX (18 files), seaborn (12 files), Plotly (5 files), NEURON (4 files), scikit-image (4 files), h5py (3 files), igraph (2 files), scikit-learn (2 files), NiBabel (1 file), Numba (1 file), tifffile (1 file)
Availability: 1 check, the latest on 26 September 2026: the link answers
  • 26 September 2026: the link answers
303 files

aplbrain/dotmotif

License: Apache-2.0
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Commit: 125fdbca18c1c8ac7ed1b8d3f595669edc36574f, 23 September 2026
Languages: Python (27)
Size: 58 files, 27 scripts
Software Heritage: archived
Found in: the end of the paper
Holds: README, license file, environment (pyproject.toml, uv.lock), tests, continuous integration, documentation
Not found: CITATION.cff
Tools: NetworkX (13 files), pandas (3 files)
Availability: 1 check, the latest on 28 September 2026: the link answers
  • 28 September 2026: the link answers
29 files

google/neuroglancer

License: Apache-2.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: da443d25610b23c40c4a46ed9b92129e6203e68e, 22 September 2026
Languages: TypeScript (702), Python (133), C++ (23), JavaScript (20), Shell (11), C/C++ (6), Go (3), NEURON (1), Jupyter (1), Rust (1), C (1)
Size: 2,060 files, 902 scripts
Software Heritage: archived
Found in: the end of the paper
Holds: README, license file, environment (pyproject.toml, setup.py, uv.lock, docs/pyproject.toml, python/Dockerfile, src/mesh/draco/Dockerfile, src/sliceview/compresso/Dockerfile, src/sliceview/crackle/Dockerfile, src/sliceview/jxl/compile.Dockerfile, src/sliceview/jxl/optimize.Dockerfile, src/sliceview/png/Dockerfile), tests, continuous integration, documentation, 1 notebook
Not found: CITATION.cff
Tools: NumPy (67 files), Pillow (3 files), SciPy (3 files), pandas (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
904 files

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 4 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 1,292 scripts, each with its path and the digest of its content;
  • 8 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Data availability statement

The original contributions presented in this study are included in the article/Supplementary material, further inquiries can be directed to the corresponding author.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 8 authors, 6 keywords, 1 funder, 193 references.

Cite

This paper

Wang, W., Yang, Y., Liu, Q., Gao, Y., Lv, Q., Zhang, K., Ning, J., & Sun, Y. (2026). Convergence-divergence circuits for multimodal integration of innate and learned opponent valences. Frontiers in systems neuroscience, 20, 1822122. https://doi.org/10.3389/fnsys.2026.1822122

BibTeX

@article{wang2026convergence,
author = {Wang, Wenjing and Yang, Yaokai and Liu, Qiong and Gao, Yunming and Lv, Qiantao and Zhang, Kaiqi and Ning, Jing and Sun, Yi},
title = {{Convergence-divergence circuits for multimodal integration of innate and learned opponent valences}},
journal = {Frontiers in systems neuroscience},
year = {2026},
month = may,
volume = {20},
pages = {1822122},
publisher = {Frontiers Media SA},
issn = {1662-5137},
doi = {10.3389/fnsys.2026.1822122},
url = {https://doi.org/10.3389/fnsys.2026.1822122},
pmid = {42181586},
pmcid = {PMC13190573}
}

RIS

TY - JOUR
AU - Wang, Wenjing
AU - Yang, Yaokai
AU - Liu, Qiong
AU - Gao, Yunming
AU - Lv, Qiantao
AU - Zhang, Kaiqi
AU - Ning, Jing
AU - Sun, Yi
TI - Convergence-divergence circuits for multimodal integration of innate and learned opponent valences
T2 - Frontiers in systems neuroscience
J2 - Front Syst Neurosci
PY - 2026
DA - 2026/05/07
VL - 20
SP - 1822122
SN - 1662-5137
PB - Frontiers Media SA
DO - 10.3389/fnsys.2026.1822122
UR - https://doi.org/10.3389/fnsys.2026.1822122
LA - en
ER -

CSL-JSON

{
"id": "10.3389/fnsys.2026.1822122",
"type": "article-journal",
"title": "Convergence-divergence circuits for multimodal integration of innate and learned opponent valences",
"container-title": "Frontiers in systems neuroscience",
"author": [
{
"family": "Wang",
"given": "Wenjing"
},
{
"family": "Yang",
"given": "Yaokai"
},
{
"family": "Liu",
"given": "Qiong"
},
{
"family": "Gao",
"given": "Yunming"
},
{
"family": "Lv",
"given": "Qiantao"
},
{
"family": "Zhang",
"given": "Kaiqi"
},
{
"family": "Ning",
"given": "Jing"
},
{
"family": "Sun",
"given": "Yi"
}
],
"container-title-short": "Front Syst Neurosci",
"volume": "20",
"page": "1822122",
"DOI": "10.3389/fnsys.2026.1822122",
"PMID": "42181586",
"PMCID": "PMC13190573",
"ISSN": "1662-5137",
"publisher": "Frontiers Media SA",
"URL": "https://doi.org/10.3389/fnsys.2026.1822122",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
7
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1038/s41586-026-10735-w [code]
Distributed control circuits across a brain-and-cord connectome.
Journal: Nature
In common: UMAP, igraph, NetworkX, 9 other tools, 39 references
[2] doi:10.1371/journal.pbio.3003959 [code]
Recurrent synapses between CO2-sensitive olfactory sensory neurons enable robust CO2 detection in Aedes aegypti mosquitoes.
Journal: PLoS biology
In common: seaborn, scikit-learn, pandas, 3 other tools, 23 references
[3] doi:10.1038/s41467-026-72057-9 [code]
Sex-specific behavioral feedback modulates sensorimotor processing and drives flexible social behavior.
Journal: Nature communications
In common: Keras, UMAP, Numba, 11 other tools, systems, 13 references
[4] doi:10.7554/elife.107589 [code]
Distinct evolutionary trajectories of two integration centres, the central complex and mushroom bodies, across Heliconiini butterflies.
Journal: eLife
In common: Plotly, 15 references
[5] doi:10.1126/sciadv.aeh7220 [code]
Central complex representations of self-movement are sufficient to compute wind direction in flight.
Journal: Science advances
In common: Keras, TensorFlow, seaborn, 5 other tools, 9 references
[6] doi:10.7554/elife.96084 [code]
Organization of circuits linking descending input to motor output in the &lt;i&gt;Drosophila&lt;/i&gt; Male Adult Nerve Cord connectome.
Journal: eLife
In common: igraph, systems, 13 references
[7] doi:10.1038/s42003-026-10957-8 [code]
Brain defence by the extracellular matrix protein Cochlin.
Journal: Communications biology
In common: Keras, UMAP, igraph, 13 other tools
[8] doi:10.7554/elife.100880 [code]
An applicable and efficient retrograde monosynaptic circuit mapping tool for larval zebrafish.
Journal: eLife
In common: NEURON, tifffile, igraph, 12 other tools, systems
[9] doi:10.7554/elife.110588 [code]
Opening the black box toward a modular approach to spike sorting.
Journal: eLife
In common: UMAP, igraph, Numba, 12 other tools, 1 reference
[10] doi:10.1126/sciadv.aeg3535 [code]
Undoing of firing rate adaptation enables invariant population codes.
Journal: Science advances
In common: scikit-learn, SciPy, Matplotlib, 1 other tool, 9 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.