Skip to content
Closed
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
177 changes: 110 additions & 67 deletions apps/class-solid/src/components/Analysis.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import type { Config } from "@classmodel/class/config";
import { calculatePlume, transposePlumeData } from "@classmodel/class/fire";
import { FirePlume, calculatePlume, noPlume } from "@classmodel/class/fire";
import {
type ClassOutput,
type OutputVariableKey,
getOutputAtTime,
outputVariables,
} from "@classmodel/class/output";
import {
type ClassProfile,
NoProfile,
generateProfiles,
noProfile,
} from "@classmodel/class/profiles";
import type { ClassData } from "@classmodel/class/runner";
import * as d3 from "d3";
import { saveAs } from "file-saver";
import { toBlob } from "html-to-image";
Expand Down Expand Up @@ -78,7 +77,7 @@ interface FlatExperiment {
color: string;
linestyle: string;
config: Config;
output?: ClassOutput;
output?: ClassData;
}

// Create a derived store for looping over all outputs:
Expand Down Expand Up @@ -119,7 +118,7 @@ const flatObservations: () => Observation[] = createMemo(() => {
});

const _allTimes = () =>
new Set(flatExperiments().flatMap((e) => e.output?.utcTime ?? []));
new Set(flatExperiments().flatMap((e) => e.output?.timeseries.utcTime ?? []));
const uniqueTimes = () => [...new Set(_allTimes())].sort((a, b) => a - b);

// TODO: could memoize all reactive elements here, would it make a difference?
Expand All @@ -133,11 +132,15 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {

const allX = () =>
flatExperiments().flatMap((e) =>
e.output ? e.output[analysis.xVariable as OutputVariableKey] : [],
e.output
? e.output.timeseries[analysis.xVariable as OutputVariableKey]
: [],
);
const allY = () =>
flatExperiments().flatMap((e) =>
e.output ? e.output[analysis.yVariable as OutputVariableKey] : [],
e.output
? e.output.timeseries[analysis.yVariable as OutputVariableKey]
: [],
);

const granularities: Record<string, number | undefined> = {
Expand All @@ -157,12 +160,12 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
...formatting,
data:
// Zip x[] and y[] into [x, y][]
output?.t.map((_, t) => ({
output?.timeseries.t.map((_, t) => ({
x: output
? output[analysis.xVariable as OutputVariableKey][t]
? output.timeseries[analysis.xVariable as OutputVariableKey][t]
: Number.NaN,
y: output
? output[analysis.yVariable as OutputVariableKey][t]
? output.timeseries[analysis.yVariable as OutputVariableKey][t]
: Number.NaN,
})) || [],
};
Expand Down Expand Up @@ -241,7 +244,7 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
export function VerticalProfilePlot({
analysis,
}: { analysis: ProfilesAnalysis }) {
const variableOptions = {
const profileVariables = {
"Potential temperature [K]": "theta",
"Virtual potential temperature [K]": "thetav",
"Specific humidity [kg/kg]": "qt",
Expand All @@ -255,79 +258,71 @@ export function VerticalProfilePlot({
"Density [kg/m³]": "rho",
"Relative humidity [%]": "rh",
} as const satisfies Record<string, keyof ClassProfile>;
type PlumeVariable = "theta" | "qt" | "thetav" | "T" | "Td" | "rh" | "w";

const classVariable = () =>
variableOptions[analysis.variable as keyof typeof variableOptions];
profileVariables[analysis.variable as keyof typeof profileVariables];

type PlumeVariable = "theta" | "qt" | "thetav" | "T" | "Td" | "rh" | "w";
function isPlumeVariable(v: string): v is PlumeVariable {
return ["theta", "qt", "thetav", "T", "Td", "rh", "w"].includes(v);
}

const showPlume = createMemo(() => isPlumeVariable(classVariable()));

const observations = () =>
flatObservations().map((o) => observationsForProfile(o, classVariable()));

const profileData = () =>
flatExperiments().map((e) => {
// Precalculate profile lines for classVariable() for all times
const allProfileLines = () =>
flatExperiments().flatMap((e) => {
const { config, output, ...formatting } = e;
const t = output?.utcTime.indexOf(uniqueTimes()[analysis.time]);
if (config.sw_ml && output && t !== undefined && t !== -1) {
const outputAtTime = getOutputAtTime(output, t);
return { ...formatting, data: generateProfiles(config, outputAtTime) };
}
return { ...formatting, data: NoProfile };

return uniqueTimes().map((time, tIndex) => {
const profile = output?.profiles?.[tIndex] ?? noProfile;

return {
...formatting,
time,
tIndex,
data: extractLine(profile, classVariable(), "z"),
};
});
});

const firePlumes = () =>
flatExperiments().map((e, i) => {
// Also precalculate plume lines
const allPlumeLines = () =>
flatExperiments().flatMap((e) => {
const { config, output, ...formatting } = e;
if (config.sw_fire && isPlumeVariable(classVariable())) {
const plume = transposePlumeData(
calculatePlume(config, profileData()[i].data),
);

return uniqueTimes().map((time, tIndex) => {
const plume = output?.plumes?.[tIndex] ?? noPlume;

return {
...formatting,
time,
tIndex,
linestyle: "4",
data: plume.z.map((z, i) => ({
x: plume[classVariable() as PlumeVariable][i],
y: z,
})),
data: extractLine(plume, classVariable() as PlumeVariable, "z"),
};
}
return { ...formatting, data: [] };
});
});

// TODO: There should be a way that this isn't needed.
const profileDataForPlot = () =>
profileData().map(({ data, label, color, linestyle }) => ({
label,
color,
linestyle,
data: data.z.map((z, i) => ({
x: data[classVariable()][i],
y: z,
})),
})) as ChartData<Point>[];
const observationLines = () =>
flatObservations().map((o) => observationsForProfile(o, classVariable()));

const allX = () => [
...firePlumes().flatMap((p) => p.data.map((d) => d.x)),
...profileDataForPlot().flatMap((p) => p.data.map((d) => d.x)),
...observations().flatMap((obs) => obs.data.map((d) => d.x)),
];
const allY = () => [
...firePlumes().flatMap((p) => p.data.map((d) => d.y)),
...profileDataForPlot().flatMap((p) => p.data.map((d) => d.y)),
...observations().flatMap((obs) => obs.data.map((d) => d.y)),
const allLines = () => [
...allPlumeLines(),
...allProfileLines(),
...observationLines(),
];

// TODO: better to include jump at top in extent calculation rather than adding random margin.
const xLim = () => getNiceAxisLimits(allX(), 1);
const yLim = () => [0, getNiceAxisLimits(allY(), 0)[1]] as [number, number];
const limits = () => {
const { xmin, xmax, ymin, ymax } = extractLimits(allLines());
return { xLim: [xmin, xmax], yLim: [ymin, ymax] };
};

const xLim = () => getNiceAxisLimits(limits().xLim);
const yLim = () => getNiceAxisLimits(limits().yLim);

function chartData() {
return [...profileData(), ...observations()];
return [...allPlumeLines(), ...observationLines()];
}

const [toggles, setToggles] = createStore<Record<string, boolean>>({});
Expand All @@ -346,33 +341,43 @@ export function VerticalProfilePlot({
setResetPlot(analysis.id);
}

const profilesAtSelectedTime = () => {
const t = analysis.time;
return allProfileLines().filter((line) => line.tIndex === t);
};

const plumesAtSelectedTime = () => {
const t = analysis.time;
return allPlumeLines().filter((line) => line.tIndex === t);
};

return (
<>
<div class="flex flex-col gap-2">
<ChartContainer>
<Legend
entries={() => [...profileData(), ...observations()]}
entries={() => [...profilesAtSelectedTime(), ...observationLines()]}
toggles={toggles}
onChange={toggleLine}
/>
<Chart id={analysis.id} title="Vertical profile plot">
<AxisBottom domain={xLim} label={analysis.variable} />
<AxisLeft domain={yLim} label="Height[m]" />
<For each={profileDataForPlot()}>
<For each={profilesAtSelectedTime()}>
{(d) => (
<Show when={toggles[d.label]}>
<Line {...d} />
</Show>
)}
</For>
<For each={observations()}>
<For each={observationLines()}>
{(d) => (
<Show when={toggles[d.label]}>
<Line {...d} />
</Show>
)}
</For>
<For each={firePlumes()}>
<For each={plumesAtSelectedTime()}>
{(d) => (
<Show when={toggles[d.label]}>
<Show when={showPlume()}>
Expand All @@ -386,7 +391,7 @@ export function VerticalProfilePlot({
<Picker
value={() => analysis.variable}
setValue={(v) => changeVar(v)}
options={Object.keys(variableOptions)}
options={Object.keys(profileVariables)}
label="variable: "
/>
{TimeSlider(
Expand Down Expand Up @@ -483,7 +488,7 @@ export function ThermodynamicPlot({ analysis }: { analysis: SkewTAnalysis }) {
const outputAtTime = getOutputAtTime(output, t);
return { ...formatting, data: generateProfiles(config, outputAtTime) };
}
return { ...formatting, data: NoProfile };
return { ...formatting, data: noProfile };
});

const firePlumes = () =>
Expand Down Expand Up @@ -620,3 +625,41 @@ export function AnalysisCard(analysis: Analysis) {
</Card>
);
}

// Helper functions

function extractLine<T extends Record<string, number[]>>(
data: T,
xvar: keyof T,
yvar: keyof T,
) {
const xs = data[xvar] ?? [];
const ys = data[yvar] ?? [];

const n = Math.min(xs.length, ys.length);

const result = new Array(n);
for (let i = 0; i < n; i++) {
result[i] = { x: xs[i], y: ys[i] };
}

return result;
}

function extractLimits(lines: { data: { x: number; y: number }[] }[]) {
let xmin = Number.POSITIVE_INFINITY;
let xmax = Number.NEGATIVE_INFINITY;
let ymin = Number.POSITIVE_INFINITY;
let ymax = Number.NEGATIVE_INFINITY;

for (const line of lines) {
for (const p of line.data) {
if (p.x < xmin) xmin = p.x;
if (p.x > xmax) xmax = p.x;
if (p.y < ymin) ymin = p.y;
if (p.y > ymax) ymax = p.y;
}
}

return { xmin, xmax, ymin, ymax };
}
7 changes: 5 additions & 2 deletions apps/class-solid/src/lib/download.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { ClassOutput, OutputVariableKey } from "@classmodel/class/output";
import type {
ClassTimeSeries,
OutputVariableKey,
} from "@classmodel/class/output";
import { BlobReader, BlobWriter, ZipWriter } from "@zip.js/zip.js";
import { toPartial } from "./encode";
import type { ExperimentConfig } from "./experiment_config";
Expand All @@ -11,7 +14,7 @@ export function toConfigBlob(experiment: ExperimentConfig) {
});
}

function outputToCsv(output: ClassOutput) {
function outputToCsv(output: ClassTimeSeries) {
const headers = Object.keys(output) as OutputVariableKey[];
const lines = [headers.join(",")];
for (let i = 0; i < output[headers[0]].length; i++) {
Expand Down
5 changes: 2 additions & 3 deletions apps/class-solid/src/lib/runner.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Config } from "@classmodel/class/config";
import type { ClassOutput } from "@classmodel/class/output";
import type { runClass } from "@classmodel/class/runner";
import type { ClassData, runClass } from "@classmodel/class/runner";
import { wrap } from "comlink";

const worker = new Worker(new URL("./worker.ts", import.meta.url), {
Expand All @@ -9,7 +8,7 @@ const worker = new Worker(new URL("./worker.ts", import.meta.url), {

const asyncRunner = wrap<typeof runClass>(worker);

export async function runClassAsync(config: Config): Promise<ClassOutput> {
export async function runClassAsync(config: Config): Promise<ClassData> {
try {
const output = asyncRunner(config);
return output;
Expand Down
7 changes: 4 additions & 3 deletions apps/class-solid/src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@ import { createUniqueId } from "solid-js";
import { createStore, produce, unwrap } from "solid-js/store";

import type { Config } from "@classmodel/class/config";
import type { ClassOutput } from "@classmodel/class/output";

import {
mergeConfigurations,
pruneConfig,
} from "@classmodel/class/config_utils";

import type { ClassData } from "@classmodel/class/runner";
import { decodeAppState } from "./encode";
import { parseExperimentConfig } from "./experiment_config";
import type { ExperimentConfig } from "./experiment_config";
import { findPresetByName } from "./presets";
import { runClassAsync } from "./runner";

interface ExperimentOutput {
reference?: ClassOutput;
permutations: Array<ClassOutput | undefined>;
reference?: ClassData;
permutations: Array<ClassData | undefined>;
running: number | false;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/class/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { readFile, writeFile } from "node:fs/promises";
import { EOL } from "node:os";
import { Command, Option } from "@commander-js/extra-typings";
import { jsonSchemaOfConfig } from "./config.js";
import type { ClassOutput, OutputVariableKey } from "./output.js";
import type { ClassTimeSeries, OutputVariableKey } from "./output.js";
import { runClass } from "./runner.js";
import { parse } from "./validate.js";

Expand Down Expand Up @@ -51,7 +51,7 @@ async function writeTextFile(body: string, fn: string): Promise<void> {
/**
* Create a DSV (delimiter-separated values) string from an object of arrays.
*/
function dsv(output: ClassOutput, delimiter: string): string {
function dsv(output: ClassTimeSeries, delimiter: string): string {
const keys = Object.keys(output) as OutputVariableKey[];
// order of headers is now in which they were added to the object
// TODO make configurable: which columns and in which order
Expand All @@ -67,7 +67,7 @@ function dsv(output: ClassOutput, delimiter: string): string {
/**
* Format the output.
*/
function formatOutput(output: ClassOutput, format: string): string {
function formatOutput(output: ClassTimeSeries, format: string): string {
switch (format) {
case "json":
return JSON.stringify(output, null, 2);
Expand Down
Loading
Loading