import { useEffect, useState, useRef } from "react"; import OptimizationPlot from "./OptimizationPlot.tsx"; import Sidebar from "./Sidebar.tsx"; import ComparePanel from "./ComparePanel"; import type { BivariatePlotView, ComparisonEntry, SettingsUi } from "./types.ts"; import usePyodide from "./usePyodide.ts"; const INITIAL_SETTINGS_UI: SettingsUi = { mode: "Univariate", functionExpr: "x^2", algorithm: "Gradient Descent", x0: "0.5", y0: "0.5", learningRate: "0.1", momentum: "0.0", }; const INITIAL_XLIM: [number, number] = [-1, 1]; const INITIAL_YLIM: [number, number] = [-1, 1]; const COMPARISON_COLORS = [ "#0072B2", "#E69F00", "#009E73", "#D55E00", "#CC79A7", "#6F4EBA", "#A6761D", "#E7298A", "#1B9E77", "#7570B3", "#E6AB02", "#666666", ] as const; function comparisonColor(id: number): string { if (id <= COMPARISON_COLORS.length) return COMPARISON_COLORS[id - 1]; // Continue with non-repeating golden-angle hues for unusually large comparisons. return `hsl(${((id - 1) * 137.508) % 360}, 72%, 40%)`; } export default function App() { const [settingsUi, setSettingsUi] = useState(INITIAL_SETTINGS_UI); const [compareActive, setCompareActive] = useState(false); const [entries, setEntries] = useState([]); const [comparisonIteration, setComparisonIteration] = useState(0); const [comparisonAnimating, setComparisonAnimating] = useState(false); const nextId = useRef(1); const [isAnimating, setIsAnimating] = useState(false); const [showGradientField, setShowGradientField] = useState(false); const [bivariatePlotView, setBivariatePlotView] = useState("Contour heatmap"); const xlimRef = useRef<[number, number]>(INITIAL_XLIM); const ylimRef = useRef<[number, number]>(INITIAL_YLIM); const settings = { ...settingsUi, xlim: xlimRef.current, ylim: ylimRef.current }; function handleSettingsUiChange(newSettings: SettingsUi) { setIsAnimating(false); if (newSettings.mode !== settingsUi.mode || newSettings.functionExpr !== settingsUi.functionExpr || newSettings.x0 !== settingsUi.x0 || newSettings.y0 !== settingsUi.y0) { setComparisonAnimating(false); setComparisonIteration(0); setEntries(previous => previous.map(entry => ({ ...entry, settings: { ...entry.settings, mode: newSettings.mode, functionExpr: newSettings.functionExpr } }))); } setSettingsUi(newSettings); api.sendInit({ ...newSettings, xlim: xlimRef.current, ylim: ylimRef.current }); } function setAxisLimits(xlim: [number, number], ylim: [number, number]) { xlimRef.current = xlim; ylimRef.current = ylim; api.sendInit({ ...settingsUi, xlim, ylim }); } function handleRandomInitialPoint() { const xRange = xlimRef.current[1] - xlimRef.current[0]; const yRange = ylimRef.current[1] - ylimRef.current[0]; const newX0 = (xlimRef.current[0] + Math.random() * xRange * 0.95).toFixed(2); const newY0 = (ylimRef.current[0] + Math.random() * yRange * 0.95).toFixed(2); const newSettings = { ...settingsUi, x0: newX0, y0: newY0 }; handleSettingsUiChange(newSettings); } const api = usePyodide(settings); const { compare, comparisonBusy, isLoading } = api; useEffect(() => { if (!isLoading) compare(entries.map(entry => ({ ...entry.settings, xlim: xlimRef.current, ylim: ylimRef.current })), entries.map(entry => entry.id), comparisonIteration); }, [entries, comparisonIteration, compare, isLoading]); useEffect(() => { if (!comparisonAnimating || comparisonBusy || isLoading) return; const timeout = window.setTimeout(() => setComparisonIteration(value => value + 1), 200); return () => window.clearTimeout(timeout); }, [comparisonAnimating, comparisonBusy, isLoading, comparisonIteration]); const sendNextStep = api.sendNextStep; const currentIteration = api.currentIteration; const totalIterations = Math.max((api.plotData.trajectoryValues?.x.length ?? 1) - 1, 0); useEffect(() => { if (!isAnimating) return; const intervalId = window.setInterval(() => sendNextStep(), 200); return () => window.clearInterval(intervalId); }, [isAnimating, sendNextStep]); //if (api.isLoading) { //return ; //} return (
Optimization Trajectory Visualizer
{/*
*/}
{api.isLoading ? "Loading Python runtime…" : "Python runtime ready"}
{settingsUi.mode === "Bivariate" && (
{(["Contour heatmap", "Function surface"] as const).map((view) => ( ))}
)} ({ ...entry, values: api.comparison.find(item => item.id === entry.id)?.values })) : undefined} isLoading={api.isLoading} xlim={xlimRef.current} ylim={ylimRef.current} setAxisLimits={setAxisLimits} currentIteration={currentIteration} showGradientField={showGradientField} bivariatePlotView={bivariatePlotView} />
{ setCompareActive(tab === "Compare"); setIsAnimating(false); setComparisonAnimating(false); }} comparison={ { const id = nextId.current++; setComparisonIteration(0); setEntries(previous => [...previous, { id, settings: configured, color: comparisonColor(id), visible: true, }]); }} onRemove={(id) => { setComparisonAnimating(false); setEntries(previous => previous.filter(entry => entry.id !== id)); }} onToggleVisibility={(id) => setEntries(previous => previous.map(entry => ( entry.id === id ? { ...entry, visible: !entry.visible } : entry )))} onAction={(action) => { if (action === "Animate") setComparisonAnimating(true); else if (action === "Pause") setComparisonAnimating(false); else if (action === "Reset") setComparisonIteration(0); else setComparisonIteration(value => Math.max(0, value + (action === "Next" ? 1 : -1))); }} />} settings={settingsUi} setSettings={handleSettingsUiChange} onRandomInitialPoint={handleRandomInitialPoint} trajectoryValues={api.plotData.trajectoryValues} onReset={() => { setIsAnimating(false); api.sendReset(); }} onNextStep={api.sendNextStep} onPrevStep={api.sendPrevStep} onAnimate={() => setIsAnimating(true)} onPause={() => setIsAnimating(false)} isAnimating={isAnimating} currentIteration={currentIteration} totalIterations={totalIterations} showGradientField={showGradientField} onShowGradientFieldChange={setShowGradientField} />
); }