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.
205 lines
5.8 KiB
JavaScript
205 lines
5.8 KiB
JavaScript
// Region Pin Manager — Pin rectangular input-space regions (Approach A: Example Pinning)
|
|
//
|
|
// Pinned regions capture training examples whose inputs fall within a rectangle
|
|
// in the 2D input space. These pinned examples:
|
|
// - Are always included in training with high weight
|
|
// - Cannot be evicted by FIFO example rotation
|
|
// - Anchor behavior in that region while the user explores elsewhere
|
|
//
|
|
// Up to 5 pinned regions, each with a distinct color from the palette.
|
|
//
|
|
// Usage:
|
|
// import { RegionPinManager } from './region-pin.js';
|
|
// const regionPins = new RegionPinManager();
|
|
// const id = regionPins.pin({ x1: 0.2, y1: 0.3, x2: 0.5, y2: 0.7 }, examples);
|
|
// const pinned = regionPins.getPinnedExamples(); // { features: [...], labels: [...] }
|
|
|
|
const MAX_PINS = 5;
|
|
|
|
// Distinct colors for pinned region overlays (used for joy-map rendering)
|
|
const PIN_PALETTE = [
|
|
'rgba(0, 188, 212, 0.15)', // teal
|
|
'rgba(156, 39, 176, 0.15)', // purple
|
|
'rgba(255, 193, 7, 0.15)', // amber
|
|
'rgba(233, 30, 99, 0.15)', // rose
|
|
'rgba(139, 195, 74, 0.15)', // lime
|
|
];
|
|
|
|
let _nextId = 1;
|
|
|
|
export class RegionPinManager {
|
|
constructor() {
|
|
// Array of { id, region: {x1,y1,x2,y2}, color, examples: { features: [[]], labels: [[]] } }
|
|
this._pins = [];
|
|
}
|
|
|
|
// ---- Public API ----
|
|
|
|
/**
|
|
* Pin a region. Captures examples whose inputs fall within the region.
|
|
* @param {{ x1: number, y1: number, x2: number, y2: number }} region
|
|
* Normalized [0,1] coordinates. x1 < x2, y1 < y2.
|
|
* @param {{ features: Array<Array<number>>, labels: Array<Array<number>> }} examples
|
|
* The current full training dataset to filter from.
|
|
* @returns {number} Pin ID, or -1 if at capacity.
|
|
*/
|
|
pin(region, examples) {
|
|
if (this._pins.length >= MAX_PINS) {
|
|
console.warn('[RegionPin] Max pins reached (' + MAX_PINS + ')');
|
|
return -1;
|
|
}
|
|
|
|
// Normalize region bounds
|
|
const r = {
|
|
x1: Math.min(region.x1, region.x2),
|
|
y1: Math.min(region.y1, region.y2),
|
|
x2: Math.max(region.x1, region.x2),
|
|
y2: Math.max(region.y1, region.y2),
|
|
};
|
|
|
|
// Filter examples whose inputs fall within the region
|
|
const pinnedFeatures = [];
|
|
const pinnedLabels = [];
|
|
if (examples && examples.features) {
|
|
for (let i = 0; i < examples.features.length; i++) {
|
|
const f = examples.features[i];
|
|
// Inputs are the first 2 elements (joystick X, Y) — without bias
|
|
const x = f[0];
|
|
const y = f.length > 1 ? f[1] : 0.5;
|
|
if (x >= r.x1 && x <= r.x2 && y >= r.y1 && y <= r.y2) {
|
|
pinnedFeatures.push([...f]);
|
|
pinnedLabels.push([...(examples.labels[i] || [])]);
|
|
}
|
|
}
|
|
}
|
|
|
|
const id = _nextId++;
|
|
const colorIndex = this._pins.length % PIN_PALETTE.length;
|
|
|
|
this._pins.push({
|
|
id,
|
|
region: r,
|
|
color: PIN_PALETTE[colorIndex],
|
|
examples: {
|
|
features: pinnedFeatures,
|
|
labels: pinnedLabels,
|
|
},
|
|
});
|
|
|
|
this._dispatch('regionpin:add', {
|
|
id,
|
|
region: r,
|
|
exampleCount: pinnedFeatures.length,
|
|
total: this._pins.length,
|
|
});
|
|
|
|
return id;
|
|
}
|
|
|
|
/**
|
|
* Remove a pinned region by ID.
|
|
* @param {number} pinId
|
|
* @returns {boolean} True if removed.
|
|
*/
|
|
unpin(pinId) {
|
|
const idx = this._pins.findIndex(p => p.id === pinId);
|
|
if (idx < 0) return false;
|
|
this._pins.splice(idx, 1);
|
|
this._dispatch('regionpin:remove', { id: pinId, total: this._pins.length });
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get all pinned regions for joy-map overlay rendering.
|
|
* @returns {Array<{ id: number, region: {x1,y1,x2,y2}, color: string, exampleCount: number }>}
|
|
*/
|
|
getRegions() {
|
|
return this._pins.map(p => ({
|
|
id: p.id,
|
|
region: { ...p.region },
|
|
color: p.color,
|
|
exampleCount: p.examples.features.length,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get all pinned examples merged together (for training).
|
|
* These should always be included in the training set alongside regular examples.
|
|
* @returns {{ features: Array<Array<number>>, labels: Array<Array<number>> }}
|
|
*/
|
|
getPinnedExamples() {
|
|
const features = [];
|
|
const labels = [];
|
|
for (const pin of this._pins) {
|
|
for (let i = 0; i < pin.examples.features.length; i++) {
|
|
features.push(pin.examples.features[i]);
|
|
labels.push(pin.examples.labels[i]);
|
|
}
|
|
}
|
|
return { features, labels };
|
|
}
|
|
|
|
/**
|
|
* Check if a 2D input point falls within any pinned region.
|
|
* @param {Array<number>} inputs [x, y, ...]
|
|
* @returns {boolean}
|
|
*/
|
|
isInPinnedRegion(inputs) {
|
|
const x = inputs[0];
|
|
const y = inputs.length > 1 ? inputs[1] : 0.5;
|
|
for (const pin of this._pins) {
|
|
const r = pin.region;
|
|
if (x >= r.x1 && x <= r.x2 && y >= r.y1 && y <= r.y2) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Current number of pinned regions.
|
|
* @returns {number}
|
|
*/
|
|
get count() {
|
|
return this._pins.length;
|
|
}
|
|
|
|
// ---- Serialization ----
|
|
|
|
getState() {
|
|
return {
|
|
pins: this._pins.map(p => ({
|
|
id: p.id,
|
|
region: { ...p.region },
|
|
color: p.color,
|
|
examples: {
|
|
features: p.examples.features.map(f => [...f]),
|
|
labels: p.examples.labels.map(l => [...l]),
|
|
},
|
|
})),
|
|
};
|
|
}
|
|
|
|
setState(saved) {
|
|
if (!saved || !Array.isArray(saved.pins)) return;
|
|
this._pins = saved.pins.map(p => ({
|
|
id: p.id || _nextId++,
|
|
region: p.region || { x1: 0, y1: 0, x2: 1, y2: 1 },
|
|
color: p.color || PIN_PALETTE[0],
|
|
examples: {
|
|
features: (p.examples?.features || []).map(f => [...f]),
|
|
labels: (p.examples?.labels || []).map(l => [...l]),
|
|
},
|
|
}));
|
|
// Advance ID counter past any restored IDs
|
|
for (const p of this._pins) {
|
|
if (p.id >= _nextId) _nextId = p.id + 1;
|
|
}
|
|
}
|
|
|
|
// ---- Internal ----
|
|
|
|
_dispatch(type, detail) {
|
|
document.dispatchEvent(new CustomEvent(type, { detail }));
|
|
}
|
|
}
|