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.
169 lines
4.6 KiB
JavaScript
169 lines
4.6 KiB
JavaScript
// Snapshot Stack — Multi-level undo for weight states
|
|
//
|
|
// A ring buffer of weight snapshots tagged with context (e.g. "before thumbs-down",
|
|
// "before train"). Auto-snapshot triggers in the app push snapshots before destructive
|
|
// operations. Users can undo (pop), peek, list, or jump to any snapshot.
|
|
//
|
|
// Each snapshot stores:
|
|
// - weights: the full flat weight array (Array<number> or Float32Array)
|
|
// - noiseLevel: the RL noise level at time of snapshot
|
|
// - zoomLevel: the input pipeline zoom level (optional)
|
|
// - tag: human-readable label for UI display
|
|
// - timestamp: Date.now() when captured
|
|
//
|
|
// Usage:
|
|
// import { SnapshotStack } from './snapshot-stack.js';
|
|
// const stack = new SnapshotStack(20);
|
|
// stack.push('before thumbs-down', { weights, noiseLevel, zoomLevel });
|
|
// const prev = stack.pop(); // undo
|
|
|
|
export class SnapshotStack {
|
|
/**
|
|
* @param {number} maxSnapshots Ring buffer capacity. Oldest evicted when full.
|
|
*/
|
|
constructor(maxSnapshots = 20) {
|
|
this._max = maxSnapshots;
|
|
this._stack = []; // Array of { tag, timestamp, state: { weights, noiseLevel, zoomLevel } }
|
|
}
|
|
|
|
// ---- Core operations ----
|
|
|
|
/**
|
|
* Push a snapshot onto the stack.
|
|
* @param {string} tag Human-readable label (e.g. "before thumbs-down")
|
|
* @param {{ weights: Array|Float32Array, noiseLevel: number, zoomLevel?: number }} state
|
|
*/
|
|
push(tag, state) {
|
|
const snapshot = {
|
|
tag,
|
|
timestamp: Date.now(),
|
|
state: {
|
|
weights: Array.isArray(state.weights)
|
|
? [...state.weights]
|
|
: Array.from(state.weights),
|
|
noiseLevel: state.noiseLevel ?? 0,
|
|
zoomLevel: state.zoomLevel ?? 1.0,
|
|
},
|
|
};
|
|
|
|
this._stack.push(snapshot);
|
|
|
|
// Ring buffer eviction: drop oldest if over capacity
|
|
if (this._stack.length > this._max) {
|
|
this._stack.shift();
|
|
}
|
|
|
|
this._dispatch('snapshot:push', { tag, depth: this.depth });
|
|
}
|
|
|
|
/**
|
|
* Pop the most recent snapshot (undo).
|
|
* @returns {{ weights: Array, noiseLevel: number, zoomLevel: number }|null}
|
|
*/
|
|
pop() {
|
|
if (this._stack.length === 0) return null;
|
|
const snapshot = this._stack.pop();
|
|
this._dispatch('snapshot:pop', { tag: snapshot.tag, depth: this.depth });
|
|
return snapshot.state;
|
|
}
|
|
|
|
/**
|
|
* Peek at the most recent snapshot without removing it.
|
|
* @returns {{ weights: Array, noiseLevel: number, zoomLevel: number }|null}
|
|
*/
|
|
peek() {
|
|
if (this._stack.length === 0) return null;
|
|
return this._stack[this._stack.length - 1].state;
|
|
}
|
|
|
|
/**
|
|
* List all snapshots (newest last) for UI display.
|
|
* @returns {Array<{ tag: string, timestamp: number, index: number }>}
|
|
*/
|
|
list() {
|
|
return this._stack.map((snap, i) => ({
|
|
tag: snap.tag,
|
|
timestamp: snap.timestamp,
|
|
index: i,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Jump to a specific snapshot by index. Removes everything above it.
|
|
* @param {number} index
|
|
* @returns {{ weights: Array, noiseLevel: number, zoomLevel: number }|null}
|
|
*/
|
|
jumpTo(index) {
|
|
if (index < 0 || index >= this._stack.length) return null;
|
|
const snapshot = this._stack[index];
|
|
// Truncate: keep entries 0..index (inclusive), remove the rest
|
|
this._stack = this._stack.slice(0, index);
|
|
this._dispatch('snapshot:jump', { tag: snapshot.tag, depth: this.depth });
|
|
return snapshot.state;
|
|
}
|
|
|
|
/**
|
|
* Current stack depth.
|
|
* @returns {number}
|
|
*/
|
|
get depth() {
|
|
return this._stack.length;
|
|
}
|
|
|
|
/**
|
|
* Clear all snapshots.
|
|
*/
|
|
clear() {
|
|
this._stack = [];
|
|
this._dispatch('snapshot:clear', { depth: 0 });
|
|
}
|
|
|
|
// ---- Serialization ----
|
|
|
|
/**
|
|
* Serialize for localStorage persistence.
|
|
*/
|
|
getState() {
|
|
return {
|
|
max: this._max,
|
|
snapshots: this._stack.map(s => ({
|
|
tag: s.tag,
|
|
timestamp: s.timestamp,
|
|
state: {
|
|
weights: s.state.weights,
|
|
noiseLevel: s.state.noiseLevel,
|
|
zoomLevel: s.state.zoomLevel,
|
|
},
|
|
})),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Restore from serialized state.
|
|
*/
|
|
setState(saved) {
|
|
if (!saved) return;
|
|
if (typeof saved.max === 'number') this._max = saved.max;
|
|
if (Array.isArray(saved.snapshots)) {
|
|
this._stack = saved.snapshots.map(s => ({
|
|
tag: s.tag || 'restored',
|
|
timestamp: s.timestamp || Date.now(),
|
|
state: {
|
|
weights: s.state?.weights || [],
|
|
noiseLevel: s.state?.noiseLevel ?? 0,
|
|
zoomLevel: s.state?.zoomLevel ?? 1.0,
|
|
},
|
|
}));
|
|
// Trim to capacity
|
|
while (this._stack.length > this._max) {
|
|
this._stack.shift();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- Internal ----
|
|
|
|
_dispatch(type, detail) {
|
|
document.dispatchEvent(new CustomEvent(type, { detail }));
|
|
}
|
|
}
|