@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/README.md +135 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +58 -0
- package/dist/context.d.ts +18 -0
- package/dist/context.js +44 -0
- package/dist/ffi.d.ts +45 -0
- package/dist/ffi.js +115 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +34 -0
- package/dist/methodResult.d.ts +28 -0
- package/dist/methodResult.js +143 -0
- package/dist/model.d.ts +197 -0
- package/dist/model.js +577 -0
- package/dist/n4m.js +16 -0
- package/dist/n4m.wasm +0 -0
- package/dist/preprocessing.d.ts +16 -0
- package/dist/preprocessing.js +121 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.js +76 -0
- package/package.json +38 -0
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
|
+
}
|