/* 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 ( {/* subtle axis ticks */} {Array.from({length: Math.floor(W/cell)}).map((_, i) => ( ))} {/* shelves */} {shelves.map(([x,y], i) => ( ))} {/* dropoffs */} {dropoffs.map(([x,y], i) => ( D{i+1} ))} {/* planned path */} {showPlannedPath && ( `${x*cell + cell/2 - 1},${y*cell + cell/2 - 1}`).join(' ')} fill="none" stroke="var(--accent-3)" strokeWidth="1" strokeDasharray="4 3" opacity="0.4" /> )} {/* trajectory: trained = filling along plan; random = wandering */} {quality < 0.6 && randomTrail ? ( `${x*cell + cell/2 - 1},${y*cell + cell/2 - 1}`).join(' ')} fill="none" stroke="var(--ink-3)" strokeWidth="1.5" strokeDasharray="2 2" opacity="0.7" /> ) : ( `${x*cell + cell/2 - 1},${y*cell + cell/2 - 1}`).join(' ')} fill="none" stroke="var(--accent)" strokeWidth="2" /> )} {/* robot */} {/* target */} {/* legend */} robot shelf drop-off ); }; 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 ( SKU → {Array.from({length: skus}).map((_, k) => ( S{k+1} ))} {Array.from({length: stores}).map((_, s) => ( #{s+1} ))} {stock.map((row, s) => row.map((v, k) => ( )))} stock level: low mid high ); }; 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 ( {/* axes */} {/* gridlines */} {[0.25, 0.5, 0.75].map(f => ( ))} NAV (norm.) t (days) {/* benchmark */} benchmark {/* policy curve */} {pts.length > 1 && ( p.join(',')).join(' ')} fill="none" stroke="var(--accent)" strokeWidth="2" /> )} {pts.length > 0 && ( )} {/* allocation bars */} {allocs.map((a, i) => { const x = i * ((pw)/6); return ( {assets[i]} {(a*100).toFixed(0)}% ); })} ); }; 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 ( {/* map outline ish */} CONTINENTAL · 8 CITIES {/* routes */} {routes.map((r, i) => { const [a, b] = r; return ( ); })} {/* active route highlight */} {trucks.map((t, i) => { const [a, b] = t.route; return ( ); })} {/* cities */} {cities.map((c, i) => ( {c.n} ))} {/* trucks */} {trucks.map((t, i) => ( {running && } ))} hub truck active route ); }; // 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 ( {Math.round(max)} 0 steps reward {pts.length > 1 && ( p.join(',')).join(' ')} fill="none" stroke="var(--accent)" strokeWidth="1.5"/> )} ); }; // ---- 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 (

Configure & Train

{algo} · {env.name} | { e.preventDefault(); onNav && onNav('pg-envs'); }} style={{color: 'var(--accent)', textDecoration: 'none'}}>← Back to Overview
Tune hyperparameters on the left, watch this algorithm learn on the right. Reward curve, seed-to-seed stability, and successful episodes update live, so you can see whether a config is actually improving the policy.
{/* LEFT, algorithm + hyperparameters + launch */}
Algorithm
class: {cls}
=0.95?'var(--ok)':fit>=0.8?'var(--accent)':'#b64a3a', fontWeight: 600}}> fit {Math.round(fit*100)}%
Launch
{algo} · {env.name}
seed={hp.seed} · {Number(hp.totalSteps).toLocaleString()} steps
{Math.min(100, tick)}%
policy:  {policyMode === 'training' ? 'training (live)' : policyMode === 'trained' ? `trained · q=${Math.round(fit*100)}%` : policyMode === 'random' ? 'random (untrained)' : tick >= 100 ? 'trained · idle' : 'untrained · idle'}
{policyMode !== 'training' ? ( ) : ( )} {policyMode !== 'random' ? : }
{tick >= 100 && (
{policyMode !== 'trained' ? : }
)}
Common shared
{HP_COMMON.map(f => setHp(h => ({...h, [f.key]: v}))}/>)}
{algo}-specific {(HP_PER_ALGO[algo] || []).length} params
{(HP_PER_ALGO[algo] || []).map(f => setHp(h => ({...h, [f.key]: v}))}/>)}
{/* RIGHT, env viz + reward + stability + successful runs */}
env {env.name} {policyMode === 'training' ? '◉ training' : policyMode === 'trained' ? '◉ trained policy' : policyMode === 'random' ? '◉ random policy' : '○ idle'} ep {episodes} · step {(tick * (hp.totalSteps/100) | 0).toLocaleString()}
{VizComponent && (() => { const isSim = policyMode === 'random' || policyMode === 'trained'; const animTick = isSim ? simTick * 1.2 : tick * 1.2; const quality = policyMode === 'random' ? 0 : policyMode === 'trained' ? Math.min(1, fit) : policyMode === 'training' ? Math.min(1, fit) * (tick/100) : tick >= 100 ? Math.min(1, fit) : 0; // idle: untrained or trained snapshot const isRunning = policyMode !== 'idle'; return ; })()}
Return (ema)
{curReturn.toFixed(1)}
{training ? '↗ learning' : '- idle'}
Stability σ
{stability.toFixed(2)}
seed-to-seed
Success %
{successRate.toFixed(1)}%
last 12 episodes
GPU-h est.
{((hp.totalSteps/50000) * (HP_COMMON.find(f=>f.key==='hidden').options.indexOf(hp.hidden) * 0.3 + 1)).toFixed(1)}
projected
Reward curve mean return vs. steps
Stability return ± σ across 3 seeds
Successful runs {recentEpisodes.filter(Boolean).length}/{recentEpisodes.length} recent episodes succeeded · justifies convergence
); }; // ---- Stability (mean ± σ band) ---- const StabilityCurve = ({ running, tick, fit }) => { const W = 560, H = 150, pad = 24; const pw = W - pad*2, ph = H - pad*2; const N = 60; // three seeds drift around a mean; bad fit = wide band const data = useMemo(() => { const arr = []; const T = Math.max(5, Math.min(N, tick)); for (let i = 0; i < T; i++) { const t = i / N; const mean = fit * (1 - Math.exp(-t * 3)) * 0.9; const noise = (1 - fit) * 0.55 + 0.08; const sigma = noise * (0.6 + 0.4*(1-t)); arr.push({ mean, sigma }); } return arr; }, [tick, fit]); const pts = (key, sign=0) => data.map((d, i) => { const x = pad + (i / Math.max(1, N-1)) * pw; const y = pad + ph - (Math.max(0, Math.min(1, d[key] + sign * d.sigma))) * ph; return [x.toFixed(1), y.toFixed(1)]; }); const meanPts = pts('mean'); const hi = pts('mean', +1); const lo = pts('mean', -1); const band = [...hi, ...lo.slice().reverse()].map(p => p.join(',')).join(' '); return ( 1.0 0 steps {data.length > 2 && } {meanPts.length > 1 && p.join(',')).join(' ')} fill="none" stroke="var(--accent-2)" strokeWidth="1.8"/>} ); }; // ---- Successful runs (episode strip) ---- const SuccessfulRuns = ({ episodes, envId }) => { const envTerm = { warehouse: 'orders filled', inventory: 'demand met', portfolio: 'beat S&P', truck: 'on-time delivery' }[envId] || 'solved'; return (
{episodes.map((ok, i) => (
{ok ? '✓' : '·'}
))}
Each square is one recent evaluation episode, filled squares are successful ({envTerm}). A rising success rate confirms the policy is actually learning the task, not just accumulating reward from easy states.
); }; const PgAlgorithms = ({ D, onPick }) => (

Algorithms // catalog of available policies

Every algorithm below is pre-wired to OptRL's orchestrator. Click an entry to load it into the sandbox with sensible defaults for the chosen environment.
{(D.playgroundAlgoGroups || []).map(g => (
{g.group} {g.hint}
{g.items.map(it => (
onPick(it.name)} style={{cursor: 'pointer'}}>
{it.name} load →
{it.note}
))}
))}
); // ---- My Sessions view ---- const PgSessions = ({ onResume }) => { const sessions = [ { id: 'sess-0421-a8', env: 'Warehouse Logistics-v2', algo: 'PPO', status: 'running', ep: 142, ret: 887.4, started: '14:02' }, { id: 'sess-0421-b3', env: 'Portfolio Allocation', algo: 'DreamerV3', status: 'running', ep: 88, ret: 16.8, started: '13:48' }, { id: 'sess-0420-f1', env: 'Inventory Mgmt', algo: 'SAC', status: 'complete', ep: 200, ret: 1174, started: 'yesterday' }, { id: 'sess-0420-d9', env: 'Truck Routing', algo: 'OptRL-Rev3', status: 'complete', ep: 200, ret: 2064, started: 'yesterday' }, { id: 'sess-0419-c2', env: 'Warehouse Logistics-v2', algo: 'DDPG', status: 'diverged', ep: 47, ret: 112, started: '2d ago' }, { id: 'sess-0418-e7', env: 'Portfolio Allocation', algo: 'CQL', status: 'complete', ep: 200, ret: 14.3, started: '3d ago' }, ]; const statusColor = { running: 'var(--accent-2)', complete: 'var(--accent)', diverged: '#b64a3a', }; return (

My Sessions // 6 runs · 2 active

Your Playground rollouts, running and archived. Resume any session to inspect live metrics; completed runs can be submitted to RLX from here.
{sessions.map((s, i) => ( ))}
# 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' ? : }
); }; const Playground = ({ envId, setEnvId, algo, setAlgo, onPublish, pgView, onNav }) => { const D = window.RLX_DATA; const [running, setRunning] = useState(true); const [tick, setTick] = useState(0); const env = D.playgroundEnvs.find(e => e.id === envId) || D.playgroundEnvs[0]; const algoIdx = D.playgroundAlgos.indexOf(algo); useEffect(() => { if (!running) return; if (pgView && pgView !== 'pg-envs') return; const id = setInterval(() => setTick(t => t + 1), 120); return () => clearInterval(id); }, [running, pgView]); // Per-(algo × env) suitability: algorithms are NOT equally good on every env. // Scaled base reward per env, then a suitability multiplier keyed on the algo's class // vs. the env's data regime. Also applies a per-algo quality prior. const metrics = useMemo(() => { // Baseline reward scale per environment const envBase = { warehouse: 920, inventory: 1180, portfolio: 17.2, truck: 2080 }[env.id] || 800; // Algorithm → class (mirrors playgroundAlgoGroups) const algoClass = { // on-policy 'PPO': 'policy', 'TRPO': 'policy', 'GRPO': 'policy', // off-policy 'SAC': 'value', 'TD-MPC2': 'value', 'DreamerV3': 'value', 'Rainbow-DQN': 'value', // inverse RL 'GAIL': 'irl', 'AIRL': 'irl', // offline 'CQL': 'offline', 'IQL': 'offline', 'Decision Transformer': 'offline', // proprietary (hybrid model-based + MPC) 'OptRL-Warehouse-v3': 'hybrid-warehouse', 'OptRL-Truck-v1': 'hybrid-truck', }[algo] || 'value'; // Env profile (what the env rewards) // reg: data regime it lives in (online-sim / logged / imitation-friendly / continuous / discrete) // longHorizon: gains from model-based planning const envProfile = { warehouse: { kind: 'discrete-combinatorial', longHorizon: 0.8, imitable: 1.0 }, inventory: { kind: 'discrete-sequential', longHorizon: 0.3, imitable: 0.6 }, portfolio: { kind: 'offline-continuous', longHorizon: 0.7, imitable: 0.2 }, truck: { kind: 'continuous-planning', longHorizon: 1.0, imitable: 0.8 }, }[env.id]; // Suitability: how well this class matches this env (0.55 = bad fit, 1.0 = great fit) let fit = 0.85; if (algoClass === 'policy') { // on-policy: fine in online sim, weaker in long-horizon and bad in offline fit = env.id === 'portfolio' ? 0.60 : (envProfile.longHorizon > 0.8 ? 0.78 : 0.92); } else if (algoClass === 'value') { // off-policy: strong in continuous/long-horizon, variable on discrete if (algo === 'Rainbow-DQN') fit = envProfile.kind.includes('discrete') ? 1.00 : 0.58; else if (algo === 'DreamerV3' || algo === 'TD-MPC2') fit = 0.85 + 0.15 * envProfile.longHorizon; else /* SAC */ fit = envProfile.kind.includes('continuous') ? 0.98 : 0.80; if (env.id === 'portfolio') fit -= 0.15; // portfolio really wants offline } else if (algoClass === 'irl') { // IRL: only good if env has expert demos available fit = 0.55 + 0.40 * envProfile.imitable; } else if (algoClass === 'offline') { // offline RL: huge on portfolio (logged data), mediocre online fit = env.id === 'portfolio' ? 1.02 : 0.70; if (algo === 'Decision Transformer' && env.id === 'portfolio') fit = 1.05; } else if (algoClass === 'hybrid-warehouse') { fit = env.id === 'warehouse' ? 1.08 : 0.82; } else if (algoClass === 'hybrid-truck') { fit = env.id === 'truck' ? 1.10 : 0.80; } // Per-algo quality prior (same order as playgroundAlgos list; falls back to 0.9) const qualityPrior = [1.0, 0.98, 0.95, 0.92, 0.88, 0.86, 0.84, 0.80, 0.76, 0.74, 0.70, 0.68][algoIdx]; const algoMod = fit * (qualityPrior || 0.9); // Learning curve: rises with tick, asymptotes at base * algoMod const curScore = Math.round(envBase * algoMod * (1 - Math.exp(-tick / 30)) * 100) / 100; // Success-% ceiling also depends on fit (bad fit plateaus lower) const successCeiling = Math.min(97, 45 + fit * 52); const success = Math.min(successCeiling, 25 + tick * 0.9 * fit).toFixed(1); const episode = Math.floor(tick / 8) + 1; const step = (tick * 1247) % env.episodeLen; return { curScore, episode, step, success, fps: 842 + (tick % 10), fit, algoClass, }; }, [env.id, algoIdx, tick, algo]); if (pgView === 'pg-algos') { return { setAlgo(name); onNav && onNav('pg-envs'); }} />; } if (pgView === 'pg-sessions') { return onNav && onNav('pg-envs')} />; } if (pgView === 'pg-train') { return ; } const VizComponent = { warehouse: WarehouseViz, inventory: InventoryViz, portfolio: PortfolioViz, truck: TruckViz }[env.id]; return (

OptRL Playground // interactive product · runs feed back to RLX

A separate interactive product from the RLX leaderboard. Pick an environment, pick an algorithm, we spin up a sandboxed rollout on our compute and stream metrics back. Submit completed runs to publish on RLX.
{/* Left: env picker */}
Environments {D.playgroundEnvs.map(e => (
{ setEnvId(e.id); setTick(0); }}>
{e.name}
{e.description}
obs={e.obsDim} act={e.actDim} ep={e.episodeLen}
))}
{/* Middle: stage */}
env {env.name} ep {metrics.episode} · step {metrics.step} · {metrics.fps} fps
{VizComponent && }
{/* Right: controls + metrics */}
Algorithm
orchestrator auto-configures hyperparams for the selected (algo × env) pair.
{(() => { const g = (D.playgroundAlgoGroups || []).find(g => g.items.some(i => i.name === algo)); return g ? <> {g.group.split(' (')[0]} {g.hint} : null; })()}
Live Metrics
Return {metrics.curScore}
Success {metrics.success}%
Stability σ {(0.55 + metrics.fit * 0.30 + Math.min(tick, 100)*0.001).toFixed(2)}
Algo × Env Fit = 0.95 ? 'var(--ok)' : metrics.fit >= 0.80 ? 'var(--accent)' : 'var(--bad, #b64a3a)'}}> {Math.round(metrics.fit * 100)}%
Wall {Math.floor(tick * 0.1)}s
{metrics.fit >= 1.0 ? 'Near-optimal match, this algo class is purpose-built for this env.' : metrics.fit >= 0.9 ? 'Strong fit, expect convergence well above baseline.' : metrics.fit >= 0.75 ? 'Workable fit, will learn, but won\'t top the leaderboard here.' : 'Poor fit, algorithm class doesn\'t match this env\'s data regime. Try a different family.'}
Reward Curve
Seed
); }; Object.assign(window, { Playground });