@nshiab/simple-data-analysis-core 2.0.8 → 2.0.9

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.
Files changed (75) hide show
  1. package/esm/class/SimpleTable.d.ts +72 -0
  2. package/esm/class/SimpleTable.d.ts.map +1 -1
  3. package/esm/class/SimpleTable.js +68 -0
  4. package/esm/helpers/appendColumnBatches.d.ts +3 -0
  5. package/esm/helpers/appendColumnBatches.d.ts.map +1 -0
  6. package/esm/helpers/appendColumnBatches.js +21 -0
  7. package/esm/helpers/buildUmapGraph.d.ts +24 -0
  8. package/esm/helpers/buildUmapGraph.d.ts.map +1 -0
  9. package/esm/helpers/buildUmapGraph.js +155 -0
  10. package/esm/helpers/cacheDatabase.d.ts.map +1 -1
  11. package/esm/helpers/cacheDatabase.js +2 -1
  12. package/esm/helpers/fitUmapCurve.d.ts +2 -0
  13. package/esm/helpers/fitUmapCurve.d.ts.map +1 -0
  14. package/esm/helpers/fitUmapCurve.js +66 -0
  15. package/esm/helpers/initialUmapCoordinates.d.ts +2 -0
  16. package/esm/helpers/initialUmapCoordinates.d.ts.map +1 -0
  17. package/esm/helpers/initialUmapCoordinates.js +5 -0
  18. package/esm/helpers/optimizeUmapLayout.d.ts +7 -0
  19. package/esm/helpers/optimizeUmapLayout.d.ts.map +1 -0
  20. package/esm/helpers/optimizeUmapLayout.js +93 -0
  21. package/esm/helpers/prepareUmapLayoutOptions.d.ts +14 -0
  22. package/esm/helpers/prepareUmapLayoutOptions.d.ts.map +1 -0
  23. package/esm/helpers/prepareUmapLayoutOptions.js +23 -0
  24. package/esm/helpers/readScalarNumber.d.ts +3 -0
  25. package/esm/helpers/readScalarNumber.d.ts.map +1 -0
  26. package/esm/helpers/readScalarNumber.js +4 -0
  27. package/esm/helpers/umapRandom.d.ts +2 -0
  28. package/esm/helpers/umapRandom.d.ts.map +1 -0
  29. package/esm/helpers/umapRandom.js +10 -0
  30. package/esm/methods/cache.js +1 -1
  31. package/esm/methods/loadArray.d.ts.map +1 -1
  32. package/esm/methods/loadArray.js +3 -20
  33. package/esm/methods/umap.d.ts +5 -0
  34. package/esm/methods/umap.d.ts.map +1 -0
  35. package/esm/methods/umap.js +156 -0
  36. package/esm/methods/updateWithJS.js +9 -21
  37. package/llm.md +80 -1
  38. package/llms.txt +1 -1
  39. package/package.json +1 -1
  40. package/script/class/SimpleTable.d.ts +72 -0
  41. package/script/class/SimpleTable.d.ts.map +1 -1
  42. package/script/class/SimpleTable.js +68 -0
  43. package/script/helpers/appendColumnBatches.d.ts +3 -0
  44. package/script/helpers/appendColumnBatches.d.ts.map +1 -0
  45. package/script/helpers/appendColumnBatches.js +24 -0
  46. package/script/helpers/buildUmapGraph.d.ts +24 -0
  47. package/script/helpers/buildUmapGraph.d.ts.map +1 -0
  48. package/script/helpers/buildUmapGraph.js +161 -0
  49. package/script/helpers/cacheDatabase.d.ts.map +1 -1
  50. package/script/helpers/cacheDatabase.js +2 -1
  51. package/script/helpers/fitUmapCurve.d.ts +2 -0
  52. package/script/helpers/fitUmapCurve.d.ts.map +1 -0
  53. package/script/helpers/fitUmapCurve.js +69 -0
  54. package/script/helpers/initialUmapCoordinates.d.ts +2 -0
  55. package/script/helpers/initialUmapCoordinates.d.ts.map +1 -0
  56. package/script/helpers/initialUmapCoordinates.js +11 -0
  57. package/script/helpers/optimizeUmapLayout.d.ts +7 -0
  58. package/script/helpers/optimizeUmapLayout.d.ts.map +1 -0
  59. package/script/helpers/optimizeUmapLayout.js +99 -0
  60. package/script/helpers/prepareUmapLayoutOptions.d.ts +14 -0
  61. package/script/helpers/prepareUmapLayoutOptions.d.ts.map +1 -0
  62. package/script/helpers/prepareUmapLayoutOptions.js +29 -0
  63. package/script/helpers/readScalarNumber.d.ts +3 -0
  64. package/script/helpers/readScalarNumber.d.ts.map +1 -0
  65. package/script/helpers/readScalarNumber.js +7 -0
  66. package/script/helpers/umapRandom.d.ts +2 -0
  67. package/script/helpers/umapRandom.d.ts.map +1 -0
  68. package/script/helpers/umapRandom.js +13 -0
  69. package/script/methods/cache.js +1 -1
  70. package/script/methods/loadArray.d.ts.map +1 -1
  71. package/script/methods/loadArray.js +2 -19
  72. package/script/methods/umap.d.ts +5 -0
  73. package/script/methods/umap.d.ts.map +1 -0
  74. package/script/methods/umap.js +162 -0
  75. package/script/methods/updateWithJS.js +9 -21
@@ -0,0 +1,7 @@
1
+ import type prepareUmapLayoutOptions from "./prepareUmapLayoutOptions.js";
2
+ export default function optimizeUmapLayout(graph: {
3
+ source: Uint32Array;
4
+ target: Uint32Array;
5
+ weight: Float64Array;
6
+ }, initial: Float64Array, options: ReturnType<typeof prepareUmapLayoutOptions>): Float64Array;
7
+ //# sourceMappingURL=optimizeUmapLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"optimizeUmapLayout.d.ts","sourceRoot":"","sources":["../../src/helpers/optimizeUmapLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,wBAAwB,MAAM,+BAA+B,CAAC;AAS1E,MAAM,CAAC,OAAO,UAAU,kBAAkB,CACxC,KAAK,EAAE;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,EACzE,OAAO,EAAE,YAAY,EACrB,OAAO,EAAE,UAAU,CAAC,OAAO,wBAAwB,CAAC,GACnD,YAAY,CA2Fd"}
@@ -0,0 +1,93 @@
1
+ import umapRandom from "./umapRandom.js";
2
+ // Internal interface. Graph entries are directed, in a stable order; the fuzzy
3
+ // union graph supplies both orientations. Coordinates are interleaved x,y.
4
+ // Inputs are borrowed read-only. State is O(edges + vertices), never O(n²).
5
+ // Implements reference Euclidean UMAP SGD: scheduled attractive edges, sampled
6
+ // repulsion, clipped gradients, symmetric attractive updates, linear decay.
7
+ // Algorithm: https://umap-learn.readthedocs.io/en/latest/how_umap_works.html
8
+ export default function optimizeUmapLayout(graph, initial, options) {
9
+ const { epochs, seed, learningRate, negativeSamples, curve: [a, b] } = options;
10
+ const count = initial.length / 2;
11
+ if (!Number.isInteger(count) || count < 3 || count > 0xffffffff) {
12
+ throw new Error("Expected interleaved coordinates for at least three vertices.");
13
+ }
14
+ for (const value of initial) {
15
+ if (!Number.isFinite(value)) {
16
+ throw new Error("Initial coordinates must be finite.");
17
+ }
18
+ }
19
+ const { source, target, weight } = graph;
20
+ if (!weight.length || source.length !== weight.length ||
21
+ target.length !== weight.length)
22
+ throw new Error("Invalid graph column lengths.");
23
+ let maximum = 0;
24
+ for (let edge = 0; edge < weight.length; edge++) {
25
+ if (source[edge] >= count || target[edge] >= count ||
26
+ source[edge] === target[edge])
27
+ throw new Error("Graph contains an invalid vertex or self edge.");
28
+ if (!Number.isFinite(weight[edge]) || weight[edge] < 0 || weight[edge] > 1)
29
+ throw new Error("Graph weights must be between zero and one.");
30
+ maximum = Math.max(maximum, weight[edge]);
31
+ }
32
+ if (!maximum)
33
+ throw new Error("Graph must contain a positive edge.");
34
+ const period = new Float64Array(weight.length);
35
+ const nextPositive = new Float64Array(weight.length);
36
+ const nextNegative = new Float64Array(weight.length);
37
+ // Reference pruning uses 500 when explicitly requesting at most ten epochs.
38
+ const threshold = maximum / (epochs > 10 ? epochs : 500);
39
+ for (let edge = 0; edge < weight.length; edge++) {
40
+ period[edge] = weight[edge] > 0 && weight[edge] >= threshold
41
+ ? epochs / ((weight[edge] / maximum) * epochs)
42
+ : Infinity;
43
+ nextPositive[edge] = period[edge];
44
+ nextNegative[edge] = period[edge] / negativeSamples;
45
+ }
46
+ const coordinates = initial.slice();
47
+ const random = umapRandom(seed ^ 0x7f4a7c15);
48
+ const clip = (gradient) => Math.max(-4, Math.min(4, gradient));
49
+ let alpha = learningRate;
50
+ for (let epoch = 0; epoch < epochs; epoch++) {
51
+ for (let edge = 0; edge < weight.length; edge++) {
52
+ if (nextPositive[edge] > epoch)
53
+ continue;
54
+ const u = source[edge] * 2, v = target[edge] * 2;
55
+ let dx = coordinates[u] - coordinates[v], dy = coordinates[u + 1] - coordinates[v + 1];
56
+ let squared = dx * dx + dy * dy;
57
+ if (squared > 0) {
58
+ const attraction = -2 * a * b * Math.pow(squared, b - 1) /
59
+ (1 + a * Math.pow(squared, b));
60
+ const gx = clip(attraction * dx) * alpha, gy = clip(attraction * dy) * alpha;
61
+ coordinates[u] += gx;
62
+ coordinates[u + 1] += gy;
63
+ coordinates[v] -= gx;
64
+ coordinates[v + 1] -= gy;
65
+ }
66
+ nextPositive[edge] += period[edge];
67
+ const negativePeriod = period[edge] / negativeSamples;
68
+ const draws = Math.floor((epoch - nextNegative[edge]) / negativePeriod);
69
+ for (let draw = 0; draw < draws; draw++) {
70
+ const other = Math.floor(random() * count) * 2;
71
+ dx = coordinates[u] - coordinates[other];
72
+ dy = coordinates[u + 1] - coordinates[other + 1];
73
+ squared = dx * dx + dy * dy;
74
+ // Coincident points get zero repulsive gradient, as in current Python
75
+ // UMAP; this intentionally differs from UMAP-JS's constant +4 fallback.
76
+ if (squared > 0) {
77
+ const repulsion = 2 * b /
78
+ ((0.001 + squared) * (1 + a * Math.pow(squared, b)));
79
+ coordinates[u] += clip(repulsion * dx) * alpha;
80
+ coordinates[u + 1] += clip(repulsion * dy) * alpha;
81
+ }
82
+ }
83
+ nextNegative[edge] += draws * negativePeriod;
84
+ }
85
+ alpha = learningRate * (1 - epoch / epochs);
86
+ }
87
+ for (const value of coordinates) {
88
+ if (!Number.isFinite(value)) {
89
+ throw new Error("UMAP optimization produced a non-finite coordinate.");
90
+ }
91
+ }
92
+ return coordinates;
93
+ }
@@ -0,0 +1,14 @@
1
+ export default function prepareUmapLayoutOptions(options?: {
2
+ epochs?: number;
3
+ seed?: number;
4
+ minDistance?: number;
5
+ learningRate?: number;
6
+ negativeSamples?: number;
7
+ }): {
8
+ epochs: number;
9
+ seed: number;
10
+ learningRate: number;
11
+ negativeSamples: number;
12
+ curve: [number, number];
13
+ };
14
+ //# sourceMappingURL=prepareUmapLayoutOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prepareUmapLayoutOptions.d.ts","sourceRoot":"","sources":["../../src/helpers/prepareUmapLayoutOptions.ts"],"names":[],"mappings":"AAGA,MAAM,CAAC,OAAO,UAAU,wBAAwB,CAC9C,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;CACrB;;;;;;EA2BP"}
@@ -0,0 +1,23 @@
1
+ import fitUmapCurve from "./fitUmapCurve.js";
2
+ // Resolve and validate settings before database work, fitting the curve once.
3
+ export default function prepareUmapLayoutOptions(options = {}) {
4
+ const { epochs = 200, seed = 42, minDistance = 0.1, learningRate = 1, negativeSamples = 5, } = options;
5
+ for (const [key, value] of Object.entries({ epochs, negativeSamples })) {
6
+ if (!Number.isSafeInteger(value) || value < 1) {
7
+ throw new Error(`${key} must be a positive safe integer.`);
8
+ }
9
+ }
10
+ if (!Number.isInteger(seed) || seed < 0 || seed > 0xffffffff) {
11
+ throw new Error("seed must be an integer between 0 and 4294967295.");
12
+ }
13
+ if (!Number.isFinite(learningRate) || learningRate <= 0) {
14
+ throw new Error("learningRate must be finite and positive.");
15
+ }
16
+ return {
17
+ epochs,
18
+ seed,
19
+ learningRate,
20
+ negativeSamples,
21
+ curve: fitUmapCurve(minDistance),
22
+ };
23
+ }
@@ -0,0 +1,3 @@
1
+ import type { DuckDBConnection } from "@duckdb/node-api";
2
+ export default function readScalarNumber(connection: DuckDBConnection, sql: string): Promise<number>;
3
+ //# sourceMappingURL=readScalarNumber.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readScalarNumber.d.ts","sourceRoot":"","sources":["../../src/helpers/readScalarNumber.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGzD,wBAA8B,gBAAgB,CAC5C,UAAU,EAAE,gBAAgB,EAC5B,GAAG,EAAE,MAAM,mBAGZ"}
@@ -0,0 +1,4 @@
1
+ // For internal queries returning a single numeric value, such as row counts.
2
+ export default async function readScalarNumber(connection, sql) {
3
+ return Number((await connection.runAndReadAll(sql)).getRowsJS()[0][0]);
4
+ }
@@ -0,0 +1,2 @@
1
+ export default function umapRandom(seed: number): () => number;
2
+ //# sourceMappingURL=umapRandom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"umapRandom.d.ts","sourceRoot":"","sources":["../../src/helpers/umapRandom.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,IAAI,EAAE,MAAM,gBAQ9C"}
@@ -0,0 +1,10 @@
1
+ // Mulberry32: separate initialization and sampling streams, stable per seed.
2
+ export default function umapRandom(seed) {
3
+ let state = seed >>> 0;
4
+ return () => {
5
+ state = (state + 0x6D2B79F5) | 0;
6
+ let value = Math.imul(state ^ state >>> 15, 1 | state);
7
+ value ^= value + Math.imul(value ^ value >>> 7, 61 | value);
8
+ return ((value ^ value >>> 14) >>> 0) / 4294967296;
9
+ };
10
+ }
@@ -7,7 +7,7 @@ import serializeCacheInputs, { getCacheInputLabel, } from "../helpers/serializeC
7
7
  import { createTableGenerationId, peekTableGeneration, restoreTableGeneration, } from "../helpers/tableGeneration.js";
8
8
  import { loadCacheDatabase, writeCacheDatabase, } from "../helpers/cacheDatabase.js";
9
9
  import { assertCacheTableMutation, cacheTableDependenciesMatch, captureCacheTableDependencies, changedCacheTableDependenciesMessage, recordCacheTableAccess, unchangedCacheTableDependenciesMessage, } from "../helpers/cacheTableDependencies.js";
10
- const CACHE_FORMAT_VERSION = "duckdb-v4";
10
+ const CACHE_FORMAT_VERSION = "duckdb-v5";
11
11
  export default async function cache(table, compute, options = {}) {
12
12
  // If cache() is itself called from another cache computation, the outer
13
13
  // computation depends on the table produced here.
@@ -1 +1 @@
1
- {"version":3,"file":"loadArray.d.ts","sourceRoot":"","sources":["../../src/methods/loadArray.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAC;AAK1B,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAKvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,EAClC,OAAO,GAAE,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAM,QAiBnE;AAED,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,WAAW,EAAE,EAAE,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,EAClC,WAAW,GAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAO,EAC3C,0BAA0B,UAAQ,GACjC,aAAa,CAsHf;AAED,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,aAAa,iBA4ExB"}
1
+ {"version":3,"file":"loadArray.d.ts","sourceRoot":"","sources":["../../src/methods/loadArray.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,WAAW,EACjB,MAAM,kBAAkB,CAAC;AAM1B,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAKvD,MAAM,CAAC,OAAO,UAAU,SAAS,CAC/B,WAAW,EAAE,WAAW,EACxB,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,EAClC,OAAO,GAAE,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAM,QAiBnE;AAED,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,EAAE,WAAW,EAAE,EAAE,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAAE,EAClC,WAAW,GAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAO,EAC3C,0BAA0B,UAAQ,GACjC,aAAa,CAsHf;AAED,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,WAAW,EACxB,QAAQ,EAAE,aAAa,iBA+DxB"}
@@ -1,4 +1,5 @@
1
- import { arrayValue, DuckDBDataChunk, DuckDBDateValue, DuckDBTimestampTZValue, DuckDBTimestampValue, DuckDBTimeValue, } from "@duckdb/node-api";
1
+ import { arrayValue, DuckDBDateValue, DuckDBTimestampTZValue, DuckDBTimestampValue, DuckDBTimeValue, } from "@duckdb/node-api";
2
+ import appendColumnBatches from "../helpers/appendColumnBatches.js";
2
3
  import prepareGeometry from "../helpers/prepareGeometry.js";
3
4
  import prepareJSON from "../helpers/prepareJSON.js";
4
5
  import geometryFromJSON from "../helpers/geometryFromJSON.js";
@@ -136,25 +137,7 @@ export async function executePreparedArray(simpleTable, prepared) {
136
137
  }
137
138
  try {
138
139
  await simpleTable.sdb.customQuery(`CREATE ${needsConversion ? "TEMP" : "OR REPLACE"} TABLE ${quoteIdentifier(staged)}(${keys.map((key, i) => `${quoteIdentifier(key)} ${storageTypes[i]}`).join(", ")})`);
139
- const appender = await simpleTable.connection
140
- .createAppender(staged);
141
- try {
142
- const duckDBTypes = storageTypes.map((d) => parseDuckDBType(d));
143
- // The maximum capacity of a DuckDB data chunk is 2048 rows.
144
- const chunkSize = 2000;
145
- for (let start = 0; start < rowCount; start += chunkSize) {
146
- const end = Math.min(start + chunkSize, rowCount);
147
- const dataChunk = DuckDBDataChunk.create(duckDBTypes, end - start);
148
- for (let i = 0; i < keys.length; i++) {
149
- dataChunk.setColumnValues(i, columnsData[i].slice(start, end));
150
- }
151
- appender.appendDataChunk(dataChunk);
152
- }
153
- appender.flushSync();
154
- }
155
- finally {
156
- appender.closeSync();
157
- }
140
+ await appendColumnBatches(simpleTable.connection, staged, storageTypes.map((type) => parseDuckDBType(type)), rowCount, (column, start, end) => columnsData[column].slice(start, end));
158
141
  if (!needsConversion)
159
142
  return;
160
143
  // CTAS is atomic: conversion must finish before the destination is replaced.
@@ -0,0 +1,5 @@
1
+ import type SimpleTable from "../class/SimpleTable.js";
2
+ type Options = NonNullable<Parameters<SimpleTable["umap"]>[1]>;
3
+ export default function umap(table: SimpleTable, column: string, options?: Options): void;
4
+ export {};
5
+ //# sourceMappingURL=umap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"umap.d.ts","sourceRoot":"","sources":["../../src/methods/umap.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAC;AAavD,KAAK,OAAO,GAAG,WAAW,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE/D,MAAM,CAAC,OAAO,UAAU,IAAI,CAC1B,KAAK,EAAE,WAAW,EAClB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,OAAY,QAStB"}
@@ -0,0 +1,156 @@
1
+ import queueOp from "../helpers/queueOp.js";
2
+ import quoteIdentifier from "../helpers/quoteIdentifier.js";
3
+ import queryDB from "../helpers/queryDB.js";
4
+ import mergeOptions from "../helpers/mergeOptions.js";
5
+ import buildUmapGraph from "../helpers/buildUmapGraph.js";
6
+ import prepareUmapLayoutOptions from "../helpers/prepareUmapLayoutOptions.js";
7
+ import readScalarNumber from "../helpers/readScalarNumber.js";
8
+ import optimizeUmapLayout from "../helpers/optimizeUmapLayout.js";
9
+ import initialUmapCoordinates from "../helpers/initialUmapCoordinates.js";
10
+ import appendColumnBatches from "../helpers/appendColumnBatches.js";
11
+ import { DOUBLE, INTEGER } from "@duckdb/node-api";
12
+ export default function umap(table, column, options = {}) {
13
+ const settings = structuredClone(options);
14
+ queueOp(table, {
15
+ kind: "barrier",
16
+ method: "umap()",
17
+ parameters: { column, options: settings },
18
+ execute: () => execute(table, column, settings),
19
+ });
20
+ }
21
+ async function execute(table, column, options) {
22
+ const { neighbors = 15, metric = "euclidean" } = options;
23
+ if (!Number.isSafeInteger(neighbors) || neighbors < 2) {
24
+ throw new Error("neighbors must be a safe integer of at least 2.");
25
+ }
26
+ const layoutOptions = prepareUmapLayoutOptions(options);
27
+ if (!["euclidean", "cosine"].includes(metric)) {
28
+ throw new Error("metric must be euclidean or cosine.");
29
+ }
30
+ const types = await table.getTypes();
31
+ const columns = Object.keys(types);
32
+ const resolve = (name) => {
33
+ const found = columns.find((c) => c.toLowerCase() === name.toLowerCase());
34
+ if (found === undefined)
35
+ throw new Error(`Column ${name} does not exist.`);
36
+ return found;
37
+ };
38
+ const vector = resolve(column);
39
+ if (columns.some((c) => ["umapx", "umapy"].includes(c.toLowerCase()))) {
40
+ throw new Error("UMAP output columns umapX and umapY must not already exist.");
41
+ }
42
+ if (!/^(FLOAT|DOUBLE|TINYINT|SMALLINT|INTEGER|BIGINT|HUGEINT|UTINYINT|USMALLINT|UINTEGER|UBIGINT|UHUGEINT|DECIMAL\(\d+,\d+\))\[\d*\]$/
43
+ .test(types[vector])) {
44
+ throw new Error("UMAP requires a one-dimensional numeric LIST or ARRAY column.");
45
+ }
46
+ const connection = table.connection;
47
+ const [sourceOid, sourceTemporary] = (await connection.runAndReadAll(`SELECT table_oid,temporary FROM duckdb_tables()
48
+ WHERE lower(table_name)=lower($1) AND schema_name=current_schema()
49
+ AND database_name IN (current_database(),'temp')
50
+ ORDER BY temporary DESC LIMIT 1`, [table.name])).getRowsJS()[0] ?? [];
51
+ if (typeof sourceOid !== "number" && typeof sourceOid !== "bigint") {
52
+ throw new Error("UMAP requires a materialized source table.");
53
+ }
54
+ const q = quoteIdentifier;
55
+ const suffix = crypto.randomUUID().replaceAll("-", "");
56
+ const name = (part) => `__sda_umap_${suffix}_${part}`;
57
+ const ordinal = q(name("ordinal"));
58
+ const source = q(table.name), snapshot = q(name("snapshot"));
59
+ const layoutName = name("layout"), layout = q(layoutName);
60
+ const names = {
61
+ rows: q(name("rows")),
62
+ knn: q(name("knn")),
63
+ search: q(name("search")),
64
+ hnsw: q(name("hnsw")),
65
+ candidates: q(name("candidates")),
66
+ scales: q(name("scales")),
67
+ directed: q(name("directed")),
68
+ graph: q(name("graph")),
69
+ };
70
+ const scratch = [
71
+ snapshot,
72
+ layout,
73
+ ...Object.entries(names)
74
+ .filter(([key]) => key !== "hnsw").map(([, value]) => value),
75
+ ];
76
+ try {
77
+ // Retain payloads in native DuckDB types. A private ordinal distinguishes
78
+ // duplicate rows and is independent of any user column named rowid.
79
+ await connection.run(`CREATE TEMP TABLE ${snapshot} AS
80
+ SELECT *, row_number() OVER () - 1 AS ${ordinal} FROM ${source}`);
81
+ const count = await readScalarNumber(connection, `SELECT count(*) FROM ${snapshot}`);
82
+ if (count < 3 || count > 0x7fffffff) {
83
+ throw new Error("UMAP requires between 3 and 2147483647 rows.");
84
+ }
85
+ if (await readScalarNumber(connection, `SELECT count(*) FROM ${snapshot}
86
+ WHERE ${q(vector)} IS NULL OR len(${q(vector)})=0
87
+ OR list_count(${q(vector)}) != len(${q(vector)})
88
+ OR NOT list_bool_and(list_transform(${q(vector)}, x -> isfinite(x::DOUBLE)))`)) {
89
+ throw new Error("Vectors must be nonempty, non-null and contain finite numbers only.");
90
+ }
91
+ if (await readScalarNumber(connection, `SELECT count(DISTINCT len(${q(vector)})) FROM ${snapshot}`) !== 1) {
92
+ throw new Error("Vectors must have equal dimensions.");
93
+ }
94
+ const dimensions = await readScalarNumber(connection, `SELECT len(${q(vector)}) FROM ${snapshot} LIMIT 1`);
95
+ await connection.run(`CREATE TEMP TABLE ${names.rows} AS
96
+ SELECT (row_number() OVER (ORDER BY ${ordinal})-1)::INTEGER AS vertex,
97
+ ${ordinal} AS ordinal, ${q(vector)}::DOUBLE[${dimensions}] AS vec FROM ${snapshot}`);
98
+ if (await readScalarNumber(connection, `SELECT count(*) FROM ${names.rows}
99
+ WHERE NOT isfinite(array_inner_product(vec,vec)) OR array_inner_product(vec,vec)>1e300
100
+ ${metric === "cosine" ? "OR array_inner_product(vec,vec)=0" : ""}`)) {
101
+ throw new Error("Vector norms overflow, or cosine distance received a zero vector.");
102
+ }
103
+ const graph = await buildUmapGraph(connection, {
104
+ count,
105
+ dimensions,
106
+ neighbors: Math.min(neighbors, count - 1),
107
+ }, {
108
+ metric,
109
+ search: count <= 1000 ? "exact" : "hnsw",
110
+ }, names);
111
+ // Release index and vector scratch before allocating optimizer state.
112
+ for (const relation of [
113
+ names.search,
114
+ names.candidates,
115
+ names.knn,
116
+ names.directed,
117
+ names.scales,
118
+ names.graph,
119
+ ]) {
120
+ await connection.run(`DROP TABLE IF EXISTS ${relation}`);
121
+ }
122
+ await connection.run(`ALTER TABLE ${names.rows} DROP COLUMN vec`);
123
+ const coordinates = optimizeUmapLayout(graph, initialUmapCoordinates(count, layoutOptions.seed), layoutOptions);
124
+ await connection.run(`CREATE TEMP TABLE ${layout} (vertex INTEGER,x DOUBLE,y DOUBLE)`);
125
+ await appendColumnBatches(connection, layoutName, [INTEGER, DOUBLE, DOUBLE], count, (column, start, end) => Array.from({ length: end - start }, (_, row) => column === 0
126
+ ? start + row
127
+ : coordinates[2 * (start + row) + column - 1]));
128
+ // Publish in one statement: failed fits never add partial columns. Existing
129
+ // native indexes must be restored because CREATE OR REPLACE drops them.
130
+ const indexes = (await connection.runAndReadAll("SELECT sql FROM duckdb_indexes() WHERE table_oid=$1", [sourceOid])).getRowsJS().map((row) => String(row[0]));
131
+ await connection.run("BEGIN TRANSACTION");
132
+ try {
133
+ await queryDB(table, `CREATE OR REPLACE ${sourceTemporary ? "TEMP " : ""}TABLE ${source} AS
134
+ SELECT ${columns.map((c) => `s.${q(c)}`).join(",")}, l.x AS "umapX", l.y AS "umapY"
135
+ FROM ${snapshot} s JOIN ${names.rows} r ON s.${ordinal}=r.ordinal
136
+ JOIN ${layout} l ON r.vertex=l.vertex ORDER BY s.${ordinal}`, mergeOptions(table, {
137
+ table: table.name,
138
+ method: "umap()",
139
+ parameters: { column, options },
140
+ noClean: true,
141
+ }));
142
+ for (const sql of indexes)
143
+ await connection.run(sql);
144
+ await connection.run("COMMIT");
145
+ }
146
+ catch (error) {
147
+ await connection.run("ROLLBACK");
148
+ throw error;
149
+ }
150
+ }
151
+ finally {
152
+ for (const relation of scratch) {
153
+ await connection.run(`DROP TABLE IF EXISTS ${relation}`);
154
+ }
155
+ }
156
+ }
@@ -4,7 +4,7 @@ import quoteIdentifier from "../helpers/quoteIdentifier.js";
4
4
  import { retainRegisteredTables } from "../helpers/tableRegistry.js";
5
5
  import queueOp from "../helpers/queueOp.js";
6
6
  import { prepareArray } from "./loadArray.js";
7
- import { DuckDBDataChunk } from "@duckdb/node-api";
7
+ import appendColumnBatches from "../helpers/appendColumnBatches.js";
8
8
  import readMutationRows from "../helpers/readMutationRows.js";
9
9
  import parseDuckDBType from "../helpers/parseDuckDBType.js";
10
10
  export default function updateWithJS(simpleTable, dataModifier, options = {}) {
@@ -133,26 +133,14 @@ async function executeUpdateWithJS(simpleTable, dataModifier, options) {
133
133
  parseDuckDBType(added.types[newKeys.indexOf(key)]));
134
134
  await simpleTable.sdb.customQuery(`CREATE OR REPLACE TABLE ${quoteIdentifier(scratch.name)} (${keys.map((key, i) => `${quoteIdentifier(key)} ${columnTypes[i]}`)
135
135
  .join(", ")})`);
136
- const appender = await simpleTable.connection.createAppender(scratch.name);
137
- try {
138
- for (let start = 0; start < modified.length; start += 2000) {
139
- const end = Math.min(start + 2000, modified.length);
140
- const chunk = DuckDBDataChunk.create(columnTypes, end - start);
141
- for (let i = 0; i < keys.length; i++) {
142
- const key = keys[i];
143
- chunk.setColumnValues(i, outputGeometry.includes(key)
144
- ? geometryData.get(key).slice(start, end)
145
- : source.types.has(key)
146
- ? modified.slice(start, end).map((row) => source.toNative(row[key], columnTypes[i]))
147
- : added.columnsData[newKeys.indexOf(key)].slice(start, end));
148
- }
149
- appender.appendDataChunk(chunk);
150
- }
151
- appender.flushSync();
152
- }
153
- finally {
154
- appender.closeSync();
155
- }
136
+ await appendColumnBatches(simpleTable.connection, scratch.name, columnTypes, modified.length, (column, start, end) => {
137
+ const key = keys[column];
138
+ return outputGeometry.includes(key)
139
+ ? geometryData.get(key).slice(start, end)
140
+ : source.types.has(key)
141
+ ? modified.slice(start, end).map((row) => source.toNative(row[key], columnTypes[column]))
142
+ : added.columnsData[newKeys.indexOf(key)].slice(start, end);
143
+ });
156
144
  const projection = keys.map((key) => outputGeometry.includes(key)
157
145
  ? `${geometryFromJSON(quoteIdentifier(key))} AS ${quoteIdentifier(key)}`
158
146
  : outputJSON.includes(key)
package/llm.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # The Simple Data Analysis (Core) Library
2
2
 
3
3
  - Package: `@nshiab/simple-data-analysis-core`
4
- - Version: `2.0.8`
4
+ - Version: `2.0.9`
5
5
 
6
6
  To install the library with Deno, use:
7
7
 
@@ -1451,6 +1451,85 @@ await table.createVssIndex("embedding_column", {
1451
1451
  }).log();
1452
1452
  ```
1453
1453
 
1454
+ #### `umap`
1455
+
1456
+ Reduces numeric vectors, such as embeddings, to a two-dimensional UMAP
1457
+ projection. The resulting coordinates are added as `umapX` and `umapY`, while
1458
+ all existing columns (including the source vector column), their values and
1459
+ types, and the input row order are preserved. DuckDB computes neighbors and the
1460
+ fuzzy graph; TypeScript optimizes the coordinates without copying the input
1461
+ vectors into JavaScript. Neighbor search is selected automatically.
1462
+
1463
+ The defaults are a starting point for exploration. To adjust the projection:
1464
+
1465
+ - `neighbors` (default `15`): How many nearby points influence the layout.
1466
+ Smaller values emphasize local detail but can fragment groups. Larger values
1467
+ emphasize broader structure, can hide local detail, and generally require more
1468
+ time and memory.
1469
+ - `metric` (default `"euclidean"`): How similarity is measured. Euclidean
1470
+ compares distance, including differences in vector magnitude. Cosine compares
1471
+ direction, ignoring magnitude. Choose according to what makes vectors similar
1472
+ in your data; neither is universally better.
1473
+ - `epochs` (default `200`): How many passes refine the coordinates. Fewer passes
1474
+ finish sooner but may leave the layout unfinished. More passes allow further
1475
+ refinement and take longer, with diminishing returns.
1476
+ - `minDistance` (default `0.1`): How tightly points can group in the projection.
1477
+ Smaller values allow tighter clumps; larger values spread points out. This
1478
+ changes the layout's appearance, not an accuracy level or the number of
1479
+ optimization passes.
1480
+ - `learningRate` (default `1`): The initial size of coordinate adjustments.
1481
+ Smaller values make gentler adjustments and may need more epochs. Larger
1482
+ values make bigger adjustments but can overshoot useful positions. This does
1483
+ not change the number of passes.
1484
+ - `negativeSamples` (default `5`): How many random points are sampled for
1485
+ repulsion per attractive update. Smaller values reduce work and repulsion;
1486
+ larger values increase both, tending to separate unrelated points more
1487
+ strongly. More samples do not guarantee a better projection.
1488
+
1489
+ ##### Signature
1490
+
1491
+ ```typescript
1492
+ umap(column: string, options?: { neighbors?: number; metric?: "euclidean" | "cosine"; epochs?: number; seed?: number; minDistance?: number; learningRate?: number; negativeSamples?: number }): this;
1493
+ ```
1494
+
1495
+ ##### Parameters
1496
+
1497
+ - **`column`**: The column containing numeric vector embeddings.
1498
+ - **`options`**: Optional projection settings.
1499
+ - **`options.neighbors`**: Neighborhood size, including the point itself.
1500
+ Integer of at least 2, clamped to row count minus one. Defaults to 15.
1501
+ - **`options.metric`**: Input distance metric: "euclidean" or "cosine". Defaults
1502
+ to "euclidean".
1503
+ - **`options.epochs`**: Integer number of refinement passes, at least 1.
1504
+ Defaults to 200.
1505
+ - **`options.seed`**: Integer used to initialize random choices. Defaults to 42.
1506
+ - **`options.minDistance`**: Grouping distance parameter between 0 and 1.
1507
+ Defaults to 0.1.
1508
+ - **`options.learningRate`**: Finite, positive initial learning rate. Defaults
1509
+ to 1.
1510
+ - **`options.negativeSamples`**: Integer number of samples used to separate
1511
+ unrelated points, at least 1. Defaults to 5.
1512
+
1513
+ ##### Returns
1514
+
1515
+ The table, so methods can be chained.
1516
+
1517
+ ##### Examples
1518
+
1519
+ ```ts
1520
+ await table.umap("embedding", {
1521
+ metric: "cosine",
1522
+ seed: 42,
1523
+ }).log();
1524
+ ```
1525
+
1526
+ ```ts
1527
+ await table.umap("embedding", {
1528
+ neighbors: 30,
1529
+ minDistance: 0.25,
1530
+ }).selectColumns(["label", "umapX", "umapY"]).log();
1531
+ ```
1532
+
1454
1533
  #### `bm25`
1455
1534
 
1456
1535
  Searches a text column using DuckDB's BM25 ranking function, which scores
package/llms.txt CHANGED
@@ -3,7 +3,7 @@
3
3
  > Lightweight DuckDB-powered TypeScript library for tabular, SQL, CSV, Parquet, and geospatial data analysis on Deno, Node.js, and Bun.
4
4
 
5
5
  - Package: `@nshiab/simple-data-analysis-core`
6
- - Version: `2.0.8`
6
+ - Version: `2.0.9`
7
7
 
8
8
  Install with Deno: `deno add jsr:@nshiab/simple-data-analysis-core`
9
9
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nshiab/simple-data-analysis-core",
3
- "version": "2.0.8",
3
+ "version": "2.0.9",
4
4
  "description": "Lightweight DuckDB-powered TypeScript library for tabular, SQL, CSV, Parquet, and geospatial data analysis on Deno, Node.js, and Bun.",
5
5
  "keywords": [
6
6
  "data-analysis",
@@ -757,6 +757,78 @@ export default class SimpleTable extends Simple {
757
757
  efSearch?: number;
758
758
  M?: number;
759
759
  }): this;
760
+ /**
761
+ * Reduces numeric vectors, such as embeddings, to a two-dimensional UMAP
762
+ * projection. The resulting coordinates are added as `umapX` and `umapY`,
763
+ * while all existing columns (including the source vector column), their
764
+ * values and types, and the input row order are preserved. DuckDB computes
765
+ * neighbors and the fuzzy graph; TypeScript optimizes the coordinates without
766
+ * copying the input vectors into JavaScript. Neighbor search is selected
767
+ * automatically.
768
+ *
769
+ * The defaults are a starting point for exploration. To adjust the projection:
770
+ *
771
+ * - `neighbors` (default `15`): How many nearby points influence the layout.
772
+ * Smaller values emphasize local detail but can fragment groups. Larger
773
+ * values emphasize broader structure, can hide local detail, and generally
774
+ * require more time and memory.
775
+ * - `metric` (default `"euclidean"`): How similarity is measured. Euclidean
776
+ * compares distance, including differences in vector magnitude. Cosine
777
+ * compares direction, ignoring magnitude. Choose according to what makes
778
+ * vectors similar in your data; neither is universally better.
779
+ * - `epochs` (default `200`): How many passes refine the coordinates. Fewer
780
+ * passes finish sooner but may leave the layout unfinished. More passes
781
+ * allow further refinement and take longer, with diminishing returns.
782
+ * - `minDistance` (default `0.1`): How tightly points can group in the
783
+ * projection. Smaller values allow tighter clumps; larger values spread
784
+ * points out. This changes the layout's appearance, not an accuracy level
785
+ * or the number of optimization passes.
786
+ * - `learningRate` (default `1`): The initial size of coordinate adjustments.
787
+ * Smaller values make gentler adjustments and may need more epochs. Larger
788
+ * values make bigger adjustments but can overshoot useful positions. This
789
+ * does not change the number of passes.
790
+ * - `negativeSamples` (default `5`): How many random points are sampled for
791
+ * repulsion per attractive update. Smaller values reduce work and
792
+ * repulsion; larger values increase both, tending to separate unrelated
793
+ * points more strongly. More samples do not guarantee a better projection.
794
+ *
795
+ * @param column - The column containing numeric vector embeddings.
796
+ * @param options - Optional projection settings.
797
+ * @param options.neighbors - Neighborhood size, including the point itself. Integer of at least 2, clamped to row count minus one. Defaults to 15.
798
+ * @param options.metric - Input distance metric: "euclidean" or "cosine". Defaults to "euclidean".
799
+ * @param options.epochs - Integer number of refinement passes, at least 1. Defaults to 200.
800
+ * @param options.seed - Integer used to initialize random choices. Defaults to 42.
801
+ * @param options.minDistance - Grouping distance parameter between 0 and 1. Defaults to 0.1.
802
+ * @param options.learningRate - Finite, positive initial learning rate. Defaults to 1.
803
+ * @param options.negativeSamples - Integer number of samples used to separate unrelated points, at least 1. Defaults to 5.
804
+ * @returns The table, so methods can be chained.
805
+ * @category Vector Search
806
+ *
807
+ * @example
808
+ * ```ts
809
+ * await table.umap("embedding", {
810
+ * metric: "cosine",
811
+ * seed: 42,
812
+ * }).log();
813
+ * ```
814
+ *
815
+ * @example
816
+ * ```ts
817
+ * await table.umap("embedding", {
818
+ * neighbors: 30,
819
+ * minDistance: 0.25,
820
+ * }).selectColumns(["label", "umapX", "umapY"]).log();
821
+ * ```
822
+ */
823
+ umap(column: string, options?: {
824
+ neighbors?: number;
825
+ metric?: "euclidean" | "cosine";
826
+ epochs?: number;
827
+ seed?: number;
828
+ minDistance?: number;
829
+ learningRate?: number;
830
+ negativeSamples?: number;
831
+ }): this;
760
832
  /**
761
833
  * Searches a text column using DuckDB's BM25 ranking function, which scores
762
834
  * matches using factors including term frequency and document length.