From f8983c4806a9e0c4140e3b357da2bac1846c5a73 Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Mon, 23 Mar 2026 23:23:07 +0200 Subject: [PATCH] feat(playground): replace JS ML engine with WASM nisps-core Compile nisps-core C++ MLP to WASM (36KB) and use it as the ML engine in the playground, replacing the JavaScript port for inference, training, and weight manipulation. - Add extern "C" WASM bindings with spread-aware drawWeights/moveWeights - WasmIML class is a drop-in replacement for the JS IML - Inference runs on main thread via WASM (fast, synchronous) - Training runs in a Web Worker with its own WASM instance (non-blocking) - Interactive training (thumbs-up, train button) no longer freezes UI/audio - Preset loading and state restore still use sync training --- playground/js/a-app.js | 56 ++-- playground/js/nisps/nisps-wasm-worker.js | 122 +++++++ playground/js/nisps/nisps-wasm.js | 405 +++++++++++++++++++++++ playground/wasm/build.sh | 42 +++ playground/wasm/nisps.js | 2 + playground/wasm/nisps.wasm | Bin 0 -> 33677 bytes playground/wasm/nisps_bindings.cpp | 179 ++++++++++ 7 files changed, 775 insertions(+), 31 deletions(-) create mode 100644 playground/js/nisps/nisps-wasm-worker.js create mode 100644 playground/js/nisps/nisps-wasm.js create mode 100755 playground/wasm/build.sh create mode 100644 playground/wasm/nisps.js create mode 100755 playground/wasm/nisps.wasm create mode 100644 playground/wasm/nisps_bindings.cpp diff --git a/playground/js/a-app.js b/playground/js/a-app.js index 7a547f5..3d986fc 100644 --- a/playground/js/a-app.js +++ b/playground/js/a-app.js @@ -1,7 +1,7 @@ // NISPS Immersive — Design A // Full-viewport flow field with floating overlays -import { IML } from './nisps/iml.js'; +import { WasmIML } from './nisps/nisps-wasm.js'; import { FlowFieldVisualizer } from './ui/visualizer.js'; import { C15Bridge } from './synth/c15-bridge.js'; import { Arpeggiator } from './synth/arpeggiator.js'; @@ -665,7 +665,7 @@ function padPresetOutputs(outputs) { } // ---- Init ---- -function init() { +async function init() { // Parse ?tame URL param const urlParams = new URLSearchParams(window.location.search); // tame is handled at groupOverrides init time, no longer needed here @@ -673,8 +673,8 @@ function init() { if (isNaN(spreadLevel)) spreadLevel = 0.6; spreadLevel = Math.max(0, Math.min(1, spreadLevel)); - // IML — fresh random weights each boot, no state restoration - iml = new IML(N_INPUTS, N_OUTPUTS, [32, 48, 64], 1000, 1.0, 0.00001); + // IML — WASM-backed, fresh random weights each boot + iml = await WasmIML.create(N_INPUTS, N_OUTPUTS, [32, 48, 64], 1000, 1.0, 0.00001); iml.setLogger(msg => console.log('[NISPS]', msg)); // Canvas + Visualizer @@ -1157,17 +1157,9 @@ function onAddExample() { } function onTrain() { - const loss = trainModel(); - if (loss !== null) { - const outputs = iml.getOutputs(); - routeOutputs(outputs); - updateHeatmap(outputs); - syncRawParamsFromOutputs(outputs); - updateStatus(); - drawLossPlot(); - drawJoyMap(); - flash('btn-train'); - } + if (iml.isTraining) return; + flash('btn-train'); + trainModelAsync(); } function onRandomize() { @@ -1203,24 +1195,18 @@ function onClear() { // ---- RL mode ---- function onThumbsUp() { + if (iml.isTraining) return; + const inputs = [joyX, joyY]; const outputs = [...iml.getOutputs()]; iml.addExample(inputs, outputs); - trainModel(); - const trainedOutputs = iml.getOutputs(); - routeOutputs(trainedOutputs); - updateHeatmap(trainedOutputs); - syncRawParamsFromOutputs(trainedOutputs); - noiseLevel *= rlExplorationDecay; noiseLevel = Math.max(noiseLevel, 0.005); - updateStatus(); - drawLossPlot(); - drawJoyMap(); - updateNoiseRing(); flash('btn-thumbsup'); + updateNoiseRing(); + trainModelAsync(); } function onThumbsDown() { @@ -1239,14 +1225,22 @@ function onThumbsDown() { } // ---- Training ---- +// Sync — used for preset loading and state restore function trainModel() { - const loss = iml.train({ - onIteration: (iter, iterLoss) => { - if (iter % 8 !== 0) return; - drawLossPlot(); - }, + return iml.train(); +} + +// Async — used for interactive training (thumbs-up, train button) +function trainModelAsync(onDone) { + iml.trainAsync(({ loss, outputs }) => { + routeOutputs(outputs); + updateHeatmap(outputs); + syncRawParamsFromOutputs(outputs); + updateStatus(); + drawLossPlot(); + drawJoyMap(); + if (onDone) onDone(); }); - return loss; } // ---- Presets ---- diff --git a/playground/js/nisps/nisps-wasm-worker.js b/playground/js/nisps/nisps-wasm-worker.js new file mode 100644 index 0000000..4949463 --- /dev/null +++ b/playground/js/nisps/nisps-wasm-worker.js @@ -0,0 +1,122 @@ +// Web Worker for async WASM training (module worker) +// Loads its own nisps WASM instance for off-thread training + +import NispsModule from '../../wasm/nisps.js'; + +let mod = null; +let w = null; +let mlp = null; +let currentLayerSizes = null; + +async function ensureModule() { + if (mod) return; + mod = await NispsModule(); + w = { + mod, + create: mod.cwrap('nisps_mlp_create', 'number', ['number', 'number', 'number', 'number']), + destroy: mod.cwrap('nisps_mlp_destroy', null, ['number']), + weightCount: mod.cwrap('nisps_mlp_weight_count', 'number', ['number']), + getWeights: mod.cwrap('nisps_mlp_get_weights', null, ['number', 'number']), + setWeights: mod.cwrap('nisps_mlp_set_weights', null, ['number', 'number']), + train: mod.cwrap('nisps_mlp_train', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number']), + alloc: mod.cwrap('nisps_alloc', 'number', ['number']), + free: mod.cwrap('nisps_free', null, ['number']), + allocInt: mod.cwrap('nisps_alloc_int', 'number', ['number']), + freeInt: mod.cwrap('nisps_free_int', null, ['number']), + }; +} + +function toHeapF32(arr) { + const ptr = w.alloc(arr.length); + w.mod.HEAPF32.set(arr, ptr >> 2); + return ptr; +} + +function toHeapI32(arr) { + const ptr = w.allocInt(arr.length); + w.mod.HEAP32.set(arr, ptr >> 2); + return ptr; +} + +function fromHeapF32(ptr, length) { + const offset = ptr >> 2; + return Array.from(w.mod.HEAPF32.subarray(offset, offset + length)); +} + +function arraysEqual(a, b) { + if (!a || !b || a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} + +self.onmessage = async function(e) { + const { type, payload } = e.data; + + if (type === 'train') { + await ensureModule(); + + const { + layerSizes, activationIds, weights, + features, labels, + nInputs, nOutputs, + learningRate, maxIterations, convergenceThreshold, + } = payload; + + // Recreate MLP if architecture changed + if (!mlp || !arraysEqual(currentLayerSizes, layerSizes)) { + if (mlp) w.destroy(mlp); + const layerPtr = toHeapI32(new Int32Array(layerSizes)); + const actPtr = toHeapI32(new Int32Array(activationIds)); + mlp = w.create(layerPtr, layerSizes.length, actPtr, activationIds.length); + w.freeInt(layerPtr); + w.freeInt(actPtr); + currentLayerSizes = [...layerSizes]; + } + + // Load weights from main thread + const weightCount = w.weightCount(mlp); + const wPtr = toHeapF32(new Float32Array(weights)); + w.setWeights(mlp, wPtr); + w.free(wPtr); + + // Build flat training arrays with bias + const featureDim = nInputs + 1; + const nSamples = features.length; + const featFlat = new Float32Array(nSamples * featureDim); + const labFlat = new Float32Array(nSamples * nOutputs); + + for (let i = 0; i < nSamples; i++) { + for (let j = 0; j < nInputs; j++) { + featFlat[i * featureDim + j] = features[i][j]; + } + featFlat[i * featureDim + nInputs] = 1.0; + for (let j = 0; j < nOutputs; j++) { + labFlat[i * nOutputs + j] = labels[i][j] || 0; + } + } + + const featPtr = toHeapF32(featFlat); + const labPtr = toHeapF32(labFlat); + + // Train + const loss = w.train( + mlp, featPtr, nSamples, featureDim, + labPtr, nOutputs, + learningRate, maxIterations, convergenceThreshold + ); + + w.free(featPtr); + w.free(labPtr); + + // Extract trained weights + const outPtr = w.alloc(weightCount); + w.getWeights(mlp, outPtr); + const trainedWeights = fromHeapF32(outPtr, weightCount); + w.free(outPtr); + + self.postMessage({ + type: 'trained', + payload: { weights: trainedWeights, loss }, + }); + } +}; diff --git a/playground/js/nisps/nisps-wasm.js b/playground/js/nisps/nisps-wasm.js new file mode 100644 index 0000000..21101e6 --- /dev/null +++ b/playground/js/nisps/nisps-wasm.js @@ -0,0 +1,405 @@ +// WASM-backed IML — drop-in replacement for the JS IML class. +// Uses nisps-core compiled to WASM for inference, training, and weight ops. +// Training runs in a Web Worker for non-blocking operation. + +// Activation function IDs matching C++ nisps::ACTIVATION_FUNCTIONS enum +const ACTIVATION = { SIGMOID: 0, TANH: 1, LINEAR: 2, RELU: 3 }; + +/** + * Load the Emscripten module. Returns the initialized module. + */ +async function loadNispsModule() { + const { default: NispsModule } = await import('../../wasm/nisps.js'); + const mod = await NispsModule(); + return mod; +} + +/** + * Wrap raw Emscripten module with typed JS helpers. + */ +function wrapModule(mod) { + return { + mod, + create: mod.cwrap('nisps_mlp_create', 'number', ['number', 'number', 'number', 'number']), + destroy: mod.cwrap('nisps_mlp_destroy', null, ['number']), + weightCount: mod.cwrap('nisps_mlp_weight_count', 'number', ['number']), + getWeights: mod.cwrap('nisps_mlp_get_weights', null, ['number', 'number']), + setWeights: mod.cwrap('nisps_mlp_set_weights', null, ['number', 'number']), + inference: mod.cwrap('nisps_mlp_inference', null, ['number', 'number', 'number', 'number', 'number']), + train: mod.cwrap('nisps_mlp_train', 'number', ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number', 'number']), + drawWeightsSpread: mod.cwrap('nisps_mlp_draw_weights_spread', null, ['number', 'number']), + moveWeightsSpread: mod.cwrap('nisps_mlp_move_weights_spread', null, ['number', 'number', 'number']), + alloc: mod.cwrap('nisps_alloc', 'number', ['number']), + free: mod.cwrap('nisps_free', null, ['number']), + allocInt: mod.cwrap('nisps_alloc_int', 'number', ['number']), + freeInt: mod.cwrap('nisps_free_int', null, ['number']), + }; +} + +/** + * Write a JS array into WASM heap, returning the pointer. + * Caller must free with w.free(ptr). + */ +function toHeapF32(w, arr) { + const ptr = w.alloc(arr.length); + w.mod.HEAPF32.set(arr, ptr >> 2); + return ptr; +} + +function toHeapI32(w, arr) { + const ptr = w.allocInt(arr.length); + w.mod.HEAP32.set(arr, ptr >> 2); + return ptr; +} + +function fromHeapF32(w, ptr, length) { + const offset = ptr >> 2; + return Array.from(w.mod.HEAPF32.subarray(offset, offset + length)); +} + +/** + * WASM-backed IML class — API-compatible with the JS IML. + * + * Construction is async: use `await WasmIML.create(...)` instead of `new IML(...)`. + */ +export class WasmIML { + /** + * Async factory — loads WASM and constructs the IML. + */ + static async create( + nInputs, + nOutputs, + hiddenLayers = [10, 10, 14], + maxIterations = 1000, + learningRate = 1.0, + convergenceThreshold = 0.00001 + ) { + const mod = await loadNispsModule(); + const iml = new WasmIML(mod, nInputs, nOutputs, hiddenLayers, + maxIterations, learningRate, convergenceThreshold); + return iml; + } + + constructor(mod, nInputs, nOutputs, hiddenLayers, maxIterations, learningRate, convergenceThreshold) { + this.nInputs = nInputs; + this.nOutputs = nOutputs; + this.maxIterations = maxIterations; + this.learningRate = learningRate; + this.convergenceThreshold = convergenceThreshold; + + // Layer sizes: input+bias, hidden..., output + const BIAS = 1; + this.layerSizes = [nInputs + BIAS, ...hiddenLayers, nOutputs]; + // Activations: RELU for hidden, SIGMOID for output + this.activationIds = [ + ...hiddenLayers.map(() => ACTIVATION.RELU), + ACTIVATION.SIGMOID, + ]; + + // WASM module + helpers + this._w = wrapModule(mod); + this._createMLP(); + + // State (JS-side, same as original IML) + this.inputState = new Array(nInputs).fill(0.5); + this.outputState = new Array(nOutputs).fill(0); + this.performInference = true; + this.inputUpdated = true; + this.storedWeights = null; + this.weightsRandomised = false; + this.lastLoss = null; + this.bestLoss = null; + this.lossHistory = []; + this.totalTrainingIterations = 0; + this.logFn = null; + + // Dataset (JS-side for persistence/visualization access) + this.dataset = { features: [], labels: [], maxExamples: 100 }; + + // Persistent WASM buffers for inference (avoid alloc/free per frame) + const inputDim = nInputs + BIAS; + this._inputPtr = this._w.alloc(inputDim); + this._outputPtr = this._w.alloc(nOutputs); + this._inputDim = inputDim; + + // Worker for async training + this._worker = null; + this._training = false; + } + + _createMLP() { + const w = this._w; + const layerPtr = toHeapI32(w, new Int32Array(this.layerSizes)); + const actPtr = toHeapI32(w, new Int32Array(this.activationIds)); + this._mlp = w.create(layerPtr, this.layerSizes.length, actPtr, this.activationIds.length); + w.freeInt(layerPtr); + w.freeInt(actPtr); + this._weightCount = w.weightCount(this._mlp); + } + + // ---- Logging ---- + setLogger(fn) { this.logFn = fn; } + log(msg) { if (this.logFn) this.logFn(msg); } + + // ---- Input / Output ---- + setInput(index, value) { + if (index >= this.nInputs) return; + this.inputState[index] = Math.max(0, Math.min(1, value)); + this.inputUpdated = true; + } + + setInputs(values) { + for (let i = 0; i < values.length && i < this.nInputs; i++) { + this.inputState[i] = Math.max(0, Math.min(1, values[i])); + } + this.inputUpdated = true; + } + + getOutputs() { return this.outputState; } + + setOutput(index, value) { + if (index >= this.nOutputs) return; + this.outputState[index] = Math.max(0, Math.min(1, value)); + } + + setOutputs(values) { + for (let i = 0; i < values.length && i < this.nOutputs; i++) { + this.outputState[i] = Math.max(0, Math.min(1, values[i])); + } + } + + // ---- Inference (WASM, synchronous — fast) ---- + process() { + if (!this.performInference || !this.inputUpdated) return; + + // Write input + bias into persistent WASM buffer + const heap = this._w.mod.HEAPF32; + const inOff = this._inputPtr >> 2; + for (let i = 0; i < this.nInputs; i++) { + heap[inOff + i] = this.inputState[i]; + } + heap[inOff + this.nInputs] = 1.0; // bias + + this._w.inference(this._mlp, this._inputPtr, this._inputDim, this._outputPtr, this.nOutputs); + + // Read output + const outOff = this._outputPtr >> 2; + for (let i = 0; i < this.nOutputs; i++) { + this.outputState[i] = heap[outOff + i]; + } + + this.inputUpdated = false; + } + + // ---- Dataset ---- + addExample(inputs, outputs) { + const inVec = inputs.slice(0, this.nInputs); + while (inVec.length < this.nInputs) inVec.push(0); + const outVec = outputs.slice(0, this.nOutputs); + while (outVec.length < this.nOutputs) outVec.push(0); + + if (this.dataset.features.length >= this.dataset.maxExamples) { + this.dataset.features.shift(); + this.dataset.labels.shift(); + } + this.dataset.features.push([...inVec]); + this.dataset.labels.push([...outVec]); + } + + clearDataset() { + this.dataset.features = []; + this.dataset.labels = []; + this.log('Dataset cleared.'); + } + + get exampleCount() { return this.dataset.features.length; } + + // ---- Training (WASM, synchronous) ---- + train(options = {}) { + if (this.weightsRandomised && this.storedWeights) { + this._setFlatWeights(this.storedWeights); + this.weightsRandomised = false; + } + + const features = this.dataset.features; + const labels = this.dataset.labels; + if (features.length === 0) { + this.log('Empty dataset, skipping training.'); + return null; + } + + this.log('Training...'); + + // Build flat arrays with bias appended to features + const featureDim = this.nInputs + 1; // +bias + const nSamples = features.length; + const featFlat = new Float32Array(nSamples * featureDim); + const labFlat = new Float32Array(nSamples * this.nOutputs); + + for (let i = 0; i < nSamples; i++) { + for (let j = 0; j < this.nInputs; j++) { + featFlat[i * featureDim + j] = features[i][j]; + } + featFlat[i * featureDim + this.nInputs] = 1.0; // bias + for (let j = 0; j < this.nOutputs; j++) { + labFlat[i * this.nOutputs + j] = labels[i][j] || 0; + } + } + + const featPtr = toHeapF32(this._w, featFlat); + const labPtr = toHeapF32(this._w, labFlat); + + const loss = this._w.train( + this._mlp, featPtr, nSamples, featureDim, + labPtr, this.nOutputs, + this.learningRate, this.maxIterations, this.convergenceThreshold + ); + + this._w.free(featPtr); + this._w.free(labPtr); + + this.lastLoss = loss; + // We don't have per-iteration history from WASM (single return value), + // so record just the final loss + this.lossHistory.push(loss); + this.totalTrainingIterations += 1; + if (this.lossHistory.length > 1200) { + this.lossHistory = this.lossHistory.slice(this.lossHistory.length - 1200); + } + this.bestLoss = this.bestLoss === null ? loss : Math.min(this.bestLoss, loss); + + // Run inference after training + this.inputUpdated = true; + this.process(); + + this.log(`Training complete. Loss: ${loss.toFixed(6)}`); + return loss; + } + + // ---- Weight manipulation ---- + randomiseWeights(spread = 0) { + this.storedWeights = this._getFlatWeights(); + this._w.drawWeightsSpread(this._mlp, spread); + this.weightsRandomised = true; + + this.inputUpdated = true; + this.process(); + this.log('Weights randomised.'); + } + + moveWeights(speed, spread = 0) { + this._w.moveWeightsSpread(this._mlp, speed, spread); + this.inputUpdated = true; + this.process(); + } + + // ---- Flat weight get/set (for storedWeights save/restore) ---- + _getFlatWeights() { + const ptr = this._w.alloc(this._weightCount); + this._w.getWeights(this._mlp, ptr); + const weights = fromHeapF32(this._w, ptr, this._weightCount); + this._w.free(ptr); + return weights; + } + + _setFlatWeights(flatWeights) { + const ptr = toHeapF32(this._w, new Float32Array(flatWeights)); + this._w.setWeights(this._mlp, ptr); + this._w.free(ptr); + } + + // ---- Async training via Web Worker ---- + get isTraining() { return this._training; } + + trainAsync(onComplete) { + if (this._training) { + this.log('Training already in progress, skipping.'); + return Promise.resolve(null); + } + + // Restore weights if randomised + if (this.weightsRandomised && this.storedWeights) { + this._setFlatWeights(this.storedWeights); + this.weightsRandomised = false; + } + + if (this.dataset.features.length === 0) { + this.log('Empty dataset, skipping training.'); + return Promise.resolve(null); + } + + this._training = true; + this.log('Training (async)...'); + + // Snapshot current weights + dataset for the worker + const flatWeights = this._getFlatWeights(); + const features = this.dataset.features; + const labels = this.dataset.labels; + + // Lazy-init worker + if (!this._worker) { + const workerUrl = new URL('./nisps-wasm-worker.js', import.meta.url); + this._worker = new Worker(workerUrl, { type: 'module' }); + } + + return new Promise((resolve) => { + const handler = (e) => { + if (e.data.type === 'trained') { + this._worker.removeEventListener('message', handler); + this._training = false; + + const { weights, loss } = e.data.payload; + + // Swap in trained weights + this._setFlatWeights(weights); + this.lastLoss = loss; + this.lossHistory.push(loss); + this.totalTrainingIterations += 1; + if (this.lossHistory.length > 1200) { + this.lossHistory = this.lossHistory.slice(this.lossHistory.length - 1200); + } + this.bestLoss = this.bestLoss === null ? loss : Math.min(this.bestLoss, loss); + + // Run inference with new weights + this.inputUpdated = true; + this.process(); + + this.log(`Training complete. Loss: ${loss.toFixed(6)}`); + if (onComplete) onComplete({ loss, outputs: [...this.outputState] }); + resolve(loss); + } + }; + + this._worker.addEventListener('message', handler); + this._worker.postMessage({ + type: 'train', + payload: { + layerSizes: this.layerSizes, + activationIds: this.activationIds, + weights: flatWeights, + features, + labels, + nInputs: this.nInputs, + nOutputs: this.nOutputs, + learningRate: this.learningRate, + maxIterations: this.maxIterations, + convergenceThreshold: this.convergenceThreshold, + }, + }); + }); + } + + // ---- Cleanup ---- + destroy() { + if (this._mlp) { + this._w.free(this._inputPtr); + this._w.free(this._outputPtr); + this._w.destroy(this._mlp); + this._mlp = null; + } + if (this._worker) { + this._worker.terminate(); + this._worker = null; + } + } +} diff --git a/playground/wasm/build.sh b/playground/wasm/build.sh new file mode 100755 index 0000000..b0724ec --- /dev/null +++ b/playground/wasm/build.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Build nisps-core WASM module for the playground +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +NISPS_CORE="$SCRIPT_DIR/../../nisps-core/include" +OUT_DIR="$SCRIPT_DIR" + +echo "Building nisps WASM module..." + +emcc "$SCRIPT_DIR/nisps_bindings.cpp" \ + -I"$NISPS_CORE" \ + -std=c++20 \ + -O2 \ + -s EXPORTED_FUNCTIONS='[ + "_nisps_mlp_create", + "_nisps_mlp_destroy", + "_nisps_mlp_weight_count", + "_nisps_mlp_get_weights", + "_nisps_mlp_set_weights", + "_nisps_mlp_inference", + "_nisps_mlp_train", + "_nisps_mlp_draw_weights_spread", + "_nisps_mlp_move_weights_spread", + "_nisps_alloc", + "_nisps_free", + "_nisps_alloc_int", + "_nisps_free_int", + "_malloc", + "_free" + ]' \ + -s EXPORTED_RUNTIME_METHODS='["cwrap","HEAPF32","HEAP32"]' \ + -s MODULARIZE=1 \ + -s EXPORT_NAME=NispsModule \ + -s EXPORT_ES6=1 \ + -s ENVIRONMENT='web,worker' \ + -s ALLOW_MEMORY_GROWTH=1 \ + -s INITIAL_MEMORY=16777216 \ + -o "$OUT_DIR/nisps.js" + +echo "Built: $OUT_DIR/nisps.js + $OUT_DIR/nisps.wasm" +echo "Size: $(du -h "$OUT_DIR/nisps.wasm" | cut -f1)" diff --git a/playground/wasm/nisps.js b/playground/wasm/nisps.js new file mode 100644 index 0000000..2795a46 --- /dev/null +++ b/playground/wasm/nisps.js @@ -0,0 +1,2 @@ +async function NispsModule(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("nisps.wasm")}return new URL("nisps.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ___assert_fail=(condition,filename,line,func)=>abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"]);class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var exceptionLast=0;var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var initRandomFill=()=>view=>crypto.getRandomValues(view);var randomFill=view=>{(randomFill=initRandomFill())(view)};var _random_get=(buffer,size)=>{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["cwrap"]=cwrap;var _nisps_mlp_create,_nisps_mlp_destroy,_nisps_mlp_weight_count,_nisps_mlp_get_weights,_nisps_mlp_set_weights,_nisps_mlp_inference,_nisps_mlp_train,_nisps_mlp_draw_weights_spread,_nisps_mlp_move_weights_spread,_nisps_alloc,_malloc,_nisps_free,_free,_nisps_alloc_int,_nisps_free_int,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_nisps_mlp_create=Module["_nisps_mlp_create"]=wasmExports["nisps_mlp_create"];_nisps_mlp_destroy=Module["_nisps_mlp_destroy"]=wasmExports["nisps_mlp_destroy"];_nisps_mlp_weight_count=Module["_nisps_mlp_weight_count"]=wasmExports["nisps_mlp_weight_count"];_nisps_mlp_get_weights=Module["_nisps_mlp_get_weights"]=wasmExports["nisps_mlp_get_weights"];_nisps_mlp_set_weights=Module["_nisps_mlp_set_weights"]=wasmExports["nisps_mlp_set_weights"];_nisps_mlp_inference=Module["_nisps_mlp_inference"]=wasmExports["nisps_mlp_inference"];_nisps_mlp_train=Module["_nisps_mlp_train"]=wasmExports["nisps_mlp_train"];_nisps_mlp_draw_weights_spread=Module["_nisps_mlp_draw_weights_spread"]=wasmExports["nisps_mlp_draw_weights_spread"];_nisps_mlp_move_weights_spread=Module["_nisps_mlp_move_weights_spread"]=wasmExports["nisps_mlp_move_weights_spread"];_nisps_alloc=Module["_nisps_alloc"]=wasmExports["nisps_alloc"];_malloc=Module["_malloc"]=wasmExports["malloc"];_nisps_free=Module["_nisps_free"]=wasmExports["nisps_free"];_free=Module["_free"]=wasmExports["free"];_nisps_alloc_int=Module["_nisps_alloc_int"]=wasmExports["nisps_alloc_int"];_nisps_free_int=Module["_nisps_free_int"]=wasmExports["nisps_free_int"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={__assert_fail:___assert_fail,__cxa_throw:___cxa_throw,_abort_js:__abort_js,emscripten_resize_heap:_emscripten_resize_heap,random_get:_random_get};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +;return moduleRtn}export default NispsModule; diff --git a/playground/wasm/nisps.wasm b/playground/wasm/nisps.wasm new file mode 100755 index 0000000000000000000000000000000000000000..e7bd20cd3beba8678ddcaeb10819b75a64dd4eef GIT binary patch literal 33677 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 +#include "nisps/mlp.hpp" +#include +#include + +extern "C" { + +// ---- Lifecycle ---- + +EMSCRIPTEN_KEEPALIVE +void* nisps_mlp_create(int* layer_sizes, int n_layers, int* activation_ids, int n_activations) { + std::vector layers(layer_sizes, layer_sizes + n_layers); + std::vector activations; + for (int i = 0; i < n_activations; i++) { + activations.push_back(static_cast(activation_ids[i])); + } + auto* mlp = new nisps::MLP(layers, activations, nisps::loss::LOSS_MSE, false, 0.0f); + return mlp; +} + +EMSCRIPTEN_KEEPALIVE +void nisps_mlp_destroy(void* ptr) { + delete static_cast*>(ptr); +} + +// ---- Weight serialization ---- +// Flat format: for each layer, for each node: [w0, w1, ..., wN, bias] + +EMSCRIPTEN_KEEPALIVE +int nisps_mlp_weight_count(void* ptr) { + auto* mlp = static_cast*>(ptr); + int count = 0; + for (auto& layer : mlp->m_layers) { + for (auto& node : layer.m_nodes) { + count += node.m_weights.size() + 1; // weights + bias + } + } + return count; +} + +EMSCRIPTEN_KEEPALIVE +void nisps_mlp_get_weights(void* ptr, float* out) { + auto* mlp = static_cast*>(ptr); + int idx = 0; + for (auto& layer : mlp->m_layers) { + for (auto& node : layer.m_nodes) { + for (size_t j = 0; j < node.m_weights.size(); j++) { + out[idx++] = node.m_weights[j]; + } + out[idx++] = node.m_bias; + } + } +} + +EMSCRIPTEN_KEEPALIVE +void nisps_mlp_set_weights(void* ptr, float* weights) { + auto* mlp = static_cast*>(ptr); + int idx = 0; + for (auto& layer : mlp->m_layers) { + for (auto& node : layer.m_nodes) { + for (size_t j = 0; j < node.m_weights.size(); j++) { + node.m_weights[j] = weights[idx++]; + } + node.m_bias = weights[idx++]; + } + } +} + +// ---- Inference ---- + +EMSCRIPTEN_KEEPALIVE +void nisps_mlp_inference(void* ptr, float* input, int input_dim, float* output, int output_dim) { + auto* mlp = static_cast*>(ptr); + std::vector in_vec(input, input + input_dim); + std::vector out_vec; + mlp->GetOutput(in_vec, &out_vec, nullptr, true); + int n = output_dim < (int)out_vec.size() ? output_dim : (int)out_vec.size(); + for (int i = 0; i < n; i++) { + output[i] = out_vec[i]; + } +} + +// ---- Training ---- +// Takes flat arrays, builds training pairs, returns final loss + +EMSCRIPTEN_KEEPALIVE +float nisps_mlp_train(void* ptr, + float* features_flat, int n_samples, int feature_dim, + float* labels_flat, int label_dim, + float learning_rate, int max_iterations, float min_error) { + + auto* mlp = static_cast*>(ptr); + + std::vector> features(n_samples); + std::vector> labels(n_samples); + + for (int i = 0; i < n_samples; i++) { + features[i].assign(features_flat + i * feature_dim, + features_flat + (i + 1) * feature_dim); + labels[i].assign(labels_flat + i * label_dim, + labels_flat + (i + 1) * label_dim); + } + + nisps::MLP::training_pair_t data(features, labels); + return mlp->Train(data, learning_rate, max_iterations, min_error, false); +} + +// ---- Weight manipulation with spread ---- +// These mirror the JS playground's spread-aware versions which the C++ core +// doesn't have natively. + +// DrawWeights: randomize with per-layer Xavier scaling controlled by spread +// spread=0: uniform [-1,1], spread=1: Xavier-scaled per layer +EMSCRIPTEN_KEEPALIVE +void nisps_mlp_draw_weights_spread(void* ptr, float spread) { + auto* mlp = static_cast*>(ptr); + for (size_t l = 0; l < mlp->m_layers.size(); l++) { + int fan_in = mlp->m_layers[l].GetInputSize(); + float xavier_scale = 1.0f / std::sqrt((float)fan_in); + float scale = 1.0f * (1.0f - spread) + xavier_scale * spread; + for (auto& node : mlp->m_layers[l].GetNodesChangeable()) { + for (size_t j = 0; j < node.m_weights.size(); j++) { + node.m_weights[j] = ((float)rand() / RAND_MAX * 2.0f - 1.0f) * scale; + } + node.m_bias = 0.0f; + } + } +} + +// MoveWeights: add per-layer noise with weight decay, controlled by spread +// spread=0: flat noise, no decay. spread=1: Xavier-scaled noise, 10% decay. +EMSCRIPTEN_KEEPALIVE +void nisps_mlp_move_weights_spread(void* ptr, float speed, float spread) { + auto* mlp = static_cast*>(ptr); + float decay = 1.0f - 0.1f * spread; + for (size_t l = 0; l < mlp->m_layers.size(); l++) { + int fan_in = mlp->m_layers[l].GetInputSize(); + float xavier_scale = 1.0f / std::sqrt((float)fan_in); + float layer_scale = 1.0f * (1.0f - spread) + xavier_scale * spread; + for (auto& node : mlp->m_layers[l].GetNodesChangeable()) { + for (size_t j = 0; j < node.m_weights.size(); j++) { + node.m_weights[j] *= decay; + // gen_randn: sum of 3 uniform randoms + float accum = 0; + for (int n = 0; n < 3; n++) { + accum += (float)rand() / RAND_MAX * 2.0f - 1.0f; + } + node.m_weights[j] += 3.0f * accum * speed * layer_scale; + } + } + } +} + +// ---- Memory helpers ---- + +EMSCRIPTEN_KEEPALIVE +float* nisps_alloc(int n) { + return (float*)malloc(n * sizeof(float)); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_free(float* ptr) { + free(ptr); +} + +EMSCRIPTEN_KEEPALIVE +int* nisps_alloc_int(int n) { + return (int*)malloc(n * sizeof(int)); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_free_int(int* ptr) { + free(ptr); +} + +} // extern "C"