umap-web 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2816 @@
1
+ import { t as __export } from "./chunk-Bp6m_JJh.js";
2
+ import { f as UmapBackendError, h as UmapValidationError } from "./init-DLiEtGpx.js";
3
+
4
+ //#region src/graph/ab.ts
5
+ function findABParams(spread, minDist) {
6
+ const N = 300;
7
+ const xv = new Float64Array(N);
8
+ const yv = new Float64Array(N);
9
+ const step = spread * 3 / (N - 1);
10
+ for (let i = 0; i < N; i++) {
11
+ const x = i * step;
12
+ xv[i] = x;
13
+ yv[i] = x < minDist ? 1 : Math.exp(-(x - minDist) / spread);
14
+ }
15
+ let a = 1;
16
+ let b = 1;
17
+ let lambda = .001;
18
+ const residuals = (pa, pb) => {
19
+ let cost$1 = 0;
20
+ for (let i = 0; i < N; i++) {
21
+ const r = 1 / (1 + pa * xv[i] ** (2 * pb)) - yv[i];
22
+ cost$1 += r * r;
23
+ }
24
+ return cost$1;
25
+ };
26
+ let cost = residuals(a, b);
27
+ for (let iter = 0; iter < 300; iter++) {
28
+ let jaa = 0;
29
+ let jab = 0;
30
+ let jbb = 0;
31
+ let ga = 0;
32
+ let gb = 0;
33
+ for (let i = 0; i < N; i++) {
34
+ const x = xv[i];
35
+ const xp = x > 0 ? x ** (2 * b) : 0;
36
+ const denom = 1 + a * xp;
37
+ const r = 1 / denom - yv[i];
38
+ const dfda = -xp / (denom * denom);
39
+ const dfdb = x > 0 ? -a * xp * 2 * Math.log(x) / (denom * denom) : 0;
40
+ jaa += dfda * dfda;
41
+ jab += dfda * dfdb;
42
+ jbb += dfdb * dfdb;
43
+ ga += dfda * r;
44
+ gb += dfdb * r;
45
+ }
46
+ const m00 = jaa * (1 + lambda);
47
+ const m11 = jbb * (1 + lambda);
48
+ const m01 = jab;
49
+ const det = m00 * m11 - m01 * m01;
50
+ if (det === 0 || !Number.isFinite(det)) break;
51
+ const da = (-ga * m11 + gb * m01) / det;
52
+ const db = (ga * m01 - gb * m00) / det;
53
+ const na = a + da;
54
+ const nb = b + db;
55
+ const newCost = residuals(na, nb);
56
+ if (newCost < cost) {
57
+ a = na;
58
+ b = nb;
59
+ const improvement = cost - newCost;
60
+ cost = newCost;
61
+ lambda = Math.max(lambda / 3, 1e-12);
62
+ if (improvement < 1e-15 && Math.abs(da) + Math.abs(db) < 1e-12) break;
63
+ } else {
64
+ lambda *= 3;
65
+ if (lambda > 0xe8d4a51000) break;
66
+ }
67
+ }
68
+ return {
69
+ a,
70
+ b
71
+ };
72
+ }
73
+
74
+ //#endregion
75
+ //#region src/sparse.ts
76
+ function csrNnz(m) {
77
+ return m.indptr[m.rows];
78
+ }
79
+ /**
80
+ * Build canonical CSR from COO triplets: duplicates summed, zeros eliminated,
81
+ * columns sorted per row.
82
+ */
83
+ function csrFromCoo(rows, cols, rowIdx, colIdx, values) {
84
+ const nnzIn = rowIdx.length;
85
+ const counts = new Int32Array(rows + 1);
86
+ for (let t = 0; t < nnzIn; t++) counts[rowIdx[t] + 1]++;
87
+ for (let r = 0; r < rows; r++) counts[r + 1] += counts[r];
88
+ const order = new Int32Array(nnzIn);
89
+ const cursor = counts.slice(0, rows);
90
+ for (let t = 0; t < nnzIn; t++) {
91
+ const r = rowIdx[t];
92
+ order[cursor[r]++] = t;
93
+ }
94
+ const indptr = new Int32Array(rows + 1);
95
+ const outIdx = new Int32Array(nnzIn);
96
+ const outVal = new Float64Array(nnzIn);
97
+ let nnz = 0;
98
+ const tmpCols = [];
99
+ const tmpVals = [];
100
+ for (let r = 0; r < rows; r++) {
101
+ tmpCols.length = 0;
102
+ tmpVals.length = 0;
103
+ for (let p = counts[r]; p < counts[r + 1]; p++) {
104
+ const t = order[p];
105
+ tmpCols.push(colIdx[t]);
106
+ tmpVals.push(values[t]);
107
+ }
108
+ const n = tmpCols.length;
109
+ if (n > 0) {
110
+ const perm = Array.from({ length: n }, (_, i) => i).sort((a, b) => tmpCols[a] - tmpCols[b]);
111
+ let lastCol = -1;
112
+ let acc = 0;
113
+ for (let i = 0; i <= n; i++) {
114
+ const c = i < n ? tmpCols[perm[i]] : -2;
115
+ if (c !== lastCol) {
116
+ if (lastCol >= 0 && acc !== 0) {
117
+ outIdx[nnz] = lastCol;
118
+ outVal[nnz] = acc;
119
+ nnz++;
120
+ }
121
+ lastCol = c;
122
+ acc = 0;
123
+ }
124
+ if (i < n) acc += tmpVals[perm[i]];
125
+ }
126
+ }
127
+ indptr[r + 1] = nnz;
128
+ }
129
+ return {
130
+ data: outVal.slice(0, nnz),
131
+ indices: outIdx.slice(0, nnz),
132
+ indptr,
133
+ rows,
134
+ cols
135
+ };
136
+ }
137
+ function csrTranspose(m) {
138
+ const nnz = csrNnz(m);
139
+ const counts = new Int32Array(m.cols + 1);
140
+ for (let t = 0; t < nnz; t++) counts[m.indices[t] + 1]++;
141
+ for (let c = 0; c < m.cols; c++) counts[c + 1] += counts[c];
142
+ const indptr = counts.slice();
143
+ const indices = new Int32Array(nnz);
144
+ const data = new Float64Array(nnz);
145
+ const cursor = counts.slice(0, m.cols);
146
+ for (let r = 0; r < m.rows; r++) for (let p = m.indptr[r]; p < m.indptr[r + 1]; p++) {
147
+ const c = m.indices[p];
148
+ const dst = cursor[c]++;
149
+ indices[dst] = r;
150
+ data[dst] = m.data[p];
151
+ }
152
+ return {
153
+ data,
154
+ indices,
155
+ indptr,
156
+ rows: m.cols,
157
+ cols: m.rows
158
+ };
159
+ }
160
+ /**
161
+ * Elementwise combine over the UNION sparsity of two canonical CSRs.
162
+ * `f(a, b)` receives 0 for absent entries; results equal to 0 are eliminated.
163
+ */
164
+ function csrBinaryOp(A, B, f) {
165
+ const rows = A.rows;
166
+ const cols = A.cols;
167
+ const indptr = new Int32Array(rows + 1);
168
+ const cap = csrNnz(A) + csrNnz(B);
169
+ const indices = new Int32Array(cap);
170
+ const data = new Float64Array(cap);
171
+ let nnz = 0;
172
+ for (let r = 0; r < rows; r++) {
173
+ let pa = A.indptr[r];
174
+ let pb = B.indptr[r];
175
+ const ea = A.indptr[r + 1];
176
+ const eb = B.indptr[r + 1];
177
+ while (pa < ea || pb < eb) {
178
+ const ca = pa < ea ? A.indices[pa] : Infinity;
179
+ const cb = pb < eb ? B.indices[pb] : Infinity;
180
+ let col;
181
+ let va = 0;
182
+ let vb = 0;
183
+ if (ca < cb) {
184
+ col = ca;
185
+ va = A.data[pa];
186
+ pa++;
187
+ } else if (cb < ca) {
188
+ col = cb;
189
+ vb = B.data[pb];
190
+ pb++;
191
+ } else {
192
+ col = ca;
193
+ va = A.data[pa];
194
+ vb = B.data[pb];
195
+ pa++;
196
+ pb++;
197
+ }
198
+ const v = f(va, vb);
199
+ if (v !== 0) {
200
+ indices[nnz] = col;
201
+ data[nnz] = v;
202
+ nnz++;
203
+ }
204
+ }
205
+ indptr[r + 1] = nnz;
206
+ }
207
+ return {
208
+ data: data.slice(0, nnz),
209
+ indices: indices.slice(0, nnz),
210
+ indptr,
211
+ rows,
212
+ cols
213
+ };
214
+ }
215
+ /** Drop explicit zeros (canonicalize). */
216
+ function csrEliminateZeros(m) {
217
+ const nnz = csrNnz(m);
218
+ let out = 0;
219
+ for (let t = 0; t < nnz; t++) if (m.data[t] !== 0) out++;
220
+ if (out === nnz) return m;
221
+ const indptr = new Int32Array(m.rows + 1);
222
+ const indices = new Int32Array(out);
223
+ const data = new Float64Array(out);
224
+ let k = 0;
225
+ for (let r = 0; r < m.rows; r++) {
226
+ for (let p = m.indptr[r]; p < m.indptr[r + 1]; p++) if (m.data[p] !== 0) {
227
+ indices[k] = m.indices[p];
228
+ data[k] = m.data[p];
229
+ k++;
230
+ }
231
+ indptr[r + 1] = k;
232
+ }
233
+ return {
234
+ data,
235
+ indices,
236
+ indptr,
237
+ rows: m.rows,
238
+ cols: m.cols
239
+ };
240
+ }
241
+ /** Map values in place. */
242
+ function csrMapValues(m, f) {
243
+ for (let t = 0; t < csrNnz(m); t++) m.data[t] = f(m.data[t]);
244
+ return m;
245
+ }
246
+ /** Max of |A - B| over union sparsity, for tests. */
247
+ function csrMaxAbsDiff(A, B) {
248
+ let worst = 0;
249
+ csrBinaryOp(A, B, (a, b) => {
250
+ const v = Math.abs(a - b);
251
+ if (v > worst) worst = v;
252
+ return v;
253
+ });
254
+ return worst;
255
+ }
256
+ /** Row-normalize by max (scipy normalize(norm='max')): divide each row by its max value. */
257
+ function csrRowNormalizeMax(m) {
258
+ for (let r = 0; r < m.rows; r++) {
259
+ let mx = 0;
260
+ for (let p = m.indptr[r]; p < m.indptr[r + 1]; p++) {
261
+ const v = m.data[p];
262
+ if (v > mx) mx = v;
263
+ }
264
+ if (mx > 0) for (let p = m.indptr[r]; p < m.indptr[r + 1]; p++) m.data[p] = m.data[p] / mx;
265
+ }
266
+ return m;
267
+ }
268
+ /** COO iteration helper. */
269
+ function csrForEach(m, cb) {
270
+ for (let r = 0; r < m.rows; r++) for (let p = m.indptr[r]; p < m.indptr[r + 1]; p++) cb(r, m.indices[p], m.data[p]);
271
+ }
272
+ /** Deep copy. */
273
+ function csrCopy(m) {
274
+ return {
275
+ data: m.data.slice(),
276
+ indices: m.indices.slice(),
277
+ indptr: m.indptr.slice(),
278
+ rows: m.rows,
279
+ cols: m.cols
280
+ };
281
+ }
282
+
283
+ //#endregion
284
+ //#region src/graph/epochs.ts
285
+ function makeEpochsPerSample(weights, nEpochs) {
286
+ const result = new Float64Array(weights.length).fill(-1);
287
+ let wMax = -Infinity;
288
+ for (let i = 0; i < weights.length; i++) if (weights[i] > wMax) wMax = weights[i];
289
+ for (let i = 0; i < weights.length; i++) {
290
+ const nSamples = nEpochs * (weights[i] / wMax);
291
+ if (nSamples > 0) result[i] = nEpochs / nSamples;
292
+ }
293
+ return result;
294
+ }
295
+ /** Default epoch rule: 500 for n<=10000 else 200; +200 when densMAP (notes §6.2). */
296
+ function defaultNEpochs(nVertices, densmap) {
297
+ let d = nVertices <= 1e4 ? 500 : 200;
298
+ if (densmap) d += 200;
299
+ return d;
300
+ }
301
+ /**
302
+ * Prune graph weights below max/n_epochs (in place on a copy) exactly like
303
+ * simplicial_set_embedding, returning the pruned graph.
304
+ */
305
+ function pruneGraphForEpochs(graph, nEpochsMax, defaultEpochs) {
306
+ const out = {
307
+ data: graph.data.slice(),
308
+ indices: graph.indices,
309
+ indptr: graph.indptr,
310
+ rows: graph.rows,
311
+ cols: graph.cols
312
+ };
313
+ let mx = -Infinity;
314
+ for (let i = 0; i < out.data.length; i++) if (out.data[i] > mx) mx = out.data[i];
315
+ const thresh = mx / (nEpochsMax > 10 ? nEpochsMax : defaultEpochs);
316
+ for (let i = 0; i < out.data.length; i++) if (out.data[i] < thresh) out.data[i] = 0;
317
+ return csrEliminateZeros(out);
318
+ }
319
+
320
+ //#endregion
321
+ //#region src/graph/smooth.ts
322
+ /**
323
+ * smooth_knn_dist — sigma/rho computation (umap_.py:143-253; notes §1).
324
+ *
325
+ * Python runs this in float32 fastmath; we compute in float64 (matches to ~1e-6,
326
+ * within the §3.3 tolerance policy).
327
+ */
328
+ const SMOOTH_K_TOLERANCE = 1e-5;
329
+ const MIN_K_DIST_SCALE = .001;
330
+ /**
331
+ * @param distances (nSamples × nNeighbors) row-major, each row sorted ascending
332
+ * (includes self at column 0 for standard kNN).
333
+ * @param k the float number of neighbors (callers pass the integer k).
334
+ */
335
+ function smoothKnnDist(distances, nNeighbors, k, { nIter = 64, localConnectivity = 1, bandwidth = 1 } = {}) {
336
+ const nSamples = distances.length / nNeighbors;
337
+ const target = Math.log2(k) * bandwidth;
338
+ const rhos = new Float32Array(nSamples);
339
+ const sigmas = new Float32Array(nSamples);
340
+ let meanDistances = 0;
341
+ for (let t = 0; t < distances.length; t++) meanDistances += distances[t];
342
+ meanDistances /= distances.length;
343
+ const nonZero = new Float64Array(nNeighbors);
344
+ for (let i = 0; i < nSamples; i++) {
345
+ const base = i * nNeighbors;
346
+ let lo = 0;
347
+ let hi = Infinity;
348
+ let mid = 1;
349
+ let nz = 0;
350
+ for (let j = 0; j < nNeighbors; j++) {
351
+ const d = distances[base + j];
352
+ if (d > 0) nonZero[nz++] = d;
353
+ }
354
+ let rho = 0;
355
+ if (nz >= localConnectivity) {
356
+ const index = Math.floor(localConnectivity);
357
+ const interpolation = localConnectivity - index;
358
+ if (index > 0) {
359
+ rho = nonZero[index - 1];
360
+ if (interpolation > SMOOTH_K_TOLERANCE) rho += interpolation * (nonZero[index] - nonZero[index - 1]);
361
+ } else rho = interpolation * nonZero[0];
362
+ } else if (nz > 0) {
363
+ let mx = -Infinity;
364
+ for (let j = 0; j < nz; j++) if (nonZero[j] > mx) mx = nonZero[j];
365
+ rho = mx;
366
+ }
367
+ rhos[i] = rho;
368
+ for (let n = 0; n < nIter; n++) {
369
+ let psum = 0;
370
+ for (let j = 1; j < nNeighbors; j++) {
371
+ const d = distances[base + j] - rho;
372
+ psum += d > 0 ? Math.exp(-(d / mid)) : 1;
373
+ }
374
+ if (Math.abs(psum - target) < SMOOTH_K_TOLERANCE) break;
375
+ if (psum > target) hi = mid;
376
+ else lo = mid;
377
+ mid = (lo + hi) / 2;
378
+ }
379
+ let sigma = mid;
380
+ if (rho > 0) {
381
+ let meanIth = 0;
382
+ for (let j = 0; j < nNeighbors; j++) meanIth += distances[base + j];
383
+ meanIth /= nNeighbors;
384
+ if (sigma < MIN_K_DIST_SCALE * meanIth) sigma = MIN_K_DIST_SCALE * meanIth;
385
+ } else if (sigma < MIN_K_DIST_SCALE * meanDistances) sigma = MIN_K_DIST_SCALE * meanDistances;
386
+ sigmas[i] = sigma;
387
+ }
388
+ return {
389
+ sigmas,
390
+ rhos
391
+ };
392
+ }
393
+
394
+ //#endregion
395
+ //#region src/graph/fuzzy.ts
396
+ function computeMembershipStrengths(knnIndices, knnDists, sigmas, rhos, nNeighbors, returnDists = false, bipartite = false) {
397
+ const nSamples = knnIndices.length / nNeighbors;
398
+ const size = nSamples * nNeighbors;
399
+ const rows = new Int32Array(size);
400
+ const cols = new Int32Array(size);
401
+ const vals = new Float32Array(size);
402
+ const dists = returnDists ? new Float32Array(size) : null;
403
+ for (let i = 0; i < nSamples; i++) {
404
+ const base = i * nNeighbors;
405
+ for (let j = 0; j < nNeighbors; j++) {
406
+ const idx = knnIndices[base + j];
407
+ if (idx === -1) continue;
408
+ let val;
409
+ if (!bipartite && idx === i) val = 0;
410
+ else if (knnDists[base + j] - rhos[i] <= 0 || sigmas[i] === 0) val = 1;
411
+ else val = Math.fround(Math.exp(-((knnDists[base + j] - rhos[i]) / sigmas[i])));
412
+ rows[base + j] = i;
413
+ cols[base + j] = idx;
414
+ vals[base + j] = val;
415
+ if (dists) dists[base + j] = knnDists[base + j];
416
+ }
417
+ }
418
+ return {
419
+ rows,
420
+ cols,
421
+ vals,
422
+ dists
423
+ };
424
+ }
425
+ /**
426
+ * Build the fuzzy simplicial set from a kNN graph (knn indices+distances required;
427
+ * the estimator computes them per §5.2 backends).
428
+ */
429
+ function fuzzySimplicialSet(nSamples, nNeighbors, knnIndices, knnDists, { setOpMixRatio = 1, localConnectivity = 1, applySetOperations = true, returnDists = false } = {}) {
430
+ const { sigmas, rhos } = smoothKnnDist(knnDists, nNeighbors, nNeighbors, { localConnectivity });
431
+ const { rows, cols, vals, dists } = computeMembershipStrengths(knnIndices, knnDists, sigmas, rhos, nNeighbors, returnDists);
432
+ let graph = csrFromCoo(nSamples, nSamples, rows, cols, vals);
433
+ if (applySetOperations) {
434
+ const transpose = csrTranspose(graph);
435
+ graph = csrBinaryOp(graph, transpose, (a, b) => {
436
+ const prod = a * b;
437
+ return setOpMixRatio * (a + b - prod) + (1 - setOpMixRatio) * prod;
438
+ });
439
+ }
440
+ graph = csrEliminateZeros(graph);
441
+ let distGraph = null;
442
+ if (returnDists && dists) {
443
+ const dmat = csrFromCoo(nSamples, nSamples, rows, cols, dists);
444
+ distGraph = csrEliminateZeros(csrBinaryOp(dmat, csrTranspose(dmat), (a, b) => Math.max(a, b)));
445
+ }
446
+ return {
447
+ graph,
448
+ sigmas,
449
+ rhos,
450
+ dists: distGraph
451
+ };
452
+ }
453
+
454
+ //#endregion
455
+ //#region src/metrics.ts
456
+ var metrics_exports = /* @__PURE__ */ __export({
457
+ ANGULAR_METRICS: () => ANGULAR_METRICS,
458
+ NAMED_METRICS: () => NAMED_METRICS,
459
+ brayCurtis: () => brayCurtis,
460
+ canberra: () => canberra,
461
+ canonicalMetricName: () => canonicalMetricName,
462
+ chebyshev: () => chebyshev,
463
+ correlation: () => correlation,
464
+ cosine: () => cosine,
465
+ dice: () => dice,
466
+ euclidean: () => euclidean,
467
+ hamming: () => hamming,
468
+ haversine: () => haversine,
469
+ hellinger: () => hellinger,
470
+ isNamedMetric: () => isNamedMetric,
471
+ jaccard: () => jaccard,
472
+ kulsinski: () => kulsinski,
473
+ llDirichlet: () => llDirichlet,
474
+ mahalanobis: () => mahalanobis,
475
+ manhattan: () => manhattan,
476
+ matching: () => matching,
477
+ minkowski: () => minkowski,
478
+ resolveMetric: () => resolveMetric,
479
+ rogersTanimoto: () => rogersTanimoto,
480
+ russellrao: () => russellrao,
481
+ sokalMichener: () => sokalMichener,
482
+ sokalSneath: () => sokalSneath,
483
+ sqeuclidean: () => sqeuclidean,
484
+ standardisedEuclidean: () => standardisedEuclidean,
485
+ symmetricKl: () => symmetricKl,
486
+ validateMetricOptionSizes: () => validateMetricOptionSizes,
487
+ weightedMinkowski: () => weightedMinkowski,
488
+ yule: () => yule
489
+ });
490
+ function euclidean(x, y) {
491
+ let result = 0;
492
+ for (let i = 0; i < x.length; i++) {
493
+ const d = x[i] - y[i];
494
+ result += d * d;
495
+ }
496
+ return Math.sqrt(result);
497
+ }
498
+ /** Squared euclidean — used internally by the optimizer (rdist). */
499
+ function sqeuclidean(x, y) {
500
+ let result = 0;
501
+ for (let i = 0; i < x.length; i++) {
502
+ const d = x[i] - y[i];
503
+ result += d * d;
504
+ }
505
+ return result;
506
+ }
507
+ function standardisedEuclidean(x, y, sigma) {
508
+ let result = 0;
509
+ for (let i = 0; i < x.length; i++) {
510
+ const d = x[i] - y[i];
511
+ result += d * d / sigma[i];
512
+ }
513
+ return Math.sqrt(result);
514
+ }
515
+ function manhattan(x, y) {
516
+ let result = 0;
517
+ for (let i = 0; i < x.length; i++) result += Math.abs(x[i] - y[i]);
518
+ return result;
519
+ }
520
+ function chebyshev(x, y) {
521
+ let result = 0;
522
+ for (let i = 0; i < x.length; i++) {
523
+ const v = Math.abs(x[i] - y[i]);
524
+ if (v > result) result = v;
525
+ }
526
+ return result;
527
+ }
528
+ function minkowski(x, y, p) {
529
+ let result = 0;
530
+ for (let i = 0; i < x.length; i++) result += Math.abs(x[i] - y[i]) ** p;
531
+ return result ** (1 / p);
532
+ }
533
+ function weightedMinkowski(x, y, w, p) {
534
+ let result = 0;
535
+ for (let i = 0; i < x.length; i++) result += w[i] * Math.abs(x[i] - y[i]) ** p;
536
+ return result ** (1 / p);
537
+ }
538
+ function mahalanobis(x, y, vinv) {
539
+ const n = x.length;
540
+ let result = 0;
541
+ const diff = new Float64Array(n);
542
+ for (let i = 0; i < n; i++) diff[i] = x[i] - y[i];
543
+ for (let i = 0; i < n; i++) {
544
+ let tmp = 0;
545
+ for (let j = 0; j < n; j++) tmp += vinv[i * n + j] * diff[j];
546
+ result += tmp * diff[i];
547
+ }
548
+ return Math.sqrt(result);
549
+ }
550
+ function hamming(x, y) {
551
+ let result = 0;
552
+ for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) result += 1;
553
+ return result / x.length;
554
+ }
555
+ function canberra(x, y) {
556
+ let result = 0;
557
+ for (let i = 0; i < x.length; i++) {
558
+ const denominator = Math.abs(x[i]) + Math.abs(y[i]);
559
+ if (denominator > 0) result += Math.abs(x[i] - y[i]) / denominator;
560
+ }
561
+ return result;
562
+ }
563
+ function brayCurtis(x, y) {
564
+ let numerator = 0;
565
+ let denominator = 0;
566
+ for (let i = 0; i < x.length; i++) {
567
+ numerator += Math.abs(x[i] - y[i]);
568
+ denominator += Math.abs(x[i] + y[i]);
569
+ }
570
+ return denominator > 0 ? numerator / denominator : 0;
571
+ }
572
+ function jaccard(x, y) {
573
+ let numNonZero = 0;
574
+ let numEqual = 0;
575
+ for (let i = 0; i < x.length; i++) {
576
+ const xt = x[i] !== 0;
577
+ const yt = y[i] !== 0;
578
+ numNonZero += xt || yt ? 1 : 0;
579
+ numEqual += xt && yt ? 1 : 0;
580
+ }
581
+ return numNonZero === 0 ? 0 : (numNonZero - numEqual) / numNonZero;
582
+ }
583
+ function matching(x, y) {
584
+ let numNotEqual = 0;
585
+ for (let i = 0; i < x.length; i++) if (x[i] !== 0 !== (y[i] !== 0)) numNotEqual += 1;
586
+ return numNotEqual / x.length;
587
+ }
588
+ function dice(x, y) {
589
+ let numTrueTrue = 0;
590
+ let numNotEqual = 0;
591
+ for (let i = 0; i < x.length; i++) {
592
+ const xt = x[i] !== 0;
593
+ const yt = y[i] !== 0;
594
+ numTrueTrue += xt && yt ? 1 : 0;
595
+ numNotEqual += xt !== yt ? 1 : 0;
596
+ }
597
+ return numNotEqual === 0 ? 0 : numNotEqual / (2 * numTrueTrue + numNotEqual);
598
+ }
599
+ function kulsinski(x, y) {
600
+ let numTrueTrue = 0;
601
+ let numNotEqual = 0;
602
+ for (let i = 0; i < x.length; i++) {
603
+ const xt = x[i] !== 0;
604
+ const yt = y[i] !== 0;
605
+ numTrueTrue += xt && yt ? 1 : 0;
606
+ numNotEqual += xt !== yt ? 1 : 0;
607
+ }
608
+ return numNotEqual === 0 ? 0 : (numNotEqual - numTrueTrue + x.length) / (numNotEqual + x.length);
609
+ }
610
+ function rogersTanimoto(x, y) {
611
+ let numNotEqual = 0;
612
+ for (let i = 0; i < x.length; i++) if (x[i] !== 0 !== (y[i] !== 0)) numNotEqual += 1;
613
+ return 2 * numNotEqual / (x.length + numNotEqual);
614
+ }
615
+ function russellrao(x, y) {
616
+ let numTrueTrue = 0;
617
+ let xNonZero = 0;
618
+ let yNonZero = 0;
619
+ for (let i = 0; i < x.length; i++) {
620
+ const xt = x[i] !== 0;
621
+ const yt = y[i] !== 0;
622
+ numTrueTrue += xt && yt ? 1 : 0;
623
+ xNonZero += xt ? 1 : 0;
624
+ yNonZero += yt ? 1 : 0;
625
+ }
626
+ if (numTrueTrue === xNonZero && numTrueTrue === yNonZero) return 0;
627
+ return (x.length - numTrueTrue) / x.length;
628
+ }
629
+ function sokalMichener(x, y) {
630
+ return rogersTanimoto(x, y);
631
+ }
632
+ function sokalSneath(x, y) {
633
+ let numTrueTrue = 0;
634
+ let numNotEqual = 0;
635
+ for (let i = 0; i < x.length; i++) {
636
+ const xt = x[i] !== 0;
637
+ const yt = y[i] !== 0;
638
+ numTrueTrue += xt && yt ? 1 : 0;
639
+ numNotEqual += xt !== yt ? 1 : 0;
640
+ }
641
+ return numNotEqual === 0 ? 0 : numNotEqual / (.5 * numTrueTrue + numNotEqual);
642
+ }
643
+ function yule(x, y) {
644
+ let tt = 0;
645
+ let tf = 0;
646
+ let ft = 0;
647
+ for (let i = 0; i < x.length; i++) {
648
+ const xt = x[i] !== 0;
649
+ const yt = y[i] !== 0;
650
+ tt += xt && yt ? 1 : 0;
651
+ tf += xt && !yt ? 1 : 0;
652
+ ft += !xt && yt ? 1 : 0;
653
+ }
654
+ const ff = x.length - tt - tf - ft;
655
+ if (tf === 0 || ft === 0) return 0;
656
+ return 2 * tf * ft / (tt * ff + tf * ft);
657
+ }
658
+ function cosine(x, y) {
659
+ let result = 0;
660
+ let normX = 0;
661
+ let normY = 0;
662
+ for (let i = 0; i < x.length; i++) {
663
+ result += x[i] * y[i];
664
+ normX += x[i] * x[i];
665
+ normY += y[i] * y[i];
666
+ }
667
+ if (normX === 0 && normY === 0) return 0;
668
+ if (normX === 0 || normY === 0) return 1;
669
+ return 1 - result / Math.sqrt(normX * normY);
670
+ }
671
+ function correlation(x, y) {
672
+ let muX = 0;
673
+ let muY = 0;
674
+ let normX = 0;
675
+ let normY = 0;
676
+ let dot = 0;
677
+ const n = x.length;
678
+ for (let i = 0; i < n; i++) {
679
+ muX += x[i];
680
+ muY += y[i];
681
+ }
682
+ muX /= n;
683
+ muY /= n;
684
+ for (let i = 0; i < n; i++) {
685
+ const sx = x[i] - muX;
686
+ const sy = y[i] - muY;
687
+ normX += sx * sx;
688
+ normY += sy * sy;
689
+ dot += sx * sy;
690
+ }
691
+ if (normX === 0 && normY === 0) return 0;
692
+ if (dot === 0) return 1;
693
+ return 1 - dot / Math.sqrt(normX * normY);
694
+ }
695
+ function hellinger(x, y) {
696
+ let result = 0;
697
+ let l1x = 0;
698
+ let l1y = 0;
699
+ for (let i = 0; i < x.length; i++) {
700
+ result += Math.sqrt(x[i] * y[i]);
701
+ l1x += x[i];
702
+ l1y += y[i];
703
+ }
704
+ if (l1x === 0 && l1y === 0) return 0;
705
+ if (l1x === 0 || l1y === 0) return 1;
706
+ return Math.sqrt(1 - result / Math.sqrt(l1x * l1y));
707
+ }
708
+ function approxLogGamma(x) {
709
+ if (x === 1) return 0;
710
+ return x * Math.log(x) - x + .5 * Math.log(2 * Math.PI / x) + 1 / (x * 12);
711
+ }
712
+ function logBeta(x, y) {
713
+ const a = Math.min(x, y);
714
+ const b = Math.max(x, y);
715
+ if (b < 5) {
716
+ let value = -Math.log(b);
717
+ for (let i = 1; i < Math.trunc(a); i++) value += Math.log(i) - Math.log(b + i);
718
+ return value;
719
+ }
720
+ return approxLogGamma(x) + approxLogGamma(y) - approxLogGamma(x + y);
721
+ }
722
+ function logSingleBeta(x) {
723
+ return Math.log(2) * (-2 * x + .5) + .5 * Math.log(2 * Math.PI / x) + .125 / x;
724
+ }
725
+ function llDirichlet(data1, data2) {
726
+ let n1 = 0;
727
+ let n2 = 0;
728
+ for (let i = 0; i < data1.length; i++) n1 += data1[i];
729
+ for (let i = 0; i < data2.length; i++) n2 += data2[i];
730
+ let logB = 0;
731
+ let selfDenom1 = 0;
732
+ let selfDenom2 = 0;
733
+ for (let i = 0; i < data1.length; i++) {
734
+ const d1 = data1[i];
735
+ const d2 = data2[i];
736
+ if (d1 * d2 > .9) {
737
+ logB += logBeta(d1, d2);
738
+ selfDenom1 += logSingleBeta(d1);
739
+ selfDenom2 += logSingleBeta(d2);
740
+ } else {
741
+ if (d1 > .9) selfDenom1 += logSingleBeta(d1);
742
+ if (d2 > .9) selfDenom2 += logSingleBeta(d2);
743
+ }
744
+ }
745
+ return Math.sqrt(1 / n2 * (logB - logBeta(n1, n2) - (selfDenom2 - logSingleBeta(n2))) + 1 / n1 * (logB - logBeta(n2, n1) - (selfDenom1 - logSingleBeta(n1))));
746
+ }
747
+ /**
748
+ * Symmetrized KL divergence. NOTE: the Python source mutates its inputs (adds z,
749
+ * normalizes in place); we copy to preserve the same numerics without side effects.
750
+ */
751
+ function symmetricKl(x, y, z = 1e-11) {
752
+ const n = x.length;
753
+ let xSum = 0;
754
+ let ySum = 0;
755
+ const xs = new Float64Array(n);
756
+ const ys = new Float64Array(n);
757
+ for (let i = 0; i < n; i++) {
758
+ xs[i] = x[i] + z;
759
+ xSum += xs[i];
760
+ ys[i] = y[i] + z;
761
+ ySum += ys[i];
762
+ }
763
+ for (let i = 0; i < n; i++) {
764
+ xs[i] = xs[i] / xSum;
765
+ ys[i] = ys[i] / ySum;
766
+ }
767
+ let kl1 = 0;
768
+ let kl2 = 0;
769
+ for (let i = 0; i < n; i++) {
770
+ kl1 += xs[i] * Math.log(xs[i] / ys[i]);
771
+ kl2 += ys[i] * Math.log(ys[i] / xs[i]);
772
+ }
773
+ return (kl1 + kl2) / 2;
774
+ }
775
+ function haversine(x, y) {
776
+ if (x.length !== 2) throw new UmapValidationError("haversine is only defined for 2 dimensional data");
777
+ const sinLat = Math.sin(.5 * (x[0] - y[0]));
778
+ const sinLong = Math.sin(.5 * (x[1] - y[1]));
779
+ const result = Math.sqrt(sinLat * sinLat + Math.cos(x[0]) * Math.cos(y[0]) * sinLong * sinLong);
780
+ return 2 * Math.asin(result);
781
+ }
782
+ function simple(fn, canonical) {
783
+ return {
784
+ canonical,
785
+ resolve: () => fn
786
+ };
787
+ }
788
+ const SPECS = {
789
+ euclidean: simple(euclidean, "euclidean"),
790
+ l2: simple(euclidean, "euclidean"),
791
+ sqeuclidean: simple(sqeuclidean, "sqeuclidean"),
792
+ manhattan: simple(manhattan, "manhattan"),
793
+ taxicab: simple(manhattan, "manhattan"),
794
+ l1: simple(manhattan, "manhattan"),
795
+ chebyshev: simple(chebyshev, "chebyshev"),
796
+ linfinity: simple(chebyshev, "chebyshev"),
797
+ linfty: simple(chebyshev, "chebyshev"),
798
+ linf: simple(chebyshev, "chebyshev"),
799
+ minkowski: {
800
+ canonical: "minkowski",
801
+ needs: ["p"],
802
+ resolve: (o) => {
803
+ const p = o.p ?? 2;
804
+ return (x, y) => minkowski(x, y, p);
805
+ }
806
+ },
807
+ seuclidean: {
808
+ canonical: "seuclidean",
809
+ needs: ["V"],
810
+ resolve: (o) => {
811
+ if (!o.V) throw new UmapValidationError("metric 'seuclidean' requires metricOptions.V");
812
+ const V = o.V;
813
+ return (x, y) => standardisedEuclidean(x, y, V);
814
+ }
815
+ },
816
+ standardised_euclidean: {
817
+ canonical: "seuclidean",
818
+ needs: ["V"],
819
+ resolve: (o) => {
820
+ if (!o.V) throw new UmapValidationError("metric 'standardised_euclidean' requires metricOptions.V");
821
+ const V = o.V;
822
+ return (x, y) => standardisedEuclidean(x, y, V);
823
+ }
824
+ },
825
+ wminkowski: {
826
+ canonical: "wminkowski",
827
+ needs: ["w", "p"],
828
+ resolve: (o) => {
829
+ if (!o.w) throw new UmapValidationError("metric 'wminkowski' requires metricOptions.w");
830
+ const w = o.w;
831
+ const p = o.p ?? 2;
832
+ return (x, y) => weightedMinkowski(x, y, w, p);
833
+ }
834
+ },
835
+ weighted_minkowski: {
836
+ canonical: "wminkowski",
837
+ needs: ["w", "p"],
838
+ resolve: (o) => {
839
+ if (!o.w) throw new UmapValidationError("metric 'weighted_minkowski' requires metricOptions.w");
840
+ const w = o.w;
841
+ const p = o.p ?? 2;
842
+ return (x, y) => weightedMinkowski(x, y, w, p);
843
+ }
844
+ },
845
+ mahalanobis: {
846
+ canonical: "mahalanobis",
847
+ needs: ["VInv"],
848
+ resolve: (o) => {
849
+ if (!o.VInv) throw new UmapValidationError("metric 'mahalanobis' requires metricOptions.VInv");
850
+ const VInv = o.VInv;
851
+ return (x, y) => mahalanobis(x, y, VInv);
852
+ }
853
+ },
854
+ canberra: simple(canberra, "canberra"),
855
+ braycurtis: simple(brayCurtis, "braycurtis"),
856
+ cosine: simple(cosine, "cosine"),
857
+ correlation: simple(correlation, "correlation"),
858
+ haversine: simple(haversine, "haversine"),
859
+ hamming: simple(hamming, "hamming"),
860
+ jaccard: simple(jaccard, "jaccard"),
861
+ dice: simple(dice, "dice"),
862
+ matching: simple(matching, "matching"),
863
+ kulsinski: simple(kulsinski, "kulsinski"),
864
+ rogerstanimoto: simple(rogersTanimoto, "rogerstanimoto"),
865
+ russellrao: simple(russellrao, "russellrao"),
866
+ sokalmichener: simple(sokalMichener, "sokalmichener"),
867
+ sokalsneath: simple(sokalSneath, "sokalsneath"),
868
+ yule: simple(yule, "yule"),
869
+ hellinger: simple(hellinger, "hellinger"),
870
+ ll_dirichlet: simple(llDirichlet, "ll_dirichlet"),
871
+ symmetric_kl: simple(symmetricKl, "symmetric_kl")
872
+ };
873
+ const NAMED_METRICS = Object.keys(SPECS);
874
+ /** Metrics that auto-enable angular RP-forest (umap_.py:1938-1946). */
875
+ const ANGULAR_METRICS = new Set([
876
+ "cosine",
877
+ "correlation",
878
+ "dice",
879
+ "jaccard",
880
+ "ll_dirichlet",
881
+ "hellinger"
882
+ ]);
883
+ function isNamedMetric(name) {
884
+ return name in SPECS;
885
+ }
886
+ function canonicalMetricName(name) {
887
+ const spec = SPECS[name];
888
+ if (!spec) throw new UmapValidationError(`unknown metric '${name}'`);
889
+ return spec.canonical;
890
+ }
891
+ function resolveMetric(name, options = {}) {
892
+ const spec = SPECS[name];
893
+ if (!spec) throw new UmapValidationError(`unknown metric '${name}' — named metrics: ${NAMED_METRICS.join(", ")}`);
894
+ return spec.resolve(options);
895
+ }
896
+ /**
897
+ * Size/positivity checks for metricOptions once the data dimension is known —
898
+ * a wrong-sized V/w/VInv would otherwise read undefined and silently poison
899
+ * every distance with NaN.
900
+ */
901
+ function validateMetricOptionSizes(name, options, dim) {
902
+ const spec = SPECS[name];
903
+ if (!spec) return;
904
+ const canonical = spec.canonical;
905
+ if ((canonical === "minkowski" || canonical === "wminkowski") && options.p !== void 0) {
906
+ if (!Number.isFinite(options.p) || options.p <= 0) throw new UmapValidationError("metricOptions.p must be a finite number > 0");
907
+ }
908
+ if (canonical === "seuclidean" && options.V) {
909
+ if (options.V.length !== dim) throw new UmapValidationError(`metricOptions.V has ${options.V.length} entries; the data has ${dim} features`);
910
+ for (let i = 0; i < dim; i++) {
911
+ const v = options.V[i];
912
+ if (!Number.isFinite(v) || v <= 0) throw new UmapValidationError(`metricOptions.V must be positive (index ${i} is ${v})`);
913
+ }
914
+ }
915
+ if (canonical === "wminkowski" && options.w) {
916
+ if (options.w.length !== dim) throw new UmapValidationError(`metricOptions.w has ${options.w.length} entries; the data has ${dim} features`);
917
+ }
918
+ if (canonical === "mahalanobis" && options.VInv) {
919
+ if (options.VInv.length !== dim * dim) throw new UmapValidationError(`metricOptions.VInv has ${options.VInv.length} entries; expected ${dim}×${dim} = ${dim * dim}`);
920
+ }
921
+ }
922
+
923
+ //#endregion
924
+ //#region src/knn/internal-metrics.ts
925
+ const FLOAT32_MAX = 34028234663852886e22;
926
+ const FLOAT32_EPS = 1.1920928955078125e-7;
927
+ function squaredEuclidean(x, y) {
928
+ let r = 0;
929
+ const n = x.length;
930
+ let i = 0;
931
+ const n4 = n - n % 4;
932
+ for (; i < n4; i += 4) {
933
+ const d0 = x[i] - y[i];
934
+ const d1 = x[i + 1] - y[i + 1];
935
+ const d2 = x[i + 2] - y[i + 2];
936
+ const d3 = x[i + 3] - y[i + 3];
937
+ r += d0 * d0 + d1 * d1 + d2 * d2 + d3 * d3;
938
+ }
939
+ for (; i < n; i++) {
940
+ const d = x[i] - y[i];
941
+ r += d * d;
942
+ }
943
+ return r;
944
+ }
945
+ /** alternative_cosine (distances.py:583-630). */
946
+ function alternativeCosine(x, y) {
947
+ let dot = 0;
948
+ let nx = 0;
949
+ let ny = 0;
950
+ for (let i = 0; i < x.length; i++) {
951
+ dot += x[i] * y[i];
952
+ nx += x[i] * x[i];
953
+ ny += y[i] * y[i];
954
+ }
955
+ if (nx === 0 && ny === 0) return 0;
956
+ if (nx === 0 || ny === 0) return FLOAT32_MAX;
957
+ if (dot <= 0) return FLOAT32_MAX;
958
+ return Math.log2(Math.sqrt(nx * ny) / dot);
959
+ }
960
+ /** alternative_hellinger (distances.py:1387-1416). */
961
+ function alternativeHellinger(x, y) {
962
+ let result = 0;
963
+ let l1x = 0;
964
+ let l1y = 0;
965
+ for (let i = 0; i < x.length; i++) {
966
+ result += Math.sqrt(x[i] * y[i]);
967
+ l1x += x[i];
968
+ l1y += y[i];
969
+ }
970
+ if (l1x === 0 && l1y === 0) return 0;
971
+ if (l1x === 0 || l1y === 0) return FLOAT32_MAX;
972
+ if (result <= 0) return FLOAT32_MAX;
973
+ return Math.log2(Math.sqrt(l1x * l1y) / result);
974
+ }
975
+ /** alternative_jaccard (distances.py:259-336 family). */
976
+ function alternativeJaccard(x, y) {
977
+ let numNonZero = 0;
978
+ let numEqual = 0;
979
+ for (let i = 0; i < x.length; i++) {
980
+ const xt = x[i] !== 0;
981
+ const yt = y[i] !== 0;
982
+ numNonZero += xt || yt ? 1 : 0;
983
+ numEqual += xt && yt ? 1 : 0;
984
+ }
985
+ if (numNonZero === 0) return 0;
986
+ if (numEqual === 0) return FLOAT32_MAX;
987
+ return -Math.log2(numEqual / numNonZero);
988
+ }
989
+ const identity = (d) => d;
990
+ /** Metric names that switch NNDescent to angular trees (pynndescent_.py:1059-1077). */
991
+ const ANGULAR_TREE_METRICS = new Set([
992
+ "cosine",
993
+ "dot",
994
+ "correlation",
995
+ "dice",
996
+ "jaccard",
997
+ "hellinger",
998
+ "hamming"
999
+ ]);
1000
+ /**
1001
+ * Resolve the internal metric + correction for NN-descent
1002
+ * (fast_distance_alternatives, distances.py:2170-2188).
1003
+ */
1004
+ function resolveInternalMetric(name, options) {
1005
+ const angularTrees = ANGULAR_TREE_METRICS.has(name);
1006
+ switch (name) {
1007
+ case "euclidean":
1008
+ case "l2": return {
1009
+ dist: squaredEuclidean,
1010
+ correction: (d) => Math.sqrt(d),
1011
+ angularTrees
1012
+ };
1013
+ case "cosine": return {
1014
+ dist: alternativeCosine,
1015
+ correction: (d) => 1 - 2 ** -d,
1016
+ angularTrees
1017
+ };
1018
+ case "hellinger": return {
1019
+ dist: alternativeHellinger,
1020
+ correction: (d) => Math.sqrt(1 - 2 ** -d),
1021
+ angularTrees
1022
+ };
1023
+ case "jaccard": return {
1024
+ dist: alternativeJaccard,
1025
+ correction: (d) => 1 - 2 ** -d,
1026
+ angularTrees
1027
+ };
1028
+ default: return {
1029
+ dist: resolveMetric(name, options),
1030
+ correction: identity,
1031
+ angularTrees
1032
+ };
1033
+ }
1034
+ }
1035
+ /** sparse_squared_euclidean (sparse.py:383-408). */
1036
+ function sparseSquaredEuclidean(ind1, data1, ind2, data2) {
1037
+ let r = 0;
1038
+ let i1 = 0;
1039
+ let i2 = 0;
1040
+ while (i1 < ind1.length && i2 < ind2.length) {
1041
+ const c1 = ind1[i1];
1042
+ const c2 = ind2[i2];
1043
+ if (c1 === c2) {
1044
+ const d = data1[i1] - data2[i2];
1045
+ r += d * d;
1046
+ i1++;
1047
+ i2++;
1048
+ } else if (c1 < c2) {
1049
+ r += data1[i1] * data1[i1];
1050
+ i1++;
1051
+ } else {
1052
+ r += data2[i2] * data2[i2];
1053
+ i2++;
1054
+ }
1055
+ }
1056
+ while (i1 < ind1.length) {
1057
+ r += data1[i1] * data1[i1];
1058
+ i1++;
1059
+ }
1060
+ while (i2 < ind2.length) {
1061
+ r += data2[i2] * data2[i2];
1062
+ i2++;
1063
+ }
1064
+ return r;
1065
+ }
1066
+ /** sparse_alternative_cosine (sparse.py:632-659). */
1067
+ function sparseAlternativeCosine(ind1, data1, ind2, data2) {
1068
+ let dot = 0;
1069
+ let i1 = 0;
1070
+ let i2 = 0;
1071
+ while (i1 < ind1.length && i2 < ind2.length) {
1072
+ const c1 = ind1[i1];
1073
+ const c2 = ind2[i2];
1074
+ if (c1 === c2) {
1075
+ dot += data1[i1] * data2[i2];
1076
+ i1++;
1077
+ i2++;
1078
+ } else if (c1 < c2) i1++;
1079
+ else i2++;
1080
+ }
1081
+ let nx = 0;
1082
+ for (let i = 0; i < data1.length; i++) nx += data1[i] * data1[i];
1083
+ let ny = 0;
1084
+ for (let i = 0; i < data2.length; i++) ny += data2[i] * data2[i];
1085
+ if (nx === 0 && ny === 0) return 0;
1086
+ if (nx === 0 || ny === 0) return FLOAT32_MAX;
1087
+ if (dot <= 0) return FLOAT32_MAX;
1088
+ return Math.log2(Math.sqrt(nx * ny) / dot);
1089
+ }
1090
+ /** sparse_correct_alternative_cosine clamps near-zero/negative to 0 (sparse.py:662-667). */
1091
+ function sparseCorrectAlternativeCosine(d) {
1092
+ if (Math.abs(d) < 1e-7 || d < 0) return 0;
1093
+ return 1 - 2 ** -d;
1094
+ }
1095
+ function resolveSparseInternalMetric(name) {
1096
+ switch (name) {
1097
+ case "euclidean":
1098
+ case "l2": return {
1099
+ dist: sparseSquaredEuclidean,
1100
+ correction: (d) => Math.sqrt(d),
1101
+ angularTrees: false
1102
+ };
1103
+ case "cosine": return {
1104
+ dist: sparseAlternativeCosine,
1105
+ correction: sparseCorrectAlternativeCosine,
1106
+ angularTrees: true
1107
+ };
1108
+ default: throw new Error(`sparse metric '${name}' is not supported (euclidean, cosine)`);
1109
+ }
1110
+ }
1111
+
1112
+ //#endregion
1113
+ //#region src/knn/exact.ts
1114
+ /** Row-wise exact kNN for dense data. Accepts a named metric or a custom MetricFn. */
1115
+ function exactKnnDense(X, k, metricName, { metricOptions = {}, disconnectionDistance = Infinity } = {}) {
1116
+ const { rows: n, cols: d, data } = X;
1117
+ const metric = typeof metricName === "function" ? metricName : resolveMetric(metricName, metricOptions);
1118
+ const fastEuclidean = metricName === "euclidean" || metricName === "l2";
1119
+ const indices = new Int32Array(n * k);
1120
+ const distances = new Float32Array(n * k);
1121
+ const rowDist = new Float64Array(n);
1122
+ const order = new Int32Array(n);
1123
+ const heapIdx = new Int32Array(k);
1124
+ const heapDst = new Float64Array(k);
1125
+ for (let i = 0; i < n; i++) {
1126
+ const xi = data.subarray(i * d, (i + 1) * d);
1127
+ if (fastEuclidean) {
1128
+ const base = i * d;
1129
+ for (let j = 0; j < n; j++) {
1130
+ const jb = j * d;
1131
+ let r = 0;
1132
+ for (let c = 0; c < d; c++) {
1133
+ const dd = data[base + c] - data[jb + c];
1134
+ r += dd * dd;
1135
+ }
1136
+ rowDist[j] = r;
1137
+ }
1138
+ } else for (let j = 0; j < n; j++) rowDist[j] = metric(xi, data.subarray(j * d, (j + 1) * d));
1139
+ selectKSmallest(rowDist, n, k, heapIdx, heapDst, order);
1140
+ for (let j = 0; j < k; j++) {
1141
+ let dist = heapDst[j];
1142
+ if (fastEuclidean) dist = Math.sqrt(dist);
1143
+ if (dist >= disconnectionDistance) {
1144
+ indices[i * k + j] = -1;
1145
+ distances[i * k + j] = Infinity;
1146
+ } else {
1147
+ indices[i * k + j] = heapIdx[j];
1148
+ distances[i * k + j] = dist;
1149
+ }
1150
+ }
1151
+ }
1152
+ return {
1153
+ indices,
1154
+ distances,
1155
+ k
1156
+ };
1157
+ }
1158
+ /**
1159
+ * Select k smallest of rowDist[0..n) with ties broken by smaller index (matching
1160
+ * a stable argsort); outputs sorted ascending into heapIdx/heapDst.
1161
+ */
1162
+ function selectKSmallest(rowDist, n, k, outIdx, outDst, scratchOrder) {
1163
+ let count = 0;
1164
+ const gt = (d1, i1, d2, i2) => d1 > d2 || d1 === d2 && i1 > i2;
1165
+ for (let j = 0; j < n; j++) {
1166
+ const dj = rowDist[j];
1167
+ if (count < k) {
1168
+ let c = count++;
1169
+ outDst[c] = dj;
1170
+ outIdx[c] = j;
1171
+ while (c > 0) {
1172
+ const parent = c - 1 >> 1;
1173
+ if (gt(outDst[c], outIdx[c], outDst[parent], outIdx[parent])) {
1174
+ swap(outDst, outIdx, c, parent);
1175
+ c = parent;
1176
+ } else break;
1177
+ }
1178
+ } else if (gt(outDst[0], outIdx[0], dj, j)) {
1179
+ outDst[0] = dj;
1180
+ outIdx[0] = j;
1181
+ let c = 0;
1182
+ for (;;) {
1183
+ const l = 2 * c + 1;
1184
+ const r = l + 1;
1185
+ let m = c;
1186
+ if (l < k && gt(outDst[l], outIdx[l], outDst[m], outIdx[m])) m = l;
1187
+ if (r < k && gt(outDst[r], outIdx[r], outDst[m], outIdx[m])) m = r;
1188
+ if (m === c) break;
1189
+ swap(outDst, outIdx, c, m);
1190
+ c = m;
1191
+ }
1192
+ }
1193
+ }
1194
+ for (let j = count; j < k; j++) {
1195
+ outDst[j] = Infinity;
1196
+ outIdx[j] = -1;
1197
+ }
1198
+ for (let j = 0; j < k; j++) scratchOrder[j] = j;
1199
+ const idxArr = Array.from({ length: k }, (_, j) => j);
1200
+ idxArr.sort((a, b) => outDst[a] - outDst[b] || outIdx[a] - outIdx[b]);
1201
+ const tmpD = new Float64Array(k);
1202
+ const tmpI = new Int32Array(k);
1203
+ for (let j = 0; j < k; j++) {
1204
+ tmpD[j] = outDst[idxArr[j]];
1205
+ tmpI[j] = outIdx[idxArr[j]];
1206
+ }
1207
+ outDst.set(tmpD);
1208
+ outIdx.set(tmpI);
1209
+ }
1210
+ function swap(d, i, a, b) {
1211
+ const td = d[a];
1212
+ d[a] = d[b];
1213
+ d[b] = td;
1214
+ const ti = i[a];
1215
+ i[a] = i[b];
1216
+ i[b] = ti;
1217
+ }
1218
+ /** Exact kNN for CSR input (euclidean or cosine). */
1219
+ function exactKnnSparse(X, k, metricName) {
1220
+ const n = X.rows;
1221
+ const internal = resolveSparseInternalMetric(metricName);
1222
+ const indices = new Int32Array(n * k);
1223
+ const distances = new Float32Array(n * k);
1224
+ const rowDist = new Float64Array(n);
1225
+ const heapIdx = new Int32Array(k);
1226
+ const heapDst = new Float64Array(k);
1227
+ const order = new Int32Array(n);
1228
+ const rowInd = (i) => X.indices.subarray(X.indptr[i], X.indptr[i + 1]);
1229
+ const rowDat = (i) => X.data.subarray(X.indptr[i], X.indptr[i + 1]);
1230
+ for (let i = 0; i < n; i++) {
1231
+ const ii = rowInd(i);
1232
+ const di = rowDat(i);
1233
+ for (let j = 0; j < n; j++) rowDist[j] = internal.dist(ii, di, rowInd(j), rowDat(j));
1234
+ selectKSmallest(rowDist, n, k, heapIdx, heapDst, order);
1235
+ for (let j = 0; j < k; j++) {
1236
+ indices[i * k + j] = heapIdx[j];
1237
+ distances[i * k + j] = internal.correction(heapDst[j]);
1238
+ }
1239
+ }
1240
+ return {
1241
+ indices,
1242
+ distances,
1243
+ k
1244
+ };
1245
+ }
1246
+ /**
1247
+ * metric='precomputed': X is a dense n×n distance matrix; per-row argsort prefix
1248
+ * (umap_.py:312-324); infinite distances → index -1.
1249
+ */
1250
+ function knnFromPrecomputed(X, k) {
1251
+ const n = X.rows;
1252
+ if (X.cols !== n) throw new UmapValidationError(`precomputed distance matrix must be square, got ${n}x${X.cols}`);
1253
+ const indices = new Int32Array(n * k);
1254
+ const distances = new Float32Array(n * k);
1255
+ const heapIdx = new Int32Array(k);
1256
+ const heapDst = new Float64Array(k);
1257
+ const order = new Int32Array(n);
1258
+ const rowDist = new Float64Array(n);
1259
+ for (let i = 0; i < n; i++) {
1260
+ for (let j = 0; j < n; j++) rowDist[j] = X.data[i * n + j];
1261
+ selectKSmallest(rowDist, n, k, heapIdx, heapDst, order);
1262
+ for (let j = 0; j < k; j++) {
1263
+ const dist = heapDst[j];
1264
+ if (dist === Infinity) {
1265
+ indices[i * k + j] = -1;
1266
+ distances[i * k + j] = Infinity;
1267
+ } else {
1268
+ indices[i * k + j] = heapIdx[j];
1269
+ distances[i * k + j] = dist;
1270
+ }
1271
+ }
1272
+ }
1273
+ return {
1274
+ indices,
1275
+ distances,
1276
+ k
1277
+ };
1278
+ }
1279
+
1280
+ //#endregion
1281
+ //#region src/rng.ts
1282
+ /**
1283
+ * Seeded PRNG — PCG32 (O'Neill, pcg-random.org), the reproducible generator of §5.5.
1284
+ *
1285
+ * Test vectors in test/unit/rng.test.ts were generated from the canonical C reference
1286
+ * (pcg_basic.c) compiled locally; the first draw for seed(42, 54) is 0xa15c02b7,
1287
+ * matching the published pcg32-demo output.
1288
+ *
1289
+ * 64-bit state is held as two u32 words; the LCG multiply uses exact 16-bit limb
1290
+ * arithmetic (all intermediates < 2^53).
1291
+ */
1292
+ const MULT_HI = 1481765933;
1293
+ const MULT_LO = 1284865837;
1294
+ var Pcg32 = class Pcg32 {
1295
+ sHi = 0;
1296
+ sLo = 0;
1297
+ iHi = 0;
1298
+ iLo = 0;
1299
+ spareNormal = null;
1300
+ constructor(seedHi, seedLo, seqHi, seqLo) {
1301
+ this.iHi = (seqHi << 1 | seqLo >>> 31) >>> 0;
1302
+ this.iLo = (seqLo << 1 | 1) >>> 0;
1303
+ this.sHi = 0;
1304
+ this.sLo = 0;
1305
+ this.nextU32();
1306
+ const lo = this.sLo + (seedLo >>> 0);
1307
+ this.sLo = lo >>> 0;
1308
+ this.sHi = this.sHi + (seedHi >>> 0) + (lo > 4294967295 ? 1 : 0) >>> 0;
1309
+ this.nextU32();
1310
+ }
1311
+ /** Derive from a JS number seed (integer-valued; uses full 53-bit range). */
1312
+ static fromSeed(seed, stream = 54) {
1313
+ const s = Math.abs(Math.floor(seed));
1314
+ return new Pcg32(Math.floor(s / 4294967296) >>> 0, s >>> 0, 0, stream >>> 0);
1315
+ }
1316
+ nextU32() {
1317
+ const oHi = this.sHi;
1318
+ const oLo = this.sLo;
1319
+ const a0 = oLo & 65535;
1320
+ const a1 = oLo >>> 16;
1321
+ const a2 = oHi & 65535;
1322
+ const a3 = oHi >>> 16;
1323
+ const b0 = MULT_LO & 65535;
1324
+ const b1 = MULT_LO >>> 16;
1325
+ const b2 = MULT_HI & 65535;
1326
+ const b3 = MULT_HI >>> 16;
1327
+ const p0 = a0 * b0;
1328
+ const p1 = a1 * b0 + a0 * b1;
1329
+ const p2 = a2 * b0 + a1 * b1 + a0 * b2;
1330
+ const p3 = a3 * b0 + a2 * b1 + a1 * b2 + a0 * b3;
1331
+ let l0 = p0 & 65535;
1332
+ let acc = Math.floor(p0 / 65536) + p1;
1333
+ let l1 = acc & 65535;
1334
+ acc = Math.floor(acc / 65536) + p2;
1335
+ let l2 = acc & 65535;
1336
+ acc = Math.floor(acc / 65536) + p3;
1337
+ let l3 = acc & 65535;
1338
+ let s = l0 + (this.iLo & 65535);
1339
+ l0 = s & 65535;
1340
+ s = l1 + (this.iLo >>> 16) + (s >>> 16);
1341
+ l1 = s & 65535;
1342
+ s = l2 + (this.iHi & 65535) + (s >>> 16);
1343
+ l2 = s & 65535;
1344
+ s = l3 + (this.iHi >>> 16) + (s >>> 16);
1345
+ l3 = s & 65535;
1346
+ this.sLo = (l1 << 16 | l0) >>> 0;
1347
+ this.sHi = (l3 << 16 | l2) >>> 0;
1348
+ const xHi = (oHi ^ oHi >>> 18) >>> 0;
1349
+ const xLo = (oLo ^ (oHi << 14 | oLo >>> 18)) >>> 0;
1350
+ const xorshifted = (xHi << 5 | xLo >>> 27) >>> 0;
1351
+ const rot = oHi >>> 27;
1352
+ return (xorshifted >>> rot | xorshifted << (-rot & 31)) >>> 0;
1353
+ }
1354
+ /** Uniform float in [0, 1). */
1355
+ nextFloat() {
1356
+ return this.nextU32() * 23283064365386963e-26;
1357
+ }
1358
+ /** Uniform float in [lo, hi). */
1359
+ uniform(lo, hi) {
1360
+ return lo + (hi - lo) * this.nextFloat();
1361
+ }
1362
+ /** Integer in [0, n) — modulo method (matches umap's structural usage). */
1363
+ randint(n) {
1364
+ return this.nextU32() % n;
1365
+ }
1366
+ /** Integer in [lo, hi) like numpy randint. */
1367
+ randintRange(lo, hi) {
1368
+ return lo + this.nextU32() % (hi - lo);
1369
+ }
1370
+ /** Standard normal via Box–Muller (cached spare). */
1371
+ normal() {
1372
+ if (this.spareNormal !== null) {
1373
+ const v$1 = this.spareNormal;
1374
+ this.spareNormal = null;
1375
+ return v$1;
1376
+ }
1377
+ let u = 0;
1378
+ let v = 0;
1379
+ do
1380
+ u = this.nextFloat();
1381
+ while (u <= 1e-12);
1382
+ v = this.nextFloat();
1383
+ const r = Math.sqrt(-2 * Math.log(u));
1384
+ const theta = 2 * Math.PI * v;
1385
+ this.spareNormal = r * Math.sin(theta);
1386
+ return r * Math.cos(theta);
1387
+ }
1388
+ /** In-place Fisher–Yates shuffle. */
1389
+ shuffle(arr) {
1390
+ for (let i = arr.length - 1; i > 0; i--) {
1391
+ const j = this.randint(i + 1);
1392
+ const t = arr[i];
1393
+ arr[i] = arr[j];
1394
+ arr[j] = t;
1395
+ }
1396
+ }
1397
+ /**
1398
+ * Seed a 3-word tau state like Python's
1399
+ * `random_state.randint(INT32_MIN, INT32_MAX, 3)` (values in int32 range).
1400
+ */
1401
+ tauState() {
1402
+ const st = new Int32Array(3);
1403
+ for (let i = 0; i < 3; i++) st[i] = this.nextU32() | 0;
1404
+ return st;
1405
+ }
1406
+ /** Snapshot the full generator state (backend-fallback rng restoration). */
1407
+ clone() {
1408
+ const c = new Pcg32(0, 0, 0, 0);
1409
+ c.sHi = this.sHi;
1410
+ c.sLo = this.sLo;
1411
+ c.iHi = this.iHi;
1412
+ c.iLo = this.iLo;
1413
+ c.spareNormal = this.spareNormal;
1414
+ return c;
1415
+ }
1416
+ /** Restore state from a clone() snapshot (un-consumes draws after the snapshot). */
1417
+ restoreFrom(snapshot) {
1418
+ this.sHi = snapshot.sHi;
1419
+ this.sLo = snapshot.sLo;
1420
+ this.iHi = snapshot.iHi;
1421
+ this.iLo = snapshot.iLo;
1422
+ this.spareNormal = snapshot.spareNormal;
1423
+ }
1424
+ };
1425
+ /**
1426
+ * taus88 combined Tausworthe step, structurally identical to umap's `tau_rand_int`
1427
+ * but on u32 words (Python runs it on int64 with 32-bit truncations). Used for the
1428
+ * SGD negative-sampling stream; parity with Python is statistical, not bitwise (§5.5).
1429
+ * Returns a signed int32.
1430
+ */
1431
+ function tauRandInt(state) {
1432
+ const s0 = state[0] >>> 0;
1433
+ const s1 = state[1] >>> 0;
1434
+ const s2 = state[2] >>> 0;
1435
+ state[0] = (s0 & 4294967294) << 12 ^ (s0 << 13 >>> 0 ^ s0) >>> 19 | 0;
1436
+ state[1] = (s1 & 4294967288) << 4 ^ (s1 << 2 >>> 0 ^ s1) >>> 25 | 0;
1437
+ state[2] = (s2 & 4294967280) << 17 ^ (s2 << 3 >>> 0 ^ s2) >>> 11 | 0;
1438
+ return state[0] ^ state[1] ^ state[2] | 0;
1439
+ }
1440
+ /** Floor-mod (Python `%` semantics) of a signed int by a positive int. */
1441
+ function floorMod(a, n) {
1442
+ const r = a % n;
1443
+ return r < 0 ? r + n : r;
1444
+ }
1445
+ /** tau_rand: uniform in [0,1] from tauRandInt (umap utils.py:66-80). */
1446
+ function tauRand(state) {
1447
+ return Math.abs(tauRandInt(state) / 2147483647);
1448
+ }
1449
+
1450
+ //#endregion
1451
+ //#region src/knn/heap.ts
1452
+ function makeHeap(n, size) {
1453
+ return {
1454
+ indices: new Int32Array(n * size).fill(-1),
1455
+ distances: new Float32Array(n * size).fill(Infinity),
1456
+ flags: new Uint8Array(n * size),
1457
+ n,
1458
+ size
1459
+ };
1460
+ }
1461
+ /**
1462
+ * checked_flagged_heap_push (utils.py:471-533). Operates on one row given by base
1463
+ * offset. Rejects p >= root (ties dropped) and duplicates. Returns 1 if pushed.
1464
+ */
1465
+ function checkedFlaggedHeapPush(heap, row, p, n, f) {
1466
+ const size = heap.size;
1467
+ const base = row * size;
1468
+ const priorities = heap.distances;
1469
+ const indices = heap.indices;
1470
+ const flags = heap.flags;
1471
+ if (p >= priorities[base]) return 0;
1472
+ for (let i$1 = 0; i$1 < size; i$1++) if (indices[base + i$1] === n) return 0;
1473
+ let i = 0;
1474
+ for (;;) {
1475
+ const ic1 = 2 * i + 1;
1476
+ const ic2 = ic1 + 1;
1477
+ let iSwap;
1478
+ if (ic1 >= size) break;
1479
+ if (ic2 >= size) if (priorities[base + ic1] > p) iSwap = ic1;
1480
+ else break;
1481
+ else if (priorities[base + ic1] >= priorities[base + ic2]) if (p < priorities[base + ic1]) iSwap = ic1;
1482
+ else break;
1483
+ else if (p < priorities[base + ic2]) iSwap = ic2;
1484
+ else break;
1485
+ priorities[base + i] = priorities[base + iSwap];
1486
+ indices[base + i] = indices[base + iSwap];
1487
+ flags[base + i] = flags[base + iSwap];
1488
+ i = iSwap;
1489
+ }
1490
+ priorities[base + i] = p;
1491
+ indices[base + i] = n;
1492
+ flags[base + i] = f;
1493
+ return 1;
1494
+ }
1495
+ /** checked_heap_push (utils.py:409-468) — no flags. */
1496
+ function checkedHeapPush(priorities, indices, base, size, p, n) {
1497
+ if (p >= priorities[base]) return 0;
1498
+ for (let i$1 = 0; i$1 < size; i$1++) if (indices[base + i$1] === n) return 0;
1499
+ let i = 0;
1500
+ for (;;) {
1501
+ const ic1 = 2 * i + 1;
1502
+ const ic2 = ic1 + 1;
1503
+ let iSwap;
1504
+ if (ic1 >= size) break;
1505
+ if (ic2 >= size) if (priorities[base + ic1] > p) iSwap = ic1;
1506
+ else break;
1507
+ else if (priorities[base + ic1] >= priorities[base + ic2]) if (p < priorities[base + ic1]) iSwap = ic1;
1508
+ else break;
1509
+ else if (p < priorities[base + ic2]) iSwap = ic2;
1510
+ else break;
1511
+ priorities[base + i] = priorities[base + iSwap];
1512
+ indices[base + i] = indices[base + iSwap];
1513
+ i = iSwap;
1514
+ }
1515
+ priorities[base + i] = p;
1516
+ indices[base + i] = n;
1517
+ return 1;
1518
+ }
1519
+ /** simple_heap_push (utils.py:352-406) — no duplicate scan (query uses visited mask). */
1520
+ function simpleHeapPush(priorities, indices, size, p, n) {
1521
+ if (p >= priorities[0]) return 0;
1522
+ let i = 0;
1523
+ for (;;) {
1524
+ const ic1 = 2 * i + 1;
1525
+ const ic2 = ic1 + 1;
1526
+ let iSwap;
1527
+ if (ic1 >= size) break;
1528
+ if (ic2 >= size) if (priorities[ic1] > p) iSwap = ic1;
1529
+ else break;
1530
+ else if (priorities[ic1] >= priorities[ic2]) if (p < priorities[ic1]) iSwap = ic1;
1531
+ else break;
1532
+ else if (p < priorities[ic2]) iSwap = ic2;
1533
+ else break;
1534
+ priorities[i] = priorities[iSwap];
1535
+ indices[i] = indices[iSwap];
1536
+ i = iSwap;
1537
+ }
1538
+ priorities[i] = p;
1539
+ indices[i] = n;
1540
+ return 1;
1541
+ }
1542
+ function siftdownRange(distances, indices, base, len, elt) {
1543
+ let e = elt;
1544
+ while (e * 2 + 1 < len) {
1545
+ const leftChild = e * 2 + 1;
1546
+ const rightChild = leftChild + 1;
1547
+ let swap$1 = e;
1548
+ if (distances[base + swap$1] < distances[base + leftChild]) swap$1 = leftChild;
1549
+ if (rightChild < len && distances[base + swap$1] < distances[base + rightChild]) swap$1 = rightChild;
1550
+ if (swap$1 === e) break;
1551
+ const td = distances[base + e];
1552
+ distances[base + e] = distances[base + swap$1];
1553
+ distances[base + swap$1] = td;
1554
+ const ti = indices[base + e];
1555
+ indices[base + e] = indices[base + swap$1];
1556
+ indices[base + swap$1] = ti;
1557
+ e = swap$1;
1558
+ }
1559
+ }
1560
+ /** deheap_sort (utils.py:189-218): per-row heapsort → ascending, (-1, inf) tail. */
1561
+ function deheapSort(heap) {
1562
+ const { indices, distances, n, size } = heap;
1563
+ for (let r = 0; r < n; r++) {
1564
+ const base = r * size;
1565
+ for (let j = size - 1; j > 0; j--) {
1566
+ const td = distances[base];
1567
+ distances[base] = distances[base + j];
1568
+ distances[base + j] = td;
1569
+ const ti = indices[base];
1570
+ indices[base] = indices[base + j];
1571
+ indices[base + j] = ti;
1572
+ siftdownRange(distances, indices, base, j, 0);
1573
+ }
1574
+ }
1575
+ }
1576
+
1577
+ //#endregion
1578
+ //#region src/knn/nndescent.ts
1579
+ /** init_rp_tree: all pairs within each leaf. */
1580
+ function initRpTree(nLeaves, leafCols, leafArray, heap, dist, leafAcceptance = "max") {
1581
+ const k = heap.size;
1582
+ const distances = heap.distances;
1583
+ const either = leafAcceptance === "either";
1584
+ for (let leaf = 0; leaf < nLeaves; leaf++) {
1585
+ const base = leaf * leafCols;
1586
+ for (let a = 0; a < leafCols; a++) {
1587
+ const p = leafArray[base + a];
1588
+ if (p < 0) break;
1589
+ for (let b = a + 1; b < leafCols; b++) {
1590
+ const q = leafArray[base + b];
1591
+ if (q < 0) break;
1592
+ const d = dist(p, q);
1593
+ if (either ? d < distances[p * k] || d < distances[q * k] : d < Math.max(distances[p * k], distances[q * k])) {
1594
+ checkedFlaggedHeapPush(heap, p, d, q, 1);
1595
+ checkedFlaggedHeapPush(heap, q, d, p, 1);
1596
+ }
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ /** init_random: one attempt per missing slot (pynndescent_.py:188-203). */
1602
+ function initRandom(n, heap, dist, rngState) {
1603
+ const k = heap.size;
1604
+ for (let i = 0; i < n; i++) if (heap.indices[i * k] < 0) {
1605
+ let filled = 0;
1606
+ for (let j = 0; j < k; j++) if (heap.indices[i * k + j] >= 0) filled++;
1607
+ const missing = k - filled;
1608
+ for (let j = 0; j < missing; j++) {
1609
+ const idx = Math.abs(tauRandInt(rngState)) % n;
1610
+ const d = dist(idx, i);
1611
+ checkedFlaggedHeapPush(heap, i, d, idx, 1);
1612
+ }
1613
+ }
1614
+ }
1615
+ /**
1616
+ * new_build_candidates (utils.py:221-320): sample up to maxCandidates new/old
1617
+ * candidates per vertex via random priorities; clear "new" flags for sampled entries.
1618
+ * The master rng state is NOT advanced (per-thread copies in Python; thread 0 here).
1619
+ */
1620
+ function buildCandidates(heap, maxCandidates, masterRng) {
1621
+ const n = heap.n;
1622
+ const k = heap.size;
1623
+ const newIdx = new Int32Array(n * maxCandidates).fill(-1);
1624
+ const newPri = new Float32Array(n * maxCandidates).fill(Infinity);
1625
+ const oldIdx = new Int32Array(n * maxCandidates).fill(-1);
1626
+ const oldPri = new Float32Array(n * maxCandidates).fill(Infinity);
1627
+ const localRng = Int32Array.from(masterRng);
1628
+ for (let i = 0; i < n; i++) for (let j = 0; j < k; j++) {
1629
+ const idx = heap.indices[i * k + j];
1630
+ if (idx < 0) continue;
1631
+ const d = tauRand(localRng);
1632
+ if (heap.flags[i * k + j] === 1) {
1633
+ checkedHeapPush(newPri, newIdx, i * maxCandidates, maxCandidates, d, idx);
1634
+ checkedHeapPush(newPri, newIdx, idx * maxCandidates, maxCandidates, d, i);
1635
+ } else {
1636
+ checkedHeapPush(oldPri, oldIdx, i * maxCandidates, maxCandidates, d, idx);
1637
+ checkedHeapPush(oldPri, oldIdx, idx * maxCandidates, maxCandidates, d, i);
1638
+ }
1639
+ }
1640
+ for (let i = 0; i < n; i++) for (let j = 0; j < k; j++) {
1641
+ const idx = heap.indices[i * k + j];
1642
+ if (idx < 0 || heap.flags[i * k + j] !== 1) continue;
1643
+ const cbase = i * maxCandidates;
1644
+ for (let c = 0; c < maxCandidates; c++) if (newIdx[cbase + c] === idx) {
1645
+ heap.flags[i * k + j] = 0;
1646
+ break;
1647
+ }
1648
+ }
1649
+ return {
1650
+ newIdx,
1651
+ oldIdx
1652
+ };
1653
+ }
1654
+ /** Local joins over candidate lists; returns number of successful pushes. */
1655
+ function processCandidates(heap, newIdx, oldIdx, maxCandidates, dist) {
1656
+ const n = heap.n;
1657
+ const k = heap.size;
1658
+ const distances = heap.distances;
1659
+ let c = 0;
1660
+ for (let i = 0; i < n; i++) {
1661
+ const base = i * maxCandidates;
1662
+ for (let j = 0; j < maxCandidates; j++) {
1663
+ const p = newIdx[base + j];
1664
+ if (p < 0) continue;
1665
+ for (let t = j; t < maxCandidates; t++) {
1666
+ const q = newIdx[base + t];
1667
+ if (q < 0) continue;
1668
+ const d = dist(p, q);
1669
+ if (d <= Math.max(distances[p * k], distances[q * k])) {
1670
+ c += checkedFlaggedHeapPush(heap, p, d, q, 1);
1671
+ c += checkedFlaggedHeapPush(heap, q, d, p, 1);
1672
+ }
1673
+ }
1674
+ for (let t = 0; t < maxCandidates; t++) {
1675
+ const q = oldIdx[base + t];
1676
+ if (q < 0) continue;
1677
+ const d = dist(p, q);
1678
+ if (d <= Math.max(distances[p * k], distances[q * k])) {
1679
+ c += checkedFlaggedHeapPush(heap, p, d, q, 1);
1680
+ c += checkedFlaggedHeapPush(heap, q, d, p, 1);
1681
+ }
1682
+ }
1683
+ }
1684
+ }
1685
+ return c;
1686
+ }
1687
+ /**
1688
+ * Full NN-descent. `leafArray` from the RP forest (or null to skip tree init).
1689
+ * Returns the heap AFTER deheap_sort: ascending distances, (-1, inf) tails.
1690
+ * Distances are in the INTERNAL metric space (corrections applied by the caller).
1691
+ */
1692
+ function nnDescent(n, nNeighbors, dist, rngState, leafArray, { maxCandidates = 60, nIters = 10, delta = .001, leafAcceptance = "max", onIteration } = {}) {
1693
+ const heap = makeHeap(n, nNeighbors);
1694
+ if (leafArray) initRpTree(leafArray.rows, leafArray.cols, leafArray.data, heap, dist, leafAcceptance);
1695
+ initRandom(n, heap, dist, rngState);
1696
+ for (let iter = 0; iter < nIters; iter++) {
1697
+ const { newIdx, oldIdx } = buildCandidates(heap, maxCandidates, rngState);
1698
+ const c = processCandidates(heap, newIdx, oldIdx, maxCandidates, dist);
1699
+ onIteration?.(iter, c);
1700
+ if (c <= delta * nNeighbors * n) break;
1701
+ }
1702
+ deheapSort(heap);
1703
+ return heap;
1704
+ }
1705
+ /** umap's NNDescent parameter rules (umap_.py:327-328). */
1706
+ function umapNnDescentParams(n) {
1707
+ return {
1708
+ nTrees: Math.min(64, 5 + Math.round(Math.sqrt(n) / 20)),
1709
+ nIters: Math.max(5, Math.round(Math.log2(n)))
1710
+ };
1711
+ }
1712
+
1713
+ //#endregion
1714
+ //#region src/knn/rpforest.ts
1715
+ const RP_EPS = 1e-8;
1716
+ const MIN_SPLIT_BALANCE = .1;
1717
+ function defaultLeafSize(nNeighbors) {
1718
+ return Math.max(60, Math.min(256, 5 * nNeighbors));
1719
+ }
1720
+ function partition(indices, side, nLeft, nRight) {
1721
+ const left = new Int32Array(nLeft);
1722
+ const right = new Int32Array(nRight);
1723
+ let il = 0;
1724
+ let ir = 0;
1725
+ for (let i = 0; i < indices.length; i++) if (side[i] === 0) left[il++] = indices[i];
1726
+ else right[ir++] = indices[i];
1727
+ return {
1728
+ left,
1729
+ right
1730
+ };
1731
+ }
1732
+ function euclideanSplit(data, dim, indices, rngState) {
1733
+ const n = indices.length;
1734
+ const leftIndex = floorMod(tauRandInt(rngState), n);
1735
+ let rightIndex = floorMod(tauRandInt(rngState), n);
1736
+ if (leftIndex === rightIndex) rightIndex += 1;
1737
+ rightIndex = floorMod(rightIndex, n);
1738
+ const l = indices[leftIndex];
1739
+ const r = indices[rightIndex];
1740
+ const hyperplane = new Float32Array(dim);
1741
+ let offset = 0;
1742
+ for (let d = 0; d < dim; d++) {
1743
+ const hv = data[l * dim + d] - data[r * dim + d];
1744
+ hyperplane[d] = hv;
1745
+ offset -= hv * (data[l * dim + d] + data[r * dim + d]) / 2;
1746
+ }
1747
+ const side = new Uint8Array(n);
1748
+ let nLeft = 0;
1749
+ let nRight = 0;
1750
+ for (let i = 0; i < n; i++) {
1751
+ let margin = offset;
1752
+ const p = indices[i];
1753
+ for (let d = 0; d < dim; d++) margin += hyperplane[d] * data[p * dim + d];
1754
+ if (Math.abs(margin) < RP_EPS) side[i] = Math.abs(tauRandInt(rngState)) % 2;
1755
+ else if (margin > 0) side[i] = 0;
1756
+ else side[i] = 1;
1757
+ if (side[i] === 0) nLeft++;
1758
+ else nRight++;
1759
+ }
1760
+ if (nLeft === 0 || nRight === 0) {
1761
+ nLeft = 0;
1762
+ nRight = 0;
1763
+ for (let i = 0; i < n; i++) {
1764
+ side[i] = floorMod(tauRandInt(rngState), 2);
1765
+ if (side[i] === 0) nLeft++;
1766
+ else nRight++;
1767
+ }
1768
+ }
1769
+ const { left, right } = partition(indices, side, nLeft, nRight);
1770
+ return {
1771
+ left,
1772
+ right,
1773
+ hyperplane,
1774
+ offset
1775
+ };
1776
+ }
1777
+ function angularSplit(data, dim, indices, rngState) {
1778
+ const n = indices.length;
1779
+ const leftIndex = floorMod(tauRandInt(rngState), n);
1780
+ let rightIndex = floorMod(tauRandInt(rngState), n);
1781
+ if (leftIndex === rightIndex) rightIndex += 1;
1782
+ rightIndex = floorMod(rightIndex, n);
1783
+ const l = indices[leftIndex];
1784
+ const r = indices[rightIndex];
1785
+ let leftNorm = 0;
1786
+ let rightNorm = 0;
1787
+ for (let d = 0; d < dim; d++) {
1788
+ leftNorm += data[l * dim + d] * data[l * dim + d];
1789
+ rightNorm += data[r * dim + d] * data[r * dim + d];
1790
+ }
1791
+ leftNorm = Math.sqrt(leftNorm);
1792
+ rightNorm = Math.sqrt(rightNorm);
1793
+ if (Math.abs(leftNorm) < RP_EPS) leftNorm = 1;
1794
+ if (Math.abs(rightNorm) < RP_EPS) rightNorm = 1;
1795
+ const hyperplane = new Float32Array(dim);
1796
+ let hnorm = 0;
1797
+ for (let d = 0; d < dim; d++) {
1798
+ const hv = data[l * dim + d] / leftNorm - data[r * dim + d] / rightNorm;
1799
+ hyperplane[d] = hv;
1800
+ hnorm += hv * hv;
1801
+ }
1802
+ hnorm = Math.sqrt(hnorm);
1803
+ if (Math.abs(hnorm) < RP_EPS) hnorm = 1;
1804
+ for (let d = 0; d < dim; d++) hyperplane[d] = hyperplane[d] / hnorm;
1805
+ const side = new Uint8Array(n);
1806
+ let nLeft = 0;
1807
+ let nRight = 0;
1808
+ for (let i = 0; i < n; i++) {
1809
+ let margin = 0;
1810
+ const p = indices[i];
1811
+ for (let d = 0; d < dim; d++) margin += hyperplane[d] * data[p * dim + d];
1812
+ if (Math.abs(margin) < RP_EPS) side[i] = floorMod(tauRandInt(rngState), 2);
1813
+ else if (margin > 0) side[i] = 0;
1814
+ else side[i] = 1;
1815
+ if (side[i] === 0) nLeft++;
1816
+ else nRight++;
1817
+ }
1818
+ if (nLeft === 0 || nRight === 0) {
1819
+ nLeft = 0;
1820
+ nRight = 0;
1821
+ for (let i = 0; i < n; i++) {
1822
+ side[i] = floorMod(tauRandInt(rngState), 2);
1823
+ if (side[i] === 0) nLeft++;
1824
+ else nRight++;
1825
+ }
1826
+ }
1827
+ const { left, right } = partition(indices, side, nLeft, nRight);
1828
+ return {
1829
+ left,
1830
+ right,
1831
+ hyperplane,
1832
+ offset: 0
1833
+ };
1834
+ }
1835
+ function buildTree(data, dim, indices, leafSize, maxDepth, angular, rngState) {
1836
+ if (indices.length > leafSize && maxDepth > 0) {
1837
+ const { left, right, hyperplane, offset } = angular ? angularSplit(data, dim, indices, rngState) : euclideanSplit(data, dim, indices, rngState);
1838
+ return {
1839
+ left: buildTree(data, dim, left, leafSize, maxDepth - 1, angular, rngState),
1840
+ right: buildTree(data, dim, right, leafSize, maxDepth - 1, angular, rngState),
1841
+ hyperplane,
1842
+ offset,
1843
+ points: null
1844
+ };
1845
+ }
1846
+ return {
1847
+ left: null,
1848
+ right: null,
1849
+ hyperplane: null,
1850
+ offset: 0,
1851
+ points: indices
1852
+ };
1853
+ }
1854
+ /** Flatten to preorder (convert_tree_format, rp_trees.py:3019-3049). */
1855
+ function flattenTree(root, nPoints, dim) {
1856
+ let nNodes = 0;
1857
+ let maxLeaf = 0;
1858
+ const count = (node) => {
1859
+ nNodes++;
1860
+ if (node.points) {
1861
+ if (node.points.length > maxLeaf) maxLeaf = node.points.length;
1862
+ } else {
1863
+ count(node.left);
1864
+ count(node.right);
1865
+ }
1866
+ };
1867
+ count(root);
1868
+ const hyperplanes = new Float32Array(nNodes * dim);
1869
+ const offsets = new Float32Array(nNodes);
1870
+ const children = new Int32Array(nNodes * 2).fill(-1);
1871
+ const indices = new Int32Array(nPoints).fill(-1);
1872
+ let nodeNum = 0;
1873
+ let leafStart = 0;
1874
+ const walk = (node) => {
1875
+ const my = nodeNum;
1876
+ if (node.points) {
1877
+ children[my * 2] = -leafStart;
1878
+ children[my * 2 + 1] = -(leafStart + node.points.length);
1879
+ indices.set(node.points, leafStart);
1880
+ leafStart += node.points.length;
1881
+ return;
1882
+ }
1883
+ hyperplanes.set(node.hyperplane, my * dim);
1884
+ offsets[my] = node.offset;
1885
+ nodeNum++;
1886
+ children[my * 2] = nodeNum;
1887
+ walk(node.left);
1888
+ nodeNum++;
1889
+ children[my * 2 + 1] = nodeNum;
1890
+ walk(node.right);
1891
+ };
1892
+ walk(root);
1893
+ return {
1894
+ hyperplanes,
1895
+ offsets,
1896
+ children,
1897
+ indices,
1898
+ leafSize: maxLeaf,
1899
+ nNodes,
1900
+ dim
1901
+ };
1902
+ }
1903
+ /** make_forest: per-tree tau states drawn from the master PRNG. */
1904
+ function makeForest(X, nNeighbors, nTrees, treeStates, { leafSize, angular = false, maxDepth = 200 } = {}) {
1905
+ const ls = leafSize ?? defaultLeafSize(nNeighbors);
1906
+ const all = new Int32Array(X.rows);
1907
+ for (let i = 0; i < X.rows; i++) all[i] = i;
1908
+ const forest = [];
1909
+ for (let t = 0; t < nTrees; t++) {
1910
+ const root = buildTree(X.data, X.cols, all.slice(), ls, maxDepth, angular, treeStates[t]);
1911
+ forest.push(flattenTree(root, X.rows, X.cols));
1912
+ }
1913
+ return forest;
1914
+ }
1915
+ /** rptree_leaf_array: rows = leaves, padded with -1 to the forest-wide max leaf size. */
1916
+ function leafArrayFromForest(forest) {
1917
+ if (forest.length === 0) return {
1918
+ data: new Int32Array([-1]),
1919
+ rows: 1,
1920
+ cols: 1
1921
+ };
1922
+ let maxLeafSize = 0;
1923
+ for (const t of forest) maxLeafSize = Math.max(maxLeafSize, t.leafSize);
1924
+ const rows = [];
1925
+ for (const t of forest) for (let node = 0; node < t.nNodes; node++) {
1926
+ const c0 = t.children[node * 2];
1927
+ if (c0 <= 0) {
1928
+ const start = -c0;
1929
+ const end = -t.children[node * 2 + 1];
1930
+ const row = new Int32Array(maxLeafSize).fill(-1);
1931
+ row.set(t.indices.subarray(start, end));
1932
+ rows.push(row);
1933
+ }
1934
+ }
1935
+ const out = new Int32Array(rows.length * maxLeafSize);
1936
+ rows.forEach((r, i) => {
1937
+ out.set(r, i * maxLeafSize);
1938
+ });
1939
+ return {
1940
+ data: out,
1941
+ rows: rows.length,
1942
+ cols: maxLeafSize
1943
+ };
1944
+ }
1945
+ /** search_flat_tree descent → [leafStart, leafEnd) into tree.indices. */
1946
+ function searchFlatTree(tree, point, rngState) {
1947
+ let node = 0;
1948
+ while (tree.children[node * 2] > 0) {
1949
+ let margin = tree.offsets[node];
1950
+ const base = node * tree.dim;
1951
+ for (let d = 0; d < tree.dim; d++) margin += tree.hyperplanes[base + d] * point[d];
1952
+ let side;
1953
+ if (Math.abs(margin) < RP_EPS) side = Math.abs(tauRandInt(rngState)) % 2;
1954
+ else side = margin > 0 ? 0 : 1;
1955
+ node = tree.children[node * 2 + side];
1956
+ }
1957
+ return [-tree.children[node * 2], -tree.children[node * 2 + 1]];
1958
+ }
1959
+ function hubSplit(data, dim, indices, hubA, hubB, angular) {
1960
+ const n = indices.length;
1961
+ const hyperplane = new Float32Array(dim);
1962
+ let offset = 0;
1963
+ if (angular) {
1964
+ let an = 0;
1965
+ let bn = 0;
1966
+ for (let d = 0; d < dim; d++) {
1967
+ an += data[hubA * dim + d] * data[hubA * dim + d];
1968
+ bn += data[hubB * dim + d] * data[hubB * dim + d];
1969
+ }
1970
+ an = Math.sqrt(an);
1971
+ bn = Math.sqrt(bn);
1972
+ if (Math.abs(an) < RP_EPS) an = 1;
1973
+ if (Math.abs(bn) < RP_EPS) bn = 1;
1974
+ let hn = 0;
1975
+ for (let d = 0; d < dim; d++) {
1976
+ const hv = data[hubA * dim + d] / an - data[hubB * dim + d] / bn;
1977
+ hyperplane[d] = hv;
1978
+ hn += hv * hv;
1979
+ }
1980
+ hn = Math.sqrt(hn);
1981
+ if (Math.abs(hn) < RP_EPS) hn = 1;
1982
+ for (let d = 0; d < dim; d++) hyperplane[d] = hyperplane[d] / hn;
1983
+ } else for (let d = 0; d < dim; d++) {
1984
+ const hv = data[hubA * dim + d] - data[hubB * dim + d];
1985
+ hyperplane[d] = hv;
1986
+ offset -= hv * (data[hubA * dim + d] + data[hubB * dim + d]) / 2;
1987
+ }
1988
+ const side = new Uint8Array(n);
1989
+ let nLeft = 0;
1990
+ let nRight = 0;
1991
+ for (let i = 0; i < n; i++) {
1992
+ let margin = offset;
1993
+ const p = indices[i];
1994
+ for (let d = 0; d < dim; d++) margin += hyperplane[d] * data[p * dim + d];
1995
+ let s;
1996
+ if (margin > RP_EPS) s = 0;
1997
+ else if (margin < -RP_EPS) s = 1;
1998
+ else s = i % 2;
1999
+ side[i] = s;
2000
+ if (s === 0) nLeft++;
2001
+ else nRight++;
2002
+ }
2003
+ return {
2004
+ hyperplane,
2005
+ offset,
2006
+ side,
2007
+ nLeft,
2008
+ nRight
2009
+ };
2010
+ }
2011
+ /** make_hub_tree (rp_trees.py:1232-1312) — top-3-degree hub pairs, best balance. */
2012
+ function makeHubTree(X, knnIndices, leafSize = 30, angular = false, maxDepth = 200) {
2013
+ const n = X.rows;
2014
+ const degrees = new Int32Array(n);
2015
+ for (let t = 0; t < knnIndices.length; t++) {
2016
+ const j = knnIndices[t];
2017
+ if (j >= 0) degrees[j]++;
2018
+ }
2019
+ const topHubs = (indices) => {
2020
+ const top = [];
2021
+ const topDeg = [];
2022
+ for (let i = 0; i < indices.length; i++) {
2023
+ const p = indices[i];
2024
+ const dg = degrees[p];
2025
+ let ins = -1;
2026
+ for (let s = 0; s < Math.min(3, top.length + 1); s++) if (s >= top.length || dg > topDeg[s]) {
2027
+ ins = s;
2028
+ break;
2029
+ }
2030
+ if (ins >= 0) {
2031
+ top.splice(ins, 0, p);
2032
+ topDeg.splice(ins, 0, dg);
2033
+ if (top.length > 3) {
2034
+ top.pop();
2035
+ topDeg.pop();
2036
+ }
2037
+ }
2038
+ }
2039
+ return top;
2040
+ };
2041
+ const build = (indices, depth) => {
2042
+ if (indices.length <= leafSize || depth <= 0) return {
2043
+ left: null,
2044
+ right: null,
2045
+ hyperplane: null,
2046
+ offset: 0,
2047
+ points: indices
2048
+ };
2049
+ const hubs = topHubs(indices);
2050
+ let best = null;
2051
+ let bestBalance = -1;
2052
+ for (let a = 0; a < hubs.length; a++) for (let b = a + 1; b < hubs.length; b++) {
2053
+ const res = hubSplit(X.data, X.cols, indices, hubs[a], hubs[b], angular);
2054
+ if (res.nLeft === 0 || res.nRight === 0) continue;
2055
+ const balance = Math.min(res.nLeft, res.nRight) / indices.length;
2056
+ if (balance > bestBalance) {
2057
+ bestBalance = balance;
2058
+ best = res;
2059
+ }
2060
+ }
2061
+ if (best === null || bestBalance < MIN_SPLIT_BALANCE) return {
2062
+ left: null,
2063
+ right: null,
2064
+ hyperplane: null,
2065
+ offset: 0,
2066
+ points: indices
2067
+ };
2068
+ const { left, right } = partition(indices, best.side, best.nLeft, best.nRight);
2069
+ return {
2070
+ left: build(left, depth - 1),
2071
+ right: build(right, depth - 1),
2072
+ hyperplane: best.hyperplane,
2073
+ offset: best.offset,
2074
+ points: null
2075
+ };
2076
+ };
2077
+ const all = new Int32Array(n);
2078
+ for (let i = 0; i < n; i++) all[i] = i;
2079
+ return flattenTree(build(all, maxDepth), n, X.cols);
2080
+ }
2081
+
2082
+ //#endregion
2083
+ //#region src/knn/search.ts
2084
+ /** Forward diversify (pynndescent_.py:369-403) on sorted rows; prob = 1.0. */
2085
+ function diversifyRows(X, dist, knnIndices, knnDistances, k) {
2086
+ const n = X.rows;
2087
+ const d = X.cols;
2088
+ for (let i = 0; i < n; i++) {
2089
+ const base = i * k;
2090
+ let retained = 1;
2091
+ for (let j = 1; j < k; j++) {
2092
+ const idxJ = knnIndices[base + j];
2093
+ if (idxJ < 0) break;
2094
+ let prune = false;
2095
+ for (let c = 0; c < retained; c++) {
2096
+ const idxC = knnIndices[base + c];
2097
+ if (knnDistances[base + c] > FLOAT32_EPS) {
2098
+ if (dist(X.data.subarray(idxJ * d, (idxJ + 1) * d), X.data.subarray(idxC * d, (idxC + 1) * d)) < knnDistances[base + j]) {
2099
+ prune = true;
2100
+ break;
2101
+ }
2102
+ }
2103
+ }
2104
+ if (!prune) {
2105
+ knnIndices[base + retained] = idxJ;
2106
+ knnDistances[base + retained] = knnDistances[base + j];
2107
+ retained++;
2108
+ }
2109
+ }
2110
+ for (let j = retained; j < k; j++) {
2111
+ knnIndices[base + j] = -1;
2112
+ knnDistances[base + j] = Infinity;
2113
+ }
2114
+ }
2115
+ }
2116
+ function csrTransposeF32(m) {
2117
+ const nnz = m.indptr[m.n];
2118
+ const counts = new Int32Array(m.n + 1);
2119
+ for (let t = 0; t < nnz; t++) counts[m.indices[t] + 1]++;
2120
+ for (let c = 0; c < m.n; c++) counts[c + 1] += counts[c];
2121
+ const indptr = counts.slice();
2122
+ const indices = new Int32Array(nnz);
2123
+ const data = new Float32Array(nnz);
2124
+ const cursor = counts.slice(0, m.n);
2125
+ for (let r = 0; r < m.n; r++) for (let p = m.indptr[r]; p < m.indptr[r + 1]; p++) {
2126
+ const c = m.indices[p];
2127
+ const dst = cursor[c]++;
2128
+ indices[dst] = r;
2129
+ data[dst] = m.data[p];
2130
+ }
2131
+ return {
2132
+ indptr,
2133
+ indices,
2134
+ data,
2135
+ n: m.n
2136
+ };
2137
+ }
2138
+ /** Reverse diversify (diversify_csr): per row in ascending-distance order. */
2139
+ function diversifyCsr(g, X, dist) {
2140
+ const d = X.cols;
2141
+ for (let r = 0; r < g.n; r++) {
2142
+ const start = g.indptr[r];
2143
+ const len = g.indptr[r + 1] - start;
2144
+ if (len <= 1) continue;
2145
+ const order = Array.from({ length: len }, (_, i) => i).sort((a, b) => g.data[start + a] - g.data[start + b]);
2146
+ const retained = [];
2147
+ for (const oi of order) {
2148
+ const p = start + oi;
2149
+ const colJ = g.indices[p];
2150
+ const dj = g.data[p];
2151
+ let prune = false;
2152
+ for (const rp of retained) if (g.data[rp] > FLOAT32_EPS) {
2153
+ if (dist(X.data.subarray(colJ * d, (colJ + 1) * d), X.data.subarray(g.indices[rp] * d, (g.indices[rp] + 1) * d)) < dj) {
2154
+ prune = true;
2155
+ break;
2156
+ }
2157
+ }
2158
+ if (prune) g.data[p] = 0;
2159
+ else retained.push(p);
2160
+ }
2161
+ }
2162
+ }
2163
+ function eliminateZerosF32(g) {
2164
+ const indptr = new Int32Array(g.n + 1);
2165
+ let nnz = 0;
2166
+ for (let t = 0; t < g.indptr[g.n]; t++) if (g.data[t] !== 0) nnz++;
2167
+ const indices = new Int32Array(nnz);
2168
+ const data = new Float32Array(nnz);
2169
+ let w = 0;
2170
+ for (let r = 0; r < g.n; r++) {
2171
+ for (let p = g.indptr[r]; p < g.indptr[r + 1]; p++) if (g.data[p] !== 0) {
2172
+ indices[w] = g.indices[p];
2173
+ data[w] = g.data[p];
2174
+ w++;
2175
+ }
2176
+ indptr[r + 1] = w;
2177
+ }
2178
+ return {
2179
+ indptr,
2180
+ indices,
2181
+ data,
2182
+ n: g.n
2183
+ };
2184
+ }
2185
+ /** Elementwise maximum over union sparsity (rows sorted). */
2186
+ function csrMaximumF32(A, B) {
2187
+ const n = A.n;
2188
+ const indptr = new Int32Array(n + 1);
2189
+ const cap = A.indptr[n] + B.indptr[n];
2190
+ const indices = new Int32Array(cap);
2191
+ const data = new Float32Array(cap);
2192
+ let nnz = 0;
2193
+ for (let r = 0; r < n; r++) {
2194
+ let pa = A.indptr[r];
2195
+ let pb = B.indptr[r];
2196
+ const ea = A.indptr[r + 1];
2197
+ const eb = B.indptr[r + 1];
2198
+ while (pa < ea || pb < eb) {
2199
+ const ca = pa < ea ? A.indices[pa] : Infinity;
2200
+ const cb = pb < eb ? B.indices[pb] : Infinity;
2201
+ let col;
2202
+ let v;
2203
+ if (ca < cb) {
2204
+ col = ca;
2205
+ v = A.data[pa];
2206
+ pa++;
2207
+ } else if (cb < ca) {
2208
+ col = cb;
2209
+ v = B.data[pb];
2210
+ pb++;
2211
+ } else {
2212
+ col = ca;
2213
+ v = Math.max(A.data[pa], B.data[pb]);
2214
+ pa++;
2215
+ pb++;
2216
+ }
2217
+ if (col !== r) {
2218
+ indices[nnz] = col;
2219
+ data[nnz] = v;
2220
+ nnz++;
2221
+ }
2222
+ }
2223
+ indptr[r + 1] = nnz;
2224
+ }
2225
+ return {
2226
+ indptr,
2227
+ indices: indices.slice(0, nnz),
2228
+ data: data.slice(0, nnz),
2229
+ n
2230
+ };
2231
+ }
2232
+ /** degree_prune: rows longer than maxDegree cut at the (maxDegree+1)-th smallest. */
2233
+ function degreePrune(g, maxDegree) {
2234
+ for (let r = 0; r < g.n; r++) {
2235
+ const start = g.indptr[r];
2236
+ const end = g.indptr[r + 1];
2237
+ if (end - start > maxDegree) {
2238
+ const cut = Array.from(g.data.subarray(start, end)).sort((a, b) => a - b)[maxDegree];
2239
+ for (let p = start; p < end; p++) if (g.data[p] > cut) g.data[p] = 0;
2240
+ }
2241
+ }
2242
+ return eliminateZerosF32(g);
2243
+ }
2244
+ /**
2245
+ * Build the prepared search index from the finished kNN graph (notes §6).
2246
+ * `knn` must be the deheap-sorted neighbor graph in INTERNAL distance space.
2247
+ */
2248
+ function prepareSearchIndex(X, knn, dist, correction, angular, nNeighbors, searchRngState, { pruningDegreeMultiplier = 1.5, searchLeafSize = 30, maxDepth = 200 } = {}) {
2249
+ const n = X.rows;
2250
+ const k = knn.k;
2251
+ const tree = makeHubTree(X, knn.indices, searchLeafSize, angular, maxDepth);
2252
+ const divIdx = knn.indices.slice();
2253
+ const divDst = knn.distances.slice();
2254
+ diversifyRows(X, dist, divIdx, divDst, k);
2255
+ let nnz = 0;
2256
+ for (let t = 0; t < divIdx.length; t++) if (divIdx[t] >= 0) nnz++;
2257
+ const indptr = new Int32Array(n + 1);
2258
+ const indices = new Int32Array(nnz);
2259
+ const data = new Float32Array(nnz);
2260
+ let w = 0;
2261
+ for (let i = 0; i < n; i++) {
2262
+ const cols = [];
2263
+ const vals = [];
2264
+ for (let j = 0; j < k; j++) {
2265
+ const idx = divIdx[i * k + j];
2266
+ if (idx < 0) continue;
2267
+ let v = divDst[i * k + j];
2268
+ if (v === 0) v = FLOAT32_EPS;
2269
+ cols.push(idx);
2270
+ vals.push(v);
2271
+ }
2272
+ const ord = cols.map((_, x) => x).sort((a, b) => cols[a] - cols[b]);
2273
+ for (const o of ord) {
2274
+ indices[w] = cols[o];
2275
+ data[w] = Math.fround(vals[o]);
2276
+ w++;
2277
+ }
2278
+ indptr[i + 1] = w;
2279
+ }
2280
+ let graph = {
2281
+ indptr,
2282
+ indices,
2283
+ data,
2284
+ n
2285
+ };
2286
+ let minDistance = Infinity;
2287
+ for (let t = 0; t < w; t++) if (data[t] < minDistance) minDistance = data[t];
2288
+ const reverse = csrTransposeF32(graph);
2289
+ diversifyCsr(reverse, X, dist);
2290
+ const reverseClean = eliminateZerosF32(reverse);
2291
+ graph = csrMaximumF32(graph, reverseClean);
2292
+ graph = degreePrune(graph, Math.round(pruningDegreeMultiplier * nNeighbors));
2293
+ const vertexOrder = tree.indices.slice();
2294
+ const inverse = new Int32Array(n);
2295
+ for (let i = 0; i < n; i++) inverse[vertexOrder[i]] = i;
2296
+ const rdata = new Float32Array(X.data.length);
2297
+ const dcols = X.cols;
2298
+ for (let i = 0; i < n; i++) rdata.set(X.data.subarray(vertexOrder[i] * dcols, (vertexOrder[i] + 1) * dcols), i * dcols);
2299
+ const rX = {
2300
+ data: rdata,
2301
+ rows: n,
2302
+ cols: dcols
2303
+ };
2304
+ const rIndptr = new Int32Array(n + 1);
2305
+ for (let i = 0; i < n; i++) {
2306
+ const src = vertexOrder[i];
2307
+ rIndptr[i + 1] = rIndptr[i] + (graph.indptr[src + 1] - graph.indptr[src]);
2308
+ }
2309
+ const rIndices = new Int32Array(rIndptr[n]);
2310
+ for (let i = 0; i < n; i++) {
2311
+ const src = vertexOrder[i];
2312
+ let dst = rIndptr[i];
2313
+ for (let p = graph.indptr[src]; p < graph.indptr[src + 1]; p++) rIndices[dst++] = inverse[graph.indices[p]];
2314
+ }
2315
+ const rTreeIndices = new Int32Array(n);
2316
+ for (let i = 0; i < n; i++) rTreeIndices[i] = inverse[tree.indices[i]];
2317
+ return {
2318
+ indptr: rIndptr,
2319
+ indices: rIndices,
2320
+ tree: {
2321
+ ...tree,
2322
+ indices: rTreeIndices
2323
+ },
2324
+ vertexOrder,
2325
+ data: rX,
2326
+ dist,
2327
+ correction,
2328
+ angular,
2329
+ minDistance,
2330
+ searchRngState,
2331
+ nNeighbors
2332
+ };
2333
+ }
2334
+ /** Binary min-heap of (d, idx) pairs for the seed set (ties break on push order). */
2335
+ var SeedHeap = class {
2336
+ d = [];
2337
+ v = [];
2338
+ get size() {
2339
+ return this.d.length;
2340
+ }
2341
+ push(dist, idx) {
2342
+ const d = this.d;
2343
+ const v = this.v;
2344
+ d.push(dist);
2345
+ v.push(idx);
2346
+ let i = d.length - 1;
2347
+ while (i > 0) {
2348
+ const parent = i - 1 >> 1;
2349
+ if (d[i] < d[parent] || d[i] === d[parent] && v[i] < v[parent]) {
2350
+ [d[i], d[parent]] = [d[parent], d[i]];
2351
+ [v[i], v[parent]] = [v[parent], v[i]];
2352
+ i = parent;
2353
+ } else break;
2354
+ }
2355
+ }
2356
+ pop() {
2357
+ const d = this.d;
2358
+ const v = this.v;
2359
+ const outD = d[0];
2360
+ const outV = v[0];
2361
+ const lastD = d.pop();
2362
+ const lastV = v.pop();
2363
+ if (d.length > 0) {
2364
+ d[0] = lastD;
2365
+ v[0] = lastV;
2366
+ let i = 0;
2367
+ for (;;) {
2368
+ const l = 2 * i + 1;
2369
+ const r = l + 1;
2370
+ let m = i;
2371
+ if (l < d.length && (d[l] < d[m] || d[l] === d[m] && v[l] < v[m])) m = l;
2372
+ if (r < d.length && (d[r] < d[m] || d[r] === d[m] && v[r] < v[m])) m = r;
2373
+ if (m === i) break;
2374
+ [d[i], d[m]] = [d[m], d[i]];
2375
+ [v[i], v[m]] = [v[m], v[i]];
2376
+ i = m;
2377
+ }
2378
+ }
2379
+ return [outD, outV];
2380
+ }
2381
+ };
2382
+ /**
2383
+ * Dense query (notes §7): tree init + random top-up + best-first expansion with
2384
+ * bound d_k + ε(d_k − minDistance). Returns ORIGINAL indices and CORRECTED distances.
2385
+ */
2386
+ function searchKnn(index, Q, k, epsilon) {
2387
+ const n = index.data.rows;
2388
+ const dim = index.data.cols;
2389
+ const nq = Q.rows;
2390
+ const outIdx = new Int32Array(nq * k).fill(-1);
2391
+ const outDst = new Float32Array(nq * k).fill(Infinity);
2392
+ const visited = new Uint8Array((n >> 3) + 1);
2393
+ const pri = new Float32Array(k);
2394
+ const idx = new Int32Array(k);
2395
+ const qbuf = new Float32Array(dim);
2396
+ for (let qi = 0; qi < nq; qi++) {
2397
+ visited.fill(0);
2398
+ pri.fill(Infinity);
2399
+ idx.fill(-1);
2400
+ let query = Q.data.subarray(qi * dim, (qi + 1) * dim);
2401
+ if (index.angular) {
2402
+ let norm$1 = 0;
2403
+ for (let d = 0; d < dim; d++) norm$1 += query[d] * query[d];
2404
+ norm$1 = Math.sqrt(norm$1);
2405
+ if (norm$1 > 0) {
2406
+ for (let d = 0; d < dim; d++) qbuf[d] = query[d] / norm$1;
2407
+ query = qbuf;
2408
+ }
2409
+ }
2410
+ const rng = Int32Array.from(index.searchRngState);
2411
+ const seeds = new SeedHeap();
2412
+ const [ls, le] = searchFlatTree(index.tree, query, rng);
2413
+ for (let p = ls; p < le; p++) {
2414
+ const cand = index.tree.indices[p];
2415
+ const d = index.dist(query, index.data.data.subarray(cand * dim, (cand + 1) * dim));
2416
+ simpleHeapPush(pri, idx, k, d, cand);
2417
+ seeds.push(d, cand);
2418
+ visited[cand >> 3] = visited[cand >> 3] | 1 << (cand & 7);
2419
+ }
2420
+ const nRandom = Math.min(k, index.nNeighbors) - (le - ls);
2421
+ for (let j = 0; j < nRandom; j++) {
2422
+ const cand = Math.abs(tauRandInt(rng)) % n;
2423
+ if ((visited[cand >> 3] & 1 << (cand & 7)) === 0) {
2424
+ visited[cand >> 3] = visited[cand >> 3] | 1 << (cand & 7);
2425
+ const d = index.dist(query, index.data.data.subarray(cand * dim, (cand + 1) * dim));
2426
+ simpleHeapPush(pri, idx, k, d, cand);
2427
+ seeds.push(d, cand);
2428
+ }
2429
+ }
2430
+ let bound = pri[0] + epsilon * (pri[0] - index.minDistance);
2431
+ while (seeds.size > 0) {
2432
+ const [dv, vertex] = seeds.pop();
2433
+ if (dv >= bound) break;
2434
+ for (let p = index.indptr[vertex]; p < index.indptr[vertex + 1]; p++) {
2435
+ const cand = index.indices[p];
2436
+ if ((visited[cand >> 3] & 1 << (cand & 7)) !== 0) continue;
2437
+ visited[cand >> 3] = visited[cand >> 3] | 1 << (cand & 7);
2438
+ const d = index.dist(query, index.data.data.subarray(cand * dim, (cand + 1) * dim));
2439
+ if (d < bound) {
2440
+ simpleHeapPush(pri, idx, k, d, cand);
2441
+ seeds.push(d, cand);
2442
+ bound = pri[0] + epsilon * (pri[0] - index.minDistance);
2443
+ }
2444
+ }
2445
+ }
2446
+ const rowIdx = idx.slice();
2447
+ const rowPri = pri.slice();
2448
+ const ord = Array.from({ length: k }, (_, x) => x).sort((a, b) => rowPri[a] - rowPri[b]);
2449
+ for (let j = 0; j < k; j++) {
2450
+ const o = ord[j];
2451
+ if (rowIdx[o] >= 0) {
2452
+ outIdx[qi * k + j] = index.vertexOrder[rowIdx[o]];
2453
+ outDst[qi * k + j] = index.correction(rowPri[o]);
2454
+ }
2455
+ }
2456
+ }
2457
+ return {
2458
+ indices: outIdx,
2459
+ distances: outDst,
2460
+ k
2461
+ };
2462
+ }
2463
+
2464
+ //#endregion
2465
+ //#region src/knn/sparse-forest.ts
2466
+ function sparseRow(X, i) {
2467
+ return {
2468
+ inds: X.indices.subarray(X.indptr[i], X.indptr[i + 1]),
2469
+ data: X.data.subarray(X.indptr[i], X.indptr[i + 1])
2470
+ };
2471
+ }
2472
+ /** merged a*sa + b*sb over union of sparse supports */
2473
+ function sparseCombine(aInds, aData, sa, bInds, bData, sb) {
2474
+ const out = [];
2475
+ const outI = [];
2476
+ let i = 0;
2477
+ let j = 0;
2478
+ while (i < aInds.length && j < bInds.length) {
2479
+ const ca = aInds[i];
2480
+ const cb = bInds[j];
2481
+ if (ca === cb) {
2482
+ const v = sa * aData[i] + sb * bData[j];
2483
+ if (v !== 0) {
2484
+ outI.push(ca);
2485
+ out.push(v);
2486
+ }
2487
+ i++;
2488
+ j++;
2489
+ } else if (ca < cb) {
2490
+ const v = sa * aData[i];
2491
+ if (v !== 0) {
2492
+ outI.push(ca);
2493
+ out.push(v);
2494
+ }
2495
+ i++;
2496
+ } else {
2497
+ const v = sb * bData[j];
2498
+ if (v !== 0) {
2499
+ outI.push(cb);
2500
+ out.push(v);
2501
+ }
2502
+ j++;
2503
+ }
2504
+ }
2505
+ for (; i < aInds.length; i++) {
2506
+ const v = sa * aData[i];
2507
+ if (v !== 0) {
2508
+ outI.push(aInds[i]);
2509
+ out.push(v);
2510
+ }
2511
+ }
2512
+ for (; j < bInds.length; j++) {
2513
+ const v = sb * bData[j];
2514
+ if (v !== 0) {
2515
+ outI.push(bInds[j]);
2516
+ out.push(v);
2517
+ }
2518
+ }
2519
+ return {
2520
+ inds: Int32Array.from(outI),
2521
+ data: Float64Array.from(out)
2522
+ };
2523
+ }
2524
+ function sparseDot(aInds, aData, bInds, bData) {
2525
+ let r = 0;
2526
+ let i = 0;
2527
+ let j = 0;
2528
+ while (i < aInds.length && j < bInds.length) {
2529
+ const ca = aInds[i];
2530
+ const cb = bInds[j];
2531
+ if (ca === cb) {
2532
+ r += aData[i] * bData[j];
2533
+ i++;
2534
+ j++;
2535
+ } else if (ca < cb) i++;
2536
+ else j++;
2537
+ }
2538
+ return r;
2539
+ }
2540
+ function norm(v) {
2541
+ let s = 0;
2542
+ for (let i = 0; i < v.length; i++) s += v[i] * v[i];
2543
+ return Math.sqrt(s);
2544
+ }
2545
+ function sparseSplit(X, indices, rngState, angular) {
2546
+ const n = indices.length;
2547
+ const leftIndex = floorMod(Math.abs(tauRandInt(rngState)), n);
2548
+ let rightIndex = floorMod(Math.abs(tauRandInt(rngState)), n);
2549
+ if (leftIndex === rightIndex) rightIndex += 1;
2550
+ rightIndex = floorMod(rightIndex, n);
2551
+ const l = indices[leftIndex];
2552
+ const r = indices[rightIndex];
2553
+ const rowL = sparseRow(X, l);
2554
+ const rowR = sparseRow(X, r);
2555
+ let hyperplane;
2556
+ let offset = 0;
2557
+ if (angular) {
2558
+ let ln = norm(rowL.data);
2559
+ let rn = norm(rowR.data);
2560
+ if (Math.abs(ln) < RP_EPS) ln = 1;
2561
+ if (Math.abs(rn) < RP_EPS) rn = 1;
2562
+ hyperplane = sparseCombine(rowL.inds, rowL.data, 1 / ln, rowR.inds, rowR.data, -1 / rn);
2563
+ let hn = norm(hyperplane.data);
2564
+ if (Math.abs(hn) < RP_EPS) hn = 1;
2565
+ for (let t = 0; t < hyperplane.data.length; t++) hyperplane.data[t] = hyperplane.data[t] / hn;
2566
+ } else {
2567
+ hyperplane = sparseCombine(rowL.inds, rowL.data, 1, rowR.inds, rowR.data, -1);
2568
+ const mid = sparseCombine(rowL.inds, rowL.data, .5, rowR.inds, rowR.data, .5);
2569
+ offset = -sparseDot(hyperplane.inds, hyperplane.data, mid.inds, mid.data);
2570
+ }
2571
+ const side = new Uint8Array(n);
2572
+ let nLeft = 0;
2573
+ let nRight = 0;
2574
+ for (let i = 0; i < n; i++) {
2575
+ const p = indices[i];
2576
+ const row = sparseRow(X, p);
2577
+ const margin = offset + sparseDot(hyperplane.inds, hyperplane.data, row.inds, row.data);
2578
+ if (Math.abs(margin) < RP_EPS) side[i] = floorMod(tauRandInt(rngState), 2);
2579
+ else if (margin > 0) side[i] = 0;
2580
+ else side[i] = 1;
2581
+ if (side[i] === 0) nLeft++;
2582
+ else nRight++;
2583
+ }
2584
+ if (nLeft === 0 || nRight === 0) {
2585
+ nLeft = 0;
2586
+ nRight = 0;
2587
+ for (let i = 0; i < n; i++) {
2588
+ side[i] = floorMod(tauRandInt(rngState), 2);
2589
+ if (side[i] === 0) nLeft++;
2590
+ else nRight++;
2591
+ }
2592
+ }
2593
+ const left = new Int32Array(nLeft);
2594
+ const right = new Int32Array(nRight);
2595
+ let il = 0;
2596
+ let ir = 0;
2597
+ for (let i = 0; i < n; i++) if (side[i] === 0) left[il++] = indices[i];
2598
+ else right[ir++] = indices[i];
2599
+ return {
2600
+ left,
2601
+ right
2602
+ };
2603
+ }
2604
+ /**
2605
+ * Build only the leaf arrays of a sparse RP forest (descent init needs nothing else).
2606
+ */
2607
+ function sparseForestLeafArray(X, nNeighbors, nTrees, treeStates, angular, maxDepth = 200) {
2608
+ const leafSize = Math.max(60, Math.min(256, 5 * nNeighbors));
2609
+ const leaves = [];
2610
+ let maxLeaf = 0;
2611
+ const build = (indices, depth, rngState) => {
2612
+ if (indices.length > leafSize && depth > 0) {
2613
+ const { left, right } = sparseSplit(X, indices, rngState, angular);
2614
+ build(left, depth - 1, rngState);
2615
+ build(right, depth - 1, rngState);
2616
+ } else {
2617
+ leaves.push(indices);
2618
+ if (indices.length > maxLeaf) maxLeaf = indices.length;
2619
+ }
2620
+ };
2621
+ const all = new Int32Array(X.rows);
2622
+ for (let i = 0; i < X.rows; i++) all[i] = i;
2623
+ for (let t = 0; t < nTrees; t++) build(all.slice(), maxDepth, treeStates[t]);
2624
+ const out = new Int32Array(leaves.length * maxLeaf).fill(-1);
2625
+ leaves.forEach((leaf, i) => {
2626
+ out.set(leaf, i * maxLeaf);
2627
+ });
2628
+ return {
2629
+ data: out,
2630
+ rows: leaves.length,
2631
+ cols: maxLeaf
2632
+ };
2633
+ }
2634
+
2635
+ //#endregion
2636
+ //#region src/knn/index.ts
2637
+ const SMALL_DATA_CUTOFF = 4096;
2638
+ function nearestNeighborsDense(X, nNeighbors, metricName, rng, { metricOptions = {}, forceApproximation = false, angularRpForest = false, disconnectionDistance = Infinity } = {}) {
2639
+ if (X.rows < SMALL_DATA_CUTOFF && !forceApproximation) return {
2640
+ knn: exactKnnDense(X, nNeighbors, metricName, {
2641
+ metricOptions,
2642
+ disconnectionDistance
2643
+ }),
2644
+ searchIndex: null,
2645
+ method: "exact"
2646
+ };
2647
+ const internal = resolveInternalMetric(metricName, metricOptions);
2648
+ const angular = internal.angularTrees || angularRpForest;
2649
+ const { nTrees, nIters } = umapNnDescentParams(X.rows);
2650
+ const rngState = rng.tauState();
2651
+ const searchRngState = rng.tauState();
2652
+ for (let i = 0; i < 10; i++) tauRandInt(searchRngState);
2653
+ const treeStates = [];
2654
+ for (let t = 0; t < nTrees; t++) treeStates.push(rng.tauState());
2655
+ const leafArray = leafArrayFromForest(makeForest(X, nNeighbors, nTrees, treeStates, {
2656
+ leafSize: defaultLeafSize(nNeighbors),
2657
+ angular
2658
+ }));
2659
+ const dim = X.cols;
2660
+ const data = X.data;
2661
+ const dist = (i, j) => internal.dist(data.subarray(i * dim, (i + 1) * dim), data.subarray(j * dim, (j + 1) * dim));
2662
+ const heap = nnDescent(X.rows, nNeighbors, dist, rngState, leafArray, {
2663
+ maxCandidates: 60,
2664
+ nIters,
2665
+ delta: .001
2666
+ });
2667
+ const knnInternal = {
2668
+ indices: heap.indices,
2669
+ distances: heap.distances,
2670
+ k: nNeighbors
2671
+ };
2672
+ const pairDist = (a, b) => internal.dist(a, b);
2673
+ const searchIndex = prepareSearchIndex(X, knnInternal, pairDist, internal.correction, angular, nNeighbors, searchRngState);
2674
+ const outDst = new Float32Array(heap.distances.length);
2675
+ for (let t = 0; t < outDst.length; t++) {
2676
+ const d = heap.distances[t];
2677
+ outDst[t] = d === Infinity ? Infinity : internal.correction(d);
2678
+ }
2679
+ const knn = {
2680
+ indices: heap.indices.slice(),
2681
+ distances: outDst,
2682
+ k: nNeighbors
2683
+ };
2684
+ applyDisconnection(knn, disconnectionDistance);
2685
+ return {
2686
+ knn,
2687
+ searchIndex,
2688
+ method: "nndescent"
2689
+ };
2690
+ }
2691
+ function nearestNeighborsSparse(X, nNeighbors, metricName, rng, { forceApproximation = false, angularRpForest = false, disconnectionDistance = Infinity } = {}) {
2692
+ if (X.rows < SMALL_DATA_CUTOFF && !forceApproximation) {
2693
+ const knn$1 = exactKnnSparse(X, nNeighbors, metricName);
2694
+ applyDisconnection(knn$1, disconnectionDistance);
2695
+ return {
2696
+ knn: knn$1,
2697
+ searchIndex: null,
2698
+ method: "exact"
2699
+ };
2700
+ }
2701
+ const internal = resolveSparseInternalMetric(metricName);
2702
+ const angular = internal.angularTrees || angularRpForest;
2703
+ const { nTrees, nIters } = umapNnDescentParams(X.rows);
2704
+ const rngState = rng.tauState();
2705
+ const searchRngState = rng.tauState();
2706
+ for (let i = 0; i < 10; i++) tauRandInt(searchRngState);
2707
+ const treeStates = [];
2708
+ for (let t = 0; t < nTrees; t++) treeStates.push(rng.tauState());
2709
+ const leafArray = sparseForestLeafArray(X, nNeighbors, nTrees, treeStates, angular);
2710
+ const rowInd = (i) => X.indices.subarray(X.indptr[i], X.indptr[i + 1]);
2711
+ const rowDat = (i) => X.data.subarray(X.indptr[i], X.indptr[i + 1]);
2712
+ const dist = (i, j) => internal.dist(rowInd(i), rowDat(i), rowInd(j), rowDat(j));
2713
+ const heap = nnDescent(X.rows, nNeighbors, dist, rngState, leafArray, {
2714
+ maxCandidates: 60,
2715
+ nIters,
2716
+ delta: .001,
2717
+ leafAcceptance: "either"
2718
+ });
2719
+ const outDst = new Float32Array(heap.distances.length);
2720
+ for (let t = 0; t < outDst.length; t++) {
2721
+ const d = heap.distances[t];
2722
+ outDst[t] = d === Infinity ? Infinity : internal.correction(d);
2723
+ }
2724
+ const knn = {
2725
+ indices: heap.indices,
2726
+ distances: outDst,
2727
+ k: nNeighbors
2728
+ };
2729
+ applyDisconnection(knn, disconnectionDistance);
2730
+ return {
2731
+ knn,
2732
+ searchIndex: null,
2733
+ method: "nndescent"
2734
+ };
2735
+ }
2736
+ function applyDisconnection(knn, dc) {
2737
+ if (dc === Infinity) return;
2738
+ for (let t = 0; t < knn.distances.length; t++) if (knn.distances[t] >= dc) {
2739
+ knn.distances[t] = Infinity;
2740
+ knn.indices[t] = -1;
2741
+ }
2742
+ }
2743
+
2744
+ //#endregion
2745
+ //#region src/util.ts
2746
+ /**
2747
+ * Bound an async backend acquisition/init: broken environments (a stalled GPU
2748
+ * service, a worker that never loads) must degrade or fail typed — never hang.
2749
+ */
2750
+ async function raceTimeout(p, ms, what) {
2751
+ let timer;
2752
+ const timeout = new Promise((_, reject) => {
2753
+ timer = setTimeout(() => reject(new UmapBackendError(`${what} timed out after ${ms}ms`)), ms);
2754
+ });
2755
+ try {
2756
+ return await Promise.race([p, timeout]);
2757
+ } finally {
2758
+ clearTimeout(timer);
2759
+ }
2760
+ }
2761
+ /** Convert any DenseInput to a canonical float32 Matrix, validating finiteness (§4.1). */
2762
+ function toMatrix(input, what = "X") {
2763
+ if (Array.isArray(input)) {
2764
+ const rows$1 = input.length;
2765
+ if (rows$1 === 0) throw new UmapValidationError(`${what} has no rows`);
2766
+ const first = input[0];
2767
+ if (!Array.isArray(first)) throw new UmapValidationError(`${what} must be number[][]`);
2768
+ const cols$1 = first.length;
2769
+ if (cols$1 === 0) throw new UmapValidationError(`${what} has no columns`);
2770
+ const data$1 = new Float32Array(rows$1 * cols$1);
2771
+ for (let r = 0; r < rows$1; r++) {
2772
+ const row = input[r];
2773
+ if (!Array.isArray(row) || row.length !== cols$1) throw new UmapValidationError(`${what} row ${r} has inconsistent length`);
2774
+ for (let c = 0; c < cols$1; c++) {
2775
+ const v = row[c];
2776
+ if (typeof v !== "number" || !Number.isFinite(v)) throw new UmapValidationError(`${what} contains a non-finite value at (${r}, ${c}); NaN/Infinity are rejected`);
2777
+ data$1[r * cols$1 + c] = v;
2778
+ }
2779
+ }
2780
+ return {
2781
+ data: data$1,
2782
+ rows: rows$1,
2783
+ cols: cols$1
2784
+ };
2785
+ }
2786
+ const { data, rows, cols } = input;
2787
+ if (data.length !== rows * cols) throw new UmapValidationError(`${what}: data length ${data.length} != rows*cols = ${rows * cols}`);
2788
+ for (let i = 0; i < data.length; i++) if (!Number.isFinite(data[i])) throw new UmapValidationError(`${what} contains a non-finite value at flat index ${i}; NaN/Infinity are rejected`);
2789
+ return {
2790
+ data: data instanceof Float32Array ? data.slice() : Float32Array.from(data),
2791
+ rows,
2792
+ cols
2793
+ };
2794
+ }
2795
+ function isCsrInput(x) {
2796
+ return typeof x === "object" && x !== null && "indptr" in x && "indices" in x && "data" in x && "rows" in x && "cols" in x;
2797
+ }
2798
+ function validateCsr(input, what = "X") {
2799
+ const { data, indices, indptr, rows, cols } = input;
2800
+ if (indptr.length !== rows + 1) throw new UmapValidationError(`${what}: indptr length must be rows+1`);
2801
+ if (indptr[0] !== 0 || indptr[rows] !== data.length || indices.length !== data.length) throw new UmapValidationError(`${what}: malformed CSR structure`);
2802
+ for (let i = 0; i < data.length; i++) {
2803
+ if (!Number.isFinite(data[i])) throw new UmapValidationError(`${what} contains a non-finite value; NaN/Infinity rejected`);
2804
+ const c = indices[i];
2805
+ if (c < 0 || c >= cols) throw new UmapValidationError(`${what}: column index out of range`);
2806
+ }
2807
+ for (let r = 0; r < rows; r++) {
2808
+ if (indptr[r + 1] < indptr[r]) throw new UmapValidationError(`${what}: indptr must be non-decreasing`);
2809
+ for (let p = indptr[r] + 1; p < indptr[r + 1]; p++) if (indices[p] <= indices[p - 1]) throw new UmapValidationError(`${what}: row ${r} has unsorted or duplicate column indices (CSR rows must be strictly increasing)`);
2810
+ }
2811
+ return input;
2812
+ }
2813
+
2814
+ //#endregion
2815
+ export { validateMetricOptionSizes as A, csrCopy as B, resolveInternalMetric as C, isNamedMetric as D, canonicalMetricName as E, smoothKnnDist as F, csrMaxAbsDiff as G, csrForEach as H, defaultNEpochs as I, csrTranspose as J, csrNnz as K, makeEpochsPerSample as L, fuzzySimplicialSet as M, MIN_K_DIST_SCALE as N, metrics_exports as O, SMOOTH_K_TOLERANCE as P, pruneGraphForEpochs as R, knnFromPrecomputed as S, NAMED_METRICS as T, csrFromCoo as U, csrEliminateZeros as V, csrMapValues as W, findABParams as Y, floorMod as _, SMALL_DATA_CUTOFF as a, exactKnnDense as b, prepareSearchIndex as c, umapNnDescentParams as d, checkedFlaggedHeapPush as f, Pcg32 as g, makeHeap as h, validateCsr as i, computeMembershipStrengths as j, resolveMetric as k, searchKnn as l, deheapSort as m, raceTimeout as n, nearestNeighborsDense as o, checkedHeapPush as p, csrRowNormalizeMax as q, toMatrix as r, nearestNeighborsSparse as s, isCsrInput as t, nnDescent as u, tauRand as v, ANGULAR_METRICS as w, exactKnnSparse as x, tauRandInt as y, csrBinaryOp as z };
2816
+ //# sourceMappingURL=core-BPpH5_PW.js.map