From eee403d155a51b70edfd85872bf022db7ea83ba1 Mon Sep 17 00:00:00 2001 From: Peter Kalverla Date: Mon, 24 Nov 2025 13:13:47 +0100 Subject: [PATCH 1/3] Update class output structure --- apps/class-solid/src/components/Analysis.tsx | 7 ++- apps/class-solid/src/lib/download.ts | 7 ++- apps/class-solid/src/lib/runner.ts | 5 +- apps/class-solid/src/lib/store.ts | 7 +-- packages/class/src/cli.ts | 6 +-- packages/class/src/fire.ts | 4 +- packages/class/src/output.ts | 12 +---- packages/class/src/profiles.ts | 4 +- packages/class/src/runner.ts | 49 ++++++++++++++++---- 9 files changed, 63 insertions(+), 38 deletions(-) diff --git a/apps/class-solid/src/components/Analysis.tsx b/apps/class-solid/src/components/Analysis.tsx index 130b8e8..b931301 100644 --- a/apps/class-solid/src/components/Analysis.tsx +++ b/apps/class-solid/src/components/Analysis.tsx @@ -1,7 +1,7 @@ import type { Config } from "@classmodel/class/config"; import { calculatePlume, transposePlumeData } from "@classmodel/class/fire"; import { - type ClassOutput, + type ClassTimeSeries, type OutputVariableKey, getOutputAtTime, outputVariables, @@ -78,7 +78,7 @@ interface FlatExperiment { color: string; linestyle: string; config: Config; - output?: ClassOutput; + output?: ClassTimeSeries; } // Create a derived store for looping over all outputs: @@ -322,8 +322,7 @@ export function VerticalProfilePlot({ ...observations().flatMap((obs) => obs.data.map((d) => d.y)), ]; - // TODO: better to include jump at top in extent calculation rather than adding random margin. - const xLim = () => getNiceAxisLimits(allX(), 1); + const xLim = () => getNiceAxisLimits(allX(), 0); const yLim = () => [0, getNiceAxisLimits(allY(), 0)[1]] as [number, number]; function chartData() { diff --git a/apps/class-solid/src/lib/download.ts b/apps/class-solid/src/lib/download.ts index d965a65..0af0c19 100644 --- a/apps/class-solid/src/lib/download.ts +++ b/apps/class-solid/src/lib/download.ts @@ -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"; @@ -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++) { diff --git a/apps/class-solid/src/lib/runner.ts b/apps/class-solid/src/lib/runner.ts index bae7006..98fdcd7 100644 --- a/apps/class-solid/src/lib/runner.ts +++ b/apps/class-solid/src/lib/runner.ts @@ -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), { @@ -9,7 +8,7 @@ const worker = new Worker(new URL("./worker.ts", import.meta.url), { const asyncRunner = wrap(worker); -export async function runClassAsync(config: Config): Promise { +export async function runClassAsync(config: Config): Promise { try { const output = asyncRunner(config); return output; diff --git a/apps/class-solid/src/lib/store.ts b/apps/class-solid/src/lib/store.ts index 190bc42..b747484 100644 --- a/apps/class-solid/src/lib/store.ts +++ b/apps/class-solid/src/lib/store.ts @@ -2,12 +2,13 @@ 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"; @@ -15,8 +16,8 @@ import { findPresetByName } from "./presets"; import { runClassAsync } from "./runner"; interface ExperimentOutput { - reference?: ClassOutput; - permutations: Array; + reference?: ClassData; + permutations: Array; running: number | false; } diff --git a/packages/class/src/cli.ts b/packages/class/src/cli.ts index 60f175b..3f05630 100755 --- a/packages/class/src/cli.ts +++ b/packages/class/src/cli.ts @@ -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"; @@ -51,7 +51,7 @@ async function writeTextFile(body: string, fn: string): Promise { /** * 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 @@ -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); diff --git a/packages/class/src/fire.ts b/packages/class/src/fire.ts index 6d13ea0..956dc9e 100644 --- a/packages/class/src/fire.ts +++ b/packages/class/src/fire.ts @@ -62,6 +62,8 @@ export interface Parcel { rh: number; // Relative humidity [%] } +export type FirePlume = Parcel[]; + /** * Initialize fire parcel with ambient conditions and fire properties */ @@ -144,7 +146,7 @@ export function calculatePlume( fire: FireConfig, bg: ClassProfile, plumeConfig: PlumeConfig = defaultPlumeConfig, -): Parcel[] { +): FirePlume { const { dz } = plumeConfig; let parcel = initializeFireParcel(bg, fire); const plume: Parcel[] = [parcel]; diff --git a/packages/class/src/output.ts b/packages/class/src/output.ts index c320afa..42d3350 100644 --- a/packages/class/src/output.ts +++ b/packages/class/src/output.ts @@ -118,14 +118,4 @@ export const outputVariables = { } as const satisfies Record; export type OutputVariableKey = keyof typeof outputVariables; -export type ClassOutput = Record; -export type ClassOutputAtSingleTime = Record; - -export function getOutputAtTime( - output: ClassOutput, - timeIndex: number, -): ClassOutputAtSingleTime { - return Object.fromEntries( - Object.entries(output).map(([key, values]) => [key, values[timeIndex]]), - ) as ClassOutputAtSingleTime; -} +export type ClassOutput = Record; diff --git a/packages/class/src/profiles.ts b/packages/class/src/profiles.ts index 06b8de3..5e278e8 100644 --- a/packages/class/src/profiles.ts +++ b/packages/class/src/profiles.ts @@ -1,7 +1,7 @@ // profiles.ts import type { MixedLayerConfig, NoWindConfig, WindConfig } from "./config.js"; -import type { ClassOutputAtSingleTime } from "./output.js"; +import type { ClassOutput } from "./output.js"; import { dewpoint, qsatLiq, @@ -55,7 +55,7 @@ export const NoProfile: ClassProfile = { */ export function generateProfiles( config: MixedLayerConfig & (WindConfig | NoWindConfig), - output: ClassOutputAtSingleTime, + output: ClassOutput, dz = 1, ): ClassProfile { const { Rd, cp, g } = CONSTANTS; diff --git a/packages/class/src/runner.ts b/packages/class/src/runner.ts index e3ca529..336f05c 100644 --- a/packages/class/src/runner.ts +++ b/packages/class/src/runner.ts @@ -5,13 +5,20 @@ */ import { CLASS } from "./class.js"; import type { Config } from "./config.js"; +import { type FirePlume, calculatePlume } from "./fire.js"; import { type ClassOutput, type OutputVariableKey, outputVariables, } from "./output.js"; +import { type ClassProfile, generateProfiles } from "./profiles.js"; import { parse } from "./validate.js"; +type ClassTimeSeries = Record; +type ClassProfiles = ClassProfile[]; +type ClassFirePlumes = FirePlume[]; +export type ClassData = [ClassTimeSeries, ClassProfiles?, ClassFirePlumes?]; + /** * Runs the CLASS model with the given configuration and frequency. * @@ -19,24 +26,41 @@ import { parse } from "./validate.js"; * @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 { +export function runClass(config: Config, freq = 600): ClassData { const validatedConfig = parse(config); const model = new CLASS(validatedConfig); - const output_keys = Object.keys(outputVariables) as OutputVariableKey[]; + const outputKeys = Object.keys(outputVariables) as OutputVariableKey[]; const writeOutput = () => { - for (const key of output_keys) { + const output: Partial = {}; + for (const key of outputKeys) { const value = model.getValue(key); if (value !== undefined) { - (output[key] as number[]).push(value as number); + output[key] = model.getValue(key); + timeSeries[key].push(value as number); + } + + // Include profiles + if (config.sw_ml) { + const profile = generateProfiles(config, output as ClassOutput); + profiles.push(profile); + + // Include fireplumes + if (config.sw_fire) { + const plume = calculatePlume(config, profile); + firePlumes.push(plume); + } } } }; - const output = Object.fromEntries( - output_keys.map((key) => [key, []]), - ) as unknown as ClassOutput; + // Initialize output arrays + const timeSeries = Object.fromEntries( + outputKeys.map((key) => [key, []]), + ) as unknown as ClassTimeSeries; + const profiles: ClassProfiles = []; + const firePlumes: ClassFirePlumes = []; // Initial time writeOutput(); @@ -50,5 +74,12 @@ export function runClass(config: Config, freq = 600): ClassOutput { } } - return output; -} + // Construct ClassData + if (config.sw_ml) { + if (config.sw_fire) { + return [timeSeries, profiles, firePlumes]; + } + return [timeSeries, profiles]; + } + return [timeSeries]; +} \ No newline at end of file From cd506474aff15bd0f1e1c50650b9cd484969a263 Mon Sep 17 00:00:00 2001 From: Peter Kalverla Date: Mon, 24 Nov 2025 17:10:06 +0100 Subject: [PATCH 2/3] Make profile and plume consistent, and extract unified profiles and plumes in analysis --- apps/class-solid/src/components/Analysis.tsx | 115 ++++++++++--------- packages/class/src/fire.ts | 26 ++++- packages/class/src/profiles.ts | 4 +- packages/class/src/runner.ts | 20 ++-- 4 files changed, 98 insertions(+), 67 deletions(-) diff --git a/apps/class-solid/src/components/Analysis.tsx b/apps/class-solid/src/components/Analysis.tsx index b931301..40bcd83 100644 --- a/apps/class-solid/src/components/Analysis.tsx +++ b/apps/class-solid/src/components/Analysis.tsx @@ -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 ClassTimeSeries, 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"; @@ -78,7 +77,7 @@ interface FlatExperiment { color: string; linestyle: string; config: Config; - output?: ClassTimeSeries; + output?: ClassData; } // Create a derived store for looping over all outputs: @@ -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? @@ -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 = { @@ -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, })) || [], }; @@ -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", @@ -255,11 +258,11 @@ export function VerticalProfilePlot({ "Density [kg/m³]": "rho", "Relative humidity [%]": "rh", } as const satisfies Record; + 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); } @@ -269,57 +272,65 @@ export function VerticalProfilePlot({ const observations = () => flatObservations().map((o) => observationsForProfile(o, classVariable())); + function extractLines>( + 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; + } + const profileData = () => flatExperiments().map((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 }; + + const targetTime = uniqueTimes()[analysis.time]; + const t = output?.timeseries.utcTime.indexOf(targetTime); + + const profile = + (t != null && t !== -1 && output?.profiles?.[t]) || noProfile; + + return { + ...formatting, + data: extractLines(profile, classVariable(), "z"), + }; }); const firePlumes = () => - flatExperiments().map((e, i) => { + flatExperiments().map((e) => { const { config, output, ...formatting } = e; - if (config.sw_fire && isPlumeVariable(classVariable())) { - const plume = transposePlumeData( - calculatePlume(config, profileData()[i].data), - ); - return { - ...formatting, - linestyle: "4", - data: plume.z.map((z, i) => ({ - x: plume[classVariable() as PlumeVariable][i], - y: 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[]; + const targetTime = uniqueTimes()[analysis.time]; + const t = output?.timeseries.utcTime.indexOf(targetTime); + + const plume = (t != null && t !== -1 && output?.plumes?.[t]) || noPlume; + return { + ...formatting, + linestyle: "4", + data: extractLines(plume, classVariable() as PlumeVariable, "z"), + }; + }); + 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)), + ...firePlumes().flatMap((p) => p.data.map((d) => d.z)), + ...profileDataForPlot().flatMap((p) => p.data.map((d) => d.z)), + ...observations().flatMap((obs) => obs.data.map((d) => d.z)), ]; const xLim = () => getNiceAxisLimits(allX(), 0); @@ -385,7 +396,7 @@ export function VerticalProfilePlot({ analysis.variable} setValue={(v) => changeVar(v)} - options={Object.keys(variableOptions)} + options={Object.keys(profileVariables)} label="variable: " /> {TimeSlider( @@ -482,7 +493,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 = () => diff --git a/packages/class/src/fire.ts b/packages/class/src/fire.ts index 956dc9e..94ac1b3 100644 --- a/packages/class/src/fire.ts +++ b/packages/class/src/fire.ts @@ -62,7 +62,25 @@ export interface Parcel { rh: number; // Relative humidity [%] } -export type FirePlume = Parcel[]; +export type FirePlume = Record; +export const noPlume: FirePlume = { + z: [], + w: [], + thetal: [], + theta: [], + qt: [], + thetav: [], + qsat: [], + b: [], + m: [], + area: [], + e: [], + d: [], + T: [], + Td: [], + p: [], + rh: [], +}; /** * Initialize fire parcel with ambient conditions and fire properties @@ -221,15 +239,13 @@ export function calculatePlume( plume.push(parcel); } - return plume; + return transposePlumeData(plume); } /** * Convert array of objects into object of arrays */ -export function transposePlumeData( - plume: Parcel[], -): Record { +export function transposePlumeData(plume: Parcel[]): FirePlume { if (plume.length === 0) { return {} as Record; } diff --git a/packages/class/src/profiles.ts b/packages/class/src/profiles.ts index 5e278e8..f14a0b0 100644 --- a/packages/class/src/profiles.ts +++ b/packages/class/src/profiles.ts @@ -18,7 +18,7 @@ const CONSTANTS = { /** * Atmospheric vertical profiles */ -export interface ClassProfile { +export interface ClassProfile extends Record { z: number[]; // Height levels (cell centers) [m] theta: number[]; // Potential temperature [K] thetav: number[]; // Virtual potential temperature [K] @@ -34,7 +34,7 @@ export interface ClassProfile { rh: number[]; // Relative humidity [%] } -export const NoProfile: ClassProfile = { +export const noProfile: ClassProfile = { z: [], theta: [], thetav: [], diff --git a/packages/class/src/runner.ts b/packages/class/src/runner.ts index 336f05c..416c3de 100644 --- a/packages/class/src/runner.ts +++ b/packages/class/src/runner.ts @@ -17,7 +17,11 @@ import { parse } from "./validate.js"; type ClassTimeSeries = Record; type ClassProfiles = ClassProfile[]; type ClassFirePlumes = FirePlume[]; -export type ClassData = [ClassTimeSeries, ClassProfiles?, ClassFirePlumes?]; +export interface ClassData { + timeseries: ClassTimeSeries; + profiles?: ClassProfiles; + plumes?: ClassFirePlumes; +} /** * Runs the CLASS model with the given configuration and frequency. @@ -38,7 +42,7 @@ export function runClass(config: Config, freq = 600): ClassData { const value = model.getValue(key); if (value !== undefined) { output[key] = model.getValue(key); - timeSeries[key].push(value as number); + timeseries[key].push(value as number); } // Include profiles @@ -49,18 +53,18 @@ export function runClass(config: Config, freq = 600): ClassData { // Include fireplumes if (config.sw_fire) { const plume = calculatePlume(config, profile); - firePlumes.push(plume); + plumes.push(plume); } } } }; // Initialize output arrays - const timeSeries = Object.fromEntries( + const timeseries = Object.fromEntries( outputKeys.map((key) => [key, []]), ) as unknown as ClassTimeSeries; const profiles: ClassProfiles = []; - const firePlumes: ClassFirePlumes = []; + const plumes: ClassFirePlumes = []; // Initial time writeOutput(); @@ -77,9 +81,9 @@ export function runClass(config: Config, freq = 600): ClassData { // Construct ClassData if (config.sw_ml) { if (config.sw_fire) { - return [timeSeries, profiles, firePlumes]; + return { timeseries, profiles, plumes }; } - return [timeSeries, profiles]; + return { timeseries, profiles }; } - return [timeSeries]; + return { timeseries }; } \ No newline at end of file From 00e5b87ea77782f26205fe4f1cb4fad743ab439a Mon Sep 17 00:00:00 2001 From: Peter Kalverla Date: Mon, 24 Nov 2025 17:46:00 +0100 Subject: [PATCH 3/3] precompute all line plot arrays, but this is very slow --- apps/class-solid/src/components/Analysis.tsx | 153 +++++++++++-------- packages/class/src/profiles.ts | 2 +- 2 files changed, 94 insertions(+), 61 deletions(-) diff --git a/apps/class-solid/src/components/Analysis.tsx b/apps/class-solid/src/components/Analysis.tsx index 40bcd83..81a9416 100644 --- a/apps/class-solid/src/components/Analysis.tsx +++ b/apps/class-solid/src/components/Analysis.tsx @@ -269,75 +269,60 @@ export function VerticalProfilePlot({ const showPlume = createMemo(() => isPlumeVariable(classVariable())); - const observations = () => - flatObservations().map((o) => observationsForProfile(o, classVariable())); - - function extractLines>( - 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; - } - - const profileData = () => - flatExperiments().map((e) => { + // Precalculate profile lines for classVariable() for all times + const allProfileLines = () => + flatExperiments().flatMap((e) => { const { config, output, ...formatting } = e; - const targetTime = uniqueTimes()[analysis.time]; - const t = output?.timeseries.utcTime.indexOf(targetTime); - - const profile = - (t != null && t !== -1 && output?.profiles?.[t]) || noProfile; + return uniqueTimes().map((time, tIndex) => { + const profile = output?.profiles?.[tIndex] ?? noProfile; - return { - ...formatting, - data: extractLines(profile, classVariable(), "z"), - }; + return { + ...formatting, + time, + tIndex, + data: extractLine(profile, classVariable(), "z"), + }; + }); }); - const firePlumes = () => - flatExperiments().map((e) => { + // Also precalculate plume lines + const allPlumeLines = () => + flatExperiments().flatMap((e) => { const { config, output, ...formatting } = e; - const targetTime = uniqueTimes()[analysis.time]; - const t = output?.timeseries.utcTime.indexOf(targetTime); - - const plume = (t != null && t !== -1 && output?.plumes?.[t]) || noPlume; + return uniqueTimes().map((time, tIndex) => { + const plume = output?.plumes?.[tIndex] ?? noPlume; - return { - ...formatting, - linestyle: "4", - data: extractLines(plume, classVariable() as PlumeVariable, "z"), - }; + return { + ...formatting, + time, + tIndex, + linestyle: "4", + data: extractLine(plume, classVariable() as PlumeVariable, "z"), + }; + }); }); - - 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.z)), - ...profileDataForPlot().flatMap((p) => p.data.map((d) => d.z)), - ...observations().flatMap((obs) => obs.data.map((d) => d.z)), + + const observationLines = () => + flatObservations().map((o) => observationsForProfile(o, classVariable())); + + const allLines = () => [ + ...allPlumeLines(), + ...allProfileLines(), + ...observationLines(), ]; - const xLim = () => getNiceAxisLimits(allX(), 0); - 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>({}); @@ -356,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 ( <>
[...profileData(), ...observations()]} + entries={() => [...profilesAtSelectedTime(), ...observationLines()]} toggles={toggles} onChange={toggleLine} /> - + {(d) => ( )} - + {(d) => ( )} - + {(d) => ( @@ -630,3 +625,41 @@ export function AnalysisCard(analysis: Analysis) { ); } + +// Helper functions + +function extractLine>( + 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 }; +} diff --git a/packages/class/src/profiles.ts b/packages/class/src/profiles.ts index f14a0b0..b54649f 100644 --- a/packages/class/src/profiles.ts +++ b/packages/class/src/profiles.ts @@ -56,7 +56,7 @@ export const noProfile: ClassProfile = { export function generateProfiles( config: MixedLayerConfig & (WindConfig | NoWindConfig), output: ClassOutput, - dz = 1, + dz = 10, ): ClassProfile { const { Rd, cp, g } = CONSTANTS; const { h, theta, qt, u, v, dtheta, dqt, du, dv } = output;