262 lines
7.4 KiB
JavaScript
262 lines
7.4 KiB
JavaScript
|
|
// NISPS Playground - Main application
|
||
|
|
// Wires IML engine to visual system with joystick input and dual learning modes
|
||
|
|
|
||
|
|
import { IML } from './nisps/iml.js';
|
||
|
|
import { FlowFieldVisualizer } from './ui/visualizer.js';
|
||
|
|
import { VirtualJoystick } from './ui/joystick.js';
|
||
|
|
import { Controls } from './ui/controls.js';
|
||
|
|
import { ParamDisplay } from './ui/param-display.js';
|
||
|
|
|
||
|
|
const N_INPUTS = 2;
|
||
|
|
const N_OUTPUTS = 8;
|
||
|
|
|
||
|
|
// --- State ---
|
||
|
|
let iml;
|
||
|
|
let visualizer;
|
||
|
|
let joystick;
|
||
|
|
let controls;
|
||
|
|
let paramDisplay;
|
||
|
|
let learningMode = 'examples'; // 'examples' | 'rl'
|
||
|
|
let noiseLevel = 0.05;
|
||
|
|
let rlExplorationDecay = 0.97;
|
||
|
|
let animating = true;
|
||
|
|
|
||
|
|
// --- Init ---
|
||
|
|
function init() {
|
||
|
|
iml = new IML(N_INPUTS, N_OUTPUTS, [10, 10, 14], 1000, 1.0, 0.00001);
|
||
|
|
iml.setLogger(msg => console.log('[NISPS]', msg));
|
||
|
|
|
||
|
|
// Visualizer
|
||
|
|
const canvas = document.getElementById('visual-canvas');
|
||
|
|
visualizer = new FlowFieldVisualizer(canvas);
|
||
|
|
|
||
|
|
// Joystick
|
||
|
|
joystick = new VirtualJoystick(document.getElementById('joystick-container'), {
|
||
|
|
size: 160,
|
||
|
|
springBack: false,
|
||
|
|
onChange: onJoystickMove,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Parameter display
|
||
|
|
paramDisplay = new ParamDisplay(document.getElementById('param-display'), N_OUTPUTS);
|
||
|
|
|
||
|
|
// Controls
|
||
|
|
controls = new Controls(document.getElementById('controls-container'), {
|
||
|
|
onAddExample,
|
||
|
|
onTrain,
|
||
|
|
onRandomize,
|
||
|
|
onClear,
|
||
|
|
onThumbsUp,
|
||
|
|
onThumbsDown,
|
||
|
|
onModeChange,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Resize handling
|
||
|
|
window.addEventListener('resize', () => {
|
||
|
|
visualizer.resize();
|
||
|
|
visualizer.initParticles();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Help overlay
|
||
|
|
const helpBtn = document.getElementById('help-btn');
|
||
|
|
const helpOverlay = document.getElementById('help-overlay');
|
||
|
|
if (helpBtn && helpOverlay) {
|
||
|
|
helpBtn.addEventListener('click', () => helpOverlay.classList.toggle('hidden'));
|
||
|
|
helpOverlay.addEventListener('click', () => helpOverlay.classList.add('hidden'));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Run initial inference to populate outputs
|
||
|
|
iml.setInput(0, 0.5);
|
||
|
|
iml.setInput(1, 0.5);
|
||
|
|
iml.process();
|
||
|
|
visualizer.setParams(iml.getOutputs());
|
||
|
|
paramDisplay.update(iml.getOutputs());
|
||
|
|
|
||
|
|
// Start animation
|
||
|
|
animate();
|
||
|
|
|
||
|
|
// Load from localStorage if available
|
||
|
|
loadState();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Animation loop ---
|
||
|
|
function animate() {
|
||
|
|
if (!animating) return;
|
||
|
|
visualizer.draw();
|
||
|
|
requestAnimationFrame(animate);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Joystick handler ---
|
||
|
|
function onJoystickMove(x, y) {
|
||
|
|
iml.setInput(0, x);
|
||
|
|
iml.setInput(1, y);
|
||
|
|
iml.process();
|
||
|
|
|
||
|
|
const outputs = iml.getOutputs();
|
||
|
|
visualizer.setParams(outputs);
|
||
|
|
|
||
|
|
// Only update param display from network in inference (not when user is dragging)
|
||
|
|
if (learningMode !== 'examples' || paramDisplay.activeBar < 0) {
|
||
|
|
paramDisplay.update(outputs);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Examples mode callbacks ---
|
||
|
|
function onAddExample() {
|
||
|
|
// Use current joystick position as input, param bar values as desired output
|
||
|
|
const inputs = [joystick.x, joystick.y];
|
||
|
|
const outputs = [...paramDisplay.values];
|
||
|
|
iml.addExample(inputs, outputs);
|
||
|
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||
|
|
flash('btn-add');
|
||
|
|
}
|
||
|
|
|
||
|
|
function onTrain() {
|
||
|
|
const loss = iml.train();
|
||
|
|
if (loss !== null) {
|
||
|
|
// After training, switch back to inference and update display
|
||
|
|
const outputs = iml.getOutputs();
|
||
|
|
visualizer.setParams(outputs);
|
||
|
|
paramDisplay.update(outputs);
|
||
|
|
controls.updateStatus(iml.exampleCount, loss, noiseLevel);
|
||
|
|
flash('btn-train');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function onRandomize() {
|
||
|
|
iml.randomiseWeights();
|
||
|
|
const outputs = iml.getOutputs();
|
||
|
|
visualizer.setParams(outputs);
|
||
|
|
paramDisplay.update(outputs);
|
||
|
|
noiseLevel = 0.05; // reset noise
|
||
|
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||
|
|
}
|
||
|
|
|
||
|
|
function onClear() {
|
||
|
|
iml.clearDataset();
|
||
|
|
noiseLevel = 0.05;
|
||
|
|
controls.updateStatus(0, null, noiseLevel);
|
||
|
|
clearState();
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- RL mode callbacks ---
|
||
|
|
function onThumbsUp() {
|
||
|
|
// Save current input->output mapping as a positive example
|
||
|
|
const inputs = [joystick.x, joystick.y];
|
||
|
|
const outputs = [...iml.getOutputs()];
|
||
|
|
iml.addExample(inputs, outputs);
|
||
|
|
|
||
|
|
// Retrain incrementally
|
||
|
|
iml.train();
|
||
|
|
|
||
|
|
// Decay noise - more positive examples = less exploration
|
||
|
|
noiseLevel *= rlExplorationDecay;
|
||
|
|
noiseLevel = Math.max(noiseLevel, 0.005);
|
||
|
|
|
||
|
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||
|
|
flash('btn-thumbsup');
|
||
|
|
}
|
||
|
|
|
||
|
|
function onThumbsDown() {
|
||
|
|
// Increase noise for more exploration
|
||
|
|
noiseLevel = Math.min(noiseLevel * 1.5, 0.3);
|
||
|
|
|
||
|
|
// Perturb weights
|
||
|
|
iml.moveWeights(noiseLevel);
|
||
|
|
|
||
|
|
const outputs = iml.getOutputs();
|
||
|
|
visualizer.setParams(outputs);
|
||
|
|
paramDisplay.update(outputs);
|
||
|
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||
|
|
flash('btn-thumbsdown');
|
||
|
|
}
|
||
|
|
|
||
|
|
function onModeChange(mode) {
|
||
|
|
learningMode = mode;
|
||
|
|
if (mode === 'examples') {
|
||
|
|
paramDisplay.setDraggable(true);
|
||
|
|
} else {
|
||
|
|
paramDisplay.setDraggable(false);
|
||
|
|
}
|
||
|
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Presets ---
|
||
|
|
window.loadPreset = function(name) {
|
||
|
|
iml.clearDataset();
|
||
|
|
|
||
|
|
if (name === 'calm-to-chaotic') {
|
||
|
|
// Bottom-left: slow, smooth, cool; top-right: fast, turbulent, warm
|
||
|
|
iml.addExample([0.1, 0.9], [0.25, 0.3, 0.1, 0.55, 0.2, 0.3, 0.02, 0.05]);
|
||
|
|
iml.addExample([0.9, 0.1], [0.75, 0.7, 0.9, 0.05, 0.8, 0.7, 0.9, 0.95]);
|
||
|
|
iml.addExample([0.5, 0.5], [0.5, 0.5, 0.5, 0.3, 0.5, 0.5, 0.4, 0.5]);
|
||
|
|
} else if (name === 'rainbow-sweep') {
|
||
|
|
// Left to right sweeps through hues
|
||
|
|
iml.addExample([0.0, 0.5], [0.5, 0.5, 0.4, 0.0, 0.3, 0.4, 0.05, 0.3]);
|
||
|
|
iml.addExample([0.5, 0.5], [0.5, 0.5, 0.4, 0.5, 0.3, 0.4, 0.05, 0.3]);
|
||
|
|
iml.addExample([1.0, 0.5], [0.5, 0.5, 0.4, 1.0, 0.3, 0.4, 0.05, 0.3]);
|
||
|
|
} else if (name === 'vortex') {
|
||
|
|
// Center: tight spiral, edges: wide flow
|
||
|
|
iml.addExample([0.5, 0.5], [0.0, 0.8, 0.8, 0.6, 0.1, 0.15, 0.02, 1.0]);
|
||
|
|
iml.addExample([0.0, 0.0], [0.5, 0.2, 0.3, 0.8, 0.9, 0.6, 0.08, 0.1]);
|
||
|
|
iml.addExample([1.0, 1.0], [0.5, 0.2, 0.3, 0.2, 0.9, 0.6, 0.08, 0.1]);
|
||
|
|
iml.addExample([0.0, 1.0], [0.3, 0.4, 0.5, 0.4, 0.5, 0.4, 0.05, 0.5]);
|
||
|
|
iml.addExample([1.0, 0.0], [0.7, 0.4, 0.5, 0.0, 0.5, 0.4, 0.05, 0.5]);
|
||
|
|
}
|
||
|
|
|
||
|
|
const loss = iml.train();
|
||
|
|
const outputs = iml.getOutputs();
|
||
|
|
visualizer.setParams(outputs);
|
||
|
|
paramDisplay.update(outputs);
|
||
|
|
controls.updateStatus(iml.exampleCount, loss, noiseLevel);
|
||
|
|
};
|
||
|
|
|
||
|
|
// --- Persistence ---
|
||
|
|
function saveState() {
|
||
|
|
try {
|
||
|
|
const state = {
|
||
|
|
features: iml.dataset.features,
|
||
|
|
labels: iml.dataset.labels,
|
||
|
|
};
|
||
|
|
localStorage.setItem('nisps-playground', JSON.stringify(state));
|
||
|
|
} catch (e) { /* ignore */ }
|
||
|
|
}
|
||
|
|
|
||
|
|
function loadState() {
|
||
|
|
try {
|
||
|
|
const data = JSON.parse(localStorage.getItem('nisps-playground'));
|
||
|
|
if (data && data.features && data.features.length > 0) {
|
||
|
|
for (let i = 0; i < data.features.length; i++) {
|
||
|
|
iml.addExample(data.features[i], data.labels[i]);
|
||
|
|
}
|
||
|
|
iml.train();
|
||
|
|
const outputs = iml.getOutputs();
|
||
|
|
visualizer.setParams(outputs);
|
||
|
|
paramDisplay.update(outputs);
|
||
|
|
controls.updateStatus(iml.exampleCount, iml.lastLoss, noiseLevel);
|
||
|
|
}
|
||
|
|
} catch (e) { /* ignore */ }
|
||
|
|
}
|
||
|
|
|
||
|
|
function clearState() {
|
||
|
|
try { localStorage.removeItem('nisps-playground'); } catch (e) { /* ignore */ }
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auto-save periodically
|
||
|
|
setInterval(saveState, 10000);
|
||
|
|
|
||
|
|
// Visual feedback flash
|
||
|
|
function flash(id) {
|
||
|
|
const el = document.getElementById(id);
|
||
|
|
if (!el) return;
|
||
|
|
el.classList.add('flash');
|
||
|
|
setTimeout(() => el.classList.remove('flash'), 200);
|
||
|
|
}
|
||
|
|
|
||
|
|
// --- Start ---
|
||
|
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
|
init();
|
||
|
|
// Start in examples mode with draggable params
|
||
|
|
paramDisplay.setDraggable(true);
|
||
|
|
});
|