@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/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# @nirs4all/methods — WebAssembly binding
|
|
2
|
+
|
|
3
|
+
Browser + Node.js binding for **libn4m** (the `nirs4all-methods` portable
|
|
4
|
+
PLS/NIRS engine), compiled via Emscripten. It is a **non-idiomatic function
|
|
5
|
+
library**: raw typed arrays in, typed arrays out. Estimator ergonomics and the
|
|
6
|
+
multi-component aggregation live in the separate `nirs4all-core` repo — not
|
|
7
|
+
here. See [`INPUT_CONTRACT.md`](INPUT_CONTRACT.md) and
|
|
8
|
+
[`examples/consume.mjs`](examples/consume.mjs).
|
|
9
|
+
|
|
10
|
+
The package ships:
|
|
11
|
+
|
|
12
|
+
- `n4m.wasm` — the libn4m C ABI compiled to WebAssembly (full engine).
|
|
13
|
+
- `n4m.js` — Emscripten MODULARIZE/EXPORT_ES6 loader.
|
|
14
|
+
- `dist/` — TypeScript wrappers (`Context`, `Config`, `Model`, `MethodResult`)
|
|
15
|
+
emitted from `src/`.
|
|
16
|
+
|
|
17
|
+
## Build
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 1. Activate the Emscripten SDK.
|
|
21
|
+
source /path/to/emsdk/emsdk_env.sh
|
|
22
|
+
# 2. Configure + build the WASM preset (zero deps beyond Emscripten).
|
|
23
|
+
cmake --preset emscripten
|
|
24
|
+
cmake --build --preset emscripten --target n4m_wasm
|
|
25
|
+
# Artifacts land in build/emscripten/bindings/js/{n4m.js,n4m.wasm}.
|
|
26
|
+
|
|
27
|
+
# 3. Build the TypeScript wrapper for distribution:
|
|
28
|
+
cd bindings/js && npm run build && npm run stage:wasm
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Smoke test (Node)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd bindings/js
|
|
35
|
+
npm test # PLS parity + API/generic/AOM/new-pack smokes
|
|
36
|
+
node examples/consume.mjs # the downstream-consumption example
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The smoke suite fits a SIMPLS PLS regression through the raw-pointer entrypoint,
|
|
40
|
+
checks the public API and generic method path, exercises POP/AOM helpers, and
|
|
41
|
+
gates the broad-model-pack additions (`ECR`, `O2PLS`, AOM Ridge/Stack,
|
|
42
|
+
DataTwinning, SystematicCircular). The PLS smoke compares coefficients +
|
|
43
|
+
predictions to a frozen native fixture (`test/parity_fixture.json`) at a 1e-9
|
|
44
|
+
isolated band (achieved ~1e-16).
|
|
45
|
+
|
|
46
|
+
## API surface
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import * as n4m from "@nirs4all/methods";
|
|
50
|
+
|
|
51
|
+
await n4m.loadModule();
|
|
52
|
+
console.log(n4m.version()); // "1.0.3+abi.2.0.0"
|
|
53
|
+
console.log(n4m.abiVersion()); // [2, 0, 0]
|
|
54
|
+
|
|
55
|
+
const rows = 40, cols = 6;
|
|
56
|
+
const X = new Float64Array(rows * cols); // row-major
|
|
57
|
+
const y = new Float64Array(rows);
|
|
58
|
+
// ... fill X, y ...
|
|
59
|
+
|
|
60
|
+
const model = n4m.fitPls({ data: X, rows, cols },
|
|
61
|
+
{ data: y, rows, cols: 1 }, 3);
|
|
62
|
+
const preds = n4m.predictPls(model, { data: X, rows, cols });
|
|
63
|
+
|
|
64
|
+
const split = n4m.computeSplitIndices("KennardStone", { data: X, rows, cols }, null);
|
|
65
|
+
// `computeSplit()` remains available when a compact train/test mask is enough.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`Context` / `Config` / `MethodResult` are also exported for the lower-level
|
|
69
|
+
path. There is no idiomatic (sklearn-style) layer — that is intentional.
|
|
70
|
+
|
|
71
|
+
## Build options
|
|
72
|
+
|
|
73
|
+
The CMake `emscripten` preset sets:
|
|
74
|
+
|
|
75
|
+
- `WASM_BIGINT=1` — int64 ABI params are exchanged as `BigInt`.
|
|
76
|
+
- `MODULARIZE=1`, `EXPORT_ES6=1` — the module factory is the default export.
|
|
77
|
+
- `ALLOW_MEMORY_GROWTH=1`, `INITIAL_MEMORY=64MB`, `MAXIMUM_MEMORY=2GB`.
|
|
78
|
+
- `EXPORTED_FUNCTIONS` — driven by `cpp/abi/expected_symbols_linux.txt`; every
|
|
79
|
+
`n4m_*` symbol that ships in `libn4m` is exported here as `_n4m_*` (the full
|
|
80
|
+
engine surface), plus `_malloc`/`_free` and the raw-pointer PLS shims.
|
|
81
|
+
|
|
82
|
+
## Generic method path (enabled)
|
|
83
|
+
|
|
84
|
+
Two ways to reach the engine, both bit-exact vs native:
|
|
85
|
+
|
|
86
|
+
- **Raw-pointer shims** — `fitPls` / `predictPls` and the other shims in
|
|
87
|
+
`src/wasm_entry.c` (e.g. `n4m_wasm_pls_fit_legacy`): typed arrays in, typed
|
|
88
|
+
arrays out, no handle bookkeeping.
|
|
89
|
+
- **Generic `MethodResult.run`** — call **any** of the ~188 `method_result`
|
|
90
|
+
producers by symbol, passing `n4m_matrix_view_t*` arguments built from JS.
|
|
91
|
+
|
|
92
|
+
The generic path **works** and is regression-tested (`test/run_generic_method.mjs`
|
|
93
|
+
fits `n4m_estimators_sparse_simpls_fit` and the generic `n4m_model_fit` and matches the raw
|
|
94
|
+
`n4m_estimators_pls_fit` oracle byte-for-byte). The previous "Emscripten miscompiles
|
|
95
|
+
matrix-view parameters" diagnosis was **wrong**: the bug was the TS `ccall` layer
|
|
96
|
+
passing a JS `number` for the `int64_t` `rows`/`cols` fields. Marshalling those
|
|
97
|
+
dims as `BigInt` under `WASM_BIGINT` (`ffi.ts` / `makeMatrixView`) made the deep
|
|
98
|
+
view-pointer path byte-correct — see the note at the top of `src/wasm_entry.c`.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
import { loadModule, Context, Config, MethodResult } from "@nirs4all/methods";
|
|
102
|
+
await loadModule();
|
|
103
|
+
const ctx = new Context(), cfg = new Config();
|
|
104
|
+
const X = makeMatrixView(rows, cols, dataF64); // row-major Float64Array
|
|
105
|
+
const res = MethodResult.run("n4m_estimators_sparse_simpls_fit", ctx, cfg, [X /* , Y */]);
|
|
106
|
+
const coef = res.matrix("coefficients"); // typed array + shape
|
|
107
|
+
res.destroy(); cfg.destroy(); ctx.destroy();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Layout
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
bindings/js/
|
|
114
|
+
├── CMakeLists.txt # Emscripten target wired into the project preset
|
|
115
|
+
├── package.json # @nirs4all/methods
|
|
116
|
+
├── tsconfig.json
|
|
117
|
+
├── INPUT_CONTRACT.md # the raw-array input contract (copied by nirs4all-lite)
|
|
118
|
+
├── src/
|
|
119
|
+
│ ├── wasm_entry.c # ABI header pull-in + raw-pointer PLS shims
|
|
120
|
+
│ ├── ffi.ts # Module loader + matrix-view helpers
|
|
121
|
+
│ ├── types.ts # Mirrored C enums + error class
|
|
122
|
+
│ ├── context.ts # Context wrapper
|
|
123
|
+
│ ├── config.ts # Config wrapper
|
|
124
|
+
│ ├── model.ts # Model fit / predict (raw-pointer path)
|
|
125
|
+
│ ├── methodResult.ts # Universal n4m_method_result_t wrapper
|
|
126
|
+
│ └── index.ts # Public barrel
|
|
127
|
+
├── examples/
|
|
128
|
+
│ └── consume.mjs # downstream-consumption example
|
|
129
|
+
└── test/
|
|
130
|
+
├── run_smoke.mjs # Node PLS smoke + parity vs native Python
|
|
131
|
+
├── run_api.mjs
|
|
132
|
+
├── run_generic_method.mjs
|
|
133
|
+
├── run_pop_aom.mjs
|
|
134
|
+
└── run_new_pack.mjs # broad-model-pack smoke
|
|
135
|
+
```
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Algorithm, Deflation, Solver } from "./types.js";
|
|
2
|
+
/** RAII wrapper around n4m_config_t. */
|
|
3
|
+
export declare class Config {
|
|
4
|
+
private _ptr;
|
|
5
|
+
private constructor();
|
|
6
|
+
static create(): Config;
|
|
7
|
+
get handle(): number;
|
|
8
|
+
destroy(): void;
|
|
9
|
+
setNComponents(k: number): void;
|
|
10
|
+
setAlgorithm(a: Algorithm): void;
|
|
11
|
+
setSolver(solver: Solver): void;
|
|
12
|
+
setDeflation(d: Deflation): void;
|
|
13
|
+
setCenterX(on: boolean): void;
|
|
14
|
+
setCenterY(on: boolean): void;
|
|
15
|
+
setStoreScores(on: boolean): void;
|
|
16
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CECILL-2.1
|
|
2
|
+
import { checkStatus, getModule } from "./ffi.js";
|
|
3
|
+
/** RAII wrapper around n4m_config_t. */
|
|
4
|
+
export class Config {
|
|
5
|
+
_ptr;
|
|
6
|
+
constructor(ptr) {
|
|
7
|
+
this._ptr = ptr;
|
|
8
|
+
}
|
|
9
|
+
static create() {
|
|
10
|
+
const m = getModule();
|
|
11
|
+
const out = m._malloc(4);
|
|
12
|
+
try {
|
|
13
|
+
const status = m.ccall("n4m_config_create", "number", ["number"], [out]);
|
|
14
|
+
checkStatus(status);
|
|
15
|
+
return new Config(m.getValue(out, "i32"));
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
m._free(out);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
get handle() {
|
|
22
|
+
return this._ptr;
|
|
23
|
+
}
|
|
24
|
+
destroy() {
|
|
25
|
+
if (this._ptr === 0)
|
|
26
|
+
return;
|
|
27
|
+
getModule().ccall("n4m_config_destroy", null, ["number"], [this._ptr]);
|
|
28
|
+
this._ptr = 0;
|
|
29
|
+
}
|
|
30
|
+
setNComponents(k) {
|
|
31
|
+
const s = getModule().ccall("n4m_config_set_n_components", "number", ["number", "number"], [this._ptr, k]);
|
|
32
|
+
checkStatus(s);
|
|
33
|
+
}
|
|
34
|
+
setAlgorithm(a) {
|
|
35
|
+
const s = getModule().ccall("n4m_config_set_algorithm", "number", ["number", "number"], [this._ptr, a]);
|
|
36
|
+
checkStatus(s);
|
|
37
|
+
}
|
|
38
|
+
setSolver(solver) {
|
|
39
|
+
const s = getModule().ccall("n4m_config_set_solver", "number", ["number", "number"], [this._ptr, solver]);
|
|
40
|
+
checkStatus(s);
|
|
41
|
+
}
|
|
42
|
+
setDeflation(d) {
|
|
43
|
+
const s = getModule().ccall("n4m_config_set_deflation", "number", ["number", "number"], [this._ptr, d]);
|
|
44
|
+
checkStatus(s);
|
|
45
|
+
}
|
|
46
|
+
setCenterX(on) {
|
|
47
|
+
const s = getModule().ccall("n4m_config_set_center_x", "number", ["number", "number"], [this._ptr, on ? 1 : 0]);
|
|
48
|
+
checkStatus(s);
|
|
49
|
+
}
|
|
50
|
+
setCenterY(on) {
|
|
51
|
+
const s = getModule().ccall("n4m_config_set_center_y", "number", ["number", "number"], [this._ptr, on ? 1 : 0]);
|
|
52
|
+
checkStatus(s);
|
|
53
|
+
}
|
|
54
|
+
setStoreScores(on) {
|
|
55
|
+
const s = getModule().ccall("n4m_config_set_store_scores", "number", ["number", "number"], [this._ptr, on ? 1 : 0]);
|
|
56
|
+
checkStatus(s);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** RAII wrapper around n4m_context_t. */
|
|
2
|
+
export declare class Context {
|
|
3
|
+
private _ptr;
|
|
4
|
+
private constructor();
|
|
5
|
+
/** Create a new context. Throws N4mError on failure. */
|
|
6
|
+
static create(): Context;
|
|
7
|
+
/** Returns the raw `n4m_context_t*` pointer (handle). */
|
|
8
|
+
get handle(): number;
|
|
9
|
+
/** Free the context. Safe to call multiple times. */
|
|
10
|
+
destroy(): void;
|
|
11
|
+
/** Set the RNG seed used by stochastic algorithms.
|
|
12
|
+
*
|
|
13
|
+
* n4m_context_set_seed takes a uint64_t. Under WASM_BIGINT=1 the i64 slot
|
|
14
|
+
* must be marshalled as 'i64' with a BigInt — passing a JS number loses
|
|
15
|
+
* precision above 2^53 and throws "Cannot convert N to a BigInt" on emsdk
|
|
16
|
+
* >= 5.0.7 (same class of bug as the matrix-view dims; see ffi.ts). */
|
|
17
|
+
setSeed(seed: bigint | number): void;
|
|
18
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CECILL-2.1
|
|
2
|
+
import { checkStatus, getModule } from "./ffi.js";
|
|
3
|
+
/** RAII wrapper around n4m_context_t. */
|
|
4
|
+
export class Context {
|
|
5
|
+
_ptr;
|
|
6
|
+
constructor(ptr) {
|
|
7
|
+
this._ptr = ptr;
|
|
8
|
+
}
|
|
9
|
+
/** Create a new context. Throws N4mError on failure. */
|
|
10
|
+
static create() {
|
|
11
|
+
const m = getModule();
|
|
12
|
+
const out = m._malloc(4);
|
|
13
|
+
try {
|
|
14
|
+
const status = m.ccall("n4m_context_create", "number", ["number"], [out]);
|
|
15
|
+
checkStatus(status);
|
|
16
|
+
const ptr = m.getValue(out, "i32");
|
|
17
|
+
return new Context(ptr);
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
m._free(out);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Returns the raw `n4m_context_t*` pointer (handle). */
|
|
24
|
+
get handle() {
|
|
25
|
+
return this._ptr;
|
|
26
|
+
}
|
|
27
|
+
/** Free the context. Safe to call multiple times. */
|
|
28
|
+
destroy() {
|
|
29
|
+
if (this._ptr === 0)
|
|
30
|
+
return;
|
|
31
|
+
getModule().ccall("n4m_context_destroy", null, ["number"], [this._ptr]);
|
|
32
|
+
this._ptr = 0;
|
|
33
|
+
}
|
|
34
|
+
/** Set the RNG seed used by stochastic algorithms.
|
|
35
|
+
*
|
|
36
|
+
* n4m_context_set_seed takes a uint64_t. Under WASM_BIGINT=1 the i64 slot
|
|
37
|
+
* must be marshalled as 'i64' with a BigInt — passing a JS number loses
|
|
38
|
+
* precision above 2^53 and throws "Cannot convert N to a BigInt" on emsdk
|
|
39
|
+
* >= 5.0.7 (same class of bug as the matrix-view dims; see ffi.ts). */
|
|
40
|
+
setSeed(seed) {
|
|
41
|
+
const status = getModule().ccall("n4m_context_set_seed", "number", ["number", "i64"], [this._ptr, BigInt(seed)]);
|
|
42
|
+
checkStatus(status, this._ptr);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/ffi.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Loose typing of the Emscripten module factory's runtime instance. */
|
|
2
|
+
export interface EmModule {
|
|
3
|
+
HEAPU8: Uint8Array;
|
|
4
|
+
HEAP32: Int32Array;
|
|
5
|
+
HEAPF64: Float64Array;
|
|
6
|
+
_malloc(size: number): number;
|
|
7
|
+
_free(ptr: number): void;
|
|
8
|
+
ccall<T>(name: string, returnType: string | null, argTypes: string[], args: unknown[]): T;
|
|
9
|
+
cwrap<T extends Function>(name: string, returnType: string | null, argTypes: string[]): T;
|
|
10
|
+
UTF8ToString(ptr: number): string;
|
|
11
|
+
stringToUTF8(s: string, ptr: number, max: number): void;
|
|
12
|
+
lengthBytesUTF8(s: string): number;
|
|
13
|
+
getValue(ptr: number, type: string): number;
|
|
14
|
+
setValue(ptr: number, value: number, type: string): void;
|
|
15
|
+
}
|
|
16
|
+
/** Load and cache the Emscripten module. Call once at app startup. */
|
|
17
|
+
export declare function loadModule(): Promise<EmModule>;
|
|
18
|
+
export declare function getModule(): EmModule;
|
|
19
|
+
export declare function checkStatus(status: number, ctxPtr?: number): void;
|
|
20
|
+
export declare const MATRIX_VIEW_SIZE = 48;
|
|
21
|
+
/** Allocate a matrix-view struct and copy `data` into the WASM heap.
|
|
22
|
+
* Returns the view pointer; caller must `free()` it.
|
|
23
|
+
*
|
|
24
|
+
* NOTE: n4m_matrix_view_init_rowmajor takes `int64_t rows, int64_t cols`.
|
|
25
|
+
* The WASM module is built with `-s WASM_BIGINT=1`, so ccall marshals i64
|
|
26
|
+
* args as BigInt — passing a plain JS number for these slots silently
|
|
27
|
+
* corrupts the struct fields (rows/cols/strides become garbage, producing
|
|
28
|
+
* ~1e32 numerics downstream) and on emsdk >= 5.0.7 throws
|
|
29
|
+
* "TypeError: Cannot convert N to a BigInt". We therefore declare the two
|
|
30
|
+
* dimension args as 'i64' and pass BigInt(...). The data/out/dtype slots
|
|
31
|
+
* stay 32-bit. dtype defaults to N4M_DTYPE_F64 (= 1; see types.ts Dtype). */
|
|
32
|
+
export declare function makeMatrixView(data: Float64Array, rows: number, cols: number, dtype?: number): {
|
|
33
|
+
viewPtr: number;
|
|
34
|
+
dataPtr: number;
|
|
35
|
+
free: () => void;
|
|
36
|
+
};
|
|
37
|
+
/** Read a core-owned `n4m_array_t*` (e.g. from n4m_model_get_array) into a
|
|
38
|
+
* JS-owned Float64Array. Does NOT free the array — the caller must call
|
|
39
|
+
* `n4m_array_free` afterwards. i64 view fields (rows @8, cols @16) are read
|
|
40
|
+
* as BigInt under WASM_BIGINT and narrowed with Number(). */
|
|
41
|
+
export declare function readArrayView(arrPtr: number): {
|
|
42
|
+
data: Float64Array;
|
|
43
|
+
rows: number;
|
|
44
|
+
cols: number;
|
|
45
|
+
};
|
package/dist/ffi.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CECILL-2.1
|
|
2
|
+
//
|
|
3
|
+
// Thin ccall/cwrap wrappers around the Emscripten module.
|
|
4
|
+
import { Status, N4mError } from "./types.js";
|
|
5
|
+
let _module = null;
|
|
6
|
+
/** Load and cache the Emscripten module. Call once at app startup. */
|
|
7
|
+
export async function loadModule() {
|
|
8
|
+
if (_module !== null)
|
|
9
|
+
return _module;
|
|
10
|
+
// n4m.js (the Emscripten loader) is staged into dist/ next to this module,
|
|
11
|
+
// so the package is self-contained (files: ["dist/"]). See package.json
|
|
12
|
+
// `stage:wasm`. The import path must stay relative to dist/ so the shipped
|
|
13
|
+
// package resolves the loader it bundles.
|
|
14
|
+
const factory = (await import("./n4m.js")).default;
|
|
15
|
+
_module = await factory({});
|
|
16
|
+
return _module;
|
|
17
|
+
}
|
|
18
|
+
export function getModule() {
|
|
19
|
+
if (_module === null) {
|
|
20
|
+
throw new Error("@nirs4all/methods not loaded — call await loadModule() first.");
|
|
21
|
+
}
|
|
22
|
+
return _module;
|
|
23
|
+
}
|
|
24
|
+
export function checkStatus(status, ctxPtr = 0) {
|
|
25
|
+
if (status === Status.OK)
|
|
26
|
+
return;
|
|
27
|
+
let msg = "";
|
|
28
|
+
if (ctxPtr !== 0) {
|
|
29
|
+
const m = getModule();
|
|
30
|
+
const cstr = m.ccall("n4m_context_last_error", "number", ["number"], [ctxPtr]);
|
|
31
|
+
if (cstr !== 0)
|
|
32
|
+
msg = m.UTF8ToString(cstr);
|
|
33
|
+
}
|
|
34
|
+
if (msg === "") {
|
|
35
|
+
const m = getModule();
|
|
36
|
+
const cstr = m.ccall("n4m_status_to_string", "number", ["number"], [status]);
|
|
37
|
+
if (cstr !== 0)
|
|
38
|
+
msg = m.UTF8ToString(cstr);
|
|
39
|
+
}
|
|
40
|
+
throw new N4mError(status, msg);
|
|
41
|
+
}
|
|
42
|
+
// ---- n4m_matrix_view_t layout (mirrors cpp/include/n4m/n4m.h) -----------
|
|
43
|
+
//
|
|
44
|
+
// struct {
|
|
45
|
+
// void* data; // 4 bytes (WASM is 32-bit; offset 0)
|
|
46
|
+
// int64_t rows; // 8 bytes (offset 8, aligned to 8)
|
|
47
|
+
// int64_t cols; // 8 bytes (offset 16)
|
|
48
|
+
// int64_t row_stride; // 8 bytes (offset 24)
|
|
49
|
+
// int64_t col_stride; // 8 bytes (offset 32)
|
|
50
|
+
// int32_t dtype; // 4 bytes (offset 40)
|
|
51
|
+
// int32_t reserved0; // 4 bytes (offset 44)
|
|
52
|
+
// }; total = 48 bytes on WASM32 with the 8-byte alignment of int64_t.
|
|
53
|
+
export const MATRIX_VIEW_SIZE = 48;
|
|
54
|
+
/** Allocate a matrix-view struct and copy `data` into the WASM heap.
|
|
55
|
+
* Returns the view pointer; caller must `free()` it.
|
|
56
|
+
*
|
|
57
|
+
* NOTE: n4m_matrix_view_init_rowmajor takes `int64_t rows, int64_t cols`.
|
|
58
|
+
* The WASM module is built with `-s WASM_BIGINT=1`, so ccall marshals i64
|
|
59
|
+
* args as BigInt — passing a plain JS number for these slots silently
|
|
60
|
+
* corrupts the struct fields (rows/cols/strides become garbage, producing
|
|
61
|
+
* ~1e32 numerics downstream) and on emsdk >= 5.0.7 throws
|
|
62
|
+
* "TypeError: Cannot convert N to a BigInt". We therefore declare the two
|
|
63
|
+
* dimension args as 'i64' and pass BigInt(...). The data/out/dtype slots
|
|
64
|
+
* stay 32-bit. dtype defaults to N4M_DTYPE_F64 (= 1; see types.ts Dtype). */
|
|
65
|
+
export function makeMatrixView(data, rows, cols, dtype = 1 /* N4M_DTYPE_F64 */) {
|
|
66
|
+
const m = getModule();
|
|
67
|
+
const expected = rows * cols;
|
|
68
|
+
if (data.length !== expected) {
|
|
69
|
+
throw new Error(`matrix data length ${data.length} != rows*cols (${expected})`);
|
|
70
|
+
}
|
|
71
|
+
const bytes = Math.max(1, data.length) * 8;
|
|
72
|
+
const dataPtr = m._malloc(bytes);
|
|
73
|
+
if (data.length > 0) {
|
|
74
|
+
m.HEAPF64.set(data, dataPtr / 8);
|
|
75
|
+
}
|
|
76
|
+
const viewPtr = m._malloc(MATRIX_VIEW_SIZE);
|
|
77
|
+
const status = m.ccall("n4m_matrix_view_init_rowmajor", "number", ["number", "number", "i64", "i64", "number"], [viewPtr, dataPtr, BigInt(rows), BigInt(cols), dtype]);
|
|
78
|
+
if (status !== Status.OK) {
|
|
79
|
+
m._free(dataPtr);
|
|
80
|
+
m._free(viewPtr);
|
|
81
|
+
checkStatus(status);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
viewPtr,
|
|
85
|
+
dataPtr,
|
|
86
|
+
free: () => {
|
|
87
|
+
m._free(viewPtr);
|
|
88
|
+
m._free(dataPtr);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Read a core-owned `n4m_array_t*` (e.g. from n4m_model_get_array) into a
|
|
93
|
+
* JS-owned Float64Array. Does NOT free the array — the caller must call
|
|
94
|
+
* `n4m_array_free` afterwards. i64 view fields (rows @8, cols @16) are read
|
|
95
|
+
* as BigInt under WASM_BIGINT and narrowed with Number(). */
|
|
96
|
+
export function readArrayView(arrPtr) {
|
|
97
|
+
const m = getModule();
|
|
98
|
+
const vp = m._malloc(MATRIX_VIEW_SIZE);
|
|
99
|
+
try {
|
|
100
|
+
const st = m.ccall("n4m_array_view", "number", ["number", "number"], [arrPtr, vp]);
|
|
101
|
+
checkStatus(st);
|
|
102
|
+
const dataPtr = m.getValue(vp, "i32");
|
|
103
|
+
const rows = Number(m.getValue(vp + 8, "i64"));
|
|
104
|
+
const cols = Number(m.getValue(vp + 16, "i64"));
|
|
105
|
+
const n = rows * cols;
|
|
106
|
+
const data = new Float64Array(n);
|
|
107
|
+
if (n > 0) {
|
|
108
|
+
data.set(m.HEAPF64.subarray(dataPtr / 8, dataPtr / 8 + n));
|
|
109
|
+
}
|
|
110
|
+
return { data, rows, cols };
|
|
111
|
+
}
|
|
112
|
+
finally {
|
|
113
|
+
m._free(vp);
|
|
114
|
+
}
|
|
115
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { loadModule, getModule, makeMatrixView, readArrayView } from "./ffi.js";
|
|
2
|
+
export { Context } from "./context.js";
|
|
3
|
+
export { Config } from "./config.js";
|
|
4
|
+
export { Model, fitPls, predictPls, fitModel, predictModel, fitAom, fitPop, fitAomRidge, fitAomStack, computeSplit, computeSplitIndices, type PlsModel, type FittedModel, type AomModel, type PopModel, type AomRidgeOptions, type AomStackOptions, type SplitKind, type SplitOptions, type SplitIndices } from "./model.js";
|
|
5
|
+
export { ppCreate, ppFit, ppTransform, ppGetState, ppSetState, ppDestroy, type PpOperator, } from "./preprocessing.js";
|
|
6
|
+
export { MethodResult } from "./methodResult.js";
|
|
7
|
+
export { Status, Dtype, Algorithm, Solver, Deflation, N4mError, type Matrix, } from "./types.js";
|
|
8
|
+
/** ABI / project version reported by the loaded WASM module. */
|
|
9
|
+
export declare function version(): string;
|
|
10
|
+
/** ABI MAJOR.MINOR.PATCH triple. */
|
|
11
|
+
export declare function abiVersion(): readonly [number, number, number];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CECILL-2.1
|
|
2
|
+
//
|
|
3
|
+
// Public TypeScript API for the @nirs4all/methods binding — a
|
|
4
|
+
// non-idiomatic function library over libn4m (raw typed arrays in/out). See
|
|
5
|
+
// INPUT_CONTRACT.md and examples/consume.mjs.
|
|
6
|
+
//
|
|
7
|
+
// Example:
|
|
8
|
+
// import * as n4m from "@nirs4all/methods";
|
|
9
|
+
// await n4m.loadModule();
|
|
10
|
+
// const model = n4m.fitPls({ data: X, rows, cols }, { data: y, rows, cols: 1 }, 3);
|
|
11
|
+
// const preds = n4m.predictPls(model, { data: X, rows, cols });
|
|
12
|
+
import { getModule } from "./ffi.js";
|
|
13
|
+
export { loadModule, getModule, makeMatrixView, readArrayView } from "./ffi.js";
|
|
14
|
+
export { Context } from "./context.js";
|
|
15
|
+
export { Config } from "./config.js";
|
|
16
|
+
export { Model, fitPls, predictPls, fitModel, predictModel, fitAom, fitPop, fitAomRidge, fitAomStack, computeSplit, computeSplitIndices } from "./model.js";
|
|
17
|
+
export { ppCreate, ppFit, ppTransform, ppGetState, ppSetState, ppDestroy, } from "./preprocessing.js";
|
|
18
|
+
export { MethodResult } from "./methodResult.js";
|
|
19
|
+
export { Status, Dtype, Algorithm, Solver, Deflation, N4mError, } from "./types.js";
|
|
20
|
+
/** ABI / project version reported by the loaded WASM module. */
|
|
21
|
+
export function version() {
|
|
22
|
+
const m = getModule();
|
|
23
|
+
const ptr = m.ccall("n4m_get_version_string", "number", [], []);
|
|
24
|
+
return ptr === 0 ? "" : m.UTF8ToString(ptr);
|
|
25
|
+
}
|
|
26
|
+
/** ABI MAJOR.MINOR.PATCH triple. */
|
|
27
|
+
export function abiVersion() {
|
|
28
|
+
const m = getModule();
|
|
29
|
+
return [
|
|
30
|
+
m.ccall("n4m_get_abi_version_major", "number", [], []),
|
|
31
|
+
m.ccall("n4m_get_abi_version_minor", "number", [], []),
|
|
32
|
+
m.ccall("n4m_get_abi_version_patch", "number", [], []),
|
|
33
|
+
];
|
|
34
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Matrix } from "./types.js";
|
|
2
|
+
export declare class MethodResult {
|
|
3
|
+
private _ptr;
|
|
4
|
+
constructor(ptr: number);
|
|
5
|
+
/** Generic runner for the `(ctx, cfg, X[, Y, ...views], ...scalar-extras)
|
|
6
|
+
* -> n4m_method_result_t**` family — the bulk of the ~150 method_result
|
|
7
|
+
* producers. Matrix views are built BigInt-safe via makeMatrixView, so
|
|
8
|
+
* the deep entrypoints get correct dimensions under WASM_BIGINT=1.
|
|
9
|
+
* Returns an owning MethodResult; read outputs with matrix()/vector().
|
|
10
|
+
*
|
|
11
|
+
* `extra` are the positional scalar args after the views, matching the C
|
|
12
|
+
* signature: "int" -> int32_t, "double" -> double, "int64" -> int64_t.
|
|
13
|
+
* (Methods taking raw caller buffers — e.g. weighted_pls sample_weights —
|
|
14
|
+
* need a thin per-method wrapper that mallocs the buffer; not handled
|
|
15
|
+
* here.) */
|
|
16
|
+
static run(symbol: string, ctxHandle: number, cfgHandle: number, views: Matrix[], extra?: ReadonlyArray<{
|
|
17
|
+
kind: "int" | "double" | "int64";
|
|
18
|
+
value: number;
|
|
19
|
+
}>): MethodResult;
|
|
20
|
+
get handle(): number;
|
|
21
|
+
/** Read a named double matrix by name. Returns a copy in JS-owned memory. */
|
|
22
|
+
matrix(name: string): Matrix;
|
|
23
|
+
/** Read a named int32 vector. */
|
|
24
|
+
vectorInt(name: string): Int32Array;
|
|
25
|
+
/** Read a named scalar (returns NaN if not present). */
|
|
26
|
+
scalar(name: string): number;
|
|
27
|
+
destroy(): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CECILL-2.1
|
|
2
|
+
//
|
|
3
|
+
// Owning wrapper around n4m_method_result_t — the universal output
|
|
4
|
+
// container used by every method shipped in Batches 1-12 of the C ABI.
|
|
5
|
+
import { checkStatus, getModule, makeMatrixView } from "./ffi.js";
|
|
6
|
+
export class MethodResult {
|
|
7
|
+
_ptr;
|
|
8
|
+
constructor(ptr) {
|
|
9
|
+
this._ptr = ptr;
|
|
10
|
+
}
|
|
11
|
+
/** Generic runner for the `(ctx, cfg, X[, Y, ...views], ...scalar-extras)
|
|
12
|
+
* -> n4m_method_result_t**` family — the bulk of the ~150 method_result
|
|
13
|
+
* producers. Matrix views are built BigInt-safe via makeMatrixView, so
|
|
14
|
+
* the deep entrypoints get correct dimensions under WASM_BIGINT=1.
|
|
15
|
+
* Returns an owning MethodResult; read outputs with matrix()/vector().
|
|
16
|
+
*
|
|
17
|
+
* `extra` are the positional scalar args after the views, matching the C
|
|
18
|
+
* signature: "int" -> int32_t, "double" -> double, "int64" -> int64_t.
|
|
19
|
+
* (Methods taking raw caller buffers — e.g. weighted_pls sample_weights —
|
|
20
|
+
* need a thin per-method wrapper that mallocs the buffer; not handled
|
|
21
|
+
* here.) */
|
|
22
|
+
static run(symbol, ctxHandle, cfgHandle, views, extra = []) {
|
|
23
|
+
const m = getModule();
|
|
24
|
+
const built = views.map((v) => makeMatrixView(v.data, v.rows, v.cols));
|
|
25
|
+
const resPP = m._malloc(4);
|
|
26
|
+
m.setValue(resPP, 0, "i32");
|
|
27
|
+
try {
|
|
28
|
+
const argTypes = ["number", "number"];
|
|
29
|
+
const args = [ctxHandle, cfgHandle];
|
|
30
|
+
for (const b of built) {
|
|
31
|
+
argTypes.push("number");
|
|
32
|
+
args.push(b.viewPtr);
|
|
33
|
+
}
|
|
34
|
+
for (const e of extra) {
|
|
35
|
+
if (e.kind === "double") {
|
|
36
|
+
argTypes.push("number");
|
|
37
|
+
args.push(e.value);
|
|
38
|
+
}
|
|
39
|
+
else if (e.kind === "int64") {
|
|
40
|
+
argTypes.push("i64");
|
|
41
|
+
args.push(BigInt(e.value));
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
argTypes.push("number");
|
|
45
|
+
args.push(e.value | 0);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
argTypes.push("number");
|
|
49
|
+
args.push(resPP); // n4m_method_result_t** out
|
|
50
|
+
const status = m.ccall(symbol, "number", argTypes, args);
|
|
51
|
+
checkStatus(status, ctxHandle);
|
|
52
|
+
const ptr = m.getValue(resPP, "i32");
|
|
53
|
+
return new MethodResult(ptr);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
for (const b of built)
|
|
57
|
+
b.free();
|
|
58
|
+
m._free(resPP);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
get handle() {
|
|
62
|
+
return this._ptr;
|
|
63
|
+
}
|
|
64
|
+
/** Read a named double matrix by name. Returns a copy in JS-owned memory. */
|
|
65
|
+
matrix(name) {
|
|
66
|
+
const m = getModule();
|
|
67
|
+
const nameBytes = m.lengthBytesUTF8(name);
|
|
68
|
+
const namePtr = m._malloc(nameBytes + 1);
|
|
69
|
+
const dataPtrPtr = m._malloc(4);
|
|
70
|
+
const rowsPtr = m._malloc(8);
|
|
71
|
+
const colsPtr = m._malloc(8);
|
|
72
|
+
try {
|
|
73
|
+
m.stringToUTF8(name, namePtr, nameBytes + 1);
|
|
74
|
+
const status = m.ccall("n4m_method_result_get_double_matrix", "number", ["number", "number", "number", "number", "number"], [this._ptr, namePtr, dataPtrPtr, rowsPtr, colsPtr]);
|
|
75
|
+
checkStatus(status);
|
|
76
|
+
const dataPtr = m.getValue(dataPtrPtr, "i32");
|
|
77
|
+
// i64 lo/hi pair — WASM_BIGINT=1 returns BigInt; use HEAP32 instead.
|
|
78
|
+
const rows = m.getValue(rowsPtr, "i64");
|
|
79
|
+
const cols = m.getValue(colsPtr, "i64");
|
|
80
|
+
const n = Number(rows) * Number(cols);
|
|
81
|
+
const data = new Float64Array(n);
|
|
82
|
+
if (n > 0) {
|
|
83
|
+
data.set(m.HEAPF64.subarray(dataPtr / 8, dataPtr / 8 + n));
|
|
84
|
+
}
|
|
85
|
+
return { data, rows: Number(rows), cols: Number(cols) };
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
m._free(namePtr);
|
|
89
|
+
m._free(dataPtrPtr);
|
|
90
|
+
m._free(rowsPtr);
|
|
91
|
+
m._free(colsPtr);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Read a named int32 vector. */
|
|
95
|
+
vectorInt(name) {
|
|
96
|
+
const m = getModule();
|
|
97
|
+
const nameBytes = m.lengthBytesUTF8(name);
|
|
98
|
+
const namePtr = m._malloc(nameBytes + 1);
|
|
99
|
+
const dataPtrPtr = m._malloc(4);
|
|
100
|
+
const sizePtr = m._malloc(4);
|
|
101
|
+
try {
|
|
102
|
+
m.stringToUTF8(name, namePtr, nameBytes + 1);
|
|
103
|
+
const status = m.ccall("n4m_method_result_get_int_vector", "number", ["number", "number", "number", "number"], [this._ptr, namePtr, dataPtrPtr, sizePtr]);
|
|
104
|
+
checkStatus(status);
|
|
105
|
+
const dataPtr = m.getValue(dataPtrPtr, "i32");
|
|
106
|
+
const size = m.getValue(sizePtr, "i32");
|
|
107
|
+
const out = new Int32Array(size);
|
|
108
|
+
if (size > 0) {
|
|
109
|
+
out.set(m.HEAP32.subarray(dataPtr / 4, dataPtr / 4 + size));
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
m._free(namePtr);
|
|
115
|
+
m._free(dataPtrPtr);
|
|
116
|
+
m._free(sizePtr);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** Read a named scalar (returns NaN if not present). */
|
|
120
|
+
scalar(name) {
|
|
121
|
+
const m = getModule();
|
|
122
|
+
const nameBytes = m.lengthBytesUTF8(name);
|
|
123
|
+
const namePtr = m._malloc(nameBytes + 1);
|
|
124
|
+
const outPtr = m._malloc(8);
|
|
125
|
+
try {
|
|
126
|
+
m.stringToUTF8(name, namePtr, nameBytes + 1);
|
|
127
|
+
const status = m.ccall("n4m_method_result_get_scalar", "number", ["number", "number", "number"], [this._ptr, namePtr, outPtr]);
|
|
128
|
+
if (status !== 0)
|
|
129
|
+
return NaN;
|
|
130
|
+
return m.getValue(outPtr, "double");
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
m._free(namePtr);
|
|
134
|
+
m._free(outPtr);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
destroy() {
|
|
138
|
+
if (this._ptr === 0)
|
|
139
|
+
return;
|
|
140
|
+
getModule().ccall("n4m_method_result_destroy", null, ["number"], [this._ptr]);
|
|
141
|
+
this._ptr = 0;
|
|
142
|
+
}
|
|
143
|
+
}
|