@nirs4all/methods 1.0.18 → 1.1.0
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 +37 -2
- package/dist/estimatorRoles.d.ts +139 -0
- package/dist/estimatorRoles.js +531 -0
- package/dist/estimatorRolesGenerated.d.ts +4711 -0
- package/dist/estimatorRolesGenerated.js +3825 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/methodResult.d.ts +2 -0
- package/dist/methodResult.js +33 -1
- package/dist/model.d.ts +4 -1
- package/dist/model.js +6 -3
- package/dist/n4m.js +1 -1
- package/dist/n4m.wasm +0 -0
- package/dist/nativeAugmentation.d.ts +29 -0
- package/dist/nativeAugmentation.js +49 -0
- package/dist/nativeModel.d.ts +17 -0
- package/dist/nativeModel.js +100 -0
- package/dist/nativePreprocessingPipeline.d.ts +42 -0
- package/dist/nativePreprocessingPipeline.js +246 -0
- package/dist/nativeSplitter.d.ts +23 -0
- package/dist/nativeSplitter.js +116 -0
- package/dist/selection.d.ts +6 -0
- package/dist/selection.js +245 -0
- package/dist/types.d.ts +14 -13
- package/dist/types.js +14 -13
- package/package.json +3 -3
package/dist/n4m.wasm
CHANGED
|
Binary file
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Matrix } from "./types.js";
|
|
2
|
+
declare const KINDS: {
|
|
3
|
+
readonly GaussianNoise: readonly [0, 1];
|
|
4
|
+
readonly MultiplicativeNoise: readonly [1, 1];
|
|
5
|
+
readonly SpikeNoise: readonly [2, 4];
|
|
6
|
+
readonly HeteroNoise: readonly [3, 2];
|
|
7
|
+
readonly LinearDrift: readonly [4, 4];
|
|
8
|
+
readonly PathLength: readonly [5, 2];
|
|
9
|
+
readonly BandPerturb: readonly [6, 7];
|
|
10
|
+
readonly BandMask: readonly [7, 5];
|
|
11
|
+
readonly ChannelDropout: readonly [8, 2];
|
|
12
|
+
readonly GaussJitter: readonly [9, 3];
|
|
13
|
+
readonly UnsharpMask: readonly [10, 4];
|
|
14
|
+
readonly LocalClip: readonly [11, 3];
|
|
15
|
+
readonly RotateTranslate: readonly [12, 2];
|
|
16
|
+
readonly RandomXOp: readonly [13, 3];
|
|
17
|
+
readonly ScatterSimMSC: readonly [14, 4];
|
|
18
|
+
readonly DeadBand: readonly [15, 6];
|
|
19
|
+
readonly BatchEffect: readonly [16, 4];
|
|
20
|
+
readonly SplineSmoothing: readonly [17, 0];
|
|
21
|
+
readonly SplineXPerturb: readonly [18, 4];
|
|
22
|
+
readonly SplineYPerturb: readonly [19, 2];
|
|
23
|
+
readonly SplineXSimplify: readonly [20, 2];
|
|
24
|
+
readonly SplineCurveSimplify: readonly [21, 2];
|
|
25
|
+
};
|
|
26
|
+
export type NativeAugmentationKind = keyof typeof KINDS;
|
|
27
|
+
/** Apply one seeded native augmenter to training X; no Y or fitted state. */
|
|
28
|
+
export declare function augmentNative(kind: NativeAugmentationKind, X: Matrix, params: readonly number[], seed?: number | bigint): Matrix;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CECILL-2.1
|
|
2
|
+
// Train-only X->X augmentation facade. All numerical work remains in C++.
|
|
3
|
+
import { checkStatus, getModule } from "./ffi.js";
|
|
4
|
+
const KINDS = {
|
|
5
|
+
GaussianNoise: [0, 1], MultiplicativeNoise: [1, 1],
|
|
6
|
+
SpikeNoise: [2, 4], HeteroNoise: [3, 2], LinearDrift: [4, 4],
|
|
7
|
+
PathLength: [5, 2], BandPerturb: [6, 7], BandMask: [7, 5],
|
|
8
|
+
ChannelDropout: [8, 2], GaussJitter: [9, 3], UnsharpMask: [10, 4],
|
|
9
|
+
LocalClip: [11, 3], RotateTranslate: [12, 2], RandomXOp: [13, 3],
|
|
10
|
+
ScatterSimMSC: [14, 4], DeadBand: [15, 6], BatchEffect: [16, 4],
|
|
11
|
+
SplineSmoothing: [17, 0], SplineXPerturb: [18, 4],
|
|
12
|
+
SplineYPerturb: [19, 2], SplineXSimplify: [20, 2],
|
|
13
|
+
SplineCurveSimplify: [21, 2],
|
|
14
|
+
};
|
|
15
|
+
/** Apply one seeded native augmenter to training X; no Y or fitted state. */
|
|
16
|
+
export function augmentNative(kind, X, params, seed = 0) {
|
|
17
|
+
if (!Object.prototype.hasOwnProperty.call(KINDS, kind))
|
|
18
|
+
throw new Error(`unknown native augmentation ${kind}`);
|
|
19
|
+
const [code, count] = KINDS[kind];
|
|
20
|
+
if (!Number.isSafeInteger(X.rows) || !Number.isSafeInteger(X.cols) ||
|
|
21
|
+
X.rows < 1 || X.cols < 1 || X.rows * X.cols > 2147483647 ||
|
|
22
|
+
X.data.length !== X.rows * X.cols || !X.data.every(Number.isFinite))
|
|
23
|
+
throw new Error("X must be a nonempty finite row-major matrix");
|
|
24
|
+
if (params.length !== count || !params.every(Number.isFinite))
|
|
25
|
+
throw new Error("invalid native augmentation parameter vector");
|
|
26
|
+
if (typeof seed === "number" && !Number.isSafeInteger(seed))
|
|
27
|
+
throw new Error("seed must be an exact unsigned integer");
|
|
28
|
+
const seed64 = BigInt(seed);
|
|
29
|
+
if (seed64 < 0n || seed64 > (1n << 64n) - 1n)
|
|
30
|
+
throw new Error("seed must fit unsigned int64");
|
|
31
|
+
const m = getModule();
|
|
32
|
+
const xp = m._malloc(X.data.byteLength);
|
|
33
|
+
const pp = count === 0 ? 0 : m._malloc(count * 8);
|
|
34
|
+
const op = m._malloc(X.data.byteLength);
|
|
35
|
+
try {
|
|
36
|
+
m.HEAPF64.set(X.data, xp / 8);
|
|
37
|
+
if (count > 0)
|
|
38
|
+
m.HEAPF64.set(params, pp / 8);
|
|
39
|
+
checkStatus(m.ccall("n4m_wasm_augmentation_apply", "number", Array(9).fill("number"), [code, pp, count, Number(seed64 & 0xffffffffn),
|
|
40
|
+
Number(seed64 >> 32n), xp, X.rows, X.cols, op]));
|
|
41
|
+
return { rows: X.rows, cols: X.cols,
|
|
42
|
+
data: Float64Array.from(m.HEAPF64.subarray(op / 8, op / 8 + X.data.length)) };
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
for (const ptr of [xp, pp, op])
|
|
46
|
+
if (ptr !== 0)
|
|
47
|
+
m._free(ptr);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Context } from "./context.js";
|
|
2
|
+
import type { MethodResult } from "./methodResult.js";
|
|
3
|
+
import type { Matrix } from "./types.js";
|
|
4
|
+
export declare class NativeModel {
|
|
5
|
+
private _ptr;
|
|
6
|
+
private readonly _ctx;
|
|
7
|
+
private constructor();
|
|
8
|
+
/** Copy an affine MethodResult into a standalone native model. */
|
|
9
|
+
static fromMethodResult(ctx: Context, result: MethodResult): NativeModel;
|
|
10
|
+
/** Import a complete native N4MM model payload. */
|
|
11
|
+
static fromN4mm(ctx: Context, payload: Uint8Array): NativeModel;
|
|
12
|
+
/** Predict through libn4m; no coefficients are evaluated in JS. */
|
|
13
|
+
predict(X: Matrix): Matrix;
|
|
14
|
+
/** Export the complete native model as N4MM bytes. */
|
|
15
|
+
toN4mm(): Uint8Array;
|
|
16
|
+
destroy(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CECILL-2.1
|
|
2
|
+
// Native predict-only model promoted from an affine MethodResult.
|
|
3
|
+
import { checkStatus, getModule, makeMatrixView } from "./ffi.js";
|
|
4
|
+
export class NativeModel {
|
|
5
|
+
_ptr;
|
|
6
|
+
_ctx;
|
|
7
|
+
constructor(ctx, ptr) {
|
|
8
|
+
this._ctx = ctx;
|
|
9
|
+
this._ptr = ptr;
|
|
10
|
+
}
|
|
11
|
+
/** Copy an affine MethodResult into a standalone native model. */
|
|
12
|
+
static fromMethodResult(ctx, result) {
|
|
13
|
+
const m = getModule();
|
|
14
|
+
const out = m._malloc(4);
|
|
15
|
+
try {
|
|
16
|
+
m.setValue(out, 0, "i32");
|
|
17
|
+
const status = m.ccall("n4m_model_from_method_result", "number", ["number", "number", "number"], [ctx.handle, result.handle, out]);
|
|
18
|
+
checkStatus(status, ctx.handle);
|
|
19
|
+
return new NativeModel(ctx, m.getValue(out, "i32"));
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
m._free(out);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Import a complete native N4MM model payload. */
|
|
26
|
+
static fromN4mm(ctx, payload) {
|
|
27
|
+
const m = getModule();
|
|
28
|
+
const data = m._malloc(Math.max(1, payload.byteLength));
|
|
29
|
+
const out = m._malloc(4);
|
|
30
|
+
try {
|
|
31
|
+
m.HEAPU8.set(payload, data);
|
|
32
|
+
m.setValue(out, 0, "i32");
|
|
33
|
+
const status = m.ccall("n4m_model_import_from_buffer", "number", ["number", "number", "number", "number"], [ctx.handle, data, payload.byteLength, out]);
|
|
34
|
+
checkStatus(status, ctx.handle);
|
|
35
|
+
return new NativeModel(ctx, m.getValue(out, "i32"));
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
m._free(data);
|
|
39
|
+
m._free(out);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Predict through libn4m; no coefficients are evaluated in JS. */
|
|
43
|
+
predict(X) {
|
|
44
|
+
const m = getModule();
|
|
45
|
+
const targetsPtr = m._malloc(4);
|
|
46
|
+
try {
|
|
47
|
+
checkStatus(m.ccall("n4m_model_get_n_targets", "number", ["number", "number"], [this._ptr, targetsPtr]));
|
|
48
|
+
const targets = m.getValue(targetsPtr, "i32");
|
|
49
|
+
const input = makeMatrixView(X.data, X.rows, X.cols);
|
|
50
|
+
try {
|
|
51
|
+
const output = makeMatrixView(new Float64Array(X.rows * targets), X.rows, targets);
|
|
52
|
+
try {
|
|
53
|
+
checkStatus(m.ccall("n4m_model_predict", "number", ["number", "number", "number", "number"], [this._ctx.handle, this._ptr, input.viewPtr, output.viewPtr]), this._ctx.handle);
|
|
54
|
+
return {
|
|
55
|
+
data: new Float64Array(m.HEAPF64.subarray(output.dataPtr / 8, output.dataPtr / 8 + X.rows * targets)),
|
|
56
|
+
rows: X.rows,
|
|
57
|
+
cols: targets,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
output.free();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
input.free();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
m._free(targetsPtr);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Export the complete native model as N4MM bytes. */
|
|
73
|
+
toN4mm() {
|
|
74
|
+
const m = getModule();
|
|
75
|
+
const sizePtr = m._malloc(4); // size_t on WASM32
|
|
76
|
+
const writtenPtr = m._malloc(4);
|
|
77
|
+
try {
|
|
78
|
+
checkStatus(m.ccall("n4m_model_export_size", "number", ["number", "number"], [this._ptr, sizePtr]));
|
|
79
|
+
const size = m.getValue(sizePtr, "i32");
|
|
80
|
+
const data = m._malloc(Math.max(1, size));
|
|
81
|
+
try {
|
|
82
|
+
checkStatus(m.ccall("n4m_model_export_to_buffer", "number", ["number", "number", "number", "number"], [this._ptr, data, size, writtenPtr]));
|
|
83
|
+
return new Uint8Array(m.HEAPU8.subarray(data, data + m.getValue(writtenPtr, "i32")));
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
m._free(data);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
m._free(sizePtr);
|
|
91
|
+
m._free(writtenPtr);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
destroy() {
|
|
95
|
+
if (this._ptr === 0)
|
|
96
|
+
return;
|
|
97
|
+
getModule().ccall("n4m_model_destroy", null, ["number"], [this._ptr]);
|
|
98
|
+
this._ptr = 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Context } from "./context.js";
|
|
2
|
+
import type { Matrix } from "./types.js";
|
|
3
|
+
/** The 15 pipeline kinds currently implemented by the native core. */
|
|
4
|
+
export declare enum PipelineOperatorKind {
|
|
5
|
+
IDENTITY = 0,
|
|
6
|
+
CENTER = 1,
|
|
7
|
+
AUTOSCALE = 2,
|
|
8
|
+
PARETO_SCALE = 3,
|
|
9
|
+
SNV = 4,
|
|
10
|
+
MSC = 5,
|
|
11
|
+
EMSC = 6,
|
|
12
|
+
DETREND_POLY = 7,
|
|
13
|
+
SAVGOL_SMOOTH = 8,
|
|
14
|
+
SAVGOL_DERIVATIVE = 9,
|
|
15
|
+
NORRIS_WILLIAMS = 10,
|
|
16
|
+
ASLS_BASELINE = 11,
|
|
17
|
+
OSC = 12,
|
|
18
|
+
EPO = 13,
|
|
19
|
+
WAVELET_DENOISE = 14
|
|
20
|
+
}
|
|
21
|
+
export interface PipelineStep {
|
|
22
|
+
readonly kind: PipelineOperatorKind;
|
|
23
|
+
/** Original positional native parameters; defaults remain an empty vector. */
|
|
24
|
+
readonly params: readonly number[];
|
|
25
|
+
}
|
|
26
|
+
/** Owning JS façade for an ordered, fitted C ABI preprocessing pipeline. */
|
|
27
|
+
export declare class NativePreprocessingPipeline {
|
|
28
|
+
private _ptr;
|
|
29
|
+
private readonly _ctx;
|
|
30
|
+
readonly nFeatures: number;
|
|
31
|
+
readonly steps: readonly PipelineStep[];
|
|
32
|
+
private constructor();
|
|
33
|
+
/** Fit an ordered recipe. OSC and EPO require Y; others may omit it. */
|
|
34
|
+
static fit(ctx: Context, steps: readonly PipelineStep[], X: Matrix, Y?: Matrix): NativePreprocessingPipeline;
|
|
35
|
+
/** Import fitted N4MP bytes; optionally attest against an external recipe. */
|
|
36
|
+
static fromBytes(ctx: Context, bytes: Uint8Array, expectedSteps?: readonly PipelineStep[]): NativePreprocessingPipeline;
|
|
37
|
+
/** Transform new rows using only native fitted state. */
|
|
38
|
+
transform(X: Matrix): Matrix;
|
|
39
|
+
/** Export the native fitted state and ordered recipe in N4MP format. */
|
|
40
|
+
toBytes(): Uint8Array;
|
|
41
|
+
destroy(): void;
|
|
42
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CECILL-2.1
|
|
2
|
+
// Fitted preprocessing and N4MP portability; all numerics remain in libn4m.
|
|
3
|
+
import { checkStatus, getModule, makeMatrixView } from "./ffi.js";
|
|
4
|
+
/** The 15 pipeline kinds currently implemented by the native core. */
|
|
5
|
+
export var PipelineOperatorKind;
|
|
6
|
+
(function (PipelineOperatorKind) {
|
|
7
|
+
PipelineOperatorKind[PipelineOperatorKind["IDENTITY"] = 0] = "IDENTITY";
|
|
8
|
+
PipelineOperatorKind[PipelineOperatorKind["CENTER"] = 1] = "CENTER";
|
|
9
|
+
PipelineOperatorKind[PipelineOperatorKind["AUTOSCALE"] = 2] = "AUTOSCALE";
|
|
10
|
+
PipelineOperatorKind[PipelineOperatorKind["PARETO_SCALE"] = 3] = "PARETO_SCALE";
|
|
11
|
+
PipelineOperatorKind[PipelineOperatorKind["SNV"] = 4] = "SNV";
|
|
12
|
+
PipelineOperatorKind[PipelineOperatorKind["MSC"] = 5] = "MSC";
|
|
13
|
+
PipelineOperatorKind[PipelineOperatorKind["EMSC"] = 6] = "EMSC";
|
|
14
|
+
PipelineOperatorKind[PipelineOperatorKind["DETREND_POLY"] = 7] = "DETREND_POLY";
|
|
15
|
+
PipelineOperatorKind[PipelineOperatorKind["SAVGOL_SMOOTH"] = 8] = "SAVGOL_SMOOTH";
|
|
16
|
+
PipelineOperatorKind[PipelineOperatorKind["SAVGOL_DERIVATIVE"] = 9] = "SAVGOL_DERIVATIVE";
|
|
17
|
+
PipelineOperatorKind[PipelineOperatorKind["NORRIS_WILLIAMS"] = 10] = "NORRIS_WILLIAMS";
|
|
18
|
+
PipelineOperatorKind[PipelineOperatorKind["ASLS_BASELINE"] = 11] = "ASLS_BASELINE";
|
|
19
|
+
PipelineOperatorKind[PipelineOperatorKind["OSC"] = 12] = "OSC";
|
|
20
|
+
PipelineOperatorKind[PipelineOperatorKind["EPO"] = 13] = "EPO";
|
|
21
|
+
PipelineOperatorKind[PipelineOperatorKind["WAVELET_DENOISE"] = 14] = "WAVELET_DENOISE";
|
|
22
|
+
})(PipelineOperatorKind || (PipelineOperatorKind = {}));
|
|
23
|
+
function validateMatrix(X, label) {
|
|
24
|
+
if (!Number.isSafeInteger(X.rows) || !Number.isSafeInteger(X.cols) ||
|
|
25
|
+
X.rows < 1 || X.cols < 1 || X.data.length !== X.rows * X.cols ||
|
|
26
|
+
!X.data.every(Number.isFinite)) {
|
|
27
|
+
throw new Error(`${label} must be a nonempty finite row-major matrix`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function validateSteps(steps) {
|
|
31
|
+
if (steps.length < 1 || steps.length > 256) {
|
|
32
|
+
throw new Error("native pipeline requires 1–256 ordered steps");
|
|
33
|
+
}
|
|
34
|
+
for (const step of steps) {
|
|
35
|
+
if (!Number.isInteger(step.kind) || step.kind < 0 || step.kind > 14 ||
|
|
36
|
+
!Array.isArray(step.params) || step.params.length > 256 ||
|
|
37
|
+
!step.params.every(Number.isFinite)) {
|
|
38
|
+
throw new Error("invalid or unsupported native pipeline step");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function readPlan(ptr) {
|
|
43
|
+
const m = getModule();
|
|
44
|
+
const info = m._malloc(12); // int64_t feature width, int32_t operator count
|
|
45
|
+
try {
|
|
46
|
+
checkStatus(m.ccall("n4m_pipeline_get_info", "number", ["number", "number", "number"], [ptr, info, info + 8]));
|
|
47
|
+
const features = Number(m.getValue(info, "i64"));
|
|
48
|
+
const count = m.getValue(info + 8, "i32");
|
|
49
|
+
if (!Number.isSafeInteger(features) || features < 1 || count < 1 || count > 256) {
|
|
50
|
+
throw new Error("invalid native pipeline metadata");
|
|
51
|
+
}
|
|
52
|
+
const kindPtr = m._malloc(4);
|
|
53
|
+
const countPtr = m._malloc(4);
|
|
54
|
+
try {
|
|
55
|
+
const steps = [];
|
|
56
|
+
for (let index = 0; index < count; ++index) {
|
|
57
|
+
checkStatus(m.ccall("n4m_pipeline_get_operator", "number", ["number", "number", "number", "number", "number", "number"], [ptr, index, kindPtr, 0, 0, countPtr]));
|
|
58
|
+
const kind = m.getValue(kindPtr, "i32");
|
|
59
|
+
const nParams = m.getValue(countPtr, "i32");
|
|
60
|
+
if (kind < 0 || kind > 14 || nParams < 0 || nParams > 256) {
|
|
61
|
+
throw new Error("invalid native pipeline operator metadata");
|
|
62
|
+
}
|
|
63
|
+
const paramsPtr = m._malloc(Math.max(1, nParams) * 8);
|
|
64
|
+
try {
|
|
65
|
+
if (nParams > 0) {
|
|
66
|
+
checkStatus(m.ccall("n4m_pipeline_get_operator", "number", ["number", "number", "number", "number", "number", "number"], [ptr, index, kindPtr, paramsPtr, nParams, countPtr]));
|
|
67
|
+
}
|
|
68
|
+
steps.push({ kind, params: Array.from(m.HEAPF64.subarray(paramsPtr / 8, paramsPtr / 8 + nParams)) });
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
m._free(paramsPtr);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { features, steps };
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
m._free(kindPtr);
|
|
78
|
+
m._free(countPtr);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
m._free(info);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function samePlan(a, b) {
|
|
86
|
+
return a.length === b.length && a.every((step, index) => {
|
|
87
|
+
const other = b[index];
|
|
88
|
+
return other !== undefined && step.kind === other.kind &&
|
|
89
|
+
step.params.length === other.params.length &&
|
|
90
|
+
step.params.every((value, i) => Object.is(value, other.params[i]));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Owning JS façade for an ordered, fitted C ABI preprocessing pipeline. */
|
|
94
|
+
export class NativePreprocessingPipeline {
|
|
95
|
+
_ptr;
|
|
96
|
+
_ctx;
|
|
97
|
+
nFeatures;
|
|
98
|
+
steps;
|
|
99
|
+
constructor(ctx, ptr, features, steps) {
|
|
100
|
+
this._ctx = ctx;
|
|
101
|
+
this._ptr = ptr;
|
|
102
|
+
this.nFeatures = features;
|
|
103
|
+
this.steps = steps.map(step => Object.freeze({
|
|
104
|
+
kind: step.kind, params: Object.freeze([...step.params]),
|
|
105
|
+
}));
|
|
106
|
+
Object.freeze(this.steps);
|
|
107
|
+
}
|
|
108
|
+
/** Fit an ordered recipe. OSC and EPO require Y; others may omit it. */
|
|
109
|
+
static fit(ctx, steps, X, Y) {
|
|
110
|
+
validateSteps(steps);
|
|
111
|
+
validateMatrix(X, "X");
|
|
112
|
+
if (Y !== undefined) {
|
|
113
|
+
validateMatrix(Y, "Y");
|
|
114
|
+
if (Y.rows !== X.rows)
|
|
115
|
+
throw new Error("Y rows must match X rows");
|
|
116
|
+
}
|
|
117
|
+
const m = getModule();
|
|
118
|
+
const outPtr = m._malloc(4);
|
|
119
|
+
let ptr = 0;
|
|
120
|
+
try {
|
|
121
|
+
m.setValue(outPtr, 0, "i32");
|
|
122
|
+
checkStatus(m.ccall("n4m_pipeline_create", "number", ["number"], [outPtr]));
|
|
123
|
+
ptr = m.getValue(outPtr, "i32");
|
|
124
|
+
for (const step of steps) {
|
|
125
|
+
const params = m._malloc(Math.max(1, step.params.length) * 8);
|
|
126
|
+
try {
|
|
127
|
+
m.HEAPF64.set(step.params, params / 8);
|
|
128
|
+
checkStatus(m.ccall("n4m_pipeline_add_operator", "number", ["number", "number", "number", "number"], [ptr, step.kind, step.params.length ? params : 0,
|
|
129
|
+
step.params.length]));
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
m._free(params);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const xView = makeMatrixView(X.data, X.rows, X.cols);
|
|
136
|
+
let yView;
|
|
137
|
+
try {
|
|
138
|
+
yView = Y === undefined ? undefined :
|
|
139
|
+
makeMatrixView(Y.data, Y.rows, Y.cols);
|
|
140
|
+
checkStatus(m.ccall("n4m_pipeline_fit", "number", ["number", "number", "number", "number"], [ctx.handle, ptr, xView.viewPtr, yView?.viewPtr ?? 0]), ctx.handle);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
yView?.free();
|
|
144
|
+
xView.free();
|
|
145
|
+
}
|
|
146
|
+
const plan = readPlan(ptr);
|
|
147
|
+
if (!samePlan(plan.steps, steps)) {
|
|
148
|
+
throw new Error("native fitted plan differs from requested recipe");
|
|
149
|
+
}
|
|
150
|
+
const result = new NativePreprocessingPipeline(ctx, ptr, plan.features, plan.steps);
|
|
151
|
+
ptr = 0;
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (ptr !== 0)
|
|
156
|
+
m.ccall("n4m_pipeline_destroy", null, ["number"], [ptr]);
|
|
157
|
+
m._free(outPtr);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** Import fitted N4MP bytes; optionally attest against an external recipe. */
|
|
161
|
+
static fromBytes(ctx, bytes, expectedSteps) {
|
|
162
|
+
if (!(bytes instanceof Uint8Array) || bytes.length === 0 ||
|
|
163
|
+
bytes.length > 64 * 1024 * 1024) {
|
|
164
|
+
throw new Error("N4MP payload must be a Uint8Array of at most 64 MiB");
|
|
165
|
+
}
|
|
166
|
+
if (expectedSteps !== undefined)
|
|
167
|
+
validateSteps(expectedSteps);
|
|
168
|
+
const m = getModule();
|
|
169
|
+
const data = m._malloc(bytes.length);
|
|
170
|
+
const outPtr = m._malloc(4);
|
|
171
|
+
let ptr = 0;
|
|
172
|
+
try {
|
|
173
|
+
m.HEAPU8.set(bytes, data);
|
|
174
|
+
m.setValue(outPtr, 0, "i32");
|
|
175
|
+
checkStatus(m.ccall("n4m_pipeline_import_from_buffer", "number", ["number", "number", "number", "number"], [ctx.handle, data, bytes.length, outPtr]), ctx.handle);
|
|
176
|
+
ptr = m.getValue(outPtr, "i32");
|
|
177
|
+
const plan = readPlan(ptr);
|
|
178
|
+
if (expectedSteps !== undefined && !samePlan(plan.steps, expectedSteps)) {
|
|
179
|
+
throw new Error("N4MP ordered plan does not match expected recipe");
|
|
180
|
+
}
|
|
181
|
+
const result = new NativePreprocessingPipeline(ctx, ptr, plan.features, plan.steps);
|
|
182
|
+
ptr = 0;
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
if (ptr !== 0)
|
|
187
|
+
m.ccall("n4m_pipeline_destroy", null, ["number"], [ptr]);
|
|
188
|
+
m._free(data);
|
|
189
|
+
m._free(outPtr);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** Transform new rows using only native fitted state. */
|
|
193
|
+
transform(X) {
|
|
194
|
+
if (this._ptr === 0)
|
|
195
|
+
throw new Error("native pipeline has been destroyed");
|
|
196
|
+
validateMatrix(X, "X");
|
|
197
|
+
if (X.cols !== this.nFeatures)
|
|
198
|
+
throw new Error("X feature width must match fit");
|
|
199
|
+
const m = getModule();
|
|
200
|
+
const input = makeMatrixView(X.data, X.rows, X.cols);
|
|
201
|
+
try {
|
|
202
|
+
const output = makeMatrixView(new Float64Array(X.data.length), X.rows, X.cols);
|
|
203
|
+
try {
|
|
204
|
+
checkStatus(m.ccall("n4m_pipeline_transform", "number", ["number", "number", "number", "number"], [this._ctx.handle, this._ptr, input.viewPtr, output.viewPtr]), this._ctx.handle);
|
|
205
|
+
return { data: Float64Array.from(m.HEAPF64.subarray(output.dataPtr / 8, output.dataPtr / 8 + X.data.length)),
|
|
206
|
+
rows: X.rows, cols: X.cols };
|
|
207
|
+
}
|
|
208
|
+
finally {
|
|
209
|
+
output.free();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
input.free();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/** Export the native fitted state and ordered recipe in N4MP format. */
|
|
217
|
+
toBytes() {
|
|
218
|
+
if (this._ptr === 0)
|
|
219
|
+
throw new Error("native pipeline has been destroyed");
|
|
220
|
+
const m = getModule();
|
|
221
|
+
const sizePtr = m._malloc(4); // WASM32 size_t
|
|
222
|
+
const writtenPtr = m._malloc(4);
|
|
223
|
+
try {
|
|
224
|
+
checkStatus(m.ccall("n4m_pipeline_export_size", "number", ["number", "number"], [this._ptr, sizePtr]));
|
|
225
|
+
const size = m.getValue(sizePtr, "i32");
|
|
226
|
+
const data = m._malloc(Math.max(1, size));
|
|
227
|
+
try {
|
|
228
|
+
checkStatus(m.ccall("n4m_pipeline_export_to_buffer", "number", ["number", "number", "number", "number"], [this._ptr, data, size, writtenPtr]));
|
|
229
|
+
return Uint8Array.from(m.HEAPU8.subarray(data, data + m.getValue(writtenPtr, "i32")));
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
m._free(data);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
m._free(sizePtr);
|
|
237
|
+
m._free(writtenPtr);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
destroy() {
|
|
241
|
+
if (this._ptr === 0)
|
|
242
|
+
return;
|
|
243
|
+
getModule().ccall("n4m_pipeline_destroy", null, ["number"], [this._ptr]);
|
|
244
|
+
this._ptr = 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Matrix } from "./types.js";
|
|
2
|
+
export type NativeSplitterKind = "KennardStone" | "SPXY" | "SPXYFold" | "SPXYGroupFold" | "KMeans" | "KBinsStratified" | "BinnedStratGroupFold" | "SystematicCircular" | "DataTwinning";
|
|
3
|
+
export interface NativeSplitterOptions {
|
|
4
|
+
testSize?: number;
|
|
5
|
+
nSplits?: number;
|
|
6
|
+
yMetric?: 0 | 1 | 2;
|
|
7
|
+
aggregation?: 0 | 1;
|
|
8
|
+
nBins?: number;
|
|
9
|
+
strategy?: 0 | 1;
|
|
10
|
+
shuffle?: boolean;
|
|
11
|
+
maxIter?: number;
|
|
12
|
+
seed?: number | bigint;
|
|
13
|
+
/** Signed int64 group IDs, one per sample. Required by group-fold kinds. */
|
|
14
|
+
groups?: readonly (number | bigint)[];
|
|
15
|
+
/** Zero-based fold index for the three fold kinds; zero otherwise. */
|
|
16
|
+
foldIndex?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface NativeSplitIndices {
|
|
19
|
+
trainIndices: Int32Array;
|
|
20
|
+
testIndices: Int32Array;
|
|
21
|
+
}
|
|
22
|
+
/** Return the native ordered row indices without sorting or host-side splitting. */
|
|
23
|
+
export declare function splitNative(kind: NativeSplitterKind, X: Matrix | null, Y?: Matrix | null, options?: NativeSplitterOptions): NativeSplitIndices;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CECILL-2.1
|
|
2
|
+
// One typed marshalling path to the nine native sample splitters.
|
|
3
|
+
import { checkStatus, getModule } from "./ffi.js";
|
|
4
|
+
const KINDS = {
|
|
5
|
+
KennardStone: 0, SPXY: 1, SPXYFold: 2, SPXYGroupFold: 3,
|
|
6
|
+
KMeans: 4, KBinsStratified: 5, BinnedStratGroupFold: 6,
|
|
7
|
+
SystematicCircular: 7, DataTwinning: 8,
|
|
8
|
+
};
|
|
9
|
+
function validMatrix(value, name) {
|
|
10
|
+
if (!Number.isSafeInteger(value.rows) || !Number.isSafeInteger(value.cols) ||
|
|
11
|
+
value.rows < 1 || value.cols < 1 ||
|
|
12
|
+
value.data.length !== value.rows * value.cols ||
|
|
13
|
+
!value.data.every(Number.isFinite)) {
|
|
14
|
+
throw new Error(`${name} must be a nonempty finite row-major matrix`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Return the native ordered row indices without sorting or host-side splitting. */
|
|
18
|
+
export function splitNative(kind, X, Y = null, options = {}) {
|
|
19
|
+
if (!Object.prototype.hasOwnProperty.call(KINDS, kind)) {
|
|
20
|
+
throw new Error(`unknown native splitter ${kind}`);
|
|
21
|
+
}
|
|
22
|
+
const code = KINDS[kind];
|
|
23
|
+
const needsX = [0, 1, 2, 3, 4, 8].includes(code);
|
|
24
|
+
const needsY = [1, 2, 3, 5, 6, 7].includes(code);
|
|
25
|
+
const grouped = code === 3 || code === 6;
|
|
26
|
+
const folded = code === 2 || code === 3 || code === 6;
|
|
27
|
+
if ((X !== null) !== needsX || (Y !== null) !== needsY ||
|
|
28
|
+
(options.groups !== undefined) !== grouped) {
|
|
29
|
+
throw new Error("X/Y/groups must match the native splitter kind");
|
|
30
|
+
}
|
|
31
|
+
if (X !== null)
|
|
32
|
+
validMatrix(X, "X");
|
|
33
|
+
if (Y !== null)
|
|
34
|
+
validMatrix(Y, "Y");
|
|
35
|
+
const n = X?.rows ?? Y.rows;
|
|
36
|
+
if (!Number.isSafeInteger(n) || n > 2147483647 ||
|
|
37
|
+
(X !== null && Y !== null && X.rows !== Y.rows)) {
|
|
38
|
+
throw new Error("native splitter requires aligned, int32-sized rows");
|
|
39
|
+
}
|
|
40
|
+
const ns = options.nSplits ?? 3;
|
|
41
|
+
const fold = options.foldIndex ?? 0;
|
|
42
|
+
if (!Number.isInteger(fold) || (folded ? fold < 0 || fold >= ns : fold !== 0) ||
|
|
43
|
+
!Number.isInteger(ns) || ns < 2) {
|
|
44
|
+
throw new Error("invalid native splitter fold configuration");
|
|
45
|
+
}
|
|
46
|
+
const rawGroups = options.groups ?? [];
|
|
47
|
+
if (grouped && rawGroups.length !== n)
|
|
48
|
+
throw new Error("groups length must match rows");
|
|
49
|
+
const groupIds = rawGroups.map(value => {
|
|
50
|
+
if (typeof value === "number" && (!Number.isSafeInteger(value))) {
|
|
51
|
+
throw new Error("group IDs must be exact signed int64 integers");
|
|
52
|
+
}
|
|
53
|
+
const id = BigInt(value);
|
|
54
|
+
if (id < -(1n << 63n) || id > (1n << 63n) - 1n) {
|
|
55
|
+
throw new Error("group IDs must fit signed int64");
|
|
56
|
+
}
|
|
57
|
+
return id;
|
|
58
|
+
});
|
|
59
|
+
const seedRaw = options.seed ?? 0;
|
|
60
|
+
if (typeof seedRaw === "number" && !Number.isSafeInteger(seedRaw)) {
|
|
61
|
+
throw new Error("seed must be an exact unsigned integer");
|
|
62
|
+
}
|
|
63
|
+
const seed = BigInt(seedRaw);
|
|
64
|
+
if (seed < 0n || seed > (1n << 64n) - 1n) {
|
|
65
|
+
throw new Error("seed must fit unsigned int64");
|
|
66
|
+
}
|
|
67
|
+
const integers = [options.yMetric ?? 1, options.aggregation ?? 0,
|
|
68
|
+
options.nBins ?? 5, options.strategy ?? 0,
|
|
69
|
+
options.maxIter ?? 100];
|
|
70
|
+
if (integers.some(value => !Number.isInteger(value) ||
|
|
71
|
+
value < 0 || value > 2147483647)) {
|
|
72
|
+
throw new Error("native splitter parameters must be nonnegative int32 values");
|
|
73
|
+
}
|
|
74
|
+
const testSize = options.testSize ?? 0.25;
|
|
75
|
+
if (!Number.isFinite(testSize) || testSize <= 0 || testSize >= 1) {
|
|
76
|
+
throw new Error("testSize must be between zero and one");
|
|
77
|
+
}
|
|
78
|
+
const m = getModule();
|
|
79
|
+
const spec = m._malloc(48); // C n4m_splitter_spec_t, WASM32 8-byte alignment
|
|
80
|
+
const xp = X === null ? 0 : m._malloc(X.data.byteLength);
|
|
81
|
+
const yp = Y === null ? 0 : m._malloc(Y.data.byteLength);
|
|
82
|
+
const gp = grouped ? m._malloc(n * 8) : 0;
|
|
83
|
+
const train = m._malloc(n * 4);
|
|
84
|
+
const test = m._malloc(n * 4);
|
|
85
|
+
const nTrain = m._malloc(4);
|
|
86
|
+
const nTest = m._malloc(4);
|
|
87
|
+
try {
|
|
88
|
+
if (X !== null)
|
|
89
|
+
m.HEAPF64.set(X.data, xp / 8);
|
|
90
|
+
if (Y !== null)
|
|
91
|
+
m.HEAPF64.set(Y.data, yp / 8);
|
|
92
|
+
const data = new DataView(m.HEAPU8.buffer);
|
|
93
|
+
groupIds.forEach((id, i) => data.setBigInt64(gp + i * 8, id, true));
|
|
94
|
+
[code, ns, integers[0], integers[1], integers[2], integers[3],
|
|
95
|
+
options.shuffle === false ? 0 : 1, integers[4]].forEach((value, i) => data.setInt32(spec + i * 4, value, true));
|
|
96
|
+
data.setFloat64(spec + 32, testSize, true);
|
|
97
|
+
data.setBigUint64(spec + 40, seed, true);
|
|
98
|
+
checkStatus(m.ccall("n4m_wasm_splitter_indices", "number", Array(12).fill("number"), [spec, xp, yp, n, X?.cols ?? 0, Y?.cols ?? 0, gp, fold,
|
|
99
|
+
train, nTrain, test, nTest]));
|
|
100
|
+
const trainCount = m.getValue(nTrain, "i32");
|
|
101
|
+
const testCount = m.getValue(nTest, "i32");
|
|
102
|
+
if (trainCount < 0 || testCount < 0 || trainCount + testCount !== n) {
|
|
103
|
+
throw new Error("invalid native splitter result sizes");
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
trainIndices: Int32Array.from(m.HEAP32.subarray(train / 4, train / 4 + trainCount)),
|
|
107
|
+
testIndices: Int32Array.from(m.HEAP32.subarray(test / 4, test / 4 + testCount)),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
for (const ptr of [spec, xp, yp, gp, train, test, nTrain, nTest]) {
|
|
112
|
+
if (ptr !== 0)
|
|
113
|
+
m._free(ptr);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Matrix } from "./types.js";
|
|
2
|
+
export declare const selectorMethods: readonly string[];
|
|
3
|
+
/** Run one of the 25 native selectors, preserving its ranked int64 indices. */
|
|
4
|
+
export declare function selectVariables(method: string, X: Matrix, Y: Matrix, nComponents?: number, methodParams?: Record<string, unknown>): BigInt64Array;
|
|
5
|
+
/** Compatibility convenience wrapper for the native SPA selector. */
|
|
6
|
+
export declare function selectSpa(X: Matrix, Y: Matrix, topK: number, nComponents?: number): BigInt64Array;
|