@wlearn/liblinear 0.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/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ - Initial release
6
+ - LIBLINEAR v2.50 compiled to WASM via Emscripten
7
+ - Unified sklearn-style API: `create()`, `fit()`, `predict()`, `score()`, `save()`, `dispose()`
8
+ - Linear classifiers (logistic regression, SVC) and regressors (SVR)
9
+ - Buffer-based model I/O (no filesystem dependency)
10
+ - Accepts both typed matrices and number[][] with configurable coercion
11
+ - `predictProba()` for logistic regression solvers
12
+ - `decisionFunction()` for decision values
13
+ - `getParams()`/`setParams()` for AutoML integration
14
+ - `defaultSearchSpace()` for hyperparameter search
15
+ - `FinalizationRegistry` safety net for leak detection
16
+ - BSD-3-Clause license (same as upstream LIBLINEAR)
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ Copyright (c) 2007-2025 The LIBLINEAR Project.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions
6
+ are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright
12
+ notice, this list of conditions and the following disclaimer in the
13
+ documentation and/or other materials provided with the distribution.
14
+
15
+ 3. Neither name of copyright holders nor the names of its contributors
16
+ may be used to endorse or promote products derived from this software
17
+ without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20
+ ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
23
+ CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
24
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
26
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
27
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
28
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
29
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # @wlearn/liblinear
2
+
3
+ LIBLINEAR v2.50 compiled to WebAssembly. Linear classification and regression in browsers and Node.js.
4
+
5
+ Based on [LIBLINEAR v2.50](https://www.csie.ntu.edu.tw/~cjlin/liblinear/) (BSD-3-Clause). Zero dependencies. ESM.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @wlearn/liblinear
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```js
16
+ import { LinearModel } from '@wlearn/liblinear'
17
+
18
+ const model = await LinearModel.create({
19
+ solver: 'L2R_LR',
20
+ C: 1.0
21
+ })
22
+
23
+ // Train -- accepts number[][] or { data: Float64Array, rows, cols }
24
+ model.fit(
25
+ [[1, 2], [3, 4], [5, 6], [7, 8]],
26
+ [0, 0, 1, 1]
27
+ )
28
+
29
+ // Predict
30
+ const preds = model.predict([[2, 3], [6, 7]]) // Float64Array
31
+
32
+ // Probabilities (logistic regression solvers only)
33
+ const probs = model.predictProba([[2, 3], [6, 7]]) // Float64Array (nrow * nclass)
34
+
35
+ // Score
36
+ const accuracy = model.score([[2, 3], [6, 7]], [0, 1])
37
+
38
+ // Save / load
39
+ const buf = model.save() // Uint8Array
40
+ const model2 = await LinearModel.load(buf)
41
+
42
+ // Clean up -- required, WASM memory is not garbage collected
43
+ model.dispose()
44
+ model2.dispose()
45
+ ```
46
+
47
+ ## Typed matrix input (fast path)
48
+
49
+ For performance-critical code and AutoML integration, pass typed matrices directly:
50
+
51
+ ```js
52
+ const X = {
53
+ data: new Float64Array([1, 2, 3, 4, 5, 6, 7, 8]),
54
+ rows: 4,
55
+ cols: 2
56
+ }
57
+ const y = new Float64Array([0, 0, 1, 1])
58
+
59
+ model.fit(X, y)
60
+ ```
61
+
62
+ This avoids the conversion cost of nested arrays.
63
+
64
+ ## Input coercion policy
65
+
66
+ Control how non-typed inputs are handled:
67
+
68
+ ```js
69
+ // Default: convert silently
70
+ const model = await LinearModel.create({ coerce: 'auto' })
71
+
72
+ // Warn once per instance when conversion happens
73
+ const model = await LinearModel.create({ coerce: 'warn' })
74
+
75
+ // Throw if input is not already a typed matrix
76
+ const model = await LinearModel.create({ coerce: 'error' })
77
+ ```
78
+
79
+ ## API
80
+
81
+ ### `LinearModel.create(params?)`
82
+
83
+ Async factory. Loads WASM module, returns a ready-to-use model.
84
+
85
+ Parameters:
86
+ - `solver` -- solver type string or number (default: `'L2R_LR'`)
87
+ - `C` -- regularization parameter (default: `1.0`)
88
+ - `eps` -- stopping tolerance (default: `0.01`)
89
+ - `bias` -- bias term, < 0 disables (default: `-1`)
90
+ - `p` -- epsilon in SVR loss (default: `0.1`)
91
+ - `coerce` -- input coercion: `'auto'` | `'warn'` | `'error'` (default: `'auto'`)
92
+
93
+ ### `model.fit(X, y)`
94
+
95
+ Train on data. Returns `this` for chaining.
96
+ - `X` -- `number[][]` or `{ data: Float64Array, rows, cols }`
97
+ - `y` -- `number[]` or `Float64Array`
98
+
99
+ ### `model.predict(X)`
100
+
101
+ Returns `Float64Array` of predicted labels.
102
+
103
+ ### `model.predictProba(X)`
104
+
105
+ Returns `Float64Array` of shape `nrow * nclass` (row-major probabilities).
106
+ Only available for logistic regression solvers (L2R_LR, L1R_LR, L2R_LR_DUAL).
107
+
108
+ ### `model.decisionFunction(X)`
109
+
110
+ Returns `Float64Array` of decision values.
111
+
112
+ ### `model.score(X, y)`
113
+
114
+ Returns accuracy (classification) or R-squared (regression).
115
+
116
+ ### `model.save()`
117
+
118
+ Returns `Uint8Array` (native LIBLINEAR model format).
119
+
120
+ ### `LinearModel.load(buffer)`
121
+
122
+ Loads from `Uint8Array`. Returns `Promise<LinearModel>`.
123
+
124
+ ### `model.dispose()`
125
+
126
+ Free WASM memory. Required. Idempotent.
127
+
128
+ ### `model.getParams()` / `model.setParams(p)`
129
+
130
+ Get/set hyperparameters. Enables AutoML grid search and cloning.
131
+
132
+ ### `LinearModel.defaultSearchSpace()`
133
+
134
+ Returns default hyperparameter search space for AutoML.
135
+
136
+ ## Solver types
137
+
138
+ | Name | Code | Task |
139
+ |------|------|------|
140
+ | L2R_LR | 0 | L2-regularized logistic regression |
141
+ | L2R_L2LOSS_SVC_DUAL | 1 | L2-loss SVM (dual) |
142
+ | L2R_L2LOSS_SVC | 2 | L2-loss SVM (primal) |
143
+ | L2R_L1LOSS_SVC_DUAL | 3 | L1-loss SVM (dual) |
144
+ | MCSVM_CS | 4 | Multi-class SVM (Crammer-Singer) |
145
+ | L1R_L2LOSS_SVC | 5 | L1-regularized L2-loss SVM |
146
+ | L1R_LR | 6 | L1-regularized logistic regression |
147
+ | L2R_LR_DUAL | 7 | L2-regularized logistic regression (dual) |
148
+ | L2R_L2LOSS_SVR | 11 | L2-loss SVR (primal) |
149
+ | L2R_L2LOSS_SVR_DUAL | 12 | L2-loss SVR (dual) |
150
+ | L2R_L1LOSS_SVR_DUAL | 13 | L1-loss SVR (dual) |
151
+
152
+ ## Resource management
153
+
154
+ WASM heap memory is not garbage collected. Call `.dispose()` on every model when done.
155
+ A `FinalizationRegistry` safety net warns if you forget, but do not rely on it.
156
+
157
+ ## Build from source
158
+
159
+ Requires [Emscripten](https://emscripten.org/) (emsdk) activated.
160
+
161
+ ```bash
162
+ git clone --recurse-submodules https://github.com/wlearn-org/liblinear-wasm
163
+ cd liblinear-wasm
164
+ npm run build
165
+ npm test
166
+ ```
167
+
168
+ If you already cloned without `--recurse-submodules`:
169
+
170
+ ```bash
171
+ git submodule update --init
172
+ ```
173
+
174
+ ## License
175
+
176
+ BSD-3-Clause (same as upstream LIBLINEAR)
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@wlearn/liblinear",
3
+ "version": "0.1.0",
4
+ "description": "LIBLINEAR v2.50 compiled to WebAssembly -- linear classification and regression in browsers and Node.js",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "wasm/",
13
+ "LICENSE",
14
+ "README.md",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "sideEffects": false,
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "test": "node --experimental-vm-modules test/test.js",
23
+ "build": "bash scripts/build-wasm.sh",
24
+ "verify": "bash scripts/verify-exports.sh"
25
+ },
26
+ "dependencies": {
27
+ "@wlearn/types": "0.1.0",
28
+ "@wlearn/core": "0.1.0"
29
+ },
30
+ "keywords": [
31
+ "liblinear",
32
+ "svm",
33
+ "logistic-regression",
34
+ "machine-learning",
35
+ "wasm",
36
+ "webassembly",
37
+ "wlearn"
38
+ ],
39
+ "author": "Anton Zemlyansky",
40
+ "license": "BSD-3-Clause"
41
+ }
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ export { loadLinear, getWasm } from './wasm.js'
2
+ export { LinearModel, Solver } from './model.js'
3
+
4
+ // Convenience: create, fit, return fitted model
5
+ export async function train(params, X, y) {
6
+ const model = await LinearModel.create(params)
7
+ model.fit(X, y)
8
+ return model
9
+ }
10
+
11
+ // Convenience: load WLRN bundle and predict, auto-disposes model
12
+ export async function predict(bundleBytes, X) {
13
+ const model = await LinearModel.load(bundleBytes)
14
+ const result = model.predict(X)
15
+ model.dispose()
16
+ return result
17
+ }
package/src/model.js ADDED
@@ -0,0 +1,475 @@
1
+ import { getWasm, loadLinear } from './wasm.js'
2
+ import {
3
+ normalizeX, normalizeY,
4
+ encodeBundle, decodeBundle,
5
+ register,
6
+ DisposedError, NotFittedError
7
+ } from '@wlearn/core'
8
+
9
+ // FinalizationRegistry safety net -- warns if dispose() was never called
10
+ const leakRegistry = typeof FinalizationRegistry !== 'undefined'
11
+ ? new FinalizationRegistry(({ ptr, freeFn }) => {
12
+ if (ptr[0]) {
13
+ console.warn('@wlearn/liblinear: Model was not disposed -- calling free() automatically. This is a bug in your code.')
14
+ freeFn(ptr[0])
15
+ }
16
+ })
17
+ : null
18
+
19
+ // --- Solver constants ---
20
+
21
+ export const Solver = {
22
+ L2R_LR: 0,
23
+ L2R_L2LOSS_SVC_DUAL: 1,
24
+ L2R_L2LOSS_SVC: 2,
25
+ L2R_L1LOSS_SVC_DUAL: 3,
26
+ MCSVM_CS: 4,
27
+ L1R_L2LOSS_SVC: 5,
28
+ L1R_LR: 6,
29
+ L2R_LR_DUAL: 7,
30
+ L2R_L2LOSS_SVR: 11,
31
+ L2R_L2LOSS_SVR_DUAL: 12,
32
+ L2R_L1LOSS_SVR_DUAL: 13
33
+ }
34
+
35
+ const SOLVER_NAMES = Object.fromEntries(
36
+ Object.entries(Solver).map(([k, v]) => [v, k])
37
+ )
38
+
39
+ const LR_SOLVERS = new Set([Solver.L2R_LR, Solver.L1R_LR, Solver.L2R_LR_DUAL])
40
+ const SVR_SOLVERS = new Set([Solver.L2R_L2LOSS_SVR, Solver.L2R_L2LOSS_SVR_DUAL, Solver.L2R_L1LOSS_SVR_DUAL])
41
+
42
+ function resolveSolver(s) {
43
+ if (typeof s === 'number') return s
44
+ if (typeof s === 'string' && s in Solver) return Solver[s]
45
+ return Solver.L2R_LR
46
+ }
47
+
48
+ // --- Helper: write C string to WASM heap ---
49
+
50
+ function withCString(wasm, str, fn) {
51
+ const bytes = new TextEncoder().encode(str + '\0')
52
+ const ptr = wasm._malloc(bytes.length)
53
+ wasm.HEAPU8.set(bytes, ptr)
54
+ try {
55
+ return fn(ptr)
56
+ } finally {
57
+ wasm._free(ptr)
58
+ }
59
+ }
60
+
61
+ function getLastError() {
62
+ const wasm = getWasm()
63
+ return wasm.ccall('wl_linear_get_last_error', 'string', [], [])
64
+ }
65
+
66
+ // --- Internal sentinel for load path ---
67
+ const LOAD_SENTINEL = Symbol('load')
68
+
69
+ // --- LinearModel ---
70
+
71
+ export class LinearModel {
72
+ #handle = null
73
+ #freed = false
74
+ #ptrRef = null
75
+ #params = {}
76
+ #coerce = 'auto'
77
+ #warned = false
78
+ #fitted = false
79
+
80
+ constructor(handle, params, coerce) {
81
+ if (handle === LOAD_SENTINEL) {
82
+ // Internal: created by load()
83
+ this.#handle = params // params holds the handle in this path
84
+ this.#params = coerce || {} // coerce holds params in this path
85
+ this.#coerce = this.#params.coerce || 'auto'
86
+ this.#fitted = true
87
+ } else {
88
+ // Normal construction (from create())
89
+ this.#handle = null
90
+ this.#params = handle || {}
91
+ this.#coerce = this.#params.coerce || 'auto'
92
+ }
93
+
94
+ this.#freed = false
95
+ if (this.#handle) {
96
+ this.#ptrRef = [this.#handle]
97
+ if (leakRegistry) {
98
+ leakRegistry.register(this, {
99
+ ptr: this.#ptrRef,
100
+ freeFn: (h) => getWasm()._wl_linear_free_model(h)
101
+ }, this)
102
+ }
103
+ }
104
+ }
105
+
106
+ static async create(params = {}) {
107
+ await loadLinear()
108
+ return new LinearModel(params)
109
+ }
110
+
111
+ // --- Estimator interface ---
112
+
113
+ fit(X, y) {
114
+ this.#ensureFitted(false)
115
+ const wasm = getWasm()
116
+
117
+ // Dispose previous model if refitting
118
+ if (this.#handle) {
119
+ wasm._wl_linear_free_model(this.#handle)
120
+ this.#handle = null
121
+ if (this.#ptrRef) this.#ptrRef[0] = null
122
+ if (leakRegistry) leakRegistry.unregister(this)
123
+ }
124
+
125
+ const { data: xData, rows, cols } = this.#normalizeX(X)
126
+ const yNorm = normalizeY(y)
127
+ // WASM boundary requires Float64Array
128
+ const yData = yNorm instanceof Float64Array ? yNorm : new Float64Array(yNorm)
129
+
130
+ if (yData.length !== rows) {
131
+ throw new Error(`y length (${yData.length}) does not match X rows (${rows})`)
132
+ }
133
+
134
+ // Allocate X on WASM heap (float64)
135
+ const xBytes = xData.length * 8
136
+ const xPtr = wasm._malloc(xBytes)
137
+ wasm.HEAPF64.set(xData, xPtr / 8)
138
+
139
+ // Allocate y on WASM heap
140
+ const yBytes = yData.length * 8
141
+ const yPtr = wasm._malloc(yBytes)
142
+ wasm.HEAPF64.set(yData, yPtr / 8)
143
+
144
+ const solver = resolveSolver(this.#params.solver)
145
+ const C = this.#params.C ?? 1.0
146
+ const eps = this.#params.eps ?? 0.01
147
+ const bias = this.#params.bias ?? -1
148
+ const p = this.#params.p ?? 0.1
149
+
150
+ const modelPtr = wasm._wl_linear_train(
151
+ xPtr, rows, cols,
152
+ yPtr,
153
+ solver, C, eps, bias, p,
154
+ 0, 0, 0 // nr_weight, weight_label, weight (none)
155
+ )
156
+
157
+ wasm._free(xPtr)
158
+ wasm._free(yPtr)
159
+
160
+ if (!modelPtr) {
161
+ throw new Error(`Training failed: ${getLastError()}`)
162
+ }
163
+
164
+ this.#handle = modelPtr
165
+ this.#fitted = true
166
+
167
+ // Register for leak detection
168
+ this.#ptrRef = [this.#handle]
169
+ if (leakRegistry) {
170
+ leakRegistry.register(this, {
171
+ ptr: this.#ptrRef,
172
+ freeFn: (h) => getWasm()._wl_linear_free_model(h)
173
+ }, this)
174
+ }
175
+
176
+ return this
177
+ }
178
+
179
+ predict(X) {
180
+ this.#ensureFitted()
181
+ const wasm = getWasm()
182
+ const { data: xData, rows, cols } = this.#normalizeX(X)
183
+
184
+ const xPtr = wasm._malloc(xData.length * 8)
185
+ wasm.HEAPF64.set(xData, xPtr / 8)
186
+
187
+ const outPtr = wasm._malloc(rows * 8)
188
+
189
+ const ret = wasm._wl_linear_predict(this.#handle, xPtr, rows, cols, outPtr)
190
+
191
+ if (ret !== 0) {
192
+ wasm._free(xPtr)
193
+ wasm._free(outPtr)
194
+ throw new Error(`Predict failed: ${getLastError()}`)
195
+ }
196
+
197
+ const result = new Float64Array(rows)
198
+ for (let i = 0; i < rows; i++) {
199
+ result[i] = wasm.HEAPF64[outPtr / 8 + i]
200
+ }
201
+
202
+ wasm._free(xPtr)
203
+ wasm._free(outPtr)
204
+ return result
205
+ }
206
+
207
+ predictProba(X) {
208
+ this.#ensureFitted()
209
+ const wasm = getWasm()
210
+ const { data: xData, rows, cols } = this.#normalizeX(X)
211
+ const nrClass = this.nrClass
212
+
213
+ const xPtr = wasm._malloc(xData.length * 8)
214
+ wasm.HEAPF64.set(xData, xPtr / 8)
215
+
216
+ const outPtr = wasm._malloc(rows * nrClass * 8)
217
+
218
+ const ret = wasm._wl_linear_predict_probability(this.#handle, xPtr, rows, cols, outPtr)
219
+
220
+ if (ret !== 0) {
221
+ wasm._free(xPtr)
222
+ wasm._free(outPtr)
223
+ throw new Error(`predictProba failed: ${getLastError()}`)
224
+ }
225
+
226
+ const total = rows * nrClass
227
+ const result = new Float64Array(total)
228
+ for (let i = 0; i < total; i++) {
229
+ result[i] = wasm.HEAPF64[outPtr / 8 + i]
230
+ }
231
+
232
+ wasm._free(xPtr)
233
+ wasm._free(outPtr)
234
+ return result
235
+ }
236
+
237
+ decisionFunction(X) {
238
+ this.#ensureFitted()
239
+ const wasm = getWasm()
240
+ const { data: xData, rows, cols } = this.#normalizeX(X)
241
+ const dim = this.decisionDim
242
+
243
+ const xPtr = wasm._malloc(xData.length * 8)
244
+ wasm.HEAPF64.set(xData, xPtr / 8)
245
+
246
+ const outPtr = wasm._malloc(rows * dim * 8)
247
+
248
+ const ret = wasm._wl_linear_predict_values(this.#handle, xPtr, rows, cols, outPtr)
249
+
250
+ if (ret !== 0) {
251
+ wasm._free(xPtr)
252
+ wasm._free(outPtr)
253
+ throw new Error(`decisionFunction failed: ${getLastError()}`)
254
+ }
255
+
256
+ const total = rows * dim
257
+ const result = new Float64Array(total)
258
+ for (let i = 0; i < total; i++) {
259
+ result[i] = wasm.HEAPF64[outPtr / 8 + i]
260
+ }
261
+
262
+ wasm._free(xPtr)
263
+ wasm._free(outPtr)
264
+ return result
265
+ }
266
+
267
+ score(X, y) {
268
+ const preds = this.predict(X)
269
+ const yArr = normalizeY(y)
270
+
271
+ if (this.#isRegressor()) {
272
+ // R-squared
273
+ let ssRes = 0, ssTot = 0
274
+ let yMean = 0
275
+ for (let i = 0; i < yArr.length; i++) yMean += yArr[i]
276
+ yMean /= yArr.length
277
+ for (let i = 0; i < yArr.length; i++) {
278
+ ssRes += (yArr[i] - preds[i]) ** 2
279
+ ssTot += (yArr[i] - yMean) ** 2
280
+ }
281
+ return ssTot === 0 ? 0 : 1 - ssRes / ssTot
282
+ } else {
283
+ // Accuracy
284
+ let correct = 0
285
+ for (let i = 0; i < preds.length; i++) {
286
+ if (preds[i] === yArr[i]) correct++
287
+ }
288
+ return correct / preds.length
289
+ }
290
+ }
291
+
292
+ // --- Model I/O ---
293
+
294
+ save() {
295
+ this.#ensureFitted()
296
+ const rawBytes = this.#saveRaw()
297
+ const solver = resolveSolver(this.#params.solver)
298
+ const typeId = SVR_SOLVERS.has(solver)
299
+ ? 'wlearn.liblinear.regressor@1'
300
+ : 'wlearn.liblinear.classifier@1'
301
+ return encodeBundle(
302
+ { typeId, params: this.getParams() },
303
+ [{ id: 'model', data: rawBytes }]
304
+ )
305
+ }
306
+
307
+ static async load(bytes) {
308
+ const { manifest, toc, blobs } = decodeBundle(bytes)
309
+ return LinearModel._fromBundle(manifest, toc, blobs)
310
+ }
311
+
312
+ static async _fromBundle(manifest, toc, blobs) {
313
+ await loadLinear()
314
+ const wasm = getWasm()
315
+
316
+ const entry = toc.find(e => e.id === 'model')
317
+ if (!entry) throw new Error('Bundle missing "model" artifact')
318
+ const raw = blobs.subarray(entry.offset, entry.offset + entry.length)
319
+
320
+ const bufPtr = wasm._malloc(raw.length)
321
+ wasm.HEAPU8.set(raw, bufPtr)
322
+ const modelPtr = wasm._wl_linear_load_model(bufPtr, raw.length)
323
+ wasm._free(bufPtr)
324
+
325
+ if (!modelPtr) {
326
+ throw new Error(`load failed: ${getLastError()}`)
327
+ }
328
+
329
+ return new LinearModel(LOAD_SENTINEL, modelPtr, manifest.params || {})
330
+ }
331
+
332
+ dispose() {
333
+ if (this.#freed) return
334
+ this.#freed = true
335
+
336
+ if (this.#handle) {
337
+ const wasm = getWasm()
338
+ wasm._wl_linear_free_model(this.#handle)
339
+ }
340
+
341
+ if (this.#ptrRef) this.#ptrRef[0] = null
342
+ if (leakRegistry) leakRegistry.unregister(this)
343
+
344
+ this.#handle = null
345
+ this.#fitted = false
346
+ }
347
+
348
+ // --- Params ---
349
+
350
+ getParams() {
351
+ return { ...this.#params }
352
+ }
353
+
354
+ setParams(p) {
355
+ Object.assign(this.#params, p)
356
+ if ('coerce' in p) this.#coerce = p.coerce
357
+ return this
358
+ }
359
+
360
+ static defaultSearchSpace() {
361
+ return {
362
+ solver: { type: 'categorical', values: ['L2R_LR', 'L2R_L2LOSS_SVC_DUAL', 'L1R_LR'] },
363
+ C: { type: 'log_uniform', low: 1e-4, high: 1e4 },
364
+ eps: { type: 'log_uniform', low: 1e-5, high: 1e-1 }
365
+ }
366
+ }
367
+
368
+ // --- Inspection ---
369
+
370
+ get nrClass() {
371
+ this.#ensureFitted()
372
+ return getWasm()._wl_linear_get_nr_class(this.#handle)
373
+ }
374
+
375
+ get nrFeature() {
376
+ this.#ensureFitted()
377
+ return getWasm()._wl_linear_get_nr_feature(this.#handle)
378
+ }
379
+
380
+ get classes() {
381
+ this.#ensureFitted()
382
+ const wasm = getWasm()
383
+ const n = this.nrClass
384
+ const outPtr = wasm._malloc(n * 4)
385
+ wasm._wl_linear_get_labels(this.#handle, outPtr)
386
+ const result = new Int32Array(n)
387
+ for (let i = 0; i < n; i++) {
388
+ result[i] = wasm.getValue(outPtr + i * 4, 'i32')
389
+ }
390
+ wasm._free(outPtr)
391
+ return result
392
+ }
393
+
394
+ get isFitted() {
395
+ return this.#fitted && !this.#freed
396
+ }
397
+
398
+ get capabilities() {
399
+ const solver = resolveSolver(this.#params.solver)
400
+ const isRegressor = SVR_SOLVERS.has(solver)
401
+ const hasProba = LR_SOLVERS.has(solver)
402
+ return {
403
+ classifier: !isRegressor,
404
+ regressor: isRegressor,
405
+ predictProba: hasProba,
406
+ decisionFunction: true,
407
+ sampleWeight: false,
408
+ csr: false,
409
+ earlyStopping: false
410
+ }
411
+ }
412
+
413
+ get probaDim() {
414
+ return this.isFitted ? this.nrClass : 0
415
+ }
416
+
417
+ get decisionDim() {
418
+ if (!this.isFitted) return 0
419
+ const n = this.nrClass
420
+ return n === 2 ? 1 : n
421
+ }
422
+
423
+ // --- Private helpers ---
424
+
425
+ #saveRaw() {
426
+ const wasm = getWasm()
427
+
428
+ const outBufPtr = wasm._malloc(4) // char** (pointer to buffer)
429
+ const outLenPtr = wasm._malloc(4) // int*
430
+
431
+ const ret = wasm._wl_linear_save_model(this.#handle, outBufPtr, outLenPtr)
432
+
433
+ if (ret !== 0) {
434
+ wasm._free(outBufPtr)
435
+ wasm._free(outLenPtr)
436
+ throw new Error(`save failed: ${getLastError()}`)
437
+ }
438
+
439
+ const bufPtr = wasm.getValue(outBufPtr, 'i32')
440
+ const bufLen = wasm.getValue(outLenPtr, 'i32')
441
+
442
+ const result = new Uint8Array(bufLen)
443
+ result.set(wasm.HEAPU8.subarray(bufPtr, bufPtr + bufLen))
444
+
445
+ wasm._wl_linear_free_buffer(bufPtr)
446
+ wasm._free(outBufPtr)
447
+ wasm._free(outLenPtr)
448
+
449
+ return result
450
+ }
451
+
452
+ #normalizeX(X) {
453
+ const coerce = this.#warned ? 'auto' : this.#coerce
454
+ const result = normalizeX(X, coerce)
455
+ if (this.#coerce === 'warn' && !this.#warned && Array.isArray(X)) {
456
+ this.#warned = true
457
+ }
458
+ return result
459
+ }
460
+
461
+ #ensureFitted(requireFit = true) {
462
+ if (this.#freed) throw new DisposedError('LinearModel has been disposed.')
463
+ if (requireFit && !this.#fitted) throw new NotFittedError('LinearModel is not fitted. Call fit() first.')
464
+ }
465
+
466
+ #isRegressor() {
467
+ const solver = resolveSolver(this.#params.solver)
468
+ return SVR_SOLVERS.has(solver)
469
+ }
470
+ }
471
+
472
+ // --- Register loaders with @wlearn/core ---
473
+
474
+ register('wlearn.liblinear.classifier@1', (m, t, b) => LinearModel._fromBundle(m, t, b))
475
+ register('wlearn.liblinear.regressor@1', (m, t, b) => LinearModel._fromBundle(m, t, b))
package/src/wasm.js ADDED
@@ -0,0 +1,27 @@
1
+ // WASM loader -- loads the LIBLINEAR WASM module (singleton, lazy init)
2
+
3
+ import { createRequire } from 'module'
4
+
5
+ let wasmModule = null
6
+ let loading = null
7
+
8
+ export async function loadLinear(options = {}) {
9
+ if (wasmModule) return wasmModule
10
+ if (loading) return loading
11
+
12
+ loading = (async () => {
13
+ // SINGLE_FILE=1: .wasm is embedded in the .js file, no locateFile needed
14
+ // Emscripten output is CJS, use createRequire for ESM compatibility
15
+ const require = createRequire(import.meta.url)
16
+ const createLinear = require('../wasm/linear.cjs')
17
+ wasmModule = await createLinear(options)
18
+ return wasmModule
19
+ })()
20
+
21
+ return loading
22
+ }
23
+
24
+ export function getWasm() {
25
+ if (!wasmModule) throw new Error('WASM not loaded -- call loadLinear() first')
26
+ return wasmModule
27
+ }
@@ -0,0 +1,6 @@
1
+ upstream: liblinear v2.50
2
+ upstream_commit: 491c9f1188b97ba70847c70a68be363d186ddf9d
3
+ build_date: 2026-02-27T13:01:37Z
4
+ emscripten: emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 5.0.2 (dc80f645ee70178c11666de0c3860d9e064d50e4)
5
+ build_flags: -O2 SINGLE_FILE=1
6
+ wasm_embedded: true
Binary file