Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add presets #80

Draft
wants to merge 7 commits into
base: 61-persist-app-state
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/class-solid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,14 @@ pnpm test -- --ui --headed
This allows you to trigger tests from the [playwright ui](https://playwright.dev/docs/test-ui-mode) and enable [watch mode](https://playwright.dev/docs/test-ui-mode#watch-mode).

## This project was created with the [Solid CLI](https://solid-cli.netlify.app)

## Presets

An experiment can get started from a preset.

The presets are stored in the `public/presets/` directory.
The format is JSON with title, desscription, reference, permutations keys.
It is the same format as downloading a configuration file from an existing experiment.

The `public/presets/index.json` is used as an index of presets.
If you add or rename a preset the `public/presets/index.json` file needs to be updated.
28 changes: 28 additions & 0 deletions apps/class-solid/public/presets/death-valley.v1.0.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "Death Valley",
"description": "Preset with Death Valley conditions",
"reference": {
"initialState": {
"theta_0": 323,
"h_0": 200,
"dtheta_0": 1,
"q_0": 0.008,
"dq_0": -0.001
},
"timeControl": {
"dt": 60,
"runtime": 4320
},
"mixedLayer": {
"wtheta": 0.1,
"advtheta": 0,
"gammatheta": 0.006,
"wq": 0.0001,
"advq": 0,
"gammaq": 0,
"divU": 0,
"beta": 0.2
}
},
"permutations": []
}
28 changes: 28 additions & 0 deletions apps/class-solid/public/presets/default.v1.0.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "Default",
"description": "The classic default configuration",
"reference": {
"initialState": {
"h_0": 200,
"theta_0": 288,
"dtheta_0": 1,
"q_0": 0.008,
"dq_0": -0.001
},
"timeControl": {
"dt": 60,
"runtime": 43200
},
"mixedLayer": {
"wtheta": 0.1,
"advtheta": 0,
"gammatheta": 0.006,
"wq": 0.0001,
"advq": 0,
"gammaq": 0,
"divU": 0,
"beta": 0.2
}
},
"permutations": []
}
1 change: 1 addition & 0 deletions apps/class-solid/public/presets/index.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
["/presets/default.v1.0.0.json", "/presets/death-valley.v1.0.0.json"]
23 changes: 22 additions & 1 deletion apps/class-solid/src/components/Experiment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ import {
} from "~/lib/store";
import { ExperimentConfigForm } from "./ExperimentConfigForm";
import { PermutationsList } from "./PermutationsList";
import { MdiCog, MdiContentCopy, MdiDelete, MdiDownload } from "./icons";
import {
MdiCog,
MdiContentCopy,
MdiDelete,
MdiDownload,
MdiStar,
} from "./icons";
import {
Card,
CardContent,
Expand Down Expand Up @@ -53,6 +59,8 @@ export function AddExperimentDialog(props: {
reference: { config: {} },
permutations: [],
running: false as const,
// TODO make sure preset ends up in store
preset: "/presets/default.v1.0.0.json",
};
};

Expand Down Expand Up @@ -279,6 +287,19 @@ export function ExperimentCard(props: {
>
<MdiDelete />
</Button>
<Show when={experiment().preset}>
<div class="text-[#888]">
<a
href={`?preset=${encodeURI(experiment().preset ?? "")}`}
target="_blank"
rel="noreferrer"
class={buttonVariants({ variant: "outline" })}
title="Preset link of experiment"
>
<MdiStar />
</a>
</div>
</Show>
</Show>
</CardFooter>
</Card>
Expand Down
40 changes: 38 additions & 2 deletions apps/class-solid/src/components/StartButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Show, createSignal } from "solid-js";
import { For, Show, createResource, createSignal } from "solid-js";
import { presetCatalog } from "~/lib/presets";
import { hasLocalStorage, loadFromLocalStorage } from "~/lib/state";
import { experiments, uploadExperiment } from "~/lib/store";
import {
Expand All @@ -16,6 +17,7 @@ import {
DialogTitle,
DialogTrigger,
} from "./ui/dialog";
import { Flex } from "./ui/flex";
import { showToast } from "./ui/toast";

function ResumeSessionButton(props: { afterClick: () => void }) {
Expand Down Expand Up @@ -144,6 +146,9 @@ function StartFromUploadButton(props: {
);
}

// Only load presets once and keep them in memory
const [presets] = createResource(() => presetCatalog());

function PresetPicker(props: {
open: boolean;
setOpen: (open: boolean) => void;
Expand All @@ -154,7 +159,38 @@ function PresetPicker(props: {
<DialogHeader>
<DialogTitle class="mr-10">Pick a preset</DialogTitle>
</DialogHeader>
<p>Presets are not implemented yet</p>
<Flex justifyContent="center" class="flex-wrap gap-4">
<For each={presets()}>
{(preset) => (
<Button
variant="outline"
class="flex h-44 w-56 flex-col gap-2 border border-dashed"
onClick={() => {
props.setOpen(false);
uploadExperiment(preset)
.then(() => {
showToast({
title: "Experiment preset loaded",
variant: "success",
duration: 1000,
});
})
.catch((error) => {
console.error(error);
showToast({
title: "Failed to load preset",
description: `${error}`,
variant: "error",
});
});
}}
>
<h1 class="text-xl">{preset.name}</h1>
<div>{preset.description}</div>
</Button>
)}
</For>
</Flex>
</DialogContent>
</Dialog>
);
Expand Down
18 changes: 18 additions & 0 deletions apps/class-solid/src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -319,3 +319,21 @@ export function MdiFileDocumentOutline(props: JSX.IntrinsicElements["svg"]) {
</svg>
);
}

export function MdiStar(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 24 24"
{...props}
>
<title>Star</title>
<path
fill="currentColor"
d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.62L12 2L9.19 8.62L2 9.24l5.45 4.73L5.82 21z"
/>
</svg>
);
}
6 changes: 5 additions & 1 deletion apps/class-solid/src/lib/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { BlobReader, BlobWriter, ZipWriter } from "@zip.js/zip.js";
import { type Experiment, outputForExperiment } from "./store";

export function toConfig(experiment: Experiment): ExperimentConfigSchema {
return {
const e: ExperimentConfigSchema = {
name: experiment.name,
description: experiment.description,
reference: experiment.reference.config,
Expand All @@ -15,6 +15,10 @@ export function toConfig(experiment: Experiment): ExperimentConfigSchema {
};
}),
};
if (experiment.preset) {
e.preset = experiment.preset;
}
return e;
}

export function toConfigBlob(experiment: Experiment) {
Expand Down
6 changes: 5 additions & 1 deletion apps/class-solid/src/lib/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import type { Analysis, Experiment } from "./store";

export function decodeAppState(encoded: string): [Experiment[], Analysis[]] {
const decoded = decodeURI(encoded);

const parsed = JSON.parse(decoded);
// TODO use ajv to validate experiment, permutation, and analysis
// now only config is validated
const experiments: Experiment[] = parsed.experiments.map(
(exp: {
name: string;
description: string;
description?: string;
preset?: string;
reference: unknown;
permutations: Record<string, unknown>;
}) => ({
name: exp.name,
description: exp.description,
preset: exp.preset,
reference: {
config: parse(exp.reference),
},
Expand All @@ -39,6 +42,7 @@ export function encodeAppState(
name: exp.name,
description: exp.description,
reference: pruneDefaults(exp.reference.config),
preset: exp.preset,
permutations: Object.fromEntries(
exp.permutations.map((perm) => [
perm.name,
Expand Down
35 changes: 35 additions & 0 deletions apps/class-solid/src/lib/presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
type ExperimentConfigSchema,
parseExperimentConfig,
} from "@classmodel/class/validate";

function absoluteUrl(rawUrl: string) {
let url = rawUrl;
if (window) {
// TODO add host:port to url
}
if (rawUrl.startsWith("/") && import.meta.env.BASE_URL !== "/_build") {
url = import.meta.env.BASE_URL + rawUrl;
}
return url;
}

export async function presetCatalog(rawUrl = "/presets/index.json") {
// TODO use /presets.json route which is materialized during build
// TODO or generate complete catalog with pnpm generate:presets to src/presets.json
const url = absoluteUrl(rawUrl);
const response = await fetch(url);
const presetUrls = (await response.json()) as string[];
return await Promise.all(presetUrls.map(loadPreset));
}

export async function loadPreset(
rawUrl: string,
): Promise<ExperimentConfigSchema> {
const url = absoluteUrl(rawUrl);
const response = await fetch(url);
const json = await response.json();
const config = parseExperimentConfig(json);
config.preset = url;
return config;
}
33 changes: 32 additions & 1 deletion apps/class-solid/src/lib/state.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { useLocation, useNavigate } from "@solidjs/router";
import { showToast } from "~/components/ui/toast";
import { encodeAppState } from "./encode";
import { analyses, experiments, loadStateFromString } from "./store";
import { loadPreset } from "./presets";
import {
analyses,
experiments,
loadStateFromString,
uploadExperiment,
} from "./store";

const localStorageName = "class-state";

Expand Down Expand Up @@ -38,6 +44,10 @@ export function loadFromLocalStorage() {
export async function onPageLoad() {
const location = useLocation();
const navigate = useNavigate();
const presetUrl = location.query.preset;
if (presetUrl) {
return await loadExperimentPreset(presetUrl);
}
const rawState = location.hash.substring(1);
if (!rawState) {
return;
Expand All @@ -63,6 +73,27 @@ export async function onPageLoad() {
navigate("/");
}

async function loadExperimentPreset(presetUrl: string) {
const navigate = useNavigate();
try {
const preset = await loadPreset(presetUrl);
await uploadExperiment(preset);
showToast({
title: "Experiment preset loaded",
variant: "success",
duration: 1000,
});
} catch (error) {
console.error(error);
showToast({
title: "Failed to load preset",
description: `${error}`,
variant: "error",
});
}
navigate("/");
}

export function saveToLocalStorage() {
const appState = encodeAppState(experiments, analyses);
if (
Expand Down
4 changes: 4 additions & 0 deletions apps/class-solid/src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface Permutation<C extends PartialConfig = PartialConfig> {
export interface Experiment {
name: string;
description: string;
preset?: string;
reference: {
// TODO change reference.config to config, as there are no other keys in reference
config: PartialConfig;
Expand Down Expand Up @@ -153,6 +154,9 @@ export async function uploadExperiment(rawData: unknown) {
permutations: upload.permutations,
running: false,
};
if (upload.preset) {
experiment.preset = upload.preset;
}
setExperiments(experiments.length, experiment);
await runExperiment(experiments.length - 1);
}
Expand Down
6 changes: 4 additions & 2 deletions packages/class/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ The CLASS web application that uses this package is available at https://classmo
The class model can be run from the command line.

```shell
# Generate default config file
pnpx @classmodel/class generate --output config.json
# Fetch default config file
wget https://classmodel.github.io/class-web/presets/default.v1.0.0.json

# Edit the config file

# Run the model
pnpx @classmodel/class run config.json
Expand Down
1 change: 0 additions & 1 deletion packages/class/src/class.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ describe("CLASS model", () => {
test("produces realistic results", () => {
const config = parse({});
const output = runClass(config);
console.log(output);
assert.ok(output);
});
});
Loading