Skip to content

Commit cd50647

Browse files
committed
Make profile and plume consistent, and extract unified profiles and plumes in analysis
1 parent eee403d commit cd50647

File tree

4 files changed

+98
-67
lines changed

4 files changed

+98
-67
lines changed

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

Lines changed: 63 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
import type { Config } from "@classmodel/class/config";
2-
import { calculatePlume, transposePlumeData } from "@classmodel/class/fire";
2+
import { FirePlume, calculatePlume, noPlume } from "@classmodel/class/fire";
33
import {
4-
type ClassTimeSeries,
54
type OutputVariableKey,
6-
getOutputAtTime,
75
outputVariables,
86
} from "@classmodel/class/output";
97
import {
108
type ClassProfile,
11-
NoProfile,
129
generateProfiles,
10+
noProfile,
1311
} from "@classmodel/class/profiles";
12+
import type { ClassData } from "@classmodel/class/runner";
1413
import * as d3 from "d3";
1514
import { saveAs } from "file-saver";
1615
import { toBlob } from "html-to-image";
@@ -78,7 +77,7 @@ interface FlatExperiment {
7877
color: string;
7978
linestyle: string;
8079
config: Config;
81-
output?: ClassTimeSeries;
80+
output?: ClassData;
8281
}
8382

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

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

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

134133
const allX = () =>
135134
flatExperiments().flatMap((e) =>
136-
e.output ? e.output[analysis.xVariable as OutputVariableKey] : [],
135+
e.output
136+
? e.output.timeseries[analysis.xVariable as OutputVariableKey]
137+
: [],
137138
);
138139
const allY = () =>
139140
flatExperiments().flatMap((e) =>
140-
e.output ? e.output[analysis.yVariable as OutputVariableKey] : [],
141+
e.output
142+
? e.output.timeseries[analysis.yVariable as OutputVariableKey]
143+
: [],
141144
);
142145

143146
const granularities: Record<string, number | undefined> = {
@@ -157,12 +160,12 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
157160
...formatting,
158161
data:
159162
// Zip x[] and y[] into [x, y][]
160-
output?.t.map((_, t) => ({
163+
output?.timeseries.t.map((_, t) => ({
161164
x: output
162-
? output[analysis.xVariable as OutputVariableKey][t]
165+
? output.timeseries[analysis.xVariable as OutputVariableKey][t]
163166
: Number.NaN,
164167
y: output
165-
? output[analysis.yVariable as OutputVariableKey][t]
168+
? output.timeseries[analysis.yVariable as OutputVariableKey][t]
166169
: Number.NaN,
167170
})) || [],
168171
};
@@ -241,7 +244,7 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
241244
export function VerticalProfilePlot({
242245
analysis,
243246
}: { analysis: ProfilesAnalysis }) {
244-
const variableOptions = {
247+
const profileVariables = {
245248
"Potential temperature [K]": "theta",
246249
"Virtual potential temperature [K]": "thetav",
247250
"Specific humidity [kg/kg]": "qt",
@@ -255,11 +258,11 @@ export function VerticalProfilePlot({
255258
"Density [kg/m³]": "rho",
256259
"Relative humidity [%]": "rh",
257260
} as const satisfies Record<string, keyof ClassProfile>;
261+
type PlumeVariable = "theta" | "qt" | "thetav" | "T" | "Td" | "rh" | "w";
258262

259263
const classVariable = () =>
260-
variableOptions[analysis.variable as keyof typeof variableOptions];
264+
profileVariables[analysis.variable as keyof typeof profileVariables];
261265

262-
type PlumeVariable = "theta" | "qt" | "thetav" | "T" | "Td" | "rh" | "w";
263266
function isPlumeVariable(v: string): v is PlumeVariable {
264267
return ["theta", "qt", "thetav", "T", "Td", "rh", "w"].includes(v);
265268
}
@@ -269,57 +272,65 @@ export function VerticalProfilePlot({
269272
const observations = () =>
270273
flatObservations().map((o) => observationsForProfile(o, classVariable()));
271274

275+
function extractLines<T extends Record<string, number[]>>(
276+
data: T,
277+
xvar: keyof T,
278+
yvar: keyof T,
279+
) {
280+
const xs = data[xvar] ?? [];
281+
const ys = data[yvar] ?? [];
282+
283+
const n = Math.min(xs.length, ys.length);
284+
285+
const result = new Array(n);
286+
for (let i = 0; i < n; i++) {
287+
result[i] = { x: xs[i], y: ys[i] };
288+
}
289+
290+
return result;
291+
}
292+
272293
const profileData = () =>
273294
flatExperiments().map((e) => {
274295
const { config, output, ...formatting } = e;
275-
const t = output?.utcTime.indexOf(uniqueTimes()[analysis.time]);
276-
if (config.sw_ml && output && t !== undefined && t !== -1) {
277-
const outputAtTime = getOutputAtTime(output, t);
278-
return { ...formatting, data: generateProfiles(config, outputAtTime) };
279-
}
280-
return { ...formatting, data: NoProfile };
296+
297+
const targetTime = uniqueTimes()[analysis.time];
298+
const t = output?.timeseries.utcTime.indexOf(targetTime);
299+
300+
const profile =
301+
(t != null && t !== -1 && output?.profiles?.[t]) || noProfile;
302+
303+
return {
304+
...formatting,
305+
data: extractLines(profile, classVariable(), "z"),
306+
};
281307
});
282308

283309
const firePlumes = () =>
284-
flatExperiments().map((e, i) => {
310+
flatExperiments().map((e) => {
285311
const { config, output, ...formatting } = e;
286-
if (config.sw_fire && isPlumeVariable(classVariable())) {
287-
const plume = transposePlumeData(
288-
calculatePlume(config, profileData()[i].data),
289-
);
290-
return {
291-
...formatting,
292-
linestyle: "4",
293-
data: plume.z.map((z, i) => ({
294-
x: plume[classVariable() as PlumeVariable][i],
295-
y: z,
296-
})),
297-
};
298-
}
299-
return { ...formatting, data: [] };
300-
});
301312

302-
// TODO: There should be a way that this isn't needed.
303-
const profileDataForPlot = () =>
304-
profileData().map(({ data, label, color, linestyle }) => ({
305-
label,
306-
color,
307-
linestyle,
308-
data: data.z.map((z, i) => ({
309-
x: data[classVariable()][i],
310-
y: z,
311-
})),
312-
})) as ChartData<Point>[];
313+
const targetTime = uniqueTimes()[analysis.time];
314+
const t = output?.timeseries.utcTime.indexOf(targetTime);
315+
316+
const plume = (t != null && t !== -1 && output?.plumes?.[t]) || noPlume;
313317

318+
return {
319+
...formatting,
320+
linestyle: "4",
321+
data: extractLines(plume, classVariable() as PlumeVariable, "z"),
322+
};
323+
});
324+
314325
const allX = () => [
315326
...firePlumes().flatMap((p) => p.data.map((d) => d.x)),
316327
...profileDataForPlot().flatMap((p) => p.data.map((d) => d.x)),
317328
...observations().flatMap((obs) => obs.data.map((d) => d.x)),
318329
];
319330
const allY = () => [
320-
...firePlumes().flatMap((p) => p.data.map((d) => d.y)),
321-
...profileDataForPlot().flatMap((p) => p.data.map((d) => d.y)),
322-
...observations().flatMap((obs) => obs.data.map((d) => d.y)),
331+
...firePlumes().flatMap((p) => p.data.map((d) => d.z)),
332+
...profileDataForPlot().flatMap((p) => p.data.map((d) => d.z)),
333+
...observations().flatMap((obs) => obs.data.map((d) => d.z)),
323334
];
324335

325336
const xLim = () => getNiceAxisLimits(allX(), 0);
@@ -385,7 +396,7 @@ export function VerticalProfilePlot({
385396
<Picker
386397
value={() => analysis.variable}
387398
setValue={(v) => changeVar(v)}
388-
options={Object.keys(variableOptions)}
399+
options={Object.keys(profileVariables)}
389400
label="variable: "
390401
/>
391402
{TimeSlider(
@@ -482,7 +493,7 @@ export function ThermodynamicPlot({ analysis }: { analysis: SkewTAnalysis }) {
482493
const outputAtTime = getOutputAtTime(output, t);
483494
return { ...formatting, data: generateProfiles(config, outputAtTime) };
484495
}
485-
return { ...formatting, data: NoProfile };
496+
return { ...formatting, data: noProfile };
486497
});
487498

488499
const firePlumes = () =>

packages/class/src/fire.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,25 @@ export interface Parcel {
6262
rh: number; // Relative humidity [%]
6363
}
6464

65-
export type FirePlume = Parcel[];
65+
export type FirePlume = Record<keyof Parcel, number[]>;
66+
export const noPlume: FirePlume = {
67+
z: [],
68+
w: [],
69+
thetal: [],
70+
theta: [],
71+
qt: [],
72+
thetav: [],
73+
qsat: [],
74+
b: [],
75+
m: [],
76+
area: [],
77+
e: [],
78+
d: [],
79+
T: [],
80+
Td: [],
81+
p: [],
82+
rh: [],
83+
};
6684

6785
/**
6886
* Initialize fire parcel with ambient conditions and fire properties
@@ -221,15 +239,13 @@ export function calculatePlume(
221239

222240
plume.push(parcel);
223241
}
224-
return plume;
242+
return transposePlumeData(plume);
225243
}
226244

227245
/**
228246
* Convert array of objects into object of arrays
229247
*/
230-
export function transposePlumeData(
231-
plume: Parcel[],
232-
): Record<keyof Parcel, number[]> {
248+
export function transposePlumeData(plume: Parcel[]): FirePlume {
233249
if (plume.length === 0) {
234250
return {} as Record<keyof Parcel, number[]>;
235251
}

packages/class/src/profiles.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const CONSTANTS = {
1818
/**
1919
* Atmospheric vertical profiles
2020
*/
21-
export interface ClassProfile {
21+
export interface ClassProfile extends Record<string, number[]> {
2222
z: number[]; // Height levels (cell centers) [m]
2323
theta: number[]; // Potential temperature [K]
2424
thetav: number[]; // Virtual potential temperature [K]
@@ -34,7 +34,7 @@ export interface ClassProfile {
3434
rh: number[]; // Relative humidity [%]
3535
}
3636

37-
export const NoProfile: ClassProfile = {
37+
export const noProfile: ClassProfile = {
3838
z: [],
3939
theta: [],
4040
thetav: [],

packages/class/src/runner.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import { parse } from "./validate.js";
1717
type ClassTimeSeries = Record<OutputVariableKey, number[]>;
1818
type ClassProfiles = ClassProfile[];
1919
type ClassFirePlumes = FirePlume[];
20-
export type ClassData = [ClassTimeSeries, ClassProfiles?, ClassFirePlumes?];
20+
export interface ClassData {
21+
timeseries: ClassTimeSeries;
22+
profiles?: ClassProfiles;
23+
plumes?: ClassFirePlumes;
24+
}
2125

2226
/**
2327
* Runs the CLASS model with the given configuration and frequency.
@@ -38,7 +42,7 @@ export function runClass(config: Config, freq = 600): ClassData {
3842
const value = model.getValue(key);
3943
if (value !== undefined) {
4044
output[key] = model.getValue(key);
41-
timeSeries[key].push(value as number);
45+
timeseries[key].push(value as number);
4246
}
4347

4448
// Include profiles
@@ -49,18 +53,18 @@ export function runClass(config: Config, freq = 600): ClassData {
4953
// Include fireplumes
5054
if (config.sw_fire) {
5155
const plume = calculatePlume(config, profile);
52-
firePlumes.push(plume);
56+
plumes.push(plume);
5357
}
5458
}
5559
}
5660
};
5761

5862
// Initialize output arrays
59-
const timeSeries = Object.fromEntries(
63+
const timeseries = Object.fromEntries(
6064
outputKeys.map((key) => [key, []]),
6165
) as unknown as ClassTimeSeries;
6266
const profiles: ClassProfiles = [];
63-
const firePlumes: ClassFirePlumes = [];
67+
const plumes: ClassFirePlumes = [];
6468

6569
// Initial time
6670
writeOutput();
@@ -77,9 +81,9 @@ export function runClass(config: Config, freq = 600): ClassData {
7781
// Construct ClassData
7882
if (config.sw_ml) {
7983
if (config.sw_fire) {
80-
return [timeSeries, profiles, firePlumes];
84+
return { timeseries, profiles, plumes };
8185
}
82-
return [timeSeries, profiles];
86+
return { timeseries, profiles };
8387
}
84-
return [timeSeries];
88+
return { timeseries };
8589
}

0 commit comments

Comments
 (0)