@nirs4all/methods 1.0.6

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/dist/model.js ADDED
@@ -0,0 +1,577 @@
1
+ // SPDX-License-Identifier: CECILL-2.1
2
+ //
3
+ // PLS regression model wrapper. Internally uses the `n4m_wasm_pls_fit`
4
+ // helper which takes raw double pointers and works around an Emscripten
5
+ // 5.0.7 codegen issue for matrix-view-pointer args (see README "Status").
6
+ import { checkStatus, getModule } from "./ffi.js";
7
+ function _malloc_f64(M, len) {
8
+ const ptr = M._malloc(len * 8);
9
+ return { ptr, len };
10
+ }
11
+ function _copy_in(M, buf, ptr) {
12
+ if (buf.length === 0)
13
+ return;
14
+ M.HEAPF64.set(buf, ptr >>> 3);
15
+ }
16
+ function _read_out(M, ptr, len) {
17
+ return new Float64Array(M.HEAPU8.buffer, ptr, len).slice();
18
+ }
19
+ /** Fit a SIMPLS PLS-regression model on (X, Y).
20
+ *
21
+ * @param X row-major (n × p) input matrix.
22
+ * @param Y row-major (n × q) target matrix.
23
+ * @param n_components number of latent components.
24
+ */
25
+ export function fitPls(X, Y, n_components) {
26
+ if (X.rows !== Y.rows) {
27
+ throw new Error(`X.rows (${X.rows}) must equal Y.rows (${Y.rows})`);
28
+ }
29
+ const M = getModule();
30
+ const n = X.rows, p = X.cols, q = Y.cols;
31
+ const xBuf = _malloc_f64(M, n * p);
32
+ const yBuf = _malloc_f64(M, n * q);
33
+ const coefsBuf = _malloc_f64(M, p * q);
34
+ const xmBuf = _malloc_f64(M, p);
35
+ const ymBuf = _malloc_f64(M, q);
36
+ try {
37
+ _copy_in(M, X.data, xBuf.ptr);
38
+ _copy_in(M, Y.data, yBuf.ptr);
39
+ // Uses the public ABI helper (1.13+): raw double pointers
40
+ // + ints, no matrix-view structs in the JS↔WASM boundary.
41
+ const status = M.ccall("n4m_estimators_pls_fit", "number", ["number", "number", "number", "number", "number",
42
+ "number", "number", "number", "number", "number"], [xBuf.ptr, yBuf.ptr, n, p, q, n_components,
43
+ coefsBuf.ptr, xmBuf.ptr, ymBuf.ptr, 0]);
44
+ checkStatus(status);
45
+ return {
46
+ coefficients: _read_out(M, coefsBuf.ptr, p * q),
47
+ xMean: _read_out(M, xmBuf.ptr, p),
48
+ yMean: _read_out(M, ymBuf.ptr, q),
49
+ n_features: p,
50
+ n_targets: q,
51
+ };
52
+ }
53
+ finally {
54
+ M._free(xBuf.ptr);
55
+ M._free(yBuf.ptr);
56
+ M._free(coefsBuf.ptr);
57
+ M._free(xmBuf.ptr);
58
+ M._free(ymBuf.ptr);
59
+ }
60
+ }
61
+ /** Predict from a fitted PlsModel for new X (row-major n_new × p). */
62
+ export function predictPls(model, X_new) {
63
+ if (X_new.cols !== model.n_features) {
64
+ throw new Error(`X_new.cols (${X_new.cols}) must equal n_features (` +
65
+ `${model.n_features})`);
66
+ }
67
+ const M = getModule();
68
+ const n_new = X_new.rows, p = model.n_features, q = model.n_targets;
69
+ const xBuf = _malloc_f64(M, n_new * p);
70
+ const coefsBuf = _malloc_f64(M, p * q);
71
+ const xmBuf = _malloc_f64(M, p);
72
+ const ymBuf = _malloc_f64(M, q);
73
+ const predsBuf = _malloc_f64(M, n_new * q);
74
+ try {
75
+ _copy_in(M, X_new.data, xBuf.ptr);
76
+ _copy_in(M, model.coefficients, coefsBuf.ptr);
77
+ _copy_in(M, model.xMean, xmBuf.ptr);
78
+ _copy_in(M, model.yMean, ymBuf.ptr);
79
+ const status = M.ccall("n4m_wasm_pls_predict_from_coeffs", "number", ["number", "number", "number", "number",
80
+ "number", "number", "number", "number"], [xBuf.ptr, n_new, p, q,
81
+ coefsBuf.ptr, xmBuf.ptr, ymBuf.ptr, predsBuf.ptr]);
82
+ checkStatus(status);
83
+ return {
84
+ data: _read_out(M, predsBuf.ptr, n_new * q),
85
+ rows: n_new,
86
+ cols: q,
87
+ };
88
+ }
89
+ finally {
90
+ M._free(xBuf.ptr);
91
+ M._free(coefsBuf.ptr);
92
+ M._free(xmBuf.ptr);
93
+ M._free(ymBuf.ptr);
94
+ M._free(predsBuf.ptr);
95
+ }
96
+ }
97
+ /** Fit any coefficient-based libn4m model by its catalog `type` token.
98
+ *
99
+ * Tier A (PLS / PLSRegression / PCR / PLSCanonical / PLSSVD / PLSDA) routes
100
+ * through the algorithm-enum model API; Tier B (Ridge, RidgePLS, CPPLS, ...)
101
+ * through the matching standalone fit. The `params` vector is the documented
102
+ * positional contract per model (see the studio-lite catalog). Unknown or
103
+ * non-coefficient tokens throw (the C side returns N4M_ERR_NOT_IMPLEMENTED).
104
+ *
105
+ * @param model catalog `type` token, e.g. `'Ridge'`.
106
+ * @param X row-major (n × p) input matrix.
107
+ * @param Y row-major (n × q) target matrix.
108
+ * @param n_components number of latent components (used by the PLS family).
109
+ * @param params positional hyper-parameter vector for the model.
110
+ */
111
+ export function fitModel(model, X, Y, n_components, params = []) {
112
+ if (X.rows !== Y.rows) {
113
+ throw new Error(`X.rows (${X.rows}) must equal Y.rows (${Y.rows})`);
114
+ }
115
+ const M = getModule();
116
+ const n = X.rows, p = X.cols, q = Y.cols;
117
+ const xBuf = _malloc_f64(M, n * p);
118
+ const yBuf = _malloc_f64(M, n * q);
119
+ const coefsBuf = _malloc_f64(M, p * q);
120
+ const xmBuf = _malloc_f64(M, p);
121
+ const ymBuf = _malloc_f64(M, q);
122
+ const interBuf = _malloc_f64(M, q);
123
+ const hasInterBuf = M._malloc(4); // int32 flag: 1 iff a genuine intercept
124
+ const pPtr = params.length > 0 ? _malloc_f64(M, params.length) : { ptr: 0, len: 0 };
125
+ try {
126
+ _copy_in(M, X.data, xBuf.ptr);
127
+ _copy_in(M, Y.data, yBuf.ptr);
128
+ if (params.length > 0)
129
+ _copy_in(M, Float64Array.from(params), pPtr.ptr);
130
+ const status = M.ccall("n4m_wasm_model_fit", "number", ["string", "number", "number", "number", "number",
131
+ "number", "number", "number", "number",
132
+ "number", "number", "number", "number", "number", "number"], [model, pPtr.ptr, params.length, xBuf.ptr, yBuf.ptr,
133
+ n, p, q, n_components,
134
+ coefsBuf.ptr, xmBuf.ptr, ymBuf.ptr, interBuf.ptr,
135
+ hasInterBuf, 0]);
136
+ checkStatus(status);
137
+ // Only models with a genuine affine intercept (currently Ridge) report
138
+ // has_intercept=1; the PLS/PCR family and the PLS-based Tier-B fits
139
+ // predict via the centred form and carry no intercept (kept null so a
140
+ // caller never adds a misleading zero/y_mean term to x.B).
141
+ const hasIntercept = M.HEAP32[hasInterBuf >> 2] === 1;
142
+ return {
143
+ coefficients: _read_out(M, coefsBuf.ptr, p * q),
144
+ xMean: _read_out(M, xmBuf.ptr, p),
145
+ yMean: _read_out(M, ymBuf.ptr, q),
146
+ intercept: hasIntercept ? _read_out(M, interBuf.ptr, q) : null,
147
+ n_features: p,
148
+ n_targets: q,
149
+ };
150
+ }
151
+ finally {
152
+ M._free(xBuf.ptr);
153
+ M._free(yBuf.ptr);
154
+ M._free(coefsBuf.ptr);
155
+ M._free(xmBuf.ptr);
156
+ M._free(ymBuf.ptr);
157
+ M._free(interBuf.ptr);
158
+ M._free(hasInterBuf);
159
+ if (pPtr.ptr !== 0)
160
+ M._free(pPtr.ptr);
161
+ }
162
+ }
163
+ /** Predict from a fitted {@link FittedModel} for new X (row-major n_new × p). */
164
+ export function predictModel(model, X_new) {
165
+ if (X_new.cols !== model.n_features) {
166
+ throw new Error(`X_new.cols (${X_new.cols}) must equal n_features (` +
167
+ `${model.n_features})`);
168
+ }
169
+ const M = getModule();
170
+ const n_new = X_new.rows, p = model.n_features, q = model.n_targets;
171
+ const xBuf = _malloc_f64(M, n_new * p);
172
+ const coefsBuf = _malloc_f64(M, p * q);
173
+ const xmBuf = _malloc_f64(M, p);
174
+ const ymBuf = _malloc_f64(M, q);
175
+ const predsBuf = _malloc_f64(M, n_new * q);
176
+ // AOM-PLS (and any future affine model) carries input-space coefficients +
177
+ // a genuine intercept and zero means — it predicts on RAW X via the
178
+ // explicit-intercept form pred = intercept + x.B. Every centred model
179
+ // (PLS family + the Tier-B fits, including Ridge) carries intercept = null
180
+ // and predicts via pred = y_mean + (x - x_mean).B. The C helper picks the
181
+ // form from whether the intercept pointer is non-NULL.
182
+ const useIntercept = model.intercept !== null;
183
+ const interBuf = useIntercept ? _malloc_f64(M, q) : { ptr: 0, len: 0 };
184
+ try {
185
+ _copy_in(M, X_new.data, xBuf.ptr);
186
+ _copy_in(M, model.coefficients, coefsBuf.ptr);
187
+ _copy_in(M, model.xMean, xmBuf.ptr);
188
+ _copy_in(M, model.yMean, ymBuf.ptr);
189
+ if (useIntercept)
190
+ _copy_in(M, model.intercept, interBuf.ptr);
191
+ const status = M.ccall("n4m_wasm_model_predict_from_coeffs", "number", ["number", "number", "number", "number",
192
+ "number", "number", "number", "number", "number"], [coefsBuf.ptr, xmBuf.ptr, ymBuf.ptr, interBuf.ptr,
193
+ xBuf.ptr, n_new, p, q, predsBuf.ptr]);
194
+ checkStatus(status);
195
+ return {
196
+ data: _read_out(M, predsBuf.ptr, n_new * q),
197
+ rows: n_new,
198
+ cols: q,
199
+ };
200
+ }
201
+ finally {
202
+ M._free(xBuf.ptr);
203
+ M._free(coefsBuf.ptr);
204
+ M._free(xmBuf.ptr);
205
+ M._free(ymBuf.ptr);
206
+ M._free(predsBuf.ptr);
207
+ if (interBuf.ptr !== 0)
208
+ M._free(interBuf.ptr);
209
+ }
210
+ }
211
+ /** Fit AOM-PLS (operator-adaptive PLS) on (X, Y).
212
+ *
213
+ * Screens a bank of strict-linear preprocessing operators by internal k-fold CV
214
+ * and fits SIMPLS on the winner, returning INPUT-SPACE coefficients so the model
215
+ * predicts on RAW X — it is therefore used WITHOUT preceding preprocessing steps
216
+ * (the screen does the preprocessing internally). Numerics are 100% libn4m
217
+ * (`n4m_model_selection_aom_pls_select`); this only builds the bank + validation plan.
218
+ *
219
+ * @param X row-major (n × p) input matrix.
220
+ * @param Y row-major (n × q) target matrix.
221
+ * @param maxComponents max latent components for the internal SIMPLS fits.
222
+ * @param nFolds internal-CV fold count for the operator screen.
223
+ * @param seed reserved (the contiguous-fold partition is deterministic).
224
+ * @param operatorKinds optional `n4m_operator_kind_t` bank override; when
225
+ * omitted a default strict bank (identity / detrend / SG smooth / SG
226
+ * derivative / finite-difference) is screened.
227
+ */
228
+ export function fitAom(X, Y, maxComponents, nFolds = 5, seed = 0, operatorKinds = []) {
229
+ if (X.rows !== Y.rows) {
230
+ throw new Error(`X.rows (${X.rows}) must equal Y.rows (${Y.rows})`);
231
+ }
232
+ const M = getModule();
233
+ const n = X.rows, p = X.cols, q = Y.cols;
234
+ const xBuf = _malloc_f64(M, n * p);
235
+ const yBuf = _malloc_f64(M, n * q);
236
+ const coefsBuf = _malloc_f64(M, p * q);
237
+ const interBuf = _malloc_f64(M, q);
238
+ const selBuf = M._malloc(4); // int32 selected operator index
239
+ const scoreBuf = _malloc_f64(M, 1);
240
+ const opsPtr = operatorKinds.length > 0
241
+ ? M._malloc(operatorKinds.length * 4)
242
+ : 0;
243
+ try {
244
+ _copy_in(M, X.data, xBuf.ptr);
245
+ _copy_in(M, Y.data, yBuf.ptr);
246
+ if (opsPtr !== 0)
247
+ M.HEAP32.set(Int32Array.from(operatorKinds), opsPtr >> 2);
248
+ const status = M.ccall("n4m_wasm_aom_fit", "number", ["number", "number", "number", "number", "number",
249
+ "number", "number", "number", "number", "number",
250
+ "number", "number", "number", "number"], [xBuf.ptr, yBuf.ptr, n, p, q,
251
+ maxComponents, nFolds, seed, opsPtr, operatorKinds.length,
252
+ coefsBuf.ptr, interBuf.ptr, selBuf, scoreBuf.ptr]);
253
+ checkStatus(status);
254
+ return {
255
+ coefficients: _read_out(M, coefsBuf.ptr, p * q),
256
+ // zero means — AOM predicts on RAW X via the affine intercept form.
257
+ xMean: new Float64Array(p),
258
+ yMean: new Float64Array(q),
259
+ intercept: _read_out(M, interBuf.ptr, q),
260
+ n_features: p,
261
+ n_targets: q,
262
+ selectedOperator: M.HEAP32[selBuf >> 2] ?? -1,
263
+ score: _read_out(M, scoreBuf.ptr, 1)[0] ?? NaN,
264
+ };
265
+ }
266
+ finally {
267
+ M._free(xBuf.ptr);
268
+ M._free(yBuf.ptr);
269
+ M._free(coefsBuf.ptr);
270
+ M._free(interBuf.ptr);
271
+ M._free(selBuf);
272
+ M._free(scoreBuf.ptr);
273
+ if (opsPtr !== 0)
274
+ M._free(opsPtr);
275
+ }
276
+ }
277
+ /** Fit POP-PLS (per-component operator-adaptive PLS) on (X, Y).
278
+ *
279
+ * Like AOM-PLS but picks one strict-linear operator PER latent component
280
+ * (`n4m_model_selection_pop_pls_select`) rather than one for the whole model, then
281
+ * returns INPUT-SPACE coefficients so it predicts on RAW X via the same affine
282
+ * intercept path — so it is used WITHOUT preceding preprocessing steps (the
283
+ * screen does the preprocessing internally). Numerics are 100% libn4m; this
284
+ * only builds the bank + validation plan.
285
+ *
286
+ * @param X row-major (n × p) input matrix.
287
+ * @param Y row-major (n × q) target matrix.
288
+ * @param maxComponents max latent components for the internal SIMPLS fits.
289
+ * @param nFolds internal-CV fold count for the operator screen.
290
+ * @param seed reserved (the contiguous-fold partition is deterministic).
291
+ * @param operatorKinds optional `n4m_operator_kind_t` bank override; when
292
+ * omitted a default strict bank (identity / detrend / SG smooth / SG
293
+ * derivative / finite-difference) is screened.
294
+ */
295
+ export function fitPop(X, Y, maxComponents, nFolds = 5, seed = 0, operatorKinds = []) {
296
+ if (X.rows !== Y.rows) {
297
+ throw new Error(`X.rows (${X.rows}) must equal Y.rows (${Y.rows})`);
298
+ }
299
+ const M = getModule();
300
+ const n = X.rows, p = X.cols, q = Y.cols;
301
+ // The selector clamps max_components to min(maxComponents, p, n-1); allocate
302
+ // the per-component op buffer to the un-clamped request (always >= clamp).
303
+ const maxComp = Math.max(1, maxComponents);
304
+ const xBuf = _malloc_f64(M, n * p);
305
+ const yBuf = _malloc_f64(M, n * q);
306
+ const coefsBuf = _malloc_f64(M, p * q);
307
+ const interBuf = _malloc_f64(M, q);
308
+ const opsOutBuf = M._malloc(maxComp * 4); // int32 per-component op indices
309
+ const nSelBuf = M._malloc(4); // int32 selected component count
310
+ const scoreBuf = _malloc_f64(M, 1);
311
+ const opsPtr = operatorKinds.length > 0
312
+ ? M._malloc(operatorKinds.length * 4)
313
+ : 0;
314
+ try {
315
+ _copy_in(M, X.data, xBuf.ptr);
316
+ _copy_in(M, Y.data, yBuf.ptr);
317
+ if (opsPtr !== 0)
318
+ M.HEAP32.set(Int32Array.from(operatorKinds), opsPtr >> 2);
319
+ const status = M.ccall("n4m_wasm_pop_fit", "number", ["number", "number", "number", "number", "number",
320
+ "number", "number", "number", "number", "number",
321
+ "number", "number", "number", "number", "number"], [xBuf.ptr, yBuf.ptr, n, p, q,
322
+ maxComp, nFolds, seed, opsPtr, operatorKinds.length,
323
+ coefsBuf.ptr, interBuf.ptr, opsOutBuf, nSelBuf, scoreBuf.ptr]);
324
+ checkStatus(status);
325
+ const nSel = Math.max(0, M.HEAP32[nSelBuf >> 2] ?? 0);
326
+ const selectedOperators = [];
327
+ for (let k = 0; k < nSel; ++k) {
328
+ selectedOperators.push(M.HEAP32[(opsOutBuf >> 2) + k] ?? -1);
329
+ }
330
+ return {
331
+ coefficients: _read_out(M, coefsBuf.ptr, p * q),
332
+ // zero means — POP predicts on RAW X via the affine intercept form.
333
+ xMean: new Float64Array(p),
334
+ yMean: new Float64Array(q),
335
+ intercept: _read_out(M, interBuf.ptr, q),
336
+ n_features: p,
337
+ n_targets: q,
338
+ selectedOperators,
339
+ selectedComponents: nSel,
340
+ score: _read_out(M, scoreBuf.ptr, 1)[0] ?? NaN,
341
+ };
342
+ }
343
+ finally {
344
+ M._free(xBuf.ptr);
345
+ M._free(yBuf.ptr);
346
+ M._free(coefsBuf.ptr);
347
+ M._free(interBuf.ptr);
348
+ M._free(opsOutBuf);
349
+ M._free(nSelBuf);
350
+ M._free(scoreBuf.ptr);
351
+ if (opsPtr !== 0)
352
+ M._free(opsPtr);
353
+ }
354
+ }
355
+ /** Fit the AOM Ridge simplex blender (n4m_ensemble_aom_ridge_blender_fit): builds
356
+ * a strict-linear chain bank internally, OOF-blends (chain, λ) Ridge candidates
357
+ * over `cv` contiguous folds, and returns the weighted final INPUT-SPACE
358
+ * coefficients + intercept — so it predicts on RAW X via the affine form
359
+ * y = intercept + X.B (used WITHOUT preceding preprocessing). */
360
+ export function fitAomRidge(X, Y, opts = {}) {
361
+ if (X.rows !== Y.rows)
362
+ throw new Error(`X.rows (${X.rows}) must equal Y.rows (${Y.rows})`);
363
+ const M = getModule();
364
+ const n = X.rows, p = X.cols, q = Y.cols;
365
+ const profile = opts.profile ?? 0;
366
+ const cv = opts.cv ?? 5;
367
+ const regularizer = opts.regularizer ?? 0.01;
368
+ const lambdas = opts.ridgeLambdas ?? [];
369
+ const xBuf = _malloc_f64(M, n * p);
370
+ const yBuf = _malloc_f64(M, n * q);
371
+ const coefsBuf = _malloc_f64(M, p * q);
372
+ const interBuf = _malloc_f64(M, q);
373
+ const lamBuf = lambdas.length > 0 ? _malloc_f64(M, lambdas.length) : { ptr: 0 };
374
+ try {
375
+ _copy_in(M, X.data, xBuf.ptr);
376
+ _copy_in(M, Y.data, yBuf.ptr);
377
+ if (lamBuf.ptr !== 0)
378
+ _copy_in(M, Float64Array.from(lambdas), lamBuf.ptr);
379
+ const status = M.ccall("n4m_wasm_aom_ridge_fit", "number", ["number", "number", "number", "number", "number",
380
+ "number", "number", "number", "number", "number",
381
+ "number", "number"], [xBuf.ptr, yBuf.ptr, n, p, q,
382
+ profile, cv, lamBuf.ptr, lambdas.length, regularizer,
383
+ coefsBuf.ptr, interBuf.ptr]);
384
+ checkStatus(status);
385
+ return {
386
+ coefficients: _read_out(M, coefsBuf.ptr, p * q),
387
+ xMean: new Float64Array(p),
388
+ yMean: new Float64Array(q),
389
+ intercept: _read_out(M, interBuf.ptr, q),
390
+ n_features: p,
391
+ n_targets: q,
392
+ };
393
+ }
394
+ finally {
395
+ M._free(xBuf.ptr);
396
+ M._free(yBuf.ptr);
397
+ M._free(coefsBuf.ptr);
398
+ M._free(interBuf.ptr);
399
+ if (lamBuf.ptr !== 0)
400
+ M._free(lamBuf.ptr);
401
+ }
402
+ }
403
+ /** Fit the AOM operator-PLS score stack with Ridge head
404
+ * (n4m_ensemble_aom_operator_pls_stack_fit). SINGLE-TARGET only (Y must be
405
+ * n × 1). Returns the stack folded into INPUT-SPACE coefficients + intercept,
406
+ * so it predicts on RAW X via the affine form (used WITHOUT preprocessing). */
407
+ export function fitAomStack(X, Y, opts = {}) {
408
+ if (X.rows !== Y.rows)
409
+ throw new Error(`X.rows (${X.rows}) must equal Y.rows (${Y.rows})`);
410
+ if (Y.cols !== 1)
411
+ throw new Error("fitAomStack is single-target: Y must have exactly 1 column");
412
+ const M = getModule();
413
+ const n = X.rows, p = X.cols, q = 1;
414
+ const profile = opts.profile ?? 0;
415
+ const cv = opts.cv ?? 5;
416
+ const maxComponents = opts.maxComponents ?? 15;
417
+ const stdPenalty = opts.stdPenalty ?? 0;
418
+ const gapPenalty = opts.gapPenalty ?? 0;
419
+ const alphas = opts.alphas ?? [];
420
+ const xBuf = _malloc_f64(M, n * p);
421
+ const yBuf = _malloc_f64(M, n * q);
422
+ const coefsBuf = _malloc_f64(M, p * q);
423
+ const interBuf = _malloc_f64(M, q);
424
+ const alphaBuf = alphas.length > 0 ? _malloc_f64(M, alphas.length) : { ptr: 0 };
425
+ try {
426
+ _copy_in(M, X.data, xBuf.ptr);
427
+ _copy_in(M, Y.data, yBuf.ptr);
428
+ if (alphaBuf.ptr !== 0)
429
+ _copy_in(M, Float64Array.from(alphas), alphaBuf.ptr);
430
+ const status = M.ccall("n4m_wasm_aom_stack_fit", "number", ["number", "number", "number", "number", "number",
431
+ "number", "number", "number", "number", "number",
432
+ "number", "number", "number", "number"], [xBuf.ptr, yBuf.ptr, n, p, q,
433
+ profile, cv, maxComponents, alphaBuf.ptr, alphas.length,
434
+ stdPenalty, gapPenalty, coefsBuf.ptr, interBuf.ptr]);
435
+ checkStatus(status);
436
+ return {
437
+ coefficients: _read_out(M, coefsBuf.ptr, p * q),
438
+ xMean: new Float64Array(p),
439
+ yMean: new Float64Array(q),
440
+ intercept: _read_out(M, interBuf.ptr, q),
441
+ n_features: p,
442
+ n_targets: q,
443
+ };
444
+ }
445
+ finally {
446
+ M._free(xBuf.ptr);
447
+ M._free(yBuf.ptr);
448
+ M._free(coefsBuf.ptr);
449
+ M._free(interBuf.ptr);
450
+ if (alphaBuf.ptr !== 0)
451
+ M._free(alphaBuf.ptr);
452
+ }
453
+ }
454
+ const SPLIT_KIND_CODE = {
455
+ KennardStone: 0,
456
+ SPXY: 1,
457
+ KMeans: 2,
458
+ KBinsStratified: 3,
459
+ DataTwinning: 4,
460
+ SystematicCircular: 5,
461
+ };
462
+ /** Compute a single train/test split over the rows of X (and Y) via libn4m's
463
+ * splitters, returning a `Uint8Array` mask of length n where 1 = test, 0 = train.
464
+ *
465
+ * Numerics are 100% libn4m (`n4m_wasm_split` → n4m_split_*). KennardStone and
466
+ * SPXY are deterministic; KMeans and KBinsStratified use `opts.seed`. SPXY and
467
+ * KBinsStratified need Y; KennardStone and KMeans use X only.
468
+ *
469
+ * @param kind splitter strategy.
470
+ * @param X row-major (n × p) input matrix.
471
+ * @param Y row-major (n × q) target matrix (required for SPXY / KBinsStratified).
472
+ * @param opts split options (testSize / seed / maxIter / nBins / strategy).
473
+ */
474
+ export function computeSplit(kind, X, Y, opts = {}) {
475
+ const M = getModule();
476
+ const n = X.rows, p = X.cols;
477
+ const q = Y ? Y.cols : 1;
478
+ const testSize = opts.testSize ?? 0.25;
479
+ const seed = (opts.seed ?? 0) >>> 0;
480
+ // p0/p1 generic int params: KMeans → maxIter; KBins → nBins, strategy.
481
+ let p0 = 0, p1 = 0;
482
+ if (kind === "KMeans")
483
+ p0 = opts.maxIter ?? 100;
484
+ if (kind === "KBinsStratified") {
485
+ p0 = opts.nBins ?? 5;
486
+ p1 = opts.strategy ?? 0;
487
+ }
488
+ const xBuf = _malloc_f64(M, n * p);
489
+ const yBuf = Y ? _malloc_f64(M, n * q) : { ptr: 0, len: 0 };
490
+ const maskBuf = M._malloc(n * 4); // int32[n]
491
+ try {
492
+ _copy_in(M, X.data, xBuf.ptr);
493
+ if (Y)
494
+ _copy_in(M, Y.data, yBuf.ptr);
495
+ const status = M.ccall("n4m_wasm_split", "number", ["number", "number", "number", "number", "number",
496
+ "number", "number", "number", "number", "number", "number"], [SPLIT_KIND_CODE[kind], testSize, seed, p0, p1,
497
+ xBuf.ptr, yBuf.ptr, n, p, q, maskBuf]);
498
+ checkStatus(status);
499
+ const mask = new Uint8Array(n);
500
+ for (let i = 0; i < n; i++)
501
+ mask[i] = M.HEAP32[(maskBuf >> 2) + i] === 1 ? 1 : 0;
502
+ return mask;
503
+ }
504
+ finally {
505
+ M._free(xBuf.ptr);
506
+ if (yBuf.ptr !== 0)
507
+ M._free(yBuf.ptr);
508
+ M._free(maskBuf);
509
+ }
510
+ }
511
+ /** Compute a train/test split and return the ordered train/test indices from libn4m.
512
+ *
513
+ * Unlike {@link computeSplit}, this preserves splitter-specific ordering, which matters
514
+ * for strict parity with the native Python binding.
515
+ */
516
+ export function computeSplitIndices(kind, X, Y, opts = {}) {
517
+ const M = getModule();
518
+ const n = X.rows, p = X.cols;
519
+ const q = Y ? Y.cols : 1;
520
+ const testSize = opts.testSize ?? 0.25;
521
+ const seed = (opts.seed ?? 0) >>> 0;
522
+ let p0 = 0, p1 = 0;
523
+ if (kind === "KMeans")
524
+ p0 = opts.maxIter ?? 100;
525
+ if (kind === "KBinsStratified") {
526
+ p0 = opts.nBins ?? 5;
527
+ p1 = opts.strategy ?? 0;
528
+ }
529
+ const xBuf = _malloc_f64(M, n * p);
530
+ const yBuf = Y ? _malloc_f64(M, n * q) : { ptr: 0, len: 0 };
531
+ const trainBuf = M._malloc(n * 4);
532
+ const testBuf = M._malloc(n * 4);
533
+ const nTrainBuf = M._malloc(4);
534
+ const nTestBuf = M._malloc(4);
535
+ try {
536
+ _copy_in(M, X.data, xBuf.ptr);
537
+ if (Y)
538
+ _copy_in(M, Y.data, yBuf.ptr);
539
+ const status = M.ccall("n4m_wasm_split_indices", "number", ["number", "number", "number", "number", "number",
540
+ "number", "number", "number", "number", "number",
541
+ "number", "number", "number", "number"], [SPLIT_KIND_CODE[kind], testSize, seed, p0, p1,
542
+ xBuf.ptr, yBuf.ptr, n, p, q,
543
+ trainBuf, nTrainBuf, testBuf, nTestBuf]);
544
+ checkStatus(status);
545
+ const nTrain = M.HEAP32[nTrainBuf >> 2] ?? 0;
546
+ const nTest = M.HEAP32[nTestBuf >> 2] ?? 0;
547
+ return {
548
+ trainIndices: new Int32Array(M.HEAPU8.buffer, trainBuf, nTrain).slice(),
549
+ testIndices: new Int32Array(M.HEAPU8.buffer, testBuf, nTest).slice(),
550
+ };
551
+ }
552
+ finally {
553
+ M._free(xBuf.ptr);
554
+ if (yBuf.ptr !== 0)
555
+ M._free(yBuf.ptr);
556
+ M._free(trainBuf);
557
+ M._free(testBuf);
558
+ M._free(nTrainBuf);
559
+ M._free(nTestBuf);
560
+ }
561
+ }
562
+ /* Legacy class wrapper preserved for backwards compat with the
563
+ * scaffold; not yet exposed via index.ts. */
564
+ export class Model {
565
+ _data;
566
+ constructor(data) { this._data = data; }
567
+ static fit(_ctx, _cfg, X, Y, n_components = 3) {
568
+ return new Model(fitPls(X, Y, n_components));
569
+ }
570
+ predict(_ctx, X_new) {
571
+ return predictPls(this._data, X_new);
572
+ }
573
+ get coefficients() { return this._data.coefficients; }
574
+ get xMean() { return this._data.xMean; }
575
+ get yMean() { return this._data.yMean; }
576
+ destroy() { }
577
+ }