Spaces:
Running
Running
support algorithm comparison
Browse files- dist/assets/index-Bs9s2ktO.css +0 -1
- dist/assets/index-D4A1ut36.css +1 -0
- dist/assets/{index-DxXDp_Rq.js → index-nsC-BkSr.js} +0 -0
- dist/assets/{pyodide.worker-CMc9uFEp.js → pyodide.worker-Cq5E6qzM.js} +3 -206
- dist/index.html +2 -2
- frontends/react/src/AlgorithmControls.tsx +167 -0
- frontends/react/src/App.tsx +43 -3
- frontends/react/src/ComparePanel.tsx +48 -0
- frontends/react/src/OptimizationPlot.tsx +21 -4
- frontends/react/src/Sidebar.tsx +14 -156
- frontends/react/src/pyodide.worker.ts +23 -0
- frontends/react/src/types.ts +5 -0
- frontends/react/src/usage.md +7 -0
- frontends/react/src/usePyodide.ts +15 -2
dist/assets/index-Bs9s2ktO.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-blue-600:oklch(54.6% .245 262.881);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-800:oklch(27.9% .041 260.031);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-950:oklch(13% .028 261.692);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-700:oklch(37.4% .01 67.558);--color-white:#fff;--spacing:.25rem;--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--animate-spin:spin 1s linear infinite;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.z-50{z-index:50}.m-4{margin:calc(var(--spacing)*4)}.-mt-2{margin-top:calc(var(--spacing)*-2)}.-mt-5{margin-top:calc(var(--spacing)*-5)}.-mt-6{margin-top:calc(var(--spacing)*-6)}.mt-0{margin-top:calc(var(--spacing)*0)}.-mb-2{margin-bottom:calc(var(--spacing)*-2)}.mb-0{margin-bottom:calc(var(--spacing)*0)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-16{height:calc(var(--spacing)*16)}.h-64{height:calc(var(--spacing)*64)}.h-full{height:100%}.h-px{height:1px}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-64{min-height:calc(var(--spacing)*64)}.min-h-\[320px\]{min-height:320px}.min-h-\[420px\]{min-height:420px}.w-16{width:calc(var(--spacing)*16)}.w-fit{width:fit-content}.w-full{width:100%}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.flex-none{flex:none}.translate-y-4{--tw-translate-y:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-\[1fr_12fr_1fr\]{grid-template-columns:1fr 12fr 1fr}.grid-cols-\[6rem_minmax\(0\,1fr\)\]{grid-template-columns:6rem minmax(0,1fr)}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-12{gap:calc(var(--spacing)*12)}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.justify-self-end{justify-self:flex-end}.overflow-auto{overflow:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-orange-400{border-color:var(--color-orange-400)}.border-slate-200{border-color:var(--color-slate-200)}.border-stone-200{border-color:var(--color-stone-200)}.border-stone-300{border-color:var(--color-stone-300)}.border-t-blue-600{border-top-color:var(--color-blue-600)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-orange-200{background-color:var(--color-orange-200)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-300{background-color:var(--color-slate-300)}.bg-stone-50{background-color:var(--color-stone-50)}.bg-stone-100{background-color:var(--color-stone-100)}.bg-white{background-color:var(--color-white)}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-5{padding-inline:calc(var(--spacing)*5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.pr-0\.25{padding-right:calc(var(--spacing)*.25)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.whitespace-nowrap{white-space:nowrap}.text-gray-700{color:var(--color-gray-700)}.text-gray-950{color:var(--color-gray-950)}.text-orange-400{color:var(--color-orange-400)}.text-slate-600{color:var(--color-slate-600)}.text-slate-800{color:var(--color-slate-800)}.text-stone-500{color:var(--color-stone-500)}.text-stone-700{color:var(--color-stone-700)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.last\:mb-0:last-child{margin-bottom:calc(var(--spacing)*0)}@media(hover:hover){.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-orange-300:hover{background-color:var(--color-orange-300)}.hover\:bg-stone-200:hover{background-color:var(--color-stone-200)}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-stone-400:focus{--tw-ring-color:var(--color-stone-400)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:64rem){.lg\:flex{display:flex}.lg\:h-dvh{height:100dvh}.lg\:grid-cols-\[2fr_1fr\]{grid-template-columns:2fr 1fr}.lg\:gap-12{gap:calc(var(--spacing)*12)}}.\[\&_p\]\:mb-0 p{margin-bottom:calc(var(--spacing)*0)}}.title{font-size:32px;font-weight:700}.status{color:var(--ink-soft);border:1px solid var(--line);background:#ffffffbf;border-radius:999px;flex:none;align-items:center;gap:8px;padding:8px 12px;font-size:12px;display:flex}.status span{background:#d69b25;border-radius:50%;width:8px;height:8px;box-shadow:0 0 0 4px #f8ebca}.status.ready span{background:#2c9b68;box-shadow:0 0 0 4px #d9f0e5}.status.error span{background:#c74848;box-shadow:0 0 0 4px #f5dddd}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
|
|
|
|
|
|
dist/assets/index-D4A1ut36.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--color-red-700:oklch(50.5% .213 27.518);--color-orange-200:oklch(90.1% .076 70.697);--color-orange-300:oklch(83.7% .128 66.29);--color-orange-400:oklch(75% .183 55.934);--color-blue-600:oklch(54.6% .245 262.881);--color-slate-50:oklch(98.4% .003 247.858);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-600:oklch(44.6% .043 257.281);--color-slate-800:oklch(27.9% .041 260.031);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-950:oklch(13% .028 261.692);--color-stone-50:oklch(98.5% .001 106.423);--color-stone-100:oklch(97% .001 106.424);--color-stone-200:oklch(92.3% .003 48.717);--color-stone-300:oklch(86.9% .005 56.366);--color-stone-400:oklch(70.9% .01 56.259);--color-stone-500:oklch(55.3% .013 58.071);--color-stone-600:oklch(44.4% .011 73.639);--color-stone-700:oklch(37.4% .01 67.558);--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1/.75);--text-sm:.875rem;--text-sm--line-height:calc(1.25/.875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75/1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75/1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2/1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--animate-spin:spin 1s linear infinite;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.fixed{position:fixed}.relative{position:relative}.inset-0{inset:calc(var(--spacing)*0)}.z-50{z-index:50}.m-4{margin:calc(var(--spacing)*4)}.-mt-2{margin-top:calc(var(--spacing)*-2)}.-mt-5{margin-top:calc(var(--spacing)*-5)}.-mt-6{margin-top:calc(var(--spacing)*-6)}.mt-0{margin-top:calc(var(--spacing)*0)}.-mb-2{margin-bottom:calc(var(--spacing)*-2)}.mb-0{margin-bottom:calc(var(--spacing)*0)}.mb-3{margin-bottom:calc(var(--spacing)*3)}.mb-4{margin-bottom:calc(var(--spacing)*4)}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-3{height:calc(var(--spacing)*3)}.h-8{height:calc(var(--spacing)*8)}.h-10{height:calc(var(--spacing)*10)}.h-16{height:calc(var(--spacing)*16)}.h-64{height:calc(var(--spacing)*64)}.h-full{height:100%}.h-px{height:1px}.min-h-0{min-height:calc(var(--spacing)*0)}.min-h-64{min-height:calc(var(--spacing)*64)}.min-h-\[320px\]{min-height:320px}.min-h-\[420px\]{min-height:420px}.w-3{width:calc(var(--spacing)*3)}.w-16{width:calc(var(--spacing)*16)}.w-fit{width:fit-content}.w-full{width:100%}.min-w-0{min-width:calc(var(--spacing)*0)}.flex-1{flex:1}.flex-none{flex:none}.shrink-0{flex-shrink:0}.translate-y-4{--tw-translate-y:calc(var(--spacing)*4);translate:var(--tw-translate-x)var(--tw-translate-y)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-\[1fr_12fr_1fr\]{grid-template-columns:1fr 12fr 1fr}.grid-cols-\[6rem_minmax\(0\,1fr\)\]{grid-template-columns:6rem minmax(0,1fr)}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing)*.5)}.gap-1{gap:calc(var(--spacing)*1)}.gap-1\.5{gap:calc(var(--spacing)*1.5)}.gap-2{gap:calc(var(--spacing)*2)}.gap-3{gap:calc(var(--spacing)*3)}.gap-4{gap:calc(var(--spacing)*4)}.gap-6{gap:calc(var(--spacing)*6)}.gap-12{gap:calc(var(--spacing)*12)}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing)*6)*var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing)*6)*calc(1 - var(--tw-space-y-reverse)))}.justify-self-end{justify-self:flex-end}.overflow-auto{overflow:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-4{border-style:var(--tw-border-style);border-width:4px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-orange-400{border-color:var(--color-orange-400)}.border-slate-200{border-color:var(--color-slate-200)}.border-stone-200{border-color:var(--color-stone-200)}.border-stone-300{border-color:var(--color-stone-300)}.border-t-blue-600{border-top-color:var(--color-blue-600)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-orange-200{background-color:var(--color-orange-200)}.bg-slate-50{background-color:var(--color-slate-50)}.bg-slate-300{background-color:var(--color-slate-300)}.bg-stone-50{background-color:var(--color-stone-50)}.bg-stone-100{background-color:var(--color-stone-100)}.bg-white{background-color:var(--color-white)}.p-2{padding:calc(var(--spacing)*2)}.p-4{padding:calc(var(--spacing)*4)}.p-6{padding:calc(var(--spacing)*6)}.px-2{padding-inline:calc(var(--spacing)*2)}.px-3{padding-inline:calc(var(--spacing)*3)}.px-5{padding-inline:calc(var(--spacing)*5)}.py-1{padding-block:calc(var(--spacing)*1)}.py-2{padding-block:calc(var(--spacing)*2)}.pr-0\.25{padding-right:calc(var(--spacing)*.25)}.pl-4{padding-left:calc(var(--spacing)*4)}.text-center{text-align:center}.text-right{text-align:right}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-6{--tw-leading:calc(var(--spacing)*6);line-height:calc(var(--spacing)*6)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.whitespace-nowrap{white-space:nowrap}.text-gray-700{color:var(--color-gray-700)}.text-gray-950{color:var(--color-gray-950)}.text-orange-400{color:var(--color-orange-400)}.text-red-700{color:var(--color-red-700)}.text-slate-600{color:var(--color-slate-600)}.text-slate-800{color:var(--color-slate-800)}.text-stone-500{color:var(--color-stone-500)}.text-stone-600{color:var(--color-stone-600)}.text-stone-700{color:var(--color-stone-700)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a),0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.last\:mb-0:last-child{margin-bottom:calc(var(--spacing)*0)}@media(hover:hover){.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-orange-300:hover{background-color:var(--color-orange-300)}.hover\:bg-stone-200:hover{background-color:var(--color-stone-200)}}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,)0 0 0 calc(2px + var(--tw-ring-offset-width))var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-stone-400:focus{--tw-ring-color:var(--color-stone-400)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:64rem){.lg\:flex{display:flex}.lg\:h-dvh{height:100dvh}.lg\:grid-cols-\[2fr_1fr\]{grid-template-columns:2fr 1fr}.lg\:gap-12{gap:calc(var(--spacing)*12)}}.\[\&_p\]\:mb-0 p{margin-bottom:calc(var(--spacing)*0)}}.title{font-size:32px;font-weight:700}.status{color:var(--ink-soft);border:1px solid var(--line);background:#ffffffbf;border-radius:999px;flex:none;align-items:center;gap:8px;padding:8px 12px;font-size:12px;display:flex}.status span{background:#d69b25;border-radius:50%;width:8px;height:8px;box-shadow:0 0 0 4px #f8ebca}.status.ready span{background:#2c9b68;box-shadow:0 0 0 4px #d9f0e5}.status.error span{background:#c74848;box-shadow:0 0 0 4px #f5dddd}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
|
dist/assets/{index-DxXDp_Rq.js → index-nsC-BkSr.js}
RENAMED
|
The diff for this file is too large to render.
See raw diff
|
|
|
dist/assets/{pyodide.worker-CMc9uFEp.js → pyodide.worker-Cq5E6qzM.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
(function(){"use strict";var
|
| 2 |
from sympy import (
|
| 3 |
lambdify,
|
| 4 |
symbols,
|
|
@@ -354,210 +354,7 @@ class OptimizationManager:
|
|
| 354 |
)
|
| 355 |
else:
|
| 356 |
raise ValueError("Unsupported algorithm for bivariate mode")
|
| 357 |
-
|
| 358 |
-
def handle_compute_trajectory(self, settings: dict) -> dict[str, dict]:
|
| 359 |
-
"""
|
| 360 |
-
Compute function values and a full trajectory for the given settings
|
| 361 |
-
without modifying the manager's current state.
|
| 362 |
-
"""
|
| 363 |
-
try:
|
| 364 |
-
mode = settings.get("mode", "").lower().strip()
|
| 365 |
-
function = settings.get("functionExpr", "").strip()
|
| 366 |
-
xlim = settings.get("xlim", [])
|
| 367 |
-
ylim = settings.get("ylim", [])
|
| 368 |
-
|
| 369 |
-
if not self._is_valid_function(function, mode, xlim, ylim):
|
| 370 |
-
return {
|
| 371 |
-
"trajectoryValues": {"x": [], "y": []},
|
| 372 |
-
"functionValues": {"x": [], "y": []},
|
| 373 |
-
}
|
| 374 |
-
|
| 375 |
-
# compute function values (local)
|
| 376 |
-
if mode == "univariate":
|
| 377 |
-
x = np.linspace(xlim[0], xlim[1], 101)
|
| 378 |
-
f = lambdify('x', self._parse_function(function), modules=['numpy'])
|
| 379 |
-
y = f(x)
|
| 380 |
-
if not isinstance(y, np.ndarray):
|
| 381 |
-
y = np.full_like(x, y)
|
| 382 |
-
function_values = {"x": x.tolist(), "y": y.tolist()}
|
| 383 |
-
|
| 384 |
-
elif mode == "bivariate":
|
| 385 |
-
x = np.linspace(xlim[0], xlim[1], 101)
|
| 386 |
-
y = np.linspace(ylim[0], ylim[1], 101)
|
| 387 |
-
X, Y = np.meshgrid(x, y)
|
| 388 |
-
expr = self._parse_function(function)
|
| 389 |
-
f = lambdify(('x', 'y'), expr, modules=['numpy'])
|
| 390 |
-
gradient_x = lambdify(('x', 'y'), expr.diff('x'), modules=['numpy'])
|
| 391 |
-
gradient_y = lambdify(('x', 'y'), expr.diff('y'), modules=['numpy'])
|
| 392 |
-
Z = f(X, Y)
|
| 393 |
-
gradient_x_values = gradient_x(X, Y)
|
| 394 |
-
gradient_y_values = gradient_y(X, Y)
|
| 395 |
-
if not isinstance(Z, np.ndarray):
|
| 396 |
-
Z = np.full_like(X, Z)
|
| 397 |
-
if not isinstance(gradient_x_values, np.ndarray):
|
| 398 |
-
gradient_x_values = np.full_like(X, gradient_x_values)
|
| 399 |
-
if not isinstance(gradient_y_values, np.ndarray):
|
| 400 |
-
gradient_y_values = np.full_like(Y, gradient_y_values)
|
| 401 |
-
function_values = {
|
| 402 |
-
"x": x.tolist(),
|
| 403 |
-
"y": y.tolist(),
|
| 404 |
-
"z": Z.tolist(),
|
| 405 |
-
"gradientX": gradient_x_values.tolist(),
|
| 406 |
-
"gradientY": gradient_y_values.tolist(),
|
| 407 |
-
}
|
| 408 |
-
else:
|
| 409 |
-
raise ValueError("Unsupported mode")
|
| 410 |
-
|
| 411 |
-
# compute trajectory values using existing helper logic
|
| 412 |
-
# choose a reasonably large number of steps for comparison
|
| 413 |
-
steps = 100
|
| 414 |
-
# reuse _compute_trajectory_values logic by calling the same functions
|
| 415 |
-
# but capture result into a local variable
|
| 416 |
-
mode_key = mode
|
| 417 |
-
algorithm = settings.get("algorithm", "").lower().strip().replace(" ", "_")
|
| 418 |
-
|
| 419 |
-
if mode_key == "univariate":
|
| 420 |
-
if algorithm == "gradient_descent":
|
| 421 |
-
traj = gd_univariate(
|
| 422 |
-
self._parse_function(function),
|
| 423 |
-
float(settings["x0"]),
|
| 424 |
-
float(settings.get("learningRate", 0.1)),
|
| 425 |
-
float(settings.get("momentum", 0.0)),
|
| 426 |
-
steps,
|
| 427 |
-
)
|
| 428 |
-
elif algorithm == "nesterov":
|
| 429 |
-
traj = nesterov_univariate(
|
| 430 |
-
self._parse_function(function),
|
| 431 |
-
float(settings["x0"]),
|
| 432 |
-
float(settings.get("learningRate", 0.1)),
|
| 433 |
-
float(settings.get("momentum", 0.0)),
|
| 434 |
-
steps,
|
| 435 |
-
)
|
| 436 |
-
elif algorithm == "adam":
|
| 437 |
-
traj = adam_univariate(
|
| 438 |
-
self._parse_function(function),
|
| 439 |
-
float(settings["x0"]),
|
| 440 |
-
float(settings.get("learningRate", 0.1)),
|
| 441 |
-
float(settings.get("beta1", 0.9)),
|
| 442 |
-
float(settings.get("beta2", 0.999)),
|
| 443 |
-
float(settings.get("epsilon", 1e-8)),
|
| 444 |
-
steps,
|
| 445 |
-
)
|
| 446 |
-
elif algorithm == "adagrad":
|
| 447 |
-
traj = adagrad_univariate(
|
| 448 |
-
self._parse_function(function),
|
| 449 |
-
float(settings["x0"]),
|
| 450 |
-
float(settings.get("learningRate", 0.1)),
|
| 451 |
-
float(settings.get("epsilon", 1e-8)),
|
| 452 |
-
steps,
|
| 453 |
-
)
|
| 454 |
-
elif algorithm == "rmsprop":
|
| 455 |
-
traj = rmsprop_univariate(
|
| 456 |
-
self._parse_function(function),
|
| 457 |
-
float(settings["x0"]),
|
| 458 |
-
float(settings.get("learningRate", 0.1)),
|
| 459 |
-
float(settings.get("beta", 0.9)),
|
| 460 |
-
float(settings.get("epsilon", 1e-8)),
|
| 461 |
-
steps,
|
| 462 |
-
)
|
| 463 |
-
elif algorithm == "adadelta":
|
| 464 |
-
traj = adadelta_univariate(
|
| 465 |
-
self._parse_function(function),
|
| 466 |
-
float(settings["x0"]),
|
| 467 |
-
float(settings.get("beta", 0.9)),
|
| 468 |
-
float(settings.get("epsilon", 1e-3)),
|
| 469 |
-
steps,
|
| 470 |
-
)
|
| 471 |
-
elif algorithm == "newton":
|
| 472 |
-
traj = newton_univariate(
|
| 473 |
-
self._parse_function(function),
|
| 474 |
-
float(settings["x0"]),
|
| 475 |
-
steps,
|
| 476 |
-
)
|
| 477 |
-
else:
|
| 478 |
-
raise ValueError("Unsupported algorithm for univariate mode")
|
| 479 |
-
|
| 480 |
-
elif mode_key == "bivariate":
|
| 481 |
-
if algorithm == "gradient_descent":
|
| 482 |
-
traj = gd_bivariate(
|
| 483 |
-
self._parse_function(function),
|
| 484 |
-
float(settings["x0"]),
|
| 485 |
-
float(settings["y0"]),
|
| 486 |
-
float(settings.get("learningRate", 0.1)),
|
| 487 |
-
float(settings.get("momentum", 0.0)),
|
| 488 |
-
steps,
|
| 489 |
-
)
|
| 490 |
-
elif algorithm == "nesterov":
|
| 491 |
-
traj = nesterov_bivariate(
|
| 492 |
-
self._parse_function(function),
|
| 493 |
-
float(settings["x0"]),
|
| 494 |
-
float(settings["y0"]),
|
| 495 |
-
float(settings.get("learningRate", 0.1)),
|
| 496 |
-
float(settings.get("momentum", 0.0)),
|
| 497 |
-
steps,
|
| 498 |
-
)
|
| 499 |
-
elif algorithm == "adam":
|
| 500 |
-
traj = adam_bivariate(
|
| 501 |
-
self._parse_function(function),
|
| 502 |
-
float(settings["x0"]),
|
| 503 |
-
float(settings["y0"]),
|
| 504 |
-
float(settings.get("learningRate", 0.1)),
|
| 505 |
-
float(settings.get("beta1", 0.9)),
|
| 506 |
-
float(settings.get("beta2", 0.999)),
|
| 507 |
-
float(settings.get("epsilon", 1e-8)),
|
| 508 |
-
steps,
|
| 509 |
-
)
|
| 510 |
-
elif algorithm == "adagrad":
|
| 511 |
-
traj = adagrad_bivariate(
|
| 512 |
-
self._parse_function(function),
|
| 513 |
-
float(settings["x0"]),
|
| 514 |
-
float(settings["y0"]),
|
| 515 |
-
float(settings.get("learningRate", 0.1)),
|
| 516 |
-
float(settings.get("epsilon", 1e-8)),
|
| 517 |
-
steps,
|
| 518 |
-
)
|
| 519 |
-
elif algorithm == "rmsprop":
|
| 520 |
-
traj = rmsprop_bivariate(
|
| 521 |
-
self._parse_function(function),
|
| 522 |
-
float(settings["x0"]),
|
| 523 |
-
float(settings["y0"]),
|
| 524 |
-
float(settings.get("learningRate", 0.1)),
|
| 525 |
-
float(settings.get("beta", 0.9)),
|
| 526 |
-
float(settings.get("epsilon", 1e-8)),
|
| 527 |
-
steps,
|
| 528 |
-
)
|
| 529 |
-
elif algorithm == "adadelta":
|
| 530 |
-
traj = adadelta_bivariate(
|
| 531 |
-
self._parse_function(function),
|
| 532 |
-
float(settings["x0"]),
|
| 533 |
-
float(settings["y0"]),
|
| 534 |
-
float(settings.get("beta", 0.9)),
|
| 535 |
-
float(settings.get("epsilon", 1e-3)),
|
| 536 |
-
steps,
|
| 537 |
-
)
|
| 538 |
-
elif algorithm == "newton":
|
| 539 |
-
traj = newton_bivariate(
|
| 540 |
-
self._parse_function(function),
|
| 541 |
-
float(settings["x0"]),
|
| 542 |
-
float(settings["y0"]),
|
| 543 |
-
steps,
|
| 544 |
-
)
|
| 545 |
-
else:
|
| 546 |
-
raise ValueError("Unsupported algorithm for bivariate mode")
|
| 547 |
-
|
| 548 |
-
else:
|
| 549 |
-
raise ValueError("Unsupported mode")
|
| 550 |
-
|
| 551 |
-
return {
|
| 552 |
-
"functionValues": function_values,
|
| 553 |
-
"trajectoryValues": traj,
|
| 554 |
-
}
|
| 555 |
-
except Exception as e:
|
| 556 |
-
return {
|
| 557 |
-
"trajectoryValues": {"x": [], "y": []},
|
| 558 |
-
"functionValues": {"x": [], "y": []},
|
| 559 |
-
}
|
| 560 |
-
`,l=`import numpy as np
|
| 561 |
from sympy import lambdify, Expr
|
| 562 |
|
| 563 |
|
|
@@ -1220,4 +1017,4 @@ def adam_bivariate(
|
|
| 1220 |
"gradient": gradient_values,
|
| 1221 |
"hessian": hessian_values,
|
| 1222 |
}
|
| 1223 |
-
`;const
|
|
|
|
| 1 |
+
(function(){"use strict";var y=`import numpy as np
|
| 2 |
from sympy import (
|
| 3 |
lambdify,
|
| 4 |
symbols,
|
|
|
|
| 354 |
)
|
| 355 |
else:
|
| 356 |
raise ValueError("Unsupported algorithm for bivariate mode")
|
| 357 |
+
`,u=`import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
from sympy import lambdify, Expr
|
| 359 |
|
| 360 |
|
|
|
|
| 1017 |
"gradient": gradient_values,
|
| 1018 |
"hessian": hessian_values,
|
| 1019 |
}
|
| 1020 |
+
`;const x="https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.mjs";let e=null,t=null;async function d(){const{loadPyodide:a}=await import(x);e=await a({indexURL:"https://cdn.jsdelivr.net/pyodide/v0.26.1/full/"}),await e.loadPackage(["numpy","sympy"]),e.FS.writeFile("optimization_logic.py",u),e.FS.writeFile("optimization_manager.py",y),e.runPython("from optimization_manager import OptimizationManager; manager = OptimizationManager();"),t=e.globals.get("manager"),t||console.error("Failed to initialize optimization manager"),self.postMessage({type:"READY"})}function s(a){if(!a)return null;try{const n=a.toJs({dict_converter:Object.fromEntries});a.destroy&&a.destroy(),self.postMessage({type:"RESULT",data:n})}catch(n){console.error("Error handling Python result:",n)}}self.onmessage=async a=>{const n=a.data;if(!t){console.warn("Pyodide is not ready yet");return}switch(n.type){case"COMPARE":{const p=n.settings.map((m,r)=>{const o=e.globals.get("OptimizationManager"),i=o();o.destroy();const f=e.toPy(m);try{i.handle_update_settings(f)?.destroy(),i._compute_trajectory_values(f,n.iteration+1);const l=i.trajectory_values;try{return{id:n.ids[r],values:l.toJs({dict_converter:Object.fromEntries})}}finally{l.destroy()}}catch(l){return{id:n.ids[r],values:{x:[],y:[]},error:String(l)}}finally{f.destroy(),i.destroy()}});self.postMessage({type:"COMPARISON_RESULT",requestId:n.requestId,trajectories:p});break}case"INIT":const _=e.toPy(n.settings);s(t.handle_update_settings(_));break;case"NEXT_STEP":s(t.handle_next_step());break;case"PREV_STEP":s(t.handle_prev_step());break;case"RESET":s(t.handle_reset());break;default:console.error("Unknown message type:",n);break}},d()})();
|
dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<title>Optimization</title>
|
| 8 |
-
<script type="module" crossorigin src="/assets/index-
|
| 9 |
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<div id="root"></div>
|
|
|
|
| 5 |
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
<title>Optimization</title>
|
| 8 |
+
<script type="module" crossorigin src="/assets/index-nsC-BkSr.js"></script>
|
| 9 |
+
<link rel="stylesheet" crossorigin href="/assets/index-D4A1ut36.css">
|
| 10 |
</head>
|
| 11 |
<body>
|
| 12 |
<div id="root"></div>
|
frontends/react/src/AlgorithmControls.tsx
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useEffect, useState } from "react";
|
| 2 |
+
import { InputField } from "@elvis/ui";
|
| 3 |
+
import { SUPPORTED_ALGORITHMS, type SettingsUi } from "./types";
|
| 4 |
+
export const DEFAULT_HYPERPARAMETERS = {
|
| 5 |
+
"Gradient Descent": { learningRate: "0.1", momentum: "0.0" },
|
| 6 |
+
"Nesterov": { learningRate: "0.1", momentum: "0.1" },
|
| 7 |
+
"Adam": { learningRate: "0.1", beta1: "0.9", beta2: "0.999", epsilon: "1e-8" },
|
| 8 |
+
"Adagrad": { learningRate: "0.1", epsilon: "1e-8" },
|
| 9 |
+
"RMSProp": { learningRate: "0.1", beta: "0.9", epsilon: "1e-8" },
|
| 10 |
+
"Adadelta": { beta: "0.9", epsilon: "1e-3" },
|
| 11 |
+
"Newton": {}
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
export default function AlgorithmControls({ settings, setSettings, onRandomInitialPoint, showInitialPoint = true }: {
|
| 16 |
+
settings: SettingsUi;
|
| 17 |
+
setSettings: (settings: SettingsUi) => void;
|
| 18 |
+
onRandomInitialPoint?: () => void;
|
| 19 |
+
showInitialPoint?: boolean;
|
| 20 |
+
}) {
|
| 21 |
+
function updateSettings(key: keyof SettingsUi, value: string) {
|
| 22 |
+
if (key === "algorithm") {
|
| 23 |
+
const defaults = DEFAULT_HYPERPARAMETERS[value as keyof typeof DEFAULT_HYPERPARAMETERS];
|
| 24 |
+
setSettings({ mode: settings.mode, functionExpr: settings.functionExpr, x0: settings.x0, y0: settings.y0, algorithm: value as SettingsUi["algorithm"], ...defaults });
|
| 25 |
+
} else {
|
| 26 |
+
setSettings({ ...settings, [key]: value });
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
const [initialPointInput, setInitialPointInput] = useState<string>(() =>
|
| 31 |
+
[settings.x0 ?? "", settings.y0 ?? ""].filter((part) => part !== "").join(", ")
|
| 32 |
+
);
|
| 33 |
+
|
| 34 |
+
useEffect(() => {
|
| 35 |
+
if (settings.mode === "Bivariate") {
|
| 36 |
+
setInitialPointInput([settings.x0 ?? "", settings.y0 ?? ""].filter((part) => part !== "").join(", "));
|
| 37 |
+
}
|
| 38 |
+
}, [settings.mode, settings.x0, settings.y0]);
|
| 39 |
+
|
| 40 |
+
function handleInitialPointChange(value: string) {
|
| 41 |
+
setInitialPointInput(value);
|
| 42 |
+
|
| 43 |
+
if (settings.mode === "Bivariate") {
|
| 44 |
+
const parts = value.split(",").map((part) => part.trim());
|
| 45 |
+
const [xValue, yValue] = parts;
|
| 46 |
+
|
| 47 |
+
setSettings({
|
| 48 |
+
...settings,
|
| 49 |
+
x0: xValue ?? "",
|
| 50 |
+
y0: yValue ?? settings.y0 ?? "",
|
| 51 |
+
});
|
| 52 |
+
} else {
|
| 53 |
+
updateSettings("x0", value);
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
return <>
|
| 58 |
+
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-0.5">
|
| 59 |
+
<label className="text-sm font-semibold whitespace-nowrap pr-0.25">Algorithm</label>
|
| 60 |
+
<div className="min-w-0">
|
| 61 |
+
<select
|
| 62 |
+
value={settings.algorithm}
|
| 63 |
+
onChange={(event) => updateSettings("algorithm", event.target.value as SettingsUi["algorithm"])}
|
| 64 |
+
className="h-10 w-full rounded border border-stone-300 bg-white px-2 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-stone-400"
|
| 65 |
+
>
|
| 66 |
+
{SUPPORTED_ALGORITHMS.map((option) => (
|
| 67 |
+
<option key={option} value={option}>
|
| 68 |
+
{option}
|
| 69 |
+
</option>
|
| 70 |
+
))}
|
| 71 |
+
</select>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
{showInitialPoint && (settings.mode === "Bivariate" ? (
|
| 75 |
+
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-0.5">
|
| 76 |
+
<label className="text-sm font-medium whitespace-nowrap pr-0.25">Initial (X, Y)</label>
|
| 77 |
+
<div className="flex min-w-0 items-center gap-2">
|
| 78 |
+
<input
|
| 79 |
+
type="text"
|
| 80 |
+
value={initialPointInput}
|
| 81 |
+
onChange={(event) => handleInitialPointChange(event.target.value)}
|
| 82 |
+
className="h-10 w-full rounded border border-stone-300 px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-stone-400"
|
| 83 |
+
/>
|
| 84 |
+
<button
|
| 85 |
+
type="button"
|
| 86 |
+
onClick={onRandomInitialPoint}
|
| 87 |
+
className="h-10 rounded border border-gray-300 bg-orange-200 px-3 py-1 text-sm text-stone-700 hover:bg-orange-300"
|
| 88 |
+
>
|
| 89 |
+
random
|
| 90 |
+
</button>
|
| 91 |
+
</div>
|
| 92 |
+
</div>
|
| 93 |
+
) : (
|
| 94 |
+
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-0.5">
|
| 95 |
+
<label className="text-sm font-medium whitespace-nowrap pr-0.25">Initial X</label>
|
| 96 |
+
<div className="flex min-w-0 items-center gap-2">
|
| 97 |
+
<input
|
| 98 |
+
type="text"
|
| 99 |
+
value={settings.x0}
|
| 100 |
+
onChange={(event) => updateSettings("x0", event.target.value)}
|
| 101 |
+
className="h-10 w-full rounded border border-stone-300 px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-stone-400"
|
| 102 |
+
/>
|
| 103 |
+
<button
|
| 104 |
+
type="button"
|
| 105 |
+
onClick={onRandomInitialPoint}
|
| 106 |
+
className="h-10 rounded border border-gray-300 bg-orange-200 px-3 py-1 text-sm text-stone-700 hover:bg-orange-300"
|
| 107 |
+
>
|
| 108 |
+
random
|
| 109 |
+
</button>
|
| 110 |
+
</div>
|
| 111 |
+
</div>
|
| 112 |
+
))}
|
| 113 |
+
|
| 114 |
+
<div className="grid grid-cols-2 gap-2">
|
| 115 |
+
{["Gradient Descent", "Nesterov", "Adam", "Adagrad", "RMSProp"].includes(settings.algorithm) && (
|
| 116 |
+
<>
|
| 117 |
+
<InputField
|
| 118 |
+
label="Learning Rate"
|
| 119 |
+
value={settings.learningRate}
|
| 120 |
+
onChange={(value) => updateSettings("learningRate", value)}
|
| 121 |
+
/>
|
| 122 |
+
</>
|
| 123 |
+
)}
|
| 124 |
+
{["Gradient Descent", "Nesterov"].includes(settings.algorithm) && (
|
| 125 |
+
<>
|
| 126 |
+
<InputField
|
| 127 |
+
label="Momentum"
|
| 128 |
+
value={settings.momentum}
|
| 129 |
+
onChange={(value) => updateSettings("momentum", value)}
|
| 130 |
+
/>
|
| 131 |
+
</>
|
| 132 |
+
)}
|
| 133 |
+
{settings.algorithm === "Adam" && (
|
| 134 |
+
<>
|
| 135 |
+
<InputField
|
| 136 |
+
label="Beta 1"
|
| 137 |
+
value={settings.beta1}
|
| 138 |
+
onChange={(value) => updateSettings("beta1", value)}
|
| 139 |
+
/>
|
| 140 |
+
<InputField
|
| 141 |
+
label="Beta 2"
|
| 142 |
+
value={settings.beta2}
|
| 143 |
+
onChange={(value) => updateSettings("beta2", value)}
|
| 144 |
+
/>
|
| 145 |
+
</>
|
| 146 |
+
)}
|
| 147 |
+
{["RMSProp", "Adadelta"].includes(settings.algorithm) && (
|
| 148 |
+
<>
|
| 149 |
+
<InputField
|
| 150 |
+
label="Beta"
|
| 151 |
+
value={settings.beta}
|
| 152 |
+
onChange={(value) => updateSettings("beta", value)}
|
| 153 |
+
/>
|
| 154 |
+
</>
|
| 155 |
+
)}
|
| 156 |
+
{["Adam", "Adagrad", "RMSProp", "Adadelta"].includes(settings.algorithm) && (
|
| 157 |
+
<>
|
| 158 |
+
<InputField
|
| 159 |
+
label="Epsilon"
|
| 160 |
+
value={settings.epsilon}
|
| 161 |
+
onChange={(value) => updateSettings("epsilon", value)}
|
| 162 |
+
/>
|
| 163 |
+
</>
|
| 164 |
+
)}
|
| 165 |
+
</div>
|
| 166 |
+
</>;
|
| 167 |
+
}
|
frontends/react/src/App.tsx
CHANGED
|
@@ -1,7 +1,8 @@
|
|
| 1 |
import { useEffect, useState, useRef } from "react";
|
| 2 |
import OptimizationPlot from "./OptimizationPlot.tsx";
|
| 3 |
import Sidebar from "./Sidebar.tsx";
|
| 4 |
-
import
|
|
|
|
| 5 |
import usePyodide from "./usePyodide.ts";
|
| 6 |
|
| 7 |
const INITIAL_SETTINGS_UI: SettingsUi = {
|
|
@@ -19,6 +20,11 @@ const INITIAL_YLIM: [number, number] = [-1, 1];
|
|
| 19 |
|
| 20 |
export default function App() {
|
| 21 |
const [settingsUi, setSettingsUi] = useState<SettingsUi>(INITIAL_SETTINGS_UI);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
const [isAnimating, setIsAnimating] = useState(false);
|
| 23 |
const [showGradientField, setShowGradientField] = useState(false);
|
| 24 |
const xlimRef = useRef<[number, number]>(INITIAL_XLIM);
|
|
@@ -27,6 +33,12 @@ export default function App() {
|
|
| 27 |
|
| 28 |
function handleSettingsUiChange(newSettings: SettingsUi) {
|
| 29 |
setIsAnimating(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
setSettingsUi(newSettings);
|
| 31 |
api.sendInit({ ...newSettings, xlim: xlimRef.current, ylim: ylimRef.current });
|
| 32 |
}
|
|
@@ -47,6 +59,17 @@ export default function App() {
|
|
| 47 |
}
|
| 48 |
|
| 49 |
const api = usePyodide(settings);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
const sendNextStep = api.sendNextStep;
|
| 51 |
const currentIteration = api.currentIteration;
|
| 52 |
const totalIterations = Math.max((api.plotData.trajectoryValues?.x.length ?? 1) - 1, 0);
|
|
@@ -81,7 +104,8 @@ export default function App() {
|
|
| 81 |
|
| 82 |
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-12 flex-1 min-h-0 overflow-auto">
|
| 83 |
<OptimizationPlot
|
| 84 |
-
data={api.plotData}
|
|
|
|
| 85 |
isLoading={api.isLoading}
|
| 86 |
xlim={xlimRef.current}
|
| 87 |
ylim={ylimRef.current}
|
|
@@ -89,7 +113,23 @@ export default function App() {
|
|
| 89 |
currentIteration={currentIteration}
|
| 90 |
showGradientField={showGradientField}
|
| 91 |
/>
|
| 92 |
-
<Sidebar
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
settings={settingsUi}
|
| 94 |
setSettings={handleSettingsUiChange}
|
| 95 |
onRandomInitialPoint={handleRandomInitialPoint}
|
|
|
|
| 1 |
import { useEffect, useState, useRef } from "react";
|
| 2 |
import OptimizationPlot from "./OptimizationPlot.tsx";
|
| 3 |
import Sidebar from "./Sidebar.tsx";
|
| 4 |
+
import ComparePanel from "./ComparePanel";
|
| 5 |
+
import type { ComparisonEntry, SettingsUi } from "./types.ts";
|
| 6 |
import usePyodide from "./usePyodide.ts";
|
| 7 |
|
| 8 |
const INITIAL_SETTINGS_UI: SettingsUi = {
|
|
|
|
| 20 |
|
| 21 |
export default function App() {
|
| 22 |
const [settingsUi, setSettingsUi] = useState<SettingsUi>(INITIAL_SETTINGS_UI);
|
| 23 |
+
const [compareActive, setCompareActive] = useState(false);
|
| 24 |
+
const [entries, setEntries] = useState<ComparisonEntry[]>([]);
|
| 25 |
+
const [comparisonIteration, setComparisonIteration] = useState(0);
|
| 26 |
+
const [comparisonAnimating, setComparisonAnimating] = useState(false);
|
| 27 |
+
const nextId = useRef(1);
|
| 28 |
const [isAnimating, setIsAnimating] = useState(false);
|
| 29 |
const [showGradientField, setShowGradientField] = useState(false);
|
| 30 |
const xlimRef = useRef<[number, number]>(INITIAL_XLIM);
|
|
|
|
| 33 |
|
| 34 |
function handleSettingsUiChange(newSettings: SettingsUi) {
|
| 35 |
setIsAnimating(false);
|
| 36 |
+
if (newSettings.mode !== settingsUi.mode || newSettings.functionExpr !== settingsUi.functionExpr
|
| 37 |
+
|| newSettings.x0 !== settingsUi.x0 || newSettings.y0 !== settingsUi.y0) {
|
| 38 |
+
setComparisonAnimating(false);
|
| 39 |
+
setComparisonIteration(0);
|
| 40 |
+
setEntries(previous => previous.map(entry => ({ ...entry, settings: { ...entry.settings, mode: newSettings.mode, functionExpr: newSettings.functionExpr } })));
|
| 41 |
+
}
|
| 42 |
setSettingsUi(newSettings);
|
| 43 |
api.sendInit({ ...newSettings, xlim: xlimRef.current, ylim: ylimRef.current });
|
| 44 |
}
|
|
|
|
| 59 |
}
|
| 60 |
|
| 61 |
const api = usePyodide(settings);
|
| 62 |
+
const { compare, comparisonBusy, isLoading } = api;
|
| 63 |
+
useEffect(() => {
|
| 64 |
+
if (!isLoading) compare(entries.map(entry => ({ ...entry.settings, xlim: xlimRef.current, ylim: ylimRef.current })), entries.map(entry => entry.id), comparisonIteration);
|
| 65 |
+
}, [entries, comparisonIteration, compare, isLoading]);
|
| 66 |
+
|
| 67 |
+
useEffect(() => {
|
| 68 |
+
if (!comparisonAnimating || comparisonBusy || isLoading) return;
|
| 69 |
+
const timeout = window.setTimeout(() => setComparisonIteration(value => value + 1), 200);
|
| 70 |
+
return () => window.clearTimeout(timeout);
|
| 71 |
+
}, [comparisonAnimating, comparisonBusy, isLoading, comparisonIteration]);
|
| 72 |
+
|
| 73 |
const sendNextStep = api.sendNextStep;
|
| 74 |
const currentIteration = api.currentIteration;
|
| 75 |
const totalIterations = Math.max((api.plotData.trajectoryValues?.x.length ?? 1) - 1, 0);
|
|
|
|
| 104 |
|
| 105 |
<div className="grid grid-cols-1 lg:grid-cols-[2fr_1fr] gap-12 flex-1 min-h-0 overflow-auto">
|
| 106 |
<OptimizationPlot
|
| 107 |
+
data={compareActive ? { functionValues: api.plotData.functionValues } : api.plotData}
|
| 108 |
+
comparisons={compareActive ? entries.map(entry => ({ ...entry, values: api.comparison.find(item => item.id === entry.id)?.values })) : undefined}
|
| 109 |
isLoading={api.isLoading}
|
| 110 |
xlim={xlimRef.current}
|
| 111 |
ylim={ylimRef.current}
|
|
|
|
| 113 |
currentIteration={currentIteration}
|
| 114 |
showGradientField={showGradientField}
|
| 115 |
/>
|
| 116 |
+
<Sidebar
|
| 117 |
+
onTabChange={(tab) => { setCompareActive(tab === "Compare"); setIsAnimating(false); setComparisonAnimating(false); }}
|
| 118 |
+
comparison={<ComparePanel settings={settingsUi} entries={entries} trajectories={api.comparison}
|
| 119 |
+
iteration={comparisonIteration} busy={comparisonBusy} ready={!api.isLoading} animating={comparisonAnimating}
|
| 120 |
+
onAdd={(configured) => {
|
| 121 |
+
const id = nextId.current++;
|
| 122 |
+
setComparisonIteration(0);
|
| 123 |
+
setEntries(previous => [...previous, { id, settings: configured, color: `hsl(${(id - 1) * 137.508 % 360}, 70%, 45%)` }]);
|
| 124 |
+
}}
|
| 125 |
+
onRemove={(id) => { setComparisonAnimating(false); setEntries(previous => previous.filter(entry => entry.id !== id)); }}
|
| 126 |
+
onAction={(action) => {
|
| 127 |
+
if (action === "Animate") setComparisonAnimating(true);
|
| 128 |
+
else if (action === "Pause") setComparisonAnimating(false);
|
| 129 |
+
else if (action === "Reset") setComparisonIteration(0);
|
| 130 |
+
else setComparisonIteration(value => Math.max(0, value + (action === "Next" ? 1 : -1)));
|
| 131 |
+
}}
|
| 132 |
+
/>}
|
| 133 |
settings={settingsUi}
|
| 134 |
setSettings={handleSettingsUiChange}
|
| 135 |
onRandomInitialPoint={handleRandomInitialPoint}
|
frontends/react/src/ComparePanel.tsx
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState } from "react";
|
| 2 |
+
import AlgorithmControls, { DEFAULT_HYPERPARAMETERS } from "./AlgorithmControls";
|
| 3 |
+
import type { ComparisonEntry, ComparisonTrajectory, SettingsUi } from "./types";
|
| 4 |
+
|
| 5 |
+
export default function ComparePanel({ settings, entries, trajectories, onAdd, onRemove, iteration, busy, ready, animating, onAction }: {
|
| 6 |
+
settings: SettingsUi;
|
| 7 |
+
entries: ComparisonEntry[];
|
| 8 |
+
trajectories: ComparisonTrajectory[];
|
| 9 |
+
onAdd: (settings: SettingsUi) => void;
|
| 10 |
+
onRemove: (id: number) => void;
|
| 11 |
+
iteration: number;
|
| 12 |
+
busy: boolean;
|
| 13 |
+
ready: boolean;
|
| 14 |
+
animating: boolean;
|
| 15 |
+
onAction: (action: string) => void;
|
| 16 |
+
}) {
|
| 17 |
+
const [draft, setDraft] = useState(settings);
|
| 18 |
+
const configured: SettingsUi = { algorithm: draft.algorithm, x0: draft.x0, y0: draft.y0,
|
| 19 |
+
mode: settings.mode, functionExpr: settings.functionExpr,
|
| 20 |
+
...Object.fromEntries(Object.entries(DEFAULT_HYPERPARAMETERS[draft.algorithm]).map(([key, fallback]) => [key, draft[key as keyof SettingsUi] ?? fallback])) };
|
| 21 |
+
const valid = [configured.x0, ...(configured.mode === "Bivariate" ? [configured.y0] : []),
|
| 22 |
+
...Object.entries(configured).filter(([key]) => ["learningRate", "momentum", "beta", "beta1", "beta2", "epsilon"].includes(key)).map(([, value]) => value)
|
| 23 |
+
].every(value => value !== undefined && value.trim() !== "" && Number.isFinite(Number(value)));
|
| 24 |
+
return <>
|
| 25 |
+
<AlgorithmControls settings={configured} setSettings={setDraft} onRandomInitialPoint={() => setDraft({ ...configured, x0: (Math.random() * 2 - 1).toFixed(2), y0: (Math.random() * 2 - 1).toFixed(2) })} />
|
| 26 |
+
<button className="rounded border border-gray-300 bg-orange-200 px-3 py-1 disabled:opacity-50" disabled={!valid || !ready || busy || animating} onClick={() => onAdd(configured)}>Add</button>
|
| 27 |
+
<div className="text-sm font-semibold">Selected algorithms</div>
|
| 28 |
+
{entries.length === 0 && <p className="text-sm text-stone-500">Add algorithms to compare their trajectories on the same objective.</p>}
|
| 29 |
+
<ul className="flex flex-col gap-2">
|
| 30 |
+
{entries.map((entry, index) => <li key={entry.id} className="rounded border border-stone-300 p-2 text-sm">
|
| 31 |
+
<div className="flex items-center gap-2">
|
| 32 |
+
<span className="h-3 w-3 rounded-full shrink-0" style={{ background: entry.color }} />
|
| 33 |
+
<span className="flex-1 font-semibold">{index + 1}. {entry.settings.algorithm}</span>
|
| 34 |
+
<button aria-label={`Remove ${index + 1}. ${entry.settings.algorithm}`} onClick={() => onRemove(entry.id)}>Remove</button>
|
| 35 |
+
</div>
|
| 36 |
+
<div className="text-xs text-stone-600">{Object.entries(entry.settings).filter(([key]) => !["mode", "functionExpr", "algorithm"].includes(key) && (key !== "y0" || settings.mode === "Bivariate")).map(([key, value]) => `${key}: ${value}`).join(", ")}</div>
|
| 37 |
+
{trajectories.find(item => item.id === entry.id)?.error && <p role="alert" className="text-red-700">Unable to compute this trajectory. Check its settings.</p>}
|
| 38 |
+
</li>)}
|
| 39 |
+
</ul>
|
| 40 |
+
<div className="text-sm" role="status">Iteration {iteration}{busy ? " · Computing…" : ""}</div>
|
| 41 |
+
<div className="flex flex-wrap gap-2">
|
| 42 |
+
{["Animate", "Previous", "Next", "Reset", "Pause"].map(action => <button key={action}
|
| 43 |
+
className="rounded border border-gray-300 bg-orange-200 px-3 py-1 text-sm disabled:opacity-50"
|
| 44 |
+
disabled={action === "Pause" ? !animating : !ready || !entries.length || busy || animating || ((action === "Previous" || action === "Reset") && iteration === 0)}
|
| 45 |
+
onClick={() => onAction(action)}>{action}</button>)}
|
| 46 |
+
</div>
|
| 47 |
+
</>;
|
| 48 |
+
}
|
frontends/react/src/OptimizationPlot.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
import { useMemo, useRef } from "react";
|
| 2 |
import Plot from "react-plotly.js";
|
| 3 |
-
import type { PlotData } from "./types";
|
| 4 |
import { Card } from "@elvis/ui";
|
| 5 |
|
| 6 |
interface OptimizationPlotProps {
|
|
|
|
| 7 |
data: PlotData;
|
| 8 |
isLoading: boolean;
|
| 9 |
xlim: [number, number];
|
|
@@ -13,8 +14,16 @@ interface OptimizationPlotProps {
|
|
| 13 |
showGradientField: boolean;
|
| 14 |
}
|
| 15 |
|
| 16 |
-
export default function OptimizationPlot({ data, isLoading, xlim, ylim, setAxisLimits, currentIteration, showGradientField }: OptimizationPlotProps) {
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
// data
|
| 19 |
let x: number[] = data.functionValues ? data.functionValues.x : [];
|
| 20 |
let y: number[] = data.functionValues ? data.functionValues.y : [];
|
|
@@ -178,6 +187,7 @@ export default function OptimizationPlot({ data, isLoading, xlim, ylim, setAxisL
|
|
| 178 |
mode: 'lines',
|
| 179 |
line: { color: '#1f77b4', width: 2 },
|
| 180 |
hoverinfo: "skip",
|
|
|
|
| 181 |
},
|
| 182 |
{
|
| 183 |
x: trajX,
|
|
@@ -187,6 +197,7 @@ export default function OptimizationPlot({ data, isLoading, xlim, ylim, setAxisL
|
|
| 187 |
line: { color: '#d97871', width: 2 },
|
| 188 |
marker: { color: '#d97871', size: 10 },
|
| 189 |
hoverinfo: "skip",
|
|
|
|
| 190 |
},
|
| 191 |
{
|
| 192 |
x: trajX[currentIteration] !== undefined ? [trajX[currentIteration]] : [],
|
|
@@ -195,9 +206,11 @@ export default function OptimizationPlot({ data, isLoading, xlim, ylim, setAxisL
|
|
| 195 |
mode: 'markers',
|
| 196 |
marker: { color: 'red', size: 12 },
|
| 197 |
hoverinfo: "skip",
|
|
|
|
| 198 |
}
|
|
|
|
| 199 |
]}
|
| 200 |
-
layout={layoutRef.current}
|
| 201 |
style={{ width: '100%', height: '100%' }}
|
| 202 |
config={{
|
| 203 |
responsive: true,
|
|
@@ -237,6 +250,7 @@ export default function OptimizationPlot({ data, isLoading, xlim, ylim, setAxisL
|
|
| 237 |
type: 'contour',
|
| 238 |
colorscale: 'Viridis',
|
| 239 |
hoverinfo: "skip",
|
|
|
|
| 240 |
contours: {
|
| 241 |
coloring: "heatmap",
|
| 242 |
showlines: false,
|
|
@@ -282,6 +296,7 @@ export default function OptimizationPlot({ data, isLoading, xlim, ylim, setAxisL
|
|
| 282 |
line: { color: '#d97871', width: 2 },
|
| 283 |
marker: { color: '#d97871', size: 10 },
|
| 284 |
hoverinfo: "skip",
|
|
|
|
| 285 |
},
|
| 286 |
{
|
| 287 |
x: trajX[currentIteration] !== undefined ? [trajX[currentIteration]] : [],
|
|
@@ -291,9 +306,11 @@ export default function OptimizationPlot({ data, isLoading, xlim, ylim, setAxisL
|
|
| 291 |
mode: 'markers',
|
| 292 |
marker: { color: 'red', size: 12 },
|
| 293 |
hoverinfo: "skip",
|
|
|
|
| 294 |
}
|
|
|
|
| 295 |
]}
|
| 296 |
-
layout={{ ...layoutRef.current, annotations: [] }}
|
| 297 |
style={{ width: '100%', height: '100%' }}
|
| 298 |
config={{
|
| 299 |
responsive: true,
|
|
|
|
| 1 |
import { useMemo, useRef } from "react";
|
| 2 |
import Plot from "react-plotly.js";
|
| 3 |
+
import type { ComparisonEntry, TrajectoryValues, PlotData } from "./types";
|
| 4 |
import { Card } from "@elvis/ui";
|
| 5 |
|
| 6 |
interface OptimizationPlotProps {
|
| 7 |
+
comparisons?: (ComparisonEntry & { values?: TrajectoryValues })[];
|
| 8 |
data: PlotData;
|
| 9 |
isLoading: boolean;
|
| 10 |
xlim: [number, number];
|
|
|
|
| 14 |
showGradientField: boolean;
|
| 15 |
}
|
| 16 |
|
| 17 |
+
export default function OptimizationPlot({ comparisons, data, isLoading, xlim, ylim, setAxisLimits, currentIteration, showGradientField }: OptimizationPlotProps) {
|
| 18 |
|
| 19 |
+
const comparisonTraces = (comparisons ?? []).flatMap((entry, index) => [
|
| 20 |
+
{ x: entry.values?.x ?? [], y: entry.values?.y ?? [], type: "scatter" as const, mode: "lines+markers" as const,
|
| 21 |
+
name: `${index + 1}. ${entry.settings.algorithm}`, legendgroup: String(entry.id), showlegend: true,
|
| 22 |
+
line: { color: entry.color, width: 2 }, marker: { color: entry.color, size: 6 } },
|
| 23 |
+
{ x: entry.values?.x.slice(-1) ?? [], y: entry.values?.y.slice(-1) ?? [], type: "scatter" as const, mode: "markers" as const,
|
| 24 |
+
name: `${index + 1}. ${entry.settings.algorithm}`, legendgroup: String(entry.id), showlegend: false,
|
| 25 |
+
marker: { color: entry.color, size: 12, line: { color: "white", width: 2 } } }
|
| 26 |
+
]);
|
| 27 |
// data
|
| 28 |
let x: number[] = data.functionValues ? data.functionValues.x : [];
|
| 29 |
let y: number[] = data.functionValues ? data.functionValues.y : [];
|
|
|
|
| 187 |
mode: 'lines',
|
| 188 |
line: { color: '#1f77b4', width: 2 },
|
| 189 |
hoverinfo: "skip",
|
| 190 |
+
showlegend: false,
|
| 191 |
},
|
| 192 |
{
|
| 193 |
x: trajX,
|
|
|
|
| 197 |
line: { color: '#d97871', width: 2 },
|
| 198 |
marker: { color: '#d97871', size: 10 },
|
| 199 |
hoverinfo: "skip",
|
| 200 |
+
showlegend: false,
|
| 201 |
},
|
| 202 |
{
|
| 203 |
x: trajX[currentIteration] !== undefined ? [trajX[currentIteration]] : [],
|
|
|
|
| 206 |
mode: 'markers',
|
| 207 |
marker: { color: 'red', size: 12 },
|
| 208 |
hoverinfo: "skip",
|
| 209 |
+
showlegend: false,
|
| 210 |
}
|
| 211 |
+
,...comparisonTraces
|
| 212 |
]}
|
| 213 |
+
layout={{ ...layoutRef.current, showlegend: comparisons !== undefined, legend: { x: 0.02, y: 0.98, xanchor: "left", yanchor: "top", bgcolor: "rgba(255,255,255,0.75)" } }}
|
| 214 |
style={{ width: '100%', height: '100%' }}
|
| 215 |
config={{
|
| 216 |
responsive: true,
|
|
|
|
| 250 |
type: 'contour',
|
| 251 |
colorscale: 'Viridis',
|
| 252 |
hoverinfo: "skip",
|
| 253 |
+
showlegend: false,
|
| 254 |
contours: {
|
| 255 |
coloring: "heatmap",
|
| 256 |
showlines: false,
|
|
|
|
| 296 |
line: { color: '#d97871', width: 2 },
|
| 297 |
marker: { color: '#d97871', size: 10 },
|
| 298 |
hoverinfo: "skip",
|
| 299 |
+
showlegend: false,
|
| 300 |
},
|
| 301 |
{
|
| 302 |
x: trajX[currentIteration] !== undefined ? [trajX[currentIteration]] : [],
|
|
|
|
| 306 |
mode: 'markers',
|
| 307 |
marker: { color: 'red', size: 12 },
|
| 308 |
hoverinfo: "skip",
|
| 309 |
+
showlegend: false,
|
| 310 |
}
|
| 311 |
+
,...comparisonTraces
|
| 312 |
]}
|
| 313 |
+
layout={{ ...layoutRef.current, annotations: [], showlegend: comparisons !== undefined, legend: { x: 0.02, y: 0.98, xanchor: "left", yanchor: "top", bgcolor: "rgba(255,255,255,0.75)" } }}
|
| 314 |
style={{ width: '100%', height: '100%' }}
|
| 315 |
config={{
|
| 316 |
responsive: true,
|
frontends/react/src/Sidebar.tsx
CHANGED
|
@@ -1,22 +1,13 @@
|
|
|
|
|
|
|
|
| 1 |
import { useCallback, useEffect, useRef, useState } from "react";
|
| 2 |
import ReactMarkdown from "react-markdown";
|
| 3 |
import { InputField, Tabs, Card } from "@elvis/ui";
|
| 4 |
-
import {
|
| 5 |
import usageMarkdown from "./usage.md?raw";
|
| 6 |
import IterationHistoryPlot from "./IterationHistoryPlot.tsx";
|
| 7 |
|
| 8 |
|
| 9 |
-
const DEFAULT_HYPERPARAMETERS = {
|
| 10 |
-
"Gradient Descent": { learningRate: "0.1", momentum: "0.0" },
|
| 11 |
-
"Nesterov": { learningRate: "0.1", momentum: "0.0" },
|
| 12 |
-
"Adam": { learningRate: "0.1", beta1: "0.9", beta2: "0.999", epsilon: "1e-8" },
|
| 13 |
-
"Adagrad": { learningRate: "0.1", epsilon: "1e-8" },
|
| 14 |
-
"RMSProp": { learningRate: "0.1", beta: "0.9", epsilon: "1e-8" },
|
| 15 |
-
"Adadelta": { beta: "0.9", epsilon: "1e-3" },
|
| 16 |
-
"Newton": {}
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
|
| 20 |
const UNIVARIATE_FUNCTION_OPTIONS = {
|
| 21 |
"--Custom--": "x^2",
|
| 22 |
"Quadratic": "x^2",
|
|
@@ -35,6 +26,8 @@ const BIVARIATE_FUNCTION_OPTIONS = {
|
|
| 35 |
|
| 36 |
|
| 37 |
interface SidebarProps {
|
|
|
|
|
|
|
| 38 |
settings: SettingsUi,
|
| 39 |
setSettings: (settings: SettingsUi) => void,
|
| 40 |
onRandomInitialPoint: () => void,
|
|
@@ -53,6 +46,8 @@ interface SidebarProps {
|
|
| 53 |
}
|
| 54 |
|
| 55 |
export default function Sidebar({
|
|
|
|
|
|
|
| 56 |
settings,
|
| 57 |
setSettings,
|
| 58 |
onRandomInitialPoint,
|
|
@@ -68,29 +63,14 @@ export default function Sidebar({
|
|
| 68 |
showGradientField,
|
| 69 |
onShowGradientFieldChange,
|
| 70 |
}: SidebarProps) {
|
| 71 |
-
const tabs = ["Settings", "Optimize", "Usage"] as const;
|
| 72 |
const [activeTab, setActiveTab] = useState<(typeof tabs)[number]>("Settings");
|
| 73 |
|
|
|
|
| 74 |
function updateSettings(key: keyof SettingsUi, value: string) {
|
| 75 |
-
|
| 76 |
-
const defaults = DEFAULT_HYPERPARAMETERS[value as keyof typeof DEFAULT_HYPERPARAMETERS];
|
| 77 |
-
setSettings({ ...settings, algorithm: value as SettingsUi["algorithm"], ...defaults });
|
| 78 |
-
} else {
|
| 79 |
-
setSettings({ ...settings, [key]: value });
|
| 80 |
-
}
|
| 81 |
}
|
| 82 |
|
| 83 |
-
const [functionOption, setFunctionOption] = useState<string>("--Custom--");
|
| 84 |
-
const [initialPointInput, setInitialPointInput] = useState<string>(() =>
|
| 85 |
-
[settings.x0 ?? "", settings.y0 ?? ""].filter((part) => part !== "").join(", ")
|
| 86 |
-
);
|
| 87 |
-
|
| 88 |
-
useEffect(() => {
|
| 89 |
-
if (settings.mode === "Bivariate") {
|
| 90 |
-
setInitialPointInput([settings.x0 ?? "", settings.y0 ?? ""].filter((part) => part !== "").join(", "));
|
| 91 |
-
}
|
| 92 |
-
}, [settings.mode, settings.x0, settings.y0]);
|
| 93 |
-
|
| 94 |
function handleProblemSettingChange(value: string) {
|
| 95 |
const [mode, option] = value.split("::") as [SettingsUi["mode"], string];
|
| 96 |
const selectedOption = option ?? "--Custom--";
|
|
@@ -106,23 +86,6 @@ export default function Sidebar({
|
|
| 106 |
return values?.[currentIteration] ?? null;
|
| 107 |
}
|
| 108 |
|
| 109 |
-
function handleInitialPointChange(value: string) {
|
| 110 |
-
setInitialPointInput(value);
|
| 111 |
-
|
| 112 |
-
if (settings.mode === "Bivariate") {
|
| 113 |
-
const parts = value.split(",").map((part) => part.trim());
|
| 114 |
-
const [xValue, yValue] = parts;
|
| 115 |
-
|
| 116 |
-
setSettings({
|
| 117 |
-
...settings,
|
| 118 |
-
x0: xValue ?? "",
|
| 119 |
-
y0: yValue ?? settings.y0 ?? "",
|
| 120 |
-
});
|
| 121 |
-
} else {
|
| 122 |
-
updateSettings("x0", value);
|
| 123 |
-
}
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
const currentX = getCurrentValue(trajectoryValues?.x);
|
| 127 |
const currentY = getCurrentValue(trajectoryValues?.y);
|
| 128 |
|
|
@@ -182,7 +145,7 @@ export default function Sidebar({
|
|
| 182 |
|
| 183 |
return (
|
| 184 |
<Card className="flex flex-col h-full p-4 gap-2 min-w-0 min-h-0 overflow-auto">
|
| 185 |
-
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
|
| 186 |
|
| 187 |
{/* Tab content */}
|
| 188 |
<div className="flex flex-1 flex-col gap-4 min-h-0 min-w-0 overflow-auto">
|
|
@@ -247,114 +210,7 @@ export default function Sidebar({
|
|
| 247 |
</fieldset>
|
| 248 |
)}
|
| 249 |
|
| 250 |
-
<
|
| 251 |
-
<label className="text-sm font-semibold whitespace-nowrap pr-0.25">Algorithm</label>
|
| 252 |
-
<div className="min-w-0">
|
| 253 |
-
<select
|
| 254 |
-
value={settings.algorithm}
|
| 255 |
-
onChange={(event) => updateSettings("algorithm", event.target.value as SettingsUi["algorithm"])}
|
| 256 |
-
className="h-10 w-full rounded border border-stone-300 bg-white px-2 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-stone-400"
|
| 257 |
-
>
|
| 258 |
-
{SUPPORTED_ALGORITHMS.map((option) => (
|
| 259 |
-
<option key={option} value={option}>
|
| 260 |
-
{option}
|
| 261 |
-
</option>
|
| 262 |
-
))}
|
| 263 |
-
</select>
|
| 264 |
-
</div>
|
| 265 |
-
</div>
|
| 266 |
-
{settings.mode === "Bivariate" ? (
|
| 267 |
-
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-0.5">
|
| 268 |
-
<label className="text-sm font-medium whitespace-nowrap pr-0.25">Initial (X, Y)</label>
|
| 269 |
-
<div className="flex min-w-0 items-center gap-2">
|
| 270 |
-
<input
|
| 271 |
-
type="text"
|
| 272 |
-
value={initialPointInput}
|
| 273 |
-
onChange={(event) => handleInitialPointChange(event.target.value)}
|
| 274 |
-
className="h-10 w-full rounded border border-stone-300 px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-stone-400"
|
| 275 |
-
/>
|
| 276 |
-
<button
|
| 277 |
-
type="button"
|
| 278 |
-
onClick={onRandomInitialPoint}
|
| 279 |
-
className="h-10 rounded border border-gray-300 bg-orange-200 px-3 py-1 text-sm text-stone-700 hover:bg-orange-300"
|
| 280 |
-
>
|
| 281 |
-
random
|
| 282 |
-
</button>
|
| 283 |
-
</div>
|
| 284 |
-
</div>
|
| 285 |
-
) : (
|
| 286 |
-
<div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-0.5">
|
| 287 |
-
<label className="text-sm font-medium whitespace-nowrap pr-0.25">Initial X</label>
|
| 288 |
-
<div className="flex min-w-0 items-center gap-2">
|
| 289 |
-
<input
|
| 290 |
-
type="text"
|
| 291 |
-
value={settings.x0}
|
| 292 |
-
onChange={(event) => updateSettings("x0", event.target.value)}
|
| 293 |
-
className="h-10 w-full rounded border border-stone-300 px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-stone-400"
|
| 294 |
-
/>
|
| 295 |
-
<button
|
| 296 |
-
type="button"
|
| 297 |
-
onClick={onRandomInitialPoint}
|
| 298 |
-
className="h-10 rounded border border-gray-300 bg-orange-200 px-3 py-1 text-sm text-stone-700 hover:bg-orange-300"
|
| 299 |
-
>
|
| 300 |
-
random
|
| 301 |
-
</button>
|
| 302 |
-
</div>
|
| 303 |
-
</div>
|
| 304 |
-
)}
|
| 305 |
-
|
| 306 |
-
<div className="grid grid-cols-2 gap-2">
|
| 307 |
-
{["Gradient Descent", "Nesterov", "Adam", "Adagrad", "RMSProp", "Adadelta"].includes(settings.algorithm) && (
|
| 308 |
-
<>
|
| 309 |
-
<InputField
|
| 310 |
-
label="Learning Rate"
|
| 311 |
-
value={settings.learningRate}
|
| 312 |
-
onChange={(value) => updateSettings("learningRate", value)}
|
| 313 |
-
/>
|
| 314 |
-
</>
|
| 315 |
-
)}
|
| 316 |
-
{["Gradient Descent", "Nesterov"].includes(settings.algorithm) && (
|
| 317 |
-
<>
|
| 318 |
-
<InputField
|
| 319 |
-
label="Momentum"
|
| 320 |
-
value={settings.momentum}
|
| 321 |
-
onChange={(value) => updateSettings("momentum", value)}
|
| 322 |
-
/>
|
| 323 |
-
</>
|
| 324 |
-
)}
|
| 325 |
-
{settings.algorithm === "Adam" && (
|
| 326 |
-
<>
|
| 327 |
-
<InputField
|
| 328 |
-
label="Beta 1"
|
| 329 |
-
value={settings.beta1}
|
| 330 |
-
onChange={(value) => updateSettings("beta1", value)}
|
| 331 |
-
/>
|
| 332 |
-
<InputField
|
| 333 |
-
label="Beta 2"
|
| 334 |
-
value={settings.beta2}
|
| 335 |
-
onChange={(value) => updateSettings("beta2", value)}
|
| 336 |
-
/>
|
| 337 |
-
</>
|
| 338 |
-
)}
|
| 339 |
-
{["RMSProp", "Adadelta"].includes(settings.algorithm) && (
|
| 340 |
-
<>
|
| 341 |
-
<InputField
|
| 342 |
-
label="Beta"
|
| 343 |
-
value={settings.beta}
|
| 344 |
-
onChange={(value) => updateSettings("beta", value)}
|
| 345 |
-
/>
|
| 346 |
-
</>
|
| 347 |
-
)}
|
| 348 |
-
{["Adam", "Adagrad", "RMSProp", "Adadelta"].includes(settings.algorithm) && (
|
| 349 |
-
<>
|
| 350 |
-
<InputField
|
| 351 |
-
label="Epsilon"
|
| 352 |
-
value={settings.epsilon}
|
| 353 |
-
onChange={(value) => updateSettings("epsilon", value)}
|
| 354 |
-
/>
|
| 355 |
-
</>
|
| 356 |
-
)}
|
| 357 |
-
</div>
|
| 358 |
</>
|
| 359 |
)}
|
| 360 |
|
|
@@ -502,6 +358,8 @@ export default function Sidebar({
|
|
| 502 |
</>
|
| 503 |
)}
|
| 504 |
|
|
|
|
|
|
|
| 505 |
{activeTab === "Usage" && (
|
| 506 |
<ReactMarkdown
|
| 507 |
components={{
|
|
|
|
| 1 |
+
import type { ReactNode } from "react";
|
| 2 |
+
import AlgorithmControls from "./AlgorithmControls";
|
| 3 |
import { useCallback, useEffect, useRef, useState } from "react";
|
| 4 |
import ReactMarkdown from "react-markdown";
|
| 5 |
import { InputField, Tabs, Card } from "@elvis/ui";
|
| 6 |
+
import { type SettingsUi, type TrajectoryValues } from "./types.ts";
|
| 7 |
import usageMarkdown from "./usage.md?raw";
|
| 8 |
import IterationHistoryPlot from "./IterationHistoryPlot.tsx";
|
| 9 |
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
const UNIVARIATE_FUNCTION_OPTIONS = {
|
| 12 |
"--Custom--": "x^2",
|
| 13 |
"Quadratic": "x^2",
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
interface SidebarProps {
|
| 29 |
+
comparison: ReactNode,
|
| 30 |
+
onTabChange: (tab: string) => void,
|
| 31 |
settings: SettingsUi,
|
| 32 |
setSettings: (settings: SettingsUi) => void,
|
| 33 |
onRandomInitialPoint: () => void,
|
|
|
|
| 46 |
}
|
| 47 |
|
| 48 |
export default function Sidebar({
|
| 49 |
+
comparison,
|
| 50 |
+
onTabChange,
|
| 51 |
settings,
|
| 52 |
setSettings,
|
| 53 |
onRandomInitialPoint,
|
|
|
|
| 63 |
showGradientField,
|
| 64 |
onShowGradientFieldChange,
|
| 65 |
}: SidebarProps) {
|
| 66 |
+
const tabs = ["Settings", "Optimize", "Compare", "Usage"] as const;
|
| 67 |
const [activeTab, setActiveTab] = useState<(typeof tabs)[number]>("Settings");
|
| 68 |
|
| 69 |
+
const [functionOption, setFunctionOption] = useState<string>("--Custom--");
|
| 70 |
function updateSettings(key: keyof SettingsUi, value: string) {
|
| 71 |
+
setSettings({ ...settings, [key]: value });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
}
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
function handleProblemSettingChange(value: string) {
|
| 75 |
const [mode, option] = value.split("::") as [SettingsUi["mode"], string];
|
| 76 |
const selectedOption = option ?? "--Custom--";
|
|
|
|
| 86 |
return values?.[currentIteration] ?? null;
|
| 87 |
}
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
const currentX = getCurrentValue(trajectoryValues?.x);
|
| 90 |
const currentY = getCurrentValue(trajectoryValues?.y);
|
| 91 |
|
|
|
|
| 145 |
|
| 146 |
return (
|
| 147 |
<Card className="flex flex-col h-full p-4 gap-2 min-w-0 min-h-0 overflow-auto">
|
| 148 |
+
<Tabs tabs={tabs} activeTab={activeTab} onChange={(tab) => { setActiveTab(tab); onTabChange(tab); }} />
|
| 149 |
|
| 150 |
{/* Tab content */}
|
| 151 |
<div className="flex flex-1 flex-col gap-4 min-h-0 min-w-0 overflow-auto">
|
|
|
|
| 210 |
</fieldset>
|
| 211 |
)}
|
| 212 |
|
| 213 |
+
<AlgorithmControls settings={settings} setSettings={setSettings} onRandomInitialPoint={onRandomInitialPoint} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
</>
|
| 215 |
)}
|
| 216 |
|
|
|
|
| 358 |
</>
|
| 359 |
)}
|
| 360 |
|
| 361 |
+
{activeTab === "Compare" && comparison}
|
| 362 |
+
|
| 363 |
{activeTab === "Usage" && (
|
| 364 |
<ReactMarkdown
|
| 365 |
components={{
|
frontends/react/src/pyodide.worker.ts
CHANGED
|
@@ -58,6 +58,29 @@ self.onmessage = async (event) => {
|
|
| 58 |
}
|
| 59 |
|
| 60 |
switch (message.type) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
case "INIT":
|
| 62 |
const pythonSettings = pyodide.toPy(message.settings);
|
| 63 |
handlePythonResult(manager.handle_update_settings(pythonSettings));
|
|
|
|
| 58 |
}
|
| 59 |
|
| 60 |
switch (message.type) {
|
| 61 |
+
case "COMPARE": {
|
| 62 |
+
const trajectories = message.settings.map((settings, index) => {
|
| 63 |
+
const managerClass = pyodide.globals.get("OptimizationManager");
|
| 64 |
+
const comparisonManager = managerClass();
|
| 65 |
+
managerClass.destroy();
|
| 66 |
+
const pythonSettings = pyodide.toPy(settings);
|
| 67 |
+
try {
|
| 68 |
+
comparisonManager.handle_update_settings(pythonSettings)?.destroy();
|
| 69 |
+
comparisonManager._compute_trajectory_values(pythonSettings, message.iteration + 1);
|
| 70 |
+
const values = comparisonManager.trajectory_values;
|
| 71 |
+
try {
|
| 72 |
+
return { id: message.ids[index], values: values.toJs({ dict_converter: Object.fromEntries }) };
|
| 73 |
+
} finally { values.destroy(); }
|
| 74 |
+
} catch (error) {
|
| 75 |
+
return { id: message.ids[index], values: { x: [], y: [] }, error: String(error) };
|
| 76 |
+
} finally {
|
| 77 |
+
pythonSettings.destroy();
|
| 78 |
+
comparisonManager.destroy();
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
self.postMessage({ type: "COMPARISON_RESULT", requestId: message.requestId, trajectories });
|
| 82 |
+
break;
|
| 83 |
+
}
|
| 84 |
case "INIT":
|
| 85 |
const pythonSettings = pyodide.toPy(message.settings);
|
| 86 |
handlePythonResult(manager.handle_update_settings(pythonSettings));
|
frontends/react/src/types.ts
CHANGED
|
@@ -65,9 +65,14 @@ export const SUPPORTED_ALGORITHMS = [
|
|
| 65 |
] as const;
|
| 66 |
|
| 67 |
export type WorkerMessage =
|
|
|
|
|
|
|
| 68 |
| { type: "READY" }
|
| 69 |
| { type: "RESULT"; data: PlotData }
|
| 70 |
| { type: "INIT"; settings: Settings }
|
| 71 |
| { type: "NEXT_STEP" }
|
| 72 |
| { type: "PREV_STEP" }
|
| 73 |
| { type: "RESET" };
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
] as const;
|
| 66 |
|
| 67 |
export type WorkerMessage =
|
| 68 |
+
| { type: "COMPARE"; requestId: number; settings: Settings[]; ids: number[]; iteration: number }
|
| 69 |
+
| { type: "COMPARISON_RESULT"; requestId: number; trajectories: ComparisonTrajectory[] }
|
| 70 |
| { type: "READY" }
|
| 71 |
| { type: "RESULT"; data: PlotData }
|
| 72 |
| { type: "INIT"; settings: Settings }
|
| 73 |
| { type: "NEXT_STEP" }
|
| 74 |
| { type: "PREV_STEP" }
|
| 75 |
| { type: "RESET" };
|
| 76 |
+
|
| 77 |
+
export type ComparisonEntry = { id: number; settings: SettingsUi; color: string };
|
| 78 |
+
export type ComparisonTrajectory = { id: number; values: TrajectoryValues; error?: string };
|
frontends/react/src/usage.md
CHANGED
|
@@ -25,3 +25,10 @@ The plot is interactive:
|
|
| 25 |
|
| 26 |
- Click and drag to pan
|
| 27 |
- Scroll to zoom
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
- Click and drag to pan
|
| 27 |
- Scroll to zoom
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
## Compare algorithms
|
| 31 |
+
|
| 32 |
+
Open **Compare** after **Optimize**. Choose an algorithm, configure its initial point and parameters, and click **Add**. Repeat to compare different algorithms or parameter choices. Each entry has a matching trajectory color and can be removed.
|
| 33 |
+
|
| 34 |
+
**Animate** advances all selected algorithms together. **Pause** stops playback; **Previous** and **Next** step through iterations; **Reset** returns to the initial points. Adding an algorithm restarts the comparison at iteration zero. Each added algorithm keeps its own initial point, shown in the selected algorithms list. The objective is shared with Settings. Changing the objective or the initial point in Settings restarts the comparison while preserving each algorithm’s configured initial point.
|
frontends/react/src/usePyodide.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
| 1 |
import { useCallback, useEffect, useRef, useState } from "react";
|
| 2 |
import PyodideWorker from "./pyodide.worker.ts?worker";
|
| 3 |
-
import type { PlotData, Settings, WorkerMessage } from "./types.ts";
|
| 4 |
|
| 5 |
|
| 6 |
export default function usePyodide(initialSettings: Settings) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
const [plotData, setPlotData] = useState<PlotData>({});
|
| 8 |
const [currentIteration, setCurrentIteration] = useState(0);
|
| 9 |
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
@@ -47,7 +54,12 @@ export default function usePyodide(initialSettings: Settings) {
|
|
| 47 |
|
| 48 |
worker.onmessage = (event) => {
|
| 49 |
const message = event.data as WorkerMessage;
|
| 50 |
-
if (message.type === "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
console.log("Pyodide is ready");
|
| 52 |
setIsLoading(false);
|
| 53 |
sendInit(initialSettings);
|
|
@@ -78,6 +90,7 @@ export default function usePyodide(initialSettings: Settings) {
|
|
| 78 |
}, []);
|
| 79 |
|
| 80 |
return {
|
|
|
|
| 81 |
isLoading,
|
| 82 |
plotData,
|
| 83 |
currentIteration,
|
|
|
|
| 1 |
import { useCallback, useEffect, useRef, useState } from "react";
|
| 2 |
import PyodideWorker from "./pyodide.worker.ts?worker";
|
| 3 |
+
import type { ComparisonTrajectory, PlotData, Settings, WorkerMessage } from "./types.ts";
|
| 4 |
|
| 5 |
|
| 6 |
export default function usePyodide(initialSettings: Settings) {
|
| 7 |
+
const [comparison, setComparison] = useState<ComparisonTrajectory[]>([]);
|
| 8 |
+
const [comparisonBusy, setComparisonBusy] = useState(false);
|
| 9 |
+
const comparisonRequest = useRef(0);
|
| 10 |
+
const compare = useCallback((settings: Settings[], ids: number[], iteration: number) => {
|
| 11 |
+
setComparisonBusy(true);
|
| 12 |
+
workerRef.current?.postMessage({ type: "COMPARE", requestId: ++comparisonRequest.current, settings, ids, iteration });
|
| 13 |
+
}, []);
|
| 14 |
const [plotData, setPlotData] = useState<PlotData>({});
|
| 15 |
const [currentIteration, setCurrentIteration] = useState(0);
|
| 16 |
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
|
|
| 54 |
|
| 55 |
worker.onmessage = (event) => {
|
| 56 |
const message = event.data as WorkerMessage;
|
| 57 |
+
if (message.type === "COMPARISON_RESULT") {
|
| 58 |
+
if (message.requestId === comparisonRequest.current) {
|
| 59 |
+
setComparison(message.trajectories);
|
| 60 |
+
setComparisonBusy(false);
|
| 61 |
+
}
|
| 62 |
+
} else if (message.type === "READY") {
|
| 63 |
console.log("Pyodide is ready");
|
| 64 |
setIsLoading(false);
|
| 65 |
sendInit(initialSettings);
|
|
|
|
| 90 |
}, []);
|
| 91 |
|
| 92 |
return {
|
| 93 |
+
comparison, comparisonBusy, compare,
|
| 94 |
isLoading,
|
| 95 |
plotData,
|
| 96 |
currentIteration,
|