@openmle/omle.js 0.1.0-rc4

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 (71) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +172 -0
  3. package/dist/engine/clustering.d.ts +8 -0
  4. package/dist/engine/clustering.d.ts.map +1 -0
  5. package/dist/engine/clustering.js +161 -0
  6. package/dist/engine/clustering.js.map +1 -0
  7. package/dist/engine/executor.d.ts +57 -0
  8. package/dist/engine/executor.d.ts.map +1 -0
  9. package/dist/engine/executor.js +1160 -0
  10. package/dist/engine/executor.js.map +1 -0
  11. package/dist/engine/explain.d.ts +68 -0
  12. package/dist/engine/explain.d.ts.map +1 -0
  13. package/dist/engine/explain.js +401 -0
  14. package/dist/engine/explain.js.map +1 -0
  15. package/dist/engine/linear.d.ts +5 -0
  16. package/dist/engine/linear.d.ts.map +1 -0
  17. package/dist/engine/linear.js +37 -0
  18. package/dist/engine/linear.js.map +1 -0
  19. package/dist/engine/naive_bayes.d.ts +5 -0
  20. package/dist/engine/naive_bayes.d.ts.map +1 -0
  21. package/dist/engine/naive_bayes.js +102 -0
  22. package/dist/engine/naive_bayes.js.map +1 -0
  23. package/dist/engine/nn.d.ts +5 -0
  24. package/dist/engine/nn.d.ts.map +1 -0
  25. package/dist/engine/nn.js +34 -0
  26. package/dist/engine/nn.js.map +1 -0
  27. package/dist/engine/ops.d.ts +14 -0
  28. package/dist/engine/ops.d.ts.map +1 -0
  29. package/dist/engine/ops.js +244 -0
  30. package/dist/engine/ops.js.map +1 -0
  31. package/dist/engine/predicates.d.ts +5 -0
  32. package/dist/engine/predicates.d.ts.map +1 -0
  33. package/dist/engine/predicates.js +105 -0
  34. package/dist/engine/predicates.js.map +1 -0
  35. package/dist/engine/preprocess.d.ts +31 -0
  36. package/dist/engine/preprocess.d.ts.map +1 -0
  37. package/dist/engine/preprocess.js +1112 -0
  38. package/dist/engine/preprocess.js.map +1 -0
  39. package/dist/engine/svm.d.ts +5 -0
  40. package/dist/engine/svm.d.ts.map +1 -0
  41. package/dist/engine/svm.js +241 -0
  42. package/dist/engine/svm.js.map +1 -0
  43. package/dist/engine/tree.d.ts +6 -0
  44. package/dist/engine/tree.d.ts.map +1 -0
  45. package/dist/engine/tree.js +274 -0
  46. package/dist/engine/tree.js.map +1 -0
  47. package/dist/engine/validate_inputs.d.ts +22 -0
  48. package/dist/engine/validate_inputs.d.ts.map +1 -0
  49. package/dist/engine/validate_inputs.js +225 -0
  50. package/dist/engine/validate_inputs.js.map +1 -0
  51. package/dist/index.d.ts +12 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +16 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/io.d.ts +9 -0
  56. package/dist/io.d.ts.map +1 -0
  57. package/dist/io.js +93 -0
  58. package/dist/io.js.map +1 -0
  59. package/dist/ir.d.ts +489 -0
  60. package/dist/ir.d.ts.map +1 -0
  61. package/dist/ir.js +41 -0
  62. package/dist/ir.js.map +1 -0
  63. package/dist/resolve.d.ts +20 -0
  64. package/dist/resolve.d.ts.map +1 -0
  65. package/dist/resolve.js +138 -0
  66. package/dist/resolve.js.map +1 -0
  67. package/dist/validate.d.ts +15 -0
  68. package/dist/validate.d.ts.map +1 -0
  69. package/dist/validate.js +332 -0
  70. package/dist/validate.js.map +1 -0
  71. package/package.json +51 -0
@@ -0,0 +1,1160 @@
1
+ // Reference execution engine for OMLE models.
2
+ //
3
+ // Executes nodes in topological order, building a namespace of named TensorData values.
4
+ // Supports: Tree, TreeEnsemble, Linear, NeuralNetwork, NaiveBayes, Clustering, SVM,
5
+ // and CompositeNode (via recursive execution).
6
+ import { scalarToNumber } from '../ir.js';
7
+ import { resolve, expandNodeInputs, expandFeatures, resolveTensorValue } from '../resolve.js';
8
+ import { tensorToData, tensorCols } from './ops.js';
9
+ import { executeTree, executeTreeEnsemble } from './tree.js';
10
+ import { executeLinear } from './linear.js';
11
+ import { executeNeuralNetwork } from './nn.js';
12
+ import { executeNaiveBayes } from './naive_bayes.js';
13
+ import { executeClustering } from './clustering.js';
14
+ import { executeSVM } from './svm.js';
15
+ import { computeExplain } from './explain.js';
16
+ import { executeTakeSlots, executeConcat, executeDerive, executeBinarizer, executeMinMaxScaler, executeMaxAbsScaler, executeRobustScaler, executeNormalizer, executeOneHotEncoder, executeBucketizer, executePolynomialFeatures, executePowerTransformer, executeQuantileTransformer, executeSplineTransformer, executeTruncatedSVD, executePCA, executeImputer, executeWeightedSum, executeNormContinuous, executeTokenizer, executeRegexTokenizer, executeNGram, executeStopWordsRemover, executeCountVectorizer, executeHashingVectorizer, executeTfIdfTransformer, executeWord2Vec, } from './preprocess.js';
17
+ export class Engine {
18
+ resolved;
19
+ constructor(model) {
20
+ this.resolved = resolve(model);
21
+ }
22
+ run(inputs) {
23
+ const { resolved } = this;
24
+ const model = resolved.model;
25
+ // Normalize all inputs to TensorData so downstream code always sees .shape/.data
26
+ const namespace = new Map();
27
+ for (const [name, val] of Object.entries(inputs)) {
28
+ namespace.set(name, normalizeInput(val));
29
+ }
30
+ const N = inferBatchSize(namespace);
31
+ expandSchemaFeatures(model, namespace, N);
32
+ // Use topologically sorted order computed during resolve()
33
+ executeNodes(resolved.executionOrder, namespace, N, resolved);
34
+ const outputs = {};
35
+ for (const outSpec of model.outputs ?? []) {
36
+ const td = namespace.get(outSpec.name);
37
+ if (td)
38
+ outputs[outSpec.name] = td;
39
+ }
40
+ return outputs;
41
+ }
42
+ verify() {
43
+ const { resolved } = this;
44
+ const model = resolved.model;
45
+ const ver = model.verification;
46
+ if (!ver?.cases?.length)
47
+ return { pass: true, cases: [] };
48
+ const atol = ver.tolerance?.atol != null ? scalarToNumber(ver.tolerance.atol) : 1e-6;
49
+ const rtol = ver.tolerance?.rtol != null ? scalarToNumber(ver.tolerance.rtol) : 1e-5;
50
+ const tensorIndex = resolved.tensorIndex;
51
+ const modelInputs = model.inputs ?? [];
52
+ const caseResults = [];
53
+ for (let ci = 0; ci < ver.cases.length; ci++) {
54
+ const vc = ver.cases[ci];
55
+ const inferInputs = {};
56
+ const vcInputs = vc.inputs ?? [];
57
+ for (let i = 0; i < vcInputs.length; i++) {
58
+ const ref = vcInputs[i];
59
+ const entry = tensorIndex.get(ref.id);
60
+ if (!entry?.dense)
61
+ continue;
62
+ const name = entry.dense.name ?? modelInputs[i]?.name ?? ref.id;
63
+ inferInputs[name] = tensorToData(entry.dense);
64
+ }
65
+ const output = this.run(inferInputs);
66
+ const outputResults = [];
67
+ const vcExpected = vc.expected_outputs ?? [];
68
+ for (let i = 0; i < vcExpected.length; i++) {
69
+ const ref = vcExpected[i];
70
+ const entry = tensorIndex.get(ref.id);
71
+ if (!entry?.dense)
72
+ continue;
73
+ const outName = entry.dense.name ?? model.outputs?.[i]?.name ?? ref.id;
74
+ const actual = output[outName];
75
+ if (!actual) {
76
+ outputResults.push({ name: outName, pass: false, maxAbsErr: Infinity, maxRelErr: Infinity, atol, rtol });
77
+ continue;
78
+ }
79
+ const expected = tensorToData(entry.dense);
80
+ const { pass, maxAbsErr, maxRelErr } = compareTensors(actual, expected, atol, rtol);
81
+ outputResults.push({ name: outName, pass, maxAbsErr, maxRelErr, atol, rtol });
82
+ }
83
+ const casePass = outputResults.length > 0 && outputResults.every(r => r.pass);
84
+ caseResults.push({ index: ci, pass: casePass, outputs: outputResults });
85
+ }
86
+ return { pass: caseResults.every(r => r.pass), cases: caseResults };
87
+ }
88
+ runWithSteps(inputs) {
89
+ const { resolved } = this;
90
+ const model = resolved.model;
91
+ const namespace = new Map();
92
+ for (const [name, val] of Object.entries(inputs)) {
93
+ namespace.set(name, normalizeInput(val));
94
+ }
95
+ const N = inferBatchSize(namespace);
96
+ expandSchemaFeatures(model, namespace, N);
97
+ const steps = [];
98
+ for (const node of resolved.executionOrder) {
99
+ const inputNames = [...new Set(expandNodeInputs(node.inputs ?? []))];
100
+ const inputsBefore = {};
101
+ for (const name of inputNames) {
102
+ inputsBefore[name] = serializeTensor(namespace.get(name));
103
+ }
104
+ const warnings = [];
105
+ const t0 = performance.now();
106
+ try {
107
+ executeNode(node, namespace, N, resolved);
108
+ }
109
+ catch (e) {
110
+ warnings.push(String(e));
111
+ }
112
+ const durationMs = performance.now() - t0;
113
+ const outputsAfter = {};
114
+ for (const out of node.outputs ?? []) {
115
+ const td = namespace.get(out.name);
116
+ outputsAfter[out.name] = serializeTensor(td);
117
+ if (!td && warnings.length === 0)
118
+ warnings.push(`Output "${out.name}" was not produced`);
119
+ }
120
+ const explain = computeExplain(node, namespace, resolved);
121
+ steps.push({ nodeName: node.name, nodeId: `node:${node.name}`, inputs: inputsBefore, outputs: outputsAfter, warnings, durationMs, explain });
122
+ }
123
+ const output = {};
124
+ for (const outSpec of model.outputs ?? []) {
125
+ const td = namespace.get(outSpec.name);
126
+ if (td)
127
+ output[outSpec.name] = td;
128
+ }
129
+ return { output, steps };
130
+ }
131
+ get model() {
132
+ return this.resolved.model;
133
+ }
134
+ }
135
+ // ── Schema feature expansion ──────────────────────────────────────────────────
136
+ // Expands model_schema features with a source+index (e.g. columns of a matrix input X)
137
+ // into individual named tensors in the namespace, so nodes can address them by name.
138
+ function expandSchemaFeatures(model, namespace, N) {
139
+ const schema = model.model_schema;
140
+ if (!schema?.features)
141
+ return;
142
+ for (const f of expandFeatures(schema.features)) {
143
+ if (!f.name || f.source === undefined)
144
+ continue;
145
+ if (namespace.has(f.name))
146
+ continue;
147
+ const src = namespace.get(f.source);
148
+ if (!src)
149
+ continue;
150
+ const cols = tensorCols(src);
151
+ const col = f.index ?? 0; // proto omits default 0, so undefined means column 0
152
+ if (col < 0 || col >= cols)
153
+ continue;
154
+ const colData = new Float64Array(N);
155
+ const data = src.data;
156
+ for (let row = 0; row < N; row++)
157
+ colData[row] = data[row * cols + col];
158
+ namespace.set(f.name, { dtype: 'FLOAT64', shape: [N], data: colData });
159
+ }
160
+ }
161
+ // ── Tensor comparison ─────────────────────────────────────────────────────────
162
+ function compareTensors(actual, expected, atol, rtol) {
163
+ const a = actual.data;
164
+ const e = expected.data;
165
+ if (a.length !== e.length)
166
+ return { pass: false, maxAbsErr: Infinity, maxRelErr: Infinity };
167
+ let maxAbsErr = 0, maxRelErr = 0, pass = true;
168
+ for (let i = 0; i < a.length; i++) {
169
+ const ai = Number(a[i]), ei = Number(e[i]);
170
+ const absErr = Math.abs(ai - ei);
171
+ const relErr = Math.abs(ei) > 0 ? absErr / Math.abs(ei) : absErr;
172
+ if (absErr > maxAbsErr)
173
+ maxAbsErr = absErr;
174
+ if (relErr > maxRelErr)
175
+ maxRelErr = relErr;
176
+ if (absErr > atol + rtol * Math.abs(ei))
177
+ pass = false;
178
+ }
179
+ return { pass, maxAbsErr, maxRelErr };
180
+ }
181
+ // ── Input normalization ───────────────────────────────────────────────────────
182
+ function normalizeInput(val) {
183
+ if (val === null || val === undefined) {
184
+ return { dtype: 'FLOAT64', shape: [1], data: new Float64Array([0]) };
185
+ }
186
+ // Already TensorData — has .data / .shape / .dtype in internal format
187
+ if (typeof val === 'object' && !Array.isArray(val)) {
188
+ const obj = val;
189
+ if ('data' in obj && 'shape' in obj && 'dtype' in obj) {
190
+ const td = obj;
191
+ // data may be a plain number[] if the caller built this from JSON — but preserve string/bool arrays
192
+ if (Array.isArray(td.data)) {
193
+ if (td.dtype === 'STRING' || td.dtype === 'BOOL')
194
+ return td;
195
+ return { dtype: td.dtype, shape: td.shape, data: new Float64Array(td.data) };
196
+ }
197
+ return td;
198
+ }
199
+ // IR Tensor format (float64_data / float32_data / int32_data / …)
200
+ if (obj['float64_data'] || obj['float32_data'] || obj['int32_data'] ||
201
+ obj['int64_data'] || obj['string_data'] || obj['bool_data'] || obj['raw_data']) {
202
+ return tensorToData(obj);
203
+ }
204
+ }
205
+ // Scalar
206
+ if (typeof val === 'number') {
207
+ return { dtype: 'FLOAT64', shape: [1], data: new Float64Array([val]) };
208
+ }
209
+ if (typeof val === 'boolean') {
210
+ return { dtype: 'BOOL', shape: [1], data: [val] };
211
+ }
212
+ if (typeof val === 'string') {
213
+ return { dtype: 'STRING', shape: [1], data: [val] };
214
+ }
215
+ if (Array.isArray(val)) {
216
+ if (val.length === 0)
217
+ return { dtype: 'FLOAT64', shape: [0], data: new Float64Array(0) };
218
+ // 2-D array
219
+ if (Array.isArray(val[0])) {
220
+ const rows = val.length;
221
+ const cols = val[0].length;
222
+ const flat = new Float64Array(rows * cols);
223
+ for (let r = 0; r < rows; r++) {
224
+ const row = val[r];
225
+ for (let c = 0; c < cols; c++)
226
+ flat[r * cols + c] = row[c];
227
+ }
228
+ return { dtype: 'FLOAT64', shape: [rows, cols], data: flat };
229
+ }
230
+ // 1-D array
231
+ const first = val[0];
232
+ if (typeof first === 'string')
233
+ return { dtype: 'STRING', shape: [val.length], data: val };
234
+ if (typeof first === 'boolean')
235
+ return { dtype: 'BOOL', shape: [val.length], data: val };
236
+ return { dtype: 'FLOAT64', shape: [val.length], data: new Float64Array(val) };
237
+ }
238
+ return { dtype: 'FLOAT64', shape: [1], data: new Float64Array([0]) };
239
+ }
240
+ // ── Node execution ────────────────────────────────────────────────────────────
241
+ function executeNodes(nodes, namespace, N, resolved) {
242
+ for (const node of nodes) {
243
+ executeNode(node, namespace, N, resolved);
244
+ }
245
+ }
246
+ function executeNode(node, namespace, N, resolved) {
247
+ if (node.composite) {
248
+ executeComposite(node, namespace, N, resolved);
249
+ return;
250
+ }
251
+ // Build flat slot space from node inputs
252
+ const inputNames = expandNodeInputs(node.inputs ?? []);
253
+ const { flatInputs, numSlots } = buildFlatSlotSpace(inputNames, namespace, N);
254
+ let outputData = null;
255
+ let outputWidth = 1;
256
+ if (node.tree) {
257
+ outputData = executeTree(node.tree, flatInputs, numSlots, N, resolved);
258
+ outputWidth = outputData.length / N;
259
+ }
260
+ else if (node.tree_ensemble) {
261
+ outputData = executeTreeEnsemble(node.tree_ensemble, flatInputs, numSlots, N, resolved);
262
+ outputWidth = outputData.length / N;
263
+ }
264
+ else if (node.linear) {
265
+ const coeffTensor = resolveTensorValue(node.linear.coefficients, resolved.tensorIndex);
266
+ const coeffShape = coeffTensor?.type?.shape ?? [];
267
+ outputWidth = coeffShape.length === 2 ? (coeffShape[0] ?? 1) : 1;
268
+ outputData = executeLinear(node.linear, flatInputs, numSlots, N, resolved);
269
+ }
270
+ else if (node.neural_network) {
271
+ const layers = node.neural_network.layers ?? [];
272
+ const lastLayer = layers.length > 0 ? layers[layers.length - 1] : undefined;
273
+ if (lastLayer) {
274
+ const wTensor = resolveTensorValue(lastLayer.weights, resolved.tensorIndex);
275
+ const wShape = wTensor?.type?.shape ?? [];
276
+ outputWidth = wShape[0] ?? 1;
277
+ }
278
+ outputData = executeNeuralNetwork(node.neural_network, flatInputs, numSlots, N, resolved);
279
+ }
280
+ else if (node.naive_bayes) {
281
+ const priorTensor = resolveTensorValue(node.naive_bayes.class_log_priors, resolved.tensorIndex);
282
+ const priorShape = priorTensor?.type?.shape ?? [];
283
+ outputWidth = priorShape[0] ?? 1;
284
+ outputData = executeNaiveBayes(node.naive_bayes, flatInputs, numSlots, N, resolved);
285
+ }
286
+ else if (node.svm) {
287
+ outputData = executeSVM(node.svm, flatInputs, numSlots, N, resolved);
288
+ outputWidth = outputData.length / N;
289
+ }
290
+ else if (node.clustering) {
291
+ const { labels, distances } = executeClustering(node.clustering, flatInputs, numSlots, N, resolved);
292
+ // Publish cluster label and distance as separate outputs
293
+ publishClusteringOutputs(node, labels, distances, namespace, N);
294
+ return;
295
+ }
296
+ else if (node.op === 'OrdinalEncoder' || node.op === 'ordinal_encoder' ||
297
+ node.op === 'TargetEncoder' || node.op === 'CountEncoder' ||
298
+ node.op === 'WOEEncoder' || node.op === 'JamesSteinEncoder' ||
299
+ node.op === 'MEstimateEncoder' || node.op === 'QuantileEncoder' ||
300
+ node.op === 'CatBoostEncoder' || node.op === 'LeaveOneOutEncoder') {
301
+ executeOrdinalEncoder(node, inputNames, namespace, N);
302
+ return;
303
+ }
304
+ else if (node.op === 'LabelEncoder') {
305
+ executeLabelEncoder(node, inputNames, namespace, N);
306
+ return;
307
+ }
308
+ else if (node.op === 'StandardScaler') {
309
+ executeStandardScaler(node, inputNames, namespace, N);
310
+ return;
311
+ }
312
+ else if (node.op === 'SAMMEVote') {
313
+ executeSAMMEVote(node, inputNames, namespace, N);
314
+ return;
315
+ }
316
+ else if (node.op === 'WeightedMedian') {
317
+ executeWeightedMedian(node, inputNames, namespace, N);
318
+ return;
319
+ }
320
+ else if (node.op === 'Average') {
321
+ executeAverage(node, inputNames, namespace, N);
322
+ return;
323
+ }
324
+ else if (node.op === 'ArgMax') {
325
+ executeArgMax(node, inputNames, namespace, N);
326
+ return;
327
+ }
328
+ else if (node.op === 'SoftVote') {
329
+ executeSoftVote(node, inputNames, namespace, N);
330
+ return;
331
+ }
332
+ else if (node.op === 'MajorityVote') {
333
+ executeMajorityVote(node, inputNames, namespace, N);
334
+ return;
335
+ }
336
+ else if (node.op === 'TakeSlots') {
337
+ executeTakeSlots(node, inputNames, namespace, N);
338
+ return;
339
+ }
340
+ else if (node.op === 'Concat') {
341
+ executeConcat(node, inputNames, namespace, N);
342
+ return;
343
+ }
344
+ else if (node.op === 'Derive') {
345
+ executeDerive(node, inputNames, namespace, N);
346
+ return;
347
+ }
348
+ else if (node.op === 'Binarizer') {
349
+ executeBinarizer(node, inputNames, namespace, N, resolved);
350
+ return;
351
+ }
352
+ else if (node.op === 'MinMaxScaler') {
353
+ executeMinMaxScaler(node, inputNames, namespace, N, resolved);
354
+ return;
355
+ }
356
+ else if (node.op === 'MaxAbsScaler') {
357
+ executeMaxAbsScaler(node, inputNames, namespace, N, resolved);
358
+ return;
359
+ }
360
+ else if (node.op === 'RobustScaler') {
361
+ executeRobustScaler(node, inputNames, namespace, N, resolved);
362
+ return;
363
+ }
364
+ else if (node.op === 'Normalizer') {
365
+ executeNormalizer(node, inputNames, namespace, N);
366
+ return;
367
+ }
368
+ else if (node.op === 'OneHotEncoder') {
369
+ executeOneHotEncoder(node, inputNames, namespace, N, resolved);
370
+ return;
371
+ }
372
+ else if (node.op === 'Bucketizer') {
373
+ executeBucketizer(node, inputNames, namespace, N, resolved);
374
+ return;
375
+ }
376
+ else if (node.op === 'PolynomialFeatures') {
377
+ executePolynomialFeatures(node, inputNames, namespace, N, resolved);
378
+ return;
379
+ }
380
+ else if (node.op === 'PowerTransformer') {
381
+ executePowerTransformer(node, inputNames, namespace, N, resolved);
382
+ return;
383
+ }
384
+ else if (node.op === 'QuantileTransformer') {
385
+ executeQuantileTransformer(node, inputNames, namespace, N, resolved);
386
+ return;
387
+ }
388
+ else if (node.op === 'SplineTransformer') {
389
+ executeSplineTransformer(node, inputNames, namespace, N, resolved);
390
+ return;
391
+ }
392
+ else if (node.op === 'TruncatedSVD') {
393
+ executeTruncatedSVD(node, inputNames, namespace, N, resolved);
394
+ return;
395
+ }
396
+ else if (node.op === 'PCA') {
397
+ executePCA(node, inputNames, namespace, N, resolved);
398
+ return;
399
+ }
400
+ else if (node.op === 'Imputer') {
401
+ executeImputer(node, inputNames, namespace, N, resolved);
402
+ return;
403
+ }
404
+ else if (node.op === 'WeightedSum') {
405
+ executeWeightedSum(node, inputNames, namespace, N, resolved);
406
+ return;
407
+ }
408
+ else if (node.op === 'NormContinuous') {
409
+ executeNormContinuous(node, inputNames, namespace, N, resolved);
410
+ return;
411
+ }
412
+ else if (node.op === 'Tokenizer') {
413
+ executeTokenizer(node, inputNames, namespace, N);
414
+ return;
415
+ }
416
+ else if (node.op === 'RegexTokenizer') {
417
+ executeRegexTokenizer(node, inputNames, namespace, N);
418
+ return;
419
+ }
420
+ else if (node.op === 'NGram') {
421
+ executeNGram(node, inputNames, namespace, N);
422
+ return;
423
+ }
424
+ else if (node.op === 'StopWordsRemover') {
425
+ executeStopWordsRemover(node, inputNames, namespace, N, resolved);
426
+ return;
427
+ }
428
+ else if (node.op === 'CountVectorizer') {
429
+ executeCountVectorizer(node, inputNames, namespace, N, resolved);
430
+ return;
431
+ }
432
+ else if (node.op === 'HashingVectorizer') {
433
+ executeHashingVectorizer(node, inputNames, namespace, N);
434
+ return;
435
+ }
436
+ else if (node.op === 'TfIdfTransformer') {
437
+ executeTfIdfTransformer(node, inputNames, namespace, N, resolved);
438
+ return;
439
+ }
440
+ else if (node.op === 'Word2Vec') {
441
+ executeWord2Vec(node, inputNames, namespace, N, resolved);
442
+ return;
443
+ }
444
+ else if (node.op === 'KNN') {
445
+ executeKNN(node, inputNames, namespace, N, resolved);
446
+ return;
447
+ }
448
+ else {
449
+ // Generic operator — pass through first input as-is (placeholder)
450
+ if (inputNames.length > 0) {
451
+ const td = namespace.get(inputNames[0]);
452
+ for (const out of node.outputs ?? []) {
453
+ if (td)
454
+ namespace.set(out.name, td);
455
+ }
456
+ }
457
+ return;
458
+ }
459
+ if (outputData) {
460
+ publishNumericOutputs(node, outputData, outputWidth, N, namespace);
461
+ }
462
+ }
463
+ // ── Composite node execution ──────────────────────────────────────────────────
464
+ function executeComposite(node, parentNamespace, N, resolved) {
465
+ const composite = node.composite;
466
+ const localNS = new Map();
467
+ // Inherit inputs from parent namespace
468
+ const inputNames = expandNodeInputs(node.inputs ?? []);
469
+ for (const name of inputNames) {
470
+ const td = parentNamespace.get(name);
471
+ if (td)
472
+ localNS.set(name, td);
473
+ }
474
+ // Apply input aliases
475
+ for (const alias of composite.input_aliases ?? []) {
476
+ const td = localNS.get(alias.from_name);
477
+ if (td) {
478
+ localNS.delete(alias.from_name);
479
+ localNS.set(alias.to_name, td);
480
+ }
481
+ }
482
+ // Expand schema features into the local namespace so that name-keyed TakeSlots
483
+ // nodes (e.g. from ColumnTransformer) can resolve individual column names.
484
+ expandSchemaFeatures(resolved.model, localNS, N);
485
+ // Execute internal nodes
486
+ executeNodes(composite.nodes ?? [], localNS, N, resolved);
487
+ // Publish outputs back to parent
488
+ for (const out of node.outputs ?? []) {
489
+ let sourceName = out.name;
490
+ for (const alias of composite.output_aliases ?? []) {
491
+ if (alias.to_name === out.name) {
492
+ sourceName = alias.from_name;
493
+ break;
494
+ }
495
+ }
496
+ const td = localNS.get(sourceName);
497
+ if (td)
498
+ parentNamespace.set(out.name, td);
499
+ }
500
+ }
501
+ // ── OrdinalEncoder ────────────────────────────────────────────────────────────
502
+ function executeOrdinalEncoder(node, inputNames, namespace, N) {
503
+ const attrs = node.attributes ?? [];
504
+ const catAttr = attrs.find(a => a.name === 'categories');
505
+ const offAttr = attrs.find(a => a.name === 'category_offsets');
506
+ if (!catAttr?.tensor)
507
+ return;
508
+ const categories = tensorToData(catAttr.tensor).data;
509
+ // When offsets are absent (e.g. Spark StringIndexer single-feature), default to one feature spanning all categories.
510
+ const offsets = offAttr?.tensor
511
+ ? Array.from(tensorToData(offAttr.tensor).data)
512
+ : [0, categories.length];
513
+ // Optional: target-encoded float values per category (e.g. CatBoost encoder)
514
+ const encAttr = attrs.find(a => a.name === 'encoded_values');
515
+ const defAttr = attrs.find(a => a.name === 'default_values');
516
+ const encodedValues = encAttr?.tensor ? tensorToData(encAttr.tensor).data : null;
517
+ const defaultValues = defAttr?.tensor ? tensorToData(defAttr.tensor).data : null;
518
+ // numFeatures comes from offsets when a single matrix input is provided,
519
+ // or from the number of named inputs when each feature is a separate column.
520
+ const numFeatures = inputNames.length > 1
521
+ ? inputNames.length
522
+ : (offsets.length > 1 ? offsets.length - 1 : 1);
523
+ const out = new Float64Array(N * numFeatures);
524
+ // Single matrix input: all features share one TensorData with column stride numFeatures.
525
+ const singleTd = inputNames.length === 1 ? namespace.get(inputNames[0]) : undefined;
526
+ const singleData = singleTd?.data ?? null;
527
+ const isNumericInput = singleData instanceof Float32Array || singleData instanceof Float64Array;
528
+ for (let fi = 0; fi < numFeatures; fi++) {
529
+ const start = offsets[fi] ?? 0;
530
+ const end = offsets[fi + 1] ?? categories.length;
531
+ if (encodedValues) {
532
+ const catStrMap = new Map();
533
+ const catNumMap = new Map();
534
+ for (let ci = start; ci < end; ci++) {
535
+ catStrMap.set(categories[ci], encodedValues[ci]);
536
+ const n = Number(categories[ci]);
537
+ if (!isNaN(n))
538
+ catNumMap.set(n, encodedValues[ci]);
539
+ }
540
+ const defaultVal = defaultValues?.[fi] ?? NaN;
541
+ for (let row = 0; row < N; row++) {
542
+ const raw = singleData ? singleData[row * numFeatures + fi]
543
+ : namespace.get(inputNames[fi])?.data?.[row];
544
+ const v = isNumericInput
545
+ ? (catNumMap.get(raw) ?? defaultVal)
546
+ : (catStrMap.get(String(raw)) ?? defaultVal);
547
+ out[row * numFeatures + fi] = v;
548
+ }
549
+ }
550
+ else {
551
+ const catStrMap = new Map();
552
+ const catNumMap = new Map();
553
+ for (let ci = start; ci < end; ci++) {
554
+ catStrMap.set(categories[ci], ci - start);
555
+ const n = Number(categories[ci]);
556
+ if (!isNaN(n))
557
+ catNumMap.set(n, ci - start);
558
+ }
559
+ for (let row = 0; row < N; row++) {
560
+ const raw = singleData ? singleData[row * numFeatures + fi]
561
+ : namespace.get(inputNames[fi])?.data?.[row];
562
+ const v = isNumericInput
563
+ ? (catNumMap.get(raw) ?? NaN)
564
+ : (catStrMap.get(String(raw)) ?? NaN);
565
+ out[row * numFeatures + fi] = v;
566
+ }
567
+ }
568
+ }
569
+ const outputs = node.outputs ?? [];
570
+ if (outputs.length > 0) {
571
+ namespace.set(outputs[0].name, { dtype: 'FLOAT64', shape: [N, numFeatures], data: out });
572
+ }
573
+ }
574
+ // ── LabelEncoder ─────────────────────────────────────────────────────────────
575
+ function executeLabelEncoder(node, inputNames, namespace, N) {
576
+ const attrs = node.attributes ?? [];
577
+ const labelsAttr = attrs.find(a => a.name === 'labels');
578
+ const offsetsAttr = attrs.find(a => a.name === 'label_offsets');
579
+ if (!labelsAttr?.tensor || !offsetsAttr?.tensor)
580
+ return;
581
+ const labels = tensorToData(labelsAttr.tensor).data;
582
+ const offsets = Array.from(tensorToData(offsetsAttr.tensor).data);
583
+ const numFeatures = inputNames.length;
584
+ const outputs = node.outputs ?? [];
585
+ const onePerOutput = outputs.length >= numFeatures;
586
+ const combined = onePerOutput ? null : new Float64Array(N * numFeatures);
587
+ for (let fi = 0; fi < numFeatures; fi++) {
588
+ const td = namespace.get(inputNames[fi]);
589
+ const start = offsets[fi] ?? 0;
590
+ const end = offsets[fi + 1] ?? labels.length;
591
+ const labelMap = new Map();
592
+ for (let ci = start; ci < end; ci++)
593
+ labelMap.set(labels[ci], ci - start);
594
+ const strData = (td?.data ?? []);
595
+ if (onePerOutput) {
596
+ const col = new Float64Array(N);
597
+ for (let row = 0; row < N; row++)
598
+ col[row] = labelMap.get(strData[row]) ?? NaN;
599
+ namespace.set(outputs[fi].name, { dtype: 'FLOAT64', shape: [N], data: col });
600
+ }
601
+ else {
602
+ for (let row = 0; row < N; row++) {
603
+ combined[row * numFeatures + fi] = labelMap.get(strData[row]) ?? NaN;
604
+ }
605
+ }
606
+ }
607
+ if (!onePerOutput && outputs.length > 0) {
608
+ namespace.set(outputs[0].name, { dtype: 'FLOAT64', shape: [N, numFeatures], data: combined });
609
+ }
610
+ }
611
+ // ── StandardScaler ────────────────────────────────────────────────────────────
612
+ function executeStandardScaler(node, inputNames, namespace, N) {
613
+ const attrs = node.attributes ?? [];
614
+ const meanAttr = attrs.find(a => a.name === 'mean');
615
+ const scaleAttr = attrs.find(a => a.name === 'scale');
616
+ if (!meanAttr?.tensor && !scaleAttr?.tensor)
617
+ return;
618
+ const meanRaw = meanAttr?.tensor ? tensorToData(meanAttr.tensor).data : null;
619
+ const scaleRaw = scaleAttr?.tensor ? tensorToData(scaleAttr.tensor).data : null;
620
+ // Collect all input tensors as a combined matrix [N, totalCols]
621
+ let totalCols = 0;
622
+ for (const name of inputNames) {
623
+ const td = namespace.get(name);
624
+ totalCols += td ? tensorCols(td) : 0;
625
+ }
626
+ const out = new Float64Array(N * totalCols);
627
+ let colOffset = 0;
628
+ for (const name of inputNames) {
629
+ const td = namespace.get(name);
630
+ if (!td)
631
+ continue;
632
+ const cols = tensorCols(td);
633
+ const data = td.data;
634
+ for (let row = 0; row < N; row++) {
635
+ for (let c = 0; c < cols; c++) {
636
+ const globalCol = colOffset + c;
637
+ const mu = meanRaw?.[globalCol] ?? 0;
638
+ const sigma = scaleRaw?.[globalCol] ?? 1;
639
+ out[row * totalCols + globalCol] = (data[row * cols + c] - mu) / sigma;
640
+ }
641
+ }
642
+ colOffset += cols;
643
+ }
644
+ const outputs = node.outputs ?? [];
645
+ if (outputs.length > 0) {
646
+ namespace.set(outputs[0].name, { dtype: 'FLOAT64', shape: [N, totalCols], data: out });
647
+ }
648
+ }
649
+ // ── SAMMEVote ─────────────────────────────────────────────────────────────────
650
+ function executeSAMMEVote(node, inputNames, namespace, N) {
651
+ const attrs = node.attributes ?? [];
652
+ const weightsAttr = attrs.find(a => a.name === 'weights');
653
+ const nClassesAttr = attrs.find(a => a.name === 'n_classes');
654
+ const weights = weightsAttr?.float64s ?? [];
655
+ const K = nClassesAttr?.i ?? 2;
656
+ // SAMME discrete: each estimator contributes +alpha to predicted class, -alpha/(K-1) to others.
657
+ // Then scores /= total_weight, prob = exp(score / (K-1)), normalize rows.
658
+ const totalWeight = weights.reduce((s, w) => s + w, 0) || 1;
659
+ const scores = new Float64Array(N * K);
660
+ for (let t = 0; t < inputNames.length; t++) {
661
+ const alpha = weights[t] ?? 1;
662
+ const td = namespace.get(inputNames[t]);
663
+ if (!td)
664
+ continue;
665
+ const preds = td.data;
666
+ for (let row = 0; row < N; row++) {
667
+ const cls = Math.round(preds[row]);
668
+ for (let k = 0; k < K; k++) {
669
+ scores[row * K + k] += k === cls ? alpha : -alpha / (K - 1);
670
+ }
671
+ }
672
+ }
673
+ for (let i = 0; i < scores.length; i++)
674
+ scores[i] /= totalWeight;
675
+ const factor = 1.0 / (K - 1);
676
+ const prob = new Float64Array(N * K);
677
+ for (let row = 0; row < N; row++) {
678
+ let sum = 0;
679
+ for (let k = 0; k < K; k++) {
680
+ prob[row * K + k] = Math.exp(factor * scores[row * K + k]);
681
+ sum += prob[row * K + k];
682
+ }
683
+ for (let k = 0; k < K; k++)
684
+ prob[row * K + k] /= sum;
685
+ }
686
+ const nClasses = K;
687
+ // Argmax for prediction
688
+ const pred = new Float64Array(N);
689
+ for (let row = 0; row < N; row++) {
690
+ let maxVal = -Infinity, maxIdx = 0;
691
+ for (let k = 0; k < nClasses; k++) {
692
+ if (prob[row * nClasses + k] > maxVal) {
693
+ maxVal = prob[row * nClasses + k];
694
+ maxIdx = k;
695
+ }
696
+ }
697
+ pred[row] = maxIdx;
698
+ }
699
+ const outputs = node.outputs ?? [];
700
+ for (const out of outputs) {
701
+ if (out.role === 'PROBABILITY') {
702
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N, nClasses], data: prob });
703
+ }
704
+ else if (out.role === 'PREDICTION') {
705
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N], data: pred });
706
+ }
707
+ }
708
+ }
709
+ // ── WeightedMedian ────────────────────────────────────────────────────────────
710
+ function executeWeightedMedian(node, inputNames, namespace, N) {
711
+ const attrs = node.attributes ?? [];
712
+ const weightsAttr = attrs.find(a => a.name === 'weights');
713
+ const weights = weightsAttr?.float64s ?? [];
714
+ const pred = new Float64Array(N);
715
+ for (let row = 0; row < N; row++) {
716
+ // Collect (value, weight) pairs
717
+ const pairs = [];
718
+ for (let t = 0; t < inputNames.length; t++) {
719
+ const td = namespace.get(inputNames[t]);
720
+ if (!td)
721
+ continue;
722
+ const v = td.data[row];
723
+ const w = weights[t] ?? 1;
724
+ pairs.push({ v, w });
725
+ }
726
+ // Sort by value
727
+ pairs.sort((a, b) => a.v - b.v);
728
+ // Find weighted median: smallest index where cumulative weight >= totalWeight/2
729
+ const totalWeight = pairs.reduce((s, p) => s + p.w, 0);
730
+ let cumW = 0;
731
+ let medianVal = pairs[0]?.v ?? 0;
732
+ for (const { v, w } of pairs) {
733
+ cumW += w;
734
+ if (cumW >= totalWeight / 2) {
735
+ medianVal = v;
736
+ break;
737
+ }
738
+ }
739
+ pred[row] = medianVal;
740
+ }
741
+ for (const out of node.outputs ?? []) {
742
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N], data: pred });
743
+ }
744
+ }
745
+ // ── Average ───────────────────────────────────────────────────────────────────
746
+ function executeAverage(node, inputNames, namespace, N) {
747
+ const inputs = inputNames.map(n => namespace.get(n)).filter(Boolean);
748
+ if (inputs.length === 0)
749
+ return;
750
+ const width = inputs[0].shape.length >= 2 ? (inputs[0].shape[inputs[0].shape.length - 1] ?? 1) : 1;
751
+ const avg = new Float64Array(N * width);
752
+ for (const td of inputs) {
753
+ const d = td.data;
754
+ for (let i = 0; i < avg.length; i++)
755
+ avg[i] += d[i] ?? 0;
756
+ }
757
+ for (let i = 0; i < avg.length; i++)
758
+ avg[i] /= inputs.length;
759
+ const outputs = node.outputs ?? [];
760
+ for (const out of outputs) {
761
+ namespace.set(out.name, {
762
+ dtype: 'FLOAT64',
763
+ shape: width > 1 ? [N, width] : [N],
764
+ data: avg,
765
+ });
766
+ }
767
+ }
768
+ // ── ArgMax ────────────────────────────────────────────────────────────────────
769
+ function executeArgMax(node, inputNames, namespace, N) {
770
+ const td = namespace.get(inputNames[0]);
771
+ if (!td)
772
+ return;
773
+ const src = td.data;
774
+ const cols = td.shape.length >= 2 ? td.shape[td.shape.length - 1] : 1;
775
+ const out = new Float64Array(N);
776
+ for (let row = 0; row < N; row++) {
777
+ let maxVal = -Infinity, maxIdx = 0;
778
+ for (let c = 0; c < cols; c++) {
779
+ const v = src[row * cols + c];
780
+ if (v > maxVal) {
781
+ maxVal = v;
782
+ maxIdx = c;
783
+ }
784
+ }
785
+ out[row] = maxIdx;
786
+ }
787
+ const outName = node.outputs?.[0]?.name;
788
+ if (outName)
789
+ namespace.set(outName, { dtype: 'FLOAT64', shape: [N], data: out });
790
+ }
791
+ // ── SoftVote ──────────────────────────────────────────────────────────────────
792
+ function executeSoftVote(node, inputNames, namespace, N) {
793
+ const normalizeRows = node.attributes?.find(a => a.name === 'normalize_rows')?.b ?? false;
794
+ const inputs = inputNames.map(n => namespace.get(n)).filter(Boolean);
795
+ if (inputs.length === 0)
796
+ return;
797
+ const colsPerInput = inputs.map(td => (td.shape.length >= 2 ? td.shape[td.shape.length - 1] : 1));
798
+ const allSameWidth = colsPerInput.every(c => c === colsPerInput[0]);
799
+ let prob;
800
+ let cols;
801
+ if (normalizeRows) {
802
+ // OvR multiclass: stack single-column inputs into N×K, then normalize each row.
803
+ cols = colsPerInput.reduce((a, b) => a + b, 0);
804
+ const raw = new Float64Array(N * cols);
805
+ let colOffset = 0;
806
+ for (let ii = 0; ii < inputs.length; ii++) {
807
+ const k = colsPerInput[ii];
808
+ const src = inputs[ii].data;
809
+ for (let row = 0; row < N; row++) {
810
+ for (let c = 0; c < k; c++)
811
+ raw[row * cols + colOffset + c] = src[row * k + c];
812
+ }
813
+ colOffset += k;
814
+ }
815
+ prob = new Float64Array(N * cols);
816
+ for (let row = 0; row < N; row++) {
817
+ let sum = 0;
818
+ for (let k = 0; k < cols; k++)
819
+ sum += raw[row * cols + k];
820
+ for (let k = 0; k < cols; k++) {
821
+ prob[row * cols + k] = sum > 0 ? raw[row * cols + k] / sum : 1 / cols;
822
+ }
823
+ }
824
+ }
825
+ else if (allSameWidth && inputs.length > 1) {
826
+ // VotingClassifier soft: average per-class probabilities across all classifiers.
827
+ cols = colsPerInput[0];
828
+ prob = new Float64Array(N * cols);
829
+ for (const td of inputs) {
830
+ const src = td.data;
831
+ for (let i = 0; i < N * cols; i++)
832
+ prob[i] += src[i];
833
+ }
834
+ for (let i = 0; i < prob.length; i++)
835
+ prob[i] /= inputs.length;
836
+ }
837
+ else {
838
+ // Single input (or unequal widths): concatenate and pass through.
839
+ cols = colsPerInput.reduce((a, b) => a + b, 0);
840
+ prob = new Float64Array(N * cols);
841
+ let colOffset = 0;
842
+ for (let ii = 0; ii < inputs.length; ii++) {
843
+ const k = colsPerInput[ii];
844
+ const src = inputs[ii].data;
845
+ for (let row = 0; row < N; row++) {
846
+ for (let c = 0; c < k; c++)
847
+ prob[row * cols + colOffset + c] = src[row * k + c];
848
+ }
849
+ colOffset += k;
850
+ }
851
+ }
852
+ const pred = new Float64Array(N);
853
+ for (let row = 0; row < N; row++) {
854
+ let maxVal = -Infinity, maxIdx = 0;
855
+ for (let k = 0; k < cols; k++) {
856
+ if (prob[row * cols + k] > maxVal) {
857
+ maxVal = prob[row * cols + k];
858
+ maxIdx = k;
859
+ }
860
+ }
861
+ pred[row] = maxIdx;
862
+ }
863
+ for (const out of node.outputs ?? []) {
864
+ if (out.role === 'PROBABILITY') {
865
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N, cols], data: prob });
866
+ }
867
+ else if (out.role === 'PREDICTION') {
868
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N], data: pred });
869
+ }
870
+ }
871
+ }
872
+ // ── MajorityVote ──────────────────────────────────────────────────────────────
873
+ function executeMajorityVote(node, inputNames, namespace, N) {
874
+ const pred = new Float64Array(N);
875
+ for (let row = 0; row < N; row++) {
876
+ const counts = new Map();
877
+ for (const name of inputNames) {
878
+ const td = namespace.get(name);
879
+ if (!td)
880
+ continue;
881
+ const label = Math.round(td.data[row]);
882
+ counts.set(label, (counts.get(label) ?? 0) + 1);
883
+ }
884
+ let bestLabel = 0, bestCount = 0;
885
+ for (const [label, count] of counts) {
886
+ if (count > bestCount || (count === bestCount && label < bestLabel)) {
887
+ bestCount = count;
888
+ bestLabel = label;
889
+ }
890
+ }
891
+ pred[row] = bestLabel;
892
+ }
893
+ for (const out of node.outputs ?? []) {
894
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N], data: pred });
895
+ }
896
+ }
897
+ // ── KNN ───────────────────────────────────────────────────────────────────────
898
+ function executeKNN(node, inputNames, namespace, N, resolved) {
899
+ const attrs = node.attributes ?? [];
900
+ function attr(name) { return attrs.find(a => a.name === name); }
901
+ function attrTd(name) {
902
+ const a = attr(name);
903
+ if (!a)
904
+ return null;
905
+ if (a.tensor)
906
+ return tensorToData(a.tensor);
907
+ if (a.tensor_ref) {
908
+ const entry = resolved.tensorIndex.get(a.tensor_ref.id);
909
+ if (entry?.dense)
910
+ return tensorToData(entry.dense);
911
+ }
912
+ return null;
913
+ }
914
+ const trainFeatTd = attrTd('train_features');
915
+ const trainTargTd = attrTd('train_targets');
916
+ if (!trainFeatTd || !trainTargTd)
917
+ return;
918
+ const task = attr('task')?.s ?? 'regression';
919
+ const weights = attr('weights')?.s ?? 'uniform';
920
+ const neighborMode = attr('neighbor_mode')?.s ?? 'knn';
921
+ const metric = attr('metric')?.s ?? 'minkowski';
922
+ const radius = attr('radius')?.f64 ?? 1.0;
923
+ const k = attr('n_neighbors')?.i ?? 5;
924
+ const nClasses = attr('n_classes')?.i ?? 2;
925
+ const outlierTd = attrTd('outlier_label');
926
+ const outlierLabel = outlierTd ? Number(outlierTd.data[0]) : 0;
927
+ // Minkowski p from metric_param_values (default p=2 = Euclidean).
928
+ // Well-known named metrics override the param: manhattan=1, euclidean=2, chebyshev=Inf.
929
+ const paramValsTd = attrTd('metric_param_values');
930
+ const metricP = paramValsTd ? Number(paramValsTd.data[0]) : 2;
931
+ const p = metric === 'manhattan' ? 1
932
+ : metric === 'euclidean' ? 2
933
+ : metric === 'chebyshev' ? Infinity
934
+ : metricP;
935
+ const nTrain = trainFeatTd.shape[0];
936
+ const nFeat = trainFeatTd.shape.length >= 2 ? trainFeatTd.shape[1] : 1;
937
+ const trainX = trainFeatTd.data;
938
+ const trainY = trainTargTd.data;
939
+ // Merge query inputs into a single [N, nFeat] matrix
940
+ const queryTd = namespace.get(inputNames[0]);
941
+ if (!queryTd)
942
+ return;
943
+ const queryX = queryTd.data;
944
+ function minkowski(ai, bi) {
945
+ if (p === Infinity) {
946
+ let maxDist = 0;
947
+ for (let f = 0; f < nFeat; f++) {
948
+ const d = Math.abs(queryX[ai * nFeat + f] - trainX[bi * nFeat + f]);
949
+ if (d > maxDist)
950
+ maxDist = d;
951
+ }
952
+ return maxDist;
953
+ }
954
+ let sum = 0;
955
+ for (let f = 0; f < nFeat; f++) {
956
+ sum += Math.abs(queryX[ai * nFeat + f] - trainX[bi * nFeat + f]) ** p;
957
+ }
958
+ return sum ** (1 / p);
959
+ }
960
+ const isRadius = neighborMode === 'radius';
961
+ const isClassify = task === 'classification';
962
+ const predOut = new Float64Array(N);
963
+ const probOut = isClassify ? new Float64Array(N * nClasses) : null;
964
+ for (let row = 0; row < N; row++) {
965
+ // Compute distances to all training points
966
+ const dists = [];
967
+ for (let t = 0; t < nTrain; t++) {
968
+ const d = minkowski(row, t);
969
+ if (isRadius ? d <= radius : true)
970
+ dists.push({ d, idx: t });
971
+ }
972
+ if (!isRadius) {
973
+ // k-NN: keep only k nearest
974
+ dists.sort((a, b) => a.d - b.d);
975
+ dists.splice(k);
976
+ }
977
+ if (dists.length === 0) {
978
+ // Outlier: no neighbors within radius
979
+ predOut[row] = outlierLabel;
980
+ if (probOut) {
981
+ const cls = Math.round(outlierLabel);
982
+ if (cls >= 0 && cls < nClasses)
983
+ probOut[row * nClasses + cls] = 1;
984
+ else
985
+ probOut[row * nClasses] = 1;
986
+ }
987
+ continue;
988
+ }
989
+ if (isClassify) {
990
+ const votes = new Float64Array(nClasses);
991
+ for (const { d, idx } of dists) {
992
+ const cls = Math.round(trainY[idx]);
993
+ if (cls < 0 || cls >= nClasses)
994
+ continue;
995
+ const w = (weights === 'distance') ? (d === 0 ? 1e12 : 1 / d) : 1;
996
+ votes[cls] += w;
997
+ }
998
+ const total = votes.reduce((a, b) => a + b, 0);
999
+ let maxV = -Infinity, maxC = 0;
1000
+ for (let c = 0; c < nClasses; c++) {
1001
+ if (probOut)
1002
+ probOut[row * nClasses + c] = total > 0 ? votes[c] / total : 1 / nClasses;
1003
+ if (votes[c] > maxV) {
1004
+ maxV = votes[c];
1005
+ maxC = c;
1006
+ }
1007
+ }
1008
+ predOut[row] = maxC;
1009
+ }
1010
+ else {
1011
+ let num = 0, den = 0;
1012
+ for (const { d, idx } of dists) {
1013
+ const w = (weights === 'distance') ? (d === 0 ? 1e12 : 1 / d) : 1;
1014
+ num += w * trainY[idx];
1015
+ den += w;
1016
+ }
1017
+ predOut[row] = den > 0 ? num / den : 0;
1018
+ }
1019
+ }
1020
+ for (const out of node.outputs ?? []) {
1021
+ if (out.role === 'PROBABILITY' && probOut) {
1022
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N, nClasses], data: probOut });
1023
+ }
1024
+ else if (out.role === 'PREDICTION' || out.role === 'SCORE' || !out.role) {
1025
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N], data: predOut });
1026
+ }
1027
+ }
1028
+ }
1029
+ // ── Slot space builder ────────────────────────────────────────────────────────
1030
+ function buildFlatSlotSpace(inputNames, namespace, N) {
1031
+ // Count total slots
1032
+ let numSlots = 0;
1033
+ for (const name of inputNames) {
1034
+ const td = namespace.get(name);
1035
+ numSlots += td ? tensorCols(td) : 0;
1036
+ }
1037
+ const flatInputs = new Float64Array(N * numSlots);
1038
+ let slotOffset = 0;
1039
+ for (const name of inputNames) {
1040
+ const td = namespace.get(name);
1041
+ if (!td)
1042
+ continue;
1043
+ const cols = tensorCols(td);
1044
+ const data = td.data;
1045
+ for (let row = 0; row < N; row++) {
1046
+ for (let col = 0; col < cols; col++) {
1047
+ flatInputs[row * numSlots + slotOffset + col] = data[row * cols + col];
1048
+ }
1049
+ }
1050
+ slotOffset += cols;
1051
+ }
1052
+ return { flatInputs, numSlots };
1053
+ }
1054
+ // ── Output publishing ─────────────────────────────────────────────────────────
1055
+ function publishNumericOutputs(node, outputData, outputWidth, N, namespace) {
1056
+ const outputs = node.outputs ?? [];
1057
+ if (outputs.length === 1) {
1058
+ // Expand binary scalar probability to [N, 2] so downstream TakeSlots/ArgMax work correctly.
1059
+ if (outputs[0].role === 'PROBABILITY' && outputWidth === 1) {
1060
+ const expanded = new Float64Array(N * 2);
1061
+ for (let row = 0; row < N; row++) {
1062
+ expanded[row * 2] = 1 - outputData[row];
1063
+ expanded[row * 2 + 1] = outputData[row];
1064
+ }
1065
+ namespace.set(outputs[0].name, { dtype: 'FLOAT64', shape: [N, 2], data: expanded });
1066
+ return;
1067
+ }
1068
+ namespace.set(outputs[0].name, {
1069
+ dtype: 'FLOAT64',
1070
+ shape: outputWidth > 1 ? [N, outputWidth] : [N],
1071
+ data: outputData,
1072
+ });
1073
+ return;
1074
+ }
1075
+ // When a PROBABILITY output is present, outputData contains post-transform
1076
+ // probabilities. PREDICTION must be derived (argmax or 0.5 threshold).
1077
+ const hasProbOutput = outputs.some(o => o.role === 'PROBABILITY');
1078
+ // Binary classification: scalar probability → expand to [1-p, p] two-column output
1079
+ const isBinaryScalar = hasProbOutput && outputWidth === 1;
1080
+ let probData = outputData;
1081
+ let probWidth = outputWidth;
1082
+ if (isBinaryScalar) {
1083
+ probData = new Float64Array(N * 2);
1084
+ for (let row = 0; row < N; row++) {
1085
+ probData[row * 2] = 1 - outputData[row];
1086
+ probData[row * 2 + 1] = outputData[row];
1087
+ }
1088
+ probWidth = 2;
1089
+ }
1090
+ for (const out of outputs) {
1091
+ if (out.role === 'PREDICTION' && hasProbOutput) {
1092
+ const pred = new Float64Array(N);
1093
+ for (let row = 0; row < N; row++) {
1094
+ let maxVal = -Infinity, maxIdx = 0;
1095
+ for (let k = 0; k < probWidth; k++) {
1096
+ const v = probData[row * probWidth + k];
1097
+ if (v > maxVal) {
1098
+ maxVal = v;
1099
+ maxIdx = k;
1100
+ }
1101
+ }
1102
+ pred[row] = maxIdx;
1103
+ }
1104
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N], data: pred });
1105
+ }
1106
+ else if (out.role === 'PROBABILITY') {
1107
+ namespace.set(out.name, {
1108
+ dtype: 'FLOAT64',
1109
+ shape: [N, probWidth],
1110
+ data: probData,
1111
+ });
1112
+ }
1113
+ else {
1114
+ // SCORE / CONFIDENCE / no role: pass raw output as-is
1115
+ namespace.set(out.name, {
1116
+ dtype: 'FLOAT64',
1117
+ shape: outputWidth > 1 ? [N, outputWidth] : [N],
1118
+ data: outputData,
1119
+ });
1120
+ }
1121
+ }
1122
+ }
1123
+ function publishClusteringOutputs(node, labels, distances, namespace, N) {
1124
+ const outputs = node.outputs ?? [];
1125
+ for (const out of outputs) {
1126
+ const role = out.role;
1127
+ if (role === 'PREDICTION' || outputs.indexOf(out) === 0) {
1128
+ const data = new Float64Array(labels.length);
1129
+ for (let i = 0; i < labels.length; i++)
1130
+ data[i] = labels[i];
1131
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N], data });
1132
+ }
1133
+ else {
1134
+ namespace.set(out.name, { dtype: 'FLOAT64', shape: [N], data: distances });
1135
+ }
1136
+ }
1137
+ }
1138
+ // ── Helpers ───────────────────────────────────────────────────────────────────
1139
+ function serializeTensor(td, max = 50) {
1140
+ if (!td)
1141
+ return { dtype: 'FLOAT64', shape: [], data: null };
1142
+ const raw = td.data;
1143
+ let data;
1144
+ if (raw instanceof Float64Array) {
1145
+ data = Array.from(raw).slice(0, max);
1146
+ }
1147
+ else {
1148
+ data = raw.slice(0, max);
1149
+ }
1150
+ return { dtype: td.dtype, shape: td.shape, data };
1151
+ }
1152
+ function inferBatchSize(namespace) {
1153
+ for (const td of namespace.values()) {
1154
+ if (td.shape.length > 0 && td.shape[0] > 0)
1155
+ return td.shape[0];
1156
+ }
1157
+ return 1;
1158
+ }
1159
+ export { resolve };
1160
+ //# sourceMappingURL=executor.js.map