Skip to content

Commit a868f09

Browse files
authored
Merge pull request #80 from classmodel/presets
Add presets
2 parents 6d08922 + 333f79f commit a868f09

30 files changed

+941
-655
lines changed

apps/class-solid/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,16 @@ pnpm test -- --ui --headed
4848
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).
4949

5050
## This project was created with the [Solid CLI](https://solid-cli.netlify.app)
51+
52+
## Presets
53+
54+
An experiment can get started from a preset.
55+
56+
The presets are stored in the `src/lib/presets/` directory.
57+
The format is JSON with content adhering to the [JSON schema](https://github.com/classmodel/class-web/blob/main/packages/class/src/config.json).
58+
59+
The `src/lib/presets.ts` is used as an index of presets.
60+
If you add a preset the `src/lib/presets.ts` file needs to be updated.
61+
62+
An experiment from a preset can be opened from a url like `?preset=<preset-name>`.
63+
For example to load <src/lib/presets/death-valley.json> use `http://localhost:3000/?preset=Death%20Valley`.

apps/class-solid/src/components/Analysis.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { BmiClass } from "@classmodel/class/bmi";
2+
import type { Config } from "@classmodel/class/config";
23
import type { ClassOutput } from "@classmodel/class/runner";
3-
import type { PartialConfig } from "@classmodel/class/validate";
44
import {
55
type Accessor,
66
For,
@@ -59,30 +59,33 @@ interface FlatExperiment {
5959
label: string;
6060
color: string;
6161
linestyle: string;
62-
config: PartialConfig;
62+
config: Config;
6363
output?: ClassOutput;
6464
}
6565

6666
// Create a derived store for looping over all outputs:
6767
const flatExperiments: () => FlatExperiment[] = createMemo(() => {
6868
return experiments
69-
.filter((e) => e.running === false) // skip running experiments
69+
.filter((e) => e.output.running === false) // skip running experiments
7070
.flatMap((e, i) => {
7171
const reference = {
7272
color: colors[0],
7373
linestyle: linestyles[i % 5],
74-
label: e.name,
75-
config: e.reference.config,
76-
output: e.reference.output,
74+
label: e.config.reference.name,
75+
config: e.config.reference,
76+
output: e.output.reference,
7777
};
7878

79-
const permutations = e.permutations.map((p, j) => ({
80-
label: `${e.name}/${p.name}`,
81-
color: colors[(j + 1) % 10],
82-
linestyle: linestyles[i % 5],
83-
config: p.config,
84-
output: p.output,
85-
}));
79+
const permutations = e.config.permutations.map((config, j) => {
80+
const output = e.output.permutations[j];
81+
return {
82+
label: `${e.config.reference.name}/${config.name}`,
83+
color: colors[(j + 1) % 10],
84+
linestyle: linestyles[i % 5],
85+
config,
86+
output,
87+
};
88+
});
8689
return [reference, ...permutations];
8790
});
8891
});

apps/class-solid/src/components/Experiment.tsx

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import {
66
createUniqueId,
77
onCleanup,
88
} from "solid-js";
9+
import { unwrap } from "solid-js/store";
910
import { Button, buttonVariants } from "~/components/ui/button";
1011
import { createArchive, toConfigBlob } from "~/lib/download";
12+
import { findPresetByName } from "~/lib/presets";
1113
import {
1214
type Experiment,
1315
addExperiment,
@@ -46,15 +48,17 @@ export function AddExperimentDialog(props: {
4648
onClose: () => void;
4749
open: boolean;
4850
}) {
49-
const initialExperiment = () => {
51+
const defaultPreset = findPresetByName();
52+
const initialExperimentConfig = createMemo(() => {
5053
return {
51-
name: `My experiment ${props.nextIndex}`,
52-
description: "",
53-
reference: { config: {} },
54+
preset: "Default",
55+
reference: {
56+
...structuredClone(defaultPreset.config),
57+
name: `My experiment ${props.nextIndex}`,
58+
},
5459
permutations: [],
55-
running: false as const,
5660
};
57-
};
61+
});
5862

5963
function setOpen(value: boolean) {
6064
if (!value) {
@@ -66,19 +70,19 @@ export function AddExperimentDialog(props: {
6670
<Dialog open={props.open} onOpenChange={setOpen}>
6771
<DialogContent class="min-w-[33%]">
6872
<DialogHeader>
69-
<DialogTitle class="mr-10">Experiment</DialogTitle>
73+
<DialogTitle class="mr-10 flex justify-between gap-1">
74+
Experiment
75+
<span class="text-gray-300 text-sm ">
76+
Preset: {defaultPreset.config.name}
77+
</span>
78+
</DialogTitle>
7079
</DialogHeader>
7180
<ExperimentConfigForm
7281
id="experiment-form"
73-
experiment={initialExperiment()}
82+
experiment={initialExperimentConfig()}
7483
onSubmit={(newConfig) => {
7584
props.onClose();
76-
const { title, description, ...strippedConfig } = newConfig;
77-
addExperiment(
78-
strippedConfig,
79-
title ?? initialExperiment().name,
80-
description ?? initialExperiment().description,
81-
);
85+
addExperiment(newConfig);
8286
}}
8387
/>
8488
<DialogFooter>
@@ -104,20 +108,19 @@ export function ExperimentSettingsDialog(props: {
104108
</DialogTrigger>
105109
<DialogContent class="min-w-[33%]">
106110
<DialogHeader>
107-
<DialogTitle class="mr-10">Experiment</DialogTitle>
111+
<DialogTitle class="mr-10 flex justify-between gap-1">
112+
Experiment
113+
<span class="text-gray-300 text-sm ">
114+
Preset: {props.experiment.config.preset}
115+
</span>
116+
</DialogTitle>
108117
</DialogHeader>
109118
<ExperimentConfigForm
110119
id="experiment-form"
111-
experiment={props.experiment}
120+
experiment={props.experiment.config}
112121
onSubmit={(newConfig) => {
113122
setOpen(false);
114-
const { title, description, ...strippedConfig } = newConfig;
115-
modifyExperiment(
116-
props.experimentIndex,
117-
strippedConfig,
118-
title ?? props.experiment.name,
119-
description ?? props.experiment.description,
120-
);
123+
modifyExperiment(props.experimentIndex, newConfig);
121124
}}
122125
/>
123126
<DialogFooter>
@@ -164,14 +167,14 @@ function RunningIndicator(props: { progress: number | false }) {
164167

165168
function DownloadExperimentConfiguration(props: { experiment: Experiment }) {
166169
const downloadUrl = createMemo(() => {
167-
return URL.createObjectURL(toConfigBlob(props.experiment));
170+
return URL.createObjectURL(toConfigBlob(unwrap(props.experiment.config)));
168171
});
169172

170173
onCleanup(() => {
171174
URL.revokeObjectURL(downloadUrl());
172175
});
173176

174-
const filename = `class-${props.experiment.name}.json`;
177+
const filename = `class-${props.experiment.config.reference.name}.json`;
175178
return (
176179
<a href={downloadUrl()} download={filename} type="application/json">
177180
Configuration
@@ -182,7 +185,7 @@ function DownloadExperimentConfiguration(props: { experiment: Experiment }) {
182185
function DownloadExperimentArchive(props: { experiment: Experiment }) {
183186
const [url, setUrl] = createSignal<string>("");
184187
createEffect(async () => {
185-
const archive = await createArchive(props.experiment);
188+
const archive = await createArchive(unwrap(props.experiment));
186189
if (!archive) {
187190
return;
188191
}
@@ -191,7 +194,7 @@ function DownloadExperimentArchive(props: { experiment: Experiment }) {
191194
onCleanup(() => URL.revokeObjectURL(objectUrl));
192195
});
193196

194-
const filename = `class-${props.experiment.name}.zip`;
197+
const filename = `class-${props.experiment.config.reference.name}.zip`;
195198
return (
196199
<a href={url()} download={filename} type="application/zip">
197200
Configuration and output
@@ -236,9 +239,9 @@ export function ExperimentCard(props: {
236239
aria-describedby={descriptionId}
237240
>
238241
<CardHeader>
239-
<CardTitle id={id}>{experiment().name}</CardTitle>
242+
<CardTitle id={id}>{experiment().config.reference.name}</CardTitle>
240243
<CardDescription id={descriptionId}>
241-
{experiment().description}
244+
{experiment().config.reference.description}
242245
</CardDescription>
243246
</CardHeader>
244247
<CardContent>
@@ -247,10 +250,10 @@ export function ExperimentCard(props: {
247250
experimentIndex={experimentIndex()}
248251
/>
249252
</CardContent>
250-
<CardFooter>
253+
<CardFooter class="gap-1">
251254
<Show
252-
when={!experiment().running}
253-
fallback={<RunningIndicator progress={experiment().running} />}
255+
when={!experiment().output.running}
256+
fallback={<RunningIndicator progress={experiment().output.running} />}
254257
>
255258
<DownloadExperiment experiment={experiment()} />
256259
<ExperimentSettingsDialog

apps/class-solid/src/components/ExperimentConfigForm.tsx

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
1-
import { pruneDefaults } from "@classmodel/class/validate";
1+
import type { Config } from "@classmodel/class/config";
2+
import { pruneConfig } from "@classmodel/class/config_utils";
23
import { type SubmitHandler, createForm } from "@modular-forms/solid";
34
import { createMemo } from "solid-js";
4-
import type { Experiment } from "~/lib/store";
5-
import {
6-
type NamedConfig,
7-
jsonSchemaOfNamedConfig,
8-
validate,
9-
} from "./NamedConfig";
5+
import { unwrap } from "solid-js/store";
6+
import type { ExperimentConfig } from "~/lib/experiment_config";
7+
import { findPresetByName } from "~/lib/presets";
108
import { ObjectField } from "./ObjectField";
119
import { ajvForm } from "./ajvForm";
1210

@@ -16,24 +14,22 @@ export function ExperimentConfigForm({
1614
onSubmit,
1715
}: {
1816
id: string;
19-
experiment: Experiment;
20-
onSubmit: (c: NamedConfig) => void;
17+
experiment: ExperimentConfig;
18+
onSubmit: (c: Config) => void;
2119
}) {
22-
const initialValues = createMemo(() => {
23-
return {
24-
title: experiment.name,
25-
description: experiment.description,
26-
...pruneDefaults(experiment.reference.config),
27-
};
28-
});
29-
const [_, { Form, Field }] = createForm<NamedConfig>({
20+
const preset = createMemo(() => findPresetByName(experiment.preset));
21+
22+
const initialValues = createMemo(() =>
23+
pruneConfig(unwrap(experiment.reference), unwrap(preset().config)),
24+
);
25+
const [_, { Form, Field }] = createForm<Config>({
3026
initialValues: initialValues(),
31-
validate: ajvForm(validate),
27+
validate: ajvForm(preset().validate),
3228
});
3329

34-
const handleSubmit: SubmitHandler<NamedConfig> = (values, event) => {
35-
// Use validate to coerce strings to numbers
36-
validate(values);
30+
const handleSubmit: SubmitHandler<Config> = (values, event) => {
31+
// Use ajv to coerce strings to numbers and fill in defaults
32+
preset().validate(values);
3733
onSubmit(values);
3834
};
3935

@@ -46,8 +42,8 @@ export function ExperimentConfigForm({
4642
>
4743
<div>
4844
<ObjectField
49-
schema={jsonSchemaOfNamedConfig}
50-
value={pruneDefaults(experiment.reference.config)}
45+
schema={preset().schema}
46+
value={initialValues()}
5147
Field={Field}
5248
/>
5349
</div>

apps/class-solid/src/components/NamedConfig.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.

apps/class-solid/src/components/PermutationSweepButton.tsx

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
1-
import { type Sweep, performSweep } from "@classmodel/class/sweep";
1+
import type { Config } from "@classmodel/class/config";
22
import {
33
type PartialConfig,
4-
overwriteDefaultsInJsonSchema,
5-
} from "@classmodel/class/validate";
6-
import { For, createMemo, createSignal } from "solid-js";
4+
mergeConfigurations,
5+
} from "@classmodel/class/config_utils";
6+
import { type Sweep, performSweep } from "@classmodel/class/sweep";
7+
import { For, createSignal } from "solid-js";
78
import { unwrap } from "solid-js/store";
89
import { Button } from "~/components/ui/button";
9-
import {
10-
type Experiment,
11-
type Permutation,
12-
runExperiment,
13-
setExperiments,
14-
} from "~/lib/store";
15-
import { jsonSchemaOfNamedConfig } from "./NamedConfig";
10+
import { type Experiment, runExperiment, setExperiments } from "~/lib/store";
1611
import {
1712
Dialog,
1813
DialogContent,
@@ -35,28 +30,25 @@ function nameForPermutation(config: PartialConfig): string {
3530
return chunks.join(",");
3631
}
3732

38-
function config2permutation(config: PartialConfig): Permutation {
33+
function config2permutation(reference: Config, config: PartialConfig): Config {
3934
return {
40-
config,
35+
...mergeConfigurations(reference, config),
4136
name: nameForPermutation(config),
37+
description: "",
4238
};
4339
}
4440

45-
function configs2Permutations(configs: PartialConfig[]): Permutation[] {
46-
return configs.map(config2permutation);
41+
function configs2Permutations(
42+
reference: Config,
43+
configs: PartialConfig[],
44+
): Config[] {
45+
return configs.map((c) => config2permutation(reference, c));
4746
}
4847

4948
export function PermutationSweepButton(props: {
5049
experiment: Experiment;
5150
experimentIndex: number;
5251
}) {
53-
const jsonSchemaOfPermutation = createMemo(() => {
54-
return overwriteDefaultsInJsonSchema(
55-
jsonSchemaOfNamedConfig,
56-
unwrap(props.experiment.reference.config),
57-
);
58-
});
59-
6052
const sweeps: Sweep[] = [
6153
{
6254
section: "initialState",
@@ -76,9 +68,12 @@ export function PermutationSweepButton(props: {
7668

7769
function addSweep() {
7870
const configs = performSweep(sweeps);
79-
const perms = configs2Permutations(configs);
71+
const perms = configs2Permutations(
72+
unwrap(props.experiment.config.reference),
73+
configs,
74+
);
8075
setOpen(false);
81-
setExperiments(props.experimentIndex, "permutations", perms);
76+
setExperiments(props.experimentIndex, "config", "permutations", perms);
8277
runExperiment(props.experimentIndex);
8378
}
8479
const [open, setOpen] = createSignal(false);

0 commit comments

Comments
 (0)