diff --git a/README.md b/README.md
index c0184d4..27fc3eb 100644
--- a/README.md
+++ b/README.md
@@ -117,10 +117,8 @@ by [Kobalte](https://kobalte.dev/docs/core/overview/introduction) and
application and tweaked further as seen fit. It can also do charts, using
[chart.js](https://www.chartjs.org/), though we might deviate from that later.
-To expose the model in a standard way we use the [Basic Model Interface (BMI)](https://bmi.readthedocs.io/).
-
To prevent the user interface from getting blocked by running the model we use a [Web worker](https://developer.mozilla.org/en-US/docs/Web/API/Worker) to run the computation in a background task/thread.
-A Web worker uses messages passing to communicate, this does not fit with the Basic Model Interface so we use [comlink](https://github.com/GoogleChromeLabs/comlink) to wrap the Web Worker in a BMI class.
+We use [comlink](https://github.com/GoogleChromeLabs/comlink) to wrap the Web Worker so it behaves the same as if the runner was used directly inside the main thread.
To format and lint the code, we use [biome](https://biomejs.dev/) as it combines eslint, prettier in one package.
diff --git a/apps/class-solid/src/components/Analysis.tsx b/apps/class-solid/src/components/Analysis.tsx
index 76d74a5..95288f9 100644
--- a/apps/class-solid/src/components/Analysis.tsx
+++ b/apps/class-solid/src/components/Analysis.tsx
@@ -1,6 +1,6 @@
-import { BmiClass } from "@classmodel/class/bmi";
import type { Config } from "@classmodel/class/config";
-import type { ClassOutput } from "@classmodel/class/runner";
+import { type ClassOutput, outputVariables } from "@classmodel/class/runner";
+import * as d3 from "d3";
import { saveAs } from "file-saver";
import { toBlob } from "html-to-image";
import {
@@ -115,9 +115,15 @@ const uniqueTimes = () => [...new Set(_allTimes())].sort((a, b) => a - b);
// TODO: could memoize all reactive elements here, would it make a difference?
export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
- const xVariableOptions = ["t"]; // TODO: separate plot types for timeseries and x-vs-y? Use time axis?
- // TODO: add nice description from config as title and dropdown option for the variable picker.
- const yVariableOptions = new BmiClass().get_output_var_names();
+ const symbols = Object.fromEntries(
+ outputVariables.map((v) => [v.key, v.symbol]),
+ );
+ const getKey = Object.fromEntries(
+ outputVariables.map((v) => [v.symbol, v.key]),
+ );
+ const labels = Object.fromEntries(
+ outputVariables.map((v) => [v.key, `${v.symbol} [${v.unit}]`]),
+ );
const allX = () =>
flatExperiments().flatMap((e) =>
@@ -128,7 +134,8 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
e.output ? e.output[analysis.yVariable] : [],
);
- const xLim = () => getNiceAxisLimits(allX(), 0, 600);
+ const granularity = () => (analysis.xVariable === "t" ? 600 : undefined);
+ const xLim = () => getNiceAxisLimits(allX(), 0, granularity());
const yLim = () => getNiceAxisLimits(allY());
const chartData = () =>
@@ -158,14 +165,34 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
setToggles(label, value);
}
+ const setXVar = (symbol: string) => {
+ updateAnalysis(analysis, { xVariable: getKey[symbol] });
+ setResetPlot(analysis.id);
+ };
+
+ const setYVar = (symbol: string) => {
+ updateAnalysis(analysis, { yVariable: getKey[symbol] });
+ setResetPlot(analysis.id);
+ };
+
+ const formatX = () =>
+ analysis.xVariable === "t" ? formatSeconds : d3.format(".4");
+ const formatY = () =>
+ analysis.yVariable === "t" ? formatSeconds : d3.format(".4");
+
return (
<>
{/* TODO: get label for yVariable from model config */}
-
-
-
+
+
+
{(d) => (
@@ -177,15 +204,15 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
analysis.xVariable}
- setValue={(v) => updateAnalysis(analysis, { xVariable: v })}
- options={xVariableOptions}
+ value={() => symbols[analysis.xVariable]}
+ setValue={(v) => setXVar(v)}
+ options={Object.values(symbols)}
label="x-axis"
/>
analysis.yVariable}
- setValue={(v) => updateAnalysis(analysis, { yVariable: v })}
- options={yVariableOptions}
+ value={() => symbols[analysis.yVariable]}
+ setValue={(v) => setYVar(v)}
+ options={Object.values(symbols)}
label="y-axis"
/>
@@ -261,6 +288,11 @@ export function VerticalProfilePlot({
setToggles(label, value);
}
+ function changeVar(v: string) {
+ updateAnalysis(analysis, { variable: v });
+ setResetPlot(analysis.id);
+ }
+
return (
<>
@@ -291,7 +323,7 @@ export function VerticalProfilePlot({
analysis.variable}
- setValue={(v) => updateAnalysis(analysis, { variable: v })}
+ setValue={(v) => changeVar(v)}
options={Object.keys(variableOptions)}
label="variable: "
/>
@@ -307,7 +339,7 @@ export function VerticalProfilePlot({
type PickerProps = {
value: Accessor;
- setValue: Setter;
+ setValue: (value: string) => void;
options: string[];
label?: string;
};
diff --git a/apps/class-solid/src/components/plots/ChartContainer.tsx b/apps/class-solid/src/components/plots/ChartContainer.tsx
index 4cd50a6..02a3859 100644
--- a/apps/class-solid/src/components/plots/ChartContainer.tsx
+++ b/apps/class-solid/src/components/plots/ChartContainer.tsx
@@ -119,8 +119,8 @@ export function Chart(props: {
children: JSX.Element;
id: string;
title?: string;
- formatX?: (value: number) => string;
- formatY?: (value: number) => string;
+ formatX?: () => (value: number) => string;
+ formatY?: () => (value: number) => string;
transformX?: (x: number, y: number, scaleY: SupportedScaleTypes) => number;
}) {
const [hovering, setHovering] = createSignal(false);
@@ -142,12 +142,17 @@ export function Chart(props: {
}
});
- if (props.formatX) {
- updateChart("formatX", () => props.formatX);
- }
- if (props.formatY) {
- updateChart("formatY", () => props.formatY);
- }
+ createEffect(() => {
+ if (props.formatX) {
+ updateChart("formatX", () => props.formatX?.());
+ }
+ });
+ createEffect(() => {
+ if (props.formatY) {
+ updateChart("formatY", () => props.formatY?.());
+ }
+ });
+
if (props.transformX) {
updateChart("transformX", () => props.transformX);
}
diff --git a/apps/class-solid/src/components/plots/skewTlogP.tsx b/apps/class-solid/src/components/plots/skewTlogP.tsx
index 6ec0e7d..684fd82 100644
--- a/apps/class-solid/src/components/plots/skewTlogP.tsx
+++ b/apps/class-solid/src/components/plots/skewTlogP.tsx
@@ -192,8 +192,8 @@ export function SkewTPlot(props: {
d3.format(".0d")}
+ formatY={() => d3.format(".0d")}
transformX={getTempAtCursor}
>
(worker);
-export async function runClass(config: Config): Promise {
+const asyncRunner = wrap(worker);
+
+export async function runClassAsync(config: Config): Promise {
try {
- const parsedConfig: Config = parse(config);
- const model = await new AsyncBmiClass();
- await model.initialize(parsedConfig);
- const output = await model.run({
- var_names: new BmiClass().get_output_var_names(),
- });
+ const output = asyncRunner(config);
return output;
} catch (error) {
console.error({ config, error });
diff --git a/apps/class-solid/src/lib/store.ts b/apps/class-solid/src/lib/store.ts
index 82d7404..d737a15 100644
--- a/apps/class-solid/src/lib/store.ts
+++ b/apps/class-solid/src/lib/store.ts
@@ -12,7 +12,7 @@ import { decodeAppState } from "./encode";
import { parseExperimentConfig } from "./experiment_config";
import type { ExperimentConfig } from "./experiment_config";
import { findPresetByName } from "./presets";
-import { runClass } from "./runner";
+import { runClassAsync } from "./runner";
interface ExperimentOutput {
reference?: ClassOutput;
@@ -38,7 +38,7 @@ export async function runExperiment(id: number) {
// Run reference
const referenceConfig = unwrap(exp.config.reference);
- const newOutput = await runClass(referenceConfig);
+ const newOutput = await runClassAsync(referenceConfig);
setExperiments(id, "output", "reference", newOutput);
@@ -50,7 +50,7 @@ export async function runExperiment(id: number) {
referenceConfig,
permConfig,
) as Config;
- const newOutput = await runClass(combinedConfig);
+ const newOutput = await runClassAsync(combinedConfig);
setExperiments(id, "output", "permutations", permCounter, newOutput);
permCounter++;
}
diff --git a/apps/class-solid/src/lib/worker.ts b/apps/class-solid/src/lib/worker.ts
index c1549b7..e66de6b 100644
--- a/apps/class-solid/src/lib/worker.ts
+++ b/apps/class-solid/src/lib/worker.ts
@@ -1,4 +1,4 @@
-import { BmiClass } from "@classmodel/class/bmi";
+import { runClass } from "@classmodel/class/runner";
import { expose } from "comlink";
-expose(BmiClass);
+expose(runClass);
diff --git a/packages/class/package.json b/packages/class/package.json
index c2e756a..4f3c14c 100644
--- a/packages/class/package.json
+++ b/packages/class/package.json
@@ -36,12 +36,6 @@
"types": "./dist/runner.d.ts"
}
},
- "./bmi": {
- "import": {
- "default": "./dist/bmi.js",
- "types": "./dist/bmi.d.ts"
- }
- },
"./validate": {
"import": {
"default": "./dist/validate.js",
diff --git a/packages/class/src/bmi.test.ts b/packages/class/src/bmi.test.ts
deleted file mode 100644
index c108f60..0000000
--- a/packages/class/src/bmi.test.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import assert from "node:assert/strict";
-import { before, describe, test } from "node:test";
-import { BmiClass } from "./bmi.js";
-
-describe("BmiClass", () => {
- let bmi: BmiClass;
- before(() => {
- bmi = new BmiClass();
- });
-
- describe("get_component_name", () => {
- test("returns the component name", () => {
- assert.strictEqual(
- bmi.get_component_name(),
- "Chemistry Land-surface Atmosphere Soil Slab model",
- );
- });
- });
-});
diff --git a/packages/class/src/bmi.ts b/packages/class/src/bmi.ts
deleted file mode 100644
index 0c29ee6..0000000
--- a/packages/class/src/bmi.ts
+++ /dev/null
@@ -1,162 +0,0 @@
-/**
- * This module contains the Basic Modelling Interface class.
- * @module
- */
-import { CLASS } from "./class.js";
-import type { Config } from "./config.js";
-import { parse } from "./validate.js";
-
-/**
- * A lightweight [Basic Modelling Interface (BMI)](https://bmi.readthedocs.io) like interface for the CLASS model.
- *
- * Inspiration https://github.com/uihilab/BMI-JS/blob/main/bmijs/bmi.js
- *
- * Deviations from the BMI standard
- * - accessors do not use dest argument
- * - initialize() accepts object instead of string
- * - parameters() returns default config
- * - run() as extra method
- */
-interface BmiLight {
- initialize(config: Config): void;
- update(): void;
- get_component_name(): string;
- get_output_item_count(): number;
- get_output_var_names(): string[];
- get_var_grid(name: string): number;
- get_var_type(name: string): string;
- get_var_location(name: string): string;
- get_current_time(): number;
- get_end_time(): number;
- get_time_units(): string;
- get_time_step(): number;
- get_value(name: string): number[];
- get_grid_type(): string;
-}
-
-const ouput_var_names: string[] = [
- "h",
- "theta",
- "dtheta",
- "q",
- "dq",
- "dthetav",
- "we",
- "ws",
- "wthetave",
- "wthetav",
-] as const;
-
-/**
- * Class representing a BMI (Basic Model Interface) implementation for the CLASS model.
- * This class provides methods to initialize, update, and retrieve information from the model.
- */
-export class BmiClass implements BmiLight {
- config: Config = parse({});
- model: CLASS = new CLASS(this.config);
-
- // Model control functions
-
- initialize(config: Config) {
- this.config = config;
- this.model = new CLASS(config);
- }
-
- update() {
- this.model.update();
- }
-
- // Model information functions
-
- get_component_name() {
- return "Chemistry Land-surface Atmosphere Soil Slab model";
- }
-
- get_output_item_count() {
- return ouput_var_names.length;
- }
-
- get_output_var_names() {
- return ouput_var_names;
- }
-
- // Variable information functions
-
- get_var_grid(name: string) {
- return 1;
- }
-
- get_var_type(name: string) {
- return "float";
- }
-
- get_var_location(name: string) {
- return "node";
- }
-
- // Time functions
- get_current_time() {
- return this.model.t;
- }
-
- get_end_time() {
- return this.config.runtime;
- }
-
- get_time_units() {
- return "s";
- }
-
- get_time_step() {
- return this.config.dt;
- }
-
- // Variable getter and setter functions
-
- get_value(name: string) {
- if (ouput_var_names.includes(name)) {
- return [this.model[name as keyof CLASS]] as [number];
- }
- throw new Error(`Variable ${name} not found`);
- }
-
- // Model grid functions
-
- get_grid_type() {
- return "scalar";
- }
-
- // Extra methods
-
- /**
- * Runs the model till the end and returns the output data.
- *
- * @param options - The options for running the model.
- * @param options.freq - The frequency at which to record the output data. Default is 60 seconds.
- * @param options.var_names - For which output variables to record the data. Default is all output variables.
- * @returns The output data, including the time values in t key and the values of the output variables.
- */
- run({
- freq = 600,
- var_names = ouput_var_names as T,
- }: {
- freq?: number;
- var_names?: T;
- }): { t: number[] } & { [K in T[number]]: number[] } {
- const output: { t: number[] } & { [K in T[number]]: number[] } =
- Object.fromEntries([["t", []], ...var_names.map((name) => [name, []])]);
- while (this.model.t <= this.config.runtime) {
- if (this.model.t % freq === 0) {
- output.t.push(this.model.t);
- for (const name of var_names) {
- const value = this.model[name as keyof CLASS] as number;
- output[name as T[number]].push(value);
- }
- // TODO progress callback?
- // Initial attempt failed with "could not be cloned" error
- }
- this.update();
- }
- return output;
- }
-}
diff --git a/packages/class/src/runner.ts b/packages/class/src/runner.ts
index 6d7e563..b868a2e 100644
--- a/packages/class/src/runner.ts
+++ b/packages/class/src/runner.ts
@@ -7,20 +7,120 @@ import { CLASS } from "./class.js";
import type { Config } from "./config.js";
import { parse } from "./validate.js";
-export type ClassOutput = Record;
+export interface OutputVariable {
+ key: string;
+ title: string;
+ unit: string;
+ symbol: string;
+}
+
+export const outputVariables: OutputVariable[] = [
+ {
+ key: "t",
+ title: "Time",
+ unit: "s",
+ symbol: "t",
+ },
+ {
+ key: "h",
+ title: "ABL height",
+ unit: "m",
+ symbol: "h",
+ },
+ {
+ key: "theta",
+ title: "Potential temperature",
+ unit: "K",
+ symbol: "θ",
+ },
+ {
+ key: "dtheta",
+ title: "Potential temperature jump",
+ unit: "K",
+ symbol: "Δθ",
+ },
+ {
+ key: "q",
+ title: "Specific humidity",
+ unit: "kg kg⁻¹",
+ symbol: "q",
+ },
+ {
+ key: "dq",
+ title: "Specific humidity jump",
+ unit: "kg kg⁻¹",
+ symbol: "Δq",
+ },
+ {
+ key: "dthetav",
+ title: "Virtual temperature jump at h",
+ unit: "K",
+ symbol: "Δθᵥ",
+ },
+ {
+ key: "we",
+ title: "Entrainment velocity",
+ unit: "m s⁻¹",
+ symbol: "wₑ",
+ },
+ {
+ key: "ws",
+ title: "Large-scale vertical velocity",
+ unit: "m s⁻¹",
+ symbol: "wₛ",
+ },
+ {
+ key: "wthetave",
+ title: "Entrainment virtual heat flux",
+ unit: "K m s⁻¹",
+ symbol: "(w'θ')ᵥₑ",
+ },
+ {
+ key: "wthetav",
+ title: "Surface virtual heat flux",
+ unit: "K m s⁻¹",
+ symbol: "(w'θ')ᵥ",
+ },
+];
+
+export type ClassOutput = {
+ [K in (typeof outputVariables)[number]["key"]]: number[];
+};
-export function runClass(config: Config): ClassOutput {
- // TODO should we do validation/coercion here, in form, or both?
+/**
+ * Runs the CLASS model with the given configuration and frequency.
+ *
+ * @param config - The configuration object for the CLASS model.
+ * @param freq - The frequency in seconds at which to write output, defaults to 600.
+ * @returns An object containing the output variables collected during the simulation.
+ */
+export function runClass(config: Config, freq = 600): ClassOutput {
const validatedConfig = parse(config);
const model = new CLASS(validatedConfig);
- const output: ClassOutput = { t: [], h: [] };
+ const writeOutput = () => {
+ for (const v of outputVariables) {
+ const value =
+ model[v.key as keyof CLASS] ?? (v.key === "t" ? model.t : undefined);
+ if (value !== undefined) {
+ (output[v.key] as number[]).push(value as number);
+ }
+ }
+ };
+
+ const output = Object.fromEntries(
+ outputVariables.map((v) => [v.key, []]),
+ ) as ClassOutput;
+
+ // Initial time
+ writeOutput();
+
+ // Update loop
while (model.t < config.runtime) {
model.update();
- if (model.t % 60 === 0) {
- output.t.push(model.t);
- output.h.push(model.h);
+ if (model.t % freq === 0) {
+ writeOutput();
}
}