diff --git a/apps/class-solid/src/components/Analysis.tsx b/apps/class-solid/src/components/Analysis.tsx
index cf43f18..3b0b526 100644
--- a/apps/class-solid/src/components/Analysis.tsx
+++ b/apps/class-solid/src/components/Analysis.tsx
@@ -1,7 +1,6 @@
-import { For, Match, Switch, createMemo, createUniqueId } from "solid-js";
+import { For, Match, Show, Switch, createMemo, createUniqueId } from "solid-js";
import { getVerticalProfiles } from "~/lib/profiles";
-import { analyses, experiments, setAnalyses } from "~/lib/store";
-import type { Experiment } from "~/lib/store";
+import { type Analysis, deleteAnalysis, experiments } from "~/lib/store";
import LinePlot from "./LinePlot";
import { MdiCog, MdiContentCopy, MdiDelete, MdiDownload } from "./icons";
import { Button } from "./ui/button";
@@ -24,54 +23,30 @@ const colors = [
const linestyles = ["none", "5,5", "10,10", "15,5,5,5", "20,10,5,5,5,10"];
-export interface Analysis {
- name: string;
- description: string;
- id: string;
- experiments: Experiment[] | undefined;
- type: string;
-}
-
-export function addAnalysis(type = "default") {
- const name = {
- default: "Final height",
- timeseries: "Timeseries",
- profiles: "Vertical profiles",
- }[type];
-
- setAnalyses(analyses.length, {
- name: name,
- id: createUniqueId(),
- experiments: experiments,
- type: type,
- });
-}
-
-function deleteAnalysis(analysis: Analysis) {
- setAnalyses(analyses.filter((ana) => ana.id !== analysis.id));
-}
-
/** Very rudimentary plot showing time series of each experiment globally available
* It only works if the time axes are equal
*/
export function TimeSeriesPlot() {
const chartData = createMemo(() => {
return experiments
- .filter((e) => e.reference.output)
+ .filter((e) => e.running === false) // Skip running experiments
.flatMap((e, i) => {
- const permutationRuns = e.permutations.map((perm, j) => {
- return {
- label: `${e.name}/${perm.name}`,
- y: perm.output === undefined ? [] : perm.output.h,
- x: perm.output === undefined ? [] : perm.output.t,
- color: colors[(j + 1) % 10],
- linestyle: linestyles[i % 5],
- };
- });
+ const experimentOutput = e.reference.output;
+ const permutationRuns = e.permutations
+ .filter((perm) => perm.output !== undefined)
+ .map((perm, j) => {
+ return {
+ label: `${e.name}/${perm.name}`,
+ y: perm.output?.h ?? [],
+ x: perm.output?.t ?? [],
+ color: colors[(j + 1) % 10],
+ linestyle: linestyles[i % 5],
+ };
+ });
return [
{
- y: e.reference.output === undefined ? [] : e.reference.output.h,
- x: e.reference.output === undefined ? [] : e.reference.output.t,
+ y: experimentOutput?.h ?? [],
+ x: experimentOutput?.t ?? [],
label: e.name,
color: colors[0],
linestyle: linestyles[i],
@@ -94,33 +69,40 @@ export function VerticalProfilePlot() {
const variable = "theta";
const time = -1;
const profileData = createMemo(() => {
- return experiments.flatMap((e, i) => {
- const permutations = e.permutations.map((p, j) => {
- // TODO get additional config info from reference
- // permutations probably usually don't have gammaq/gammatetha set?
- return {
- color: colors[(j + 1) % 10],
- linestyle: linestyles[i % 5],
- label: `${e.name}/${p.name}`,
- ...getVerticalProfiles(p.output, p.config, variable, time),
- };
- });
+ return experiments
+ .filter((e) => e.running === false) // Skip running experiments
+ .flatMap((e, i) => {
+ const permutations = e.permutations.map((p, j) => {
+ // TODO get additional config info from reference
+ // permutations probably usually don't have gammaq/gammatetha set?
+ return {
+ color: colors[(j + 1) % 10],
+ linestyle: linestyles[i % 5],
+ label: `${e.name}/${p.name}`,
+ ...getVerticalProfiles(p.output, p.config, variable, time),
+ };
+ });
- return [
- {
- label: e.name,
- color: colors[0],
- linestyle: linestyles[i],
- ...getVerticalProfiles(
- e.reference.output,
- e.reference.config,
- variable,
- time,
- ),
- },
- ...permutations,
- ];
- });
+ return [
+ {
+ label: e.name,
+ color: colors[0],
+ linestyle: linestyles[i],
+ ...getVerticalProfiles(
+ e.reference.output ?? {
+ t: [],
+ h: [],
+ theta: [],
+ dtheta: [],
+ },
+ e.reference.config,
+ variable,
+ time,
+ ),
+ },
+ ...permutations,
+ ];
+ });
});
return (
{(experiment) => {
- const h = () =>
- experiment.reference.output?.h[
- experiment.reference.output.h.length - 1
- ] || 0;
+ const h = () => {
+ const experimentOutput = experiment.reference.output;
+ return experimentOutput?.h[experimentOutput?.h.length - 1] || 0;
+ };
return (
- <>
+
{experiment.name}: {h().toFixed()} m
{(perm) => {
- const h = () => perm.output?.h[perm.output.h.length - 1] || 0;
+ const h = () => {
+ const permOutput = perm.output;
+ return permOutput?.h?.length
+ ? permOutput.h[permOutput.h.length - 1]
+ : 0;
+ };
return (
{experiment.name}/{perm.name}: {h().toFixed()} m
@@ -156,7 +143,7 @@ function FinalHeights() {
);
}}
- >
+
);
}}
@@ -196,7 +183,7 @@ export function AnalysisCard(analysis: Analysis) {
Unknown analysis type
}>
-
+
diff --git a/apps/class-solid/src/components/Experiment.tsx b/apps/class-solid/src/components/Experiment.tsx
index 020505e..11333b3 100644
--- a/apps/class-solid/src/components/Experiment.tsx
+++ b/apps/class-solid/src/components/Experiment.tsx
@@ -17,7 +17,6 @@ import {
} from "~/lib/store";
import { ExperimentConfigForm } from "./ExperimentConfigForm";
import { PermutationsList } from "./PermutationsList";
-import { ShareButton } from "./ShareButton";
import { MdiCog, MdiContentCopy, MdiDelete, MdiDownload } from "./icons";
import {
Card,
@@ -53,7 +52,7 @@ export function AddExperimentDialog(props: {
description: "",
reference: { config: {} },
permutations: [],
- running: false,
+ running: false as const,
};
};
@@ -131,14 +130,15 @@ export function ExperimentSettingsDialog(props: {
);
}
-function RunningIndicator() {
+function RunningIndicator(props: { progress: number | false }) {
return (
-
+
- Running ...
+
+ Running {props.progress ? (props.progress * 100).toFixed() : 100}% ...
+
);
}
@@ -181,6 +183,9 @@ function DownloadExperimentArchive(props: { experiment: Experiment }) {
const [url, setUrl] = createSignal
("");
createEffect(async () => {
const archive = await createArchive(props.experiment);
+ if (!archive) {
+ return;
+ }
const objectUrl = URL.createObjectURL(archive);
setUrl(objectUrl);
onCleanup(() => URL.revokeObjectURL(objectUrl));
@@ -243,7 +248,10 @@ export function ExperimentCard(props: {
/>
- }>
+ }
+ >
-
diff --git a/apps/class-solid/src/components/Nav.tsx b/apps/class-solid/src/components/Nav.tsx
index ca8cc53..d521655 100644
--- a/apps/class-solid/src/components/Nav.tsx
+++ b/apps/class-solid/src/components/Nav.tsx
@@ -1,4 +1,7 @@
import { useLocation } from "@solidjs/router";
+import { saveToLocalStorage } from "~/lib/state";
+import { ShareButton } from "./ShareButton";
+import { MdiContentSave } from "./icons";
export default function Nav() {
const location = useLocation();
@@ -8,13 +11,27 @@ export default function Nav() {
: "border-transparent hover:border-sky-600";
return (
);
diff --git a/apps/class-solid/src/components/PermutationSweepButton.tsx b/apps/class-solid/src/components/PermutationSweepButton.tsx
new file mode 100644
index 0000000..d83f0f0
--- /dev/null
+++ b/apps/class-solid/src/components/PermutationSweepButton.tsx
@@ -0,0 +1,125 @@
+import { type Sweep, performSweep } from "@classmodel/class/sweep";
+import {
+ type PartialConfig,
+ overwriteDefaultsInJsonSchema,
+} from "@classmodel/class/validate";
+import { For, createMemo, createSignal } from "solid-js";
+import { unwrap } from "solid-js/store";
+import { Button } from "~/components/ui/button";
+import {
+ type Experiment,
+ type Permutation,
+ runExperiment,
+ setExperiments,
+} from "~/lib/store";
+import { jsonSchemaOfNamedConfig } from "./NamedConfig";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "./ui/dialog";
+
+function nameForPermutation(config: PartialConfig): string {
+ const chunks = [];
+ for (const [section, params] of Object.entries(config)) {
+ const paramChunks = [];
+ for (const [param, value] of Object.entries(params)) {
+ paramChunks.push(`${param}=${value}`);
+ }
+ // Add section?
+ chunks.push(paramChunks.join(","));
+ }
+ return chunks.join(",");
+}
+
+function config2permutation(config: PartialConfig): Permutation {
+ return {
+ config,
+ name: nameForPermutation(config),
+ };
+}
+
+function configs2Permutations(configs: PartialConfig[]): Permutation[] {
+ return configs.map(config2permutation);
+}
+
+export function PermutationSweepButton(props: {
+ experiment: Experiment;
+ experimentIndex: number;
+}) {
+ const jsonSchemaOfPermutation = createMemo(() => {
+ return overwriteDefaultsInJsonSchema(
+ jsonSchemaOfNamedConfig,
+ unwrap(props.experiment.reference.config),
+ );
+ });
+
+ const sweeps: Sweep[] = [
+ {
+ section: "initialState",
+ parameter: "h_0",
+ start: 100,
+ step: 100,
+ steps: 5,
+ },
+ {
+ section: "mixedLayer",
+ parameter: "beta",
+ start: 0.1,
+ step: 0.1,
+ steps: 5,
+ },
+ ];
+
+ function addSweep() {
+ const configs = performSweep(sweeps);
+ const perms = configs2Permutations(configs);
+ setOpen(false);
+ setExperiments(props.experimentIndex, "permutations", perms);
+ runExperiment(props.experimentIndex);
+ }
+ const [open, setOpen] = createSignal(false);
+ return (
+
+ );
+}
diff --git a/apps/class-solid/src/components/PermutationsList.tsx b/apps/class-solid/src/components/PermutationsList.tsx
index 19dfcd5..e249f13 100644
--- a/apps/class-solid/src/components/PermutationsList.tsx
+++ b/apps/class-solid/src/components/PermutationsList.tsx
@@ -21,6 +21,7 @@ import {
validate,
} from "./NamedConfig";
import { ObjectField } from "./ObjectField";
+import { PermutationSweepButton } from "./PermutationSweepButton";
import { ajvForm } from "./ajvForm";
import {
MdiCakeVariantOutline,
@@ -318,8 +319,12 @@ export function PermutationsList(props: {
experiment={props.experiment}
experimentIndex={props.experimentIndex}
/>
+
-
+
{(perm, permutationIndex) => (
-
diff --git a/apps/class-solid/src/components/ShareButton.tsx b/apps/class-solid/src/components/ShareButton.tsx
index 45e0109..35f2706 100644
--- a/apps/class-solid/src/components/ShareButton.tsx
+++ b/apps/class-solid/src/components/ShareButton.tsx
@@ -1,7 +1,7 @@
-import { type Accessor, Show, createMemo, createSignal } from "solid-js";
+import { Show, createMemo, createSignal } from "solid-js";
import { Button } from "~/components/ui/button";
-import { encodeExperiment } from "~/lib/encode";
-import type { Experiment } from "~/lib/store";
+import { encodeAppState } from "~/lib/encode";
+import { analyses, experiments } from "~/lib/store";
import {
MdiClipboard,
MdiClipboardCheck,
@@ -10,7 +10,6 @@ import {
import {
Dialog,
DialogContent,
- DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
@@ -18,7 +17,9 @@ import {
import { TextField, TextFieldInput } from "./ui/text-field";
import { showToast } from "./ui/toast";
-export function ShareButton(props: { experiment: Accessor }) {
+const MAX_SHAREABLE_LINK_LENGTH = 32_000;
+
+export function ShareButton() {
const [open, setOpen] = createSignal(false);
const [isCopied, setIsCopied] = createSignal(false);
let inputRef: HTMLInputElement | undefined;
@@ -26,8 +27,9 @@ export function ShareButton(props: { experiment: Accessor }) {
if (!open()) {
return "";
}
- const encodedExperiment = encodeExperiment(props.experiment());
- const url = `${window.location.origin}#${encodedExperiment}`;
+
+ const appState = encodeAppState(experiments, analyses);
+ const url = `${window.location.origin}#${appState}`;
return url;
});
@@ -57,50 +59,68 @@ export function ShareButton(props: { experiment: Accessor }) {
return (