Phase 2 — Pinning + History: - snapshot-stack.js: ring buffer (20 max) with auto-snapshot on train/randomize/thumbs-down, multi-level undo, tagged entries - ab-compare.js: A/B weight state comparison with capture/toggle/accept/revert - region-pin.js: pin rectangular input-space regions (Approach A: example pinning), pinned examples always included in training - param-pin.js: per-output pin flags, pin mask skips pinned nodes in moveWeights - phase2-ui.js: undo button with history popup, A/B toggle, long-press region pin, double-tap param pin - Modified mlp.js/iml.js/nisps-wasm.js to accept outputPinMask in moveWeights Phase 3 — Input Refinement + Exploration: - pressure-feedback.js: touch force + hold duration → intensity multiplier - auto-explore.js: automated thumbs-down at configurable interval, zoom-scaled - input-heatmap.js: 16×16 MLP sampling, 3 color modes (luminance/variance/ divergence), zoom-aware resampling, offscreen canvas rendering - phase3-ui.js: auto-explore toggle with progress ring, heatmap eye icon, pressure indicators, settings drawer section - joy-map-enhanced.js: added setHeatmap() for background layer rendering Phase 4 — Output Pipeline + Visualization + Polish: - output-pipeline.js: global curve → smoothing → slew rate → freeze gate - weight-health.js: weight magnitude histogram, dead/saturating/healthy status - gradient-flow.js: per-layer weight-delta analysis, vanishing/exploding detection - session-presets.js: save/load full state, URL sharing via compact params - phase4-ui.js: freeze button, network health panel, session preset UI All phases merged into a-app.js with proper integration: auto-snapshots, pressure-modulated RL, heatmap triggers, output pipeline in routeOutputs, gradient capture around training, persistence for all new state.
153 lines
4.6 KiB
JavaScript
153 lines
4.6 KiB
JavaScript
// A/B Compare — Rapid toggle between two weight states
|
|
//
|
|
// Workflow:
|
|
// 1. User presses "A" to capture current state as the reference
|
|
// 2. User continues exploring (this becomes the live "B" state)
|
|
// 3. Toggle switches instantly between A and B
|
|
// 4. "Accept B" discards A and exits A/B mode
|
|
// 5. "Revert to A" restores the A snapshot and discards B
|
|
//
|
|
// The state object stored for each side includes weights, noiseLevel, and
|
|
// an optional controlState (for any control surface params worth preserving).
|
|
//
|
|
// Usage:
|
|
// import { ABCompare } from './ab-compare.js';
|
|
// const ab = new ABCompare();
|
|
// ab.captureA({ weights, noiseLevel, controlState });
|
|
// // ... user explores ...
|
|
// const stateToRestore = ab.toggle(); // switches to A, returns A's state
|
|
// const stateToRestore2 = ab.toggle(); // switches back to B, returns B's state
|
|
|
|
export class ABCompare {
|
|
constructor() {
|
|
this._a = null; // { weights, noiseLevel, controlState }
|
|
this._b = null; // { weights, noiseLevel, controlState }
|
|
this._current = null; // 'a' | 'b' | null (null = inactive)
|
|
}
|
|
|
|
// ---- Public API ----
|
|
|
|
/**
|
|
* Capture the current state as side "A" and enter A/B mode.
|
|
* The live state after this call becomes "B".
|
|
* @param {{ weights: Array, noiseLevel: number, controlState?: any }} state
|
|
*/
|
|
captureA(state) {
|
|
this._a = {
|
|
weights: Array.isArray(state.weights)
|
|
? [...state.weights]
|
|
: Array.from(state.weights),
|
|
noiseLevel: state.noiseLevel ?? 0,
|
|
controlState: state.controlState ?? null,
|
|
};
|
|
this._b = null; // B is "live" — captured on first toggle away from B
|
|
this._current = 'b'; // We're currently hearing the B (live) side
|
|
this._dispatch('ab:activate', { side: 'b' });
|
|
}
|
|
|
|
/**
|
|
* Whether A/B mode is currently active.
|
|
* @returns {boolean}
|
|
*/
|
|
get active() {
|
|
return this._current !== null;
|
|
}
|
|
|
|
/**
|
|
* Which side is currently active: 'a', 'b', or null if inactive.
|
|
* @returns {'a'|'b'|null}
|
|
*/
|
|
get current() {
|
|
return this._current;
|
|
}
|
|
|
|
/**
|
|
* Toggle between A and B. Returns the state for whichever side we're
|
|
* switching TO, so the caller can restore weights/noise from it.
|
|
*
|
|
* The caller must pass the current live state so we can snapshot whichever
|
|
* side we're leaving.
|
|
*
|
|
* @param {{ weights: Array, noiseLevel: number, controlState?: any }} currentLiveState
|
|
* @returns {{ weights: Array, noiseLevel: number, controlState?: any }|null}
|
|
*/
|
|
toggle(currentLiveState) {
|
|
if (!this.active) return null;
|
|
|
|
if (this._current === 'b') {
|
|
// Switching B → A: save current live state as B, return A
|
|
this._b = {
|
|
weights: Array.isArray(currentLiveState.weights)
|
|
? [...currentLiveState.weights]
|
|
: Array.from(currentLiveState.weights),
|
|
noiseLevel: currentLiveState.noiseLevel ?? 0,
|
|
controlState: currentLiveState.controlState ?? null,
|
|
};
|
|
this._current = 'a';
|
|
this._dispatch('ab:toggle', { side: 'a' });
|
|
return this._cloneState(this._a);
|
|
} else {
|
|
// Switching A → B: save current live state as A, return B
|
|
this._a = {
|
|
weights: Array.isArray(currentLiveState.weights)
|
|
? [...currentLiveState.weights]
|
|
: Array.from(currentLiveState.weights),
|
|
noiseLevel: currentLiveState.noiseLevel ?? 0,
|
|
controlState: currentLiveState.controlState ?? null,
|
|
};
|
|
this._current = 'b';
|
|
this._dispatch('ab:toggle', { side: 'b' });
|
|
return this._cloneState(this._b);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accept B: discard A, exit A/B mode.
|
|
* The current live state (B) is kept as-is — nothing to restore.
|
|
*/
|
|
acceptB() {
|
|
this._a = null;
|
|
this._b = null;
|
|
this._current = null;
|
|
this._dispatch('ab:deactivate', { accepted: 'b' });
|
|
}
|
|
|
|
/**
|
|
* Revert to A: restore A state and discard B, exit A/B mode.
|
|
* @returns {{ weights: Array, noiseLevel: number, controlState?: any }|null}
|
|
*/
|
|
revertToA() {
|
|
if (!this._a) return null;
|
|
const state = this._cloneState(this._a);
|
|
this._a = null;
|
|
this._b = null;
|
|
this._current = null;
|
|
this._dispatch('ab:deactivate', { accepted: 'a' });
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Cancel A/B mode without choosing. Stays on current live state.
|
|
*/
|
|
cancel() {
|
|
this._a = null;
|
|
this._b = null;
|
|
this._current = null;
|
|
this._dispatch('ab:deactivate', { accepted: null });
|
|
}
|
|
|
|
// ---- Internal ----
|
|
|
|
_cloneState(s) {
|
|
if (!s) return null;
|
|
return {
|
|
weights: [...s.weights],
|
|
noiseLevel: s.noiseLevel,
|
|
controlState: s.controlState,
|
|
};
|
|
}
|
|
|
|
_dispatch(type, detail) {
|
|
document.dispatchEvent(new CustomEvent(type, { detail }));
|
|
}
|
|
}
|