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 7c290a0..f30179d 100755
Binary files a/manifold/public/nisps.wasm and b/manifold/public/nisps.wasm differ
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}
+
+ + output
+
{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).
-
-
-
- Keep current
-
-
- Reshape to {target}
-
-
-
-
- );
-}
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 && (
+
+ ×
+
+ )}
))}
-
- CCs
-
- ctx.setMidiCcCount(Math.max(1, Math.min(ctx.params.length, num(e.target.value, ctx.midiCcCount))))
- }
- style={{ ...cellInput, width: 60 }}
- />
-
+
+ {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",