From 0010097d0151d824eeb1d0fe406a6a748b298122 Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Sat, 25 Jul 2026 15:16:37 +0200 Subject: [PATCH] feat(manifold): edit I/O as identity-aware cards --- MAP.md | 13 +- docs/AGENT-REFERENCE.md | 2 +- manifold/ONBOARDING.md | 34 +-- manifold/public/nisps.js | 2 +- manifold/public/nisps.wasm | Bin 129136 -> 130494 bytes manifold/src/backends/backend.ts | 2 +- manifold/src/backends/manager.ts | 17 +- manifold/src/backends/presets.ts | 4 +- manifold/src/console/ConsoleApp.tsx | 220 ++++++++++++++---- manifold/src/console/Drawers.tsx | 56 ++++- manifold/src/console/OutputStage.tsx | 2 +- manifold/src/console/ReshapeModal.tsx | 86 ------- manifold/src/console/model.ts | 42 +++- manifold/src/console/output-mode.ts | 9 +- manifold/src/console/types.ts | 4 +- manifold/src/dock/OutputControlRow.tsx | 32 ++- manifold/src/dock/OutputsBackendConfig.tsx | 22 +- manifold/src/engine/engine-api.ts | 13 +- manifold/src/engine/index.ts | 12 + manifold/src/engine/io-reshape.ts | 154 ++++++++++++ manifold/src/engine/types.ts | 2 + manifold/src/engine/wasm-iml.ts | 210 ++++++++++------- manifold/src/feedback/controller.ts | 10 + manifold/src/inputs/input-layer.ts | 7 +- manifold/src/settings/settings-store.ts | 19 ++ manifold/tests/backend-manager-switch.test.ts | 31 ++- .../tests/e2e/output-display-count.spec.ts | 50 +++- manifold/tests/io-reshape.test.ts | 49 ++++ nisps/wasm/bindings.cpp | 26 +++ scripts/build-wasm.sh | 2 +- 30 files changed, 848 insertions(+), 284 deletions(-) delete mode 100644 manifold/src/console/ReshapeModal.tsx create mode 100644 manifold/src/engine/io-reshape.ts create mode 100644 manifold/tests/io-reshape.test.ts diff --git a/MAP.md b/MAP.md index ef37aa6..06abc5d 100644 --- a/MAP.md +++ b/MAP.md @@ -58,7 +58,7 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set. consts (mode_id, engine_id, ml dims, params, voice_spaces, ui) — the SOURCE OF TRUTH for `MF_MODES`. Switching mode reshapes the WASM net to the mode's `ml` dims (ConsoleApp P5.3; boot mode paf_synth → 4→[10,10,14]→33). -- `manifold/src/dock/` — `OutputControlRow` (off/fixed/live + mute + solo/arm + min/max/curve), `output-state.ts`, +- `manifold/src/dock/` — `OutputControlRow` (add/delete card identity + off/fixed/live + mute + solo/arm + min/max/curve), `output-state.ts`, `OutputsBackendConfig.tsx` (per-backend specialised Outputs panel — the sole per-backend editor). - `manifold/src/backends/` — `OutputBackend` adapter + `BackendManager` (spine consumer); `midi-backend.ts` (WebMIDI), `osc-backend.ts`+`osc-client.ts` (OSC-over-WS), `vcv-backend.ts` (VCV-over-WS), `cv-backend.ts` @@ -74,9 +74,10 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set. `setInputs`, plus an `onReducedInput` callback the manifold tracks. The WASM net is over-provisioned to a 32-input head (`MAX_AXES`, `nisps/wasm/bindings.cpp`); unused slots are zero-padded and a zero input is inert, so idle sources cannot perturb the net. Mean-blending was removed deliberately — it diluted every source and - biased the net toward idle sources' resting values. Changing the ACTIVE axis count offers a reshape - (`ConsoleApp` → `ReshapeModal`): new net at the new arity, warm-started from overlapping weights, examples and - feedback state reset; declining keeps the over-provisioned head. Sources: `xy-pad-source` (push-driven), + biased the net toward idle sources' resting values. Active-axis edits follow the persistent I/O policy: + keep-capacity permutes stable identities in place until more slots are required; exact-I/O reconstructs + to the active count. Surviving weights and (under adapt policy) examples are identity-remapped; feedback + scratch state resets. Sources: `xy-pad-source` (push-driven), `gamepad-source` (sticks→axes single/double; buttons emit press+release actions, bound in `ConsoleApp` to verdicts — LB/RB=down/up, X/Y/B=randomise/nudge/undo, A-hold=reposition), `midi-input-source` (device picker + BATCH "MIDI Learn": every CC swept while armed becomes an axis, shown as read-only meters). `useInputLayer.ts` @@ -84,11 +85,13 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set. `backends/base-backend.ts` is its output-side counterpart (status + throttle + lastSent) used by the midi/osc/vcv transports. - `manifold/src/feedback/` — `controller.ts` (Explore-and-place scratchpad + geometric-dislike + solo; a thin driver over the shared C++ core). -- `manifold/src/settings/` — `settings-store.ts` (monochrome icons, input-map shape, corner radius, and the opt-in legacy Xavier/spread feature flag; Manifold randomisation is full-range uniform by default). +- `manifold/src/settings/` — `settings-store.ts` (monochrome icons, input-map shape, I/O resize policy, corner radius, and the opt-in legacy Xavier/spread feature flag; Manifold randomisation is full-range uniform by default). - `manifold/src/serial/` — `memlnaut-serial.ts` Web Serial scaffold + `EditorPanel.tsx` (MEMLNaut Editor mode). - `manifold/src/engine/exploration.ts` — Jolt press + OU explore gestures (Learning drawer): a thin timer-driver over the shared C++ core via the `nisps_ml_jolt_*`/`nisps_ml_ou_*` bindings (the interim TS math and `jolt.ts`/`ou-explore.ts` were deleted when P3 landed). +- `manifold/src/engine/io-reshape.ts` — the deep identity-migration module for I/O card edits: + exact-vs-capacity reconstruction decisions, flat weight remapping, and example vector adaptation. - `manifold/src/debug/probe.ts` — `window.__nisps` (`?debug=1`). `manifold/tests/e2e/` — `smoke`, `probe-api` (engine-contract port), `spine` (spine invariant + probe-survives-mode-switch), `geo-dislike`, `reshape`, `schema-modes`, `training-health` (the loss/layer-stats panel + its diff --git a/docs/AGENT-REFERENCE.md b/docs/AGENT-REFERENCE.md index 48a62a4..028ce32 100644 --- a/docs/AGENT-REFERENCE.md +++ b/docs/AGENT-REFERENCE.md @@ -115,7 +115,7 @@ Two WASM instances at runtime: C API is in `nisps/wasm/bindings.cpp`. Build: `bash scripts/build-wasm.sh` (~94KB output to `manifold/public/`). -The browser MLP is runtime-shaped since P2 (`MLPCore`): `nisps_ml_create` honours `(input, output, hidden[3])`; non-positive/null args default to `32→[10,14,18]→126`. `nisps_ml_reshape` swaps in a new shape warm-started from the overlapping weights (examples + feedback state reset). Per-mode dims have been schema-real since P5.3 on both targets — modes no longer slice a shared 126-wide default. +The browser MLP is runtime-shaped since P2 (`MLPCore`): `nisps_ml_create` honours `(input, output, hidden[3])`; non-positive/null args default to `32→[10,14,18]→126`. Raw `nisps_ml_reshape` reconstructs and prefix-warm-starts a new shape. Manifold's higher-level `engine/io-reshape.ts` seam adds stable input/output identity: it can permute weights and examples without reconstruction while capacity suffices, or reconstruct with arbitrary surviving-dimension remaps. Persistent settings select capacity-vs-exact arity and adapt-vs-clear examples (neutral new-input/output defaults 0/0.5); feedback/exploration scratch state resets on either identity edit. Per-mode dims have been schema-real since P5.3 on both targets — modes no longer slice a shared 126-wide default. ### Known limitations diff --git a/manifold/ONBOARDING.md b/manifold/ONBOARDING.md index 347362b..2570456 100644 --- a/manifold/ONBOARDING.md +++ b/manifold/ONBOARDING.md @@ -121,10 +121,11 @@ for the narrow pane. - **Output modes** (the TOP dock selector, NOT the same axis as `focus`): `src/console/output-mode.ts` defines `OUTPUT_MODES` = **particles** (default) / midi / osc / cv / synth / editor, each mapping to a - `BackendId`. `DEFAULT_OUTPUT_MODE='particles'`. `outputDisplayCount()` is the shared presentation - boundary for the stage and routing rows: MIDI uses its configured CC count, while backends without - a separate count present the full mode parameter set. The condensed Outputs panel reports this as an - `N outputs` chip. This does not reshape the MLP or clear examples. + `BackendId`. `DEFAULT_OUTPUT_MODE='particles'`. `outputDisplayCount()` is the shared active-card + boundary for the stage, backend context, and routing rows. MIDI starts with eight cards; every + backend can add a card or delete any individual card in condensed and expanded Outputs drawers. + The `N outputs` chip always reports that same set. `MFParam.id` is semantic identity; array position + is not. Settings decides whether edits retain spare network capacity or keep exact arity. - `src/console/output-mode.ts`, `types.ts`, `model.ts` are the shared vocabulary — read these first when touching anything cross-cutting: - `types.ts`: `Focus`, `OutputMode`, `DrawerKey`, `DrawerDepth`, `FeedbackModeUI`, `SoloMode`, @@ -236,25 +237,25 @@ a setting → `--r-*` tokens. - `input-layer.ts` — composition hub. One rAF loop polls sources, pulls all axes into a vector, forwards N→engine. **`MAX_AXES = 32`** (WASM net over-provisioned to 32 inputs). **Dedicated dimensions, NO mean-blending** — each active axis drives its own engine slot 1:1; unused slots - zero-padded (inert). Changing axis count requires a **net reset** (UI confirm modal). + zero-padded (inert). Changing the layout uses the same persisted identity-aware I/O policy as output + cards. - `base-source.ts` + sources: `xy-pad-source.ts` (push, 2 axes), `gamepad-source.ts` (single=2 / double=4 axes, deadzone 0.08), `midi-input-source.ts` (Web MIDI, batch CC-learn, multi-port). - `useInputLayer.ts` — React binding; manages exclusive input mode + gamepad stick mode + MIDI device/learn map; exposes `pushPad`, `sources`, `channelLayout`, etc. -- **Reshape (P2.3, live):** the net is now **runtime-shaped**. It boots at the default +- **I/O migration (P2.3, live):** the net is **runtime-shaped**. It boots at the default over-provisioned 32-input head (zero-padding preserved), and `EngineApi.reshape({ inputSize, … })` - → `WasmIML.reshape` swaps in a new net at the requested arity, **warm-started** from the overlapping - weights (`nisps_ml_reshape`; C-side dataset + feedback state RESET). When the active axis layout - CHANGES to a count ≠ the net's arity, `ConsoleApp` offers the swap behind `ReshapeModal.tsx` - (reset-on-reshape confirm; declining keeps the zero-padded head). Never offered on load. The - spine tolerates the arity change (buffers resize, version bumps); the training worker + → `WasmIML.reshape`. `engine/io-reshape.ts` owns the identity map: **Keep capacity** (default) + permutes weights/examples in place and reconstructs only when active I/O outgrows the net; + **Exact I/O** reconstructs whenever active arity changes. Existing examples either adapt by + deleting removed dimensions and inserting the saved neutral placeholders (input 0, output 0.5 by + default), or clear, according to the persistent Settings choice. Feedback replay/exploration state + resets because it has no stable-ID contract. The spine tolerates arity changes; the training worker (`wasm-worker.ts`) carries the current dims in its train message and re-creates its mirror net to - match. Debug: `window.__nisps.reshape(nIn)` / `.describe()`. See the `manifold-mixed-inputs` memory - for the locked design (adaptive slider viz when >2 dims is still pending). + match. The raw debug `window.__nisps.reshape(nIn)` remains a low-level reconstruct-and-clear call. - **Per-mode net dims (P5.3):** switching INSTRUMENT mode reshapes the net to that mode's schema `ml` config (`MFMode.ml` — input/hidden/output + legacy spread) via a `ConsoleApp` effect keyed on - `[engine, modeId]`. No confirm modal (switching instrument is deliberate); the axis-count - `ReshapeModal` above is for input-LAYOUT changes only. The effect depends on `engine`, so on boot + `[engine, modeId]`. No confirm modal (switching instrument is deliberate). The effect depends on `engine`, so on boot it fires once WASM is ready and lands the boot mode's dims (**paf_synth → 4→[10,10,14]→33**, weights 809 — NOT the 32→126 default). The reshape-offer effect reads the engine's CURRENT `inputSize` live, so a mode switch that changes arity doesn't spuriously prompt (its baseline tracks axis @@ -297,7 +298,8 @@ a setting → `--r-*` tokens. ### Misc - `src/serial/memlnaut-serial.ts` — **STUB** Web Serial scaffold for the MEMLNaut Editor mode (protocol TODO). `EditorPanel.tsx` is its UI. - `src/settings/settings-store.ts` — localStorage settings (`mf-settings`): icon style, input-map - shape, corner radius, and the opt-in legacy Xavier/spread feature flag. + shape, exact-vs-capacity network resizing, adapt-vs-clear examples + neutral new-dimension values, + corner radius, and the opt-in legacy Xavier/spread feature flag. - `src/midi-devices/` — codegen'd external-synth device templates. - `src/debug/probe.ts` — `window.__nisps` synchronous probe (engine/audio/bus). Some playground feature-store methods are present-but-inert (not ported yet) to keep the surface stable. diff --git a/manifold/public/nisps.js b/manifold/public/nisps.js index 04d4f5d..be081a4 100644 --- a/manifold/public/nisps.js +++ b/manifold/public/nisps.js @@ -6,7 +6,7 @@ var createNispsModule = (() => { function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";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){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{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=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}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 dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);function findWasmBinary(){var f="nisps.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;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"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["d"];updateMemoryViews();addOnInit(wasmExports["e"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+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 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=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=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}}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 UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;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)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";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)};var wasmImports={c:__abort_js,b:__emscripten_memcpy_js,a:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["e"])();var _nisps_ml_create=Module["_nisps_ml_create"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["f"])(a0,a1,a2,a3,a4);var _nisps_ml_reshape=Module["_nisps_ml_reshape"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_ml_reshape=Module["_nisps_ml_reshape"]=wasmExports["g"])(a0,a1,a2,a3,a4,a5);var _nisps_ml_destroy=Module["_nisps_ml_destroy"]=a0=>(_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["h"])(a0);var _nisps_ml_set_input=Module["_nisps_ml_set_input"]=(a0,a1,a2)=>(_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["i"])(a0,a1,a2);var _nisps_ml_process=Module["_nisps_ml_process"]=a0=>(_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["j"])(a0);var _nisps_ml_outputs=Module["_nisps_ml_outputs"]=a0=>(_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["k"])(a0);var _nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=(a0,a1,a2,a3)=>(_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["l"])(a0,a1,a2,a3);var _nisps_ml_add_example=Module["_nisps_ml_add_example"]=(a0,a1,a2)=>(_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["m"])(a0,a1,a2);var _nisps_ml_train=Module["_nisps_ml_train"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["n"])(a0,a1,a2,a3,a4);var _nisps_ml_set_train_config=Module["_nisps_ml_set_train_config"]=(a0,a1,a2,a3)=>(_nisps_ml_set_train_config=Module["_nisps_ml_set_train_config"]=wasmExports["o"])(a0,a1,a2,a3);var _nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["p"])(a0);var _nisps_ml_loss_history=Module["_nisps_ml_loss_history"]=(a0,a1,a2)=>(_nisps_ml_loss_history=Module["_nisps_ml_loss_history"]=wasmExports["q"])(a0,a1,a2);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["r"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["s"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["t"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["u"])(a0,a1);var _nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=(a0,a1)=>(_nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=wasmExports["v"])(a0,a1);var _nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=a0=>(_nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=wasmExports["w"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["x"])(a0);var _nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=(a0,a1,a2)=>(_nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=wasmExports["y"])(a0,a1,a2);var _nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=wasmExports["z"])(a0,a1,a2,a3,a4);var _nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=a0=>(_nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=wasmExports["A"])(a0);var _nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=(a0,a1)=>(_nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=wasmExports["B"])(a0,a1);var _nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=(a0,a1)=>(_nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=wasmExports["C"])(a0,a1);var _nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=a0=>(_nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=wasmExports["D"])(a0);var _nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=(a0,a1)=>(_nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=wasmExports["E"])(a0,a1);var _nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=(a0,a1)=>(_nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=wasmExports["F"])(a0,a1);var _nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=a0=>(_nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=wasmExports["G"])(a0);var _nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=a0=>(_nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=wasmExports["H"])(a0);var _nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=a0=>(_nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=wasmExports["I"])(a0);var _nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=a0=>(_nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=wasmExports["J"])(a0);var _nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=a0=>(_nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=wasmExports["K"])(a0);var _nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=(a0,a1)=>(_nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=wasmExports["L"])(a0,a1);var _nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=(a0,a1,a2)=>(_nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=wasmExports["M"])(a0,a1,a2);var _nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=(a0,a1)=>(_nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=wasmExports["N"])(a0,a1);var _nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=a0=>(_nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=wasmExports["O"])(a0);var _nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=a0=>(_nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=wasmExports["P"])(a0);var _nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=(a0,a1)=>(_nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=wasmExports["Q"])(a0,a1);var _nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=a0=>(_nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=wasmExports["R"])(a0);var _nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=a0=>(_nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=wasmExports["S"])(a0);var _nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=a0=>(_nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=wasmExports["T"])(a0);var _nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=a0=>(_nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=wasmExports["U"])(a0);var _nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=(a0,a1)=>(_nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=wasmExports["V"])(a0,a1);var _nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=a0=>(_nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=wasmExports["W"])(a0);var _nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=(a0,a1,a2)=>(_nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=wasmExports["X"])(a0,a1,a2);var _nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=(a0,a1)=>(_nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=wasmExports["Y"])(a0,a1);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["Z"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=(a0,a1)=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["_"])(a0,a1);var _nisps_pipeline_create=Module["_nisps_pipeline_create"]=()=>(_nisps_pipeline_create=Module["_nisps_pipeline_create"]=wasmExports["$"])();var _nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=a0=>(_nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=wasmExports["aa"])(a0);var _nisps_input_set_config=Module["_nisps_input_set_config"]=(a0,a1,a2)=>(_nisps_input_set_config=Module["_nisps_input_set_config"]=wasmExports["ba"])(a0,a1,a2);var _nisps_input_process=Module["_nisps_input_process"]=(a0,a1,a2,a3,a4)=>(_nisps_input_process=Module["_nisps_input_process"]=wasmExports["ca"])(a0,a1,a2,a3,a4);var _nisps_input_reset=Module["_nisps_input_reset"]=a0=>(_nisps_input_reset=Module["_nisps_input_reset"]=wasmExports["da"])(a0);var _nisps_output_set_config=Module["_nisps_output_set_config"]=(a0,a1,a2,a3,a4)=>(_nisps_output_set_config=Module["_nisps_output_set_config"]=wasmExports["ea"])(a0,a1,a2,a3,a4);var _nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=(a0,a1,a2)=>(_nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=wasmExports["fa"])(a0,a1,a2);var _nisps_output_process=Module["_nisps_output_process"]=(a0,a1,a2,a3)=>(_nisps_output_process=Module["_nisps_output_process"]=wasmExports["ga"])(a0,a1,a2,a3);var _nisps_output_reset=Module["_nisps_output_reset"]=a0=>(_nisps_output_reset=Module["_nisps_output_reset"]=wasmExports["ha"])(a0);var _nisps_curve_apply=Module["_nisps_curve_apply"]=(a0,a1,a2)=>(_nisps_curve_apply=Module["_nisps_curve_apply"]=wasmExports["ia"])(a0,a1,a2);var _nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=(a0,a1,a2,a3,a4)=>(_nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=wasmExports["ja"])(a0,a1,a2,a3,a4);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["ka"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["la"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["ma"])(a0,a1,a2);var _nisps_engine_process_block=Module["_nisps_engine_process_block"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_engine_process_block=Module["_nisps_engine_process_block"]=wasmExports["na"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["pa"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["qa"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["ra"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["sa"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["ta"])();Module["ccall"]=ccall;Module["cwrap"]=cwrap;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";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){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{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=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}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 dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);function findWasmBinary(){var f="nisps.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;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"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["d"];updateMemoryViews();addOnInit(wasmExports["e"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+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 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=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=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}}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 UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;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)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";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)};var wasmImports={c:__abort_js,b:__emscripten_memcpy_js,a:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["e"])();var _nisps_ml_create=Module["_nisps_ml_create"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["f"])(a0,a1,a2,a3,a4);var _nisps_ml_reshape=Module["_nisps_ml_reshape"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_ml_reshape=Module["_nisps_ml_reshape"]=wasmExports["g"])(a0,a1,a2,a3,a4,a5);var _nisps_ml_destroy=Module["_nisps_ml_destroy"]=a0=>(_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["h"])(a0);var _nisps_ml_set_input=Module["_nisps_ml_set_input"]=(a0,a1,a2)=>(_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["i"])(a0,a1,a2);var _nisps_ml_process=Module["_nisps_ml_process"]=a0=>(_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["j"])(a0);var _nisps_ml_outputs=Module["_nisps_ml_outputs"]=a0=>(_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["k"])(a0);var _nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=(a0,a1,a2,a3)=>(_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["l"])(a0,a1,a2,a3);var _nisps_ml_add_example=Module["_nisps_ml_add_example"]=(a0,a1,a2)=>(_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["m"])(a0,a1,a2);var _nisps_ml_train=Module["_nisps_ml_train"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["n"])(a0,a1,a2,a3,a4);var _nisps_ml_set_train_config=Module["_nisps_ml_set_train_config"]=(a0,a1,a2,a3)=>(_nisps_ml_set_train_config=Module["_nisps_ml_set_train_config"]=wasmExports["o"])(a0,a1,a2,a3);var _nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["p"])(a0);var _nisps_ml_loss_history=Module["_nisps_ml_loss_history"]=(a0,a1,a2)=>(_nisps_ml_loss_history=Module["_nisps_ml_loss_history"]=wasmExports["q"])(a0,a1,a2);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["r"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["s"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["t"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["u"])(a0,a1);var _nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=(a0,a1)=>(_nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=wasmExports["v"])(a0,a1);var _nisps_ml_feedback_reset=Module["_nisps_ml_feedback_reset"]=a0=>(_nisps_ml_feedback_reset=Module["_nisps_ml_feedback_reset"]=wasmExports["w"])(a0);var _nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=a0=>(_nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=wasmExports["x"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["y"])(a0);var _nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=(a0,a1,a2)=>(_nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=wasmExports["z"])(a0,a1,a2);var _nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=wasmExports["A"])(a0,a1,a2,a3,a4);var _nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=a0=>(_nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=wasmExports["B"])(a0);var _nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=(a0,a1)=>(_nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=wasmExports["C"])(a0,a1);var _nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=(a0,a1)=>(_nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=wasmExports["D"])(a0,a1);var _nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=a0=>(_nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=wasmExports["E"])(a0);var _nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=(a0,a1)=>(_nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=wasmExports["F"])(a0,a1);var _nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=(a0,a1)=>(_nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=wasmExports["G"])(a0,a1);var _nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=a0=>(_nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=wasmExports["H"])(a0);var _nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=a0=>(_nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=wasmExports["I"])(a0);var _nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=a0=>(_nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=wasmExports["J"])(a0);var _nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=a0=>(_nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=wasmExports["K"])(a0);var _nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=a0=>(_nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=wasmExports["L"])(a0);var _nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=(a0,a1)=>(_nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=wasmExports["M"])(a0,a1);var _nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=(a0,a1,a2)=>(_nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=wasmExports["N"])(a0,a1,a2);var _nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=(a0,a1)=>(_nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=wasmExports["O"])(a0,a1);var _nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=a0=>(_nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=wasmExports["P"])(a0);var _nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=a0=>(_nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=wasmExports["Q"])(a0);var _nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=(a0,a1)=>(_nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=wasmExports["R"])(a0,a1);var _nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=a0=>(_nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=wasmExports["S"])(a0);var _nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=a0=>(_nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=wasmExports["T"])(a0);var _nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=a0=>(_nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=wasmExports["U"])(a0);var _nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=a0=>(_nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=wasmExports["V"])(a0);var _nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=(a0,a1)=>(_nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=wasmExports["W"])(a0,a1);var _nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=a0=>(_nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=wasmExports["X"])(a0);var _nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=(a0,a1,a2)=>(_nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=wasmExports["Y"])(a0,a1,a2);var _nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=(a0,a1)=>(_nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=wasmExports["Z"])(a0,a1);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["_"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=(a0,a1)=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["$"])(a0,a1);var _nisps_pipeline_create=Module["_nisps_pipeline_create"]=()=>(_nisps_pipeline_create=Module["_nisps_pipeline_create"]=wasmExports["aa"])();var _nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=a0=>(_nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=wasmExports["ba"])(a0);var _nisps_input_set_config=Module["_nisps_input_set_config"]=(a0,a1,a2)=>(_nisps_input_set_config=Module["_nisps_input_set_config"]=wasmExports["ca"])(a0,a1,a2);var _nisps_input_process=Module["_nisps_input_process"]=(a0,a1,a2,a3,a4)=>(_nisps_input_process=Module["_nisps_input_process"]=wasmExports["da"])(a0,a1,a2,a3,a4);var _nisps_input_reset=Module["_nisps_input_reset"]=a0=>(_nisps_input_reset=Module["_nisps_input_reset"]=wasmExports["ea"])(a0);var _nisps_output_set_config=Module["_nisps_output_set_config"]=(a0,a1,a2,a3,a4)=>(_nisps_output_set_config=Module["_nisps_output_set_config"]=wasmExports["fa"])(a0,a1,a2,a3,a4);var _nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=(a0,a1,a2)=>(_nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=wasmExports["ga"])(a0,a1,a2);var _nisps_output_process=Module["_nisps_output_process"]=(a0,a1,a2,a3)=>(_nisps_output_process=Module["_nisps_output_process"]=wasmExports["ha"])(a0,a1,a2,a3);var _nisps_output_reset=Module["_nisps_output_reset"]=a0=>(_nisps_output_reset=Module["_nisps_output_reset"]=wasmExports["ia"])(a0);var _nisps_curve_apply=Module["_nisps_curve_apply"]=(a0,a1,a2)=>(_nisps_curve_apply=Module["_nisps_curve_apply"]=wasmExports["ja"])(a0,a1,a2);var _nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=(a0,a1,a2,a3,a4)=>(_nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=wasmExports["ka"])(a0,a1,a2,a3,a4);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["la"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["ma"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["na"])(a0,a1,a2);var _nisps_engine_process_block=Module["_nisps_engine_process_block"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_engine_process_block=Module["_nisps_engine_process_block"]=wasmExports["oa"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["qa"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["ra"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["sa"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["ta"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["ua"])();Module["ccall"]=ccall;Module["cwrap"]=cwrap;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; return moduleRtn; diff --git a/manifold/public/nisps.wasm b/manifold/public/nisps.wasm index 7c290a00aa686c16c6ddd10a7e38370505d4790e..f30179d559df6d16b0ddb7bfa5fbfb97a5f1ab77 100755 GIT binary patch delta 3389 zcmb7GYmijM74Gl!otZn2g)0opz%EPAUD#d1BRpr2McfO5Bq-sLpr%5}q%g?r26?SK zg3e;8StyK@gSkjSpekro-VE_VTzM5BRtZ50tMU@TSYq)=_+e3rmR3&Rxe$!`k^H&m z>+aL1PoMLhbNl-B$iXinyQ?v7`Lc-UWPps$h8YG72|!8-vZ6A2M;F;uh7cn%LWTh; zl8D76h0u*EMRzGuJ$j0`EY3pp!{Hhcj!uj_whCmY#fU8mY)ue3RxK?oD5Z1LJ6zR7UPX3YeaEnr_V~Ok{uG+(uCA|vC^U1dm@OIjQ-Pr zXz_socCmZyL^Kp<)zDsuiWr*2TJ%>R!8K z6mMKPkGj(PS3g0qVrO^Mm-_D9R9T81KUR&-rPCKL14By({(6^Ldh>66P1d}4qn+6C zL7#UouKeeAzR94^Cz|N&Eg`%~EQf`#45)W^gmDZRcVPrSWN|8j$MR~+Txj3j=5;?ZOGx~ICM$!&ZdtG$X84mP5<`|fn4SZtn( zO7Dy5sHAXjC7q4ZeQ!FtqPN$72Hr_ZGaWAu`Cd0S(TrcgbhmL9G2DBn885T*-edEy zkAva$nUDSw!`&&1;PUX(ML4aey;hvp@|R0+Ln7+3daFk(&`LbH^huo8@`05|7%XQ$ zgY|6F{qpBn!lJbums!kOg-7_H6RXe~@{T``dXrVJxtIqi_eU?`+bq_UMTalEt1qFK z#9%MB3Dty%SHBtGF<`q3evMOzxOZ>GCXVh0Tk)J;ZT=0$P%IKxm#U1X&O2rO$jDa%vo>tBaxMdCF8YaykFz?GTto}9PwSFNyQX0 z)Yj{c6Xvrj77K!$BN;=T&veMW}ZB+r?4ygX+(+J-i*C0Hk( zp$@6y?yA@E;P`>!BBfJG3=|hA-9w3fQlzWdCz&A@40g-B4aZoRp%jnSBd@BtAYx7` z&!5A**wY%~R&GPOuF8%#&^e?Nd~rsGom9}Gi|V41ldA0K_zJGD*MIr-y$)RgsM;p$6RPf@+zXfHknvxC-Usl^}QIuEhXU&*@5cltsuDf zmrz}Z=ktog*Fb_*Tk?vO(UQ;5Ny*pr!%#i;{L0b?*S?g`sGdA?QtHTX*sQ)bCt|cs zw~;`q)F|O7IZ%vsFo9FesL3H8HBc&TaLjc^GxRPqL!VX9H`B-IZ1iK9m6o3XD{nCc zr6U?KmgPV(#b{!e7)%RHJ7ScWrm3ghNRt--?SyY0VEPA`{sH=g3_YmDC^4>l-V`>e zdW;fHtXJjzwuG}35zZFkK)Jto1E&j4Q?pG>rL4R_N+^aSvvR#G!@*g(Hh>LT*&aZZ zm2Ck`W#vDDZB|xZ51^TqpEAW8=&P&v@p}r+Z3<2GrtYRR8SRpTX5#%D$0=4E_B*`8xQSApJm0yrZVIm)z+1#EV|It%E9fm$aj$S|4&e!2-FOHu0yXZ?BY1(u@grEvV#ZOdLXCIf zC=P;^HXp;sDE!$Zv)o6vpxKr|AK}vB8s%E$X!x}nF4Hv1nc!=rZ0;EAQ6401gA=3g zXot9XHA>hfU&d4+|IS537oa9ZB3|X97uIXsu-)xqCxl1R>d3Bg>eQmCS(RN7J zv2e^h#~6wHhZc3BRZvq+GsGSROC-dp-yDIy!%UNl1#3h^4bcRzX^O^0G z`OF~TGdIV8&)N+(?Vk|(`U#{$-#meHr)1f@&t>1zINfa{GW`0ysLup6jb+XteM6|5=L#U9JCPF>2!J z6zjt{qr%aJrpNr7uuZF)nv|h`3ETpiG7Q%s9n#XU!&-$o**pfZt zD(o-#$y8xp^CwwvE%?dh?>6mFi(OXZ`8N0lZmEX+{SkjsTe-P^o!;*|k+8!pc3F-0 zKV9A*nt4;VY`JXUmQ_|or!Ox?{(7y-GI`C6i0z$m*fJ{wW(`b zHzY1;-H;-;1vF?#%xV=bHzeZA`{i41NE~HN$kF`b)m5p2J4DuC-f(wBVl{@oy2n{Kd1h51_Sxk z?3rgVNWV3+=fIEJq2A8-@p9Ci`yp0hn0w_zOsE+aR|d0=T`)wgh+F(oqS=|hz!vO> zd*=mIx!by78XNhl8-5S(`xg-7H=q0PMU27_cf&;@6tr%=-xElsuKblTDNqrgi_ zW6DRM){S+vDFXw;f|sIAko{UrC4WPFB$?qb`FrtXx@62g?QW|P86 z*=>asO4MeXnNo8tZ!VL}v`ARZ4qK+>tW5ElS^w}i&PYc5t;v%D-F|QF1*7=Zf*#B( z%C<(5x3o2-<~i6baT{YSw|%D_2BoEBCtcPv+fOwu(-HF@nBND)ia%N4_=UEJgG&F& zyLK-|Wl?Ur)2Os{8Ixj%Rq3zaG$;Dm_1LxvPR{ogpM~F~;#K}9JJtbuap>tIaQyiP z{;Jh>fEQdkPcj#G(gj71=jaYC{`9duHOi~T?f#atucTo4eeZn$bofiAmc)v)j$A~u z|Iv{t_!~cLj*&k0@hGPhuUx&Zn7ew2a=vo4Ck89XaGc-ajs zW*v9MGbU9o!Yn74)bYz0WblH8AAqo>3JVBT-z@x!meonz12r;9Xh2tx^{|g_61+2jm#J34_SIM*(4{6GfuiNthVWWQH?P6P5VdvqSRkgzeQhwf z5oZ8~6{X)WJU(#}tdNspK%Mp2pT@jv9=6QzGcJ79J!sP1gen_|Een?1>=9;6qtha(w z2ra`YI&m?_xW;LvZQ&{5c94&e;V%nmsPqBc+tn(VT~FzV3{R5HboUF-fvXK=xD{p$ zPu5G?MQ@VCaWd9gH>fiQV7JscMw-6~J3)6QO>>eGAymmKnY?4R6JJ)r=$OPyXn_%Q zyhP&=hUz|uvoM}MM7?MPZPeQ#*>yN1)u)F`Z0iwXH453O79XSrH`Kq6mJBV&$Y7A6 z+vRIKn}UsF9+~eSM@_5Y&F8F}Gg%=MZfwZWJ*mt8BNIbT!lR6y8%LKgWJ0}r5UIFi zyg1sNZ2a9z+i7~`IL{WqvqbAO7T6{S5F9;?36Rt3U*kQfqvx=SHrAcTLj;eV$Il5Cy^8Cr+NoVR?jX46z{kCa$N^A9Vf^p9{z6gKvQ*ct`N|pRg~kzA}j#RroU=N2kIhzTk9*|Ds$! zz4IuOj9?}~=?hoVkWKS35$yd7Od4G3@Dy&u4eI?VEXP8%=n{=qU2VOD6*PbTc?pjS RTCVvUF2M=DaTyQH{4Z;|=F9*9 diff --git a/manifold/src/backends/backend.ts b/manifold/src/backends/backend.ts index d8eb38f..dafb6a1 100644 --- a/manifold/src/backends/backend.ts +++ b/manifold/src/backends/backend.ts @@ -33,7 +33,7 @@ export interface OutputMapping { /** What a backend needs to know about the active mode/output set. */ export interface BackendContext { modeId: string; - /** Model output dims in use (≤ 126). */ + /** Active output cards routed to this backend (may be < model capacity). */ outputCount: number; /** Per-output baseline mapping, length === outputCount. */ mappings: OutputMapping[]; diff --git a/manifold/src/backends/manager.ts b/manifold/src/backends/manager.ts index 3d99dfb..155c5d3 100644 --- a/manifold/src/backends/manager.ts +++ b/manifold/src/backends/manager.ts @@ -53,6 +53,8 @@ export class BackendManager { * click no longer silently drops the second request. Cleared before the * re-run so it can only ever chain one hop at a time (no unbounded loop). */ private pendingId: BackendId | null = null; + /** Reused active-prefix buffer when model capacity exceeds active cards. */ + private routedScratch = new Float32Array(0); private statusListeners = new Set<(id: BackendId, s: BackendStatus) => void>(); private offBackendStatus: (() => void) | null = null; @@ -77,7 +79,17 @@ export class BackendManager { this.active.setInputVector(this.engine.inputVector()); } const routed = this.engine.routedOutput(); - if (routed) this.active.send(routed); + if (!routed) return; + const activeCount = this.ctx?.outputCount ?? routed.length; + if (activeCount >= routed.length) { + this.active.send(routed); + } else { + if (this.routedScratch.length !== activeCount) { + this.routedScratch = new Float32Array(activeCount); + } + this.routedScratch.set(routed.subarray(0, activeCount)); + this.active.send(this.routedScratch); + } }); } @@ -127,6 +139,9 @@ export class BackendManager { /** Provide / refresh the BackendContext (mappings + names) for the active set. */ setContext(ctx: BackendContext): void { this.ctx = ctx; + if (this.routedScratch.length !== ctx.outputCount) { + this.routedScratch = new Float32Array(ctx.outputCount); + } this.active?.setContext?.(ctx); } diff --git a/manifold/src/backends/presets.ts b/manifold/src/backends/presets.ts index 3a88275..0c303db 100644 --- a/manifold/src/backends/presets.ts +++ b/manifold/src/backends/presets.ts @@ -14,6 +14,7 @@ import type { BackendId } from '../dock/output-state'; /** The per-output config a preset captures (a slice of MFParam). */ export interface OutputPresetRow { + id?: string; name: string; status: MFParam['status']; muted?: boolean; @@ -71,6 +72,7 @@ export function listPresets(backend: BackendId): OutputPreset[] { /** Project the live params into preset rows. */ export function rowsFromParams(params: MFParam[]): OutputPresetRow[] { return params.map((p) => ({ + id: p.id, name: p.name, status: p.status, muted: p.muted, @@ -140,7 +142,7 @@ export function renamePreset(backend: BackendId, from: string, to: string): bool */ export function applyPreset(params: MFParam[], preset: OutputPreset): MFParam[] { return params.map((p, i) => { - const r = preset.rows[i]; + const r = preset.rows.find((row) => row.id === p.id) ?? preset.rows[i]; if (!r) return p; return { ...p, diff --git a/manifold/src/console/ConsoleApp.tsx b/manifold/src/console/ConsoleApp.tsx index 901d2d2..a0a0a85 100644 --- a/manifold/src/console/ConsoleApp.tsx +++ b/manifold/src/console/ConsoleApp.tsx @@ -30,7 +30,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import type { CSSProperties } from 'react'; import { useEngine, useEngineVersion, ExplorationController } from '../engine'; -import { MF_MODES, modeEngineId, shapeValues } from './model'; +import { MF_MODES, createOutputParam, modeEngineId, shapeValues } from './model'; import type { MFParam } from './model'; import { CompositeStage } from './CompositeStage'; import { ParticleStage } from './ParticleStage'; @@ -39,7 +39,6 @@ import { OutputStage } from './OutputStage'; import { Manifold } from './Manifold'; import { VerdictCluster } from './VerdictCluster'; import { Dock } from './Dock'; -import { ReshapeModal } from './ReshapeModal'; import type { ConsoleCtx, DrawerDepth, @@ -57,6 +56,11 @@ import { DEFAULT_OUTPUT_MODE, outputDisplayCount, outputModeDescriptor } from '. import { useSettings, resolveInputMap } from '../settings/settings-store'; import { useBackendManager } from '../backends'; import { useInputLayer } from '../inputs'; +import { + completeDimensionMap, + resizeTarget, + type DimensionMap, +} from '../engine/io-reshape'; /** * Console debug seam, installed on `window.__mf` under `?debug=1` (see the @@ -102,6 +106,7 @@ export function ConsoleApp() { const [modeId, setModeId] = useState('paf_synth'); const mode = MF_MODES.find((m) => m.id === modeId) ?? MF_MODES[0]; const [params, setParams] = useState(() => mode.params.map((p) => ({ ...p }))); + const [paramsModeId, setParamsModeId] = useState(mode.id); const [pos, setPos] = useState<[number, number]>([0.5, 0.5]); const [noiseCap, setNoiseCap] = useState(0.12); @@ -146,10 +151,13 @@ export function ConsoleApp() { // Per-backend transport settings (backends-spec §2.3/§2.4). Persisted via the // named-preset system; these are the live working values. const [midiOutputId, setMidiOutputId] = useState(null); - const [midiCcCount, setMidiCcCount] = useState(8); - const displayOutputCount = outputDisplayCount(outputMode, params.length, { - midi: midiCcCount, + const [outputCounts, setOutputCounts] = useState>>({ + midi: 8, }); + const midiCcCount = outputDisplayCount('midi', params.length, outputCounts); + const setMidiCcCount = (n: number) => + setOutputCounts((counts) => ({ ...counts, midi: Math.max(1, Math.floor(n)) })); + const displayOutputCount = outputDisplayCount(outputMode, params.length, outputCounts); const [oscUrl, setOscUrl] = useState('ws://localhost:8765'); const [oscSendRaw, setOscSendRaw] = useState(false); // VCV bridge: WS URL of the Deno bridge that relays to the VCV module over UDP @@ -249,9 +257,9 @@ export function ConsoleApp() { // level — the true gradient column-freeze (`train_masked`) is the C++ step. useEffect(() => { controllerRef.current?.setSoloMode(soloMode); - controllerRef.current?.setArmMask(buildArmMask(params)); + controllerRef.current?.setArmMask(buildArmMask(params.slice(0, displayOutputCount))); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [engine, params, soloMode]); + }, [engine, params, displayOutputCount, soloMode]); // Keep the audio backend pointed at the current mode (audio itself is gated // behind a user gesture — see startAudio below). @@ -276,6 +284,7 @@ export function ConsoleApp() { // reset transient state on mode switch useEffect(() => { setParams(mode.params.map((p) => ({ ...p }))); + setParamsModeId(modeId); setPos([0.5, 0.5]); setExamples(0); setFollow(false); @@ -301,43 +310,55 @@ export function ConsoleApp() { // `pushPad`; MIDI + gamepad are pulled by the layer's own rAF loop. const inputs = useInputLayer(engine); - // ---- Reshape offer (runtime-shaped net, P2) -------------------------------- - // When the ACTIVE input layout CHANGES (source added/removed, MIDI-learn axes - // change, gamepad stick mode) to an axis count that differs from the net's - // current input arity, offer a warm-started reshape behind a confirm modal - // (locked decision: "reshapeable N-D net, reset-on-reshape modal"). We only - // offer on a genuine CHANGE — never on first load — so the default 32-input - // over-provisioned head is preserved untouched, and declining keeps the - // zero-padding path working. Once per layout change (debounced by the ref). - const [reshapeTarget, setReshapeTarget] = useState(null); - const prevAxisCountRef = useRef(null); + // ---- Identity-aware input-card migration ----------------------------------- + // Input sources already expose add/remove cards (MIDI learns, gamepad axes). + // Preserve their semantic coordinates by source+label when those rows change. + const inputSlotsRef = useRef | null>(null); + const inputLayoutKey = inputs.channelLayout + .map((channel) => `${channel.source}\u0000${channel.label}`) + .join('\u0001'); useEffect(() => { if (!engine) return; - const n = inputs.axisCount; - // Ignore the boot transient (sources attach a frame after mount, so the - // count ramps 0 → 2) and any "no active axes" lull — neither is a layout the - // user chose, and treating 0 as a baseline would make the first real layout - // look like a change and prompt on load. - if (n < 1) return; + const keys = inputLayoutKey ? inputLayoutKey.split('\u0001') : []; + if (keys.length < 1) return; const inSize = inputs.engineInputSize; - // First established layout is the baseline (default load) — record, never - // prompt. This is the default over-provisioned case that must stay untouched. - if (prevAxisCountRef.current === null) { - prevAxisCountRef.current = n; + const previous = inputSlotsRef.current; + if (previous === null) { + inputSlotsRef.current = new Map(keys.map((key, i) => [key, i])); return; } - if (n === prevAxisCountRef.current) return; // arity unchanged → no offer - prevAxisCountRef.current = n; - // Offer iff the new active layout no longer matches the net's arity. - if (n !== inSize) setReshapeTarget(n); - else setReshapeTarget(null); - }, [engine, inputs.axisCount, inputs.engineInputSize]); - - const confirmReshape = () => { - const n = reshapeTarget; - setReshapeTarget(null); - if (engine && n != null) engine.reshape({ inputSize: n }); - }; + const activeMap: DimensionMap = keys.map((key) => previous.get(key) ?? null); + const target = + resizeTarget(keys.length, inSize, settings.networkResizePolicy) ?? inSize; + const inputMap = completeDimensionMap(activeMap, target, inSize); + const needsMigration = + target !== inSize || inputMap.some((oldIndex, newIndex) => oldIndex !== newIndex); + if (needsMigration) { + engine.reshape( + { inputSize: target }, + randomisationSpread, + { + inputMap, + examples: settings.exampleResizePolicy, + addedInputValue: settings.addedInputExampleValue, + addedOutputValue: settings.addedOutputExampleValue, + }, + ); + controllerRef.current?.resetAfterIoChange(); + syncController(); + setExamples(engine.getState().exampleCount); + } + inputSlotsRef.current = new Map(keys.map((key, i) => [key, i])); + }, [ + engine, + inputLayoutKey, + inputs.engineInputSize, + randomisationSpread, + settings.networkResizePolicy, + settings.exampleResizePolicy, + settings.addedInputExampleValue, + settings.addedOutputExampleValue, + ]); // ---- Console debug seam (`?debug=1`) ---------------------------------------- // There is no instrument-mode picker in the UI yet (ctx.modes/setModeId are @@ -389,6 +410,10 @@ export function ConsoleApp() { // spine and forwards routed outputs to the active backend; switching Mode // tears down the old backend, starts the new one, and gates synth audio // (mute on non-synth modes). MIDI/OSC config + names ride the shared params. + const backendParams = useMemo( + () => params.slice(0, displayOutputCount), + [params, displayOutputCount], + ); const { manager: backendManager, status: backendStatus, @@ -401,7 +426,7 @@ export function ConsoleApp() { engine, outputBackend, modeId, - params, + backendParams, { outputId: midiOutputId, ccCount: midiCcCount }, { url: oscUrl, sendRaw: oscSendRaw }, { url: vcvUrl, sendRaw: vcvSendRaw }, @@ -636,6 +661,104 @@ export function ConsoleApp() { }); }, [version]); + /** + * The single output-card mutation seam. Callers provide the desired active + * cards; this module decides whether to keep capacity, permute in place, or + * reconstruct, then makes card order and MLP coordinates agree again. + */ + const applyOutputCards = (activeCards: MFParam[], spareCards: MFParam[]) => { + if (activeCards.length < 1) return; + const oldCapacity = engine?.architecture.outputSize ?? mode.ml.outputSize; + const target = + resizeTarget(activeCards.length, oldCapacity, settings.networkResizePolicy) ?? + oldCapacity; + const activeMap: DimensionMap = activeCards.map((param) => { + const index = param.engineIndex; + return index !== undefined && index >= 0 && index < oldCapacity ? index : null; + }); + const outputMap = completeDimensionMap(activeMap, target, oldCapacity); + const oldByIndex = new Map(); + for (const param of params) { + if (param.engineIndex !== undefined) oldByIndex.set(param.engineIndex, param); + } + const placed = new Set(activeCards.map((param) => param.id)); + const ordered: MFParam[] = [...activeCards]; + for (const oldIndex of outputMap.slice(activeCards.length)) { + if (oldIndex === null) continue; + const param = oldByIndex.get(oldIndex); + if (param && !placed.has(param.id)) { + ordered.push(param); + placed.add(param.id); + } + } + for (const param of [...spareCards, ...params]) { + if (!placed.has(param.id)) { + ordered.push(param); + placed.add(param.id); + } + } + + const needsMigration = + target !== oldCapacity || + outputMap.some((oldIndex, newIndex) => oldIndex !== newIndex); + if (engine && needsMigration) { + engine.reshape( + { outputSize: target }, + randomisationSpread, + { + outputMap, + examples: settings.exampleResizePolicy, + addedInputValue: settings.addedInputExampleValue, + addedOutputValue: settings.addedOutputExampleValue, + }, + ); + controllerRef.current?.resetAfterIoChange(); + syncController(); + setExamples(engine.getState().exampleCount); + } + setParams( + ordered.map((param, index) => ({ + ...param, + engineIndex: index < target ? index : undefined, + })), + ); + setOutputCounts((counts) => ({ ...counts, [outputMode]: activeCards.length })); + }; + + const addOutput = () => { + const active = params.slice(0, displayOutputCount); + const next = params[displayOutputCount] ?? createOutputParam(params.length); + const spares = params + .slice(displayOutputCount + (params[displayOutputCount] ? 1 : 0)) + .filter((param) => param.id !== next.id); + applyOutputCards([...active, next], spares); + }; + + const deleteOutput = (index: number) => { + if (displayOutputCount <= 1 || index < 0 || index >= displayOutputCount) return; + const active = params.slice(0, displayOutputCount); + const [removed] = active.splice(index, 1); + applyOutputCards(active, [removed, ...params.slice(displayOutputCount)]); + }; + + // Switching output targets (or changing the saved resize policy) reconciles + // the network to that target's persisted active-card count. + useEffect(() => { + if (paramsModeId !== modeId) return; + const count = outputDisplayCount(outputMode, params.length, outputCounts); + applyOutputCards(params.slice(0, count), params.slice(count)); + // `params` is deliberately read as the current card catalogue but omitted: + // applyOutputCards itself replaces it, so including it would self-trigger. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + engine, + modeId, + paramsModeId, + outputMode, + outputCounts[outputMode], + settings.networkResizePolicy, + ]); + // Ref-mirror of everything the two global-listener effects below close over // that is NOT already React-stable (verdict/navigation handlers are plain // consts re-created every render; `pos`/`inputs.inputMode` are per-render @@ -804,6 +927,8 @@ export function ConsoleApp() { }, params, setParam, + addOutput, + deleteOutput, displayOutputCount, outputMode, setOutputMode, @@ -840,8 +965,11 @@ export function ConsoleApp() { setSoloMode, exploring, learningPaused, - armedCount: params.filter((p) => p.armed).length, - clearArmed: () => setParams((ps) => ps.map((p) => (p.armed ? { ...p, armed: false } : p))), + armedCount: params.slice(0, displayOutputCount).filter((p) => p.armed).length, + clearArmed: () => + setParams((ps) => + ps.map((p, i) => (i < displayOutputCount && p.armed ? { ...p, armed: false } : p)), + ), // exploration gestures (Jolt + OU explore) joltActive, onJoltPress, @@ -1123,14 +1251,6 @@ export function ConsoleApp() { }} /> - {reshapeTarget !== null && ( - setReshapeTarget(null)} - /> - )} ); } diff --git a/manifold/src/console/Drawers.tsx b/manifold/src/console/Drawers.tsx index 578983f..4f6d64f 100644 --- a/manifold/src/console/Drawers.tsx +++ b/manifold/src/console/Drawers.tsx @@ -30,6 +30,7 @@ import { shapeValues } from './model'; import { outputModeDescriptor } from './output-mode'; import { useSettings, unfocusedIconCss } from '../settings/settings-store'; import type { UnfocusedIconColour, InputMapMode } from '../settings/settings-store'; +import type { ExampleResizePolicy, NetworkResizePolicy } from '../engine/io-reshape'; import { EditorPanel } from '../serial/EditorPanel'; import { TrainingHealth } from './TrainingHealth'; import { @@ -632,6 +633,9 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { off {counts.off || 0} muted {mutedN} + {ModeConfig(ctx, depth)} {/* Specialised per-backend config + named-preset bar (MIDI/OSC); hidden when condensed. */} @@ -651,10 +655,11 @@ function RoutingDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { const labelled = { ...p, name: nameFor(i, p.name) }; return ( ctx.setParam(i, patch)} + onDelete={activeParams.length > 1 ? () => ctx.deleteOutput(i) : undefined} showCurve={expanded} /> ); @@ -681,6 +686,14 @@ const INPUT_MAP_OPTS: { value: InputMapMode; label: string }[] = [ { value: 'rectangular', label: 'Rectangular' }, { value: 'circular', label: 'Circular' }, ]; +const NETWORK_RESIZE_OPTS: { value: NetworkResizePolicy; label: string }[] = [ + { value: 'capacity', label: 'Keep capacity' }, + { value: 'exact', label: 'Exact I/O' }, +]; +const EXAMPLE_RESIZE_OPTS: { value: ExampleResizePolicy; label: string }[] = [ + { value: 'adapt', label: 'Adapt' }, + { value: 'clear', label: 'Clear' }, +]; function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) { const { settings, set } = useSettings(); @@ -728,6 +741,47 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {

)} + I/O editing +
Network size
+ set('networkResizePolicy', v as NetworkResizePolicy)} + options={NETWORK_RESIZE_OPTS} + ariaLabel="Network resize policy" + /> +
Existing examples
+ set('exampleResizePolicy', v as ExampleResizePolicy)} + options={EXAMPLE_RESIZE_OPTS} + ariaLabel="Example resize policy" + /> + {depth === 'expanded' && ( + <> +

+ Keep capacity edits mappings in place and reconstructs only when the active cards outgrow + the network. Exact I/O keeps network arity equal to the cards. Surviving dimensions retain + their identity and weights; exploration scratch state resets after an I/O edit. +

+ set('addedInputExampleValue', v)} + /> + set('addedOutputExampleValue', v)} + /> + + )} + Chrome params.length - 4; return (
void; - onCancel: () => void; -} - -export function ReshapeModal({ target, current, onConfirm, onCancel }: ReshapeModalProps) { - return ( -
-
e.stopPropagation()} - style={{ - width: 'min(420px, 90vw)', - background: 'var(--bg-1)', - border: '1px solid var(--line)', - borderRadius: 'var(--r-2)', - boxShadow: '0 12px 40px rgba(0,0,0,0.5)', - fontFamily: 'var(--font-mono)', - color: 'var(--fg)', - padding: 'var(--sp-4, 18px)', - display: 'flex', - flexDirection: 'column', - gap: 14, - }} - > -
- Reshape the net? -
-

- Reshape the net to {target} input{target === 1 ? '' : 's'}? Weights are warm-started from - the current {current}-input net; examples and exploration state reset. -

-

- Decline to keep the current net — the extra axes stay zero-padded (inert). -

-
- - -
-
-
- ); -} diff --git a/manifold/src/console/model.ts b/manifold/src/console/model.ts index 0bcd980..a78d82c 100644 --- a/manifold/src/console/model.ts +++ b/manifold/src/console/model.ts @@ -101,6 +101,10 @@ export const DEFAULT_MODE_ML: ModeML = { * are re-declared here loosely to avoid a console→dock import cycle. */ export interface MFParam { + /** Stable semantic identity; array position is presentation, not identity. */ + id: string; + /** Current MLP output coordinate, absent while the card is inactive/spare. */ + engineIndex?: number; name: string; group: string; status: ParamStatus; @@ -166,7 +170,17 @@ function mkParams(spec: Spec): MFParam[] { const out: MFParam[] = []; for (const [group, names] of spec) { names.forEach((name) => - out.push({ name, group, status: 'live', val: 0.5, min: 0, max: 1, curve: 0.5 }), + out.push({ + id: `${group}:${name}:${out.length}`, + engineIndex: out.length, + name, + group, + status: 'live', + val: 0.5, + min: 0, + max: 1, + curve: 0.5, + }), ); } return out; @@ -190,7 +204,9 @@ function mlFromSchema(schema: ModeSchema): ModeML { * are surfaced as ENGINE-unit metadata for display only. */ function paramsFromSchema(schema: ModeSchema): MFParam[] { - return schema.params.map((p) => ({ + return schema.params.map((p, i) => ({ + id: `${schema.mode_id}:${p.name}`, + engineIndex: i, name: p.name, group: p.group, status: 'live' as ParamStatus, @@ -369,12 +385,32 @@ export function shapeValues(params: MFParam[], engineOut: Float32Array | null): if (p.status === 'fixed') { return p.min + Math.max(0, Math.min(1, p.val ?? 0.5)) * (p.max - p.min); } - const raw = engineOut && i < engineOut.length ? engineOut[i] : 0.5; + const engineIndex = p.engineIndex ?? i; + const raw = engineOut && engineIndex < engineOut.length ? engineOut[engineIndex] : 0.5; const v = p.min + applyCurve(raw, p.curve) * (p.max - p.min); return Math.max(0, Math.min(1, v)); }); } +/** Create a backend-agnostic output card after every schema output is active. */ +export function createOutputParam(index: number): MFParam { + const suffix = index + 1; + const unique = + typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function' + ? crypto.randomUUID() + : `${Date.now()}-${index}`; + return { + id: `custom:${unique}`, + name: `Output ${suffix}`, + group: 'custom', + status: 'live', + val: 0.5, + min: 0, + max: 1, + curve: 0.5, + }; +} + /** * Map a mode id → the audio-engine backend id. Routes on `MFMode.engineId` * (schema truth — `slp_workshop`'s schema already declares `engine_id: diff --git a/manifold/src/console/output-mode.ts b/manifold/src/console/output-mode.ts index af5e20a..69f183a 100644 --- a/manifold/src/console/output-mode.ts +++ b/manifold/src/console/output-mode.ts @@ -82,12 +82,9 @@ export function outputModeDescriptor(id: OutputMode): OutputModeDescriptor { } /** - * Number of output controls the active backend presents. - * - * The model may expose more parameters than a backend currently maps (MIDI is - * the live example: its CC count is adjustable). Keep that presentation - * boundary separate from the model arity so changing a backend count does not - * silently reshape the net and clear its examples. + * Number of active output cards the selected backend presents. Model capacity + * may be larger under the persisted keep-capacity policy; exact-I/O makes this + * count the model arity. The identity-aware engine action owns that distinction. */ export function outputDisplayCount( id: OutputMode, diff --git a/manifold/src/console/types.ts b/manifold/src/console/types.ts index 6c9b595..0934ae6 100644 --- a/manifold/src/console/types.ts +++ b/manifold/src/console/types.ts @@ -76,6 +76,8 @@ export interface ConsoleCtx { params: MFParam[]; /** Patch one output row in the shared store (drives stage + dock in sync). */ setParam: (i: number, patch: Partial) => void; + addOutput: () => void; + deleteOutput: (i: number) => void; /** Active backend outputs currently presented by the stage + routing rows. */ displayOutputCount: number; @@ -85,7 +87,7 @@ export interface ConsoleCtx { /** Available Web MIDI output ports (for the MIDI config picker). */ midiPorts: { id: string; name: string }[]; refreshMidiPorts: () => void; - /** MIDI backend settings (selected port + number of CCs mapped). */ + /** MIDI backend settings (selected port + active output-card count). */ midiOutputId: string | null; setMidiOutputId: (id: string | null) => void; midiCcCount: number; diff --git a/manifold/src/dock/OutputControlRow.tsx b/manifold/src/dock/OutputControlRow.tsx index fd5e3e7..40c13f9 100644 --- a/manifold/src/dock/OutputControlRow.tsx +++ b/manifold/src/dock/OutputControlRow.tsx @@ -60,11 +60,19 @@ export interface OutputControlRowProps { /** Live (computed) value for the value bar. */ value: number; onChange: (patch: Partial) => void; + /** Remove this semantic output from the active card set. */ + onDelete?: () => void; /** Show the curve pad inline (expand depth); hidden in compact rows. */ showCurve?: boolean; } -export function OutputControlRow({ param, value, onChange, showCurve = false }: OutputControlRowProps) { +export function OutputControlRow({ + param, + value, + onChange, + onDelete, + showCurve = false, +}: OutputControlRowProps) { const gc = `var(${GROUP_COLOR[param.group] || '--accent'})`; const muted = param.muted ?? false; const armed = param.armed ?? false; @@ -99,6 +107,28 @@ export function OutputControlRow({ param, value, onChange, showCurve = false }: {param.name} {param.group} + {onDelete && ( + + )} ))} - + + {ctx.midiCcCount} CC output{ctx.midiCcCount === 1 ? '' : 's'} · add/delete cards below + {s.message}
@@ -478,7 +468,7 @@ function OscConfig({ ctx }: { ctx: ConsoleCtx }) { - {ctx.params.map((p, i) => { + {ctx.params.slice(0, ctx.displayOutputCount).map((p, i) => { const o = p.osc ?? defaultOscSpec(p.name); return ( @@ -558,7 +548,7 @@ function VcvConfig({ ctx }: { ctx: ConsoleCtx }) { - {ctx.params.map((p, i) => { + {ctx.params.slice(0, ctx.displayOutputCount).map((p, i) => { const v = p.vcv ?? defaultVcvSpec(); return ( @@ -622,7 +612,7 @@ function CvConfig({ ctx }: { ctx: ConsoleCtx }) { - {ctx.params.map((p, i) => { + {ctx.params.slice(0, ctx.displayOutputCount).map((p, i) => { const c = (p.cv as { channel: CvChannelId; gateThreshold: number } | undefined) ?? defaultCvSpec(i); const isGate = c.channel.startsWith('gate'); return ( diff --git a/manifold/src/engine/engine-api.ts b/manifold/src/engine/engine-api.ts index 800d19b..a6d61df 100644 --- a/manifold/src/engine/engine-api.ts +++ b/manifold/src/engine/engine-api.ts @@ -21,6 +21,7 @@ import type { InputConfig, OutputConfig } from './pipeline-types'; import { Spine, type BackendSend } from './spine'; import type { EngineId, FeedbackMode, LayerStats } from './types'; import { WasmIML } from './wasm-iml'; +import type { IoMigration } from './io-reshape'; export interface EngineFeedbackApi { /** Positive feedback (thumbs-up). Returns the FeedbackAction int. */ @@ -315,17 +316,17 @@ export class EngineApi { } /** - * Reshape the net to new dims (runtime-shaped MLP; one-core-engine P2). The - * new net is warm-started from the current net's overlapping weights; the - * dataset + feedback/exploration state RESET (front-end shows a confirm modal - * first). Returns true on success. On success the spine re-reads its arity and - * re-ticks the last input so outputs/audio reflect the new net. + * Reconfigure runtime I/O. Without a migration plan this is the legacy + * reconstruct-and-clear operation. With one, stable dimension maps preserve + * semantic weights and optionally adapt examples; same-shape permutations do + * not reconstruct the MLP. Feedback/exploration scratch state always resets. */ reshape( dims: { inputSize?: number; outputSize?: number; hidden?: [number, number, number] }, spread = this.spread_, + migration?: IoMigration, ): boolean { - const ok = this.iml.reshape(dims, spread); + const ok = this.iml.reshape(dims, spread, migration); if (ok) this.process(); return ok; } diff --git a/manifold/src/engine/index.ts b/manifold/src/engine/index.ts index bbe4338..b77c471 100644 --- a/manifold/src/engine/index.ts +++ b/manifold/src/engine/index.ts @@ -25,6 +25,18 @@ export type { WasmIMLOptions } from './wasm-iml'; export { EngineHost } from './engine-host'; export { Dataset } from './dataset'; +export { + completeDimensionMap, + remapFlatWeights, + remapVector, + resizeTarget, +} from './io-reshape'; +export type { + DimensionMap, + ExampleResizePolicy, + IoMigration, + NetworkResizePolicy, +} from './io-reshape'; export { noopSink } from './sink'; export type { EngineSink, EngineStatePatch } from './sink'; diff --git a/manifold/src/engine/io-reshape.ts b/manifold/src/engine/io-reshape.ts new file mode 100644 index 0000000..a0bd09c --- /dev/null +++ b/manifold/src/engine/io-reshape.ts @@ -0,0 +1,154 @@ +/** + * Identity-aware I/O migration for the runtime-shaped MLP. + * + * A dimension map is destination-first: `map[newIndex]` is the old dimension + * whose meaning survives there, or `null` for a newly-created dimension. + * Keeping this algebra in one framework-neutral module prevents card order, + * flat-weight layout, and example migration from becoming three independent + * sources of truth. + */ +import type { MLArchitecture } from './types'; + +export type DimensionMap = ReadonlyArray; +export type ExampleResizePolicy = 'adapt' | 'clear'; +export type NetworkResizePolicy = 'capacity' | 'exact'; + +export interface IoMigration { + inputMap?: DimensionMap; + outputMap?: DimensionMap; + examples: ExampleResizePolicy; + addedInputValue: number; + addedOutputValue: number; +} + +/** Fill the inactive tail of an active dimension map with unused old slots. */ +export function completeDimensionMap( + active: DimensionMap, + newSize: number, + oldSize: number, +): Array { + const out = Array.from({ length: newSize }, (_, i) => active[i] ?? null); + const used = new Set(); + for (const oldIndex of out) { + if (oldIndex !== null && oldIndex >= 0 && oldIndex < oldSize) used.add(oldIndex); + } + const spare: number[] = []; + for (let i = 0; i < oldSize; ++i) if (!used.has(i)) spare.push(i); + let next = 0; + for (let i = active.length; i < newSize && next < spare.length; ++i) { + out[i] = spare[next++]; + } + return out; +} + +function prefixMap(size: number, oldSize: number): Array { + return Array.from({ length: size }, (_, i) => (i < oldSize ? i : null)); +} + +interface FlatLayout { + dims: [number, number, number, number, number]; + weightOffsets: [number, number, number, number]; + biasOffsets: [number, number, number, number]; +} + +function flatLayout(arch: MLArchitecture): FlatLayout { + const dims: FlatLayout['dims'] = [ + arch.inputSize, + arch.hidden[0], + arch.hidden[1], + arch.hidden[2], + arch.outputSize, + ]; + const weightOffsets: number[] = []; + let offset = 0; + for (let layer = 0; layer < 4; ++layer) { + weightOffsets.push(offset); + offset += dims[layer] * dims[layer + 1]; + } + const biasOffsets: number[] = []; + for (let layer = 0; layer < 4; ++layer) { + biasOffsets.push(offset); + offset += dims[layer + 1]; + } + return { + dims, + weightOffsets: weightOffsets as FlatLayout['weightOffsets'], + biasOffsets: biasOffsets as FlatLayout['biasOffsets'], + }; +} + +/** + * Overlay all surviving semantic coordinates from `oldWeights` onto the + * freshly-initialised destination weights. Hidden layers retain prefix + * identity; input columns and output rows follow the supplied maps. + */ +export function remapFlatWeights( + oldWeights: Float32Array, + oldArch: MLArchitecture, + freshWeights: Float32Array, + newArch: MLArchitecture, + inputMap?: DimensionMap, + outputMap?: DimensionMap, +): Float32Array { + const src = flatLayout(oldArch); + const dst = flatLayout(newArch); + const maps: Array> = [ + Array.from(inputMap ?? prefixMap(newArch.inputSize, oldArch.inputSize)), + prefixMap(newArch.hidden[0], oldArch.hidden[0]), + prefixMap(newArch.hidden[1], oldArch.hidden[1]), + prefixMap(newArch.hidden[2], oldArch.hidden[2]), + Array.from(outputMap ?? prefixMap(newArch.outputSize, oldArch.outputSize)), + ]; + const out = new Float32Array(freshWeights); + + for (let layer = 0; layer < 4; ++layer) { + const oldIn = src.dims[layer]; + const oldOut = src.dims[layer + 1]; + const newIn = dst.dims[layer]; + const newOut = dst.dims[layer + 1]; + const inputIndices = maps[layer]; + const outputIndices = maps[layer + 1]; + for (let node = 0; node < newOut; ++node) { + const oldNode = outputIndices[node]; + if (oldNode === null || oldNode < 0 || oldNode >= oldOut) continue; + for (let input = 0; input < newIn; ++input) { + const oldInput = inputIndices[input]; + if (oldInput === null || oldInput < 0 || oldInput >= oldIn) continue; + out[dst.weightOffsets[layer] + node * newIn + input] = + oldWeights[src.weightOffsets[layer] + oldNode * oldIn + oldInput]; + } + out[dst.biasOffsets[layer] + node] = oldWeights[src.biasOffsets[layer] + oldNode]; + } + } + return out; +} + +/** Remap one feature/label vector, filling genuinely new dimensions neutrally. */ +export function remapVector( + source: ArrayLike, + map: DimensionMap | undefined, + newSize: number, + placeholder: number, +): Float32Array { + const indices = map ?? prefixMap(newSize, source.length); + const out = new Float32Array(newSize); + for (let i = 0; i < newSize; ++i) { + const oldIndex = indices[i]; + out[i] = + oldIndex !== null && oldIndex >= 0 && oldIndex < source.length + ? source[oldIndex] + : placeholder; + } + return out; +} + +/** Decide whether an arity edit needs a real network reconstruction. */ +export function resizeTarget( + activeCount: number, + currentCapacity: number, + policy: NetworkResizePolicy, +): number | null { + const wanted = Math.max(1, Math.floor(activeCount)); + if (policy === 'exact') return wanted === currentCapacity ? null : wanted; + return wanted > currentCapacity ? wanted : null; +} diff --git a/manifold/src/engine/types.ts b/manifold/src/engine/types.ts index 00452d6..b036502 100644 --- a/manifold/src/engine/types.ts +++ b/manifold/src/engine/types.ts @@ -75,6 +75,8 @@ export interface NispsModule { // RandomiseMlp). Mode ints: 0=Avoid 1=RandomiseOutputs 2=RandomiseMlp. // Action return ints come from FeedbackController::on_*; see feedback.hpp. _nisps_ml_feedback_set_mode(ml: number, mode: number): void; + /** Reset index-aligned feedback/exploration state without rebuilding the MLP. */ + _nisps_ml_feedback_reset(ml: number): void; _nisps_ml_feedback_get_mode(ml: number): number; _nisps_ml_feedback_exploring(ml: number): number; // 1 = exploring _nisps_ml_feedback_set_focus(ml: number, mask_ptr: number, n: number): void; diff --git a/manifold/src/engine/wasm-iml.ts b/manifold/src/engine/wasm-iml.ts index dbed2e0..90cc053 100644 --- a/manifold/src/engine/wasm-iml.ts +++ b/manifold/src/engine/wasm-iml.ts @@ -38,6 +38,11 @@ import { type NispsModuleFactory, } from './types'; import { createTrainer, type WasmTrainer } from './wasm-worker'; +import { + remapFlatWeights, + remapVector, + type IoMigration, +} from './io-reshape'; /** Default architecture matches `nisps/wasm/bindings.cpp` instantiation. */ const DEFAULT_INPUT_SIZE = 2; @@ -352,98 +357,145 @@ export class WasmIML { // ------------------------------------------------------------------- /** - * Swap the net for one at new dims, warm-started from the overlapping weights - * of the current net (`nisps_ml_reshape`). Any omitted dim keeps its current - * value. Returns true on success (false = C-side rejected / no change). - * - * The C side RESETS its dataset/examples and feedback/exploration state on - * reshape, so this method also clears the TS `Dataset` mirror, reallocates - * every dim-dependent heap buffer, refreshes `weightCount`, and pushes the new - * shape + zeroed example/output state through the sink so React re-reads. + * Apply one identity-aware I/O edit. A changed shape reconstructs the C++ MLP; + * a same-shape permutation edits weights/examples in place. */ reshape( dims: { inputSize?: number; outputSize?: number; hidden?: readonly [number, number, number] }, spread = 0, + migration?: IoMigration, ): boolean { + const oldArch = this.arch_; const wantIn = dims.inputSize ?? this.arch_.inputSize; const wantOut = dims.outputSize ?? this.arch_.outputSize; const wantHidden = dims.hidden ?? this.arch_.hidden; + const shapeChanged = + wantIn !== oldArch.inputSize || + wantOut !== oldArch.outputSize || + wantHidden.some((n, i) => n !== oldArch.hidden[i]); + const oldWeights = migration ? this.getWeights() : null; + const examples = + migration?.examples === 'adapt' + ? Array.from({ length: this.dataset.size }, (_, i) => ({ + features: new Float32Array(this.dataset.feature(i)), + labels: new Float32Array(this.dataset.label(i)), + })) + : []; - const hiddenPtr = this.module._malloc(wantHidden.length * 4); - new Int32Array(this.module.HEAP32.buffer, hiddenPtr, wantHidden.length).set(wantHidden); - const ok = this.module._nisps_ml_reshape( - this.mlHandle, - wantIn, - wantOut, - hiddenPtr, - wantHidden.length, - spread, - ); - this.module._free(hiddenPtr); - if (ok !== 1) return false; + if (shapeChanged || !migration) { + const hiddenPtr = this.module._malloc(wantHidden.length * 4); + new Int32Array(this.module.HEAP32.buffer, hiddenPtr, wantHidden.length).set(wantHidden); + const ok = this.module._nisps_ml_reshape( + this.mlHandle, + wantIn, + wantOut, + hiddenPtr, + wantHidden.length, + spread, + ); + this.module._free(hiddenPtr); + if (ok !== 1) return false; - // Re-describe the (new) instance and refresh the weight count. - this.module._nisps_ml_describe(this.mlHandle, this.describePtr); - const d = new Int32Array(this.module.HEAP32.buffer, this.describePtr, 7); - this.arch_ = { - inputSize: d[0], - hidden: [d[1], d[2], d[3]], - outputSize: d[4], - numLayers: d[5], - maxExamples: d[6], - }; - this.weightCount_ = this.module._nisps_ml_weight_count(this.mlHandle); - // nisps_ml_reshape never varies max_examples (no such parameter exists on - // that C API) — it always reconstructs at kDefaultMaxExamples, same as - // create(). The Dataset mirror's cap is therefore still correct; only its - // contents are cleared below, matching the C++ side's dataset reset. + this.module._nisps_ml_describe(this.mlHandle, this.describePtr); + const d = new Int32Array(this.module.HEAP32.buffer, this.describePtr, 7); + this.arch_ = { + inputSize: d[0], + hidden: [d[1], d[2], d[3]], + outputSize: d[4], + numLayers: d[5], + maxExamples: d[6], + }; + this.weightCount_ = this.module._nisps_ml_weight_count(this.mlHandle); - // Reallocate every dim-dependent heap buffer. Freeing first then reallocating - // means a later malloc may sbrk-grow the heap and detach earlier views, so we - // rebind() all of them afterwards. - this.featuresBuf.free(); - this.labelsBuf.free(); - this.weightsBuf.free(); - this.statsBuf.free(); - this.batchInBuf.free(); - this.batchOutBuf.free(); - this.pinMaskBuf.free(); - this.feedbackBuf.free(); - this.outProcBuf.free(); - this.pipeMaskBuf.free(); - this.featuresBuf = new HeapBuffer(this.module, this.arch_.inputSize); - this.labelsBuf = new HeapBuffer(this.module, this.arch_.outputSize); - this.weightsBuf = new HeapBuffer(this.module, this.weightCount_); - this.statsBuf = new HeapBuffer(this.module, this.arch_.numLayers * 4); - this.batchInBuf = new HeapBuffer(this.module, WasmIML.MAX_BATCH * this.arch_.inputSize); - this.batchOutBuf = new HeapBuffer(this.module, WasmIML.MAX_BATCH * this.arch_.outputSize); - this.pinMaskBuf = new HeapU8(this.module, this.arch_.outputSize); - this.feedbackBuf = new HeapBuffer(this.module, this.arch_.outputSize); - this.outProcBuf = new HeapBuffer(this.module, this.arch_.outputSize); - this.pipeMaskBuf = new HeapU8(this.module, this.arch_.outputSize); - this.featuresBuf.rebind(); - this.labelsBuf.rebind(); - this.weightsBuf.rebind(); - this.statsBuf.rebind(); - this.batchInBuf.rebind(); - this.batchOutBuf.rebind(); - this.pinMaskBuf.rebind(); - this.feedbackBuf.rebind(); - this.outProcBuf.rebind(); - this.pipeMaskBuf.rebind(); - // Fixed-size pipeline buffers were not reallocated but a grow above may have - // detached their views — rebind so later writes hit the live heap. - this.inCfgBuf.rebind(); - this.inXYBuf.rebind(); - this.curveBuf.rebind(); + // A malloc may grow WASM memory, so all retained fixed-size views are + // rebound after reallocating the dimension-dependent buffers. + this.featuresBuf.free(); + this.labelsBuf.free(); + this.weightsBuf.free(); + this.statsBuf.free(); + this.batchInBuf.free(); + this.batchOutBuf.free(); + this.pinMaskBuf.free(); + this.feedbackBuf.free(); + this.outProcBuf.free(); + this.pipeMaskBuf.free(); + this.featuresBuf = new HeapBuffer(this.module, this.arch_.inputSize); + this.labelsBuf = new HeapBuffer(this.module, this.arch_.outputSize); + this.weightsBuf = new HeapBuffer(this.module, this.weightCount_); + this.statsBuf = new HeapBuffer(this.module, this.arch_.numLayers * 4); + this.batchInBuf = new HeapBuffer(this.module, WasmIML.MAX_BATCH * this.arch_.inputSize); + this.batchOutBuf = new HeapBuffer(this.module, WasmIML.MAX_BATCH * this.arch_.outputSize); + this.pinMaskBuf = new HeapU8(this.module, this.arch_.outputSize); + this.feedbackBuf = new HeapBuffer(this.module, this.arch_.outputSize); + this.outProcBuf = new HeapBuffer(this.module, this.arch_.outputSize); + this.pipeMaskBuf = new HeapU8(this.module, this.arch_.outputSize); + this.featuresBuf.rebind(); + this.labelsBuf.rebind(); + this.weightsBuf.rebind(); + this.statsBuf.rebind(); + this.batchInBuf.rebind(); + this.batchOutBuf.rebind(); + this.pinMaskBuf.rebind(); + this.feedbackBuf.rebind(); + this.outProcBuf.rebind(); + this.pipeMaskBuf.rebind(); + this.inCfgBuf.rebind(); + this.inXYBuf.rebind(); + this.curveBuf.rebind(); + } else { + // Pure permutations do not recreate the network. Feedback replay, + // scratchpad, Jolt, and OU state are index-aligned, so reset only those. + this.module._nisps_ml_feedback_reset(this.mlHandle); + const hasNewDimension = + migration.inputMap?.some((oldIndex) => oldIndex === null) || + migration.outputMap?.some((oldIndex) => oldIndex === null); + if (hasNewDimension) { + // Establish fresh deterministic values, then the remap below overlays + // every surviving coordinate. Only genuinely new columns/rows remain. + this.module._nisps_ml_draw_weights(this.mlHandle, spread); + } + } - // C-side dataset/examples reset on reshape → clear the TS mirror to match. + if (migration && oldWeights) { + this.setWeights( + remapFlatWeights( + oldWeights, + oldArch, + this.getWeights(), + this.arch_, + migration.inputMap, + migration.outputMap, + ), + ); + } + + // Rebuild both example mirrors from the same adapted vectors. The C++ + // reshape already cleared its side; same-shape migration needs this call. this.dataset.clear(); + this.module._nisps_ml_clear_examples(this.mlHandle); + if (migration?.examples === 'adapt') { + for (const example of examples) { + const features = remapVector( + example.features, + migration.inputMap, + this.arch_.inputSize, + migration.addedInputValue, + ); + const labels = remapVector( + example.labels, + migration.outputMap, + this.arch_.outputSize, + migration.addedOutputValue, + ); + this.dataset.add(Array.from(features), Array.from(labels)); + this.copyExampleToWasm_(Array.from(features), Array.from(labels)); + } + } this.lastLoss_ = null; + this.resetInput(); + this.resetOutput(); - // The lazy training worker's mirror net is now stale (wrong arity). Dropping - // it makes the next trainAsync re-create it; the train protocol also carries - // the current dims so a fresh worker matches (see wasm-worker.ts). + // The worker mirror is stale after any weight, example, or shape migration. if (this.trainer) { this.trainer.dispose(); this.trainer = null; @@ -452,7 +504,7 @@ export class WasmIML { this.sink.setState({ inputSize: this.arch_.inputSize, outputSize: this.arch_.outputSize, - exampleCount: 0, + exampleCount: this.dataset.size, lastLoss: null, lossHistory: [], }); @@ -460,6 +512,8 @@ export class WasmIML { this.sink.emit('ml.reshaped', { inputSize: this.arch_.inputSize, outputSize: this.arch_.outputSize, + reconstructed: shapeChanged || !migration, + exampleCount: this.dataset.size, }); this.scheduleSave_(); return true; diff --git a/manifold/src/feedback/controller.ts b/manifold/src/feedback/controller.ts index e0f8bcd..838f532 100644 --- a/manifold/src/feedback/controller.ts +++ b/manifold/src/feedback/controller.ts @@ -176,6 +176,16 @@ export class FeedbackController { this.spread = spread; } + /** + * An I/O identity edit resets the core's index-aligned scratch/replay state. + * Mirror that reset locally without issuing another core transition. + */ + resetAfterIoChange(): void { + this.exploringFlag = false; + this.pickingFlag = false; + this.anchors = []; + } + /** * Set the arm/solo mask. The dock builds this from the per-output `armed` * flags (dock/output-state.ts buildArmMask). We RESPECT it at the example diff --git a/manifold/src/inputs/input-layer.ts b/manifold/src/inputs/input-layer.ts index a5cffd1..ac11006 100644 --- a/manifold/src/inputs/input-layer.ts +++ b/manifold/src/inputs/input-layer.ts @@ -23,9 +23,10 @@ * behaviour) — that diluted every source and biased the net toward idle * sources' resting values. * - * Changing the ACTIVE axis count offers a reshape (ConsoleApp → ReshapeModal): - * a new net at the new arity, warm-started from the overlapping weights, with - * examples + feedback state reset. Declining keeps this over-provisioned head. + * Changing the ACTIVE layout runs through ConsoleApp's persistent I/O policy: + * keep-capacity remaps stable dimensions in place until capacity is exceeded; + * exact-I/O reconstructs to the active arity. Example adaptation/clearing is a + * separate persisted choice. */ import type { InputAction, InputSource } from './types'; diff --git a/manifold/src/settings/settings-store.ts b/manifold/src/settings/settings-store.ts index 20a51a9..0968a15 100644 --- a/manifold/src/settings/settings-store.ts +++ b/manifold/src/settings/settings-store.ts @@ -17,6 +17,10 @@ * read + mutate settings without a render tree. */ import { useSyncExternalStore } from 'react'; +import type { + ExampleResizePolicy, + NetworkResizePolicy, +} from '../engine/io-reshape'; /** Resting (unfocused) icon colour choice. Focused is always --accent. */ export type UnfocusedIconColour = 'off-white' | 'white' | 'orange'; @@ -43,6 +47,17 @@ export interface Settings { * Learning-drawer control. Off means full-range uniform randomisation. */ xavierSpreadEnabled: boolean; + /** + * `capacity` keeps the current network while the edited card set fits; + * `exact` reconstructs whenever active I/O arity changes. + */ + networkResizePolicy: NetworkResizePolicy; + /** What a required I/O migration does with existing training examples. */ + exampleResizePolicy: ExampleResizePolicy; + /** Neutral feature value inserted into old examples for a new input. */ + addedInputExampleValue: number; + /** Neutral label value inserted into old examples for a new output. */ + addedOutputExampleValue: number; } export const DEFAULT_SETTINGS: Settings = { @@ -51,6 +66,10 @@ export const DEFAULT_SETTINGS: Settings = { inputMap: 'follow-mode', cornerRadius: 2, xavierSpreadEnabled: false, + networkResizePolicy: 'capacity', + exampleResizePolicy: 'adapt', + addedInputExampleValue: 0, + addedOutputExampleValue: 0.5, }; const STORAGE_KEY = 'mf-settings'; diff --git a/manifold/tests/backend-manager-switch.test.ts b/manifold/tests/backend-manager-switch.test.ts index 836ad72..7e1efb1 100644 --- a/manifold/tests/backend-manager-switch.test.ts +++ b/manifold/tests/backend-manager-switch.test.ts @@ -28,6 +28,7 @@ class FakeBackend implements OutputBackend { readonly id: BackendId; startCalls = 0; teardownCalls = 0; + sent: number[][] = []; private release: (() => void) | null = null; private hold: boolean; @@ -59,7 +60,9 @@ class FakeBackend implements OutputBackend { this.teardownCalls++; } - send(): void {} + send(routed: Float32Array): void { + this.sent.push(Array.from(routed)); + } status(): BackendStatus { return { state: 'ready', message: 'fake' }; @@ -121,6 +124,32 @@ test('rapid repeated switches to the SAME pending id only apply it once', async expect(osc.startCalls).toBe(1); }); +test('capacity slots are not forwarded beyond the active output-card count', async () => { + let notify: (() => void) | null = null; + const engine: ManagerEngine = { + subscribe: (cb) => { + notify = cb; + return () => {}; + }, + routedOutput: () => new Float32Array([0.1, 0.2, 0.3, 0.4]), + audio: { setMuted: () => {} }, + }; + const midi = new FakeBackend('midi'); + const manager = new BackendManager(engine, { midi }); + manager.setContext({ + modeId: 'test', + outputCount: 2, + mappings: [], + names: [], + }); + await manager.setActive('midi'); + notify!(); + + expect(midi.sent).toEqual([ + Array.from(new Float32Array([0.1, 0.2])), + ]); +}); + // NOTE: `manifold/package.json`'s `test` script is `bun test src // tests/pipeline-golden.test.ts` — an explicit file list, not a directory // glob, so this file (like any other new file under tests/) is NOT picked up diff --git a/manifold/tests/e2e/output-display-count.spec.ts b/manifold/tests/e2e/output-display-count.spec.ts index fea7fbd..8de0892 100644 --- a/manifold/tests/e2e/output-display-count.spec.ts +++ b/manifold/tests/e2e/output-display-count.spec.ts @@ -8,7 +8,7 @@ declare global { } } -test('output sliders follow the active backend output count', async ({ page }) => { +test('output cards add/delete in both drawer depths and capacity mode avoids reconstruction', async ({ page }) => { await loadProbe(page); // Drive the real UI path: output target → Outputs drawer → expanded MIDI @@ -18,14 +18,25 @@ test('output sliders follow the active backend output count', async ({ page }) = await page.getByRole('button', { name: 'MIDI', exact: true }).click(); await page.getByTitle('Outputs').click(); await page.getByTitle('Expand').click(); - await page.getByLabel('CCs').fill('2'); + + // MIDI starts with eight output cards. Delete six real cards instead of + // editing a detached numeric count. + await expect(page.getByText('8 outputs', { exact: true })).toBeVisible(); + for (let i = 0; i < 6; ++i) { + await page.getByRole('button', { name: /^Delete .* output$/ }).last().click(); + } + await expect(page.getByText('2 outputs', { exact: true })).toBeVisible(); + // Default "Keep capacity" policy edits semantic mappings in place. + expect(await page.evaluate(() => window.__nisps!.getOutputs().length)).toBe(33); // The count remains visible in condensed chrome after leaving the advanced - // config, and closing the drawer reveals the same number of stage columns. + // config. Adding in condensed depth updates that same card set. await page.getByTitle('Condense').click(); await expect(page.getByText('2 outputs', { exact: true })).toBeVisible(); + await page.getByRole('button', { name: '+ output', exact: true }).click(); + await expect(page.getByText('3 outputs', { exact: true })).toBeVisible(); await page.getByTitle('Close').click(); - await expect(page.getByTestId('output-stage')).toHaveAttribute('data-output-count', '2'); + await expect(page.getByTestId('output-stage')).toHaveAttribute('data-output-count', '3'); // Backends without a configured count present the full mode output set. const fullCount = await page.evaluate(() => window.__mf!.paramCount()); @@ -33,3 +44,34 @@ test('output sliders follow the active backend output count', async ({ page }) = await page.getByRole('button', { name: 'OSC', exact: true }).click(); await expect(page.getByTestId('output-stage')).toHaveAttribute('data-output-count', String(fullCount)); }); + +test('exact I/O persists and adapts examples across a deleted output identity', async ({ page }) => { + await loadProbe(page); + + await page.getByTitle('Settings').click(); + await page.getByRole('radio', { name: 'Exact I/O' }).click(); + await page.getByTitle('Close').click(); + + await page.getByTitle(/^Mode:/).click(); + await page.getByRole('button', { name: 'MIDI', exact: true }).click(); + await expect.poll(() => page.evaluate(() => window.__nisps!.getOutputs().length)).toBe(8); + + const added = await page.evaluate(() => { + const probe = window.__nisps!; + probe.setFeedbackMode('explore_and_place'); + return probe.addExample([0.1, 0.2, 0.3, 0.4], Array.from(probe.getOutputs())); + }); + expect(added).toBe(true); + expect(await page.evaluate(() => window.__nisps!.getExampleCount())).toBe(1); + + await page.getByTitle('Outputs').click(); + await page.getByRole('button', { name: /^Delete .* output$/ }).first().click(); + await expect.poll(() => page.evaluate(() => window.__nisps!.getOutputs().length)).toBe(7); + expect(await page.evaluate(() => window.__nisps!.getExampleCount())).toBe(1); + expect(await page.evaluate(() => window.__nisps!.getFeedbackMode())).toBe('explore_and_place'); + + const persisted = await page.evaluate(() => JSON.parse(localStorage.getItem('mf-settings') ?? '{}')); + expect(persisted.networkResizePolicy).toBe('exact'); + expect(persisted.exampleResizePolicy).toBe('adapt'); + expect(persisted.addedOutputExampleValue).toBe(0.5); +}); diff --git a/manifold/tests/io-reshape.test.ts b/manifold/tests/io-reshape.test.ts new file mode 100644 index 0000000..2b0b8ab --- /dev/null +++ b/manifold/tests/io-reshape.test.ts @@ -0,0 +1,49 @@ +import { expect, test } from 'bun:test'; +import type { MLArchitecture } from '../src/engine/types'; +import { + completeDimensionMap, + remapFlatWeights, + remapVector, + resizeTarget, +} from '../src/engine/io-reshape'; + +function arch(inputSize: number, outputSize: number): MLArchitecture { + return { inputSize, hidden: [2, 2, 2], outputSize, numLayers: 4, maxExamples: 8 }; +} + +test('capacity policy only reconstructs when active I/O exceeds capacity', () => { + expect(resizeTarget(2, 8, 'capacity')).toBeNull(); + expect(resizeTarget(9, 8, 'capacity')).toBe(9); + expect(resizeTarget(2, 8, 'exact')).toBe(2); + expect(resizeTarget(8, 8, 'exact')).toBeNull(); +}); + +test('a middle deletion keeps surviving identities and moves unused slots to the tail', () => { + expect(completeDimensionMap([0, 2], 4, 4)).toEqual([0, 2, 1, 3]); +}); + +test('example vectors remove deleted dimensions and fill additions with placeholders', () => { + expect(Array.from(remapVector([10, 20, 30], [0, 2], 2, -1))).toEqual([10, 30]); + expect(Array.from(remapVector([10, 20], [0, null, 1], 3, 0.5))).toEqual([10, 0.5, 20]); +}); + +test('weight remap preserves an arbitrary output row and bias by identity', () => { + const oldArch = arch(2, 3); + const newArch = arch(2, 2); + // 4 layers: weights 4 + 4 + 4 + 6, then biases 2 + 2 + 2 + 3 = 27. + const oldWeights = Float32Array.from({ length: 27 }, (_, i) => i + 1); + const freshWeights = new Float32Array(24).fill(-1); + const remapped = remapFlatWeights( + oldWeights, + oldArch, + freshWeights, + newArch, + undefined, + [0, 2], + ); + // Final-layer weights begin at 12. Old rows: [13,14], [15,16], [17,18]. + expect(Array.from(remapped.slice(12, 16))).toEqual([13, 14, 17, 18]); + // Destination output biases are the final two entries; old output biases + // are [25,26,27], so output identity 2 must retain 27 rather than 26. + expect(Array.from(remapped.slice(22, 24))).toEqual([25, 27]); +}); diff --git a/nisps/wasm/bindings.cpp b/nisps/wasm/bindings.cpp index 772c663..05d7345 100644 --- a/nisps/wasm/bindings.cpp +++ b/nisps/wasm/bindings.cpp @@ -401,6 +401,8 @@ int nisps_ml_reshape(void* ml, int input_size, int output_size, const MlDims d = sanitise_dims(input_size, output_size, hidden, n_hidden); if (!d.ok) return 0; + const auto feedback_mode = h->feedback.mode(); + const auto avoid_style = h->feedback.avoid_style(); BrowserMLP fresh(h->seed64, d.n_in, std::span(d.hidden, 3u), d.n_out); if (!fresh.valid()) return 0; fresh.draw_weights(spread); @@ -409,6 +411,8 @@ int nisps_ml_reshape(void* ml, int input_size, int output_size, BrowserFeedback fb(h->seed64 ^ kFeedbackSalt, d.n_out, fresh.weight_count(), kFeedbackUndoDepth, d.n_in, kFeedbackReplayCap); if (!fb.valid()) return 0; + fb.set_avoid_style(avoid_style); + fb.set_mode(feedback_mode, fresh); h->mlp = static_cast(fresh); h->feedback = static_cast(fb); @@ -614,6 +618,28 @@ void nisps_ml_feedback_set_mode(void* ml, int mode) { h->feedback.set_mode(m, h->mlp); } +// Reset only the index-aligned feedback/exploration state. Same-capacity I/O +// edits remap weights and examples without reconstructing the network, but +// replay/scratch vectors have no stable parameter-identity contract. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_reset(void* ml) { + if (!ml) return; + auto* h = static_cast(ml); + const auto feedback_mode = h->feedback.mode(); + const auto avoid_style = h->feedback.avoid_style(); + BrowserFeedback fb(h->seed64 ^ kFeedbackSalt, h->n_out(), h->mlp.weight_count(), + kFeedbackUndoDepth, h->n_in(), kFeedbackReplayCap); + if (!fb.valid()) return; + fb.set_avoid_style(avoid_style); + fb.set_mode(feedback_mode, h->mlp); + h->feedback = static_cast(fb); + h->feedback_static_scratch.assign(h->n_out(), 0.f); + h->mlp.reset_optimizer_state(); + h->jolt.release(); + h->ou.reset(); + h->jolt_scratch.assign(h->mlp.weight_count(), 0.f); +} + EMSCRIPTEN_KEEPALIVE int nisps_ml_feedback_get_mode(void* ml) { if (!ml) return 0; diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index a4adf8a..5f33ea1 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -46,7 +46,7 @@ EXPORTED_FUNCS='[ "_nisps_ml_clear_examples", "_nisps_ml_weight_count","_nisps_ml_get_weights","_nisps_ml_set_weights", "_nisps_ml_draw_weights", - "_nisps_ml_feedback_set_mode","_nisps_ml_feedback_get_mode", + "_nisps_ml_feedback_set_mode","_nisps_ml_feedback_reset","_nisps_ml_feedback_get_mode", "_nisps_ml_feedback_exploring", "_nisps_ml_feedback_set_focus","_nisps_ml_feedback_down", "_nisps_ml_feedback_up","_nisps_ml_feedback_static_output",