memlnaut-nisps/nisps/wasm
monkey-w1n5t0n b16f26e6ab refactor(ml): one runtime-configurable training default (S26)
The operator's call: "there should be one default learning rate and one
default max iterations and they should both be configurable at runtime."

There were SIX copies, not the four the audit described, and they did not
agree:

  nisps/ml/mlp.hpp        no-arg train() hardcoding 1.f / 1000u / 0.001f —
                          and firmware's ONLY training path calls exactly
                          this, so firmware had no runtime knob at all
  wasm-iml.ts             train() and trainAsync() TS default params (x2)
  engine-api.ts           learningRate ?? 1.0, with no maxIterations knob
  vcv/src/iml.hpp         200 / 0.1 / 0.00001 — silently divergent
  external_synth_midi.hpp its own kDefaultLearningRate/kDefaultMaxIterations
  schemas/modes/*.json    x9, identical, read by nobody at runtime

Now: schemas/ml_defaults.json is the single declaration (validated against a
sibling meta-schema, matching the midi_device.schema.json convention), codegen
emits it to C++ and TS in the same run, and MLPCore carries a TrainConfig whose
default member initialisers read the generated constant.
set_train_config()/nisps_ml_set_train_config() make it runtime-overridable on
every target; the explicit-argument train() overload is untouched. min_error
joins the tuple — it was duplicated identically and belongs with the other two.

The per-mode ml block loses default_learning_rate/default_max_iterations.
default_spread stays (genuinely wired on both targets) and input_channels stays
(codegen-time validated, real information for sound_analysis_midi).

VCV BEHAVIOUR CHANGE, deliberate: MEMLNaut.cpp constructs IML positionally and
relies on those defaults, so the module moves to 1000/1.0/0.001 — 5x the max
iterations, 10x the learning rate, and a 100x looser early-stop threshold. The
old values were never justified anywhere; they arrived with fbc68eb alongside
an unrelated module rewrite and no tuning rationale. Firmware and WASM have
shipped 1.0/1000 all along. It is now runtime-settable if this turns out worse.

The generated header lands in nisps/ml/generated/, not nisps/modes/generated/
where the rest of codegen output lives: training hyperparameters are an ML
fact, and nisps/ml sits below nisps/modes, so emitting them there would make
mlp.hpp include upward. The agent that built this flagged the directory-crossing
rather than hiding it; this is the fix. CI's generated-freshness gate learns the
new directory.

Gates: run-all-tests.sh ALL GREEN — 4/4 ctest, parity PASS (max delta 2.38e-7),
lint clean, manifold typecheck + 17 unit + 33 e2e (which exercise train() and
trainAsync() through a real browser).
2026-07-21 17:20:10 +02:00
..
bindings.cpp refactor(ml): one runtime-configurable training default (S26) 2026-07-21 17:20:10 +02:00
README.md refactor(wasm): delete 12 dead C-API entries and the weights-publish channel 2026-07-21 12:48:50 +02:00

nisps/wasm

Emscripten target that exposes nisps/ml (MLP) and nisps/engines (audio engines) to the browser apps via a flat C ABI.

This directory is a leaf — it does not export headers for inclusion by other C++ code. The only artifact is bindings.cpp plus the build script that turns it into manifold/public/nisps.{wasm,js} (with a transitional copy to playground/public/ until P1 of docs/specs/plans/one-core-engine-refactor.md retires the playground).

Building

scripts/build-wasm.sh

Requires emcc (Emscripten). The script defaults to /usr/lib/emscripten/emcc and respects an EMCC env var override.

Output:

  • manifold/public/nisps.wasm — the compiled module.
  • manifold/public/nisps.js — Emscripten glue (factory function createNispsModule, MODULARIZE=1).

Both files are committed (so the browser apps work from a fresh clone without a C++ toolchain). Re-run build-wasm.sh after changes to nisps/{core,ml,engines,wasm}.

Architecture (runtime-shaped since one-core-engine P2)

The browser MLP is MLPCore<DynamicStorage> (nisps/ml/dynamic_storage.hpp): nisps_ml_create(input, output, hidden[3], n, seed) HONOURS its dimensions. The 4-layer topology (ReLU×3 + Sigmoid) is fixed; only the dimensions are runtime, capped at 4096 per dim. Non-positive/null arguments fall back to the historical defaults:

32 inputs → [10, 14, 18] hidden → 126 outputs

nisps_ml_reshape(ml, in, out, hidden, n, spread) constructs a new net at the requested shape, warm-starts it by copying the overlapping weight region (nisps/ml/warm_start.hpp), and swaps it in. The C-side dataset and the feedback controller state RESET on reshape (front-end shows a confirm modal). Heap is used only at create/reshape time, never per-call; the firmware target never compiles the dynamic storage at all (#error under NISPS_TARGET_EMBEDDED).

C API surface

See bindings.cpp for the full list. Summary:

Group Functions
ML life nisps_ml_create, nisps_ml_destroy, nisps_ml_reshape
ML I/O nisps_ml_set_input, nisps_ml_process, nisps_ml_outputs, nisps_ml_infer_batch
Training nisps_ml_add_example, nisps_ml_train, nisps_ml_eval_loss, nisps_ml_clear_examples
Weights nisps_ml_weight_count, nisps_ml_get_weights, nisps_ml_set_weights, nisps_ml_draw_weights
Diag nisps_ml_get_layer_stats, nisps_ml_describe
Engines nisps_engine_create, nisps_engine_destroy, nisps_engine_set_params, nisps_engine_process_block

Engine-id strings follow the C++ engine_id() constexpr accessors: thru, paf_synth, channel_strip, xiasri, verb_fx, memlcelium, breakor, elysiamorf, analysis. Unknown ids fall back to thru (silent passthrough).