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 0000000..e7bd20c Binary files /dev/null and b/playground/wasm/nisps.wasm differ diff --git a/playground/wasm/nisps_bindings.cpp b/playground/wasm/nisps_bindings.cpp new file mode 100644 index 0000000..2d011c8 --- /dev/null +++ b/playground/wasm/nisps_bindings.cpp @@ -0,0 +1,179 @@ +// WASM bindings for nisps-core MLP engine +// Provides a flat C API for use from JavaScript via Emscripten + +#include +#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"