fix(manifold): broaden randomisation by default

This commit is contained in:
monkey-w1n5t0n 2026-07-25 15:07:35 +02:00
parent 0bd65917c4
commit 6e71ad7d35
13 changed files with 128 additions and 18 deletions

2
MAP.md
View file

@ -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. `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 - `manifold/src/feedback/``controller.ts` (Explore-and-place scratchpad + geometric-dislike + solo; a thin
driver over the shared C++ core). 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/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 - `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 timer-driver over the shared C++ core via the `nisps_ml_jolt_*`/`nisps_ml_ou_*` bindings (the interim

View file

@ -130,7 +130,7 @@ The browser MLP is runtime-shaped since P2 (`MLPCore<DynamicStorage>`): `nisps_m
|-------|-------|---------|--------| |-------|-------|---------|--------|
| `debug` | 1 | _(off)_ | Exposes the `window.__nisps` debug probe. | | `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 ### `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. - `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. - 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) ## Verification chokepoints (user-confirmed)
- **A. Hardware**: each firmware mode flashes and produces correct audio on RP2350. - **A. Hardware**: each firmware mode flashes and produces correct audio on RP2350.

View file

@ -251,7 +251,7 @@ a setting → `--r-*` tokens.
match. Debug: `window.__nisps.reshape(nIn)` / `.describe()`. See the `manifold-mixed-inputs` memory 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). 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 - **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 `[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 `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 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` Debug seam for tests: under `?debug=1` ConsoleApp installs `window.__mf`
(`setMode`/`getModeId`/`paramCount`/`modeIds`) — the UI-level analogue of `__nisps`, since no (`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). 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/` ### Feedback — `src/feedback/`
- `controller.ts``FeedbackController`, framework-neutral, owned by ConsoleApp. **As of one-core- - `controller.ts``FeedbackController`, framework-neutral, owned by ConsoleApp. **As of one-core-
@ -291,7 +295,8 @@ a setting → `--r-*` tokens.
### Misc ### Misc
- `src/serial/memlnaut-serial.ts`**STUB** Web Serial scaffold for the MEMLNaut Editor mode (protocol TODO). `EditorPanel.tsx` is its UI. - `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/midi-devices/` — codegen'd external-synth device templates.
- `src/debug/probe.ts``window.__nisps` synchronous probe (engine/audio/bus). Some playground - `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. feature-store methods are present-but-inert (not ported yet) to keep the surface stable.

View file

@ -109,6 +109,11 @@ export function ConsoleApp() {
const [addingExample, setAddingExample] = useState(false); const [addingExample, setAddingExample] = useState(false);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [spread, setSpread] = 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<DrawerKey | null>(null); const [active, setActive] = useState<DrawerKey | null>(null);
const [depth, setDepth] = useState<DrawerDepth>('condensed'); const [depth, setDepth] = useState<DrawerDepth>('condensed');
// Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon. // Sandwich (parameter-landscape) centre-stage toggle — dock-bottom layers icon.
@ -172,7 +177,7 @@ export function ConsoleApp() {
const controllerRef = useRef<FeedbackController | null>(null); const controllerRef = useRef<FeedbackController | null>(null);
if (engine && !controllerRef.current) { if (engine && !controllerRef.current) {
controllerRef.current = new FeedbackController(engine, { 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 // eslint-disable-next-line react-hooks/exhaustive-deps
}, [engine, feedbackMode]); }, [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 // 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 // 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. // 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). // modal stays for input-LAYOUT changes only (see the reshape-offer effect).
useEffect(() => { useEffect(() => {
if (!engine) return; if (!engine) return;
const { inputSize, outputSize, hidden, defaultSpread } = mode.ml; const { inputSize, outputSize, hidden } = mode.ml;
engine.reshape({ inputSize, outputSize, hidden }, defaultSpread); engine.reshape({ inputSize, outputSize, hidden }, randomisationSpread);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [engine, modeId]); }, [engine, modeId]);
@ -402,7 +413,7 @@ export function ConsoleApp() {
const forwardVcvFeedback = (op: 'up' | 'down' | 'rand' | 'clear') => { const forwardVcvFeedback = (op: 'up' | 'down' | 'rand' | 'clear') => {
backendManager?.forwardFeedback({ backendManager?.forwardFeedback({
op, op,
spread: spread ? 1 : 0.6, spread: randomisationSpread,
input: [pos[0], pos[1]], input: [pos[0], pos[1]],
output: Array.from(engine?.getOutputs() ?? new Float32Array(0)), output: Array.from(engine?.getOutputs() ?? new Float32Array(0)),
}); });
@ -508,7 +519,7 @@ export function ConsoleApp() {
c.reroll(); c.reroll();
} else { } else {
// Outside a scratchpad session a re-roll randomises the real net directly. // 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. // VCV bridged mode: re-roll the module's net too.
forwardVcvFeedback('rand'); forwardVcvFeedback('rand');
@ -794,6 +805,7 @@ export function ConsoleApp() {
inputs, inputs,
spread, spread,
setSpread, setSpread,
xavierSpreadEnabled: settings.xavierSpreadEnabled,
noiseCap, noiseCap,
setNoiseCap, setNoiseCap,
// learning-behaviour // learning-behaviour

View file

@ -9,7 +9,7 @@
* MEMLNaut serial panel. The old separate "Synth" and * MEMLNaut serial panel. The old separate "Synth" and
* "Particle/Visual" drawers are REMOVED their config now * "Particle/Visual" drawers are REMOVED their config now
* lives here under the active Mode (TOP dock selector). * 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 * help Help : keymap + the loop explanation
* *
* The TOP dock selector ("Mode") chooses the active OUTPUT backend/target; this * 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 training is paused and the joystick auditions a random scratchpad net; + commits a placed
anchor and restores the real net. anchor and restores the real net.
</p> </p>
<Switch checked={ctx.spread} onChange={ctx.setSpread} label="Xavier (centered) weight regime" /> {ctx.xavierSpreadEnabled && (
<Switch checked={ctx.spread} onChange={ctx.setSpread} label="Xavier (centred) weight regime" />
)}
<SectionLabel>Training health</SectionLabel> <SectionLabel>Training health</SectionLabel>
<TrainingHealth /> <TrainingHealth />
@ -742,6 +744,20 @@ function SettingsDrawer({ depth }: { ctx: ConsoleCtx; depth: DrawerDepth }) {
buttons are intentionally exempt. Default 2px. buttons are intentionally exempt. Default 2px.
</p> </p>
)} )}
<SectionLabel>Experimental features</SectionLabel>
<Switch
checked={settings.xavierSpreadEnabled}
onChange={(v) => set('xavierSpreadEnabled', v)}
label="Xavier / spread randomisation"
/>
{depth === 'expanded' && (
<p style={{ fontSize: 9, color: 'var(--fg-dim)', margin: 0, lineHeight: 1.6 }}>
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.
</p>
)}
</> </>
); );
} }

View file

@ -111,8 +111,8 @@ export function SettingsIcon({ size = 18, style }: IconProps) {
size, size,
style, style,
<> <>
<path d="M12.2 2h-.4a2 2 0 0 0-2 2v.2a2 2 0 0 1-1 1.7l-.4.3a2 2 0 0 1-2 0l-.2-.1a2 2 0 0 0-2.7.7l-.2.4A2 2 0 0 0 4 9.9l.2.1a2 2 0 0 1 1 1.7v.6a2 2 0 0 1-1 1.7l-.2.1a2 2 0 0 0-.7 2.7l.2.4a2 2 0 0 0 2.7.7l.2-.1a2 2 0 0 1 2 0l.4.3a2 2 0 0 1 1 1.7v.2a2 2 0 0 0 2 2h.4a2 2 0 0 0 2-2v-.2a2 2 0 0 1 1-1.7l.4-.3a2 2 0 0 1 2 0l.2.1a2 2 0 0 0 2.7-.7l.2-.4a2 2 0 0 0-.7-2.7l-.2-.1a2 2 0 0 1-1-1.7v-.6a2 2 0 0 1 1-1.7l.2-.1a2 2 0 0 0 .7-2.7l-.2-.4a2 2 0 0 0-2.7-.7l-.2.1a2 2 0 0 1-2 0l-.4-.3a2 2 0 0 1-1-1.7V4a2 2 0 0 0-2-2Z" />
<circle cx="12" cy="12" r="3" /> <circle cx="12" cy="12" r="3" />
<path d="M12 2.5v3M12 18.5v3M2.5 12h3M18.5 12h3M5.2 5.2l2.1 2.1M16.7 16.7l2.1 2.1M18.8 5.2l-2.1 2.1M7.3 16.7l-2.1 2.1" />
</>, </>,
); );
} }

View file

@ -118,6 +118,8 @@ export interface ConsoleCtx {
spread: boolean; spread: boolean;
setSpread: (v: boolean) => void; setSpread: (v: boolean) => void;
/** Settings feature flag: expose and apply the legacy Xavier/spread regime. */
xavierSpreadEnabled: boolean;
noiseCap: number; noiseCap: number;
setNoiseCap: (v: number) => void; setNoiseCap: (v: number) => void;

View file

@ -109,7 +109,7 @@ export interface EngineApiOptions {
learningRate?: number; learningRate?: number;
/** Default max training iterations for train/trainAsync. */ /** Default max training iterations for train/trainAsync. */
maxIterations?: number; maxIterations?: number;
/** Default RL move speed / spread for thumbsDown. */ /** Default RL move speed / randomisation spread. Spread defaults to 0. */
noiseCap?: number; noiseCap?: number;
spread?: number; spread?: number;
/** /**
@ -141,7 +141,7 @@ export class EngineApi {
this.learningRate = opts.learningRate ?? ML_TRAIN_DEFAULTS.learningRate; this.learningRate = opts.learningRate ?? ML_TRAIN_DEFAULTS.learningRate;
this.maxIterations = opts.maxIterations ?? ML_TRAIN_DEFAULTS.maxIterations; this.maxIterations = opts.maxIterations ?? ML_TRAIN_DEFAULTS.maxIterations;
this.noiseCap = opts.noiseCap ?? 0.3; 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 // Persist the configured default on the underlying MLP too (S26) — makes
// the WASM engine's OWN training config match EngineApi's knobs, the same // the WASM engine's OWN training config match EngineApi's knobs, the same
// real runtime-configurability firmware/VCV get for free from // real runtime-configurability firmware/VCV get for free from
@ -216,6 +216,7 @@ export class EngineApi {
const spine = new Spine(); const spine = new Spine();
const iml = await WasmIML.create({ const iml = await WasmIML.create({
seed: opts.seed, seed: opts.seed,
initialSpread: opts.spread ?? 0,
storageKey: opts.storageKey, storageKey: opts.storageKey,
maxExamples: opts.maxExamples, maxExamples: opts.maxExamples,
sink: spine, sink: spine,

View file

@ -123,6 +123,11 @@ export interface WasmIMLOptions {
outputSize?: number; outputSize?: number;
hiddenLayers?: ReadonlyArray<number>; hiddenLayers?: ReadonlyArray<number>;
seed?: number; 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. */ /** localStorage key the loaded weights/dataset will be persisted under. */
storageKey?: string; storageKey?: string;
/** /**
@ -231,6 +236,10 @@ export class WasmIML {
const seed = (opts.seed ?? (Date.now() >>> 0)) >>> 0; const seed = (opts.seed ?? (Date.now() >>> 0)) >>> 0;
this.mlHandle = this.module._nisps_ml_create(wantedIn, wantedOut, 0, 0, seed); 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'); 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); this.module._nisps_ml_describe(this.mlHandle, this.describePtr);
const dims = new Int32Array(this.module.HEAP32.buffer, this.describePtr, 7); const dims = new Int32Array(this.module.HEAP32.buffer, this.describePtr, 7);
@ -354,7 +363,7 @@ export class WasmIML {
*/ */
reshape( reshape(
dims: { inputSize?: number; outputSize?: number; hidden?: readonly [number, number, number] }, dims: { inputSize?: number; outputSize?: number; hidden?: readonly [number, number, number] },
spread = 0.6, spread = 0,
): boolean { ): boolean {
const wantIn = dims.inputSize ?? this.arch_.inputSize; const wantIn = dims.inputSize ?? this.arch_.inputSize;
const wantOut = dims.outputSize ?? this.arch_.outputSize; const wantOut = dims.outputSize ?? this.arch_.outputSize;
@ -743,7 +752,7 @@ export class WasmIML {
// RL ops // RL ops
// ------------------------------------------------------------------- // -------------------------------------------------------------------
randomiseWeights(spread = 0.6): void { randomiseWeights(spread = 0): void {
this.module._nisps_ml_draw_weights(this.mlHandle, spread); this.module._nisps_ml_draw_weights(this.mlHandle, spread);
this.sink.emit('ml.delta_update', { reason: 'randomise' }); this.sink.emit('ml.delta_update', { reason: 'randomise' });
this.scheduleSave_(); this.scheduleSave_();

View file

@ -108,7 +108,7 @@ export interface FeedbackControllerState {
} }
export interface FeedbackControllerOptions { 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; spread?: number;
/** Nudge perturbation standard deviation (small bounded weight jitter). */ /** Nudge perturbation standard deviation (small bounded weight jitter). */
nudgeStddev?: number; nudgeStddev?: number;
@ -145,7 +145,7 @@ export class FeedbackController {
constructor(engine: ControllerEngine, opts: FeedbackControllerOptions = {}) { constructor(engine: ControllerEngine, opts: FeedbackControllerOptions = {}) {
this.engine = engine; this.engine = engine;
this.spread = opts.spread ?? 0.6; this.spread = opts.spread ?? 0;
this.nudgeStddev = opts.nudgeStddev ?? 0.05; this.nudgeStddev = opts.nudgeStddev ?? 0.05;
} }

View file

@ -8,6 +8,9 @@
* - inputMap: the 2D input-surface shape. 'follow-mode' (default) uses the * - inputMap: the 2D input-surface shape. 'follow-mode' (default) uses the
* active mode's declared input (joystick circular, else rectangular); * active mode's declared input (joystick circular, else rectangular);
* 'rectangular' / 'circular' are explicit global overrides. * '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 * 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 * 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). * verdict buttons are intentionally exempt (separate tokens).
*/ */
cornerRadius: number; 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 = { export const DEFAULT_SETTINGS: Settings = {
@ -42,6 +50,7 @@ export const DEFAULT_SETTINGS: Settings = {
unfocusedIconColour: 'off-white', unfocusedIconColour: 'off-white',
inputMap: 'follow-mode', inputMap: 'follow-mode',
cornerRadius: 2, cornerRadius: 2,
xavierSpreadEnabled: false,
}; };
const STORAGE_KEY = 'mf-settings'; const STORAGE_KEY = 'mf-settings';

View file

@ -63,6 +63,33 @@ test.describe('ML engine — debug probe contract', () => {
expect(countChanged(before, after, 1e-3)).toBeGreaterThan(0); 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 }) => { test('setInputs runs inference and yields bounded outputs', async ({ page }) => {
await page.evaluate(() => window.__nisps!.setInputs(0.25, 0.75)); await page.evaluate(() => window.__nisps!.setInputs(0.25, 0.75));
const outs = await getOutputs(page); const outs = await getOutputs(page);

View file

@ -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();
});