@wlearn/libsvm 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,17 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ - Initial release
6
+ - LIBSVM v3.37 compiled to WASM via Emscripten
7
+ - Unified sklearn-style API: `create()`, `fit()`, `predict()`, `score()`, `save()`, `dispose()`
8
+ - Kernel SVM: C-SVC, nu-SVC, one-class SVM, epsilon-SVR, nu-SVR
9
+ - Kernels: linear, polynomial, RBF, sigmoid
10
+ - Buffer-based model I/O (no filesystem dependency)
11
+ - Accepts both typed matrices and number[][] with configurable coercion
12
+ - `predictProba()` for probability estimates
13
+ - `decisionFunction()` for decision values
14
+ - `getParams()`/`setParams()` for AutoML integration
15
+ - `defaultSearchSpace()` for hyperparameter search
16
+ - `FinalizationRegistry` safety net for leak detection
17
+ - BSD-3-Clause license (same as upstream LIBSVM)
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ Copyright (c) 2000-2025 Chih-Chung Chang and Chih-Jen Lin
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,167 @@
1
+ # @wlearn/libsvm
2
+
3
+ LIBSVM v3.37 compiled to WebAssembly. Kernel SVM classification, regression, and novelty detection in browsers and Node.js.
4
+
5
+ Based on [LIBSVM v3.37](https://www.csie.ntu.edu.tw/~cjlin/libsvm/) (BSD-3-Clause). Zero dependencies. ESM.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @wlearn/libsvm
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```js
16
+ import { SVMModel } from '@wlearn/libsvm'
17
+
18
+ const model = await SVMModel.create({
19
+ svmType: 'C_SVC',
20
+ kernel: 'RBF',
21
+ C: 1.0,
22
+ gamma: 0.5
23
+ })
24
+
25
+ // Train -- accepts number[][] or { data: Float64Array, rows, cols }
26
+ model.fit(
27
+ [[1, 2], [3, 4], [5, 6], [7, 8]],
28
+ [0, 0, 1, 1]
29
+ )
30
+
31
+ // Predict
32
+ const preds = model.predict([[2, 3], [6, 7]]) // Float64Array
33
+
34
+ // Score
35
+ const accuracy = model.score([[2, 3], [6, 7]], [0, 1])
36
+
37
+ // Save / load
38
+ const buf = model.save() // Uint8Array
39
+ const model2 = await SVMModel.load(buf)
40
+
41
+ // Clean up -- required, WASM memory is not garbage collected
42
+ model.dispose()
43
+ model2.dispose()
44
+ ```
45
+
46
+ ## Typed matrix input (fast path)
47
+
48
+ ```js
49
+ const X = {
50
+ data: new Float64Array([1, 2, 3, 4, 5, 6, 7, 8]),
51
+ rows: 4,
52
+ cols: 2
53
+ }
54
+ model.fit(X, new Float64Array([0, 0, 1, 1]))
55
+ ```
56
+
57
+ ## Input coercion policy
58
+
59
+ ```js
60
+ const model = await SVMModel.create({ coerce: 'auto' }) // convert silently (default)
61
+ const model = await SVMModel.create({ coerce: 'warn' }) // warn once per instance
62
+ const model = await SVMModel.create({ coerce: 'error' }) // throw on non-typed input
63
+ ```
64
+
65
+ ## API
66
+
67
+ ### `SVMModel.create(params?)`
68
+
69
+ Async factory. Loads WASM module, returns ready-to-use model.
70
+
71
+ Parameters:
72
+ - `svmType` -- `'C_SVC'` | `'NU_SVC'` | `'ONE_CLASS'` | `'EPSILON_SVR'` | `'NU_SVR'` (default: `'C_SVC'`)
73
+ - `kernel` -- `'LINEAR'` | `'POLY'` | `'RBF'` | `'SIGMOID'` (default: `'RBF'`)
74
+ - `C` -- regularization (default: `1.0`)
75
+ - `gamma` -- kernel coefficient, <= 0 means 1/n_features (default: `0`)
76
+ - `degree` -- polynomial degree (default: `3`)
77
+ - `coef0` -- independent term in kernel (default: `0`)
78
+ - `nu` -- for NU_SVC/NU_SVR/ONE_CLASS (default: `0.5`)
79
+ - `eps` -- stopping tolerance (default: `0.001`)
80
+ - `p` -- epsilon in SVR loss (default: `0.1`)
81
+ - `shrinking` -- use shrinking heuristic (default: `1`)
82
+ - `probability` -- enable probability estimates (default: `0`)
83
+ - `cacheSize` -- kernel cache in MB (default: `100`)
84
+ - `coerce` -- `'auto'` | `'warn'` | `'error'` (default: `'auto'`)
85
+
86
+ ### `model.fit(X, y)`
87
+
88
+ Train on data. Returns `this`.
89
+
90
+ ### `model.predict(X)`
91
+
92
+ Returns `Float64Array` of predicted labels.
93
+
94
+ ### `model.predictProba(X)`
95
+
96
+ Returns `Float64Array` of shape `nrow * nclass` (row-major probabilities).
97
+ Requires `probability: 1` in constructor params.
98
+
99
+ ### `model.decisionFunction(X)`
100
+
101
+ Returns `Float64Array` of decision values.
102
+ - Binary classification: `nrow` values
103
+ - Multi-class: `nrow * nr_class*(nr_class-1)/2` pairwise margins
104
+
105
+ ### `model.score(X, y)`
106
+
107
+ Returns accuracy (classification) or R-squared (regression).
108
+
109
+ ### `model.save()` / `SVMModel.load(buffer)`
110
+
111
+ Save to / load from `Uint8Array` (native LIBSVM format).
112
+
113
+ ### `model.dispose()`
114
+
115
+ Free WASM memory. Required. Idempotent.
116
+
117
+ ### `model.getParams()` / `model.setParams(p)`
118
+
119
+ Get/set hyperparameters.
120
+
121
+ ### `SVMModel.defaultSearchSpace()`
122
+
123
+ Returns default hyperparameter search space for AutoML.
124
+
125
+ ## SVM types
126
+
127
+ | Name | Code | Task |
128
+ |------|------|------|
129
+ | C_SVC | 0 | C-support vector classification |
130
+ | NU_SVC | 1 | nu-support vector classification |
131
+ | ONE_CLASS | 2 | One-class SVM (novelty detection) |
132
+ | EPSILON_SVR | 3 | epsilon-support vector regression |
133
+ | NU_SVR | 4 | nu-support vector regression |
134
+
135
+ ## Kernels
136
+
137
+ | Name | Code | Formula |
138
+ |------|------|---------|
139
+ | LINEAR | 0 | u'*v |
140
+ | POLY | 1 | (gamma*u'*v + coef0)^degree |
141
+ | RBF | 2 | exp(-gamma*\|u-v\|^2) |
142
+ | SIGMOID | 3 | tanh(gamma*u'*v + coef0) |
143
+
144
+ ## Resource management
145
+
146
+ WASM heap memory is not garbage collected. Call `.dispose()` on every model when done.
147
+
148
+ ## Build from source
149
+
150
+ Requires [Emscripten](https://emscripten.org/) (emsdk) activated.
151
+
152
+ ```bash
153
+ git clone --recurse-submodules https://github.com/wlearn-org/libsvm-wasm
154
+ cd libsvm-wasm
155
+ npm run build
156
+ npm test
157
+ ```
158
+
159
+ If you already cloned without `--recurse-submodules`:
160
+
161
+ ```bash
162
+ git submodule update --init
163
+ ```
164
+
165
+ ## License
166
+
167
+ BSD-3-Clause (same as upstream LIBSVM)
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@wlearn/libsvm",
3
+ "version": "0.1.0",
4
+ "description": "LIBSVM v3.37 compiled to WebAssembly -- SVM classification, regression, and novelty detection 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
+ "keywords": [
27
+ "libsvm",
28
+ "svm",
29
+ "kernel",
30
+ "machine-learning",
31
+ "wasm",
32
+ "webassembly",
33
+ "wlearn"
34
+ ],
35
+ "author": "Anton Zemlyansky",
36
+ "license": "BSD-3-Clause",
37
+ "dependencies": {
38
+ "@wlearn/types": "0.1.0",
39
+ "@wlearn/core": "0.1.0"
40
+ }
41
+ }
package/src/index.js ADDED
@@ -0,0 +1,17 @@
1
+ export { loadSVM, getWasm } from './wasm.js'
2
+ export { SVMModel, SVMType, Kernel } from './model.js'
3
+
4
+ // Convenience: create, fit, return fitted model
5
+ export async function train(params, X, y) {
6
+ const model = await (await import('./model.js')).SVMModel.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 (await import('./model.js')).SVMModel.load(bundleBytes)
14
+ const result = model.predict(X)
15
+ model.dispose()
16
+ return result
17
+ }
package/src/model.js ADDED
@@ -0,0 +1,481 @@
1
+ import { getWasm, loadSVM } 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/libsvm: Model was not disposed -- calling free() automatically. This is a bug in your code.')
14
+ freeFn(ptr[0])
15
+ }
16
+ })
17
+ : null
18
+
19
+ // --- SVM type and kernel constants ---
20
+
21
+ export const SVMType = {
22
+ C_SVC: 0,
23
+ NU_SVC: 1,
24
+ ONE_CLASS: 2,
25
+ EPSILON_SVR: 3,
26
+ NU_SVR: 4
27
+ }
28
+
29
+ export const Kernel = {
30
+ LINEAR: 0,
31
+ POLY: 1,
32
+ RBF: 2,
33
+ SIGMOID: 3
34
+ }
35
+
36
+ const SVR_TYPES = new Set([SVMType.EPSILON_SVR, SVMType.NU_SVR])
37
+
38
+ // --- Input resolution ---
39
+
40
+ function resolveSVMType(s) {
41
+ if (typeof s === 'number') return s
42
+ if (typeof s === 'string' && s in SVMType) return SVMType[s]
43
+ return SVMType.C_SVC
44
+ }
45
+
46
+ function resolveKernel(k) {
47
+ if (typeof k === 'number') return k
48
+ if (typeof k === 'string' && k in Kernel) return Kernel[k]
49
+ return Kernel.RBF
50
+ }
51
+
52
+ function getLastError() {
53
+ const wasm = getWasm()
54
+ return wasm.ccall('wl_svm_get_last_error', 'string', [], [])
55
+ }
56
+
57
+ // --- Internal sentinel for load path ---
58
+ const LOAD_SENTINEL = Symbol('load')
59
+
60
+ // --- SVMModel ---
61
+
62
+ export class SVMModel {
63
+ #handle = null
64
+ #freed = false
65
+ #ptrRef = null
66
+ #params = {}
67
+ #coerce = 'auto'
68
+ #warned = false
69
+ #fitted = false
70
+ #ncol = 0 // track feature count for gamma default
71
+
72
+ constructor(handle, params, coerce) {
73
+ if (handle === LOAD_SENTINEL) {
74
+ this.#handle = params
75
+ this.#params = coerce || {}
76
+ this.#coerce = this.#params.coerce || 'auto'
77
+ this.#fitted = true
78
+ } else {
79
+ this.#handle = null
80
+ this.#params = handle || {}
81
+ this.#coerce = this.#params.coerce || 'auto'
82
+ }
83
+
84
+ this.#freed = false
85
+ if (this.#handle) {
86
+ this.#ptrRef = [this.#handle]
87
+ if (leakRegistry) {
88
+ leakRegistry.register(this, {
89
+ ptr: this.#ptrRef,
90
+ freeFn: (h) => getWasm()._wl_svm_free_model(h)
91
+ }, this)
92
+ }
93
+ }
94
+ }
95
+
96
+ static async create(params = {}) {
97
+ await loadSVM()
98
+ return new SVMModel(params)
99
+ }
100
+
101
+ // --- Estimator interface ---
102
+
103
+ fit(X, y) {
104
+ this.#ensureFitted(false)
105
+ const wasm = getWasm()
106
+
107
+ // Dispose previous model if refitting
108
+ if (this.#handle) {
109
+ wasm._wl_svm_free_model(this.#handle)
110
+ this.#handle = null
111
+ if (this.#ptrRef) this.#ptrRef[0] = null
112
+ if (leakRegistry) leakRegistry.unregister(this)
113
+ }
114
+
115
+ const { data: xData, rows, cols } = this.#normalizeX(X)
116
+ const yNorm = normalizeY(y)
117
+ // WASM boundary requires Float64Array
118
+ const yData = yNorm instanceof Float64Array ? yNorm : new Float64Array(yNorm)
119
+ this.#ncol = cols
120
+
121
+ if (yData.length !== rows) {
122
+ throw new Error(`y length (${yData.length}) does not match X rows (${rows})`)
123
+ }
124
+
125
+ // Allocate on WASM heap
126
+ const xPtr = wasm._malloc(xData.length * 8)
127
+ wasm.HEAPF64.set(xData, xPtr / 8)
128
+
129
+ const yPtr = wasm._malloc(yData.length * 8)
130
+ wasm.HEAPF64.set(yData, yPtr / 8)
131
+
132
+ const svmType = resolveSVMType(this.#params.svmType)
133
+ const kernel = resolveKernel(this.#params.kernel)
134
+ const degree = this.#params.degree ?? 3
135
+ const gamma = this.#params.gamma ?? 0 // 0 means 1/n_features in C wrapper
136
+ const coef0 = this.#params.coef0 ?? 0
137
+ const C = this.#params.C ?? 1.0
138
+ const nu = this.#params.nu ?? 0.5
139
+ const eps = this.#params.eps ?? 0.001
140
+ const p = this.#params.p ?? 0.1
141
+ const shrinking = this.#params.shrinking ?? 1
142
+ const probability = this.#params.probability ?? 0
143
+ const cacheSize = this.#params.cacheSize ?? 100
144
+
145
+ const modelPtr = wasm._wl_svm_train(
146
+ xPtr, rows, cols, yPtr,
147
+ svmType, kernel, degree, gamma, coef0,
148
+ C, nu, eps, p,
149
+ shrinking, probability, cacheSize
150
+ )
151
+
152
+ wasm._free(xPtr)
153
+ wasm._free(yPtr)
154
+
155
+ if (!modelPtr) {
156
+ throw new Error(`Training failed: ${getLastError()}`)
157
+ }
158
+
159
+ this.#handle = modelPtr
160
+ this.#fitted = true
161
+
162
+ this.#ptrRef = [this.#handle]
163
+ if (leakRegistry) {
164
+ leakRegistry.register(this, {
165
+ ptr: this.#ptrRef,
166
+ freeFn: (h) => getWasm()._wl_svm_free_model(h)
167
+ }, this)
168
+ }
169
+
170
+ return this
171
+ }
172
+
173
+ predict(X) {
174
+ this.#ensureFitted()
175
+ const wasm = getWasm()
176
+ const { data: xData, rows, cols } = this.#normalizeX(X)
177
+
178
+ const xPtr = wasm._malloc(xData.length * 8)
179
+ wasm.HEAPF64.set(xData, xPtr / 8)
180
+
181
+ const outPtr = wasm._malloc(rows * 8)
182
+
183
+ const ret = wasm._wl_svm_predict(this.#handle, xPtr, rows, cols, outPtr)
184
+
185
+ if (ret !== 0) {
186
+ wasm._free(xPtr)
187
+ wasm._free(outPtr)
188
+ throw new Error(`Predict failed: ${getLastError()}`)
189
+ }
190
+
191
+ const result = new Float64Array(rows)
192
+ for (let i = 0; i < rows; i++) {
193
+ result[i] = wasm.HEAPF64[outPtr / 8 + i]
194
+ }
195
+
196
+ wasm._free(xPtr)
197
+ wasm._free(outPtr)
198
+ return result
199
+ }
200
+
201
+ predictProba(X) {
202
+ this.#ensureFitted()
203
+ const wasm = getWasm()
204
+ const { data: xData, rows, cols } = this.#normalizeX(X)
205
+ const nrClass = this.nrClass
206
+
207
+ const xPtr = wasm._malloc(xData.length * 8)
208
+ wasm.HEAPF64.set(xData, xPtr / 8)
209
+
210
+ const outPtr = wasm._malloc(rows * nrClass * 8)
211
+
212
+ const ret = wasm._wl_svm_predict_probability(this.#handle, xPtr, rows, cols, outPtr)
213
+
214
+ if (ret !== 0) {
215
+ wasm._free(xPtr)
216
+ wasm._free(outPtr)
217
+ throw new Error(`predictProba failed: ${getLastError()}`)
218
+ }
219
+
220
+ const total = rows * nrClass
221
+ const result = new Float64Array(total)
222
+ for (let i = 0; i < total; i++) {
223
+ result[i] = wasm.HEAPF64[outPtr / 8 + i]
224
+ }
225
+
226
+ wasm._free(xPtr)
227
+ wasm._free(outPtr)
228
+ return result
229
+ }
230
+
231
+ decisionFunction(X) {
232
+ this.#ensureFitted()
233
+ const wasm = getWasm()
234
+ const { data: xData, rows, cols } = this.#normalizeX(X)
235
+
236
+ const xPtr = wasm._malloc(xData.length * 8)
237
+ wasm.HEAPF64.set(xData, xPtr / 8)
238
+
239
+ const dimPtr = wasm._malloc(4)
240
+ const nrClass = this.nrClass
241
+ const maxDim = nrClass * (nrClass - 1) / 2 || 1
242
+ const outPtr = wasm._malloc(rows * maxDim * 8)
243
+
244
+ const ret = wasm._wl_svm_predict_values(
245
+ this.#handle, xPtr, rows, cols, outPtr, dimPtr
246
+ )
247
+
248
+ if (ret !== 0) {
249
+ wasm._free(xPtr)
250
+ wasm._free(outPtr)
251
+ wasm._free(dimPtr)
252
+ throw new Error(`decisionFunction failed: ${getLastError()}`)
253
+ }
254
+
255
+ const dim = wasm.getValue(dimPtr, 'i32')
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
+ wasm._free(dimPtr)
265
+ return result
266
+ }
267
+
268
+ score(X, y) {
269
+ const preds = this.predict(X)
270
+ const yArr = normalizeY(y)
271
+
272
+ if (this.#isRegressor()) {
273
+ // R-squared
274
+ let ssRes = 0, ssTot = 0, 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 svmType = resolveSVMType(this.#params.svmType)
298
+ const typeId = SVR_TYPES.has(svmType)
299
+ ? 'wlearn.libsvm.regressor@1'
300
+ : 'wlearn.libsvm.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 SVMModel._fromBundle(manifest, toc, blobs)
310
+ }
311
+
312
+ static async _fromBundle(manifest, toc, blobs) {
313
+ await loadSVM()
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_svm_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 SVMModel(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_svm_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
+ svmType: { type: 'categorical', values: ['C_SVC', 'NU_SVC'] },
363
+ kernel: { type: 'categorical', values: ['RBF', 'LINEAR', 'POLY'] },
364
+ C: { type: 'log_uniform', low: 1e-3, high: 1e3 },
365
+ gamma: { type: 'log_uniform', low: 1e-5, high: 1e1 },
366
+ degree: { type: 'int_uniform', low: 2, high: 5, condition: { kernel: 'POLY' } },
367
+ nu: { type: 'uniform', low: 0.01, high: 0.99, condition: { svmType: 'NU_SVC' } }
368
+ }
369
+ }
370
+
371
+ // --- Inspection ---
372
+
373
+ get nrClass() {
374
+ this.#ensureFitted()
375
+ return getWasm()._wl_svm_get_nr_class(this.#handle)
376
+ }
377
+
378
+ get svCount() {
379
+ this.#ensureFitted()
380
+ return getWasm()._wl_svm_get_sv_count(this.#handle)
381
+ }
382
+
383
+ get classes() {
384
+ this.#ensureFitted()
385
+ const wasm = getWasm()
386
+ const n = this.nrClass
387
+ const outPtr = wasm._malloc(n * 4)
388
+ wasm._wl_svm_get_labels(this.#handle, outPtr)
389
+ const result = new Int32Array(n)
390
+ for (let i = 0; i < n; i++) {
391
+ result[i] = wasm.getValue(outPtr + i * 4, 'i32')
392
+ }
393
+ wasm._free(outPtr)
394
+ return result
395
+ }
396
+
397
+ get isFitted() {
398
+ return this.#fitted && !this.#freed
399
+ }
400
+
401
+ get capabilities() {
402
+ const svmType = resolveSVMType(this.#params.svmType)
403
+ const isRegressor = SVR_TYPES.has(svmType)
404
+ const probability = this.#params.probability ?? 0
405
+ return {
406
+ classifier: !isRegressor,
407
+ regressor: isRegressor,
408
+ predictProba: !isRegressor && probability === 1,
409
+ decisionFunction: true,
410
+ oneClass: svmType === SVMType.ONE_CLASS,
411
+ sampleWeight: false,
412
+ csr: false,
413
+ earlyStopping: false
414
+ }
415
+ }
416
+
417
+ get probaDim() {
418
+ return this.isFitted ? this.nrClass : 0
419
+ }
420
+
421
+ get decisionDim() {
422
+ if (!this.isFitted) return 0
423
+ const svmType = resolveSVMType(this.#params.svmType)
424
+ if (svmType === SVMType.ONE_CLASS || SVR_TYPES.has(svmType)) return 1
425
+ const n = this.nrClass
426
+ return n === 2 ? 1 : n * (n - 1) / 2
427
+ }
428
+
429
+ // --- Private helpers ---
430
+
431
+ #saveRaw() {
432
+ const wasm = getWasm()
433
+
434
+ const outBufPtr = wasm._malloc(4)
435
+ const outLenPtr = wasm._malloc(4)
436
+
437
+ const ret = wasm._wl_svm_save_model(this.#handle, outBufPtr, outLenPtr)
438
+
439
+ if (ret !== 0) {
440
+ wasm._free(outBufPtr)
441
+ wasm._free(outLenPtr)
442
+ throw new Error(`save failed: ${getLastError()}`)
443
+ }
444
+
445
+ const bufPtr = wasm.getValue(outBufPtr, 'i32')
446
+ const bufLen = wasm.getValue(outLenPtr, 'i32')
447
+
448
+ const result = new Uint8Array(bufLen)
449
+ result.set(wasm.HEAPU8.subarray(bufPtr, bufPtr + bufLen))
450
+
451
+ wasm._wl_svm_free_buffer(bufPtr)
452
+ wasm._free(outBufPtr)
453
+ wasm._free(outLenPtr)
454
+
455
+ return result
456
+ }
457
+
458
+ #normalizeX(X) {
459
+ const coerce = this.#warned ? 'auto' : this.#coerce
460
+ const result = normalizeX(X, coerce)
461
+ if (this.#coerce === 'warn' && !this.#warned && Array.isArray(X)) {
462
+ this.#warned = true
463
+ }
464
+ return result
465
+ }
466
+
467
+ #ensureFitted(requireFit = true) {
468
+ if (this.#freed) throw new DisposedError('SVMModel has been disposed.')
469
+ if (requireFit && !this.#fitted) throw new NotFittedError('SVMModel is not fitted. Call fit() first.')
470
+ }
471
+
472
+ #isRegressor() {
473
+ const svmType = resolveSVMType(this.#params.svmType)
474
+ return SVR_TYPES.has(svmType)
475
+ }
476
+ }
477
+
478
+ // --- Register loaders with @wlearn/core ---
479
+
480
+ register('wlearn.libsvm.classifier@1', (m, t, b) => SVMModel._fromBundle(m, t, b))
481
+ register('wlearn.libsvm.regressor@1', (m, t, b) => SVMModel._fromBundle(m, t, b))
package/src/wasm.js ADDED
@@ -0,0 +1,27 @@
1
+ // WASM loader -- loads the LIBSVM 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 loadSVM(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 createSVM = require('../wasm/svm.cjs')
17
+ wasmModule = await createSVM(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 loadSVM() first')
26
+ return wasmModule
27
+ }
@@ -0,0 +1,6 @@
1
+ upstream: libsvm v3.37
2
+ upstream_commit: 6b907139084abf2da4d6d3cb10dc3b7eaffa2fbb
3
+ build_date: 2026-02-27T13:03:12Z
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
package/wasm/svm.cjs ADDED
Binary file