From a1cd26ff68be5faac555e863071ad96c5bdfeaeb Mon Sep 17 00:00:00 2001 From: monkey-w1n5t0n Date: Sat, 25 Jul 2026 16:14:35 +0200 Subject: [PATCH] feat(manifold): replay geometric dislikes over time --- ALIGNMENT.md | 19 +- MAP.md | 6 +- docs/adr/rl-feedback-design.md | 22 ++- .../findings-push-away-upstream-comparison.md | 179 ++++++++++++++++++ manifold/public/nisps.js | 2 +- manifold/public/nisps.wasm | Bin 130494 -> 133970 bytes manifold/src/console/ConsoleApp.tsx | 20 +- manifold/src/console/Drawers.tsx | 77 +++++++- manifold/src/console/types.ts | 3 + manifold/src/debug/probe.ts | 16 ++ manifold/src/engine/engine-api.ts | 23 +++ manifold/src/engine/index.ts | 7 +- manifold/src/engine/types.ts | 7 + manifold/src/engine/wasm-iml.ts | 18 ++ manifold/src/feedback/controller.ts | 85 ++++++++- manifold/tests/e2e/geo-dislike.spec.ts | 32 ++++ nisps/ml/dynamic_storage.hpp | 10 +- nisps/ml/feedback.hpp | 147 +++++++++----- nisps/ml/replay.hpp | 49 +++-- nisps/wasm/bindings.cpp | 35 ++++ scripts/build-wasm.sh | 1 + tests/cpp/parity_check.cpp | 11 +- tests/cpp/parity_wasm.mjs | 8 +- tests/cpp/test_mlp_geo_dislike.cpp | 57 ++++-- 24 files changed, 720 insertions(+), 114 deletions(-) create mode 100644 docs/specs/recon/findings-push-away-upstream-comparison.md diff --git a/ALIGNMENT.md b/ALIGNMENT.md index 82a82d2..22f131b 100644 --- a/ALIGNMENT.md +++ b/ALIGNMENT.md @@ -72,12 +72,11 @@ one thumbs-up can move the mapping somewhere in the space by more than the entir range. Retention (how much of the previous teaching survives) is 0.38; at `iters=1` it is 0.80. -**Why it blocks the mission.** This is the other half of the feedback asymmetry, and -RMSProp did NOT fix it — normalising the step size does not change the dose. Upstream -keeps both directions on small repeated steps; we take one enormous positive step and -one small negative one, so teaching feels like a lurch and correcting feels like -nothing. The two numbers now differ by ~70x rather than ~2e6x, which is progress and -still not a design. +**Why it blocks the mission.** RMSProp did NOT fix the positive lurch — normalising the +step size does not change its dose. The negative path now exposes upstream-style repeated +small steps (rate/lifetime/LR are live controls), so its old fixed ~70x comparison is no +longer current. Positive teaching remains one enormous blocking train, and the two doses +still need a matched head-to-head rather than independent tuning. **Rough cost.** Cheap to change, expensive to choose: the tuning space is now measurable (`ml_bench` U4 sweeps dose; U1 sweeps upstream's soft-target alpha, where alpha=1 is @@ -102,7 +101,7 @@ Legacy a-immersive was mobile-first; Manifold is desktop-first. Defer until user - **EOC effects chain, ShapeSeq sequencer, modular engine (Phase E)** — legacy features consciously out of the v1 rewrite; revisit only if a mode wants them. - **Inputs multi-source composition** (2026-06-28, reaffirmed 2026-07-21) — mix-and-match pad+gamepad+MIDI is a recorded, unreversed decision; the UI currently enforces exclusive single-source and the composition machinery sits dormant *by design*. Schedule or keep dormant — but the inputs-spec must stop presenting composition as current behaviour (plan §8). - **Schema content is partially placeholder** (2026-07-21) — 20 anonymous "Param NN" slots across paf_synth/channel_strip/xiasri and copy-pasted ML defaults across all 9 modes. Name them during the first curated-preset pass per mode (plan §6.5c), or shrink `output_size` where the engine allows. -- **Geometric-dislike deliberate divergences** (2026-07-14, one-core P3; reaffirmed 2026-07-25): (1) the degenerate-branch RNG draws from the controller's deterministic `nisps::Rng`, not libc `rand()` — native==WASM parity holds; (2) upstream's async shuffled two-LR `optimise()` is collapsed into one synchronous `dislike_geometric()` training only the pressed negative's target — behavioural, not bitwise, parity with firmware upstream, by design; (3) `RandomiseMlp` uses `draw_weights(spread)` rather than the old asymmetric ranges. All intentional. The 2026-07-25 re-base onto `e291192` took upstream's constants and deleted the taper but did NOT adopt (2)'s counterpart — upstream retrains a batch over ALL live negatives on every 200 Hz tick and holds each negative at full strength for a fixed `kDislikeLifetimeMs = 2500` instead of decaying it proportionally. Adopting that needs a per-tick call site and a millisecond clock inside `nisps/ml`, which the core does not have; it is a real interface decision, not a constant, and is tracked as its own task. +- **Geometric-dislike deliberate divergences** (2026-07-25): (1) the degenerate-branch RNG draws from deterministic `nisps::Rng`, not libc `rand()`; (2) each live rejection computes its liked centroid at its own stored input, rather than upstream reinterpreting every old rejection around the cursor's current position; (3) live negatives are applied as deterministic per-item RMSProp steps rather than one shuffled `TrainBatch`; (4) a repeated nearby rejection refreshes its lifetime (upstream's current dedup path leaves the original timestamp untouched); (5) upstream's default removal of a nearby positive is not yet adopted because Manifold also has a separate positive MLP dataset that would keep pulling; (6) `RandomiseMlp` uses `draw_weights(spread)` rather than old asymmetric ranges. Native↔WASM parity covers the elapsed-time replay path. - **Manifold dock splits `state`/`muted`/`armed`** (2026-06-28) — deliberate divergence from the deployed conflated `frozen`↔`muted` model (dock-spec §3.3). `muted`-downstream and the `soloMode` gradient-mask variants remain UI-only; the C API exposes `set_focus` but no per-mode gradient masking yet. (The audit found `soloMode` behaviourally inert in the controller — plan L20 trims it until `train_masked` exists.) ## Recently resolved (delete after a few weeks) @@ -116,8 +115,10 @@ Legacy a-immersive was mobile-first; Manifold is desktop-first. Defer until user mapping before any likes exist (`ml_bench` E1: 0 -> 2.3e-3). One dislike moves the mapping **5.3e-2**, up from 1.6e-2 after the RMSProp fix and 5.3e-5 before it — a ~1000x change end to end, and now within ~4x of the legacy Diffuse design instead of - ~4100x (`ml_bench` A4). Not adopted: upstream's per-tick batch retraining and fixed - 2500 ms dislike lifetime — see "Deferred / accepted debt". + ~4100x (`ml_bench` A4). The follow-up now adopts upstream's repeated-all-negatives + schedule and full-strength wall-clock lifetime through a deterministic elapsed-time + core seam. Manifold defaults to 0.001 LR, 200 Hz and 2500 ms, exposes all three in the + expanded Learning panel, and allows rate/lifetime zero as an explicit one-shot A/B. - 2026-07-25: **`InterfaceRL` is back in the tree (defect 6c).** Vendored verbatim from memllib `e291192` at `firmware/MEMLNaut-NISPS/lib/memllib/reference/` — outside `src/`, so PlatformIO never compiles it. Upstream drift in the feedback subsystem is a `diff` diff --git a/MAP.md b/MAP.md index 4e30e23..6c4ab7e 100644 --- a/MAP.md +++ b/MAP.md @@ -6,7 +6,7 @@ MEMLNaut-NISPS — Neural Interactive Shaping of Parameter Spaces. One C++20 cod ### `nisps/` — platform-agnostic C++20 library (the only ML/DSP/engine code) - `nisps/core/` — `perf.hpp` (hot-path/inlining attrs), `types.hpp`, `concepts.hpp` (`MLEngine`, `AudioEngine`, `Mode`), `ring_buffer.hpp` (SPSC lock-free cross-core channel, replaces pico/util/queue), `event_queue.hpp` (single-threaded in-engine event FIFO — deliberately NOT RingBuffer, which is an atomics-based cross-thread channel), `rng.hpp` (xoshiro256+ deterministic), `math.hpp` (fast_sigmoid, `Curve` enum + `apply_curve`). -- `nisps/ml/` — the MLP core, written once against a storage policy (`mlp.hpp` `MLPCore`): `storage.hpp` (`FixedStorage` — template-sized `std::array`, zero heap; `MLP` alias preserves the classic compile-time surface) and `dynamic_storage.hpp` (`DynamicStorage` — runtime dims, single arena alloc at construction; `#error`s on RP2350 builds, sole lint heap-allowlist entry). Fixed↔dynamic bit-parity enforced by `tests/cpp/test_mlp_storage_parity.cpp`. Files: `mlp.hpp`, `activations.hpp`, `loss.hpp` (MSE, no double-scaling), `training.hpp` (**RMSProp** + grad clipping — `rmsprop_step()` is a line-for-line port of upstream memlp `Layer.h:239 ApplyAccumulatedGradients` @ `ea777502`; the per-weight running squared-gradient average is optimiser state held in the storage policies, NOT part of `weight_count()`/`get_weights()`, and `MLPCore::reset_optimizer_state()` clears it. `draw_weights()` deliberately does not, matching upstream `DrawWeights`), `init.hpp` (spread-aware uniform↔Xavier), `rl.hpp` (`move_weights` with output pin mask + per-layer scaling + weight decay), `jolt.hpp` (`Jolt` — held continuous weight-morph over the flat weight buffer + post-release LR ramp; ported from upstream InterfaceRL), `ou_noise.hpp` (`OUNoise` — Ornstein-Uhlenbeck exploration walk on the output vector; ported from upstream InterfaceRL), `feedback.hpp` (`FeedbackControllerCore` — the "Down Action" state machine: Avoid (geometric push-away default / Diffuse legacy) / RandomiseOutputs / RandomiseMlp / ExploreAndPlace; storage-policied like the MLP, own deterministic RNG, exposed via `nisps_ml_feedback_*` C API), `replay.hpp` (`ReplayView` — reward-tagged memory: dedup/deepen, k-NN positive centroid with deterministic tie-break, proportional decay+eviction), `geo_push.hpp` (push-away target computation, upstream InterfaceRL @ `e291192` — re-based from `0a541cc` on 2026-07-25: `kGeometricPushScale` 1.0, `kNegLRBase` 1.5, and NO `/(1+len)` taper, so distance from the liked centroid does not shrink a "no". Cold start is the same push in a random direction, not a separate branch), `warm_start.hpp` (overlapping-weights copy for reshape), `stats.hpp`. `generated/ml_defaults.hpp` is codegen output (do not edit): `nisps::ml::generated::kMlTrainDefaults`, the ONE learning-rate / max-iterations / min-error default shared by firmware, WASM and VCV (source `schemas/ml_defaults.json`); `MLPCore::set_train_config()` and `nisps_ml_set_train_config()` override it at runtime. It lives under `ml/` rather than `modes/generated/` because `nisps/ml` sits below `nisps/modes` — mlp.hpp must not include upward. Jolt + OU are inert by default and wired into `ModeBase`, so every mode exposes `jolt_press/jolt_release`, `jolt_lr_scale`, and `set_explore_intensity`. +- `nisps/ml/` — the MLP core, written once against a storage policy (`mlp.hpp` `MLPCore`): `storage.hpp` (`FixedStorage` — template-sized `std::array`, zero heap; `MLP` alias preserves the classic compile-time surface) and `dynamic_storage.hpp` (`DynamicStorage` — runtime dims, single arena alloc at construction; `#error`s on RP2350 builds, sole lint heap-allowlist entry). Fixed↔dynamic bit-parity enforced by `tests/cpp/test_mlp_storage_parity.cpp`. Files: `mlp.hpp`, `activations.hpp`, `loss.hpp` (MSE, no double-scaling), `training.hpp` (**RMSProp** + grad clipping — `rmsprop_step()` is a line-for-line port of upstream memlp `Layer.h:239 ApplyAccumulatedGradients` @ `ea777502`; the per-weight running squared-gradient average is optimiser state held in the storage policies, NOT part of `weight_count()`/`get_weights()`, and `MLPCore::reset_optimizer_state()` clears it. `draw_weights()` deliberately does not, matching upstream `DrawWeights`), `init.hpp` (spread-aware uniform↔Xavier), `rl.hpp` (`move_weights` with output pin mask + per-layer scaling + weight decay), `jolt.hpp` (`Jolt` — held continuous weight-morph over the flat weight buffer + post-release LR ramp; ported from upstream InterfaceRL), `ou_noise.hpp` (`OUNoise` — Ornstein-Uhlenbeck exploration walk on the output vector; ported from upstream InterfaceRL), `feedback.hpp` (`FeedbackControllerCore` — the "Down Action" state machine: Avoid (geometric push-away default / Diffuse legacy) / RandomiseOutputs / RandomiseMlp / ExploreAndPlace; geometric replay is advanced deterministically by caller-supplied elapsed time with configurable rate/lifetime; storage-policied like the MLP, own deterministic RNG, exposed via `nisps_ml_feedback_*` C API), `replay.hpp` (`ReplayView` — reward-tagged memory: dedup/deepen, k-NN positive centroid with deterministic tie-break, wall-clock age/lifetime + eviction), `geo_push.hpp` (push-away target computation, upstream InterfaceRL @ `e291192` — re-based from `0a541cc` on 2026-07-25: `kGeometricPushScale` 1.0, `kNegLRBase` 1.5, and NO `/(1+len)` taper, so distance from the liked centroid does not shrink a "no". Cold start is the same push in a random direction, not a separate branch), `warm_start.hpp` (overlapping-weights copy for reshape), `stats.hpp`. `generated/ml_defaults.hpp` is codegen output (do not edit): `nisps::ml::generated::kMlTrainDefaults`, the ONE learning-rate / max-iterations / min-error default shared by firmware, WASM and VCV (source `schemas/ml_defaults.json`); `MLPCore::set_train_config()` and `nisps_ml_set_train_config()` override it at runtime. It lives under `ml/` rather than `modes/generated/` because `nisps/ml` sits below `nisps/modes` — mlp.hpp must not include upward. Jolt + OU are inert by default and wired into `ModeBase`, so every mode exposes `jolt_press/jolt_release`, `jolt_lr_scale`, and `set_explore_intensity`. - `nisps/pipeline/` — the control-rate input/output processing chains (P4): `input_chain.hpp` (`InputChain` — invert→deadzone→circular clamp→momentum-modulated zoom→centred power→EMA→momentum; caller-supplied dt, internal clock, fixed velocity ring, serialisable state) and `output_chain.hpp` (`OutputChain` — curve→EMA→slew→freeze(+mask), capacity-templated). Behaviour contract = the retired manifold TS pipelines, pinned by `manifold/tests/fixtures/` and parity stage 7. - `nisps/dsp/` — `biquad.hpp`, `delay.hpp`, `reverb.hpp`, `filter.hpp`, `env.hpp`, `osc.hpp`, `pitch_shift.hpp`, `dc_blocker.hpp`, plus the sequencer primitives shared by the sequencer engines: `ratio_seq.hpp` and `seq_clock.hpp` (bar phasor + MIDI clock + bpm). Lean primitives extracted from maximilian; daisysp PitchShifter replaced with custom granular impl. - `nisps/engines/` — eight audio engines, each satisfying `AudioEngine`: `paf_synth.hpp`, `channel_strip.hpp`, `xiasri.hpp`, `verb_fx.hpp`, `memlcelium.hpp`, `breakor.hpp` (sequencer, NoOp audio), `elysiamorf.hpp` (sequencer, NoOp audio), `analysis.hpp` (input-side spectral features). Plus `base.hpp` (`NoOpEngine`, engine_id "thru"). @@ -85,8 +85,8 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set. BATCH "MIDI Learn": every CC swept while armed becomes an axis, shown as read-only meters). `useInputLayer.ts` is the React binding; `base-source.ts` shared status/action plumbing; `types.ts` the adapter contract. `backends/base-backend.ts` is its output-side counterpart (status + throttle + lastSent) used by the midi/osc/vcv transports. -- `manifold/src/feedback/` — `controller.ts` (Explore-and-place scratchpad + geometric-dislike + solo; a thin - driver over the shared C++ core). +- `manifold/src/feedback/` — `controller.ts` (Explore-and-place scratchpad + geometric-dislike + solo; owns the + elapsed-time timer for geometric replay while every weight-affecting step stays in the shared C++ core). - `manifold/src/settings/` — `settings-store.ts` (monochrome icons, input-map shape, I/O resize policy, corner radius, and the opt-in legacy Xavier/spread feature flag; Manifold randomisation is full-range uniform by default). - `manifold/src/serial/` — `memlnaut-serial.ts` Web Serial scaffold + `EditorPanel.tsx` (MEMLNaut Editor mode). - `manifold/src/engine/exploration.ts` — Jolt press + OU explore gestures (Learning drawer): a thin diff --git a/docs/adr/rl-feedback-design.md b/docs/adr/rl-feedback-design.md index 06647d3..babf3e1 100644 --- a/docs/adr/rl-feedback-design.md +++ b/docs/adr/rl-feedback-design.md @@ -4,6 +4,26 @@ --- +### 2026-07-25 implementation amendment + +The verified upstream reference is now memllib `e291192`, vendored under +`firmware/MEMLNaut-NISPS/lib/memllib/reference/`. Its geometric constants and schedule +supersede the older `0a541cc` constants and synchronous-one-shot wording below: + +- push scale `1.0`, negative-LR base `1.5`, no distance taper; cold start pushes in a + deterministic random direction; +- a press stores the rejection and performs one immediate update, then + `FeedbackControllerCore::advance_geometric(dt)` replays **all** live negatives at a + configurable rate for a full-strength wall-clock lifetime; +- defaults match upstream: LR `0.001`, `200 Hz`, `2500 ms`; rate or lifetime zero is + explicit one-shot mode; +- the host supplies elapsed time, but target computation and every weight mutation stay + in the allocation-free shared C++ core. Native↔WASM parity covers this seam. + +Manifold exposes LR/rate/lifetime at expanded Learning depth. Deliberate divergences are +kept current in `ALIGNMENT.md`; source comparison evidence is immutable in +`docs/specs/recon/findings-push-away-upstream-comparison.md`. + ## 0. Decision summary | Setting | Default | Also selectable | @@ -222,4 +242,4 @@ Extend Stage 5: **(S5a Mode 1)** seed `ParityMLP`+`ReplayStore`, store 3 fixed p 2. Record the **single deliberate firmware divergence**: the `useRandom` degenerate branch uses `nisps::Rng`, not libc `rand()` (value generated, never compared; native==WASM holds). 3. Record the **TrainBatch-vs-unshuffled-SGD** behavioural divergence: nisps trains per-sample SGD, not shuffled two-LR batches — `browser != firmware` behaviourally, by design (separate verification targets). 4. State the **shared-trunk solo limit** as accepted: no realisation both perfectly isolates and stays expressive. -5. Re-pin orphaned memllib `4733ca0` → `0a541cc`. \ No newline at end of file +5. Re-pin orphaned memllib `4733ca0` → `0a541cc`. diff --git a/docs/specs/recon/findings-push-away-upstream-comparison.md b/docs/specs/recon/findings-push-away-upstream-comparison.md new file mode 100644 index 0000000..2048f97 --- /dev/null +++ b/docs/specs/recon/findings-push-away-upstream-comparison.md @@ -0,0 +1,179 @@ +--- +kind: finding +date: 2026-07-25 +immutable: true +--- + +# Findings — Manifold “Push away” vs upstream geometric dislike + +_Read-only comparison, 2026-07-25. “Confirmed” claims cite source; “Inference” labels +interpretation. No implementation decision is made here._ + +## Scope and source identity + +The hardware repository named in the question, +[`MusicallyEmbodiedML/MEMLNaut`](https://github.com/MusicallyEmbodiedML/MEMLNaut), +does not contain the active ML implementation. The firmware repository is +[`MusicallyEmbodiedML/MEMLNaut-NISPS`](https://github.com/MusicallyEmbodiedML/MEMLNaut-NISPS/tree/701f2d9f1b4698e0ddfa147193928489de12601f), +whose `src/memllib` submodule pins +[`MusicallyEmbodiedML/memllib` at `e291192`](https://github.com/MusicallyEmbodiedML/memllib/tree/e291192d8e4f2fca7b79670c4df9c2ec8bdf03cd). +The local read-only copies of upstream `InterfaceRL.hpp`, `.tpp`, and `.cpp` have Git +blob hashes `03e9255`, `9ec762b`, and `60204d1`, respectively—the same blobs returned +by GitHub for that pin. The comparison below is therefore against the exact upstream +source, not an approximation. Provenance is recorded locally in +`firmware/MEMLNaut-NISPS/lib/memllib/reference/README.md:1-15`. + +## Confirmed behaviour + +### The mental model is substantially right, but “different” needs a target + +A negative verdict has no supervised label by itself. Upstream defines “different” as: +take the output that was heard, find the mean output of the four liked positions nearest +the current control input, and create a new target one unit farther from that liked +centroid in output space. With no likes—or a degenerate zero-length direction—it uses a +random direction. The target is clamped to `[0,1]` +([upstream `InterfaceRL.tpp:698-761`](https://github.com/MusicallyEmbodiedML/memllib/blob/e291192d8e4f2fca7b79670c4df9c2ec8bdf03cd/examples/InterfaceRL.tpp#L698-L761); +local mirror `firmware/MEMLNaut-NISPS/lib/memllib/reference/InterfaceRL.tpp:698-761`). + +That target is then passed to `synthMapping.TrainBatch`. Inference subsequently reads +the same MLP through `synthMapping.GetOutput`. Therefore current upstream push-away is +**weight training on the mapping MLP**, not a separate rejection layer after inference +([upstream training call](https://github.com/MusicallyEmbodiedML/memllib/blob/e291192d8e4f2fca7b79670c4df9c2ec8bdf03cd/examples/InterfaceRL.tpp#L757-L761); +[upstream inference path](https://github.com/MusicallyEmbodiedML/memllib/blob/e291192d8e4f2fca7b79670c4df9c2ec8bdf03cd/examples/InterfaceRL.tpp#L869-L890)). +OU noise and `paramTransformHook` do exist after MLP inference, but are independent of +the dislike algorithm (`InterfaceRL.tpp:877-890`). + +The Manifold path has the same basic semantics. The UI passes its current post-output- +pipeline vector to `FeedbackController.dislike`, which calls the WASM geometric-dislike +entry point (`manifold/src/console/ConsoleApp.tsx:509-535`; +`manifold/src/feedback/controller.ts:347-373`; +`manifold/src/engine/wasm-iml.ts:956-978`). The C++ controller computes the target and +calls `MLPCore::train_targets`, which runs forward propagation, backpropagation and one +RMSProp update on the same network weights (`nisps/ml/feedback.hpp:395-477`; +`nisps/ml/mlp.hpp:250-287`). + +### The target maths now matches current upstream, but the training schedule does not + +Current Manifold main now matches upstream’s untapered target formula and constants: +`kGeometricPushScale=1.0`, `kNegLRBase=1.5`, no `/(1+distance)` taper +(`nisps/ml/geo_push.hpp:1-110`; upstream `InterfaceRL.hpp:406-414` and +`InterfaceRL.tpp:723-761`). It also now uses the upstream RMSProp update rather than +interpreting an upstream RMSProp learning rate as plain SGD +(`nisps/ml/training.hpp:1-92`). + +The remaining major divergence is dose: + +- Upstream stores the dislike at press time, then its main loop calls `optimise()` on + subsequent cycles (`InterfaceRL.tpp:38-54,186-232`). +- Every optimisation scans **all** live negatives and batch-trains them again + (`InterfaceRL.tpp:673-761`). +- Upstream computes one liked centroid around the **current live control input** and + applies it to every negative in that cycle, even if the user has moved away from the + original disliked position (`InterfaceRL.tpp:698-755`). Manifold instead computes the + centroid at the just-pressed negative’s stored input (`nisps/ml/feedback.hpp:431-467`). +- A negative remains at full strength for 2500 ms, then expires; the number of updates + depends on the mode’s loop rate (`InterfaceRL.hpp:406-414`). +- Manifold collapses press and optimisation into **one synchronous + `train_targets` call for only the just-pressed negative**, then proportionally decays + stored negatives (`nisps/ml/feedback.hpp:395-477`; `nisps/ml/replay.hpp:165-185`). + There is no background/per-frame feedback optimiser. + +Thus a Manifold click names a strongly displaced target, but takes only one step toward +it. Upstream keeps walking toward its target for the next 2.5 seconds. This is the most +direct explanation for a remaining perceptual strength difference. + +As checked on 2026-07-25, the WASM served by +`https://meml.lnfinitemonkeys.org/next/nisps.wasm` has SHA-256 +`d1c58a59517a00c6f51870ea1ec21194561b81058e22bbfb2e11de4af45c645a`, exactly matching +`manifold/public/nisps.wasm` on current main. The reported live behaviour therefore +cannot be explained by production still serving the pre-RMSProp or tapered binary. + +### Upstream also cancels a nearby positive; Manifold does not + +Upstream’s default replay policy is `REPLACE_10_PERCENT` +(`InterfaceRL.hpp:404-405`). When a negative is stored, a positive within input-space +distance `0.10` is removed before the negative is added +(`InterfaceRL.tpp:904-929,950-979`). Manifold’s replay method only deepens or adds a +negative and leaves positives intact (`nisps/ml/replay.hpp:102-121`). Manifold also +keeps liked examples in the separate MLP dataset +(`manifold/src/feedback/controller.ts:375-390`). + +**Inference:** this is less about the first click’s amplitude than persistence. A later +positive training run can pull the mapping back toward a sound rejected near an +existing like, whereas upstream removes that local positive from its continuously +trained replay set. + +### Current measured scale + +On current main, the native behavioural benchmark at shape `2→16→16→16→8`, seed +`24301`, reports: + +- one geometric dislike: at-point L2 movement `0.05335`; +- one legacy undirected Diffuse dislike: `0.22626`; +- repeated geometric dislikes: `0.05314` after 1, `0.33748` after 10, `1.09504` + after 100. + +Commands: + +```bash +scripts/bench-ml.sh --native-only --scenario A4_negative_once +scripts/bench-ml.sh --native-only --scenario D1_geo_anatomy +``` + +These numbers confirm that the current path is no longer inert, but also that a single +geometric press is still about 4.2× smaller than the legacy random-diffusion gesture in +this benchmark. They do not by themselves establish the right musical feel. + +At Manifold’s current default PAF shape (`4→10→10→14→33`), the same seeded scenarios +report one-click movement `0.09654` geometric versus `0.20788` Diffuse (about 2.2× +smaller), and geometric movement `0.55935` after ten presses. These are vector L2 +distances across 33 parameters, so they establish that weights move; they do not prove +that the affected parameters produce a perceptually obvious timbral change. + +The more revealing PAF-shape `A12_like_then_dislike` journey dislikes exactly where a +liked target was taught. With one update, distance from that rejected liked target +changes from `0.38291` to `0.36030` (`rejection_moved=-0.02261`): the mapping moves, but +slightly **toward** the particular target the user just rejected. Ten updates change the +distance to `0.67843` (`rejection_moved=+0.29552`). This is deterministic evidence that +one update is not sufficient to realise the user-facing semantic in an important +contradictory-feedback case; it also motivates testing upstream’s nearby-like removal +separately. + +## Why it likely felt weak + +1. **Fixed today: optimiser mismatch.** Before `f57cddc`, the port used a tiny upstream + RMSProp learning rate inside plain SGD, reducing one press to roughly `5.3e-5` + movement. +2. **Fixed today: superseded push formula.** Before `ec31180`, the port halved the target + step, used one-third the negative-LR base, and tapered the step by distance—the exact + case upstream says made “no” ineffective. +3. **Still present: one update versus a time window of updates.** Manifold performs one + weight update per click; upstream replays all dislikes repeatedly for 2.5 seconds. +4. **Still present: highly asymmetric teaching dose.** A Manifold like trains the whole + positive dataset at `lr=1.0` for up to 1000 iterations, while a dislike takes one + roughly `0.0015` RMSProp step. Relative to the lurch caused by a like, correction + still feels small (`ALIGNMENT.md:66-84`). +5. **Potential persistence mismatch:** nearby likes are retained locally but removed by + upstream’s default replay policy, so future positive retraining may partly undo the + rejection. + +## Recommended next experiment + +Do not start by increasing `geo_lr` blindly. That can make a click stronger but does not +answer whether the intended interaction is one discrete correction or a short-lived +repulsive constraint. + +Add a deterministic “advance feedback by `dt`” core seam and benchmark three matched +variants from the same seeded prefix: + +1. current one-shot update; +2. upstream-style replay of all negatives for 2500 ms, while separately testing whether + centroid lookup follows each stored negative or upstream’s current live input; +3. a bounded discrete equivalent (for example 10/25/50 steps at press time) that avoids + wall-clock ownership in the core. + +For each, report at-point movement, neighbourhood rings, global blast ratio, collateral +movement at liked positions, and like→dislike→like persistence. Then perform an +in-Manifold blind A/B at the real per-mode output arities and choose the smallest dose +that makes one rejection obvious without damaging liked regions. Test the nearby-like +removal policy as a separate axis rather than coupling it to dose. diff --git a/manifold/public/nisps.js b/manifold/public/nisps.js index be081a4..cf45c26 100644 --- a/manifold/public/nisps.js +++ b/manifold/public/nisps.js @@ -6,7 +6,7 @@ var createNispsModule = (() => { function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);function findWasmBinary(){var f="nisps.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["d"];updateMemoryViews();addOnInit(wasmExports["e"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var wasmImports={c:__abort_js,b:__emscripten_memcpy_js,a:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["e"])();var _nisps_ml_create=Module["_nisps_ml_create"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["f"])(a0,a1,a2,a3,a4);var _nisps_ml_reshape=Module["_nisps_ml_reshape"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_ml_reshape=Module["_nisps_ml_reshape"]=wasmExports["g"])(a0,a1,a2,a3,a4,a5);var _nisps_ml_destroy=Module["_nisps_ml_destroy"]=a0=>(_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["h"])(a0);var _nisps_ml_set_input=Module["_nisps_ml_set_input"]=(a0,a1,a2)=>(_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["i"])(a0,a1,a2);var _nisps_ml_process=Module["_nisps_ml_process"]=a0=>(_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["j"])(a0);var _nisps_ml_outputs=Module["_nisps_ml_outputs"]=a0=>(_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["k"])(a0);var _nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=(a0,a1,a2,a3)=>(_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["l"])(a0,a1,a2,a3);var _nisps_ml_add_example=Module["_nisps_ml_add_example"]=(a0,a1,a2)=>(_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["m"])(a0,a1,a2);var _nisps_ml_train=Module["_nisps_ml_train"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["n"])(a0,a1,a2,a3,a4);var _nisps_ml_set_train_config=Module["_nisps_ml_set_train_config"]=(a0,a1,a2,a3)=>(_nisps_ml_set_train_config=Module["_nisps_ml_set_train_config"]=wasmExports["o"])(a0,a1,a2,a3);var _nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["p"])(a0);var _nisps_ml_loss_history=Module["_nisps_ml_loss_history"]=(a0,a1,a2)=>(_nisps_ml_loss_history=Module["_nisps_ml_loss_history"]=wasmExports["q"])(a0,a1,a2);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["r"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["s"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["t"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["u"])(a0,a1);var _nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=(a0,a1)=>(_nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=wasmExports["v"])(a0,a1);var _nisps_ml_feedback_reset=Module["_nisps_ml_feedback_reset"]=a0=>(_nisps_ml_feedback_reset=Module["_nisps_ml_feedback_reset"]=wasmExports["w"])(a0);var _nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=a0=>(_nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=wasmExports["x"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["y"])(a0);var _nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=(a0,a1,a2)=>(_nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=wasmExports["z"])(a0,a1,a2);var _nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=wasmExports["A"])(a0,a1,a2,a3,a4);var _nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=a0=>(_nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=wasmExports["B"])(a0);var _nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=(a0,a1)=>(_nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=wasmExports["C"])(a0,a1);var _nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=(a0,a1)=>(_nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=wasmExports["D"])(a0,a1);var _nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=a0=>(_nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=wasmExports["E"])(a0);var _nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=(a0,a1)=>(_nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=wasmExports["F"])(a0,a1);var _nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=(a0,a1)=>(_nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=wasmExports["G"])(a0,a1);var _nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=a0=>(_nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=wasmExports["H"])(a0);var _nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=a0=>(_nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=wasmExports["I"])(a0);var _nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=a0=>(_nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=wasmExports["J"])(a0);var _nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=a0=>(_nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=wasmExports["K"])(a0);var _nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=a0=>(_nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=wasmExports["L"])(a0);var _nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=(a0,a1)=>(_nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=wasmExports["M"])(a0,a1);var _nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=(a0,a1,a2)=>(_nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=wasmExports["N"])(a0,a1,a2);var _nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=(a0,a1)=>(_nisps_ml_feedback_store_positive=Module["_nisps_ml_feedback_store_positive"]=wasmExports["O"])(a0,a1);var _nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=a0=>(_nisps_ml_feedback_positive_count=Module["_nisps_ml_feedback_positive_count"]=wasmExports["P"])(a0);var _nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=a0=>(_nisps_ml_feedback_negative_count=Module["_nisps_ml_feedback_negative_count"]=wasmExports["Q"])(a0);var _nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=(a0,a1)=>(_nisps_ml_feedback_set_avoid_style=Module["_nisps_ml_feedback_set_avoid_style"]=wasmExports["R"])(a0,a1);var _nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=a0=>(_nisps_ml_jolt_press=Module["_nisps_ml_jolt_press"]=wasmExports["S"])(a0);var _nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=a0=>(_nisps_ml_jolt_step=Module["_nisps_ml_jolt_step"]=wasmExports["T"])(a0);var _nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=a0=>(_nisps_ml_jolt_release=Module["_nisps_ml_jolt_release"]=wasmExports["U"])(a0);var _nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=a0=>(_nisps_ml_jolt_active=Module["_nisps_ml_jolt_active"]=wasmExports["V"])(a0);var _nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=(a0,a1)=>(_nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=wasmExports["W"])(a0,a1);var _nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=a0=>(_nisps_ml_explore_get_intensity=Module["_nisps_ml_explore_get_intensity"]=wasmExports["X"])(a0);var _nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=(a0,a1,a2)=>(_nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=wasmExports["Y"])(a0,a1,a2);var _nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=(a0,a1)=>(_nisps_ml_get_layer_stats=Module["_nisps_ml_get_layer_stats"]=wasmExports["Z"])(a0,a1);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["_"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=(a0,a1)=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["$"])(a0,a1);var _nisps_pipeline_create=Module["_nisps_pipeline_create"]=()=>(_nisps_pipeline_create=Module["_nisps_pipeline_create"]=wasmExports["aa"])();var _nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=a0=>(_nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=wasmExports["ba"])(a0);var _nisps_input_set_config=Module["_nisps_input_set_config"]=(a0,a1,a2)=>(_nisps_input_set_config=Module["_nisps_input_set_config"]=wasmExports["ca"])(a0,a1,a2);var _nisps_input_process=Module["_nisps_input_process"]=(a0,a1,a2,a3,a4)=>(_nisps_input_process=Module["_nisps_input_process"]=wasmExports["da"])(a0,a1,a2,a3,a4);var _nisps_input_reset=Module["_nisps_input_reset"]=a0=>(_nisps_input_reset=Module["_nisps_input_reset"]=wasmExports["ea"])(a0);var _nisps_output_set_config=Module["_nisps_output_set_config"]=(a0,a1,a2,a3,a4)=>(_nisps_output_set_config=Module["_nisps_output_set_config"]=wasmExports["fa"])(a0,a1,a2,a3,a4);var _nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=(a0,a1,a2)=>(_nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=wasmExports["ga"])(a0,a1,a2);var _nisps_output_process=Module["_nisps_output_process"]=(a0,a1,a2,a3)=>(_nisps_output_process=Module["_nisps_output_process"]=wasmExports["ha"])(a0,a1,a2,a3);var _nisps_output_reset=Module["_nisps_output_reset"]=a0=>(_nisps_output_reset=Module["_nisps_output_reset"]=wasmExports["ia"])(a0);var _nisps_curve_apply=Module["_nisps_curve_apply"]=(a0,a1,a2)=>(_nisps_curve_apply=Module["_nisps_curve_apply"]=wasmExports["ja"])(a0,a1,a2);var _nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=(a0,a1,a2,a3,a4)=>(_nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=wasmExports["ka"])(a0,a1,a2,a3,a4);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["la"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["ma"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["na"])(a0,a1,a2);var _nisps_engine_process_block=Module["_nisps_engine_process_block"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_engine_process_block=Module["_nisps_engine_process_block"]=wasmExports["oa"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["qa"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["ra"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["sa"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["ta"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["ua"])();Module["ccall"]=ccall;Module["cwrap"]=cwrap;var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise; +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var wasmBinary=Module["wasmBinary"];var wasmMemory;var ABORT=false;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function postRun(){var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);function findWasmBinary(){var f="nisps.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["d"];updateMemoryViews();addOnInit(wasmExports["e"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var __abort_js=()=>{abort("")};var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};var wasmImports={c:__abort_js,b:__emscripten_memcpy_js,a:_emscripten_resize_heap};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["e"])();var _nisps_ml_create=Module["_nisps_ml_create"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_create=Module["_nisps_ml_create"]=wasmExports["f"])(a0,a1,a2,a3,a4);var _nisps_ml_reshape=Module["_nisps_ml_reshape"]=(a0,a1,a2,a3,a4,a5)=>(_nisps_ml_reshape=Module["_nisps_ml_reshape"]=wasmExports["g"])(a0,a1,a2,a3,a4,a5);var _nisps_ml_destroy=Module["_nisps_ml_destroy"]=a0=>(_nisps_ml_destroy=Module["_nisps_ml_destroy"]=wasmExports["h"])(a0);var _nisps_ml_set_input=Module["_nisps_ml_set_input"]=(a0,a1,a2)=>(_nisps_ml_set_input=Module["_nisps_ml_set_input"]=wasmExports["i"])(a0,a1,a2);var _nisps_ml_process=Module["_nisps_ml_process"]=a0=>(_nisps_ml_process=Module["_nisps_ml_process"]=wasmExports["j"])(a0);var _nisps_ml_outputs=Module["_nisps_ml_outputs"]=a0=>(_nisps_ml_outputs=Module["_nisps_ml_outputs"]=wasmExports["k"])(a0);var _nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=(a0,a1,a2,a3)=>(_nisps_ml_infer_batch=Module["_nisps_ml_infer_batch"]=wasmExports["l"])(a0,a1,a2,a3);var _nisps_ml_add_example=Module["_nisps_ml_add_example"]=(a0,a1,a2)=>(_nisps_ml_add_example=Module["_nisps_ml_add_example"]=wasmExports["m"])(a0,a1,a2);var _nisps_ml_train=Module["_nisps_ml_train"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_train=Module["_nisps_ml_train"]=wasmExports["n"])(a0,a1,a2,a3,a4);var _nisps_ml_set_train_config=Module["_nisps_ml_set_train_config"]=(a0,a1,a2,a3)=>(_nisps_ml_set_train_config=Module["_nisps_ml_set_train_config"]=wasmExports["o"])(a0,a1,a2,a3);var _nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=a0=>(_nisps_ml_eval_loss=Module["_nisps_ml_eval_loss"]=wasmExports["p"])(a0);var _nisps_ml_loss_history=Module["_nisps_ml_loss_history"]=(a0,a1,a2)=>(_nisps_ml_loss_history=Module["_nisps_ml_loss_history"]=wasmExports["q"])(a0,a1,a2);var _nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=a0=>(_nisps_ml_weight_count=Module["_nisps_ml_weight_count"]=wasmExports["r"])(a0);var _nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=(a0,a1)=>(_nisps_ml_get_weights=Module["_nisps_ml_get_weights"]=wasmExports["s"])(a0,a1);var _nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=(a0,a1)=>(_nisps_ml_set_weights=Module["_nisps_ml_set_weights"]=wasmExports["t"])(a0,a1);var _nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=(a0,a1)=>(_nisps_ml_draw_weights=Module["_nisps_ml_draw_weights"]=wasmExports["u"])(a0,a1);var _nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=(a0,a1)=>(_nisps_ml_feedback_set_mode=Module["_nisps_ml_feedback_set_mode"]=wasmExports["v"])(a0,a1);var _nisps_ml_feedback_reset=Module["_nisps_ml_feedback_reset"]=a0=>(_nisps_ml_feedback_reset=Module["_nisps_ml_feedback_reset"]=wasmExports["w"])(a0);var _nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=a0=>(_nisps_ml_feedback_get_mode=Module["_nisps_ml_feedback_get_mode"]=wasmExports["x"])(a0);var _nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=a0=>(_nisps_ml_feedback_exploring=Module["_nisps_ml_feedback_exploring"]=wasmExports["y"])(a0);var _nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=(a0,a1,a2)=>(_nisps_ml_feedback_set_focus=Module["_nisps_ml_feedback_set_focus"]=wasmExports["z"])(a0,a1,a2);var _nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=(a0,a1,a2,a3,a4)=>(_nisps_ml_feedback_down=Module["_nisps_ml_feedback_down"]=wasmExports["A"])(a0,a1,a2,a3,a4);var _nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=a0=>(_nisps_ml_feedback_up=Module["_nisps_ml_feedback_up"]=wasmExports["B"])(a0);var _nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=(a0,a1)=>(_nisps_ml_feedback_static_output=Module["_nisps_ml_feedback_static_output"]=wasmExports["C"])(a0,a1);var _nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=(a0,a1)=>(_nisps_ml_feedback_enter_explore=Module["_nisps_ml_feedback_enter_explore"]=wasmExports["D"])(a0,a1);var _nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=a0=>(_nisps_ml_feedback_exit_explore=Module["_nisps_ml_feedback_exit_explore"]=wasmExports["E"])(a0);var _nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=(a0,a1)=>(_nisps_ml_feedback_reroll=Module["_nisps_ml_feedback_reroll"]=wasmExports["F"])(a0,a1);var _nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=(a0,a1)=>(_nisps_ml_feedback_nudge=Module["_nisps_ml_feedback_nudge"]=wasmExports["G"])(a0,a1);var _nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=a0=>(_nisps_ml_feedback_undo=Module["_nisps_ml_feedback_undo"]=wasmExports["H"])(a0);var _nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=a0=>(_nisps_ml_feedback_like=Module["_nisps_ml_feedback_like"]=wasmExports["I"])(a0);var _nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=a0=>(_nisps_ml_feedback_commit_place=Module["_nisps_ml_feedback_commit_place"]=wasmExports["J"])(a0);var _nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=a0=>(_nisps_ml_feedback_cancel_place=Module["_nisps_ml_feedback_cancel_place"]=wasmExports["K"])(a0);var _nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=a0=>(_nisps_ml_feedback_undo_depth=Module["_nisps_ml_feedback_undo_depth"]=wasmExports["L"])(a0);var _nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=(a0,a1)=>(_nisps_ml_feedback_placed_output=Module["_nisps_ml_feedback_placed_output"]=wasmExports["M"])(a0,a1);var _nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=(a0,a1,a2)=>(_nisps_ml_feedback_dislike_geometric=Module["_nisps_ml_feedback_dislike_geometric"]=wasmExports["N"])(a0,a1,a2);var _nisps_ml_feedback_set_geometric_config=Module["_nisps_ml_feedback_set_geometric_config"]=(a0,a1,a2,a3)=>(_nisps_ml_feedback_set_geometric_config=Module["_nisps_ml_feedback_set_geometric_config"]=wasmExports["O"])(a0,a1,a2,a3);var _nisps_ml_feedback_advance_geometric=Module["_nisps_ml_feedback_advance_geometric"]=(a0,a1)=>(_nisps_ml_feedback_advance_geometric=Module["_nisps_ml_feedback_advance_geometric"]=wasmExports["P"])(a0,a1);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_explore_intensity=Module["_nisps_ml_explore_intensity"]=(a0,a1)=>(_nisps_ml_explore_intensity=Module["_nisps_ml_explore_intensity"]=wasmExports["Y"])(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["Z"])(a0);var _nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=(a0,a1,a2)=>(_nisps_ml_explore_apply=Module["_nisps_ml_explore_apply"]=wasmExports["_"])(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["$"])(a0,a1);var _nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=a0=>(_nisps_ml_clear_examples=Module["_nisps_ml_clear_examples"]=wasmExports["aa"])(a0);var _nisps_ml_describe=Module["_nisps_ml_describe"]=(a0,a1)=>(_nisps_ml_describe=Module["_nisps_ml_describe"]=wasmExports["ba"])(a0,a1);var _nisps_pipeline_create=Module["_nisps_pipeline_create"]=()=>(_nisps_pipeline_create=Module["_nisps_pipeline_create"]=wasmExports["ca"])();var _nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=a0=>(_nisps_pipeline_destroy=Module["_nisps_pipeline_destroy"]=wasmExports["da"])(a0);var _nisps_input_set_config=Module["_nisps_input_set_config"]=(a0,a1,a2)=>(_nisps_input_set_config=Module["_nisps_input_set_config"]=wasmExports["ea"])(a0,a1,a2);var _nisps_input_process=Module["_nisps_input_process"]=(a0,a1,a2,a3,a4)=>(_nisps_input_process=Module["_nisps_input_process"]=wasmExports["fa"])(a0,a1,a2,a3,a4);var _nisps_input_reset=Module["_nisps_input_reset"]=a0=>(_nisps_input_reset=Module["_nisps_input_reset"]=wasmExports["ga"])(a0);var _nisps_output_set_config=Module["_nisps_output_set_config"]=(a0,a1,a2,a3,a4)=>(_nisps_output_set_config=Module["_nisps_output_set_config"]=wasmExports["ha"])(a0,a1,a2,a3,a4);var _nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=(a0,a1,a2)=>(_nisps_output_set_freeze_mask=Module["_nisps_output_set_freeze_mask"]=wasmExports["ia"])(a0,a1,a2);var _nisps_output_process=Module["_nisps_output_process"]=(a0,a1,a2,a3)=>(_nisps_output_process=Module["_nisps_output_process"]=wasmExports["ja"])(a0,a1,a2,a3);var _nisps_output_reset=Module["_nisps_output_reset"]=a0=>(_nisps_output_reset=Module["_nisps_output_reset"]=wasmExports["ka"])(a0);var _nisps_curve_apply=Module["_nisps_curve_apply"]=(a0,a1,a2)=>(_nisps_curve_apply=Module["_nisps_curve_apply"]=wasmExports["la"])(a0,a1,a2);var _nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=(a0,a1,a2,a3,a4)=>(_nisps_curve_apply_batch=Module["_nisps_curve_apply_batch"]=wasmExports["ma"])(a0,a1,a2,a3,a4);var _nisps_engine_create=Module["_nisps_engine_create"]=(a0,a1)=>(_nisps_engine_create=Module["_nisps_engine_create"]=wasmExports["na"])(a0,a1);var _nisps_engine_destroy=Module["_nisps_engine_destroy"]=a0=>(_nisps_engine_destroy=Module["_nisps_engine_destroy"]=wasmExports["oa"])(a0);var _nisps_engine_set_params=Module["_nisps_engine_set_params"]=(a0,a1,a2)=>(_nisps_engine_set_params=Module["_nisps_engine_set_params"]=wasmExports["pa"])(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["qa"])(a0,a1,a2,a3,a4,a5);var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["sa"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["ta"])(a0);var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["ua"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["va"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["wa"])();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 f30179d559df6d16b0ddb7bfa5fbfb97a5f1ab77..d5d2937ed4c6c7627b10d1959698b148c9d4f755 100755 GIT binary patch delta 20741 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 delta 17069 zcmeHudz4kxndkoYIrshG)&r`j2kdhw;8p<t^BEhbVMW;1lv<)%D*tTm-CQJ`;=)}yJ3EKEtCOz}}zI|@h zEx^21&szOQ>!SAK?8mpi_x`?pU-*3T+wUf_s{htoNGV0{QpfJ>{J0wg!JTUNwaXeE zuB?lrjsMg-9o$L5?uWfN_oMdWj36eTD9{ZK^;5sn_U2eCqau%Dbf9ac(kB z(BxRFk8cC$^}J)2iAWl_f<^F0L6! zt*Jf1H5HFfYEkiop6qC?2==l`m0S@Tgu&H{j!qs*`R~pQewhep;{Q z(#`5ZDyvV?fV!PFs87%>Y72c#-AW%=~L>XR8lulpSqD2s2gaZx}FxP>u9lBM@!UNTB_Crv`nq0OVzb>nfeG_ zu0Bjxs1MPVY85S4e?lwNHS_`XLApwI(X> zx}4Uk%cw(Ls{8xZGJRdYTB@(_S4;E_{c5qkv0p9HH}$K9`lJ17fnMLQ`t;5Hs-(-8 z^s8Py(627h8~WA7`j&n*Uw^D$&C?(6R~PAx{iO7!el=Zh?pLn9wO?)3Tl&=}^(Xq( zZRuaVkhp!wb$Zj(URjhoe|+msPi38qp@q$}7Os5Pd*_M$h1SY{%K7;INbdZ`*6QaM zy!Qup{J8!we|{%71)tmVi`(0y&jlW*vCMr1?jqC*twV$P$CH&i+AgG)%A;);=qJj?HHmVUR&%sMXQJe8_Q!GOxZ2L-**deEPn)hwZrRflOdO>jHw{a2Dw8o8 zyI&>W<_8(ug%g{Y1W(hh z6tu~dLFi0vCunn8XC>LvnaImg+{&C5zcRZm5uE)TSyMep^&DA410_8GW<;$N+*$^s z`EfvINTdr8$8e_6<-m0{h|yqo5A5zS--7dFCN6kfQgW8%NG8f~EWiv{_Ei<5Ms;ep z2VAJ+n}bgy>w>qLr?kmdR5hrT++9d%NT_kf#2P7xpL_@CI|D3XxEHM7aZ1D!jpccTen#l zno;O(fiDdhWI(3uJKfqyJty^|)KgNQDfO(>XGR!!9Y4fD~dQF9CQ)R5(kW3Rw1+V2@bou1#JJT`8w`fx>^V zX}}Cm1A*hq+&i&KRm@{DBEyCVh$_)$KMDO`-3DsH%QInQQlV^3#le)aH39 zs7+@hvs@acm8M{Lj?)&z?o8Y@MU{@zfS+Uss;G+$ z5ID&CHuEgw&$f)81*hPff)j(H0GOe_SPZ)LF*qx%-hRYopm#<6X5@G4!?p$9;MO71(11nl`s2iu@&4l~cb@17+KiPE&86}t zV#RUY`s+Nqty@3D<-~6NO)eonyqe$Mtq*cL*@^Ex&I(&Si0Ue45$<2*P1U`+7tDGH zMiAoE;pavOhbEK&zE15k!H%!MMxv+LuftL5mbLnEaFhsOqnY{@k9iBsHy#CT@+LUE zD##^(B*1aLFATYwIieBbQD@mV14yhuc?Q)yPX{MEy#}rdCi@NSP1KhIECK$lXXdT= zQ_PgT7|_5i^hz1^nmv0w!&Ak~GCSwZc+1WcAjZ+EVpDOBY|VORPt2Ii?gc+Mdii_- zGvR6W`_niD1rQ5fgG(MX#X*L146$CH<=&
eR&dEOH(=Hg9Mq`)ERz~;E$h~t5%UIodKekEEn^tD5i>$4`IvQ<8L>6% z*6Kbw+UHJC^R#VP>$?GuWup`wCSj(@Vd7>QP9%o`m$D6T`7kV2HHTqeA2bIg!4quF zvZrq%aNsOCYbV5RM`r;?$ZhRxhlwsq*jf$~?IVYgec%Z*u}5(RTMexO+JRridVHEa zOAU*GQ-XVsv37&q#zCNb;ieV!`8+ld3DdYX<7BYtMW|7^EwbP-9fZ;4Vv|OX3zo~m zEf5Rmv%re>Sp&zhOS?7oWKBIA)kT+qpX0zK_!&4~k_-5X$X8omi*_1Wd~|7<2FZ^z zTS3~{Ny99L2LBjN2yP9W%z_EP3C@q;Ojj3Mi+fxZPFoKwptfNFUCS33zQf9n02G9> zyd`iZ1kRDgN0*ja0w>5;W9`Aqn?M8LaP6{zQfjU5@&bH0 z_7WM4OAOs3E1LxOkKq^KB%1^5d{+g~kPz0~aZ>;@6`Yo+3j;7qY$03EFSE6T)!fe| z%>4agO}q6zGJ%-U{ji-&tr^{iD%;K80k09OF;;T$F?!2>rbE^8@_3e9sI;ZNs2{Ks znD=>t8HEvq8Es&BonRW+Y}WI)L*z9@3_gifRi2GXU?y2-dW4QxwFz1$vZEY0u~H^> z)Ma@B6b*Sq4G~O)Rstcf$G zxr{%P%OE=I9X1XF0K&vzsevT;?L0olgCME{eskGxm9YW#vuneIrLl}3i`Hzlv!bn< zqopyLM@3^`oi4LMCcXr0@m)v&n=Uyw8e%3Apa=7Hj-@hZofac@3``NcNP;tFGt&r= zWFfigrr5eSqK!x!0&YdPlyJjzOTu zn)LvF4`YoTpg_DZzX{<0@xe?}?N@jS_2u{a2xkGx=`8C;PD7rRY~D1EXiL*j3sauw z=7ZCL0=HO}6crZ5@FLX7^#z(?@Df%LmjC#~j1=~V1Y@jzAzIPVS$i789@<_$a@26q zfV#n#0H2a*!QHpxvS^6iv3{l;AyU6g32{JbR!?D8s=E4E*rUr<2CAe(l_+B`rABPJ+k3i%VQVc5*om{}=cl^oh&VTHM;L_Yy0&4tvE}$j%{$WbQ!0 zjt@JexIkx{?WV=#1fOY@v`jDv7JU&fWATQgC^Sr)i=jhcc41kR=4&z#?}VW zSrqj1pNoyR6S~^kfYodm*h&muJX*bE4b(UKlo}yb3NjB)o@a0H%>T1-4wQ4C*nvKF zbmY>`6l6OMp~|pov#gu8+!84}nrV0R6?gm>hV1=&1Lm7OTz>+N#@;H9sy8L2#;_CPW2z}`_q)t=jul|jX=eI% zzq4=FUwG5-sdKwG)l4^C+W}QA%to1MJr0K&iZ%){)dm)(2M9ypKy1N&2Qob)2Or(g z@3JcjKr<{resy_3;&(=Sm>Jvs>DZ@+79&W2{{t+XhJ;ZHz6TyWW4dtOPH(Ec#)90| z*3_OE9Lvtn78LM(Hll!9*?|L7s(4&4*E^0*}*{q|l!diat(#@UNd zPLj3FLb%7lh$q&;{1quMETma4!R#OccmswbZn6#q6w1aPr`gul_c#SxTi4@Qm)G;a zDcJzN)$9RZiEo8F!iyBilf*^TGDsMD3((?_!bTWdkPs_3`ye3VI0i-0kWomX6~^&U z*i#9m93lY{4wGF#LWc5Xm3$DfE_ldGK<#W2(wNxLJvN+ic&}$L5nrs!%=Z(kmkIz{ zL%wa21o92`jYlq4B;N>Fh^>KSvgSJbkPaErVS)}w|6U`350oCF9N3zb7YM}4D{E{b zNc<}Pvc=cDsg5?qMSw-U!t3#k5@^cW_*~V+NJe**+DkFtUQ*Wo#6qk)r~V4HrfBZzI_4)y~k` zBAk-3bl76)aK5EO*3tnfpAkB28$*X1_av^Xi)CLK)%YZus z*I8&`e7IH`P(R#gvm}A|lf$turWY=Vdl$43dBhO;y!m5%AE(3_Ofl+5(?u;4u>&e+=+!`e5ui3i$f~ z4;5MiyhQyGiU{C;Aj74_a@r_m7Pb~3$Jv%58mZY%;xHU@^pP|rhSh;QB67u6nT)d& z8s7G>yDFj@LA0iILHeaB7wt999^YXN>Tvt9HJAu;B0UozN)bm8Ty~50U!H^8#e3s) zXKYA++=ddSKoW*u#$@4E;pj8|)w=c!eN8T*Q+*$S1BE>ei=lokic4F&h6i#^V6pTC2N`3hDS}Uzbaqdj&3b(b<_0(jS$+pw=p#hG= zyW8oUaHGMWU$)alv8K@_J}`-vVmy}U44;}ri%^$!-anacpb2)J$7~(oEJqh0xjEc3 zg=T~?kCudP2X?*9qs8Ht4)p!pqZ?_wowv!S8^g~`#r|*lbS`@35Z6qhdEt4}F!u2& zv;@7f_xn0%G3v7S10A$B+%uh~hEt}ZHlc_W9-N8^{Y9MM*HfuGd~0?ZMi*x9Dq{ABXQ2675wo9}f!Wt~Qg1kWCi=eH zi5Z7yV(w*Kw3g~E37+i&*VJ3+J7!RKtbP=Ke>Q^_;G3M`eI=0Sk3P1Gw~O;{+)2^HK6b0@Y~l=Gff_T=^DCc zJY|P(*+3t0QGRbDbvcv`Pu)r(nmf19)0Ac7nl1geWGP+94qtm49Zup?dqAle7M;G0 zx>2mzMzaCd-P>rcOI^dy?f~XccKsz?Jr1DXbuXQR;(#sw$rkzhAdrWLci%_P61@^$ zdq4GlnC>5UT(<8)vdd6MR;`w!moAks3m+p4stn59dt8I z=wZ=i4f##v^7(no!SF4_$?0I&`~~XBL%KazF2F45;iX@oCoVaN+a|nMmgGY&=Pgk! zJr*y+*r7!JA`Q>o+3#}TtJB%%A}opWA(s>IozAXs=@)61{XY%qkAlHNS&>m;%FrvrV#pzhY$7&)9eDZA=)1?PP{Q#}^ zTmdDnd%+S+zgg-y9%BhrXQ`98YoLm$%w+iXIvoGS z90*_r`JXUOT%uFqrkt6J@nPv})Y^3B63ipV!o(vqui@=znoJ+m7ZGNC=WFEW#>^Ui z?Q3)s;WVqhPS5cviVu^2>6jrS%=dN`PX^%OMH}zH8$N`7F*A^O8ZdgJkB3gDr0ZC? z_hGuFc=q*^wjh&pw6aar=~o-Ho!8jKlu?QWTpy7{L33%zWPP z0QF7WA0NzO!ZL zpK1@rpV=C3Yk+p;wE<|nZ9;*{LA>`PglRbJfHQ)=2t1*Nd{Vk}I6Uz;WM)BqA?)C{ z`Wf-)vl_7hhPKf<^Yy0QG~(fgq`orYxkJ=cz@y@->pmV%dnQ~pL^I0?GXWR+aW+?z zgo~;1&oNClWE@@X5if!-$T1yu7de}XAX0&!-4<#)k(xQP6Db)xJ^VjIG(9(F#c=YY z@P6ih3pz%in;CiOfDR9)q`@FL3>I|bXhLNidN}VsX0=*S0AHTrlO$mkXux-7r-jeK z`NAK5liDXrMup#pp1yqee-F{*0DvWZiRvYhNXnaWz_NV+0xpw7*gLAA7$JK(!~_5@ zPt+xG#u+hA10ZSEgiXc+|MeFCDIrGVdk`Z9E?o+Le=IdPp??N7xbqBZaOY{%;3Y>^ z6lw%fib#$!TcA%dw;-Ev8a?nXo)cUNNeM>u7_(lL9ub7~ObAkaV1aLt#pO{B5^&+BA!_*d zB^L531-Ex0gKhjf6pI2Z8#GM8=V!SNvV-=ybrt_nAk!YgHtMWcZ~__mWRYnr7Mx4w z2S_7g!4DWBu}H8oz(1#&PaY!{Bfr~-n67~zfytY_8m2q5vnmz|R#FqfcmI~^a;Y zh6@oNK(c|~xVsx*gpW8dBw}gW`u^!phyMF7f8%U7E$BJouGkZ2lPxVEH|EICBHM_{IiF*`N(Obn8{T(Ok8_?zaL4B>@x?iaii)yJkZ77yvjWBv5`^tHB5PrbDW2uItT7G zJ5H$1j(jLok&N3B^_s}TLN8JY;vu41=y6U)DApzj=x}&apurgneT8Mb9Vf(f4aFA*ohz$$UH|9l2|SE1t$3`&fAM)ASGY za(L0-(?qmB`1kZ(^27FL=sW}*Ry;%FX~pnO&mho(h{*T9OGk4nN*Pan$cNvp%nQGK zgf1+sLh@Ib4PGN|7%ReG9wEP>4~afLGqeJWOLcf>c-i-;uCA{X!!me>s68NfmMVp} ze2)&+_vT$i+KFX=Y$Rj2|D0#(?`Tr^zn`T&DDL_`?Lsl`C?Xe=!h4TW8(khgdXy$N z^g(P*s*|6u!W;Ng0MZw}a+C}JPyK+Nuaoc52NP%D?|cpsrH1FI4nw4V<#SY++*gX@ z?yWP7H;oh=1U2sfD1_Cjvo4&v0$YFv3rw6zuu@*dvCzsG1BS!UmEljGqxsjY;;+lnA3BSp(FYK_bsD%I<32vphs(SZZ5q|84w4~#N z<~uF~2JEq4BhU>6v<#Q*6DUXc?Z-c)uHb~0C}d_5Sg9r&@iL~$%~LJe40?^dud*AT z`AJeI?Nwzz9)dYz_#587%!juhx86Sg)CjON`?!GB7S_s+{x-%V0AiCB4G{^@y}&gT~nm4v5r;Os6^@-pYW?BR@V#$ zR+bH7V>jTps=UFnF*|_cL2bJ{>uOnQf!z$c8nJ%pYF36CSm8%HEd97%qo%_xFVemd z)r~!?s+*w@)s596)lL5&RX0#+Rk8kkpFtluTbabc%z|OA)0O@ZFK)|`~8USpXy9NMJd~zawkr^G~ zciF3EAP8uU%vP3Vi|rLe3o?S8M{(T~*(2zxwjd z7ZK>#iF)79j^6w3Ki)g^*YkTMINJC5KY#edUk&~2=o3K?La-QIMBIvZi1$7B##@6K z{Pe2t#p7T3#s3K7Z_-?PC@j563xSHya)dI(i7h?t*&38o^ St?([0.5, 0.5]); const [noiseCap, setNoiseCap] = useState(0.12); + const [geometricConfig, setGeometricConfig] = useState(() => ({ + ...DEFAULT_GEOMETRIC_FEEDBACK_CONFIG, + })); const [examples, setExamples] = useState(0); const [addingExample, setAddingExample] = useState(false); const [busy, setBusy] = useState(false); @@ -186,6 +195,7 @@ export function ConsoleApp() { if (engine && !controllerRef.current) { controllerRef.current = new FeedbackController(engine, { spread: randomisationSpread, + geometricConfig, }); } @@ -210,6 +220,8 @@ export function ConsoleApp() { } useEffect( () => () => { + controllerRef.current?.dispose(); + controllerRef.current = null; explorationRef.current?.dispose(); explorationRef.current = null; }, @@ -248,6 +260,10 @@ export function ConsoleApp() { controllerRef.current?.setSpread(randomisationSpread); }, [engine, randomisationSpread]); + useEffect(() => { + controllerRef.current?.setGeometricConfig(geometricConfig); + }, [engine, geometricConfig]); + // Push the selected solo-mode + the arm mask into the controller whenever they // change (dock-spec §1.2). The controller RESPECTS the arm mask at the example // level in BOTH modes and forwards it to engine.feedback.setFocus. @@ -968,6 +984,8 @@ export function ConsoleApp() { xavierSpreadEnabled: settings.xavierSpreadEnabled, noiseCap, setNoiseCap, + geometricConfig, + setGeometricConfig, // learning-behaviour feedbackMode, setFeedbackMode, diff --git a/manifold/src/console/Drawers.tsx b/manifold/src/console/Drawers.tsx index ea94a44..7ab4685 100644 --- a/manifold/src/console/Drawers.tsx +++ b/manifold/src/console/Drawers.tsx @@ -31,7 +31,11 @@ import { outputModeDescriptor } from './output-mode'; import { useSettings, unfocusedIconCss } from '../settings/settings-store'; import type { UnfocusedIconColour, InputMapMode } from '../settings/settings-store'; import type { ExampleResizePolicy, NetworkResizePolicy } from '../engine/io-reshape'; -import { useEngine, useEngineVersion } from '../engine'; +import { + useEngine, + useEngineVersion, + DEFAULT_GEOMETRIC_FEEDBACK_CONFIG, +} from '../engine'; import { EditorPanel } from '../serial/EditorPanel'; import { TrainingHealth } from './TrainingHealth'; import { @@ -315,7 +319,76 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { Live training params - + {ctx.feedbackMode === 'geometric-dislike' ? ( + <> + v.toFixed(4)} + onChange={(learningRate) => + ctx.setGeometricConfig({ ...ctx.geometricConfig, learningRate }) + } + /> + `${Math.round(v)} Hz`} + onChange={(updatesPerSecond) => + ctx.setGeometricConfig({ ...ctx.geometricConfig, updatesPerSecond }) + } + /> + `${v.toFixed(1)} s`} + onChange={(seconds) => + ctx.setGeometricConfig({ + ...ctx.geometricConfig, + lifetimeMs: seconds * 1000, + }) + } + /> +
+ + + {ctx.geometricConfig.updatesPerSecond <= 0 || + ctx.geometricConfig.lifetimeMs <= 0 + ? 'one immediate update per press' + : `≈ ${Math.round( + (ctx.geometricConfig.updatesPerSecond * + ctx.geometricConfig.lifetimeMs) / + 1000, + )} replay updates + the press`} + +
+ + ) : ( + + )} )} diff --git a/manifold/src/console/types.ts b/manifold/src/console/types.ts index 5c4fa79..73ed15e 100644 --- a/manifold/src/console/types.ts +++ b/manifold/src/console/types.ts @@ -5,6 +5,7 @@ import type { MFMode, MFParam } from './model'; import type { FeedbackMode } from '../engine/types'; import type { BackendStatus } from '../backends/backend'; import type { UseInputLayer } from '../inputs'; +import type { GeometricFeedbackConfig } from '../engine'; /** The two product feedback modes (dock-spec §1.1; rl-feedback-design §0). */ export type FeedbackModeUI = 'explore-and-place' | 'geometric-dislike'; @@ -124,6 +125,8 @@ export interface ConsoleCtx { xavierSpreadEnabled: boolean; noiseCap: number; setNoiseCap: (v: number) => void; + geometricConfig: GeometricFeedbackConfig; + setGeometricConfig: (config: GeometricFeedbackConfig) => void; // ---- Learning-behaviour (dock-spec §1; rl-feedback-design) ---- feedbackMode: FeedbackModeUI; diff --git a/manifold/src/debug/probe.ts b/manifold/src/debug/probe.ts index d0c52e0..1f9ea58 100644 --- a/manifold/src/debug/probe.ts +++ b/manifold/src/debug/probe.ts @@ -52,6 +52,8 @@ export interface DebugProbe { * int (14=GeometricPush, 15=GeometricColdStart). */ dislikeGeometric(heardVec: ReadonlyArray, lr?: number): number; + setGeometricConfig(learningRate: number, updatesPerSecond: number, lifetimeMs: number): void; + advanceGeometric(dtSeconds: number): number; /** Feed a positive into the k-NN centroid (omit vec → live MLP output). */ storePositive(vec?: ReadonlyArray): void; /** Replay-memory sizes (Mode 1). */ @@ -174,6 +176,20 @@ function makeProbe(engine: EngineApi): DebugProbe { return a; }, + setGeometricConfig( + learningRate: number, + updatesPerSecond: number, + lifetimeMs: number, + ): void { + engine.feedback.setGeometricConfig({ learningRate, updatesPerSecond, lifetimeMs }); + }, + + advanceGeometric(dtSeconds: number): number { + const steps = engine.feedback.advanceGeometric(dtSeconds); + if (steps > 0) engine.process(); + return steps; + }, + storePositive(vec?: ReadonlyArray): void { engine.feedback.storePositive(vec ? Float32Array.from(vec) : undefined); }, diff --git a/manifold/src/engine/engine-api.ts b/manifold/src/engine/engine-api.ts index f775017..206fbec 100644 --- a/manifold/src/engine/engine-api.ts +++ b/manifold/src/engine/engine-api.ts @@ -23,6 +23,18 @@ import type { EngineId, FeedbackMode, LayerStats } from './types'; import { WasmIML } from './wasm-iml'; import type { IoMigration } from './io-reshape'; +export interface GeometricFeedbackConfig { + learningRate: number; + updatesPerSecond: number; + lifetimeMs: number; +} + +export const DEFAULT_GEOMETRIC_FEEDBACK_CONFIG: Readonly = { + learningRate: 0.001, + updatesPerSecond: 200, + lifetimeMs: 2500, +}; + export interface EngineFeedbackApi { /** Positive feedback (thumbs-up). Returns the FeedbackAction int. */ thumbsUp(): number; @@ -65,6 +77,10 @@ export interface EngineFeedbackApi { * FeedbackAction int (14=GeometricPush, 15=GeometricColdStart). */ dislikeGeometric(heardVec?: Float32Array, lr?: number): number; + /** Configure upstream-style replay dose and wall-clock lifetime. */ + setGeometricConfig(config: GeometricFeedbackConfig): void; + /** Advance replay by elapsed wall-clock time; returns optimise cycles run. */ + advanceGeometric(dtSeconds: number): number; /** Feed a positive (like) into the k-NN centroid (null → live MLP output). */ storePositive(vec?: Float32Array): void; positiveCount(): number; @@ -185,6 +201,13 @@ export class EngineApi { placedOutput: () => this.iml.feedbackPlacedOutput(), dislikeGeometric: (heardVec?: Float32Array, lr = 0) => this.iml.feedbackDislikeGeometric(heardVec, lr), + setGeometricConfig: (config) => + this.iml.feedbackSetGeometricConfig( + config.learningRate, + config.updatesPerSecond, + config.lifetimeMs, + ), + advanceGeometric: (dtSeconds) => this.iml.feedbackAdvanceGeometric(dtSeconds), storePositive: (vec?: Float32Array) => this.iml.feedbackStorePositive(vec), positiveCount: () => this.iml.feedbackPositiveCount(), negativeCount: () => this.iml.feedbackNegativeCount(), diff --git a/manifold/src/engine/index.ts b/manifold/src/engine/index.ts index b77c471..8ac898a 100644 --- a/manifold/src/engine/index.ts +++ b/manifold/src/engine/index.ts @@ -5,12 +5,17 @@ * (everything except EngineProvider/useEngine) imports NO React. */ -export { EngineApi, createEngine } from './engine-api'; +export { + EngineApi, + createEngine, + DEFAULT_GEOMETRIC_FEEDBACK_CONFIG, +} from './engine-api'; export type { EngineApiOptions, EngineAudioApi, EngineFeedbackApi, EngineExploreApi, + GeometricFeedbackConfig, } from './engine-api'; export { Spine } from './spine'; diff --git a/manifold/src/engine/types.ts b/manifold/src/engine/types.ts index b036502..a392379 100644 --- a/manifold/src/engine/types.ts +++ b/manifold/src/engine/types.ts @@ -114,6 +114,13 @@ export interface NispsModule { // controller default (1e-3). Returns the FeedbackAction int (14=GeometricPush, // 15=GeometricColdStart when no positives exist yet). _nisps_ml_feedback_dislike_geometric(ml: number, current_out_ptr: number, lr: number): number; + _nisps_ml_feedback_set_geometric_config( + ml: number, + lr: number, + update_hz: number, + lifetime_ms: number, + ): void; + _nisps_ml_feedback_advance_geometric(ml: number, dt_seconds: number): number; // Store a positive (like) into the replay memory so the k-NN centroid sees it. // current_out may be null (live output used). Caller still runs addExample+train. _nisps_ml_feedback_store_positive(ml: number, current_out_ptr: number): void; diff --git a/manifold/src/engine/wasm-iml.ts b/manifold/src/engine/wasm-iml.ts index 90cc053..457943e 100644 --- a/manifold/src/engine/wasm-iml.ts +++ b/manifold/src/engine/wasm-iml.ts @@ -978,6 +978,24 @@ export class WasmIML { return action; } + feedbackSetGeometricConfig(lr: number, updateHz: number, lifetimeMs: number): void { + this.module._nisps_ml_feedback_set_geometric_config( + this.mlHandle, + lr, + updateHz, + lifetimeMs, + ); + } + + feedbackAdvanceGeometric(dtSeconds: number): number { + const steps = this.module._nisps_ml_feedback_advance_geometric(this.mlHandle, dtSeconds); + if (steps > 0) { + this.sink.emit('ml.delta_update', { reason: 'geometric-replay' }); + this.scheduleSave_(); + } + return steps; + } + /** * Feed a positive (like) into the replay memory so the k-NN centroid sees it. * `vec` is the heard output at the liked input (null → the live MLP output). diff --git a/manifold/src/feedback/controller.ts b/manifold/src/feedback/controller.ts index 838f532..39f54cc 100644 --- a/manifold/src/feedback/controller.ts +++ b/manifold/src/feedback/controller.ts @@ -1,7 +1,7 @@ /** - * FeedbackController — framework-neutral learning-engine behaviour for the two - * feedback modes plus solo/arm, prototyped in pure TS on the EXISTING engine - * primitives (NO C++/WASM change). + * FeedbackController — framework-neutral browser driver for the two feedback + * modes plus solo/arm. Weight-affecting behaviour lives in the shared C++ core; + * this layer owns UI scheduling and caller-owned example storage. * * Authoritative design: docs/adr/rl-feedback-design.md (Mode 2 default; * Mode 1 selectable; SOLO default MaskGradients). Engine primitives audited in @@ -14,7 +14,7 @@ * setInput(x,y) / getOutputs() — synchronous forward inference (the spine) * process() — re-run last input after a weight change * addExample([x,y], outVec) — append a training example - * train() — SGD over the dataset + * train() — supervised training over the dataset * feedback.{setFocus,thumbsUp,dislikeGeometric,…} * — the SHARED C++ core's RL primitives * @@ -27,6 +27,10 @@ */ import type { FeedbackMode } from '../engine/types'; +import { + DEFAULT_GEOMETRIC_FEEDBACK_CONFIG, + type GeometricFeedbackConfig, +} from '../engine/engine-api'; /** The two product feedback modes (rl-feedback-design §0). */ export type ProtoFeedbackMode = 'explore-and-place' | 'geometric-dislike'; @@ -67,6 +71,8 @@ export interface ControllerEngine { // `heardVec` is the post-pipeline (HEARD) output; returns the FeedbackAction // int (14=GeometricPush, 15=GeometricColdStart). dislikeGeometric(heardVec?: Float32Array, lr?: number): number; + setGeometricConfig(config: GeometricFeedbackConfig): void; + advanceGeometric(dtSeconds: number): number; positiveCount(): number; negativeCount(): number; // ExploreAndPlace lifecycle — the SHARED C++ core (mode 'explore_and_place'). @@ -112,12 +118,16 @@ export interface FeedbackControllerOptions { spread?: number; /** Nudge perturbation standard deviation (small bounded weight jitter). */ nudgeStddev?: number; + geometricConfig?: GeometricFeedbackConfig; } export class FeedbackController { private engine: ControllerEngine; private spread: number; private nudgeStddev: number; + private geometricConfig: GeometricFeedbackConfig; + private geometricTimer: ReturnType | null = null; + private geometricLastTickMs = 0; private mode: ProtoFeedbackMode = 'explore-and-place'; private soloMode: ProtoSoloMode = 'mask-gradients'; @@ -147,6 +157,10 @@ export class FeedbackController { this.engine = engine; this.spread = opts.spread ?? 0; this.nudgeStddev = opts.nudgeStddev ?? 0.05; + this.geometricConfig = { + ...(opts.geometricConfig ?? DEFAULT_GEOMETRIC_FEEDBACK_CONFIG), + }; + this.engine.feedback.setGeometricConfig(this.geometricConfig); } // =================================================================== @@ -158,6 +172,7 @@ export class FeedbackController { // Switching mode aborts any active scratchpad session. For explore-and-place // the SHARED C++ core owns the scratchpad, so delegate the teardown to it. if (this.exploringFlag) this.cancel(); + if (mode !== 'geometric-dislike') this.stopGeometricReplay(); this.mode = mode; // Keep the C++ core's feedback mode in lockstep so the shared explore-and- // place lifecycle is active when this mode is selected. @@ -176,11 +191,29 @@ export class FeedbackController { this.spread = spread; } + setGeometricConfig(config: GeometricFeedbackConfig): void { + this.geometricConfig = { + learningRate: Math.max(0.000001, config.learningRate), + updatesPerSecond: Math.max(0, config.updatesPerSecond), + lifetimeMs: Math.max(0, config.lifetimeMs), + }; + this.engine.feedback.setGeometricConfig(this.geometricConfig); + if (this.geometricTimer) { + this.stopGeometricReplay(); + if (this.engine.feedback.negativeCount() > 0) this.startGeometricReplay(); + } + } + + getGeometricConfig(): Readonly { + return this.geometricConfig; + } + /** * An I/O identity edit resets the core's index-aligned scratch/replay state. * Mirror that reset locally without issuing another core transition. */ resetAfterIoChange(): void { + this.stopGeometricReplay(); this.exploringFlag = false; this.pickingFlag = false; this.anchors = []; @@ -351,14 +384,15 @@ export class FeedbackController { /** * DISLIKE (thumbs-down in Mode 1). Push the current mapping away from the liked * centroid — the SHARED C++ core's geometric push (nisps/ml/geo_push.hpp + - * replay.hpp + mlp.train_targets, ported from upstream InterfaceRL 0a541cc). + * replay.hpp + mlp.train_targets, ported from upstream InterfaceRL e291192). * * The core uses the MLP's CURRENT input and the passed HEARD output vector: * 1. stores the negative (input, a_neg) in the ReplayStore (dedup within 0.05) * 2. k-NN(k=4) centroid of positives near the input - * 3. target[j] = clamp(a_neg[j] + dir/||dir|| · pushStep/(1+||dir||), 0, 1) - * 4. trains toward that target at lr·negLRRatio - * 5. cold-start fallback (negative-LR) when there are no positives yet. + * 3. target[j] = clamp(a_neg[j] + dir/||dir|| · pushStep, 0, 1) + * 4. trains all live negatives once immediately, then at the configured + * replay rate until each rejection reaches its wall-clock lifetime + * 5. uses a deterministic random direction when there are no positives yet. * Soloed/active dims come from the core's focus mask (set via setArmMask). * * @param output the HEARD (post-pipeline) output vector a_neg. MUST be the @@ -369,9 +403,44 @@ export class FeedbackController { dislike(output: Float32Array): number { const action = this.engine.feedback.dislikeGeometric(output); this.engine.process(); + this.startGeometricReplay(); return action; } + private startGeometricReplay(): void { + this.stopGeometricReplay(); + if ( + this.mode !== 'geometric-dislike' || + this.geometricConfig.updatesPerSecond <= 0 || + this.geometricConfig.lifetimeMs <= 0 || + this.engine.feedback.negativeCount() <= 0 + ) { + return; + } + this.geometricLastTickMs = Date.now(); + const intervalMs = Math.max( + 5, + Math.min(50, 1000 / this.geometricConfig.updatesPerSecond), + ); + this.geometricTimer = setInterval(() => { + const now = Date.now(); + const dtSeconds = Math.max(0, (now - this.geometricLastTickMs) / 1000); + this.geometricLastTickMs = now; + const steps = this.engine.feedback.advanceGeometric(dtSeconds); + if (steps > 0) this.engine.process(); + if (this.engine.feedback.negativeCount() <= 0) this.stopGeometricReplay(); + }, intervalMs); + } + + private stopGeometricReplay(): void { + if (this.geometricTimer !== null) clearInterval(this.geometricTimer); + this.geometricTimer = null; + } + + dispose(): void { + this.stopGeometricReplay(); + } + /** * LIKE + train (thumbs-up in Mode 1). Feeds the positive into the C++ core's * k-NN centroid (via the core's thumbsUp auto-store, ADR §2.1) AND stores the diff --git a/manifold/tests/e2e/geo-dislike.spec.ts b/manifold/tests/e2e/geo-dislike.spec.ts index f96da48..3192887 100644 --- a/manifold/tests/e2e/geo-dislike.spec.ts +++ b/manifold/tests/e2e/geo-dislike.spec.ts @@ -70,4 +70,36 @@ test.describe('geometric dislike (Mode 1) — core-backed', () => { expect(countChanged(before, after, 1e-4)).toBeGreaterThan(0); expect(allWithin(after, 0, 1)).toBe(true); }); + + test('parameterised replay applies configured dose then expires by wall time', async ({ page }) => { + const result = await page.evaluate((heard) => { + const p = window.__nisps!; + p.setFeedbackMode('avoid'); + p.setAvoidStyle(0); + p.setGeometricConfig(0.001, 20, 100); + p.setInputs(0.4, 0.6); + p.dislikeGeometric(heard); + const first = p.advanceGeometric(0.05); + const live = p.feedbackCounts().negative; + const second = p.advanceGeometric(0.05); + const expired = p.feedbackCounts().negative; + return { first, live, second, expired }; + }, HEARD); + + expect(result).toEqual({ first: 1, live: 1, second: 1, expired: 0 }); + }); + + test('expanded Learning panel exposes the upstream-default experiment controls', async ({ page }) => { + await page.getByTitle('Learning', { exact: true }).click(); + await page.getByTitle('Expand', { exact: true }).click(); + + await expect(page.getByText('push · learning rate', { exact: true })).toBeVisible(); + await expect(page.getByText('push · updates / second', { exact: true })).toBeVisible(); + await expect(page.getByText('push · lifetime', { exact: true })).toBeVisible(); + await expect(page.getByText('0.0010', { exact: true })).toBeVisible(); + await expect(page.getByText('200 Hz', { exact: true })).toBeVisible(); + await expect(page.getByText('2.5 s', { exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Upstream defaults' })).toBeVisible(); + await expect(page.getByText('≈ 500 replay updates + the press')).toBeVisible(); + }); }); diff --git a/nisps/ml/dynamic_storage.hpp b/nisps/ml/dynamic_storage.hpp index daf1fdf..de95e3e 100644 --- a/nisps/ml/dynamic_storage.hpp +++ b/nisps/ml/dynamic_storage.hpp @@ -249,6 +249,7 @@ class DynamicFeedbackStorage { + replay_cap * n_in // replay inputs + replay_cap * n_out // replay actions + replay_cap // replay rewards + + replay_cap // replay ages (ms) + n_out * 2u // centroid + target + focus_floats; arena_ = new (std::nothrow) float[total](); @@ -260,7 +261,8 @@ class DynamicFeedbackStorage { 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_replay_age_ = off_replay_r_ + replay_cap_; + off_centroid_ = off_replay_age_ + replay_cap_; off_target_ = off_centroid_ + n_out_; off_focus_ = off_target_ + n_out_; } @@ -302,6 +304,7 @@ class DynamicFeedbackStorage { 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 replay_ages_ms() noexcept { return {arena_ + off_replay_age_, 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 { @@ -318,7 +321,8 @@ class DynamicFeedbackStorage { 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_replay_r_ = o.off_replay_r_; off_replay_age_ = o.off_replay_age_; + off_centroid_ = o.off_centroid_; off_target_ = o.off_target_; arena_ = o.arena_; o.arena_ = nullptr; @@ -328,7 +332,7 @@ class DynamicFeedbackStorage { std::size_t off_placed_ = 0u, off_snap_ = 0u, off_scratch_ = 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; + off_replay_age_ = 0u, off_target_ = 0u; float* arena_ = nullptr; }; diff --git a/nisps/ml/feedback.hpp b/nisps/ml/feedback.hpp index 94b15e3..1c55516 100644 --- a/nisps/ml/feedback.hpp +++ b/nisps/ml/feedback.hpp @@ -18,9 +18,9 @@ // - Geometric (DEFAULT) — the ported firmware k-NN // centroid push-away, backed by the controller's // own ReplayMemory (see dislike_geometric() below). -// Cold-starts with a negative-LR fallback until the -// first positive is stored. The old "geometric push -// not ported" note is stale — it IS ported. +// Replays all live negatives for a parameterised +// wall-clock window; cold start uses a deterministic +// random direction until a positive is stored. // - Diffuse — the pre-P3 undirected MLP::move_weights // perturb. Deliberately-retained research reserve, // not the product default (see its definition below @@ -138,7 +138,7 @@ enum class FeedbackAction : std::uint8_t { 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. + GeometricColdStart = 15, // no positives yet: random-direction push ran; UI shows the cold-start prompt. }; // --------------------------------------------------------------------------- @@ -180,6 +180,7 @@ class FixedFeedbackStorage { 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_; } + NISPS_FORCE_INLINE std::span replay_ages_ms() noexcept { return replay_age_ms_; } // 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_; } @@ -194,6 +195,7 @@ class FixedFeedbackStorage { std::array replay_in_{}; std::array replay_act_{}; std::array replay_rew_{}; + std::array replay_age_ms_{}; std::array centroid_{}; std::array target_{}; }; @@ -230,6 +232,17 @@ class FeedbackControllerCore : public FbStorage { // InterfaceRL default 1e-3, pre-scaling). void set_geo_lr(float lr) noexcept { geo_lr_ = lr; } float geo_lr() const noexcept { return geo_lr_; } + void set_geo_update_hz(float hz) noexcept { + geo_update_hz_ = (hz > 0.f) ? hz : 0.f; + geo_step_accum_ = 0.f; + if (!(geo_update_hz_ > 0.f)) replay_().remove_all_negatives(); + } + float geo_update_hz() const noexcept { return geo_update_hz_; } + void set_geo_lifetime_ms(float ms) noexcept { + geo_lifetime_ms_ = (ms > 0.f) ? ms : 0.f; + if (!(geo_lifetime_ms_ > 0.f)) replay_().remove_all_negatives(); + } + float geo_lifetime_ms() const noexcept { return geo_lifetime_ms_; } // `exploring()` is true whenever a scratchpad net is live and learning is // paused — for the legacy RANDOMISE_* modes, AND for ExploreAndPlace in @@ -393,9 +406,9 @@ 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). + // Geometric dislike (rl-feedback-design §2.1): press stores the rejection + // and performs one immediate update; advance_geometric() supplies the + // repeated, wall-clock-bounded optimise dose used by current upstream. // ========================================================================= std::size_t replay_size() const noexcept { return replay_count_; } @@ -416,18 +429,12 @@ class FeedbackControllerCore : public FbStorage { // 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). - // 2. k-NN(4) positive centroid — or zeros when nothing is liked yet — - // → push-away target → train toward it at lr * negLRRatio, gated by - // the focus/solo mask. With no positives the push direction is - // random per dim, which is upstream's cold start (there is no - // separate negative-LR branch any more; see the body). - // 3. proportional decay + eviction of expired negatives. + // 2. immediately optimise all live negatives once. Subsequent updates + // come from advance_geometric(). 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; @@ -436,44 +443,46 @@ class FeedbackControllerCore : public FbStorage { // 1. store/deepen the negative (InterfaceRL.cpp:42-66). replay.deepen_or_store_negative(x_neg, a_neg); - 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(); - - // 2+3. ONE path, as upstream (InterfaceRL.tpp:696-761). With no likes - // stored there is no centroid to push away from, so the centroid is - // zeros and `have_positives=false` sends every dim in a random - // direction — upstream's `useRandom = !havePositives || ...`. We still - // report GeometricColdStart so the caller can show its "like a few - // sounds first" prompt, but the TRAINING is the same push at the same - // LR either way. (Until 2026-07-25 the cold start instead trained AWAY - // from the heard action at a NEGATIVE lr — a port of the older - // `0a541cc` fallback that upstream has since replaced.) - auto mean = this->centroid_buf(); - const bool have_positives = (pos_total > 0u); - if (have_positives) { - (void)replay.knn_positive_centroid(x_neg, kCentroidK, mean); - } else { - for (std::size_t j = 0; j < n_out; ++j) mean[j] = 0.f; + const bool have_positives = replay.positive_count() > 0u; + (void)optimise_geometric_once_(mlp, lr); + if (!(geo_update_hz_ > 0.f) || !(geo_lifetime_ms_ > 0.f)) { + replay.remove_all_negatives(); } - - 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), have_positives, - 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_()); - - const FeedbackAction action = have_positives + return have_positives ? FeedbackAction::GeometricPush : FeedbackAction::GeometricColdStart; + } - // 4. decay + evict expired negatives (InterfaceRL.cpp:752-760). - replay.decay_negatives(); + // Advance the upstream-style replay optimiser by elapsed wall-clock time. + // Returns the number of optimisation cycles applied. A long scheduler gap + // does not create an unbounded catch-up burst: upstream also cannot execute + // missed loop iterations while blocked. Ages still advance by the full dt. + template + std::size_t advance_geometric(M& mlp, float dt_seconds) noexcept { + if (mode_ != FeedbackMode::Avoid || avoid_style_ != AvoidStyle::Geometric || + !(dt_seconds > 0.f)) { + return 0u; + } + auto replay = replay_(); + if (replay.negative_count() == 0u) { + geo_step_accum_ = 0.f; + return 0u; + } - return action; + constexpr float kMaxDoseDtSeconds = 0.1f; + const float dose_dt = (dt_seconds < kMaxDoseDtSeconds) + ? dt_seconds + : kMaxDoseDtSeconds; + geo_step_accum_ += dose_dt * geo_update_hz_; + std::size_t steps = static_cast(geo_step_accum_); + geo_step_accum_ -= static_cast(steps); + + for (std::size_t i = 0; i < steps; ++i) { + if (!optimise_geometric_once_(mlp, geo_lr_)) break; + } + replay.advance_negative_ages(dt_seconds * 1000.f, geo_lifetime_ms_); + if (replay.negative_count() == 0u) geo_step_accum_ = 0.f; + return steps; } // ========================================================================= @@ -661,8 +670,43 @@ class FeedbackControllerCore : public FbStorage { // 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_); + this->replay_rewards(), this->replay_ages_ms(), + this->n_in(), this->n_out(), this->replay_cap(), + replay_count_); + } + + // One upstream-style optimise cycle over ALL live negatives. Unlike the + // upstream cursor-coupled implementation, the positive centroid is looked + // up at each negative's own stored input, so moving the cursor cannot + // reinterpret an older rejection. + template + bool optimise_geometric_once_(M& mlp, float lr) noexcept { + auto replay = replay_(); + const std::size_t pos_total = replay.positive_count(); + const std::size_t neg_total = replay.negative_count(); + if (neg_total == 0u) return false; + const float avg_neg = replay.avg_negative_reward(); + const std::size_t n_out = this->n_out(); + const float ratio = geo_neg_lr_ratio(neg_total, pos_total); + + for (std::size_t i = 0; i < replay.size(); ++i) { + if (replay.reward(i) > 0.f) continue; + const auto x_neg = replay.input(i); + const auto a_neg = replay.action(i); + auto mean = this->centroid_buf(); + const bool have_positives = + replay.knn_positive_centroid(x_neg, kCentroidK, mean) > 0u; + if (!have_positives) { + for (std::size_t j = 0; j < n_out; ++j) mean[j] = 0.f; + } + auto target = this->target_buf(); + compute_push_target(a_neg, std::span(mean.data(), n_out), + focus_span_(), geo_push_step(avg_neg), + have_positives, rng_, target); + mlp.train_targets(x_neg, std::span(target.data(), n_out), + lr * ratio, focus_span_()); + } + return true; } // The focus mask as the geometric active-dims gate (empty ⇒ all active). @@ -795,6 +839,9 @@ class FeedbackControllerCore : public FbStorage { // ---- Geometric dislike state --------------------------------------------- std::size_t replay_count_ = 0u; float geo_lr_ = 0.001f; // upstream InterfaceRL.hpp:312 + float geo_update_hz_ = 200.f; // upstream default optimise loop + float geo_lifetime_ms_ = 2500.f; // upstream kDislikeLifetimeMs + float geo_step_accum_ = 0.f; // ---- ExploreAndPlace state ---------------------------------------------- ExploreState ep_state_ = ExploreState::Idle; diff --git a/nisps/ml/replay.hpp b/nisps/ml/replay.hpp index 1aeb059..fc0cae5 100644 --- a/nisps/ml/replay.hpp +++ b/nisps/ml/replay.hpp @@ -5,7 +5,7 @@ // * `_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). +// * wall-clock negative lifetime + eviction (current upstream e291192). // // STORAGE: the buffers live in the feedback controller's storage policy // (nisps/ml/feedback.hpp — fixed std::array on firmware, arena slice in the @@ -29,22 +29,21 @@ namespace nisps::ml { -// Upstream constants (InterfaceRL.hpp:293-296, .cpp:42-66,664). +// Upstream constants (InterfaceRL.hpp / InterfaceRL.tpp @ e291192). 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, +// cap×n_out, rewards/ages: 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), + std::span rewards, std::span ages_ms, + 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), ages_ms_(ages_ms), n_in_(n_in), n_out_(n_out), cap_(cap), count_(count) {} std::size_t size() const noexcept { return count_; } @@ -57,6 +56,7 @@ class ReplayView { return actions_.subspan(i * n_out_, n_out_); } float reward(std::size_t i) const noexcept { return rewards_[i]; } + float age_ms(std::size_t i) const noexcept { return ages_ms_[i]; } std::size_t positive_count() const noexcept { std::size_t n = 0u; @@ -113,6 +113,7 @@ class ReplayView { 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]; + ages_ms_[i] = 0.f; return true; } } @@ -162,21 +163,21 @@ class ReplayView { 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 { + // Advance wall-clock age for every negative and remove those which have + // lived their configured full-strength window. Positives do not expire. + // Current upstream uses a timestamp and kDislikeLifetimeMs=2500; explicit + // elapsed time keeps the core deterministic on firmware, native, and WASM. + std::size_t advance_negative_ages(float dt_ms, float lifetime_ms) noexcept { + if (!(dt_ms > 0.f) || !(lifetime_ms > 0.f)) return 0u; 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) { + ages_ms_[i] += dt_ms; + if (ages_ms_[i] >= lifetime_ms) { evict_(i); ++evicted; - continue; // same index now holds the next item + continue; } } ++i; @@ -184,6 +185,17 @@ class ReplayView { return evicted; } + void remove_all_negatives() noexcept { + std::size_t i = 0u; + while (i < count_) { + if (rewards_[i] <= 0.f) { + evict_(i); + continue; + } + ++i; + } + } + void clear() noexcept { count_ = 0u; } private: @@ -207,6 +219,7 @@ class ReplayView { 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; + ages_ms_[slot] = 0.f; } // Remove item i, shifting everything after it down one slot. @@ -219,6 +232,7 @@ class ReplayView { 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]; + ages_ms_[m - 1u] = ages_ms_[m]; } --count_; } @@ -226,6 +240,7 @@ class ReplayView { std::span inputs_; std::span actions_; std::span rewards_; + std::span ages_ms_; std::size_t n_in_; std::size_t n_out_; std::size_t cap_; diff --git a/nisps/wasm/bindings.cpp b/nisps/wasm/bindings.cpp index 05d7345..cb8105b 100644 --- a/nisps/wasm/bindings.cpp +++ b/nisps/wasm/bindings.cpp @@ -403,6 +403,9 @@ int nisps_ml_reshape(void* ml, int input_size, int output_size, const auto feedback_mode = h->feedback.mode(); const auto avoid_style = h->feedback.avoid_style(); + const float geo_lr = h->feedback.geo_lr(); + const float geo_update_hz = h->feedback.geo_update_hz(); + const float geo_lifetime_ms = h->feedback.geo_lifetime_ms(); BrowserMLP fresh(h->seed64, d.n_in, std::span(d.hidden, 3u), d.n_out); if (!fresh.valid()) return 0; fresh.draw_weights(spread); @@ -412,6 +415,9 @@ int nisps_ml_reshape(void* ml, int input_size, int output_size, kFeedbackUndoDepth, d.n_in, kFeedbackReplayCap); if (!fb.valid()) return 0; fb.set_avoid_style(avoid_style); + fb.set_geo_lr(geo_lr); + fb.set_geo_update_hz(geo_update_hz); + fb.set_geo_lifetime_ms(geo_lifetime_ms); fb.set_mode(feedback_mode, fresh); h->mlp = static_cast(fresh); @@ -627,10 +633,16 @@ void nisps_ml_feedback_reset(void* ml) { auto* h = static_cast(ml); const auto feedback_mode = h->feedback.mode(); const auto avoid_style = h->feedback.avoid_style(); + const float geo_lr = h->feedback.geo_lr(); + const float geo_update_hz = h->feedback.geo_update_hz(); + const float geo_lifetime_ms = h->feedback.geo_lifetime_ms(); BrowserFeedback fb(h->seed64 ^ kFeedbackSalt, h->n_out(), h->mlp.weight_count(), kFeedbackUndoDepth, h->n_in(), kFeedbackReplayCap); if (!fb.valid()) return; fb.set_avoid_style(avoid_style); + fb.set_geo_lr(geo_lr); + fb.set_geo_update_hz(geo_update_hz); + fb.set_geo_lifetime_ms(geo_lifetime_ms); fb.set_mode(feedback_mode, h->mlp); h->feedback = static_cast(fb); h->feedback_static_scratch.assign(h->n_out(), 0.f); @@ -817,6 +829,29 @@ int nisps_ml_feedback_dislike_geometric(void* ml, const float* current_out, floa return static_cast(h->feedback.dislike_geometric(h->mlp, out, use_lr)); } +// Configure the replay dose. Non-negative values are accepted; zero update +// rate or lifetime gives one-shot press behaviour. Defaults are upstream: +// 1e-3, 200 Hz, 2500 ms. +EMSCRIPTEN_KEEPALIVE +void nisps_ml_feedback_set_geometric_config(void* ml, float lr, + float update_hz, + float lifetime_ms) { + if (!ml) return; + auto* h = static_cast(ml); + if (lr > 0.f) h->feedback.set_geo_lr(lr); + h->feedback.set_geo_update_hz(update_hz); + h->feedback.set_geo_lifetime_ms(lifetime_ms); +} + +// Deterministic elapsed-time seam. The host owns the clock; every weight- +// affecting step remains in the shared C++ core. Returns optimise cycles run. +EMSCRIPTEN_KEEPALIVE +int nisps_ml_feedback_advance_geometric(void* ml, float dt_seconds) { + if (!ml) return 0; + auto* h = static_cast(ml); + return static_cast(h->feedback.advance_geometric(h->mlp, dt_seconds)); +} + // 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. diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 5f33ea1..c5f5708 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -56,6 +56,7 @@ EXPORTED_FUNCS='[ "_nisps_ml_feedback_undo_depth", "_nisps_ml_feedback_placed_output", "_nisps_ml_feedback_dislike_geometric","_nisps_ml_feedback_store_positive", + "_nisps_ml_feedback_set_geometric_config","_nisps_ml_feedback_advance_geometric", "_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", diff --git a/tests/cpp/parity_check.cpp b/tests/cpp/parity_check.cpp index 19fac10..941d969 100644 --- a/tests/cpp/parity_check.cpp +++ b/tests/cpp/parity_check.cpp @@ -281,10 +281,10 @@ int main(int argc, char** argv) { // ---- 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). + // a probed input. Eight 5ms driver ticks cover the parameterised + // upstream-style 200 Hz replay seam over all live negatives. 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) { @@ -316,6 +316,9 @@ int main(int argc, char** argv) { }; dislike_at(0.25f, 0.75f); dislike_at(0.26f, 0.74f); // within dedup radius → deepen + push + for (int i = 0; i < 8; ++i) { + fb.advance_geometric(mlp, 0.005f); + } payload.push_back(static_cast(fb.positive_count())); payload.push_back(static_cast(fb.negative_count())); diff --git a/tests/cpp/parity_wasm.mjs b/tests/cpp/parity_wasm.mjs index 29cdc19..813ed26 100644 --- a/tests/cpp/parity_wasm.mjs +++ b/tests/cpp/parity_wasm.mjs @@ -104,6 +104,7 @@ 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']), + feedbackAdvanceGeometric: cwrap('nisps_ml_feedback_advance_geometric', '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']), @@ -333,9 +334,9 @@ async function main() { // --- 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. + // the Avoid+Geometric on_up path, then two dislikes (second deepens) plus + // eight 5ms replay ticks train toward the computed push-away target. f32 + // arithmetic for the "heard" vector via Math.fround matches native exactly. const FB_AVOID = 0; api.feedbackSetMode(ml, FB_AVOID); @@ -368,6 +369,7 @@ async function main() { }; dislikeAt(0.25, 0.75); dislikeAt(0.26, 0.74); // within dedup radius: deepen + push + for (let i = 0; i < 8; i++) api.feedbackAdvanceGeometric(ml, 0.005); feedbackFloats.push(api.feedbackPositiveCount(ml)); feedbackFloats.push(api.feedbackNegativeCount(ml)); diff --git a/tests/cpp/test_mlp_geo_dislike.cpp b/tests/cpp/test_mlp_geo_dislike.cpp index f8cb4df..f626e2d 100644 --- a/tests/cpp/test_mlp_geo_dislike.cpp +++ b/tests/cpp/test_mlp_geo_dislike.cpp @@ -3,9 +3,8 @@ // // 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. +// the liked centroid), cold-start posMemCount==0 fallback, parameterised replay +// dose/lifetime, solo/focus gating, and fixed-seed determinism. #include #include @@ -36,10 +35,11 @@ struct RawReplay { std::array in{}; std::array act{}; std::array rew{}; + std::array age_ms{}; std::size_t count = 0u; ReplayView view() { - return ReplayView(in, act, rew, kNIn, kNOut, kCap, count); + return ReplayView(in, act, rew, age_ms, kNIn, kNOut, kCap, count); } }; @@ -118,24 +118,27 @@ NISPS_TEST(replay_knn_centroid_deterministic_tie_break) { NISPS_EXPECT(used_after_neg == 3u); } -NISPS_TEST(replay_decay_and_evict) { +NISPS_TEST(replay_wall_clock_lifetime_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)); + r.store(-1.f, 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 + std::size_t evicted = r.advance_negative_ages(2499.f, 2500.f); + NISPS_EXPECT(evicted == 0u); + NISPS_EXPECT(r.size() == 1u); + NISPS_EXPECT_NEAR(r.age_ms(0), 2499.f, 1e-6); + evicted = r.advance_negative_ages(1.f, 2500.f); NISPS_EXPECT(evicted == 1u); NISPS_EXPECT(r.size() == 0u); - // Positives are never decayed/evicted. + // Positives do not age or expire. r.store(1.f, std::span(x), std::span(a)); - evicted = r.decay_negatives(); + evicted = r.advance_negative_ages(5000.f, 2500.f); NISPS_EXPECT(evicted == 0u); NISPS_EXPECT(r.size() == 1u); + NISPS_EXPECT(r.age_ms(0) == 0.f); } // -- compute_push_target -------------------------------------------------------- @@ -302,6 +305,38 @@ NISPS_TEST(geo_dislike_deterministic_under_fixed_seed) { } } +NISPS_TEST(geo_dislike_replay_rate_and_lifetime_are_parameterised) { + GeoMLP m(88ull); + m.draw_weights(0.5f); + GeoFB fb(99ull); + NISPS_EXPECT(fb.geo_lr() == 0.001f); + NISPS_EXPECT(fb.geo_update_hz() == 200.f); + NISPS_EXPECT(fb.geo_lifetime_ms() == 2500.f); + + fb.set_geo_lr(0.002f); + fb.set_geo_update_hz(20.f); + fb.set_geo_lifetime_ms(100.f); + set_inputs(m, 0.4f, 0.6f); + m.process(); + fb.on_down(m, {}, 0.1f, 0.5f, {}); // one immediate update + NISPS_ASSERT(fb.negative_count() == 1u); + + // 20 Hz gives one replay update per 50 ms. At 100 ms the rejection + // expires after its second scheduled update. + NISPS_EXPECT(fb.advance_geometric(m, 0.025f) == 0u); + NISPS_EXPECT(fb.advance_geometric(m, 0.025f) == 1u); + NISPS_EXPECT(fb.negative_count() == 1u); + NISPS_EXPECT(fb.advance_geometric(m, 0.05f) == 1u); + NISPS_EXPECT(fb.negative_count() == 0u); + NISPS_EXPECT(fb.advance_geometric(m, 0.05f) == 0u); + + // Zero rate is an explicit one-shot mode: the press still updates once, + // but no stale negative remains to affect a later press. + fb.set_geo_update_hz(0.f); + fb.on_down(m, {}, 0.1f, 0.5f, {}); + NISPS_EXPECT(fb.negative_count() == 0u); +} + NISPS_TEST(geo_dislike_diffuse_style_preserves_legacy_path) { GeoMLP m(9ull); GeoFB fb(9ull);