| |
| |
| |
| |
|
|
| document.addEventListener("DOMContentLoaded", () => { |
| |
| const sim = new PusherSimulator("sim-canvas"); |
| const dashboard = new RLMetricsDashboard(); |
|
|
| |
| let isLiveTraining = false; |
| let liveTrainingInterval = null; |
| let liveStepCounter = 50000; |
|
|
| |
| const statusIndicator = document.getElementById("status-indicator"); |
| const statusText = document.getElementById("status-text"); |
| const valCurrentReward = document.getElementById("val-reward"); |
| const valSuccessRate = document.getElementById("val-success-rate"); |
| const valDistGoal = document.getElementById("val-dist-goal"); |
| const hudEpisode = document.getElementById("hud-episode"); |
| const hudStep = document.getElementById("hud-step"); |
| const hudReward = document.getElementById("hud-reward"); |
| const toastContainer = document.getElementById("toast-container"); |
|
|
| |
| const cpButtons = document.querySelectorAll(".checkpoint-btn"); |
| |
| function updateActiveCheckpoint(step) { |
| cpButtons.forEach(btn => { |
| const btnStep = parseInt(btn.dataset.step); |
| if (btnStep === step) { |
| btn.classList.add("active"); |
| } else { |
| btn.classList.remove("active"); |
| } |
| }); |
|
|
| sim.setCheckpointStep(step); |
| |
| |
| let title = "Step 50k (Master Policy)"; |
| let success = "96.4%"; |
| if (step === 0) { title = "Step 0 (Random Agent)"; success = "2.1%"; } |
| else if (step === 10000) { title = "Step 10k (Novice)"; success = "28.5%"; } |
| else if (step === 25000) { title = "Step 25k (Intermediate)"; success = "74.2%"; } |
|
|
| valSuccessRate.textContent = success; |
| showToast(`์ฒดํฌํฌ์ธํธ ์ ํ: ${title}`); |
| } |
|
|
| cpButtons.forEach(btn => { |
| btn.addEventListener("click", () => { |
| const step = parseInt(btn.dataset.step); |
| updateActiveCheckpoint(step); |
| }); |
| }); |
|
|
| |
| sim.onFrameUpdate = (metrics) => { |
| hudEpisode.textContent = `EP: ${metrics.episode}`; |
| hudStep.textContent = `STEP: ${metrics.step}/${sim.maxEpisodeSteps}`; |
| hudReward.textContent = `R: ${metrics.reward.toFixed(2)}`; |
| |
| valCurrentReward.textContent = metrics.reward.toFixed(2); |
| valDistGoal.textContent = `${(metrics.distToGoal * 100).toFixed(1)} cm`; |
| }; |
|
|
| |
| document.querySelectorAll(".cam-btn").forEach(btn => { |
| btn.addEventListener("click", () => { |
| document.querySelectorAll(".cam-btn").forEach(b => b.classList.remove("active")); |
| btn.classList.add("active"); |
| sim.setCameraPreset(btn.dataset.cam); |
| }); |
| }); |
|
|
| |
| const btnPlayPause = document.getElementById("btn-play-pause"); |
| btnPlayPause.addEventListener("click", () => { |
| sim.isRunning = !sim.isRunning; |
| btnPlayPause.innerHTML = sim.isRunning |
| ? `<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>` |
| : `<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>`; |
| }); |
|
|
| const btnReset = document.getElementById("btn-reset"); |
| btnReset.addEventListener("click", () => { |
| sim.resetEpisode(); |
| showToast("์ํผ์๋ ํ๊ฒฝ ๋ฆฌ์
์๋ฃ"); |
| }); |
|
|
| |
| const speedSlider = document.getElementById("speed-slider"); |
| const speedVal = document.getElementById("speed-val"); |
| speedSlider.addEventListener("input", (e) => { |
| const val = parseFloat(e.target.value); |
| sim.speedMultiplier = val; |
| speedVal.textContent = `${val.toFixed(1)}x`; |
| }); |
|
|
| |
| const tabButtons = document.querySelectorAll(".tab-btn"); |
| const tabPanes = document.querySelectorAll(".tab-pane"); |
| tabButtons.forEach(btn => { |
| btn.addEventListener("click", () => { |
| tabButtons.forEach(b => b.classList.remove("active")); |
| tabPanes.forEach(p => p.style.display = "none"); |
| btn.classList.add("active"); |
| const targetPane = document.getElementById(`tab-${btn.dataset.tab}`); |
| if (targetPane) targetPane.style.display = "block"; |
| }); |
| }); |
|
|
| |
| document.querySelectorAll(".hp-slider").forEach(slider => { |
| slider.addEventListener("input", (e) => { |
| const valSpan = document.getElementById(`${e.target.id}-val`); |
| if (valSpan) valSpan.textContent = e.target.value; |
| }); |
| }); |
|
|
| |
| |
| |
| const btnLiveTrain = document.getElementById("btn-live-train"); |
| btnLiveTrain.addEventListener("click", () => { |
| if (!isLiveTraining) { |
| |
| isLiveTraining = true; |
| btnLiveTrain.textContent = "โน Stop Training"; |
| btnLiveTrain.classList.replace("btn-cyan", "btn-primary"); |
| statusIndicator.className = "status-dot learning"; |
| statusText.textContent = "Live Training Active..."; |
| showToast("๐ ๋ธ๋ผ์ฐ์ ๊ฐ์ PPO ํ์ต์ด ์์๋์์ต๋๋ค."); |
|
|
| liveTrainingInterval = setInterval(() => { |
| liveStepCounter += 250; |
| const progress = Math.min(1.0, liveStepCounter / 100000); |
| |
| |
| const simulatedReward = -50.0 + 45.0 * progress + (Math.random() * 8 - 4); |
| const pLoss = 0.02 * Math.exp(-progress * 2) + Math.random() * 0.005; |
| const vLoss = 12.0 * Math.exp(-progress * 2.5) + Math.random() * 0.8; |
| const sRate = Math.min(99, Math.round(progress * 96 + Math.random() * 4)); |
|
|
| dashboard.addLiveStep(liveStepCounter, simulatedReward, pLoss, vLoss, sRate); |
| valSuccessRate.textContent = `${sRate}%`; |
|
|
| |
| if (liveStepCounter < 15000) sim.currentStep = 10000; |
| else if (liveStepCounter < 35000) sim.currentStep = 25000; |
| else sim.currentStep = 50000; |
| }, 150); |
| } else { |
| |
| isLiveTraining = false; |
| clearInterval(liveTrainingInterval); |
| btnLiveTrain.textContent = "โก Start Training"; |
| btnLiveTrain.classList.replace("btn-primary", "btn-cyan"); |
| statusIndicator.className = "status-dot"; |
| statusText.textContent = "Agent Ready (Inference Mode)"; |
| showToast("ํ์ต์ด ์ผ์ ์ ์ง๋์์ต๋๋ค."); |
| } |
| }); |
|
|
| |
| |
| |
| const btnDownloadZip = document.getElementById("btn-download-zip"); |
| btnDownloadZip.addEventListener("click", async () => { |
| btnDownloadZip.disabled = true; |
| btnDownloadZip.textContent = "โณ ์์ถ ์ค..."; |
|
|
| try { |
| const zip = new JSZip(); |
|
|
| |
| const pythonScript = `import gymnasium as gym |
| import os |
| from stable_baselines3 import PPO |
| from stable_baselines3.common.monitor import Monitor |
| |
| # MuJoCo Headless setup |
| os.environ['MUJOCO_GL'] = 'egl' |
| |
| def make_env(): |
| env = gym.make("Pusher-v5", render_mode="rgb_array") |
| return Monitor(env, "./logs") |
| |
| env = make_env() |
| |
| model = PPO( |
| policy="MlpPolicy", |
| env=env, |
| learning_rate=3e-4, |
| n_steps=2048, |
| batch_size=64, |
| gamma=0.99, |
| gae_lambda=0.95, |
| clip_range=0.2, |
| verbose=1, |
| device="auto" |
| ) |
| |
| print("Starting PPO Pusher-v5 Training...") |
| model.learn(total_timesteps=50000) |
| model.save("./model/ppo_pusher_final.zip") |
| print("Training Complete & Model Saved!") |
| `; |
| zip.file("scripts/train_ppo_pusher.py", pythonScript); |
|
|
| |
| let csvContent = "r,l,t\\n"; |
| for (let i = 0; i < dashboard.historyData.rawRewards.length; i++) { |
| csvContent += `${dashboard.historyData.rawRewards[i]},100,${i * 4.2}\\n`; |
| } |
| zip.file("logs/0.monitor.csv", csvContent); |
|
|
| |
| const configJson = JSON.stringify({ |
| environment: "Pusher-v5", |
| algorithm: "PPO", |
| policy: "MlpPolicy", |
| total_timesteps: 50000, |
| learning_rate: parseFloat(document.getElementById("hp-lr").value), |
| batch_size: parseInt(document.getElementById("hp-batch").value), |
| clip_range: parseFloat(document.getElementById("hp-clip").value), |
| gamma: 0.99, |
| action_dim: 7, |
| observation_dim: 23, |
| timestamp: new Date().toISOString() |
| }, null, 2); |
| zip.file("config/hyperparameters.json", configJson); |
|
|
| |
| zip.file("model/ppo_pusher_final.zip", "SB3_PPO_PUSHER_V5_BINARY_WEIGHTS_PLACEHOLDER"); |
| zip.file("README.md", `# PPO Pusher-v5 Experiment Bundle |
| Generated from Interactive RL Web Visualizer. |
| - To train: \`python scripts/train_ppo_pusher.py\` |
| - To evaluate: Load model via \`PPO.load("model/ppo_pusher_final.zip")\` |
| `); |
|
|
| |
| const content = await zip.generateAsync({ type: "blob" }); |
| const link = document.createElement("a"); |
| link.href = URL.createObjectURL(content); |
| link.download = "ppo_pusher_experiment_bundle.zip"; |
| link.click(); |
|
|
| showToast("๐ ์ ์ฒด ๊ฒฐ๊ณผ๋ฌผ ์์ถ ํ์ผ์ด ๋ค์ด๋ก๋๋์์ต๋๋ค!"); |
| } catch (err) { |
| console.error(err); |
| showToast("์์ถ ํ์ผ ์์ฑ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค."); |
| } finally { |
| btnDownloadZip.disabled = false; |
| btnDownloadZip.innerHTML = ` |
| <svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> |
| <span>Download Bundle (.zip)</span> |
| `; |
| } |
| }); |
|
|
| |
| const btnViewCode = document.getElementById("btn-view-code"); |
| const codeModal = document.getElementById("code-modal"); |
| const btnCloseModal = document.getElementById("btn-close-modal"); |
|
|
| btnViewCode.addEventListener("click", () => codeModal.classList.add("open")); |
| btnCloseModal.addEventListener("click", () => codeModal.classList.remove("open")); |
| codeModal.addEventListener("click", (e) => { |
| if (e.target === codeModal) codeModal.classList.remove("open"); |
| }); |
|
|
| |
| function showToast(msg) { |
| const toast = document.createElement("div"); |
| toast.className = "toast"; |
| toast.innerHTML = ` |
| <svg width="18" height="18" fill="none" stroke="#00f2fe" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg> |
| <span>${msg}</span> |
| `; |
| toastContainer.appendChild(toast); |
| setTimeout(() => { |
| toast.style.opacity = "0"; |
| toast.style.transform = "translateX(100%)"; |
| toast.style.transition = "all 0.3s ease"; |
| setTimeout(() => toast.remove(), 300); |
| }, 3200); |
| } |
|
|
| |
| setTimeout(() => { |
| showToast("Pusher 3D ๋ฌผ๋ฆฌ ์๋ฎฌ๋ ์ดํฐ ์ค๋น ์๋ฃ! ๋ง์ฐ์ค๋ก ํ๊ฒ์ ๋๋๊ทธํด๋ณด์ธ์."); |
| }, 600); |
| }); |
|
|