nanye commited on
Commit
603b5b4
·
1 Parent(s): 647cf08

support algorithm comparison

Browse files
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 s=`import numpy as np
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 f="https://cdn.jsdelivr.net/pyodide/v0.26.1/full/pyodide.mjs";let e=null,t=null;async function r(){const{loadPyodide:n}=await import(f);e=await n({indexURL:"https://cdn.jsdelivr.net/pyodide/v0.26.1/full/"}),await e.loadPackage(["numpy","sympy"]),e.FS.writeFile("optimization_logic.py",l),e.FS.writeFile("optimization_manager.py",s),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 i(n){if(!n)return null;try{const a=n.toJs({dict_converter:Object.fromEntries});n.destroy&&n.destroy(),self.postMessage({type:"RESULT",data:a})}catch(a){console.error("Error handling Python result:",a)}}self.onmessage=async n=>{const a=n.data;if(!t){console.warn("Pyodide is not ready yet");return}switch(a.type){case"INIT":const o=e.toPy(a.settings);i(t.handle_update_settings(o));break;case"NEXT_STEP":i(t.handle_next_step());break;case"PREV_STEP":i(t.handle_prev_step());break;case"RESET":i(t.handle_reset());break;default:console.error("Unknown message type:",a);break}},r()})();
 
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-DxXDp_Rq.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Bs9s2ktO.css">
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 type { SettingsUi } from "./types.ts";
 
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 { SUPPORTED_ALGORITHMS, type SettingsUi, type TrajectoryValues } from "./types.ts";
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
- if (key === "algorithm") {
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
- <div className="grid grid-cols-[6rem_minmax(0,1fr)] items-center gap-0.5">
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 === "READY") {
 
 
 
 
 
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,