diff --git a/MAP.md b/MAP.md index eeb4c6b..ef37aa6 100644 --- a/MAP.md +++ b/MAP.md @@ -84,7 +84,7 @@ anchor + locked decisions) and the `docs/specs/*-spec.md` set. `backends/base-backend.ts` is its output-side counterpart (status + throttle + lastSent) used by the midi/osc/vcv transports. - `manifold/src/feedback/` — `controller.ts` (Explore-and-place scratchpad + geometric-dislike + solo; a thin driver over the shared C++ core). -- `manifold/src/settings/` — `settings-store.ts` (monochrome icons, input-map shape, corner radius). +- `manifold/src/settings/` — `settings-store.ts` (monochrome icons, input-map shape, corner radius, and the opt-in legacy Xavier/spread feature flag; Manifold randomisation is full-range uniform by default). - `manifold/src/serial/` — `memlnaut-serial.ts` Web Serial scaffold + `EditorPanel.tsx` (MEMLNaut Editor mode). - `manifold/src/engine/exploration.ts` — Jolt press + OU explore gestures (Learning drawer): a thin timer-driver over the shared C++ core via the `nisps_ml_jolt_*`/`nisps_ml_ou_*` bindings (the interim diff --git a/docs/AGENT-REFERENCE.md b/docs/AGENT-REFERENCE.md index 026a7d4..48a62a4 100644 --- a/docs/AGENT-REFERENCE.md +++ b/docs/AGENT-REFERENCE.md @@ -130,7 +130,7 @@ The browser MLP is runtime-shaped since P2 (`MLPCore`): `nisps_m |-------|-------|---------|--------| | `debug` | 1 | _(off)_ | Exposes the `window.__nisps` debug probe. | -(The playground-era `tame`/`spread`/`preset` URL params died with the playground; `spread` survives as an engine concept — see below.) +(The playground-era `tame`/`spread`/`preset` URL params died with the playground; `spread` survives as an opt-in engine concept — see below.) ### `spread` — sigmoid saturation control @@ -140,6 +140,13 @@ The MLP uses ReLU hidden layers with a sigmoid output. With uniform [-1,1] weigh - `spread=1` (centered): Xavier-scaled weights, RL noise cap 0.05, 10% weight decay per move. Outputs spread across [0,1] — better for fine-grained shaping. - Intermediate values interpolate. +Manifold deliberately defaults to `spread=0` at every browser boundary: initial construction, +mode-switch reshape, direct re-roll, explore-and-place scratchpad re-roll, and forwarded VCV +randomise. The old schema spread and the expanded Learning-drawer centred switch are available only +after enabling Settings → Experimental features → **Xavier / spread randomisation**. This is a +Manifold compatibility flag; the shared C++ core and generated mode schemas still expose spread for +firmware, VCV, benchmarks, and explicit API callers. + ## Verification chokepoints (user-confirmed) - **A. Hardware**: each firmware mode flashes and produces correct audio on RP2350. diff --git a/manifold/ONBOARDING.md b/manifold/ONBOARDING.md index ca5139a..29e0107 100644 --- a/manifold/ONBOARDING.md +++ b/manifold/ONBOARDING.md @@ -251,7 +251,7 @@ a setting → `--r-*` tokens. match. Debug: `window.__nisps.reshape(nIn)` / `.describe()`. See the `manifold-mixed-inputs` memory for the locked design (adaptive slider viz when >2 dims is still pending). - **Per-mode net dims (P5.3):** switching INSTRUMENT mode reshapes the net to that mode's schema - `ml` config (`MFMode.ml` — input/hidden/output + spread) via a `ConsoleApp` effect keyed on + `ml` config (`MFMode.ml` — input/hidden/output + legacy spread) via a `ConsoleApp` effect keyed on `[engine, modeId]`. No confirm modal (switching instrument is deliberate); the axis-count `ReshapeModal` above is for input-LAYOUT changes only. The effect depends on `engine`, so on boot it fires once WASM is ready and lands the boot mode's dims (**paf_synth → 4→[10,10,14]→33**, weights @@ -261,6 +261,10 @@ a setting → `--r-*` tokens. Debug seam for tests: under `?debug=1` ConsoleApp installs `window.__mf` (`setMode`/`getModeId`/`paramCount`/`modeIds`) — the UI-level analogue of `__nisps`, since no in-UI instrument picker exists yet (`ctx.modes`/`setModeId` are plumbed but unrendered). + Manifold passes `spread=0` for boot, mode-switch reshapes, direct re-rolls, explore-and-place + scratchpad rolls, and VCV-forwarded randomise gestures by default. Settings → Experimental + features → **Xavier / spread randomisation** restores the schema spread and reveals the centred + regime switch in the expanded Learning drawer. ### Feedback — `src/feedback/` - `controller.ts` — `FeedbackController`, framework-neutral, owned by ConsoleApp. **As of one-core- @@ -291,7 +295,8 @@ a setting → `--r-*` tokens. ### Misc - `src/serial/memlnaut-serial.ts` — **STUB** Web Serial scaffold for the MEMLNaut Editor mode (protocol TODO). `EditorPanel.tsx` is its UI. -- `src/settings/settings-store.ts` — localStorage settings (`mf-settings`): icon style, input-map shape, corner radius. +- `src/settings/settings-store.ts` — localStorage settings (`mf-settings`): icon style, input-map + shape, corner radius, and the opt-in legacy Xavier/spread feature flag. - `src/midi-devices/` — codegen'd external-synth device templates. - `src/debug/probe.ts` — `window.__nisps` synchronous probe (engine/audio/bus). Some playground feature-store methods are present-but-inert (not ported yet) to keep the surface stable. diff --git a/manifold/src/console/ConsoleApp.tsx b/manifold/src/console/ConsoleApp.tsx index 37bd08c..cbbc480 100644 --- a/manifold/src/console/ConsoleApp.tsx +++ b/manifold/src/console/ConsoleApp.tsx @@ -109,6 +109,11 @@ export function ConsoleApp() { const [addingExample, setAddingExample] = useState(false); const [busy, setBusy] = useState(false); const [spread, setSpread] = useState(false); + // Manifold's normal regime is a full-range uniform draw. The per-mode legacy + // spread remains available only when explicitly enabled in Settings. + const randomisationSpread = settings.xavierSpreadEnabled + ? (spread ? 1 : mode.ml.defaultSpread) + : 0; const [active, setActive] = useState(null); const [depth, setDepth] = useState('condensed'); // Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon. @@ -172,7 +177,7 @@ export function ConsoleApp() { const controllerRef = useRef(null); if (engine && !controllerRef.current) { controllerRef.current = new FeedbackController(engine, { - spread: 0.6, + spread: randomisationSpread, }); } @@ -229,6 +234,12 @@ export function ConsoleApp() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [engine, feedbackMode]); + // Settings changes affect future randomise/explore gestures without + // destructively re-drawing the currently audible network. + useEffect(() => { + controllerRef.current?.setSpread(randomisationSpread); + }, [engine, randomisationSpread]); + // 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. @@ -257,8 +268,8 @@ export function ConsoleApp() { // modal stays for input-LAYOUT changes only (see the reshape-offer effect). useEffect(() => { if (!engine) return; - const { inputSize, outputSize, hidden, defaultSpread } = mode.ml; - engine.reshape({ inputSize, outputSize, hidden }, defaultSpread); + const { inputSize, outputSize, hidden } = mode.ml; + engine.reshape({ inputSize, outputSize, hidden }, randomisationSpread); // eslint-disable-next-line react-hooks/exhaustive-deps }, [engine, modeId]); @@ -402,7 +413,7 @@ export function ConsoleApp() { const forwardVcvFeedback = (op: 'up' | 'down' | 'rand' | 'clear') => { backendManager?.forwardFeedback({ op, - spread: spread ? 1 : 0.6, + spread: randomisationSpread, input: [pos[0], pos[1]], output: Array.from(engine?.getOutputs() ?? new Float32Array(0)), }); @@ -508,7 +519,7 @@ export function ConsoleApp() { c.reroll(); } else { // Outside a scratchpad session a re-roll randomises the real net directly. - engine?.randomise(spread ? 1 : 0.6); + engine?.randomise(randomisationSpread); } // VCV bridged mode: re-roll the module's net too. forwardVcvFeedback('rand'); @@ -794,6 +805,7 @@ export function ConsoleApp() { inputs, spread, setSpread, + xavierSpreadEnabled: settings.xavierSpreadEnabled, noiseCap, setNoiseCap, // learning-behaviour diff --git a/manifold/src/console/Drawers.tsx b/manifold/src/console/Drawers.tsx index bfe8112..578983f 100644 --- a/manifold/src/console/Drawers.tsx +++ b/manifold/src/console/Drawers.tsx @@ -9,7 +9,7 @@ * MEMLNaut serial panel. The old separate "Synth" and * "Particle/Visual" drawers are REMOVED — their config now * lives here under the active Mode (TOP dock selector). - * settings — Settings : icon style + input-map shape (settings-store) + * settings — Settings : icon style, input-map shape + feature flags * help — Help : keymap + the loop explanation * * The TOP dock selector ("Mode") chooses the active OUTPUT backend/target; this @@ -259,7 +259,9 @@ function LearningDrawer(ctx: ConsoleCtx, depth: DrawerDepth) { training is paused and the joystick auditions a random scratchpad net; + commits a placed anchor and restores the real net.

- + {ctx.xavierSpreadEnabled && ( + + )} Training health @@ -742,6 +744,20 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) { buttons are intentionally exempt. Default 2px.

)} + + Experimental features + set('xavierSpreadEnabled', v)} + label="Xavier / spread randomisation" + /> + {depth === 'expanded' && ( +

+ Off by default: new networks and re-rolls use full-range uniform weights for broad, + strongly varied mappings. Enable this to restore the legacy centred regime and its + Learning-drawer switch. +

+ )} ); } diff --git a/manifold/src/console/icons.tsx b/manifold/src/console/icons.tsx index fba380c..f2bfd21 100644 --- a/manifold/src/console/icons.tsx +++ b/manifold/src/console/icons.tsx @@ -111,8 +111,8 @@ export function SettingsIcon({ size = 18, style }: IconProps) { size, style, <> + - , ); } diff --git a/manifold/src/console/types.ts b/manifold/src/console/types.ts index 2edddee..6c9b595 100644 --- a/manifold/src/console/types.ts +++ b/manifold/src/console/types.ts @@ -118,6 +118,8 @@ export interface ConsoleCtx { spread: boolean; setSpread: (v: boolean) => void; + /** Settings feature flag: expose and apply the legacy Xavier/spread regime. */ + xavierSpreadEnabled: boolean; noiseCap: number; setNoiseCap: (v: number) => void; diff --git a/manifold/src/engine/engine-api.ts b/manifold/src/engine/engine-api.ts index 8809195..800d19b 100644 --- a/manifold/src/engine/engine-api.ts +++ b/manifold/src/engine/engine-api.ts @@ -109,7 +109,7 @@ export interface EngineApiOptions { learningRate?: number; /** Default max training iterations for train/trainAsync. */ maxIterations?: number; - /** Default RL move speed / spread for thumbsDown. */ + /** Default RL move speed / randomisation spread. Spread defaults to 0. */ noiseCap?: number; spread?: number; /** @@ -141,7 +141,7 @@ export class EngineApi { this.learningRate = opts.learningRate ?? ML_TRAIN_DEFAULTS.learningRate; this.maxIterations = opts.maxIterations ?? ML_TRAIN_DEFAULTS.maxIterations; this.noiseCap = opts.noiseCap ?? 0.3; - this.spread_ = opts.spread ?? 0.6; + this.spread_ = opts.spread ?? 0; // Persist the configured default on the underlying MLP too (S26) — makes // the WASM engine's OWN training config match EngineApi's knobs, the same // real runtime-configurability firmware/VCV get for free from @@ -216,6 +216,7 @@ export class EngineApi { const spine = new Spine(); const iml = await WasmIML.create({ seed: opts.seed, + initialSpread: opts.spread ?? 0, storageKey: opts.storageKey, maxExamples: opts.maxExamples, sink: spine, diff --git a/manifold/src/engine/wasm-iml.ts b/manifold/src/engine/wasm-iml.ts index 153de86..dbed2e0 100644 --- a/manifold/src/engine/wasm-iml.ts +++ b/manifold/src/engine/wasm-iml.ts @@ -123,6 +123,11 @@ export interface WasmIMLOptions { outputSize?: number; hiddenLayers?: ReadonlyArray; seed?: number; + /** + * Initial draw regime. Manifold defaults to 0 (full-range uniform); callers + * must opt into the legacy Xavier/spread behaviour explicitly. + */ + initialSpread?: number; /** localStorage key the loaded weights/dataset will be persisted under. */ storageKey?: string; /** @@ -231,6 +236,10 @@ export class WasmIML { const seed = (opts.seed ?? (Date.now() >>> 0)) >>> 0; this.mlHandle = this.module._nisps_ml_create(wantedIn, wantedOut, 0, 0, seed); if (!this.mlHandle) throw new Error('[wasm-iml] nisps_ml_create returned null'); + // The C ABI constructor retains the cross-platform core's historical + // initialisation. Manifold deliberately overrides it at its boundary so an + // unconfigured browser engine starts with genuinely broad randomisation. + this.module._nisps_ml_draw_weights(this.mlHandle, opts.initialSpread ?? 0); this.module._nisps_ml_describe(this.mlHandle, this.describePtr); const dims = new Int32Array(this.module.HEAP32.buffer, this.describePtr, 7); @@ -354,7 +363,7 @@ export class WasmIML { */ reshape( dims: { inputSize?: number; outputSize?: number; hidden?: readonly [number, number, number] }, - spread = 0.6, + spread = 0, ): boolean { const wantIn = dims.inputSize ?? this.arch_.inputSize; const wantOut = dims.outputSize ?? this.arch_.outputSize; @@ -743,7 +752,7 @@ export class WasmIML { // RL ops // ------------------------------------------------------------------- - randomiseWeights(spread = 0.6): void { + randomiseWeights(spread = 0): void { this.module._nisps_ml_draw_weights(this.mlHandle, spread); this.sink.emit('ml.delta_update', { reason: 'randomise' }); this.scheduleSave_(); diff --git a/manifold/src/feedback/controller.ts b/manifold/src/feedback/controller.ts index 68412e1..e0f8bcd 100644 --- a/manifold/src/feedback/controller.ts +++ b/manifold/src/feedback/controller.ts @@ -108,7 +108,7 @@ export interface FeedbackControllerState { } export interface FeedbackControllerOptions { - /** Master spread for randomise / nudge (mirrors the engine spread knob). */ + /** Master spread for randomise / nudge. Defaults to full-range uniform (0). */ spread?: number; /** Nudge perturbation standard deviation (small bounded weight jitter). */ nudgeStddev?: number; @@ -145,7 +145,7 @@ export class FeedbackController { constructor(engine: ControllerEngine, opts: FeedbackControllerOptions = {}) { this.engine = engine; - this.spread = opts.spread ?? 0.6; + this.spread = opts.spread ?? 0; this.nudgeStddev = opts.nudgeStddev ?? 0.05; } diff --git a/manifold/src/settings/settings-store.ts b/manifold/src/settings/settings-store.ts index 1be16f3..20a51a9 100644 --- a/manifold/src/settings/settings-store.ts +++ b/manifold/src/settings/settings-store.ts @@ -8,6 +8,9 @@ * - inputMap: the 2D input-surface shape. 'follow-mode' (default) uses the * active mode's declared input (joystick → circular, else rectangular); * 'rectangular' / 'circular' are explicit global overrides. + * - xavierSpreadEnabled: compatibility feature flag for the old centred + * Xavier/spread randomisation regime. Off by default, so Manifold initial + * weights and re-rolls use the full uniform range. * * British spelling in copy. No React inside the store itself — the hook is a * separate, additive binding so a headless consumer (debug probe / test) can @@ -35,6 +38,11 @@ export interface Settings { * verdict buttons are intentionally exempt (separate tokens). */ cornerRadius: number; + /** + * Restore the legacy Xavier/spread randomisation regime and expose its + * Learning-drawer control. Off means full-range uniform randomisation. + */ + xavierSpreadEnabled: boolean; } export const DEFAULT_SETTINGS: Settings = { @@ -42,6 +50,7 @@ export const DEFAULT_SETTINGS: Settings = { unfocusedIconColour: 'off-white', inputMap: 'follow-mode', cornerRadius: 2, + xavierSpreadEnabled: false, }; const STORAGE_KEY = 'mf-settings'; diff --git a/manifold/tests/e2e/probe-api.spec.ts b/manifold/tests/e2e/probe-api.spec.ts index a70edf2..9356ad3 100644 --- a/manifold/tests/e2e/probe-api.spec.ts +++ b/manifold/tests/e2e/probe-api.spec.ts @@ -63,6 +63,33 @@ test.describe('ML engine — debug probe contract', () => { expect(countChanged(before, after, 1e-3)).toBeGreaterThan(0); }); + test('randomise defaults to a broad full-range mapping', async ({ page }) => { + const distribution = await page.evaluate(() => { + const probe = window.__nisps!; + const values: number[] = []; + for (let draw = 0; draw < 48; ++draw) { + probe.randomise(); + probe.setInputs(0.5, 0.5); + values.push(...probe.getOutputs()); + } + values.sort((a, b) => a - b); + const percentile = (p: number) => values[Math.floor((values.length - 1) * p)]!; + const centralFraction = + values.filter((value) => value >= 0.35 && value <= 0.65).length / values.length; + return { + p05: percentile(0.05), + p95: percentile(0.95), + centralFraction, + }; + }); + + // The former implicit spread=0.6 regime put ~99.8% of values inside this + // central band. Uniform spread=0 must visibly reach both sides of it. + expect(distribution.p05).toBeLessThan(0.3); + expect(distribution.p95).toBeGreaterThan(0.7); + expect(distribution.centralFraction).toBeLessThan(0.8); + }); + test('setInputs runs inference and yields bounded outputs', async ({ page }) => { await page.evaluate(() => window.__nisps!.setInputs(0.25, 0.75)); const outs = await getOutputs(page); diff --git a/manifold/tests/e2e/randomisation-settings.spec.ts b/manifold/tests/e2e/randomisation-settings.spec.ts new file mode 100644 index 0000000..fafc810 --- /dev/null +++ b/manifold/tests/e2e/randomisation-settings.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from '@playwright/test'; +import { loadProbe } from './helpers'; + +test('legacy Xavier control is hidden until enabled in Settings', async ({ page }) => { + await loadProbe(page); + + await page.getByTitle('Learning').click(); + await page.getByTitle('Expand').click(); + await expect(page.getByText('Xavier (centred) weight regime')).toHaveCount(0); + + await page.getByTitle('Close').click(); + await page.getByTitle('Settings').click(); + const featureFlag = page.getByRole('switch', { name: 'Xavier / spread randomisation' }); + await expect(featureFlag).toHaveAttribute('aria-checked', 'false'); + await featureFlag.click(); + await expect(featureFlag).toHaveAttribute('aria-checked', 'true'); + + await page.getByTitle('Close').click(); + await page.getByTitle('Learning').click(); + await page.getByTitle('Expand').click(); + await expect(page.getByRole('switch', { name: 'Xavier (centred) weight regime' })).toBeVisible(); +});