/* global React */ const { useState, useEffect, useRef, useMemo } = React; // ============================================================ // PLAYGROUND // ============================================================ // Simple env visualizations const WarehouseViz = ({ running, tick, quality = 1, policyMode }) => { // Robot moves along a path on a grid const W = 560, H = 380; const cell = 24; const shelves = [ [3,2],[3,3],[3,4], [7,2],[7,3],[7,4], [11,2],[11,3],[11,4], [3,7],[3,8],[3,9], [7,7],[7,8],[7,9], [11,7],[11,8],[11,9], [15,2],[15,3],[15,4], [15,7],[15,8],[15,9], ]; const dropoffs = [[19,2],[19,7],[19,12]]; const pathPoints = [ [1,1],[3,1],[3,5],[7,5],[7,1],[11,1],[11,5],[15,5],[19,5],[19,2],[19,7],[15,7],[11,10],[7,10],[3,10],[1,10],[1,5],[1,1] ]; // Random wandering trajectory: drunkard's walk avoiding shelves/walls. Keeps last ~24 points. const randomTrail = useMemo(() => { if (quality >= 0.6) return null; const shelfSet = new Set(shelves.map(([x,y]) => `${x},${y}`)); const blocked = (x, y) => x < 1 || x > 19 || y < 1 || y > 13 || shelfSet.has(`${x},${y}`); let s = 31; // seed const rnd = () => { s = (s * 9301 + 49297) % 233280; return s/233280; }; const trail = [[1,1]]; const T = Math.max(2, Math.floor(tick) % 60 + 6); for (let i = 1; i < T; i++) { const [px, py] = trail[trail.length-1]; const dirs = [[1,0],[-1,0],[0,1],[0,-1]]; let moved = false; for (let tries = 0; tries < 8 && !moved; tries++) { const [dx, dy] = dirs[Math.floor(rnd()*4)]; if (!blocked(px+dx, py+dy)) { trail.push([px+dx, py+dy]); moved = true; } } if (!moved) trail.push([px, py]); } return trail.slice(-24); }, [quality, Math.floor(tick)]); const idx = running ? Math.floor(tick) % pathPoints.length : 0; const robot = quality < 0.6 && randomTrail ? randomTrail[randomTrail.length-1] : pathPoints[idx]; const nextIdx = (idx + 1) % pathPoints.length; const next = pathPoints[nextIdx]; const showPlannedPath = quality >= 0.4; return ( ); }; const InventoryViz = ({ running, tick }) => { const W = 560, H = 380; const stores = 12, skus = 10; // Deterministic stock levels const stock = Array.from({length: stores}, (_, s) => Array.from({length: skus}, (_, k) => { const base = 0.3 + 0.5 * Math.sin((s*7 + k*3 + (running?tick:0)*0.3) * 0.4); return Math.max(0, Math.min(1, base)); }) ); const cw = 36, ch = 28; const offX = 60, offY = 30; return ( ); }; const PortfolioViz = ({ running, tick }) => { const W = 560, H = 380; const pad = 40; const pw = W - 2*pad, ph = H - 2*pad - 40; // Generate price series const n = 120; const series = useMemo(() => { const arr = [100]; for (let i = 1; i < n; i++) { const r = Math.sin(i*0.15) * 0.8 + Math.sin(i*0.05) * 1.2 + (((i*13)%7)-3)*0.3; arr.push(Math.max(50, arr[i-1] + r + 0.15)); } return arr; }, []); const shown = running ? Math.min(n, 20 + Math.floor(tick * 0.6)) : n; const visible = series.slice(0, shown); const min = Math.min(...series), max = Math.max(...series); const range = max - min; const pts = visible.map((v, i) => { const x = pad + (i / (n-1)) * pw; const y = pad + ph - ((v - min)/range) * ph; return [x, y]; }); const allocs = [0.32, 0.24, 0.18, 0.14, 0.08, 0.04]; const assets = ['AAPL','MSFT','NVDA','GOOG','TSLA','AMZN']; return ( ); }; const TruckViz = ({ running, tick }) => { const W = 560, H = 380; const cities = [ {n:'NYC',x:450,y:140}, {n:'CHI',x:320,y:130}, {n:'DAL',x:280,y:260}, {n:'LAX',x:80,y:240}, {n:'SEA',x:90,y:80}, {n:'ATL',x:400,y:230}, {n:'DEN',x:210,y:170}, {n:'MIA',x:460,y:310} ]; const routes = [ [0,1],[1,2],[2,5],[5,0],[1,6],[6,3],[3,4],[4,1],[2,3],[6,4],[5,7],[2,7] ]; // Active trucks moving along routes const trucks = routes.slice(0, 6).map((r, i) => { const [a, b] = r; const t = running ? ((tick * 0.02 + i*0.18) % 1) : 0.3 + i*0.1; const x = cities[a].x + (cities[b].x - cities[a].x) * t; const y = cities[a].y + (cities[b].y - cities[a].y) * t; return {x, y, route: r, t}; }); return ( ); }; // Reward curve (small) const RewardCurve = ({ running, tick, algoIdx = 0, fit = 0.9, envId = 'warehouse' }) => { const W = 270, H = 110; const pad = 26; const pw = W - pad - 10, ph = H - pad - 8; const n = 100; const data = useMemo(() => { const envBase = { warehouse: 920, inventory: 1180, portfolio: 17.2, truck: 2080 }[envId] || 800; const asymp = envBase * fit; const noiseScale = asymp * 0.04 * (1.1 - fit); // poor fit = noisier const arr = []; for (let i = 0; i < n; i++) { const v = asymp * (1 - Math.exp(-i / 22)) + Math.sin(i*0.4 + algoIdx) * noiseScale + ((i*7)%17 - 8) * (noiseScale * 0.25); arr.push(v); } return arr; }, [algoIdx, fit, envId]); const shown = running ? Math.min(n, Math.floor(tick * 0.9) + 5) : n; const visible = data.slice(0, shown); const max = Math.max(...data), min = Math.min(...data, 0); const pts = visible.map((v, i) => { const x = pad + (i / (n-1)) * pw; const y = pad + ph - ((v - min)/(max-min)) * ph; return [x, y]; }); return ( ); }; // ---- Configure & Train view ---- // Per-algorithm hyperparameter schemas. Each entry has a "common" block (shared) // and an "algo" block (tailored to that specific algorithm's equations). const HP_COMMON = [ { key: 'totalSteps', label: 'Total steps', type: 'number', def: 200000, min: 10000, max: 5000000, step: 10000, help: 'Total environment steps to train for.' }, { key: 'batchSize', label: 'Batch size', type: 'number', def: 256, min: 32, max: 4096, step: 32, help: 'Minibatch size per gradient update.' }, { key: 'lr', label: 'Learning rate', type: 'number', def: 3e-4, min: 1e-6, max: 1e-2, step: 1e-5, help: 'Adam learning rate.' }, { key: 'gamma', label: 'Discount γ', type: 'slider', def: 0.99, min: 0.8, max: 0.999, step: 0.001, help: 'Future-reward discount factor.' }, { key: 'seed', label: 'Seed', type: 'number', def: 42, min: 0, max: 100000, step: 1, help: 'RNG seed for reproducibility.' }, { key: 'hidden', label: 'Hidden units', type: 'select', def: '256x2', options: ['64x2','128x2','256x2','256x3','512x3','1024x3'], help: 'MLP width × depth.' }, ]; const HP_PER_ALGO = { 'PPO': [ { key: 'clip', label: 'Clip ε', type: 'slider', def: 0.2, min: 0.05, max: 0.5, step: 0.01, help: 'Clipped surrogate bound. 0.1–0.3 is typical.' }, { key: 'gaeLambda', label: 'GAE λ', type: 'slider', def: 0.95, min: 0.8, max: 1.0, step: 0.01, help: 'Generalized Advantage Estimation trade-off.' }, { key: 'entCoef', label: 'Entropy coef', type: 'slider', def: 0.01, min: 0, max: 0.1, step: 0.001, help: 'Exploration bonus on policy entropy.' }, { key: 'vfCoef', label: 'Value-fn coef', type: 'slider', def: 0.5, min: 0.1, max: 1.0, step: 0.05, help: 'Critic loss weighting.' }, { key: 'epochs', label: 'Epochs / rollout', type: 'number', def: 10, min: 1, max: 40, step: 1, help: 'SGD passes over each on-policy buffer.' }, { key: 'rolloutLen', label: 'Rollout length', type: 'number', def: 2048, min: 128, max: 8192, step: 128, help: 'Steps collected per update.' }, ], 'TRPO': [ { key: 'klTarget', label: 'KL target', type: 'slider', def: 0.01, min: 0.001, max: 0.1, step: 0.001, help: 'Trust-region KL budget per step.' }, { key: 'cgIters', label: 'CG iterations', type: 'number', def: 10, min: 5, max: 50, step: 1, help: 'Conjugate gradient steps for Fisher-vector product.' }, { key: 'damping', label: 'CG damping', type: 'slider', def: 0.1, min: 0.01, max: 1.0, step: 0.01, help: 'Levenberg-Marquardt damping on Fisher matrix.' }, { key: 'gaeLambda', label: 'GAE λ', type: 'slider', def: 0.98, min: 0.8, max: 1.0, step: 0.01, help: 'Advantage estimator smoothing.' }, ], 'GRPO': [ { key: 'groupSize', label: 'Group size', type: 'number', def: 8, min: 2, max: 64, step: 1, help: 'Rollouts per prompt for group-relative baseline.' }, { key: 'klCoef', label: 'KL coef', type: 'slider', def: 0.04, min: 0, max: 0.2, step: 0.005, help: 'KL-to-reference regularizer.' }, { key: 'clip', label: 'Clip ε', type: 'slider', def: 0.2, min: 0.05, max: 0.5, step: 0.01, help: 'Importance-ratio clip.' }, { key: 'refreshRef', label: 'Refresh reference', type: 'select', def: 'off', options: ['off','every 10k','every 50k'], help: 'Cadence to re-snap the reference policy.' }, ], 'SAC': [ { key: 'tau', label: 'Polyak τ', type: 'slider', def: 0.005, min: 0.001, max: 0.05, step: 0.001, help: 'Target network soft-update rate.' }, { key: 'alpha', label: 'Entropy α', type: 'slider', def: 0.2, min: 0, max: 1.0, step: 0.01, help: 'Entropy temperature. 0 = auto-tune.' }, { key: 'autoAlpha', label: 'Auto α', type: 'select', def: 'on', options: ['on','off'], help: 'Target-entropy based α tuning.' }, { key: 'replaySize', label: 'Replay buffer', type: 'select', def: '1e6', options: ['1e5','5e5','1e6','5e6'], help: 'Off-policy replay capacity.' }, { key: 'updatesPerStep',label: 'UTD ratio', type: 'slider', def: 1, min: 0.25, max: 4, step: 0.25, help: 'Gradient updates per env step.' }, { key: 'warmup', label: 'Warmup steps', type: 'number', def: 10000, min: 0, max: 100000, step: 1000, help: 'Random-action collection before learning.' }, ], 'TD-MPC2': [ { key: 'horizon', label: 'Planning horizon', type: 'number', def: 5, min: 1, max: 30, step: 1, help: 'MPC rollout depth.' }, { key: 'numSamples', label: 'MPPI samples', type: 'number', def: 512, min: 64, max: 4096, step: 64, help: 'Candidate trajectories per step.' }, { key: 'temperature', label: 'MPPI temp', type: 'slider', def: 0.5, min: 0.01, max: 2.0, step: 0.01, help: 'Softmax temperature over trajectory returns.' }, { key: 'latentDim', label: 'Latent dim', type: 'select', def: '512', options: ['128','256','512','1024'], help: 'World-model latent size.' }, { key: 'consistencyCoef', label: 'Consistency coef', type: 'slider', def: 20, min: 0, max: 100, step: 1, help: 'Latent-dynamics consistency loss.' }, ], 'DreamerV3': [ { key: 'imgHorizon', label: 'Imagination H', type: 'number', def: 15, min: 5, max: 50, step: 1, help: 'Latent rollout horizon for actor-critic.' }, { key: 'worldLr', label: 'World-model LR', type: 'number', def: 1e-4, min: 1e-6, max: 1e-2, step: 1e-5, help: 'Separate LR for RSSM world model.' }, { key: 'actorLr', label: 'Actor LR', type: 'number', def: 3e-5, min: 1e-6, max: 1e-2, step: 1e-6, help: 'Actor network learning rate.' }, { key: 'kl', label: 'KL balance', type: 'slider', def: 0.8, min: 0.1, max: 0.99, step: 0.01, help: 'Prior vs posterior KL weight.' }, { key: 'symlog', label: 'Symlog rewards', type: 'select', def: 'on', options: ['on','off'], help: 'Apply symlog transform to returns.' }, ], 'Rainbow-DQN': [ { key: 'epsilon', label: 'ε-greedy', type: 'slider', def: 0.05, min: 0, max: 1.0, step: 0.01, help: 'Exploration rate.' }, { key: 'nStep', label: 'n-step returns', type: 'number', def: 3, min: 1, max: 10, step: 1, help: 'Multi-step bootstrap length.' }, { key: 'prioritized', label: 'Prioritized replay',type: 'select', def: 'on', options: ['on','off'], help: 'Use TD-error priorities.' }, { key: 'noisyNets', label: 'Noisy nets', type: 'select', def: 'on', options: ['on','off'], help: 'Parameter-space exploration.' }, { key: 'distAtoms', label: 'Distributional atoms', type: 'number', def: 51, min: 11, max: 101, step: 2, help: 'C51 return-distribution bins.' }, { key: 'replaySize', label: 'Replay buffer', type: 'select', def: '1e6', options: ['1e5','5e5','1e6','5e6'], help: 'Replay capacity.' }, ], 'GAIL': [ { key: 'numDemos', label: '# expert demos', type: 'number', def: 40, min: 5, max: 500, step: 5, help: 'Trajectories available to the discriminator.' }, { key: 'discLr', label: 'Discriminator LR', type: 'number', def: 1e-4, min: 1e-6, max: 1e-2, step: 1e-5, help: 'D network learning rate.' }, { key: 'discSteps', label: 'D / G ratio', type: 'number', def: 2, min: 1, max: 10, step: 1, help: 'Discriminator updates per policy update.' }, { key: 'entCoef', label: 'Entropy coef', type: 'slider', def: 0.01, min: 0, max: 0.1, step: 0.001, help: 'Policy-entropy bonus.' }, { key: 'rewardClip', label: 'Reward clip', type: 'select', def: '±10', options: ['off','±1','±10','tanh'], help: 'Stabilizer on imitation reward scale.' }, ], 'AIRL': [ { key: 'numDemos', label: '# expert demos', type: 'number', def: 40, min: 5, max: 500, step: 5, help: 'Demo trajectories available.' }, { key: 'shapingLr', label: 'Shaping fn LR', type: 'number', def: 1e-4, min: 1e-6, max: 1e-2, step: 1e-5, help: 'LR for disentangled potential g(s).' }, { key: 'gradPenalty', label: 'Grad penalty λ', type: 'slider', def: 10, min: 0, max: 50, step: 1, help: 'WGAN-GP style regularizer on discriminator.' }, { key: 'rewardType', label: 'Reward form', type: 'select', def: 'airl', options: ['airl','vail'], help: 'AIRL disentangled vs variational.' }, ], 'CQL': [ { key: 'cqlAlpha', label: 'CQL α', type: 'slider', def: 1.0, min: 0, max: 10, step: 0.1, help: 'Conservative Q regularizer strength.' }, { key: 'lagrangeThresh',label: 'Lagrange thresh', type: 'slider', def: 10, min: 0, max: 50, step: 1, help: 'Target gap for auto-tuning α.' }, { key: 'numRandom', label: 'OOD samples', type: 'number', def: 10, min: 1, max: 50, step: 1, help: 'Random actions sampled for Q-penalty.' }, { key: 'dataset', label: 'Dataset', type: 'select', def: 'medium-expert', options: ['random','medium','medium-replay','medium-expert','expert'], help: 'D4RL-style dataset quality.' }, ], 'IQL': [ { key: 'expectile', label: 'Expectile τ', type: 'slider', def: 0.7, min: 0.5, max: 0.95, step: 0.01, help: 'Asymmetric expectile for value target.' }, { key: 'beta', label: 'AWR β', type: 'slider', def: 3.0, min: 0.1, max: 10, step: 0.1, help: 'Advantage-weighted temperature.' }, { key: 'dataset', label: 'Dataset', type: 'select', def: 'medium-expert', options: ['random','medium','medium-replay','medium-expert','expert'], help: 'Dataset quality.' }, { key: 'clipAdv', label: 'Advantage clip', type: 'slider', def: 100, min: 10, max: 500, step: 10, help: 'Max |A| for AWR weights.' }, ], 'Decision Transformer': [ { key: 'contextLen', label: 'Context length K', type: 'number', def: 20, min: 5, max: 200, step: 5, help: 'Token window fed to transformer.' }, { key: 'embedDim', label: 'Embed dim', type: 'select', def: '128', options: ['64','128','256','512'], help: 'Per-token embedding size.' }, { key: 'nLayers', label: 'Transformer layers',type: 'number', def: 6, min: 2, max: 24, step: 1, help: 'Depth of the decoder stack.' }, { key: 'nHeads', label: 'Attn heads', type: 'number', def: 8, min: 1, max: 16, step: 1, help: 'Multi-head attention.' }, { key: 'targetReturn', label: 'Conditioning RTG', type: 'number', def: 2000, min: 100, max: 10000, step: 100, help: 'Return-to-go conditioning at rollout.' }, ], 'OptRL-Warehouse-v3': [ { key: 'horizon', label: 'MPC horizon', type: 'number', def: 32, min: 4, max: 128, step: 4, help: 'Planner lookahead depth.' }, { key: 'hierLevels', label: 'Hierarchy levels', type: 'select', def: 3, options: [2,3,4], help: 'Abstraction levels: pick-plan-route.' }, { key: 'constraintW', label: 'Constraint weight', type: 'slider', def: 0.5, min: 0, max: 2.0, step: 0.05, help: 'Lagrangian weight on capacity constraints.' }, { key: 'demandModel', label: 'Demand model', type: 'select', def: 'poisson-mix', options: ['poisson','poisson-mix','neural'], help: 'Arrival model for forward sim.' }, ], 'OptRL-Truck-v1': [ { key: 'graphLayers', label: 'GNN layers', type: 'number', def: 4, min: 2, max: 12, step: 1, help: 'Graph encoder depth over road graph.' }, { key: 'mpcHorizon', label: 'MPC horizon', type: 'number', def: 24, min: 4, max: 96, step: 4, help: 'Trajectory lookahead for MPC decoder.' }, { key: 'fleetSize', label: 'Fleet size', type: 'number', def: 50, min: 5, max: 500, step: 5, help: 'Simulated concurrent trucks.' }, { key: 'fuelCoef', label: 'Fuel cost coef', type: 'slider', def: 1.2, min: 0, max: 5, step: 0.1, help: 'Multi-objective weight on fuel vs time.' }, ], }; const schemaForAlgo = (algo) => [...HP_COMMON, ...(HP_PER_ALGO[algo] || [])]; const classFor = (algo) => ({ 'PPO':'policy','TRPO':'policy','GRPO':'policy', 'SAC':'value','TD-MPC2':'value','DreamerV3':'value','Rainbow-DQN':'value', 'GAIL':'irl','AIRL':'irl', 'CQL':'offline','IQL':'offline','Decision Transformer':'offline', 'OptRL-Warehouse-v3':'hybrid-warehouse','OptRL-Truck-v1':'hybrid-truck', }[algo] || 'value'); const HpField = ({ f, val, onChange }) => { const safeVal = val === undefined || val === null ? f.def : val; if (f.type === 'slider') { return ( ); } if (f.type === 'select') { return ( ); } return ( ); }; const PgTrain = ({ D, algo, setAlgo, envId, setEnvId, onPublish, onNav }) => { const cls = classFor(algo); const schema = useMemo(() => schemaForAlgo(algo), [algo]); const [hp, setHp] = useState(() => Object.fromEntries(schema.map(f => [f.key, f.def]))); // reset hp when algo changes (each algo has its own parameter set) useEffect(() => { setHp(Object.fromEntries(schemaForAlgo(algo).map(f => [f.key, f.def]))); }, [algo]); const [training, setTraining] = useState(false); const [tick, setTick] = useState(0); // policyMode: 'idle' (no sim), 'random' (untrained sim), 'training' (learning), 'trained' (deployed sim) const [policyMode, setPolicyMode] = useState('idle'); // separate animation tick for sim-only modes (random / trained) so it loops without progressing training const [simTick, setSimTick] = useState(0); useEffect(() => { if (policyMode !== 'training') return; const id = setInterval(() => setTick(t => { const next = t + 1; if (next >= 100) { setTraining(false); setPolicyMode('trained'); return 100; } return next; }), 140); return () => clearInterval(id); }, [policyMode]); useEffect(() => { if (policyMode === 'random' || policyMode === 'trained') { const id = setInterval(() => setSimTick(t => t + 1), 140); return () => clearInterval(id); } }, [policyMode]); const env = D.playgroundEnvs.find(e => e.id === envId) || D.playgroundEnvs[0]; const algoIdx = D.playgroundAlgos.indexOf(algo); // Algo × env × hyperparameter fit (drives curve quality) const fit = useMemo(() => { const profile = { warehouse: { kind:'discrete-combinatorial', longH:0.8, imit:1.0 }, inventory: { kind:'discrete-sequential', longH:0.3, imit:0.6 }, portfolio: { kind:'offline-continuous', longH:0.7, imit:0.2 }, truck: { kind:'continuous-planning', longH:1.0, imit:0.8 }, }[env.id]; let f = 0.85; if (cls === 'policy') f = env.id==='portfolio' ? 0.60 : (profile.longH>0.8 ? 0.78 : 0.92); else if (cls === 'value') { if (algo === 'Rainbow-DQN') f = profile.kind.includes('discrete') ? 1.00 : 0.58; else if (algo === 'DreamerV3' || algo === 'TD-MPC2') f = 0.85 + 0.15 * profile.longH; else f = profile.kind.includes('continuous') ? 0.98 : 0.80; if (env.id === 'portfolio') f -= 0.15; } else if (cls === 'irl') f = 0.55 + 0.40 * profile.imit; else if (cls === 'offline') { f = env.id === 'portfolio' ? 1.02 : 0.70; if (algo==='Decision Transformer' && env.id==='portfolio') f = 1.05; } else if (cls === 'hybrid-warehouse') f = env.id === 'warehouse' ? 1.08 : 0.82; else if (cls === 'hybrid-truck') f = env.id === 'truck' ? 1.10 : 0.80; // hyperparameter sensitivity penalties, make tuning feel consequential if (hp.lr > 3e-3 || hp.lr < 1e-5) f *= 0.75; if (algo === 'PPO' && (hp.clip < 0.1 || hp.clip > 0.35)) f *= 0.9; if (algo === 'SAC' && hp.tau > 0.02) f *= 0.9; if (algo === 'TRPO' && hp.klTarget > 0.05) f *= 0.85; if (algo === 'CQL' && (hp.cqlAlpha === 0)) f *= 0.7; return Math.max(0.3, f); }, [algo, cls, env.id, hp.lr, hp.clip, hp.tau, hp.klTarget, hp.cqlAlpha]); const VizComponent = { warehouse: WarehouseViz, inventory: InventoryViz, portfolio: PortfolioViz, truck: TruckViz }[env.id]; // Derived training stats const envBase = { warehouse: 920, inventory: 1180, portfolio: 17.2, truck: 2080 }[env.id] || 800; const curReturn = envBase * fit * (1 - Math.exp(-tick/25)); const successRate = Math.min(97, 25 + tick * 0.9 * fit); const stability = 0.55 + fit * 0.35 + Math.min(tick, 100) * 0.0012; const episodes = Math.floor(tick * (hp.totalSteps / 100) / env.episodeLen); // Successful-runs tracker: N=10 recent episodes, each one is success if a biased coin flips true const recentEpisodes = useMemo(() => { const n = 12; const p = successRate / 100; const arr = []; // deterministic pseudo-random keyed on tick + algo + env so the pattern is stable per render let s = (tick * 9301 + algoIdx * 49297 + env.id.length * 233280) % 233280; for (let i = 0; i < n; i++) { s = (s * 9301 + 49297) % 233280; arr.push((s / 233280) < p); } return arr; }, [tick, algoIdx, env.id, successRate]); return (
| # | Session ID | Environment | Algorithm | Status | Episode | Return | Started | |
|---|---|---|---|---|---|---|---|---|
| {String(i+1).padStart(2,'0')} | {s.id} | {s.env} | {s.algo} | {s.status} | {s.ep} | {s.ret} | {s.started} | {s.status === 'running' ? : s.status === 'complete' ? : } |