File size: 8,743 Bytes
e85a399
779968d
 
603b5b4
0244a95
60943f3
779968d
e048104
779968d
 
 
 
 
 
 
60943f3
779968d
e048104
 
 
e2e60c0
 
 
 
 
 
 
 
 
 
0244a95
60943f3
e048104
603b5b4
 
 
 
 
e85a399
14a60d6
0244a95
e048104
 
 
779968d
e048104
e85a399
603b5b4
 
 
 
 
 
e048104
 
779968d
 
 
e048104
 
 
779968d
 
9292daf
 
 
 
 
 
 
 
 
ab2e84f
603b5b4
 
 
 
 
 
 
 
 
 
 
e85a399
 
 
 
 
 
 
 
 
ab2e84f
b26246b
 
 
60943f3
779968d
3f34e78
b26246b
3adb9d3
d7d3667
 
 
 
 
e88bc7f
d7d3667
 
 
 
 
 
 
3adb9d3
3f34e78
0244a95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603b5b4
 
 
 
 
 
 
0244a95
 
 
e2e60c0
8071c8f
0244a95
603b5b4
 
8071c8f
 
 
603b5b4
 
 
 
 
 
 
b26246b
 
 
 
779968d
e85a399
 
 
 
 
 
 
 
 
 
 
14a60d6
 
b26246b
 
 
 
 
779968d
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
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<SettingsUi>(INITIAL_SETTINGS_UI);
  const [compareActive, setCompareActive] = useState(false);
  const [entries, setEntries] = useState<ComparisonEntry[]>([]);
  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<BivariatePlotView>("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 <LoadingScreen message="Loading" />;
  //}

  return (
		<div className="grid grid-cols-[1fr_12fr_1fr] lg:h-dvh bg-stone-50">
			<div/>
			<div className="flex flex-col gap-6 p-6 min-h-0">
				<header className="grid grid-cols-1 items-center gap-3 lg:grid-cols-[2fr_1fr] lg:gap-12">
					<div className="title">Optimization Trajectory Visualizer</div>
					{/* <div className="desc"></div> */}
					<div
						id="runtime-status"
						className={`status translate-y-4 w-fit justify-self-end ${api.isLoading ? "loading" : "ready"}`}
						role="status"
						aria-live="polite"
					>
						<span></span>
						{api.isLoading ? "Loading Python runtime…" : "Python runtime ready"}
					</div>
				</header>

				<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-12 flex-1 min-h-0 overflow-auto">
					<div className="flex h-full min-h-0 min-w-0 flex-col gap-2">
            {settingsUi.mode === "Bivariate" && (
              <div className="flex w-full border-b border-gray-200" role="tablist" aria-label="Bivariate plot view">
                {(["Contour heatmap", "Function surface"] as const).map((view) => (
                  <button
                    key={view}
                    type="button"
                    role="tab"
                    aria-selected={bivariatePlotView === view}
                    onClick={() => setBivariatePlotView(view)}
                    className={`w-1/2 cursor-pointer rounded px-5 py-1 ${bivariatePlotView === view
                      ? "border-b-2 border-orange-400 text-orange-400"
                      : "text-gray-950 hover:bg-gray-200"}`}
                  >
                    {view}
                  </button>
                ))}
              </div>
            )}
            <OptimizationPlot 
              data={compareActive ? { functionValues: api.plotData.functionValues } : api.plotData}
              comparisons={compareActive ? entries.map(entry => ({ ...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}
            />
					</div>
					<Sidebar
            onTabChange={(tab) => { setCompareActive(tab === "Compare"); setIsAnimating(false); setComparisonAnimating(false); }}
            comparison={<ComparePanel settings={settingsUi} entries={entries} trajectories={api.comparison}
              iteration={comparisonIteration} busy={comparisonBusy} ready={!api.isLoading} animating={comparisonAnimating}
              onAdd={(configured) => {
                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}
					/>
				</div>
    </div>
		<div/>
	</div>
  );
}