@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.
- package/LICENSE +201 -0
- package/README.md +172 -0
- package/dist/engine/clustering.d.ts +8 -0
- package/dist/engine/clustering.d.ts.map +1 -0
- package/dist/engine/clustering.js +161 -0
- package/dist/engine/clustering.js.map +1 -0
- package/dist/engine/executor.d.ts +57 -0
- package/dist/engine/executor.d.ts.map +1 -0
- package/dist/engine/executor.js +1160 -0
- package/dist/engine/executor.js.map +1 -0
- package/dist/engine/explain.d.ts +68 -0
- package/dist/engine/explain.d.ts.map +1 -0
- package/dist/engine/explain.js +401 -0
- package/dist/engine/explain.js.map +1 -0
- package/dist/engine/linear.d.ts +5 -0
- package/dist/engine/linear.d.ts.map +1 -0
- package/dist/engine/linear.js +37 -0
- package/dist/engine/linear.js.map +1 -0
- package/dist/engine/naive_bayes.d.ts +5 -0
- package/dist/engine/naive_bayes.d.ts.map +1 -0
- package/dist/engine/naive_bayes.js +102 -0
- package/dist/engine/naive_bayes.js.map +1 -0
- package/dist/engine/nn.d.ts +5 -0
- package/dist/engine/nn.d.ts.map +1 -0
- package/dist/engine/nn.js +34 -0
- package/dist/engine/nn.js.map +1 -0
- package/dist/engine/ops.d.ts +14 -0
- package/dist/engine/ops.d.ts.map +1 -0
- package/dist/engine/ops.js +244 -0
- package/dist/engine/ops.js.map +1 -0
- package/dist/engine/predicates.d.ts +5 -0
- package/dist/engine/predicates.d.ts.map +1 -0
- package/dist/engine/predicates.js +105 -0
- package/dist/engine/predicates.js.map +1 -0
- package/dist/engine/preprocess.d.ts +31 -0
- package/dist/engine/preprocess.d.ts.map +1 -0
- package/dist/engine/preprocess.js +1112 -0
- package/dist/engine/preprocess.js.map +1 -0
- package/dist/engine/svm.d.ts +5 -0
- package/dist/engine/svm.d.ts.map +1 -0
- package/dist/engine/svm.js +241 -0
- package/dist/engine/svm.js.map +1 -0
- package/dist/engine/tree.d.ts +6 -0
- package/dist/engine/tree.d.ts.map +1 -0
- package/dist/engine/tree.js +274 -0
- package/dist/engine/tree.js.map +1 -0
- package/dist/engine/validate_inputs.d.ts +22 -0
- package/dist/engine/validate_inputs.d.ts.map +1 -0
- package/dist/engine/validate_inputs.js +225 -0
- package/dist/engine/validate_inputs.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/io.d.ts +9 -0
- package/dist/io.d.ts.map +1 -0
- package/dist/io.js +93 -0
- package/dist/io.js.map +1 -0
- package/dist/ir.d.ts +489 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +41 -0
- package/dist/ir.js.map +1 -0
- package/dist/resolve.d.ts +20 -0
- package/dist/resolve.d.ts.map +1 -0
- package/dist/resolve.js +138 -0
- package/dist/resolve.js.map +1 -0
- package/dist/validate.d.ts +15 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +332 -0
- package/dist/validate.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,1112 @@
|
|
|
1
|
+
// Preprocessing operator implementations for omle.feature and omle.core ops.
|
|
2
|
+
import { tensorToData, tensorCols } from './ops.js';
|
|
3
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
4
|
+
function getAttr(node, name) {
|
|
5
|
+
return (node.attributes ?? []).find(a => a.name === name);
|
|
6
|
+
}
|
|
7
|
+
function getTensorData(attr, resolved) {
|
|
8
|
+
if (!attr)
|
|
9
|
+
return null;
|
|
10
|
+
if (attr.tensor)
|
|
11
|
+
return tensorToData(attr.tensor);
|
|
12
|
+
if (attr.tensor_ref) {
|
|
13
|
+
const entry = resolved.tensorIndex.get(attr.tensor_ref.id);
|
|
14
|
+
if (entry?.dense)
|
|
15
|
+
return tensorToData(entry.dense);
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function getMatrixInput(inputNames, namespace, N) {
|
|
20
|
+
// Merge all inputs into a single [N, totalCols] matrix
|
|
21
|
+
let totalCols = 0;
|
|
22
|
+
for (const name of inputNames) {
|
|
23
|
+
const td = namespace.get(name);
|
|
24
|
+
totalCols += td ? tensorCols(td) : 0;
|
|
25
|
+
}
|
|
26
|
+
const data = new Float64Array(N * totalCols);
|
|
27
|
+
let colOffset = 0;
|
|
28
|
+
for (const name of inputNames) {
|
|
29
|
+
const td = namespace.get(name);
|
|
30
|
+
if (!td)
|
|
31
|
+
continue;
|
|
32
|
+
const cols = tensorCols(td);
|
|
33
|
+
const src = td.data;
|
|
34
|
+
for (let row = 0; row < N; row++) {
|
|
35
|
+
for (let c = 0; c < cols; c++) {
|
|
36
|
+
data[row * totalCols + colOffset + c] = src[row * cols + c];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
colOffset += cols;
|
|
40
|
+
}
|
|
41
|
+
return { data, cols: totalCols };
|
|
42
|
+
}
|
|
43
|
+
function publishMatrix(node, data, N, cols, namespace) {
|
|
44
|
+
const outputs = node.outputs ?? [];
|
|
45
|
+
if (outputs.length >= cols && cols > 1) {
|
|
46
|
+
// Multi-output: publish each column to its own named output
|
|
47
|
+
for (let ci = 0; ci < cols; ci++) {
|
|
48
|
+
const col = new Float64Array(N);
|
|
49
|
+
for (let row = 0; row < N; row++)
|
|
50
|
+
col[row] = data[row * cols + ci];
|
|
51
|
+
const outName = outputs[ci]?.name;
|
|
52
|
+
if (outName)
|
|
53
|
+
namespace.set(outName, { dtype: 'FLOAT64', shape: [N], data: col });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const out = outputs[0];
|
|
58
|
+
if (out)
|
|
59
|
+
namespace.set(out.name, { dtype: 'FLOAT64', shape: cols === 1 ? [N] : [N, cols], data });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ── omle.text ─────────────────────────────────────────────────────────────
|
|
63
|
+
export function executeTokenizer(node, inputNames, namespace, N) {
|
|
64
|
+
const td = namespace.get(inputNames[0]);
|
|
65
|
+
if (!td)
|
|
66
|
+
return;
|
|
67
|
+
const strings = td.data;
|
|
68
|
+
const tokenized = strings.map(s => s.toLowerCase().trim().split(/\s+/).filter(t => t.length > 0));
|
|
69
|
+
const maxLen = tokenized.reduce((m, toks) => Math.max(m, toks.length), 0);
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const toks of tokenized) {
|
|
72
|
+
for (let i = 0; i < maxLen; i++)
|
|
73
|
+
out.push(toks[i] ?? '');
|
|
74
|
+
}
|
|
75
|
+
const outEntry = (node.outputs ?? [])[0];
|
|
76
|
+
if (outEntry) {
|
|
77
|
+
namespace.set(outEntry.name, {
|
|
78
|
+
dtype: 'STRING',
|
|
79
|
+
shape: maxLen === 1 ? [N] : [N, maxLen],
|
|
80
|
+
data: out,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export function executeRegexTokenizer(node, inputNames, namespace, N) {
|
|
85
|
+
const td = namespace.get(inputNames[0]);
|
|
86
|
+
if (!td)
|
|
87
|
+
return;
|
|
88
|
+
const strings = td.data;
|
|
89
|
+
const pattern = (node.attributes ?? []).find(a => a.name === 'pattern')?.s ?? '\\s+';
|
|
90
|
+
const gaps = (node.attributes ?? []).find(a => a.name === 'gaps')?.b ?? true;
|
|
91
|
+
const minLen = (node.attributes ?? []).find(a => a.name === 'min_token_length')?.i ?? 1;
|
|
92
|
+
const re = new RegExp(pattern);
|
|
93
|
+
const tokenized = strings.map(s => {
|
|
94
|
+
const lower = s.toLowerCase();
|
|
95
|
+
const toks = gaps ? lower.split(re) : (lower.match(new RegExp(pattern, 'g')) ?? []);
|
|
96
|
+
return toks.filter(t => t.length >= minLen);
|
|
97
|
+
});
|
|
98
|
+
const maxLen = tokenized.reduce((m, toks) => Math.max(m, toks.length), 0);
|
|
99
|
+
const out = [];
|
|
100
|
+
for (const toks of tokenized) {
|
|
101
|
+
for (let i = 0; i < maxLen; i++)
|
|
102
|
+
out.push(toks[i] ?? '');
|
|
103
|
+
}
|
|
104
|
+
const outEntry = (node.outputs ?? [])[0];
|
|
105
|
+
if (outEntry) {
|
|
106
|
+
namespace.set(outEntry.name, {
|
|
107
|
+
dtype: 'STRING',
|
|
108
|
+
shape: maxLen === 1 ? [N] : [N, maxLen],
|
|
109
|
+
data: out,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Unpack a STRING tensor [N] or [N, M] into per-row token arrays (empty strings filtered).
|
|
114
|
+
function getStringRows(td, N) {
|
|
115
|
+
const data = td.data;
|
|
116
|
+
const cols = td.shape.length >= 2 ? (td.shape[1] ?? 1) : 1;
|
|
117
|
+
const rows = [];
|
|
118
|
+
for (let row = 0; row < N; row++) {
|
|
119
|
+
const toks = [];
|
|
120
|
+
for (let c = 0; c < cols; c++) {
|
|
121
|
+
const t = data[row * cols + c] ?? '';
|
|
122
|
+
if (t.length > 0)
|
|
123
|
+
toks.push(t);
|
|
124
|
+
}
|
|
125
|
+
rows.push(toks);
|
|
126
|
+
}
|
|
127
|
+
return rows;
|
|
128
|
+
}
|
|
129
|
+
function publishStringRows(node, rows, N, namespace) {
|
|
130
|
+
const maxLen = rows.reduce((m, r) => Math.max(m, r.length), 0);
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const r of rows) {
|
|
133
|
+
for (let i = 0; i < maxLen; i++)
|
|
134
|
+
out.push(r[i] ?? '');
|
|
135
|
+
}
|
|
136
|
+
const outEntry = (node.outputs ?? [])[0];
|
|
137
|
+
if (outEntry) {
|
|
138
|
+
namespace.set(outEntry.name, {
|
|
139
|
+
dtype: 'STRING',
|
|
140
|
+
shape: maxLen <= 1 ? [N] : [N, maxLen],
|
|
141
|
+
data: out,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export function executeNGram(node, inputNames, namespace, N) {
|
|
146
|
+
const td = namespace.get(inputNames[0]);
|
|
147
|
+
if (!td)
|
|
148
|
+
return;
|
|
149
|
+
const nMin = (node.attributes ?? []).find(a => a.name === 'n_min')?.i ?? 2;
|
|
150
|
+
const nMax = (node.attributes ?? []).find(a => a.name === 'n_max')?.i ?? nMin;
|
|
151
|
+
const rows = getStringRows(td, N);
|
|
152
|
+
const tokenized = rows.map(toks => {
|
|
153
|
+
const ngrams = [];
|
|
154
|
+
for (let n = nMin; n <= nMax; n++) {
|
|
155
|
+
for (let i = 0; i <= toks.length - n; i++) {
|
|
156
|
+
ngrams.push(toks.slice(i, i + n).join(' '));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return ngrams;
|
|
160
|
+
});
|
|
161
|
+
publishStringRows(node, tokenized, N, namespace);
|
|
162
|
+
}
|
|
163
|
+
export function executeStopWordsRemover(node, inputNames, namespace, N, resolved) {
|
|
164
|
+
const td = namespace.get(inputNames[0]);
|
|
165
|
+
if (!td)
|
|
166
|
+
return;
|
|
167
|
+
const stopAttr = (node.attributes ?? []).find(a => a.name === 'stop_words');
|
|
168
|
+
const stopTd = getTensorData(stopAttr, resolved);
|
|
169
|
+
const stopWords = stopTd
|
|
170
|
+
? stopTd.data
|
|
171
|
+
: (stopAttr?.strings ?? []);
|
|
172
|
+
const caseSensitive = (node.attributes ?? []).find(a => a.name === 'case_sensitive')?.b ?? false;
|
|
173
|
+
const stopSet = new Set(caseSensitive ? stopWords : stopWords.map(w => w.toLowerCase()));
|
|
174
|
+
const rows = getStringRows(td, N);
|
|
175
|
+
const filtered = rows.map(toks => toks.filter(t => !stopSet.has(caseSensitive ? t : t.toLowerCase())));
|
|
176
|
+
publishStringRows(node, filtered, N, namespace);
|
|
177
|
+
}
|
|
178
|
+
export function executeCountVectorizer(node, inputNames, namespace, N, resolved) {
|
|
179
|
+
const td = namespace.get(inputNames[0]);
|
|
180
|
+
if (!td)
|
|
181
|
+
return;
|
|
182
|
+
const vocabAttr = (node.attributes ?? []).find(a => a.name === 'vocabulary');
|
|
183
|
+
const vocabTd = getTensorData(vocabAttr, resolved);
|
|
184
|
+
const vocab = vocabTd
|
|
185
|
+
? vocabTd.data
|
|
186
|
+
: (vocabAttr?.strings ?? []);
|
|
187
|
+
if (vocab.length === 0)
|
|
188
|
+
return;
|
|
189
|
+
const binary = (node.attributes ?? []).find(a => a.name === 'binary')?.b ?? false;
|
|
190
|
+
const vocabIndex = new Map(vocab.map((w, i) => [w, i]));
|
|
191
|
+
const rows = getStringRows(td, N);
|
|
192
|
+
const out = new Float64Array(N * vocab.length);
|
|
193
|
+
for (let row = 0; row < N; row++) {
|
|
194
|
+
for (const tok of rows[row]) {
|
|
195
|
+
const idx = vocabIndex.get(tok);
|
|
196
|
+
if (idx !== undefined)
|
|
197
|
+
out[row * vocab.length + idx] += 1;
|
|
198
|
+
}
|
|
199
|
+
if (binary) {
|
|
200
|
+
for (let j = 0; j < vocab.length; j++) {
|
|
201
|
+
if (out[row * vocab.length + j] > 0)
|
|
202
|
+
out[row * vocab.length + j] = 1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
publishMatrix(node, out, N, vocab.length, namespace);
|
|
207
|
+
}
|
|
208
|
+
// MurmurHash3_x86_32 on raw UTF-8 bytes — matches Spark HashingTF (useNewHashingTF=true, seed=42).
|
|
209
|
+
function murmur3Hash(s) {
|
|
210
|
+
// Encode string as UTF-8 bytes
|
|
211
|
+
const bytes = [];
|
|
212
|
+
for (let i = 0; i < s.length; i++) {
|
|
213
|
+
const c = s.charCodeAt(i);
|
|
214
|
+
if (c < 0x80) {
|
|
215
|
+
bytes.push(c);
|
|
216
|
+
}
|
|
217
|
+
else if (c < 0x800) {
|
|
218
|
+
bytes.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
bytes.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const c1 = 0xcc9e2d51, c2 = 0x1b873593, seed = 42;
|
|
225
|
+
let h1 = seed | 0;
|
|
226
|
+
const nblocks = Math.floor(bytes.length / 4);
|
|
227
|
+
for (let i = 0; i < nblocks; i++) {
|
|
228
|
+
let k1 = (bytes[i * 4]) | (bytes[i * 4 + 1] << 8) | (bytes[i * 4 + 2] << 16) | (bytes[i * 4 + 3] << 24);
|
|
229
|
+
k1 = Math.imul(k1, c1) | 0;
|
|
230
|
+
k1 = ((k1 << 15) | (k1 >>> 17)) | 0;
|
|
231
|
+
k1 = Math.imul(k1, c2) | 0;
|
|
232
|
+
h1 ^= k1;
|
|
233
|
+
h1 = ((h1 << 13) | (h1 >>> 19)) | 0;
|
|
234
|
+
h1 = (Math.imul(h1, 5) + 0xe6546b64) | 0;
|
|
235
|
+
}
|
|
236
|
+
let k1 = 0;
|
|
237
|
+
const tail = bytes.length & 3, off = nblocks * 4;
|
|
238
|
+
if (tail >= 3)
|
|
239
|
+
k1 ^= bytes[off + 2] << 16;
|
|
240
|
+
if (tail >= 2)
|
|
241
|
+
k1 ^= bytes[off + 1] << 8;
|
|
242
|
+
if (tail >= 1) {
|
|
243
|
+
k1 ^= bytes[off];
|
|
244
|
+
k1 = Math.imul(k1, c1) | 0;
|
|
245
|
+
k1 = ((k1 << 15) | (k1 >>> 17)) | 0;
|
|
246
|
+
k1 = Math.imul(k1, c2) | 0;
|
|
247
|
+
h1 ^= k1;
|
|
248
|
+
}
|
|
249
|
+
h1 ^= bytes.length;
|
|
250
|
+
h1 ^= (h1 >>> 16);
|
|
251
|
+
h1 = Math.imul(h1, 0x85ebca6b) | 0;
|
|
252
|
+
h1 ^= (h1 >>> 13);
|
|
253
|
+
h1 = Math.imul(h1, 0xc2b2ae35) | 0;
|
|
254
|
+
h1 ^= (h1 >>> 16);
|
|
255
|
+
return h1;
|
|
256
|
+
}
|
|
257
|
+
export function executeHashingVectorizer(node, inputNames, namespace, N) {
|
|
258
|
+
const td = namespace.get(inputNames[0]);
|
|
259
|
+
if (!td)
|
|
260
|
+
return;
|
|
261
|
+
const attrs = node.attributes ?? [];
|
|
262
|
+
const numFeatures = attrs.find(a => a.name === 'num_features')?.i ?? 262144;
|
|
263
|
+
const binary = attrs.find(a => a.name === 'binary')?.b ?? false;
|
|
264
|
+
const rows = getStringRows(td, N);
|
|
265
|
+
const out = new Float64Array(N * numFeatures);
|
|
266
|
+
for (let row = 0; row < N; row++) {
|
|
267
|
+
for (const tok of rows[row]) {
|
|
268
|
+
const h = murmur3Hash(tok);
|
|
269
|
+
const idx = ((h % numFeatures) + numFeatures) % numFeatures;
|
|
270
|
+
if (binary)
|
|
271
|
+
out[row * numFeatures + idx] = 1;
|
|
272
|
+
else
|
|
273
|
+
out[row * numFeatures + idx] += 1;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
publishMatrix(node, out, N, numFeatures, namespace);
|
|
277
|
+
}
|
|
278
|
+
export function executeTfIdfTransformer(node, inputNames, namespace, N, resolved) {
|
|
279
|
+
const td = namespace.get(inputNames[0]);
|
|
280
|
+
if (!td)
|
|
281
|
+
return;
|
|
282
|
+
const idfTd = getTensorData((node.attributes ?? []).find(a => a.name === 'idf'), resolved);
|
|
283
|
+
if (!idfTd)
|
|
284
|
+
return;
|
|
285
|
+
const idf = idfTd.data;
|
|
286
|
+
const cols = td.shape.length >= 2 ? (td.shape[1] ?? 1) : 1;
|
|
287
|
+
const src = td.data;
|
|
288
|
+
const out = new Float64Array(N * cols);
|
|
289
|
+
for (let row = 0; row < N; row++) {
|
|
290
|
+
for (let c = 0; c < cols; c++) {
|
|
291
|
+
out[row * cols + c] = Number(src[row * cols + c]) * (idf[c] ?? 0);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
295
|
+
}
|
|
296
|
+
export function executeWord2Vec(node, inputNames, namespace, N, resolved) {
|
|
297
|
+
const td = namespace.get(inputNames[0]);
|
|
298
|
+
if (!td)
|
|
299
|
+
return;
|
|
300
|
+
const attrs = node.attributes ?? [];
|
|
301
|
+
const vocabAttr = attrs.find(a => a.name === 'vocabulary');
|
|
302
|
+
const vocabTd = getTensorData(vocabAttr, resolved);
|
|
303
|
+
const vocab = vocabTd ? vocabTd.data : (vocabAttr?.strings ?? []);
|
|
304
|
+
const embTd = getTensorData(attrs.find(a => a.name === 'embeddings'), resolved);
|
|
305
|
+
if (!embTd || vocab.length === 0)
|
|
306
|
+
return;
|
|
307
|
+
const emb = embTd.data;
|
|
308
|
+
const embDim = embTd.shape[1] ?? Math.round(emb.length / vocab.length);
|
|
309
|
+
const vocabIndex = new Map(vocab.map((w, i) => [w, i]));
|
|
310
|
+
const rows = getStringRows(td, N);
|
|
311
|
+
const out = new Float64Array(N * embDim);
|
|
312
|
+
for (let row = 0; row < N; row++) {
|
|
313
|
+
const toks = rows[row].filter(t => vocabIndex.has(t));
|
|
314
|
+
if (toks.length === 0)
|
|
315
|
+
continue;
|
|
316
|
+
for (const tok of toks) {
|
|
317
|
+
const wi = vocabIndex.get(tok);
|
|
318
|
+
for (let d = 0; d < embDim; d++)
|
|
319
|
+
out[row * embDim + d] += emb[wi * embDim + d];
|
|
320
|
+
}
|
|
321
|
+
for (let d = 0; d < embDim; d++)
|
|
322
|
+
out[row * embDim + d] /= toks.length;
|
|
323
|
+
}
|
|
324
|
+
publishMatrix(node, out, N, embDim, namespace);
|
|
325
|
+
}
|
|
326
|
+
// ── omle.core ──────────────────────────────────────────────────────────────
|
|
327
|
+
export function executeTakeSlots(node, inputNames, namespace, N) {
|
|
328
|
+
// Name-based selection: look up each named column directly from the namespace.
|
|
329
|
+
// The composite executor populates the namespace with individual schema feature
|
|
330
|
+
// entries so that string-keyed TakeSlots (from ColumnTransformer) can find them.
|
|
331
|
+
const namesAttr = getAttr(node, 'names');
|
|
332
|
+
const names = namesAttr?.strings ?? [];
|
|
333
|
+
if (names.length > 0) {
|
|
334
|
+
const outCols = names.length;
|
|
335
|
+
const out = new Float64Array(N * outCols);
|
|
336
|
+
for (let ci = 0; ci < outCols; ci++) {
|
|
337
|
+
const td = namespace.get(names[ci]);
|
|
338
|
+
if (!td)
|
|
339
|
+
continue;
|
|
340
|
+
const src = td.data;
|
|
341
|
+
const srcCols = tensorCols(td);
|
|
342
|
+
for (let row = 0; row < N; row++) {
|
|
343
|
+
out[row * outCols + ci] = src[row * srcCols];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
publishMatrix(node, out, N, outCols, namespace);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// Index-based selection.
|
|
350
|
+
const indices = getAttr(node, 'indices')?.ints ?? [];
|
|
351
|
+
if (indices.length === 0)
|
|
352
|
+
return;
|
|
353
|
+
const { data: inData, cols: inCols } = getMatrixInput(inputNames, namespace, N);
|
|
354
|
+
const outCols = indices.length;
|
|
355
|
+
const out = new Float64Array(N * outCols);
|
|
356
|
+
for (let row = 0; row < N; row++) {
|
|
357
|
+
for (let ci = 0; ci < outCols; ci++) {
|
|
358
|
+
const srcCol = indices[ci] ?? 0;
|
|
359
|
+
out[row * outCols + ci] = inData[row * inCols + srcCol];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
publishMatrix(node, out, N, outCols, namespace);
|
|
363
|
+
}
|
|
364
|
+
export function executeConcat(node, inputNames, namespace, N) {
|
|
365
|
+
const { data, cols } = getMatrixInput(inputNames, namespace, N);
|
|
366
|
+
publishMatrix(node, data, N, cols, namespace);
|
|
367
|
+
}
|
|
368
|
+
export function executeDerive(node, inputNames, namespace, N) {
|
|
369
|
+
const exprAttr = getAttr(node, 'expr');
|
|
370
|
+
if (!exprAttr?.expr)
|
|
371
|
+
return;
|
|
372
|
+
const expr = exprAttr.expr;
|
|
373
|
+
// Detect simple unary function applied to a matrix reference
|
|
374
|
+
// (e.g. FunctionTransformer log applied to whole matrix X)
|
|
375
|
+
if (expr.apply?.arguments?.length === 1) {
|
|
376
|
+
const arg = expr.apply.arguments[0];
|
|
377
|
+
const fn = expr.apply.function ?? '';
|
|
378
|
+
const baseName = fn.split('.').pop()?.toLowerCase() ?? '';
|
|
379
|
+
const mathFn = resolveMathFn(baseName);
|
|
380
|
+
if (mathFn && arg.ref) {
|
|
381
|
+
const td = namespace.get(arg.ref.value ?? '');
|
|
382
|
+
if (td) {
|
|
383
|
+
const src = td.data;
|
|
384
|
+
const cols = td.shape.length >= 2 ? td.shape.slice(1).reduce((a, b) => a * b, 1) : 1;
|
|
385
|
+
const out = new Float64Array(src.length);
|
|
386
|
+
for (let i = 0; i < src.length; i++)
|
|
387
|
+
out[i] = mathFn(src[i]);
|
|
388
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
// General case: evaluate expression per row → single-column output
|
|
394
|
+
const vals = new Array(N);
|
|
395
|
+
for (let row = 0; row < N; row++)
|
|
396
|
+
vals[row] = evalExpr(expr, row, namespace);
|
|
397
|
+
if (N > 0 && typeof vals[0] === 'string') {
|
|
398
|
+
const firstOutput = (node.outputs ?? [])[0];
|
|
399
|
+
if (firstOutput)
|
|
400
|
+
namespace.set(firstOutput.name, { dtype: 'STRING', shape: [N], data: vals });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const out = new Float64Array(N);
|
|
404
|
+
for (let i = 0; i < N; i++)
|
|
405
|
+
out[i] = vals[i];
|
|
406
|
+
publishMatrix(node, out, N, 1, namespace);
|
|
407
|
+
}
|
|
408
|
+
function evalExpr(expr, row, namespace) {
|
|
409
|
+
if (expr.literal != null) {
|
|
410
|
+
const s = expr.literal;
|
|
411
|
+
if (s.double_value !== undefined)
|
|
412
|
+
return s.double_value;
|
|
413
|
+
if (s.float_value !== undefined)
|
|
414
|
+
return s.float_value;
|
|
415
|
+
if (s.int_value !== undefined)
|
|
416
|
+
return s.int_value;
|
|
417
|
+
if (s.string_value !== undefined)
|
|
418
|
+
return s.string_value;
|
|
419
|
+
return NaN;
|
|
420
|
+
}
|
|
421
|
+
if (expr.ref != null) {
|
|
422
|
+
const name = expr.ref.value ?? '';
|
|
423
|
+
const td = namespace.get(name);
|
|
424
|
+
if (!td)
|
|
425
|
+
return NaN;
|
|
426
|
+
if (td.dtype === 'STRING')
|
|
427
|
+
return td.data[row] ?? '';
|
|
428
|
+
const data = td.data;
|
|
429
|
+
const cols = td.shape.length >= 2 ? td.shape.slice(1).reduce((a, b) => a * b, 1) : 1;
|
|
430
|
+
return data[row * cols] ?? NaN;
|
|
431
|
+
}
|
|
432
|
+
if (expr.apply != null) {
|
|
433
|
+
const fn = expr.apply.function ?? '';
|
|
434
|
+
const args = expr.apply.arguments ?? [];
|
|
435
|
+
const baseName = fn.split('.').pop()?.toLowerCase() ?? '';
|
|
436
|
+
// Coalesce — variadic, short-circuit on first non-NaN/non-empty
|
|
437
|
+
if (baseName === 'coalesce') {
|
|
438
|
+
for (const arg of args) {
|
|
439
|
+
const v = evalExpr(arg, row, namespace);
|
|
440
|
+
if (typeof v === 'string' || !isNaN(v))
|
|
441
|
+
return v;
|
|
442
|
+
}
|
|
443
|
+
return NaN;
|
|
444
|
+
}
|
|
445
|
+
// Conditional if — evaluate lazily
|
|
446
|
+
if (baseName === 'if' && args.length === 3) {
|
|
447
|
+
const cond = evalExpr(args[0], row, namespace);
|
|
448
|
+
return (cond !== 0 && !isNaN(cond))
|
|
449
|
+
? evalExpr(args[1], row, namespace)
|
|
450
|
+
: evalExpr(args[2], row, namespace);
|
|
451
|
+
}
|
|
452
|
+
// String operations
|
|
453
|
+
if (baseName === 'lower' && args.length === 1)
|
|
454
|
+
return String(evalExpr(args[0], row, namespace)).toLowerCase();
|
|
455
|
+
if (baseName === 'upper' && args.length === 1)
|
|
456
|
+
return String(evalExpr(args[0], row, namespace)).toUpperCase();
|
|
457
|
+
if (baseName === 'trim' && args.length === 1)
|
|
458
|
+
return String(evalExpr(args[0], row, namespace)).trim();
|
|
459
|
+
if (baseName === 'concat' && args.length === 2)
|
|
460
|
+
return String(evalExpr(args[0], row, namespace)) + String(evalExpr(args[1], row, namespace));
|
|
461
|
+
if (baseName === 'substring' && args.length >= 2) {
|
|
462
|
+
const s = String(evalExpr(args[0], row, namespace));
|
|
463
|
+
const pos = Number(evalExpr(args[1], row, namespace));
|
|
464
|
+
const start = Math.max(0, pos - 1); // SQL uses 1-based positions
|
|
465
|
+
if (args.length >= 3) {
|
|
466
|
+
const len = Number(evalExpr(args[2], row, namespace));
|
|
467
|
+
return s.slice(start, start + len);
|
|
468
|
+
}
|
|
469
|
+
return s.slice(start);
|
|
470
|
+
}
|
|
471
|
+
// Binary functions
|
|
472
|
+
if (args.length === 2) {
|
|
473
|
+
const xv = evalExpr(args[0], row, namespace);
|
|
474
|
+
const yv = evalExpr(args[1], row, namespace);
|
|
475
|
+
const x = xv, y = yv;
|
|
476
|
+
switch (baseName) {
|
|
477
|
+
case 'add':
|
|
478
|
+
case 'plus': return x + y;
|
|
479
|
+
case 'subtract':
|
|
480
|
+
case 'sub':
|
|
481
|
+
case 'minus': return x - y;
|
|
482
|
+
case 'multiply':
|
|
483
|
+
case 'mul': return x * y;
|
|
484
|
+
case 'divide':
|
|
485
|
+
case 'div': return x / y;
|
|
486
|
+
case 'pow':
|
|
487
|
+
case 'power': return Math.pow(x, y);
|
|
488
|
+
case 'min': return Math.min(x, y);
|
|
489
|
+
case 'max': return Math.max(x, y);
|
|
490
|
+
case 'mod': return x % y;
|
|
491
|
+
case 'equal': return (xv === yv) ? 1 : 0;
|
|
492
|
+
case 'not_equal': return (xv !== yv) ? 1 : 0;
|
|
493
|
+
case 'less_than': return (xv < yv) ? 1 : 0;
|
|
494
|
+
case 'less_or_equal': return (xv <= yv) ? 1 : 0;
|
|
495
|
+
case 'greater_than': return (xv > yv) ? 1 : 0;
|
|
496
|
+
case 'greater_or_equal': return (xv >= yv) ? 1 : 0;
|
|
497
|
+
case 'and': return (!isNaN(x) && x !== 0 && !isNaN(y) && y !== 0) ? 1 : 0;
|
|
498
|
+
case 'or': return ((!isNaN(x) && x !== 0) || (!isNaN(y) && y !== 0)) ? 1 : 0;
|
|
499
|
+
default: break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
// Unary
|
|
503
|
+
if (args.length >= 1) {
|
|
504
|
+
const xv = evalExpr(args[0], row, namespace);
|
|
505
|
+
const x = xv;
|
|
506
|
+
const mathFn = resolveMathFn(baseName);
|
|
507
|
+
if (mathFn)
|
|
508
|
+
return mathFn(x);
|
|
509
|
+
if (baseName === 'not')
|
|
510
|
+
return (isNaN(x) || x === 0) ? 1 : 0;
|
|
511
|
+
if (baseName === 'is_missing')
|
|
512
|
+
return isNaN(x) ? 1 : 0;
|
|
513
|
+
if (baseName === 'is_not_missing')
|
|
514
|
+
return isNaN(x) ? 0 : 1;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return NaN;
|
|
518
|
+
}
|
|
519
|
+
function resolveMathFn(baseName) {
|
|
520
|
+
switch (baseName) {
|
|
521
|
+
case 'log': return Math.log;
|
|
522
|
+
case 'log2': return Math.log2;
|
|
523
|
+
case 'log10': return Math.log10;
|
|
524
|
+
case 'log1p': return x => Math.log(1 + x);
|
|
525
|
+
case 'exp': return Math.exp;
|
|
526
|
+
case 'expm1': return x => Math.exp(x) - 1;
|
|
527
|
+
case 'sqrt': return Math.sqrt;
|
|
528
|
+
case 'abs': return Math.abs;
|
|
529
|
+
case 'square': return x => x * x;
|
|
530
|
+
case 'cbrt': return Math.cbrt;
|
|
531
|
+
case 'sign': return Math.sign;
|
|
532
|
+
case 'ceil': return Math.ceil;
|
|
533
|
+
case 'floor': return Math.floor;
|
|
534
|
+
case 'round': return Math.round;
|
|
535
|
+
case 'sigmoid': return x => 1 / (1 + Math.exp(-x));
|
|
536
|
+
case 'tanh': return Math.tanh;
|
|
537
|
+
case 'reciprocal': return x => 1 / x;
|
|
538
|
+
case 'neg':
|
|
539
|
+
case 'negate': return x => -x;
|
|
540
|
+
default: return null;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
// ── omle.feature ───────────────────────────────────────────────────────────
|
|
544
|
+
export function executeBinarizer(node, inputNames, namespace, N, resolved) {
|
|
545
|
+
const threshAttr = getAttr(node, 'thresholds') ?? getAttr(node, 'threshold');
|
|
546
|
+
const threshTd = threshAttr ? getTensorData(threshAttr, resolved) : null;
|
|
547
|
+
const thresholds = threshTd ? threshTd.data : null;
|
|
548
|
+
const fallback = threshAttr?.f64 ?? 0;
|
|
549
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
550
|
+
const out = new Float64Array(N * cols);
|
|
551
|
+
for (let row = 0; row < N; row++) {
|
|
552
|
+
for (let c = 0; c < cols; c++) {
|
|
553
|
+
const thr = thresholds ? (thresholds[c] ?? thresholds[0] ?? fallback) : fallback;
|
|
554
|
+
out[row * cols + c] = inData[row * cols + c] > thr ? 1 : 0;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
558
|
+
}
|
|
559
|
+
export function executeMinMaxScaler(node, inputNames, namespace, N, resolved) {
|
|
560
|
+
const dataMin = getTensorData(getAttr(node, 'data_min'), resolved);
|
|
561
|
+
const dataMax = getTensorData(getAttr(node, 'data_max'), resolved);
|
|
562
|
+
if (!dataMin || !dataMax)
|
|
563
|
+
return;
|
|
564
|
+
const rangeMin = getAttr(node, 'feature_range_min')?.f64 ?? 0;
|
|
565
|
+
const rangeMax = getAttr(node, 'feature_range_max')?.f64 ?? 1;
|
|
566
|
+
const scale = rangeMax - rangeMin;
|
|
567
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
568
|
+
const mn = dataMin.data;
|
|
569
|
+
const rng = dataMax.data; // data_max attr stores data_range_ (max - min)
|
|
570
|
+
const out = new Float64Array(N * cols);
|
|
571
|
+
for (let row = 0; row < N; row++) {
|
|
572
|
+
for (let c = 0; c < cols; c++) {
|
|
573
|
+
const x = inData[row * cols + c];
|
|
574
|
+
const r = rng[c] ?? 1;
|
|
575
|
+
const norm = r !== 0 ? (x - (mn[c] ?? 0)) / r : 0;
|
|
576
|
+
out[row * cols + c] = norm * scale + rangeMin;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
580
|
+
}
|
|
581
|
+
export function executeMaxAbsScaler(node, inputNames, namespace, N, resolved) {
|
|
582
|
+
const scaleData = getTensorData(getAttr(node, 'scale'), resolved);
|
|
583
|
+
if (!scaleData)
|
|
584
|
+
return;
|
|
585
|
+
const scale = scaleData.data;
|
|
586
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
587
|
+
const out = new Float64Array(N * cols);
|
|
588
|
+
for (let row = 0; row < N; row++) {
|
|
589
|
+
for (let c = 0; c < cols; c++) {
|
|
590
|
+
out[row * cols + c] = inData[row * cols + c] / (scale[c] ?? 1);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
594
|
+
}
|
|
595
|
+
export function executeRobustScaler(node, inputNames, namespace, N, resolved) {
|
|
596
|
+
const centerData = getTensorData(getAttr(node, 'center'), resolved);
|
|
597
|
+
const scaleData = getTensorData(getAttr(node, 'scale'), resolved);
|
|
598
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
599
|
+
const center = centerData ? centerData.data : null;
|
|
600
|
+
const scale = scaleData ? scaleData.data : null;
|
|
601
|
+
const out = new Float64Array(N * cols);
|
|
602
|
+
for (let row = 0; row < N; row++) {
|
|
603
|
+
for (let c = 0; c < cols; c++) {
|
|
604
|
+
let x = inData[row * cols + c];
|
|
605
|
+
if (center)
|
|
606
|
+
x -= center[c] ?? 0;
|
|
607
|
+
if (scale)
|
|
608
|
+
x /= (scale[c] ?? 1);
|
|
609
|
+
out[row * cols + c] = x;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
613
|
+
}
|
|
614
|
+
export function executeNormalizer(node, inputNames, namespace, N) {
|
|
615
|
+
const normType = getAttr(node, 'norm')?.s ?? 'l2';
|
|
616
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
617
|
+
const out = new Float64Array(N * cols);
|
|
618
|
+
for (let row = 0; row < N; row++) {
|
|
619
|
+
const base = row * cols;
|
|
620
|
+
let norm = 0;
|
|
621
|
+
if (normType === 'l1') {
|
|
622
|
+
for (let c = 0; c < cols; c++)
|
|
623
|
+
norm += Math.abs(inData[base + c]);
|
|
624
|
+
}
|
|
625
|
+
else if (normType === 'max') {
|
|
626
|
+
for (let c = 0; c < cols; c++)
|
|
627
|
+
norm = Math.max(norm, Math.abs(inData[base + c]));
|
|
628
|
+
}
|
|
629
|
+
else {
|
|
630
|
+
// l2 (default)
|
|
631
|
+
for (let c = 0; c < cols; c++)
|
|
632
|
+
norm += inData[base + c] ** 2;
|
|
633
|
+
norm = Math.sqrt(norm);
|
|
634
|
+
}
|
|
635
|
+
const inv = norm > 0 ? 1 / norm : 0;
|
|
636
|
+
for (let c = 0; c < cols; c++)
|
|
637
|
+
out[base + c] = inData[base + c] * inv;
|
|
638
|
+
}
|
|
639
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
640
|
+
}
|
|
641
|
+
export function executeOneHotEncoder(node, inputNames, namespace, N, resolved) {
|
|
642
|
+
const catAttr = getAttr(node, 'categories');
|
|
643
|
+
const offAttr = getAttr(node, 'category_offsets');
|
|
644
|
+
if (!catAttr)
|
|
645
|
+
return;
|
|
646
|
+
// Categories may be stored as an inline string list (attr.strings) or as a tensor.
|
|
647
|
+
const catTd = getTensorData(catAttr, resolved);
|
|
648
|
+
const categories = catTd
|
|
649
|
+
? catTd.data
|
|
650
|
+
: (catAttr.strings ?? []);
|
|
651
|
+
if (categories.length === 0)
|
|
652
|
+
return;
|
|
653
|
+
// Offsets may be absent (Spark single-feature case) — default to one feature spanning all categories.
|
|
654
|
+
const offTd = offAttr ? getTensorData(offAttr, resolved) : null;
|
|
655
|
+
const offsets = offTd
|
|
656
|
+
? Array.from(offTd.data).map(Math.round)
|
|
657
|
+
: [0, categories.length];
|
|
658
|
+
const dropLast = getAttr(node, 'drop_last')?.b ?? false;
|
|
659
|
+
const { data: inData, cols: inCols } = getMatrixInput(inputNames, namespace, N);
|
|
660
|
+
const nFeatures = inCols;
|
|
661
|
+
// Compute per-feature category counts, applying drop_last
|
|
662
|
+
const featCats = [];
|
|
663
|
+
for (let fi = 0; fi < nFeatures; fi++) {
|
|
664
|
+
const nCats = (offsets[fi + 1] ?? categories.length) - (offsets[fi] ?? 0);
|
|
665
|
+
featCats.push(dropLast ? Math.max(nCats - 1, 0) : nCats);
|
|
666
|
+
}
|
|
667
|
+
const outCols = featCats.reduce((s, c) => s + c, 0);
|
|
668
|
+
const outputs = node.outputs ?? [];
|
|
669
|
+
const onePerOutput = outputs.length >= nFeatures;
|
|
670
|
+
const combined = onePerOutput ? null : new Float64Array(N * outCols);
|
|
671
|
+
let outBase = 0;
|
|
672
|
+
for (let fi = 0; fi < nFeatures; fi++) {
|
|
673
|
+
const start = offsets[fi] ?? 0;
|
|
674
|
+
const end = offsets[fi + 1] ?? categories.length;
|
|
675
|
+
const keep = featCats[fi];
|
|
676
|
+
if (onePerOutput) {
|
|
677
|
+
const col = new Float64Array(N * keep);
|
|
678
|
+
for (let row = 0; row < N; row++) {
|
|
679
|
+
const rawVal = inData[row * nFeatures + fi];
|
|
680
|
+
const catIdx = matchCategory(rawVal, categories, start, end);
|
|
681
|
+
if (catIdx >= 0) {
|
|
682
|
+
const c = catIdx - start;
|
|
683
|
+
if (c < keep)
|
|
684
|
+
col[row * keep + c] = 1;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
const outName = outputs[fi]?.name;
|
|
688
|
+
if (outName)
|
|
689
|
+
namespace.set(outName, { dtype: 'FLOAT64', shape: keep === 1 ? [N] : [N, keep], data: col });
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
for (let row = 0; row < N; row++) {
|
|
693
|
+
const rawVal = inData[row * nFeatures + fi];
|
|
694
|
+
const catIdx = matchCategory(rawVal, categories, start, end);
|
|
695
|
+
if (catIdx >= 0) {
|
|
696
|
+
const c = catIdx - start;
|
|
697
|
+
if (c < keep)
|
|
698
|
+
combined[row * outCols + outBase + c] = 1;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
outBase += keep;
|
|
703
|
+
}
|
|
704
|
+
if (!onePerOutput)
|
|
705
|
+
publishMatrix(node, combined, N, outCols, namespace);
|
|
706
|
+
}
|
|
707
|
+
function matchCategory(val, cats, start, end) {
|
|
708
|
+
// Try integer string first, then float string
|
|
709
|
+
const intStr = String(Math.round(val));
|
|
710
|
+
for (let i = start; i < end; i++) {
|
|
711
|
+
if (cats[i] === intStr)
|
|
712
|
+
return i;
|
|
713
|
+
}
|
|
714
|
+
const floatStr = String(val);
|
|
715
|
+
for (let i = start; i < end; i++) {
|
|
716
|
+
if (cats[i] === floatStr)
|
|
717
|
+
return i;
|
|
718
|
+
}
|
|
719
|
+
// Try "val.0" form
|
|
720
|
+
const dotStr = val.toFixed(1);
|
|
721
|
+
for (let i = start; i < end; i++) {
|
|
722
|
+
if (cats[i] === dotStr)
|
|
723
|
+
return i;
|
|
724
|
+
}
|
|
725
|
+
return -1;
|
|
726
|
+
}
|
|
727
|
+
export function executeBucketizer(node, inputNames, namespace, N, resolved) {
|
|
728
|
+
const boundAttr = getAttr(node, 'boundaries');
|
|
729
|
+
const offAttr = getAttr(node, 'boundary_offsets');
|
|
730
|
+
if (!boundAttr || !offAttr)
|
|
731
|
+
return;
|
|
732
|
+
const boundTd = getTensorData(boundAttr, resolved);
|
|
733
|
+
const offTd = getTensorData(offAttr, resolved);
|
|
734
|
+
if (!boundTd || !offTd)
|
|
735
|
+
return;
|
|
736
|
+
const boundaries = boundTd.data;
|
|
737
|
+
const offsets = Array.from(offTd.data).map(Math.round);
|
|
738
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
739
|
+
const out = new Float64Array(N * cols);
|
|
740
|
+
for (let fi = 0; fi < cols; fi++) {
|
|
741
|
+
const start = offsets[fi] ?? 0;
|
|
742
|
+
const end = offsets[fi + 1] ?? boundaries.length;
|
|
743
|
+
for (let row = 0; row < N; row++) {
|
|
744
|
+
const x = inData[row * cols + fi];
|
|
745
|
+
// bisect_right: count how many boundaries are <= x... actually <= or <
|
|
746
|
+
// sklearn uses digitize which is like searchsorted right
|
|
747
|
+
let bin = 0;
|
|
748
|
+
for (let bi = start; bi < end; bi++) {
|
|
749
|
+
if (x >= (boundaries[bi] ?? Infinity))
|
|
750
|
+
bin++;
|
|
751
|
+
else
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
out[row * cols + fi] = bin;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
758
|
+
}
|
|
759
|
+
export function executePolynomialFeatures(node, inputNames, namespace, N, resolved) {
|
|
760
|
+
const powersAttr = getAttr(node, 'powers');
|
|
761
|
+
if (!powersAttr)
|
|
762
|
+
return;
|
|
763
|
+
const powersTd = getTensorData(powersAttr, resolved);
|
|
764
|
+
if (!powersTd)
|
|
765
|
+
return;
|
|
766
|
+
const { data: inData, cols: inCols } = getMatrixInput(inputNames, namespace, N);
|
|
767
|
+
// powers shape: [n_output_features, n_input_features]
|
|
768
|
+
const powersShape = powersTd.shape ?? [];
|
|
769
|
+
const nOut = powersShape[0] ?? 0;
|
|
770
|
+
const nIn = powersShape[1] ?? inCols;
|
|
771
|
+
const powers = powersTd.data;
|
|
772
|
+
const out = new Float64Array(N * nOut);
|
|
773
|
+
for (let row = 0; row < N; row++) {
|
|
774
|
+
for (let oi = 0; oi < nOut; oi++) {
|
|
775
|
+
let val = 1;
|
|
776
|
+
for (let ii = 0; ii < nIn; ii++) {
|
|
777
|
+
const p = powers[oi * nIn + ii] ?? 0;
|
|
778
|
+
if (p !== 0)
|
|
779
|
+
val *= Math.pow(inData[row * inCols + ii], p);
|
|
780
|
+
}
|
|
781
|
+
out[row * nOut + oi] = val;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
publishMatrix(node, out, N, nOut, namespace);
|
|
785
|
+
}
|
|
786
|
+
export function executePowerTransformer(node, inputNames, namespace, N, resolved) {
|
|
787
|
+
const method = getAttr(node, 'method')?.s ?? 'yeo_johnson';
|
|
788
|
+
const standardize = getAttr(node, 'standardize')?.b ?? true;
|
|
789
|
+
const lambdasTd = getTensorData(getAttr(node, 'lambdas'), resolved);
|
|
790
|
+
const meanTd = standardize ? getTensorData(getAttr(node, 'mean'), resolved) : null;
|
|
791
|
+
const scaleTd = standardize ? getTensorData(getAttr(node, 'scale'), resolved) : null;
|
|
792
|
+
if (!lambdasTd)
|
|
793
|
+
return;
|
|
794
|
+
const lambdas = lambdasTd.data;
|
|
795
|
+
const mean = meanTd ? meanTd.data : null;
|
|
796
|
+
const scale = scaleTd ? scaleTd.data : null;
|
|
797
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
798
|
+
const out = new Float64Array(N * cols);
|
|
799
|
+
for (let row = 0; row < N; row++) {
|
|
800
|
+
for (let c = 0; c < cols; c++) {
|
|
801
|
+
const x = inData[row * cols + c];
|
|
802
|
+
const lam = lambdas[c] ?? 0;
|
|
803
|
+
let y = method === 'box_cox' ? boxCox(x, lam) : yeoJohnson(x, lam);
|
|
804
|
+
if (standardize && mean && scale) {
|
|
805
|
+
y = (y - (mean[c] ?? 0)) / (scale[c] ?? 1);
|
|
806
|
+
}
|
|
807
|
+
out[row * cols + c] = y;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
811
|
+
}
|
|
812
|
+
function yeoJohnson(x, lam) {
|
|
813
|
+
const eps = 1e-10;
|
|
814
|
+
if (x >= 0) {
|
|
815
|
+
if (Math.abs(lam) < eps)
|
|
816
|
+
return Math.log(x + 1);
|
|
817
|
+
return (Math.pow(x + 1, lam) - 1) / lam;
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
const lam2 = 2 - lam;
|
|
821
|
+
if (Math.abs(lam2) < eps)
|
|
822
|
+
return -Math.log(-x + 1);
|
|
823
|
+
return -(Math.pow(-x + 1, lam2) - 1) / lam2;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function boxCox(x, lam) {
|
|
827
|
+
const eps = 1e-10;
|
|
828
|
+
if (Math.abs(lam) < eps)
|
|
829
|
+
return Math.log(x);
|
|
830
|
+
return (Math.pow(x, lam) - 1) / lam;
|
|
831
|
+
}
|
|
832
|
+
export function executeQuantileTransformer(node, inputNames, namespace, N, resolved) {
|
|
833
|
+
const outputDist = getAttr(node, 'output_distribution')?.s ?? 'uniform';
|
|
834
|
+
const quantilesTd = getTensorData(getAttr(node, 'quantiles'), resolved);
|
|
835
|
+
const referencesTd = getTensorData(getAttr(node, 'references'), resolved);
|
|
836
|
+
if (!quantilesTd || !referencesTd)
|
|
837
|
+
return;
|
|
838
|
+
// quantiles: [n_quantiles] — the quantile levels (0..1) = t.references_
|
|
839
|
+
// references: [n_features, n_quantiles] — feature values at each level = t.quantiles_.T
|
|
840
|
+
const quantiles = quantilesTd.data;
|
|
841
|
+
const nQ = quantiles.length;
|
|
842
|
+
const references = referencesTd.data;
|
|
843
|
+
const refShape = referencesTd.shape ?? [];
|
|
844
|
+
const nFeatures = refShape[0] ?? 1;
|
|
845
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
846
|
+
const out = new Float64Array(N * cols);
|
|
847
|
+
for (let row = 0; row < N; row++) {
|
|
848
|
+
for (let fi = 0; fi < cols && fi < nFeatures; fi++) {
|
|
849
|
+
const x = inData[row * cols + fi];
|
|
850
|
+
const refs = references;
|
|
851
|
+
const fiOffset = fi * nQ;
|
|
852
|
+
// Interpolate x in refs[fi, :] to get quantile level
|
|
853
|
+
let q = interpSearchSorted(refs, fiOffset, nQ, x, quantiles);
|
|
854
|
+
if (outputDist === 'normal') {
|
|
855
|
+
// Clamp slightly away from 0 and 1 to avoid Infinity
|
|
856
|
+
q = Math.max(1e-7, Math.min(1 - 1e-7, q));
|
|
857
|
+
q = probitApprox(q);
|
|
858
|
+
}
|
|
859
|
+
out[row * cols + fi] = q;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
863
|
+
}
|
|
864
|
+
function interpSearchSorted(xp, xpOffset, n, x, fp) {
|
|
865
|
+
if (n === 0)
|
|
866
|
+
return 0;
|
|
867
|
+
if (x <= xp[xpOffset])
|
|
868
|
+
return fp[0] ?? 0;
|
|
869
|
+
if (x >= xp[xpOffset + n - 1])
|
|
870
|
+
return fp[n - 1] ?? 1;
|
|
871
|
+
// Binary search
|
|
872
|
+
let lo = 0, hi = n - 1;
|
|
873
|
+
while (lo < hi - 1) {
|
|
874
|
+
const mid = (lo + hi) >> 1;
|
|
875
|
+
if (xp[xpOffset + mid] <= x)
|
|
876
|
+
lo = mid;
|
|
877
|
+
else
|
|
878
|
+
hi = mid;
|
|
879
|
+
}
|
|
880
|
+
const x0 = xp[xpOffset + lo], x1 = xp[xpOffset + hi];
|
|
881
|
+
const f0 = fp[lo] ?? 0, f1 = fp[hi] ?? 1;
|
|
882
|
+
const t = x1 !== x0 ? (x - x0) / (x1 - x0) : 0;
|
|
883
|
+
return f0 + t * (f1 - f0);
|
|
884
|
+
}
|
|
885
|
+
function probitApprox(p) {
|
|
886
|
+
const a = [2.515517, 0.802853, 0.010328];
|
|
887
|
+
const b = [1.432788, 0.189269, 0.001308];
|
|
888
|
+
const sign = p < 0.5 ? -1 : 1;
|
|
889
|
+
const q = Math.min(p, 1 - p);
|
|
890
|
+
const t = Math.sqrt(-2 * Math.log(q));
|
|
891
|
+
const num = a[0] + a[1] * t + a[2] * t * t;
|
|
892
|
+
const den = 1 + b[0] * t + b[1] * t * t + b[2] * t * t * t;
|
|
893
|
+
return sign * (t - num / den);
|
|
894
|
+
}
|
|
895
|
+
export function executeSplineTransformer(node, inputNames, namespace, N, resolved) {
|
|
896
|
+
const knotsTd = getTensorData(getAttr(node, 'knots'), resolved);
|
|
897
|
+
const degree = getAttr(node, 'degree')?.i ?? 3;
|
|
898
|
+
const includeBias = getAttr(node, 'include_bias')?.b ?? true;
|
|
899
|
+
const extrapolation = getAttr(node, 'extrapolation')?.s ?? 'constant';
|
|
900
|
+
if (!knotsTd)
|
|
901
|
+
return;
|
|
902
|
+
// knots shape: [n_aug_knots, n_features]
|
|
903
|
+
const knotsShape = knotsTd.shape ?? [];
|
|
904
|
+
const nAugKnots = knotsShape[0] ?? 0;
|
|
905
|
+
const nFeaturesKnots = knotsShape[1] ?? 1;
|
|
906
|
+
const knotsData = knotsTd.data;
|
|
907
|
+
const nSplines = nAugKnots - degree - 1; // per feature
|
|
908
|
+
const nSplineOut = includeBias ? nSplines : nSplines - 1;
|
|
909
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
910
|
+
const outCols = nSplineOut * Math.min(cols, nFeaturesKnots);
|
|
911
|
+
const out = new Float64Array(N * outCols);
|
|
912
|
+
for (let fi = 0; fi < cols && fi < nFeaturesKnots; fi++) {
|
|
913
|
+
// Extract knot vector for feature fi: column fi of knots matrix
|
|
914
|
+
const t = new Float64Array(nAugKnots);
|
|
915
|
+
for (let k = 0; k < nAugKnots; k++) {
|
|
916
|
+
t[k] = knotsData[k * nFeaturesKnots + fi];
|
|
917
|
+
}
|
|
918
|
+
const tMin = t[0], tMax = t[nAugKnots - 1];
|
|
919
|
+
const outOffset = fi * nSplineOut;
|
|
920
|
+
for (let row = 0; row < N; row++) {
|
|
921
|
+
let x = inData[row * cols + fi];
|
|
922
|
+
// Handle extrapolation
|
|
923
|
+
if (extrapolation === 'constant') {
|
|
924
|
+
x = Math.max(tMin, Math.min(tMax, x));
|
|
925
|
+
}
|
|
926
|
+
// Cox-de Boor recursion: compute all B-splines at once
|
|
927
|
+
const basis = bsplineBasis(x, t, nAugKnots, degree, nSplines);
|
|
928
|
+
const splineStart = includeBias ? 0 : 1;
|
|
929
|
+
for (let si = 0; si < nSplineOut; si++) {
|
|
930
|
+
out[row * outCols + outOffset + si] = basis[splineStart + si];
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
publishMatrix(node, out, N, outCols, namespace);
|
|
935
|
+
}
|
|
936
|
+
export function executeNormContinuous(node, inputNames, namespace, N, resolved) {
|
|
937
|
+
const origTd = getTensorData(getAttr(node, 'orig_points'), resolved);
|
|
938
|
+
const normTd = getTensorData(getAttr(node, 'norm_points'), resolved);
|
|
939
|
+
const offTd = getTensorData(getAttr(node, 'point_offsets'), resolved);
|
|
940
|
+
if (!origTd || !normTd)
|
|
941
|
+
return;
|
|
942
|
+
const orig = origTd.data;
|
|
943
|
+
const norm = normTd.data;
|
|
944
|
+
const offsets = offTd ? Array.from(offTd.data).map(Math.round) : [0, orig.length];
|
|
945
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
946
|
+
const nFeatures = offsets.length > 1 ? offsets.length - 1 : 1;
|
|
947
|
+
const out = new Float64Array(N * nFeatures);
|
|
948
|
+
for (let fi = 0; fi < nFeatures; fi++) {
|
|
949
|
+
const start = offsets[fi] ?? 0;
|
|
950
|
+
const end = offsets[fi + 1] ?? orig.length;
|
|
951
|
+
const inCol = Math.min(fi, cols - 1);
|
|
952
|
+
for (let row = 0; row < N; row++) {
|
|
953
|
+
const x = inData[row * cols + inCol];
|
|
954
|
+
let y;
|
|
955
|
+
if (x <= orig[start]) {
|
|
956
|
+
y = norm[start];
|
|
957
|
+
}
|
|
958
|
+
else if (x >= orig[end - 1]) {
|
|
959
|
+
y = norm[end - 1];
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
// Binary search for the interval
|
|
963
|
+
let lo = start, hi = end - 2;
|
|
964
|
+
while (lo < hi) {
|
|
965
|
+
const mid = (lo + hi) >> 1;
|
|
966
|
+
if (orig[mid + 1] <= x)
|
|
967
|
+
lo = mid + 1;
|
|
968
|
+
else
|
|
969
|
+
hi = mid;
|
|
970
|
+
}
|
|
971
|
+
const t = (x - orig[lo]) / (orig[lo + 1] - orig[lo]);
|
|
972
|
+
y = norm[lo] + t * (norm[lo + 1] - norm[lo]);
|
|
973
|
+
}
|
|
974
|
+
out[row * nFeatures + fi] = y;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
publishMatrix(node, out, N, nFeatures, namespace);
|
|
978
|
+
}
|
|
979
|
+
export function executeWeightedSum(node, inputNames, namespace, N, resolved) {
|
|
980
|
+
const weightsTd = getTensorData(getAttr(node, 'weights'), resolved);
|
|
981
|
+
if (!weightsTd)
|
|
982
|
+
return;
|
|
983
|
+
const weights = weightsTd.data;
|
|
984
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
985
|
+
const out = new Float64Array(N * cols);
|
|
986
|
+
for (let row = 0; row < N; row++) {
|
|
987
|
+
for (let c = 0; c < cols; c++) {
|
|
988
|
+
out[row * cols + c] = inData[row * cols + c] * (weights[c] ?? 1);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
publishMatrix(node, out, N, cols, namespace);
|
|
992
|
+
}
|
|
993
|
+
export function executeImputer(node, inputNames, namespace, N, resolved) {
|
|
994
|
+
const fillTd = getTensorData(getAttr(node, 'fill_tensor'), resolved);
|
|
995
|
+
const fills = fillTd ? fillTd.data : new Float64Array(0);
|
|
996
|
+
const outputs = node.outputs ?? [];
|
|
997
|
+
if (inputNames.length === 1) {
|
|
998
|
+
// Single matrix input [N, F] — impute column-wise
|
|
999
|
+
const td = namespace.get(inputNames[0]);
|
|
1000
|
+
if (!td)
|
|
1001
|
+
return;
|
|
1002
|
+
const src = td.data;
|
|
1003
|
+
const cols = td.shape.length >= 2 ? td.shape.slice(1).reduce((a, b) => a * b, 1) : 1;
|
|
1004
|
+
const out = new Float64Array(N * cols);
|
|
1005
|
+
for (let row = 0; row < N; row++) {
|
|
1006
|
+
for (let c = 0; c < cols; c++) {
|
|
1007
|
+
const v = src[row * cols + c];
|
|
1008
|
+
out[row * cols + c] = isNaN(v) ? (fills[c] ?? 0) : v;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
const outEntry = outputs[0];
|
|
1012
|
+
if (outEntry) {
|
|
1013
|
+
namespace.set(outEntry.name, {
|
|
1014
|
+
dtype: 'FLOAT64',
|
|
1015
|
+
shape: cols > 1 ? [N, cols] : [N],
|
|
1016
|
+
data: out,
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
// Multiple named inputs — one per feature
|
|
1022
|
+
for (let fi = 0; fi < inputNames.length; fi++) {
|
|
1023
|
+
const td = namespace.get(inputNames[fi]);
|
|
1024
|
+
if (!td)
|
|
1025
|
+
continue;
|
|
1026
|
+
const src = td.data;
|
|
1027
|
+
const fill = fills[fi] ?? 0;
|
|
1028
|
+
const out = new Float64Array(N);
|
|
1029
|
+
for (let row = 0; row < N; row++) {
|
|
1030
|
+
const v = src[row];
|
|
1031
|
+
out[row] = isNaN(v) ? fill : v;
|
|
1032
|
+
}
|
|
1033
|
+
const outEntry = outputs[fi];
|
|
1034
|
+
if (outEntry)
|
|
1035
|
+
namespace.set(outEntry.name, { dtype: 'FLOAT64', shape: [N], data: out });
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
export function executePCA(node, inputNames, namespace, N, resolved) {
|
|
1039
|
+
const compTd = getTensorData(getAttr(node, 'components'), resolved);
|
|
1040
|
+
if (!compTd)
|
|
1041
|
+
return;
|
|
1042
|
+
// components shape: [k, n_features]
|
|
1043
|
+
const k = compTd.shape[0] ?? 1;
|
|
1044
|
+
const nFeatures = compTd.shape[1] ?? (compTd.data.length / k);
|
|
1045
|
+
const comp = compTd.data;
|
|
1046
|
+
const meanTd = getTensorData(getAttr(node, 'mean'), resolved);
|
|
1047
|
+
const mean = meanTd?.data;
|
|
1048
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
1049
|
+
const out = new Float64Array(N * k);
|
|
1050
|
+
for (let row = 0; row < N; row++) {
|
|
1051
|
+
for (let j = 0; j < k; j++) {
|
|
1052
|
+
let sum = 0;
|
|
1053
|
+
for (let i = 0; i < nFeatures; i++) {
|
|
1054
|
+
const x = inData[row * cols + i] - (mean?.[i] ?? 0);
|
|
1055
|
+
sum += x * comp[j * nFeatures + i];
|
|
1056
|
+
}
|
|
1057
|
+
out[row * k + j] = sum;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
publishMatrix(node, out, N, k, namespace);
|
|
1061
|
+
}
|
|
1062
|
+
function bsplineBasis(x, t, n, degree, nSplines) {
|
|
1063
|
+
// Cox-de Boor algorithm: compute all B-spline basis values at x
|
|
1064
|
+
const b = new Float64Array(n);
|
|
1065
|
+
// Degree 0: indicator functions
|
|
1066
|
+
for (let i = 0; i < n - 1; i++) {
|
|
1067
|
+
b[i] = (x >= t[i] && x < t[i + 1]) ? 1 : 0;
|
|
1068
|
+
}
|
|
1069
|
+
// Handle right boundary: last spline should be 1 at t[-1]
|
|
1070
|
+
if (x === t[n - 1])
|
|
1071
|
+
b[n - degree - 2] = 1;
|
|
1072
|
+
// Build up degrees 1..degree
|
|
1073
|
+
for (let d = 1; d <= degree; d++) {
|
|
1074
|
+
for (let i = 0; i < n - d - 1; i++) {
|
|
1075
|
+
const left = t[i + d] - t[i] > 0
|
|
1076
|
+
? ((x - t[i]) / (t[i + d] - t[i])) * b[i]
|
|
1077
|
+
: 0;
|
|
1078
|
+
const right = t[i + d + 1] - t[i + 1] > 0
|
|
1079
|
+
? ((t[i + d + 1] - x) / (t[i + d + 1] - t[i + 1])) * b[i + 1]
|
|
1080
|
+
: 0;
|
|
1081
|
+
b[i] = left + right;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return b.slice(0, nSplines);
|
|
1085
|
+
}
|
|
1086
|
+
// ── TruncatedSVD ──────────────────────────────────────────────────────────────
|
|
1087
|
+
export function executeTruncatedSVD(node, inputNames, namespace, N, resolved) {
|
|
1088
|
+
const compData = getTensorData(getAttr(node, 'components'), resolved);
|
|
1089
|
+
if (!compData)
|
|
1090
|
+
return;
|
|
1091
|
+
// components shape: [n_components, n_features]
|
|
1092
|
+
const compShape = compData.shape;
|
|
1093
|
+
const nComponents = compShape.length >= 2 ? Number(compShape[0]) : 1;
|
|
1094
|
+
const nFeatures = compShape.length >= 2 ? Number(compShape[1]) : compData.data.length;
|
|
1095
|
+
const comp = compData.data;
|
|
1096
|
+
const { data: inData, cols } = getMatrixInput(inputNames, namespace, N);
|
|
1097
|
+
if (cols !== nFeatures)
|
|
1098
|
+
return; // shape mismatch
|
|
1099
|
+
// X_out = X @ components.T → shape [N, nComponents]
|
|
1100
|
+
const out = new Float64Array(N * nComponents);
|
|
1101
|
+
for (let row = 0; row < N; row++) {
|
|
1102
|
+
for (let c = 0; c < nComponents; c++) {
|
|
1103
|
+
let acc = 0;
|
|
1104
|
+
for (let k = 0; k < nFeatures; k++) {
|
|
1105
|
+
acc += inData[row * cols + k] * comp[c * nFeatures + k];
|
|
1106
|
+
}
|
|
1107
|
+
out[row * nComponents + c] = acc;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
publishMatrix(node, out, N, nComponents, namespace);
|
|
1111
|
+
}
|
|
1112
|
+
//# sourceMappingURL=preprocess.js.map
|