From 9490e20a7a4cc65df6de316ff90bc4d57664bb35 Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Tue, 14 Jul 2026 04:16:21 +0200 Subject: [PATCH] =?UTF-8?q?feat(ml)!:=20P3=20core=20=E2=80=94=20geometric?= =?UTF-8?q?=20dislike=20in=20nisps/,=20jolt/OU=20+=20geo=20ABI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Geometric dislike (rl-feedback-design §2.1/§4; upstream InterfaceRL @ 0a541cc ported verbatim, constants included): - nisps/ml/replay.hpp: ReplayView over storage-owned buffers — deepen-or- store negatives (dedup 0.05, clamp -16), k-NN positive centroid with deterministic index tie-break + fixed accumulation order, proportional decay (0.0025*max(|r|,1)) + eviction, order-preserving compaction. - nisps/ml/geo_push.hpp: push-away target (pushStep clamp(|avgNeg|,.25,1) *0.5, taper /(1+len), useRandom on len<=1e-4 via nisps::Rng — the single deliberate divergence from libc rand()), negLRRatio 0.5-0.4*negFraction. - mlp.hpp: train_targets(input, computed-target, lr, out_mask) — trains toward computed targets (negative lr = cold-start train-away); solo/ focus gating zeroes masked derivs. - feedback.hpp: AvoidStyle {Geometric (new default), Diffuse (legacy move_weights, kept for A/B)}; dislike_geometric() collapses upstream's press+optimise into one synchronous call; on_up in geometric Avoid feeds the positive centroid; dislike-multiplier bookkeeping. Storage gains replay buffers (Fixed: ReplayCap=32 firmware default ≈ +8KB SRAM; Dynamic arena: cap 64). - bindings: nisps_ml_feedback_{dislike_geometric,store_positive, positive_count,negative_count,set_avoid_style} + P3.2 jolt/OU ABI: nisps_ml_jolt_{press,step,release,active,lr_scale,tick_lr_ramp}, nisps_ml_explore_{intensity,get_intensity,apply} (OUNoise<4096> over-provisioned; same code the firmware ModeBase runs). - parity v4: Stage 6 scripted geometric session (2 likes → 2 dislikes, f32-exact heard vectors via Math.fround) — 961 floats PASS at 2.4e-7. - tests: test_mlp_geo_dislike.cpp (replay dedup/deepen/clamp, centroid tie-break, push direction/taper/mask/clamp, cold-start inertness + train-away, determinism, Diffuse legacy); legacy Avoid test pinned to Diffuse per the ADR's deliberate-break note. Firmware: PAFSynth .text/.data unchanged (geometric path not referenced by current glue). NOTE: discovered pre-existing bug 10c3e55c — the explore/place wiring is linker-GC'd out of the PAFSynth ELF (predates this refactor; evidence in the ergo task). --- manifold/public/nisps.js | 2 +- manifold/public/nisps.wasm | Bin 111206 -> 125610 bytes nisps/CMakeLists.txt | 1 + nisps/ml/dynamic_storage.hpp | 57 ++++-- nisps/ml/feedback.hpp | 176 +++++++++++++++++- nisps/ml/geo_push.hpp | 95 ++++++++++ nisps/ml/mlp.hpp | 36 ++++ nisps/ml/replay.hpp | 235 +++++++++++++++++++++++ nisps/wasm/bindings.cpp | 154 ++++++++++++++- scripts/build-wasm.sh | 6 + tests/cpp/parity_check.cpp | 54 +++++- tests/cpp/parity_diff.mjs | 2 +- tests/cpp/parity_wasm.mjs | 55 +++++- tests/cpp/test_mlp_feedback.cpp | 4 + tests/cpp/test_mlp_geo_dislike.cpp | 289 +++++++++++++++++++++++++++++ 15 files changed, 1136 insertions(+), 30 deletions(-) create mode 100644 nisps/ml/geo_push.hpp create mode 100644 nisps/ml/replay.hpp create mode 100644 tests/cpp/test_mlp_geo_dislike.cpp diff --git a/manifold/public/nisps.js b/manifold/public/nisps.js index 7188327..54335cd 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_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["o"])(a0);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["p"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["q"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["r"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["s"])(a0,a1);var _nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=(a0,a1,a2,a3)=>(_nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=wasmExports["t"])(a0,a1,a2,a3);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["u"])(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["v"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["w"])(a0);var _nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=a0=>(_nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=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_drag=Module["_nisps_ml_feedback_drag"]=a0=>(_nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=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_placing=Module["_nisps_ml_feedback_placing"]=a0=>(_nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=wasmExports["L"])(a0);var _nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=a0=>(_nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=wasmExports["M"])(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["N"])(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["O"])(a0,a1);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["P"])(a0,a1);var _nisps_ml_example_count=Module["_nisps_ml_example_count"]=a0=>(_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["Q"])(a0);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["R"])(a0);var _nisps_ml_reset=Module["_nisps_ml_reset"]=a0=>(_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["S"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=(a0,a1)=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["T"])(a0,a1);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["U"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["V"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["W"])(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["X"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["Z"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["_"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["$"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["aa"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["ba"])();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_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["o"])(a0);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["p"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["q"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["r"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["s"])(a0,a1);var _nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=(a0,a1,a2,a3)=>(_nisps_ml_move_weights=Module["_nisps_ml_move_weights"]=wasmExports["t"])(a0,a1,a2,a3);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["u"])(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["v"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["w"])(a0);var _nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=a0=>(_nisps_ml_feedback_learning_paused=Module["_nisps_ml_feedback_learning_paused"]=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_drag=Module["_nisps_ml_feedback_drag"]=a0=>(_nisps_ml_feedback_drag=Module["_nisps_ml_feedback_drag"]=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_placing=Module["_nisps_ml_feedback_placing"]=a0=>(_nisps_ml_feedback_placing=Module["_nisps_ml_feedback_placing"]=wasmExports["L"])(a0);var _nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=a0=>(_nisps_ml_feedback_state=Module["_nisps_ml_feedback_state"]=wasmExports["M"])(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["N"])(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["O"])(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["P"])(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["Q"])(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["R"])(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["S"])(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["T"])(a0,a1);var _nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=a0=>(_nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=wasmExports["U"])(a0);var _nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=a0=>(_nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=wasmExports["V"])(a0);var _nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=a0=>(_nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=wasmExports["W"])(a0);var _nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=a0=>(_nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=wasmExports["X"])(a0);var _nisps_ml_jolt_lr_scale=Module["_nisps_ml_jolt_lr_scale"]=a0=>(_nisps_ml_jolt_lr_scale=Module["_nisps_ml_jolt_lr_scale"]=wasmExports["Y"])(a0);var _nisps_ml_jolt_tick_lr_ramp=Module["_nisps_ml_jolt_tick_lr_ramp"]=a0=>(_nisps_ml_jolt_tick_lr_ramp=Module["_nisps_ml_jolt_tick_lr_ramp"]=wasmExports["Z"])(a0);var _nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=(a0,a1)=>(_nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=wasmExports["_"])(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["$"])(a0);var _nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=(a0,a1,a2)=>(_nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=wasmExports["aa"])(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["ba"])(a0,a1);var _nisps_ml_example_count=Module["_nisps_ml_example_count"]=a0=>(_nisps_ml_example_count=Module["_nisps_ml_example_count"]=wasmExports["ca"])(a0);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["da"])(a0);var _nisps_ml_reset=Module["_nisps_ml_reset"]=a0=>(_nisps_ml_reset=Module["_nisps_ml_reset"]=wasmExports["ea"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=(a0,a1)=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["fa"])(a0,a1);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["ga"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["ha"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["ia"])(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["ja"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["la"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["ma"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["na"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["oa"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["pa"])();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 a9a19e427ff3a877e534299397eb534222c849b9..f15c4f363266873cb0ef34c8e6f6200c9deaaa42 100755 GIT binary patch delta 25273 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 Oe+9Mg#6Q>l{{I2>)QA%R delta 11228 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 diff --git a/nisps/CMakeLists.txt b/nisps/CMakeLists.txt index daa5880..b22d424 100644 --- a/nisps/CMakeLists.txt +++ b/nisps/CMakeLists.txt @@ -60,6 +60,7 @@ if(NOT EMSCRIPTEN) ${NISPS_TEST_DIR}/test_mlp_jolt.cpp ${NISPS_TEST_DIR}/test_mlp_ou_noise.cpp ${NISPS_TEST_DIR}/test_mlp_feedback.cpp + ${NISPS_TEST_DIR}/test_mlp_geo_dislike.cpp ${NISPS_TEST_DIR}/test_mlp_serialize.cpp ) target_link_libraries(nisps_core_tests PRIVATE nisps_core) diff --git a/nisps/ml/dynamic_storage.hpp b/nisps/ml/dynamic_storage.hpp index 9d92e3e..3f465ce 100644 --- a/nisps/ml/dynamic_storage.hpp +++ b/nisps/ml/dynamic_storage.hpp @@ -203,23 +203,39 @@ class DynamicFeedbackStorage { public: DynamicFeedbackStorage(std::size_t n_out, std::size_t n_weights, - std::size_t undo_depth = 4u) noexcept - : n_out_(n_out), n_weights_(n_weights), undo_cap_(undo_depth) { - if (n_out == 0u || n_weights == 0u || undo_depth == 0u) return; - // float regions: static_out, placed_out, snapshot, scratch, undo ring + std::size_t undo_depth = 4u, + std::size_t n_in = 2u, + std::size_t replay_cap = 64u) noexcept + : n_out_(n_out), n_weights_(n_weights), undo_cap_(undo_depth), + n_in_(n_in), replay_cap_(replay_cap) { + if (n_out == 0u || n_weights == 0u || undo_depth == 0u || + n_in == 0u || replay_cap == 0u) { + return; + } + // float regions: static_out, placed_out, snapshot, scratch, undo ring, + // replay (inputs/actions/rewards), centroid + target scratch // byte region: focus mask (n_out bytes, rounded up to whole floats) const std::size_t focus_floats = (n_out + sizeof(float) - 1u) / sizeof(float); const std::size_t total = n_out * 2u // static_out + placed_out + n_weights * 2u // snapshot + scratch + n_weights * undo_depth // undo ring + + replay_cap * n_in // replay inputs + + replay_cap * n_out // replay actions + + replay_cap // replay rewards + + n_out * 2u // centroid + target + focus_floats; arena_ = new (std::nothrow) float[total](); if (!arena_) return; - off_placed_ = n_out_; - off_snap_ = off_placed_ + n_out_; - off_scratch_ = off_snap_ + n_weights_; - off_undo_ = off_scratch_ + n_weights_; - off_focus_ = off_undo_ + n_weights_ * undo_cap_; + off_placed_ = n_out_; + off_snap_ = off_placed_ + n_out_; + off_scratch_ = off_snap_ + n_weights_; + off_undo_ = off_scratch_ + n_weights_; + off_replay_in_ = off_undo_ + n_weights_ * undo_cap_; + off_replay_a_ = off_replay_in_ + replay_cap_ * n_in_; + off_replay_r_ = off_replay_a_ + replay_cap_ * n_out_; + off_centroid_ = off_replay_r_ + replay_cap_; + off_target_ = off_centroid_ + n_out_; + off_focus_ = off_target_ + n_out_; } ~DynamicFeedbackStorage() { delete[] arena_; } @@ -237,9 +253,11 @@ class DynamicFeedbackStorage { bool valid() const noexcept { return arena_ != nullptr; } - std::size_t n_out() const noexcept { return n_out_; } - std::size_t n_weights() const noexcept { return n_weights_; } - std::size_t undo_cap() const noexcept { return undo_cap_; } + std::size_t n_out() const noexcept { return n_out_; } + std::size_t n_weights() const noexcept { return n_weights_; } + std::size_t undo_cap() const noexcept { return undo_cap_; } + std::size_t n_in() const noexcept { return n_in_; } + std::size_t replay_cap() const noexcept { return replay_cap_; } std::span static_out() noexcept { return {arena_, n_out_}; } std::span static_out() const noexcept { return {arena_, n_out_}; } @@ -254,6 +272,11 @@ class DynamicFeedbackStorage { std::span undo_slot(std::size_t i) const noexcept { return {arena_ + off_undo_ + i * n_weights_, n_weights_}; } + std::span replay_inputs() noexcept { return {arena_ + off_replay_in_, replay_cap_ * n_in_}; } + std::span replay_actions() noexcept { return {arena_ + off_replay_a_, replay_cap_ * n_out_}; } + std::span replay_rewards() noexcept { return {arena_ + off_replay_r_, replay_cap_}; } + std::span centroid_buf() noexcept { return {arena_ + off_centroid_, n_out_}; } + std::span target_buf() noexcept { return {arena_ + off_target_, n_out_}; } std::span focus() noexcept { return {reinterpret_cast(arena_ + off_focus_), n_out_}; } @@ -264,15 +287,21 @@ class DynamicFeedbackStorage { private: void move_from_(DynamicFeedbackStorage& o) noexcept { n_out_ = o.n_out_; n_weights_ = o.n_weights_; undo_cap_ = o.undo_cap_; + n_in_ = o.n_in_; replay_cap_ = o.replay_cap_; off_placed_ = o.off_placed_; off_snap_ = o.off_snap_; off_scratch_ = o.off_scratch_; off_undo_ = o.off_undo_; off_focus_ = o.off_focus_; + off_replay_in_ = o.off_replay_in_; off_replay_a_ = o.off_replay_a_; + off_replay_r_ = o.off_replay_r_; off_centroid_ = o.off_centroid_; + off_target_ = o.off_target_; arena_ = o.arena_; o.arena_ = nullptr; } - std::size_t n_out_ = 0u, n_weights_ = 0u, undo_cap_ = 0u; + std::size_t n_out_ = 0u, n_weights_ = 0u, undo_cap_ = 0u, n_in_ = 0u, replay_cap_ = 0u; std::size_t off_placed_ = 0u, off_snap_ = 0u, off_scratch_ = 0u, - off_undo_ = 0u, off_focus_ = 0u; + off_undo_ = 0u, off_focus_ = 0u, off_replay_in_ = 0u, + off_replay_a_ = 0u, off_replay_r_ = 0u, off_centroid_ = 0u, + off_target_ = 0u; float* arena_ = nullptr; }; diff --git a/nisps/ml/feedback.hpp b/nisps/ml/feedback.hpp index ae11dab..a004c31 100644 --- a/nisps/ml/feedback.hpp +++ b/nisps/ml/feedback.hpp @@ -51,16 +51,27 @@ #include "../core/perf.hpp" #include "../core/rng.hpp" +#include "geo_push.hpp" +#include "replay.hpp" namespace nisps::ml { enum class FeedbackMode : std::uint8_t { - Avoid = 0, // down → move_weights (Gaussian perturb). No internal state. + Avoid = 0, // down → geometric push-away (or legacy Diffuse — see AvoidStyle). RandomiseOutputs = 1, // down → bypass MLP, hold static random vector; re-roll each down. RandomiseMlp = 2, // down → snapshot + draw_weights live net; down-again cancels. ExploreAndPlace = 3, // Idle→Exploring→Placing→Idle scratchpad lifecycle (default product mode). }; +// How the Avoid mode realises a dislike (rl-feedback-design §2.1). Geometric +// is the ported firmware behaviour (replay-backed k-NN centroid push-away); +// Diffuse is the pre-P3 undirected move_weights, kept reachable as a legacy +// sub-mode for A/B comparison. +enum class AvoidStyle : std::uint8_t { + Geometric = 0, + Diffuse = 1, +}; + // The explicit lifecycle state for FeedbackMode::ExploreAndPlace. The whole // mode is a three-state machine; granular methods drive the transitions // (firmware maps buttons to them directly), while on_down/on_up implement the @@ -97,6 +108,9 @@ enum class FeedbackAction : std::uint8_t { BeginPlace = 11, // Exploring→Placing; placed_out captured + frozen (no store yet). CommitPlace = 12, // Placing→Idle; real net restored. CALLER adds +1 (input→placed_output) + trains. CancelPlace = 13, // Placing→Exploring; backed out of placing (no store). + // ---- Geometric dislike (append-only) ---- + GeometricPush = 14, // dislike trained toward the computed push-away target. + GeometricColdStart = 15, // no positives yet: negative-LR fallback ran; UI shows the cold-start prompt. }; // --------------------------------------------------------------------------- @@ -105,16 +119,21 @@ enum class FeedbackAction : std::uint8_t { // ExploreAndPlace; each undo slot is NWeights floats. WASM historically used // depth 4, firmware 2 (per rl-feedback-design §2.2 — SRAM budget). // --------------------------------------------------------------------------- -template +template class FixedFeedbackStorage { public: static constexpr std::size_t kNOut = NOut; static constexpr std::size_t kWeights = NWeights; static constexpr std::size_t kUndoDepth = UndoDepth; + static constexpr std::size_t kNIn = NIn; + static constexpr std::size_t kReplayCap = ReplayCap; - static constexpr std::size_t n_out() noexcept { return NOut; } - static constexpr std::size_t n_weights() noexcept { return NWeights; } - static constexpr std::size_t undo_cap() noexcept { return UndoDepth; } + static constexpr std::size_t n_out() noexcept { return NOut; } + static constexpr std::size_t n_weights() noexcept { return NWeights; } + static constexpr std::size_t undo_cap() noexcept { return UndoDepth; } + static constexpr std::size_t n_in() noexcept { return NIn; } + static constexpr std::size_t replay_cap() noexcept { return ReplayCap; } NISPS_FORCE_INLINE std::span static_out() noexcept { return static_out_; } NISPS_FORCE_INLINE std::span static_out() const noexcept { return static_out_; } @@ -129,6 +148,13 @@ class FixedFeedbackStorage { NISPS_FORCE_INLINE std::span undo_slot(std::size_t i) const noexcept { return undo_ring_[i]; } + // Replay memory buffers (geometric dislike — nisps/ml/replay.hpp). + NISPS_FORCE_INLINE std::span replay_inputs() noexcept { return replay_in_; } + NISPS_FORCE_INLINE std::span replay_actions() noexcept { return replay_act_; } + NISPS_FORCE_INLINE std::span replay_rewards() noexcept { return replay_rew_; } + // Centroid + push-target scratch (n_out each). + NISPS_FORCE_INLINE std::span centroid_buf() noexcept { return centroid_; } + NISPS_FORCE_INLINE std::span target_buf() noexcept { return target_; } private: std::array static_out_{}; @@ -137,6 +163,11 @@ class FixedFeedbackStorage { std::array placed_out_{}; std::array, UndoDepth> undo_ring_{}; std::array scratch_buf_{}; + std::array replay_in_{}; + std::array replay_act_{}; + std::array replay_rew_{}; + std::array centroid_{}; + std::array target_{}; }; // --------------------------------------------------------------------------- @@ -162,6 +193,16 @@ class FeedbackControllerCore : public FbStorage { } FeedbackMode mode() const noexcept { return mode_; } + // Avoid sub-mode: Geometric (default, the ported firmware behaviour) or + // Diffuse (legacy undirected move_weights — kept for A/B comparison). + void set_avoid_style(AvoidStyle s) noexcept { avoid_style_ = s; } + AvoidStyle avoid_style() const noexcept { return avoid_style_; } + + // Base learning rate for the geometric push training (upstream + // InterfaceRL default 1e-3, pre-scaling). + void set_geo_lr(float lr) noexcept { geo_lr_ = lr; } + float geo_lr() const noexcept { return geo_lr_; } + // `exploring()` is true whenever a scratchpad net is live and learning is // paused — for the legacy RANDOMISE_* modes, AND for ExploreAndPlace in // either Exploring or Placing (the real net stays snapshotted aside the @@ -209,8 +250,11 @@ class FeedbackControllerCore : public FbStorage { std::span pin_mask) noexcept { switch (mode_) { case FeedbackMode::Avoid: - mlp.move_weights(speed, spread, pin_mask); - return FeedbackAction::AvoidPerturb; + if (avoid_style_ == AvoidStyle::Diffuse) { + mlp.move_weights(speed, spread, pin_mask); + return FeedbackAction::AvoidPerturb; + } + return dislike_geometric(mlp, current_out, geo_lr_); case FeedbackMode::RandomiseOutputs: if (!explore_active_) { enter_randomise_outputs(current_out); @@ -272,6 +316,11 @@ class FeedbackControllerCore : public FbStorage { restore_after_explore(mlp); return FeedbackAction::CommitStore; } + if (mode_ == FeedbackMode::Avoid && avoid_style_ == AvoidStyle::Geometric) { + // A geometric-mode like also feeds the positive centroid (ADR + // §2.1); the caller still runs addExample + train as usual. + store_positive(mlp); + } return FeedbackAction::LikeStore; } @@ -309,6 +358,92 @@ class FeedbackControllerCore : public FbStorage { void seed(std::uint64_t s) noexcept { rng_.seed(s); } + // ========================================================================= + // Geometric dislike (rl-feedback-design §2.1) — the press-time half and + // the async optimise() half of upstream InterfaceRL collapsed into ONE + // synchronous call (nisps has no background optimise driver). + // ========================================================================= + + std::size_t replay_size() const noexcept { return replay_count_; } + std::size_t positive_count() noexcept { return replay_().positive_count(); } + std::size_t negative_count() noexcept { return replay_().negative_count(); } + std::size_t dislike_multiplier() const noexcept { return dislike_multiplier_; } + + // Store a positive (like) into the replay so the k-NN centroid sees it. + // `current_out` may be empty ⇒ the MLP's live output vector is used. The + // input is the MLP's current input vector. + template + void store_positive(M& mlp, std::span current_out = {}) noexcept { + std::span a = current_out.empty() + ? std::span(mlp.outputs()) + : current_out; + replay_().store(1.f, std::span(mlp.input_buf()), a); + } + + // Thumbs-down at the MLP's CURRENT input with heard action `current_out` + // (empty ⇒ the MLP's live outputs). Runs the full upstream sequence: + // 1. deepen-or-store the negative (dedup radius 0.05); double the + // dislike multiplier (max 16). + // 2. cold start (no positives): train AWAY from the heard action at + // lr * 0.1 * avgRewardNeg (negative LR — upstream fallback). + // 3. else: k-NN(4) positive centroid → push-away target → train toward + // it at lr * negLRRatio, gated by the focus/solo mask. + // 4. proportional decay + eviction of expired negatives; halve the + // multiplier per expiry. + template + FeedbackAction dislike_geometric(M& mlp, std::span current_out, + float lr) noexcept { + auto replay = replay_(); + const std::size_t n_out = this->n_out(); + + std::span a_neg = current_out.empty() + ? std::span(mlp.outputs()) + : current_out; + std::span x_neg(mlp.input_buf()); + + // 1. store/deepen the negative (InterfaceRL.cpp:42-66). + replay.deepen_or_store_negative(x_neg, a_neg); + dislike_multiplier_ *= 2u; + if (dislike_multiplier_ > 16u) dislike_multiplier_ = 16u; + + const std::size_t pos_total = replay.positive_count(); + const std::size_t neg_total = replay.negative_count(); + const float avg_neg = replay.avg_negative_reward(); + + FeedbackAction action; + if (pos_total == 0u) { + // 2. cold-start fallback (InterfaceRL.cpp:746): negative-LR + // training away from the heard action; no geometric push. The + // caller shows the "like a few sounds first" prompt. + mlp.train_targets(x_neg, a_neg, lr * 0.1f * avg_neg, focus_span_()); + action = FeedbackAction::GeometricColdStart; + } else { + // 3. centroid → target → train (InterfaceRL.cpp:602-743). + auto mean = this->centroid_buf(); + const std::size_t used = + replay.knn_positive_centroid(x_neg, kCentroidK, mean); + auto target = this->target_buf(); + compute_push_target(a_neg.subspan(0, (a_neg.size() < n_out) ? a_neg.size() : n_out), + std::span(mean.data(), n_out), + focus_span_(), geo_push_step(avg_neg), rng_, target); + const float ratio = geo_neg_lr_ratio(neg_total, pos_total); + mlp.train_targets(x_neg, std::span(target.data(), n_out), + lr * ratio, focus_span_()); + (void)used; + action = FeedbackAction::GeometricPush; + } + + // 4. decay + evict; halve the multiplier per expiry, reset when no + // negatives remain (InterfaceRL.cpp:752-760). + const std::size_t expired = replay.decay_negatives(); + for (std::size_t i = 0; i < expired; ++i) { + dislike_multiplier_ = (dislike_multiplier_ > 1u) ? dislike_multiplier_ / 2u : 1u; + } + if (expired > 0u && replay.negative_count() == 0u) dislike_multiplier_ = 1u; + + return action; + } + // ========================================================================= // ExploreAndPlace — granular lifecycle methods (firmware maps buttons to // these directly; on_down/on_up call them for the browser default policy). @@ -491,6 +626,20 @@ class FeedbackControllerCore : public FbStorage { } private: + // The replay view over the storage-owned buffers. + ReplayView replay_() noexcept { + return ReplayView(this->replay_inputs(), this->replay_actions(), + this->replay_rewards(), this->n_in(), this->n_out(), + this->replay_cap(), replay_count_); + } + + // The focus mask as the geometric active-dims gate (empty ⇒ all active). + std::span focus_span_() const noexcept { + if (focus_count_ == 0u) return {}; + const auto focus = this->focus(); + return focus.subspan(0, focus_count_); + } + void capture_placed(std::span src) noexcept { auto placed = this->placed_out(); const std::size_t n = (src.size() < placed.size()) ? src.size() : placed.size(); @@ -608,10 +757,16 @@ class FeedbackControllerCore : public FbStorage { } FeedbackMode mode_ = FeedbackMode::Avoid; + AvoidStyle avoid_style_ = AvoidStyle::Geometric; bool explore_active_ = false; bool learning_paused_ = false; std::size_t focus_count_ = 0; // 0 ⇒ all active + // ---- Geometric dislike state --------------------------------------------- + std::size_t replay_count_ = 0u; + std::size_t dislike_multiplier_ = 1u; + float geo_lr_ = 0.001f; // upstream InterfaceRL.hpp:312 + // ---- ExploreAndPlace state ---------------------------------------------- ExploreState ep_state_ = ExploreState::Idle; bool last_placed_valid_ = false; @@ -624,8 +779,11 @@ class FeedbackControllerCore : public FbStorage { // The classic fixed-size controller over a compile-time MLP type — the // firmware model and the default for tests. Sizes derive from the MLP. -template +// ReplayCap 32 is the firmware SRAM-budget default (rl-feedback-design §4); +// the browser's DynamicFeedbackStorage uses 64. +template using FeedbackController = FeedbackControllerCore< - FixedFeedbackStorage>; + FixedFeedbackStorage>; } // namespace nisps::ml diff --git a/nisps/ml/geo_push.hpp b/nisps/ml/geo_push.hpp new file mode 100644 index 0000000..364aa0e --- /dev/null +++ b/nisps/ml/geo_push.hpp @@ -0,0 +1,95 @@ +// nisps/ml/geo_push.hpp — geometric push-away target computation for the +// dislike gesture (docs/adr/rl-feedback-design.md §2.1/§4). +// +// Verbatim port of the per-negative target computation in upstream +// InterfaceRL.cpp:713-735 (memllib @ 0a541cc): +// +// pushStep = clamp(|avgRewardNeg|, 0.25, 1.0) * kGeometricPushScale +// dir[j] = neg_action[j] - meanPositiveAction[j] +// len = ||dir|| +// useRandom = (len <= 1e-4) (disliked ON the centroid) +// effectivePushStep = pushStep / (1 + len) (taper for far items) +// d = useRandom ? random ∈ [-1,1] : dir[j] / len +// target[j] = clamp(neg_action[j] + d * effectivePushStep, 0, 1) +// inactive dims keep neg_action[j] +// +// SINGLE DELIBERATE FIRMWARE DIVERGENCE (recorded in ALIGNMENT.md): the +// upstream `useRandom` branch draws libc `rand() & 0xFF`; we draw from the +// caller's deterministic per-instance `nisps::Rng` so native == WASM parity +// holds. The branch only fires when a disliked action sits exactly on the +// centroid. +// +// Pure free functions over spans — no replay/centroid logic in the MLP +// kernel, no state, no heap (Anchor-First graft, ADR §0). + +#pragma once + +#include +#include +#include +#include + +#include "../core/perf.hpp" +#include "../core/rng.hpp" + +namespace nisps::ml { + +// Upstream InterfaceRL.hpp:293. +inline constexpr float kGeometricPushScale = 0.5f; + +// pushStep from the mean negative reward (InterfaceRL.cpp:713). +inline float geo_push_step(float avg_reward_neg) noexcept { + float mag = (avg_reward_neg < 0.f) ? -avg_reward_neg : avg_reward_neg; + if (mag < 0.25f) mag = 0.25f; + if (mag > 1.0f) mag = 1.0f; + return mag * kGeometricPushScale; +} + +// Compute the push-away target for ONE disliked action. `active_mask` +// (1 = active) gates which dims move — empty ⇒ all active (this is the solo/ +// focus mask; upstream `activeDims_`). Writes n_out floats into `target`. +inline void compute_push_target(std::span neg_action, + std::span mean_positive, + std::span active_mask, + float push_step, + Rng& rng, + std::span target) noexcept { + const std::size_t n = neg_action.size(); + + float len_sq = 0.f; + for (std::size_t j = 0; j < n; ++j) { + const float d = neg_action[j] - mean_positive[j]; + len_sq += d * d; + } + const float len = std::sqrt(len_sq); + const bool use_random = (len <= 1e-4f); + const float effective = push_step / (1.0f + len); + + for (std::size_t j = 0; j < n; ++j) { + const bool active = + active_mask.empty() || (j < active_mask.size() && active_mask[j] != 0u); + if (!active) { + target[j] = neg_action[j]; + continue; + } + const float d = use_random + ? rng.next_float_signed() + : ((neg_action[j] - mean_positive[j]) / len); + float t = neg_action[j] + d * effective; + if (t < 0.f) t = 0.f; + if (t > 1.f) t = 1.f; + target[j] = t; + } +} + +// Dynamic LR ratio (InterfaceRL.cpp:742-743): push harder when dislikes are +// rare, gentler when they flood the buffer. +inline float geo_neg_lr_ratio(std::size_t neg_count, std::size_t pos_count) noexcept { + const std::size_t total = neg_count + pos_count; + const float neg_fraction = (total > 0u) + ? static_cast(neg_count) / static_cast(total) + : 0.f; + return 0.5f - 0.4f * neg_fraction; +} + +} // namespace nisps::ml diff --git a/nisps/ml/mlp.hpp b/nisps/ml/mlp.hpp index e1306a9..eaba105 100644 --- a/nisps/ml/mlp.hpp +++ b/nisps/ml/mlp.hpp @@ -209,6 +209,42 @@ class MLPCore : public Storage { return epoch_loss; } + // Train ONE step toward a COMPUTED target vector (not a stored label) — + // the geometric-dislike hook (docs/adr/rl-feedback-design.md §4). The + // dataset is untouched. A negative `lr` trains AWAY from the target (the + // upstream cold-start fallback). `out_mask` (1 = active) zeroes the + // loss-derivative of inactive output dims before backprop — the solo/ + // focus gate; empty ⇒ all active. Returns the sample loss. + float train_targets(std::span input, + std::span target, + float lr, + std::span out_mask = {}) noexcept { + if (!storage_ok_()) return 0.f; + const std::size_t n_out = this->n_out(); + if (input.size() < this->n_in() || target.size() < n_out) return 0.f; + + forward_(input); + + auto deriv = this->template eval_act_l<3u>(); + const float loss = mse_per_sample( + target, + std::span(this->template act_l<3u>()), + deriv); + if (!out_mask.empty()) { + for (std::size_t j = 0; j < n_out; ++j) { + const bool active = (j < out_mask.size() && out_mask[j] != 0u); + if (!active) deriv[j] = 0.f; + } + } + + backprop_(input, deriv, 1.f); + apply_grad_<3u>(lr); + apply_grad_<2u>(lr); + apply_grad_<1u>(lr); + apply_grad_<0u>(lr); + return loss; + } + // --------------------------------------------------------------- // RL ops (concept: move_weights / draw_weights) // --------------------------------------------------------------- diff --git a/nisps/ml/replay.hpp b/nisps/ml/replay.hpp new file mode 100644 index 0000000..1aeb059 --- /dev/null +++ b/nisps/ml/replay.hpp @@ -0,0 +1,235 @@ +// nisps/ml/replay.hpp — reward-tagged replay memory algorithms for the +// geometric-dislike feedback mode (docs/adr/rl-feedback-design.md §4). +// +// Ported from upstream InterfaceRL (memllib @ 0a541cc "highlighting"): +// * `_perform_dislike_action()` (InterfaceRL.cpp:42-66) — nearby-negative +// deepening within Euclidean 0.05, else store reward=-1. +// * `optimise()` k-NN positive centroid (InterfaceRL.cpp:602-627). +// * proportional negative decay + eviction (InterfaceRL.cpp:664, :752-760). +// +// STORAGE: the buffers live in the feedback controller's storage policy +// (nisps/ml/feedback.hpp — fixed std::array on firmware, arena slice in the +// browser). `ReplayView` binds those spans plus the live item count and +// carries the algorithms — no ownership, no heap, deterministic. +// +// DETERMINISM (the classic float-sum parity traps, per the ADR): +// * k-NN selection uses a fixed-size insertion into k slots with ties +// broken by LOWER index (no std::sort, no heap). +// * centroid accumulation runs in slot order (nearest first) — a fixed +// summation order so native == WASM bitwise. +// * eviction compacts in place preserving insertion order. + +#pragma once + +#include +#include +#include + +#include "../core/perf.hpp" + +namespace nisps::ml { + +// Upstream constants (InterfaceRL.hpp:293-296, .cpp:42-66,664). +inline constexpr float kReplayDedupRadius = 0.05f; +inline constexpr float kReplayDecayStep = 0.0025f; +inline constexpr float kReplayEvictThreshold = -0.01f; +inline constexpr float kMaxDislikeMagnitude = 16.f; +inline constexpr std::size_t kCentroidK = 4u; + +// A non-owning view over the replay buffers (inputs: cap×n_in, actions: +// cap×n_out, rewards: cap) plus the live count. All methods deterministic, +// allocation-free. +class ReplayView { + public: + ReplayView(std::span inputs, std::span actions, + std::span rewards, std::size_t n_in, std::size_t n_out, + std::size_t cap, std::size_t& count) noexcept + : inputs_(inputs), actions_(actions), rewards_(rewards), + n_in_(n_in), n_out_(n_out), cap_(cap), count_(count) {} + + std::size_t size() const noexcept { return count_; } + std::size_t capacity() const noexcept { return cap_; } + + std::span input(std::size_t i) const noexcept { + return inputs_.subspan(i * n_in_, n_in_); + } + std::span action(std::size_t i) const noexcept { + return actions_.subspan(i * n_out_, n_out_); + } + float reward(std::size_t i) const noexcept { return rewards_[i]; } + + std::size_t positive_count() const noexcept { + std::size_t n = 0u; + for (std::size_t i = 0; i < count_; ++i) { + if (rewards_[i] > 0.f) ++n; + } + return n; + } + std::size_t negative_count() const noexcept { + std::size_t n = 0u; + for (std::size_t i = 0; i < count_; ++i) { + if (rewards_[i] <= 0.f) ++n; + } + return n; + } + // Mean reward across negatives (≤ 0); 0 when there are none. Fixed + // accumulation order (insertion order). + float avg_negative_reward() const noexcept { + float sum = 0.f; + std::size_t n = 0u; + for (std::size_t i = 0; i < count_; ++i) { + if (rewards_[i] <= 0.f) { + sum += rewards_[i]; + ++n; + } + } + return (n > 0u) ? (sum / static_cast(n)) : 0.f; + } + + // Store an item. When full, the OLDEST item is evicted (shift-down — + // deterministic, preserves relative order). + void store(float reward, std::span x, std::span a) noexcept { + std::size_t slot; + if (count_ < cap_) { + slot = count_++; + } else { + evict_(0u); + slot = count_++; + } + write_(slot, reward, x, a); + } + + // Upstream `_perform_dislike_action` core: a negative within Euclidean + // `radius` of x has its reward deepened (clamped at -kMaxDislikeMagnitude) + // and its ACTION REFRESHED to the latest heard vector; otherwise a new + // reward=-1 item is stored. Returns true when an existing item deepened. + bool deepen_or_store_negative(std::span x, std::span a, + float radius = kReplayDedupRadius) noexcept { + for (std::size_t i = 0; i < count_; ++i) { + if (rewards_[i] < 0.f && distance_(i, x) < radius) { + float r = rewards_[i] - 1.f; + if (r < -kMaxDislikeMagnitude) r = -kMaxDislikeMagnitude; + rewards_[i] = r; + auto act = actions_.subspan(i * n_out_, n_out_); + const std::size_t n = (a.size() < n_out_) ? a.size() : n_out_; + for (std::size_t j = 0; j < n; ++j) act[j] = a[j]; + return true; + } + } + store(-1.f, x, a); + return false; + } + + // k-NN positive centroid (InterfaceRL.cpp:602-627): mean action of the k + // positives nearest to x. Writes into `mean` (n_out floats) and returns + // the number of positives used (0 ⇒ cold start; `mean` untouched). + // Deterministic: fixed k-slot insertion, ties keep the LOWER index; + // accumulation in slot order. + std::size_t knn_positive_centroid(std::span x, std::size_t k, + std::span mean) const noexcept { + constexpr std::size_t kMaxK = 8u; + if (k > kMaxK) k = kMaxK; + float best_d[kMaxK]; + std::size_t best_i[kMaxK]; + std::size_t used = 0u; + + for (std::size_t i = 0; i < count_; ++i) { + if (rewards_[i] <= 0.f) continue; + const float d = distance_(i, x); + // Insertion: strictly-less displaces, so equal distances keep the + // earlier (lower-index) item. + std::size_t pos = used; + while (pos > 0u && d < best_d[pos - 1u]) --pos; + if (pos >= k) continue; + const std::size_t tail = (used < k) ? used : (k - 1u); + for (std::size_t m = tail; m > pos; --m) { + best_d[m] = best_d[m - 1u]; + best_i[m] = best_i[m - 1u]; + } + best_d[pos] = d; + best_i[pos] = i; + if (used < k) ++used; + } + if (used == 0u) return 0u; + + for (std::size_t j = 0; j < n_out_; ++j) mean[j] = 0.f; + for (std::size_t s = 0; s < used; ++s) { + const auto act = action(best_i[s]); + for (std::size_t j = 0; j < n_out_; ++j) mean[j] += act[j]; + } + const float inv = 1.f / static_cast(used); + for (std::size_t j = 0; j < n_out_; ++j) mean[j] *= inv; + return used; + } + + // Proportional decay of every negative (`reward += 0.0025 * max(|r|, 1)`) + // and in-place eviction of items decayed past -0.01. Returns the number + // evicted (the caller halves its dislike multiplier per expiry, matching + // upstream InterfaceRL.cpp:752-760). + std::size_t decay_negatives() noexcept { + std::size_t evicted = 0u; + std::size_t i = 0u; + while (i < count_) { + if (rewards_[i] <= 0.f) { + const float mag = (rewards_[i] < 0.f) ? -rewards_[i] : rewards_[i]; + rewards_[i] += kReplayDecayStep * ((mag > 1.f) ? mag : 1.f); + if (rewards_[i] > kReplayEvictThreshold) { + evict_(i); + ++evicted; + continue; // same index now holds the next item + } + } + ++i; + } + return evicted; + } + + void clear() noexcept { count_ = 0u; } + + private: + float distance_(std::size_t i, std::span x) const noexcept { + const auto in = input(i); + const std::size_t n = (x.size() < n_in_) ? x.size() : n_in_; + float acc = 0.f; + for (std::size_t j = 0; j < n; ++j) { + const float d = in[j] - x[j]; + acc += d * d; + } + return std::sqrt(acc); + } + + void write_(std::size_t slot, float reward, std::span x, + std::span a) noexcept { + auto in = inputs_.subspan(slot * n_in_, n_in_); + auto act = actions_.subspan(slot * n_out_, n_out_); + const std::size_t nx = (x.size() < n_in_) ? x.size() : n_in_; + const std::size_t na = (a.size() < n_out_) ? a.size() : n_out_; + for (std::size_t j = 0; j < n_in_; ++j) in[j] = (j < nx) ? x[j] : 0.f; + for (std::size_t j = 0; j < n_out_; ++j) act[j] = (j < na) ? a[j] : 0.f; + rewards_[slot] = reward; + } + + // Remove item i, shifting everything after it down one slot. + void evict_(std::size_t i) noexcept { + for (std::size_t m = i + 1u; m < count_; ++m) { + auto dst_in = inputs_.subspan((m - 1u) * n_in_, n_in_); + auto src_in = inputs_.subspan(m * n_in_, n_in_); + for (std::size_t j = 0; j < n_in_; ++j) dst_in[j] = src_in[j]; + auto dst_act = actions_.subspan((m - 1u) * n_out_, n_out_); + auto src_act = actions_.subspan(m * n_out_, n_out_); + for (std::size_t j = 0; j < n_out_; ++j) dst_act[j] = src_act[j]; + rewards_[m - 1u] = rewards_[m]; + } + --count_; + } + + std::span inputs_; + std::span actions_; + std::span rewards_; + std::size_t n_in_; + std::size_t n_out_; + std::size_t cap_; + std::size_t& count_; +}; + +} // namespace nisps::ml diff --git a/nisps/wasm/bindings.cpp b/nisps/wasm/bindings.cpp index f5dea44..7bea365 100644 --- a/nisps/wasm/bindings.cpp +++ b/nisps/wasm/bindings.cpp @@ -65,7 +65,9 @@ #include "../core/types.hpp" #include "../ml/dynamic_storage.hpp" #include "../ml/feedback.hpp" +#include "../ml/jolt.hpp" #include "../ml/mlp.hpp" +#include "../ml/ou_noise.hpp" #include "../ml/stats.hpp" #include "../ml/warm_start.hpp" @@ -89,12 +91,18 @@ constexpr std::size_t kDefaultOutputs = 126u; constexpr std::size_t kMaxDim = 4096u; constexpr std::uint64_t kFeedbackSalt = 0xFEEDBACC0DEull; +// Distinct salts keep the jolt/OU RNG streams independent of the MLP's and +// the feedback controller's (mirrors the firmware ModeBase seeding). +constexpr std::uint64_t kJoltSalt = 0xB01DFACEull; +constexpr std::uint64_t kOUSalt = 0x0DDBA11ull; using BrowserMLP = nisps::ml::MLPCore; using BrowserFeedback = nisps::ml::FeedbackControllerCore; constexpr std::size_t kFeedbackUndoDepth = 4u; +// Browser replay capacity (rl-feedback-design §4: WASM 64, firmware 32). +constexpr std::size_t kFeedbackReplayCap = 64u; struct MlDims { std::size_t n_in; @@ -145,15 +153,26 @@ struct MLHandle { std::array stats_scratch{}; // Static-output buffer for the RandomiseOutputs bypass path. std::vector feedback_static_scratch; + // Jolt (held weight morph) + OU exploration noise — the P3 gesture + // engines, same code the firmware ModeBase runs. Jolt operates on the + // flat weight buffer via jolt_scratch; OU state is over-provisioned to + // kMaxDim and applies to the first n_out entries. + nisps::ml::Jolt jolt; + nisps::ml::OUNoise ou; + std::vector jolt_scratch; // infer_batch cap; callers must split larger requests. static constexpr std::size_t kMaxBatch = 4096u; MLHandle(std::uint64_t seed, const MlDims& d) noexcept : seed64(seed), mlp(seed, d.n_in, std::span(d.hidden, 3u), d.n_out), - feedback(seed ^ kFeedbackSalt, d.n_out, mlp.weight_count(), kFeedbackUndoDepth), + feedback(seed ^ kFeedbackSalt, d.n_out, mlp.weight_count(), kFeedbackUndoDepth, + d.n_in, kFeedbackReplayCap), output_scratch(d.n_out, 0.f), - feedback_static_scratch(d.n_out, 0.f) {} + feedback_static_scratch(d.n_out, 0.f), + jolt(seed ^ kJoltSalt), + ou(seed ^ kOUSalt), + jolt_scratch(mlp.weight_count(), 0.f) {} bool valid() const noexcept { return mlp.valid() && feedback.valid(); } std::size_t n_in() const noexcept { return mlp.n_in(); } @@ -372,13 +391,16 @@ int nisps_ml_reshape(void* ml, int input_size, int output_size, nisps::ml::warm_start_copy(fresh, h->mlp); BrowserFeedback fb(h->seed64 ^ kFeedbackSalt, d.n_out, fresh.weight_count(), - kFeedbackUndoDepth); + kFeedbackUndoDepth, d.n_in, kFeedbackReplayCap); if (!fb.valid()) return 0; h->mlp = static_cast(fresh); h->feedback = static_cast(fb); h->output_scratch.assign(d.n_out, 0.f); h->feedback_static_scratch.assign(d.n_out, 0.f); + h->jolt.release(); + h->ou.reset(); + h->jolt_scratch.assign(h->mlp.weight_count(), 0.f); return 1; } @@ -737,6 +759,132 @@ int nisps_ml_feedback_placed_output(void* ml, float* out) { return 1; } +// --------------------------------------------------------------------------- +// ML feedback — geometric dislike (one-core-engine P3; rl-feedback-design +// §2.1). The Avoid mode's default realisation. current_out may be null (the +// MLP's live output is used — note the zero-derivative caveat: pass the +// HEARD post-pipeline vector for an audible push). lr <= 0 uses the +// controller default (1e-3, upstream). +// --------------------------------------------------------------------------- + +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_dislike_geometric(void* ml, const float* current_out, float lr) { + if (!ml) return 0; + auto* h = static_cast(ml); + std::span out; + if (current_out) out = std::span(current_out, h->n_out()); + const float use_lr = (lr > 0.f) ? lr : h->feedback.geo_lr(); + return static_cast(h->feedback.dislike_geometric(h->mlp, out, use_lr)); +} + +// Store a positive (like) into the replay memory so the k-NN centroid sees +// it. current_out may be null (live output used). The caller still runs its +// usual addExample + train. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_store_positive(void* ml, const float* current_out) { + if (!ml) return; + auto* h = static_cast(ml); + std::span out; + if (current_out) out = std::span(current_out, h->n_out()); + h->feedback.store_positive(h->mlp, out); +} + +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_positive_count(void* ml) { + if (!ml) return 0; + return static_cast(static_cast(ml)->feedback.positive_count()); +} + +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_negative_count(void* ml) { + if (!ml) return 0; + return static_cast(static_cast(ml)->feedback.negative_count()); +} + +// Avoid sub-mode: 0 = Geometric (default), 1 = Diffuse (legacy move_weights, +// kept for A/B comparison). +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_set_avoid_style(void* ml, int style) { + if (!ml) return; + static_cast(ml)->feedback.set_avoid_style( + style == 1 ? nisps::ml::AvoidStyle::Diffuse : nisps::ml::AvoidStyle::Geometric); +} + +// --------------------------------------------------------------------------- +// Jolt (held weight morph) + OU exploration noise (one-core-engine P3.2) — +// the same nisps/ml/{jolt,ou_noise}.hpp the firmware ModeBase runs. +// --------------------------------------------------------------------------- + +EMSCRIPTEN_KEEPALIVE +void nisps_ml_jolt_press(void* ml) { + if (!ml) return; + auto* h = static_cast(ml); + h->jolt.press(h->mlp.weight_count()); +} + +// One ~200 Hz morph tick while held (no-op when inactive): reads the flat +// weights, glides the jolt-selected few, writes them back. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_jolt_step(void* ml) { + if (!ml) return; + auto* h = static_cast(ml); + if (!h->jolt.active()) return; + auto w = h->mlp.get_weights(); + for (std::size_t i = 0; i < w.size(); ++i) h->jolt_scratch[i] = w[i]; + h->jolt.step(std::span(h->jolt_scratch.data(), w.size())); + h->mlp.set_weights(std::span(h->jolt_scratch.data(), w.size())); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_ml_jolt_release(void* ml) { + if (!ml) return; + static_cast(ml)->jolt.release(); +} + +EMSCRIPTEN_KEEPALIVE +int nisps_ml_jolt_active(void* ml) { + if (!ml) return 0; + return static_cast(ml)->jolt.active() ? 1 : 0; +} + +// Post-release learning-rate ramp: multiply the training LR by this (0 while +// held, ramps back to 1 over ~5 s of ticks). +EMSCRIPTEN_KEEPALIVE +float nisps_ml_jolt_lr_scale(void* ml) { + if (!ml) return 1.f; + return static_cast(ml)->jolt.lr_scale(); +} + +EMSCRIPTEN_KEEPALIVE +void nisps_ml_jolt_tick_lr_ramp(void* ml) { + if (!ml) return; + static_cast(ml)->jolt.tick_lr_ramp(); +} + +// Exploration amount in [0,1]; 0 disables (inert — parity-safe). +EMSCRIPTEN_KEEPALIVE +void nisps_ml_explore_intensity(void* ml, float level) { + if (!ml) return; + static_cast(ml)->ou.set_intensity(level); +} + +EMSCRIPTEN_KEEPALIVE +float nisps_ml_explore_get_intensity(void* ml) { + if (!ml) return 0.f; + return static_cast(ml)->ou.intensity(); +} + +// Advance the OU walk and add it (clamped to [0,1]) to `inout` (n floats, +// capped at the instance's n_out). No-op at intensity 0. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_explore_apply(void* ml, float* inout, int n) { + if (!ml || !inout || n <= 0) return; + auto* h = static_cast(ml); + std::size_t count = static_cast(n); + if (count > h->n_out()) count = h->n_out(); + h->ou.apply(std::span(inout, count)); +} + EMSCRIPTEN_KEEPALIVE void nisps_ml_get_layer_stats(void* ml, float* out_stats) { if (!ml || !out_stats) return; diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 47311a7..80d9d29 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -47,6 +47,12 @@ EXPORTED_FUNCS='[ "_nisps_ml_feedback_like","_nisps_ml_feedback_commit_place","_nisps_ml_feedback_cancel_place", "_nisps_ml_feedback_placing","_nisps_ml_feedback_state","_nisps_ml_feedback_undo_depth", "_nisps_ml_feedback_placed_output", + "_nisps_ml_feedback_dislike_geometric","_nisps_ml_feedback_store_positive", + "_nisps_ml_feedback_positive_count","_nisps_ml_feedback_negative_count", + "_nisps_ml_feedback_set_avoid_style", + "_nisps_ml_jolt_press","_nisps_ml_jolt_step","_nisps_ml_jolt_release", + "_nisps_ml_jolt_active","_nisps_ml_jolt_lr_scale","_nisps_ml_jolt_tick_lr_ramp", + "_nisps_ml_explore_intensity","_nisps_ml_explore_get_intensity","_nisps_ml_explore_apply", "_nisps_ml_get_layer_stats","_nisps_ml_describe", "_nisps_engine_create","_nisps_engine_destroy", "_nisps_engine_set_params","_nisps_engine_process_block" diff --git a/tests/cpp/parity_check.cpp b/tests/cpp/parity_check.cpp index 82da3c3..2b93e85 100644 --- a/tests/cpp/parity_check.cpp +++ b/tests/cpp/parity_check.cpp @@ -83,7 +83,7 @@ constexpr std::array kProbeIdx = { }; constexpr std::uint32_t kMagic = 0x5450524Eu; // 'NPRT' -constexpr std::uint32_t kVersion = 3u; // v3 adds stage 5d (ExploreAndPlace lifecycle) +constexpr std::uint32_t kVersion = 4u; // v4 adds stage 6 (geometric dislike) // Must match the salt in nisps/wasm/bindings.cpp MLHandle so the controller's // static-output RNG stream is identical native ↔ WASM. @@ -270,6 +270,58 @@ int main(int argc, char** argv) { const auto v = fb.committed_output(); for (std::size_t i = 0; i < 126u; ++i) payload.push_back(i < v.size() ? v[i] : 0.f); } + + // ---- Stage 6: geometric dislike (one-core-engine P3) ---- + // Scripted feedback session — likes at two corners feed the replay + // positives (via the Avoid+Geometric on_up path), then two dislikes at + // a probed input: the first stores the negative and trains toward the + // computed push-away target; the second deepens and pushes again. The + // weight trajectory must match native↔WASM within 1e-5 (the useRandom + // branch never fires here; the controller Rng is untouched). + fb.set_mode(nisps::ml::FeedbackMode::Avoid, mlp); + + auto like_at = [&](float x, float y) { + mlp.set_input(0u, x); + mlp.set_input(1u, y); + mlp.process(); + fb.on_up(mlp); // Avoid+Geometric: store_positive + LikeStore + }; + like_at(0.2f, 0.2f); + like_at(0.8f, 0.8f); + + auto dislike_at = [&](float x, float y) { + mlp.set_input(0u, x); + mlp.set_input(1u, y); + mlp.process(); + // The "heard" vector deliberately differs from the raw output + // (the browser passes the post-pipeline vector) so the push + // trains meaningfully. f32 arithmetic mirrored in parity_wasm.mjs + // via Math.fround. + std::array heard{}; + const auto outs = mlp.outputs(); + for (std::size_t j = 0; j < 126u; ++j) { + float v = outs[j] + (((j & 1u) != 0u) ? -0.15f : 0.15f); + if (v < 0.f) v = 0.f; + if (v > 1.f) v = 1.f; + heard[j] = v; + } + fb.on_down(mlp, heard, 0.1f, 0.5f, no_mask); + }; + dislike_at(0.25f, 0.75f); + dislike_at(0.26f, 0.74f); // within dedup radius → deepen + push + + payload.push_back(static_cast(fb.positive_count())); + payload.push_back(static_cast(fb.negative_count())); + + mlp.set_input(0u, kInputX); + mlp.set_input(1u, kInputY); + mlp.process(); + { + const auto outs = mlp.outputs(); + push_floats(payload, std::span(outs.data(), 126u)); + const auto w = mlp.get_weights(); + for (std::size_t idx : kProbeIdx) payload.push_back(idx < w.size() ? w[idx] : 0.f); + } } // ---- Sanity: every value finite ---- diff --git a/tests/cpp/parity_diff.mjs b/tests/cpp/parity_diff.mjs index c2cc6e6..b843a47 100644 --- a/tests/cpp/parity_diff.mjs +++ b/tests/cpp/parity_diff.mjs @@ -16,7 +16,7 @@ import { readFile } from 'node:fs/promises'; const MAGIC = 0x5450524e; -const VERSION = 3; +const VERSION = 4; // v4 adds stage 6 (geometric dislike) const DEFAULT_TOL = 1e-5; // Layout for context in error messages — must match parity_check.cpp / parity_wasm.mjs. diff --git a/tests/cpp/parity_wasm.mjs b/tests/cpp/parity_wasm.mjs index 6ae21b2..b0cc675 100644 --- a/tests/cpp/parity_wasm.mjs +++ b/tests/cpp/parity_wasm.mjs @@ -38,7 +38,7 @@ const __dirname = dirname(__filename); const repoRoot = resolve(__dirname, '..', '..'); const MAGIC = 0x5450524e; // 'NPRT' -const VERSION = 3; // v3 adds stage 5d (ExploreAndPlace lifecycle) +const VERSION = 4; // v4 adds stage 6 (geometric dislike) const SEED = 42 >>> 0; const INPUT_X = 0.25; @@ -112,6 +112,8 @@ function bind(Module) { feedbackLike: cwrap('nisps_ml_feedback_like', null, ['number']), feedbackCommitPlace: cwrap('nisps_ml_feedback_commit_place', null, ['number']), feedbackPlacedOutput: cwrap('nisps_ml_feedback_placed_output', 'number', ['number','number']), + feedbackPositiveCount: cwrap('nisps_ml_feedback_positive_count', 'number', ['number']), + feedbackNegativeCount: cwrap('nisps_ml_feedback_negative_count', 'number', ['number']), describe: cwrap('nisps_ml_describe', null, ['number','number']), engineCreate: cwrap('nisps_engine_create', 'number', ['string','number']), @@ -324,6 +326,57 @@ async function main() { api.free(committedBuf); } + // --- Stage 6: geometric dislike (one-core-engine P3) --- + // Mirrors parity_check.cpp stage 6: two likes feed the replay positives via + // the Avoid+Geometric on_up path, then two dislikes (second deepens) train + // toward the computed push-away target. f32 arithmetic for the "heard" + // vector via Math.fround to match native float ops exactly. + const FB_AVOID = 0; + api.feedbackSetMode(ml, FB_AVOID); + + const likeAt = (x, y) => { + api.setInput(ml, 0, x); + api.setInput(ml, 1, y); + api.process(ml); + api.feedbackUp(ml); // Avoid+Geometric: store_positive + LikeStore + }; + likeAt(0.2, 0.2); + likeAt(0.8, 0.8); + + const POS_DELTA = Math.fround(0.15); + const dislikeAt = (x, y) => { + api.setInput(ml, 0, x); + api.setInput(ml, 1, y); + api.process(ml); + const outs = getOutputsCopy(api, ml, N_OUT); + const heard = new Float32Array(N_OUT); + for (let j = 0; j < N_OUT; j++) { + let v = Math.fround(outs[j] + ((j & 1) !== 0 ? -POS_DELTA : POS_DELTA)); + if (v < 0) v = 0; + if (v > 1) v = 1; + heard[j] = v; + } + const heardBuf = api.malloc(N_OUT * 4); + new Float32Array(api.HEAPF32.buffer, heardBuf, N_OUT).set(heard); + api.feedbackDown(ml, heardBuf, 0.1, 0.5, 0); + api.free(heardBuf); + }; + dislikeAt(0.25, 0.75); + dislikeAt(0.26, 0.74); // within dedup radius: deepen + push + + feedbackFloats.push(api.feedbackPositiveCount(ml)); + feedbackFloats.push(api.feedbackNegativeCount(ml)); + + api.setInput(ml, 0, INPUT_X); + api.setInput(ml, 1, INPUT_Y); + api.process(ml); + { + const outs = getOutputsCopy(api, ml, N_OUT); + for (const v of outs) feedbackFloats.push(v); + const w = getWeightsCopy(api, ml); + for (const idx of PROBE_IDX) feedbackFloats.push(idx < w.length ? w[idx] : 0); + } + api.destroy(ml); // --- Build payload, write blob --- diff --git a/tests/cpp/test_mlp_feedback.cpp b/tests/cpp/test_mlp_feedback.cpp index debc7e4..d029453 100644 --- a/tests/cpp/test_mlp_feedback.cpp +++ b/tests/cpp/test_mlp_feedback.cpp @@ -57,6 +57,10 @@ NISPS_TEST(feedback_avoid_routes_to_move_weights) { SmallMLP m(99ull); m.draw_weights(0.5f); FB fb(7ull); // default mode is Avoid + // Since one-core-engine P3 the Avoid default is the GEOMETRIC push + // (rl-feedback-design §2.1); the undirected move_weights survives as the + // legacy Diffuse sub-mode, pinned here. + fb.set_avoid_style(nisps::ml::AvoidStyle::Diffuse); const auto before = snapshot_weights(m); const FeedbackAction a = fb.on_down(m, kNoOut, 0.1f, 0.5f, kNoMask); diff --git a/tests/cpp/test_mlp_geo_dislike.cpp b/tests/cpp/test_mlp_geo_dislike.cpp new file mode 100644 index 0000000..82994f5 --- /dev/null +++ b/tests/cpp/test_mlp_geo_dislike.cpp @@ -0,0 +1,289 @@ +// tests/cpp/test_mlp_geo_dislike.cpp — geometric dislike (one-core-engine P3; +// docs/adr/rl-feedback-design.md §2.1/§4/§6.1). +// +// Covers: replay dedup/deepen at radius 0.05, k-NN centroid selection with +// deterministic index tie-break, push direction sign (target moves AWAY from +// the liked centroid), taper, cold-start posMemCount==0 fallback, decay/ +// eviction + dislike-multiplier bookkeeping, solo/focus gating, and fixed-seed +// determinism. + +#include +#include +#include +#include + +#include "../../nisps/ml/feedback.hpp" +#include "../../nisps/ml/geo_push.hpp" +#include "../../nisps/ml/mlp.hpp" +#include "../../nisps/ml/replay.hpp" +#include "test_helpers.hpp" + +namespace { + +using nisps::ml::AvoidStyle; +using nisps::ml::FeedbackAction; +using nisps::ml::FeedbackMode; +using nisps::ml::ReplayView; + +using GeoMLP = nisps::ml::MLP<2u, 4u, 4u, 4u, 6u, 8u, 32u>; +using GeoFB = nisps::ml::FeedbackController; + +constexpr std::size_t kNIn = 2u; +constexpr std::size_t kNOut = 6u; +constexpr std::size_t kCap = 16u; + +struct RawReplay { + std::array in{}; + std::array act{}; + std::array rew{}; + std::size_t count = 0u; + + ReplayView view() { + return ReplayView(in, act, rew, kNIn, kNOut, kCap, count); + } +}; + +void set_inputs(GeoMLP& m, float x, float y) { + m.set_input(0u, x); + m.set_input(1u, y); +} + +} // namespace + +// -- ReplayView primitives ---------------------------------------------------- + +NISPS_TEST(replay_deepen_within_radius_else_store) { + RawReplay raw; + auto r = raw.view(); + const float x1[kNIn] = {0.5f, 0.5f}; + const float x2[kNIn] = {0.52f, 0.52f}; // within 0.05 of x1 + const float x3[kNIn] = {0.9f, 0.9f}; // far away + const float a[kNOut] = {0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f}; + const float a2[kNOut] = {0.9f, 0.8f, 0.7f, 0.6f, 0.5f, 0.4f}; + + NISPS_EXPECT(!r.deepen_or_store_negative(std::span(x1), std::span(a))); + NISPS_ASSERT(r.size() == 1u); + NISPS_EXPECT(r.reward(0) == -1.f); + + // Nearby dislike deepens (reward -2) and refreshes the action. + NISPS_EXPECT(r.deepen_or_store_negative(std::span(x2), std::span(a2))); + NISPS_ASSERT(r.size() == 1u); + NISPS_EXPECT(r.reward(0) == -2.f); + NISPS_EXPECT(r.action(0)[0] == 0.9f); + + // Far dislike stores a new item. + NISPS_EXPECT(!r.deepen_or_store_negative(std::span(x3), std::span(a))); + NISPS_ASSERT(r.size() == 2u); + + // Deepening clamps at -16. + for (int i = 0; i < 40; ++i) { + r.deepen_or_store_negative(std::span(x1), std::span(a)); + } + NISPS_EXPECT(r.reward(0) == -16.f); +} + +NISPS_TEST(replay_knn_centroid_deterministic_tie_break) { + RawReplay raw; + auto r = raw.view(); + const float probe[kNIn] = {0.5f, 0.5f}; + + // Two positives EQUIDISTANT from the probe, distinct actions; k=1 must + // pick the LOWER index deterministically. + const float pa[kNIn] = {0.4f, 0.5f}; + const float pb[kNIn] = {0.6f, 0.5f}; + float aa[kNOut]; for (std::size_t j = 0; j < kNOut; ++j) aa[j] = 0.2f; + float ab[kNOut]; for (std::size_t j = 0; j < kNOut; ++j) ab[j] = 0.8f; + r.store(1.f, std::span(pa), std::span(aa)); + r.store(1.f, std::span(pb), std::span(ab)); + + std::array mean{}; + const std::size_t used = r.knn_positive_centroid(std::span(probe), 1u, mean); + NISPS_ASSERT(used == 1u); + NISPS_EXPECT(mean[0] == 0.2f); // index 0 wins the tie + + // k=4 with 3 positives → uses all 3; centroid is their mean. + const float pc[kNIn] = {0.5f, 0.6f}; + float ac[kNOut]; for (std::size_t j = 0; j < kNOut; ++j) ac[j] = 0.5f; + r.store(1.f, std::span(pc), std::span(ac)); + const std::size_t used4 = r.knn_positive_centroid(std::span(probe), 4u, mean); + NISPS_ASSERT(used4 == 3u); + NISPS_EXPECT_NEAR(mean[0], (0.2f + 0.8f + 0.5f) / 3.f, 1e-6); + + // Negatives never contribute. + const float nx[kNIn] = {0.5f, 0.5f}; + float na[kNOut]; for (std::size_t j = 0; j < kNOut; ++j) na[j] = 0.0f; + r.store(-1.f, std::span(nx), std::span(na)); + const std::size_t used_after_neg = + r.knn_positive_centroid(std::span(probe), 4u, mean); + NISPS_EXPECT(used_after_neg == 3u); +} + +NISPS_TEST(replay_decay_and_evict) { + RawReplay raw; + auto r = raw.view(); + const float x[kNIn] = {0.1f, 0.1f}; + const float a[kNOut] = {}; + // A shallow negative just above the evict threshold decays out in a few + // calls; rewards move by +0.0025*max(|r|,1) per call. + r.store(-0.012f, std::span(x), std::span(a)); + NISPS_ASSERT(r.size() == 1u); + std::size_t evicted = r.decay_negatives(); // -0.012 + 0.0025 = -0.0095 > -0.01 → evict + NISPS_EXPECT(evicted == 1u); + NISPS_EXPECT(r.size() == 0u); + + // Positives are never decayed/evicted. + r.store(1.f, std::span(x), std::span(a)); + evicted = r.decay_negatives(); + NISPS_EXPECT(evicted == 0u); + NISPS_EXPECT(r.size() == 1u); +} + +// -- compute_push_target -------------------------------------------------------- + +NISPS_TEST(geo_push_target_moves_away_from_centroid) { + nisps::Rng rng(7ull); + std::array neg{}; + std::array mean{}; + std::array target{}; + for (std::size_t j = 0; j < kNOut; ++j) { + neg[j] = 0.6f; + mean[j] = 0.4f; // dir = +0.2 per dim → push increases values + } + const float step = nisps::ml::geo_push_step(-1.f); // clamp(1,0.25,1)*0.5 = 0.5 + NISPS_EXPECT_NEAR(step, 0.5f, 1e-7); + nisps::ml::compute_push_target(neg, mean, {}, step, rng, target); + for (std::size_t j = 0; j < kNOut; ++j) { + NISPS_EXPECT(target[j] > neg[j]); // strictly away from the centroid + NISPS_EXPECT(target[j] <= 1.f); + } + + // Taper: a far-away negative moves LESS than a near one for the same step. + std::array mean_far{}; + std::array target_far{}; + for (std::size_t j = 0; j < kNOut; ++j) mean_far[j] = 0.0f; // larger len + nisps::ml::compute_push_target(neg, mean_far, {}, step, rng, target_far); + NISPS_EXPECT((target_far[0] - neg[0]) < (target[0] - neg[0])); +} + +NISPS_TEST(geo_push_respects_active_mask_and_clamps) { + nisps::Rng rng(7ull); + std::array neg{}; + std::array mean{}; + std::array target{}; + for (std::size_t j = 0; j < kNOut; ++j) { + neg[j] = 0.99f; + mean[j] = 0.01f; + } + std::array mask{1u, 0u, 1u, 0u, 1u, 0u}; + nisps::ml::compute_push_target(neg, mean, mask, 0.5f, rng, target); + for (std::size_t j = 0; j < kNOut; ++j) { + if (mask[j]) { + NISPS_EXPECT(target[j] >= neg[j]); // pushed (and clamped at 1) + NISPS_EXPECT(target[j] <= 1.f); + } else { + NISPS_EXPECT(target[j] == neg[j]); // frozen dim untouched + } + } +} + +// -- controller end-to-end ------------------------------------------------------ + +NISPS_TEST(geo_dislike_cold_start_then_push) { + GeoMLP m(42ull); + m.draw_weights(0.5f); + GeoFB fb(42ull ^ 0xFEEDBACC0DEull); + NISPS_ASSERT(fb.avoid_style() == AvoidStyle::Geometric); // the P3 default + + set_inputs(m, 0.25f, 0.75f); + m.process(); + + // Cold start: no positives yet → negative-LR fallback. + auto before = m.get_weights(); + std::array snap{}; + for (std::size_t i = 0; i < snap.size(); ++i) snap[i] = before[i]; + + // With the heard action == the net's own output the MSE derivative is + // zero, so the fallback is INERT — the conservative cold start the ADR + // mandates (never destabilise before any positives exist). + const FeedbackAction a1 = fb.on_down(m, {}, 0.1f, 0.5f, {}); + NISPS_EXPECT(a1 == FeedbackAction::GeometricColdStart); + NISPS_EXPECT(fb.negative_count() == 1u); + NISPS_EXPECT(fb.dislike_multiplier() == 2u); + { + auto after = m.get_weights(); + for (std::size_t i = 0; i < snap.size(); ++i) { + NISPS_ASSERT(after[i] == snap[i]); + } + } + + // When the HEARD action differs from the net's raw output (the real + // browser/firmware case — the user hears the post-pipeline vector), the + // negative-LR fallback trains AWAY from it: weights move. + std::array heard{}; + { + auto outs = m.outputs(); + for (std::size_t j = 0; j < kNOut; ++j) { + heard[j] = (outs[j] < 0.5f) ? outs[j] + 0.2f : outs[j] - 0.2f; + } + } + const FeedbackAction a1b = fb.on_down(m, heard, 0.1f, 0.5f, {}); + NISPS_EXPECT(a1b == FeedbackAction::GeometricColdStart); + bool moved = false; + { + auto after = m.get_weights(); + for (std::size_t i = 0; i < snap.size(); ++i) { + if (after[i] != snap[i]) { moved = true; break; } + } + } + NISPS_EXPECT(moved); + + // Feed positives via the like path, then dislike → geometric push. + set_inputs(m, 0.2f, 0.2f); + m.process(); + NISPS_EXPECT(fb.on_up(m) == FeedbackAction::LikeStore); + set_inputs(m, 0.8f, 0.8f); + m.process(); + NISPS_EXPECT(fb.on_up(m) == FeedbackAction::LikeStore); + NISPS_EXPECT(fb.positive_count() == 2u); + + set_inputs(m, 0.25f, 0.75f); + m.process(); + const FeedbackAction a2 = fb.on_down(m, {}, 0.1f, 0.5f, {}); + NISPS_EXPECT(a2 == FeedbackAction::GeometricPush); + NISPS_EXPECT(!fb.exploring()); + NISPS_EXPECT(!fb.learning_paused()); +} + +NISPS_TEST(geo_dislike_deterministic_under_fixed_seed) { + auto run = [](std::span out_weights) { + GeoMLP m(123ull); + m.draw_weights(0.6f); + GeoFB fb(456ull); + set_inputs(m, 0.3f, 0.3f); + m.process(); + fb.on_up(m); // positive + set_inputs(m, 0.31f, 0.31f); + m.process(); + fb.on_down(m, {}, 0.1f, 0.5f, {}); // geometric push + fb.on_down(m, {}, 0.1f, 0.5f, {}); // deepen + push again + auto w = m.get_weights(); + for (std::size_t i = 0; i < w.size(); ++i) out_weights[i] = w[i]; + }; + std::array w1{}, w2{}; + run(w1); + run(w2); + for (std::size_t i = 0; i < w1.size(); ++i) { + NISPS_ASSERT(w1[i] == w2[i]); + } +} + +NISPS_TEST(geo_dislike_diffuse_style_preserves_legacy_path) { + GeoMLP m(9ull); + GeoFB fb(9ull); + fb.set_avoid_style(AvoidStyle::Diffuse); + set_inputs(m, 0.5f, 0.5f); + m.process(); + const FeedbackAction a = fb.on_down(m, {}, 0.1f, 0.5f, {}); + NISPS_EXPECT(a == FeedbackAction::AvoidPerturb); + NISPS_EXPECT(fb.replay_size() == 0u); // diffuse touches no replay +}