Skip to content

Commit ef47cc5

Browse files
Class updates: add wind, piecewise lapse rate, and time-dependent surface fluxes + various QOL improvements (#144)
* Implement basic zoom; TODO: fix angle in skewT * Add logarithmic zoom for skew-T diagram; TODO: T calculations should use real axis extent rather than fixes base/top pressure * Fix skew-T lines responding to original extent instead of actual; now they don't tilt anymore * Add panning effect, but it is stroboscopic and doesn't work for skewT yet * combine side-effects for both axes in a single callback * Remove animationframe * Use produce to update both scales in a single call * Don't update panstart; this fixes the jittering * Also work in log space * Make consistent for x-direction * zoom towards cursor * Add reset plot button * Thinner lines * Higher resolution plot * Round time to steps of 10 minutes * Use runClass from package, skipping BMI altogher + rich metadata for output vars; however, app/model hangs on second or third run * Fix hanging issue: wrap only once... * Make sure initial state is included in output * Reset pan/zoom when variable changes * formatting * Use output metadata in plot labels and variable pickers * ditch BMI * Fix xlabel in timeseries plot; fix axes extent for non-time on x-axis * Remove config.JSON everywhere; only keep the embedded version * Change units to use superscript * Drop _0 for initial fields * Make wq number array in form/config; TODO: interpolate value in model * Make gammaq and gammatheta arrays; TODO: fix profiles in app, use in model * Add free-troposphere anchor points for theta and q; TODO: use in model and app; update reference/default/test configs * Add wind variables to config. TODO: use in model / display profiles in app? * Implement hourly interpolation of surface fluxes + add them to output + move output spec to separate module * Implement piecewise lapse rate in model * formatting * Fix display of piecewise lapse rate in web app * Add basic wind logistics; rethink optional variable groups; use getValue rather than direct getters to access nested object groups; TODO in config only allow wind if mixed layer is on; TODO complete wind implementation * Complete wind implementation in model; include it in timeseries plot for validation * Add varnavas reference config as preset * Add u and v to profile plots * Make sure thermodynamic plot works with piecewise lapse rate * Determine axis extents of profile plots posterior to generating the profiles, so lapse part is fully included * Fix tests, set some values back to their previous default * Make wind from observations also visible in profile plots * Added Varnavas config with observations to test folder + increased max height in preset * Make labels of du and dv unique * Combine tooltip and label + for number[] input show extra description about comma seperator * Fix types in tests * fix type issue --------- Co-authored-by: sverhoeven <[email protected]>
1 parent 7b0543b commit ef47cc5

File tree

22 files changed

+1094
-344
lines changed

22 files changed

+1094
-344
lines changed

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

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Config } from "@classmodel/class/config";
2-
import { type ClassOutput, outputVariables } from "@classmodel/class/runner";
2+
import { type ClassOutput, outputVariables } from "@classmodel/class/output";
33
import * as d3 from "d3";
44
import { saveAs } from "file-saver";
55
import { toBlob } from "html-to-image";
@@ -226,33 +226,16 @@ export function VerticalProfilePlot({
226226
const variableOptions = {
227227
"Potential temperature [K]": "theta",
228228
"Specific humidity [kg/kg]": "q",
229+
"u-wind component [m/s]": "u",
230+
"v-wind component [m/s]": "v",
229231
};
230232

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

234236
const observations = () =>
235237
flatObservations().map((o) => observationsForProfile(o, classVariable()));
236-
const obsAllX = () =>
237-
observations().flatMap((obs) => obs.data.map((d) => d.x));
238-
const obsAllY = () =>
239-
observations().flatMap((obs) => obs.data.map((d) => d.y));
240-
241-
const allValues = () => [
242-
...flatExperiments().flatMap((e) =>
243-
e.output ? e.output[classVariable()] : [],
244-
),
245-
...obsAllX(),
246-
];
247-
const allHeights = () => [
248-
...flatExperiments().flatMap((e) => (e.output ? e.output.h : [])),
249-
...obsAllY(),
250-
];
251238

252-
// TODO: better to include jump at top in extent calculation rather than adding random margin.
253-
const xLim = () => getNiceAxisLimits(allValues(), 1);
254-
const yLim = () =>
255-
[0, getNiceAxisLimits(allHeights(), 0)[1]] as [number, number];
256239
const profileData = () =>
257240
flatExperiments().map((e) => {
258241
const { config, output, ...formatting } = e;
@@ -271,6 +254,19 @@ export function VerticalProfilePlot({
271254
};
272255
});
273256

257+
const allX = () => [
258+
...profileData().flatMap((p) => p.data.map((d) => d.x)),
259+
...observations().flatMap((obs) => obs.data.map((d) => d.x)),
260+
];
261+
const allY = () => [
262+
...profileData().flatMap((p) => p.data.map((d) => d.y)),
263+
...observations().flatMap((obs) => obs.data.map((d) => d.y)),
264+
];
265+
266+
// TODO: better to include jump at top in extent calculation rather than adding random margin.
267+
const xLim = () => getNiceAxisLimits(allX(), 1);
268+
const yLim = () => [0, getNiceAxisLimits(allY(), 0)[1]] as [number, number];
269+
274270
function chartData() {
275271
return [...profileData(), ...observations()];
276272
}

apps/class-solid/src/lib/download.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ClassOutput } from "@classmodel/class/runner";
1+
import type { ClassOutput } from "@classmodel/class/output";
22
import { BlobReader, BlobWriter, ZipWriter } from "@zip.js/zip.js";
33
import { toPartial } from "./encode";
44
import type { ExperimentConfig } from "./experiment_config";

apps/class-solid/src/lib/presets.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { overwriteDefaultsInJsonSchema } from "@classmodel/form/utils";
88
import type { DefinedError, JSONSchemaType, ValidateFunction } from "ajv";
99
// TODO replace with preset of a forest fire
1010
import deathValley from "./presets/death-valley.json";
11+
import varnavas from "./presets/varnavas.json";
1112

1213
const presetConfigs = [
1314
{
1415
name: "Default",
1516
description: "The classic default configuration",
1617
},
1718
deathValley,
19+
varnavas,
1820
] as const;
1921

2022
export interface Preset {
Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
{
22
"name": "Death Valley",
33
"description": "Preset with Death Valley conditions",
4-
"theta_0": 323,
5-
"h_0": 200,
6-
"dtheta_0": 1,
7-
"q_0": 0.008,
8-
"dq_0": -0.001,
4+
"theta": 323,
5+
"h": 200,
6+
"dtheta": 1,
7+
"q": 0.008,
8+
"dq": -0.001,
99
"dt": 60,
1010
"runtime": 43200,
1111
"wtheta": [0.1],
1212
"advtheta": 0,
13-
"gammatheta": 0.006,
14-
"wq": 0.0001,
13+
"gammatheta": [0.006],
14+
"wq": [0.0001],
1515
"advq": 0,
16-
"gammaq": 0,
16+
"gammaq": [0],
1717
"divU": 0,
1818
"beta": 0.2
1919
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "Varnavas",
3+
"description": "EWED Reference configuration",
4+
"h": 665.4086303710938,
5+
"theta": 301.971435546875,
6+
"dtheta": 0.889312744140625,
7+
"gammatheta": [0.0015336532378569245, 0.006792282219976187],
8+
"z_theta": [1667.025390625, 3720],
9+
"q": 0.010794480331242085,
10+
"dq": -0.001073240302503109,
11+
"gammaq": [-2.382889988439274e-6, -2.5458646177867195e-6],
12+
"z_q": [1360.52392578125, 3720],
13+
"divU": 1.0981982995872386e-5,
14+
"u": -1.6769592761993408,
15+
"du": -1.1726601123809814,
16+
"gamma_u": [-0.001995581667870283, -0.002064943080767989],
17+
"z_u": [1573.5614013671875, 3720],
18+
"v": -10.087028503417969,
19+
"dv": -1.5727958679199219,
20+
"gamma_v": [-0.0005029001622460783, 0.004509935155510902],
21+
"z_v": [3037.957763671875, 3720],
22+
"wtheta": [
23+
0.33137200988670706, 0.28088430353706567, 0.21148639643745845,
24+
0.1354453158978963, 0.0558820378352221, 0.0001602229911650289, 0.0, 0.0,
25+
0.0, 0.0, 0.0, 0.0
26+
],
27+
"wq": [
28+
1.9756698748096824e-5, 2.111953654093668e-5, 2.2018328309059143e-5,
29+
2.2002774130669422e-5, 1.9648385205073282e-5, 1.6331905499100685e-5,
30+
1.3947577826911584e-5, 1.2534562301880214e-5, 1.1651739441731479e-5,
31+
1.1526963135111146e-5, 1.3352756468520965e-5, 1.3273539479996543e-5
32+
],
33+
"sw_wind": true,
34+
"advtheta": 0,
35+
"advq": 0,
36+
"advu": 0,
37+
"advv": 0,
38+
"beta": 0.2,
39+
"ustar": 0.3,
40+
"dt": 60,
41+
"runtime": 43200
42+
}

apps/class-solid/src/lib/profiles.ts

Lines changed: 152 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Config } from "@classmodel/class/config";
2-
import type { ClassOutput } from "@classmodel/class/runner";
2+
import type { ClassOutput } from "@classmodel/class/output";
3+
import { findInsertIndex } from "@classmodel/class/utils";
34
import type { Point } from "~/components/plots/Line";
45
import type { Observation } from "./experiment_config";
56

@@ -15,31 +16,126 @@ export function getVerticalProfiles(
1516
return [];
1617
}
1718

18-
// Extract height profile
19-
const height = output.h.slice(t)[0];
20-
const dh = 1600; // how much free troposphere to display?
21-
const hProfile = [0, height, height, height + dh];
2219
if (variable === "theta") {
23-
// Extract potential temperature profile
24-
const theta = output.theta.slice(t)[0];
20+
let z = output.h.slice(t)[0];
21+
let theta = output.theta.slice(t)[0];
2522
const dtheta = output.dtheta.slice(t)[0];
2623
const gammatheta = config.gammatheta;
27-
const thetaProfile = [
28-
theta,
29-
theta,
30-
theta + dtheta,
31-
theta + dtheta + dh * gammatheta,
24+
const z_theta = config.z_theta;
25+
const maxHeight = z_theta.slice(-1)[0];
26+
27+
// Mixed layer
28+
const profile = [
29+
{ x: theta, y: 0 },
30+
{ x: theta, y: z },
3231
];
33-
return hProfile.map((h, i) => ({ x: thetaProfile[i], y: h }));
32+
33+
// Inversion
34+
theta += dtheta;
35+
profile.push({ x: theta, y: z });
36+
37+
// Free troposphere
38+
while (z < maxHeight) {
39+
const idx = findInsertIndex(z_theta, z);
40+
const lapse_rate = gammatheta[idx] ?? 0;
41+
const dz = z_theta[idx] - z;
42+
z += dz;
43+
theta += lapse_rate * dz;
44+
profile.push({ x: theta, y: z });
45+
}
46+
return profile;
3447
}
48+
3549
if (variable === "q") {
36-
// Extract humidity profile
37-
const q = output.q.slice(t)[0];
50+
let z = output.h.slice(t)[0];
51+
let q = output.q.slice(t)[0];
3852
const dq = output.dq.slice(t)[0];
3953
const gammaq = config.gammaq;
40-
const qProfile = [q, q, q + dq, q + dq + dh * gammaq];
41-
return hProfile.map((h, i) => ({ x: qProfile[i], y: h }));
54+
const z_q = config.z_q;
55+
const maxHeight = z_q.slice(-1)[0];
56+
57+
// Mixed layer
58+
const profile = [
59+
{ x: q, y: 0 },
60+
{ x: q, y: z },
61+
];
62+
63+
// Inversion
64+
q += dq;
65+
profile.push({ x: q, y: z });
66+
67+
// Free troposphere
68+
while (z < maxHeight) {
69+
const idx = findInsertIndex(z_q, z);
70+
const lapse_rate = gammaq[idx] ?? 0;
71+
const dz = z_q[idx] - z;
72+
z += dz;
73+
q += lapse_rate * dz;
74+
profile.push({ x: q, y: z });
75+
}
76+
return profile;
77+
}
78+
79+
if (config.sw_wind && variable === "u") {
80+
let z = output.h.slice(t)[0];
81+
let u = output.u.slice(t)[0];
82+
const du = output.du.slice(t)[0];
83+
const gammau = config.gamma_u;
84+
const z_u = config.z_u;
85+
const maxHeight = z_u.slice(-1)[0];
86+
87+
// Mixed layer
88+
const profile = [
89+
{ x: u, y: 0 },
90+
{ x: u, y: z },
91+
];
92+
93+
// Inversion
94+
u += du;
95+
profile.push({ x: u, y: z });
96+
97+
// Free troposphere
98+
while (z < maxHeight) {
99+
const idx = findInsertIndex(z_u, z);
100+
const lapse_rate = gammau[idx] ?? 0;
101+
const dz = z_u[idx] - z;
102+
z += dz;
103+
u += lapse_rate * dz;
104+
profile.push({ x: u, y: z });
105+
}
106+
return profile;
107+
}
108+
109+
if (config.sw_wind && variable === "v") {
110+
let z = output.h.slice(t)[0];
111+
let v = output.v.slice(t)[0];
112+
const dv = output.dv.slice(t)[0];
113+
const gammav = config.gamma_v;
114+
const z_v = config.z_v;
115+
const maxHeight = z_v.slice(-1)[0];
116+
117+
// Mixed layer
118+
const profile = [
119+
{ x: v, y: 0 },
120+
{ x: v, y: z },
121+
];
122+
123+
// Inversion
124+
v += dv;
125+
profile.push({ x: v, y: z });
126+
127+
// Free troposphere
128+
while (z < maxHeight) {
129+
const idx = findInsertIndex(z_v, z);
130+
const lapse_rate = gammav[idx] ?? 0;
131+
const dz = z_v[idx] - z;
132+
z += dz;
133+
v += lapse_rate * dz;
134+
profile.push({ x: v, y: z });
135+
}
136+
return profile;
42137
}
138+
43139
return [];
44140
}
45141

@@ -142,6 +238,8 @@ export function getThermodynamicProfiles(
142238
const h = output.h.slice(t)[0];
143239
const gammaTheta = config.gammatheta;
144240
const gammaq = config.gammaq;
241+
const z_theta = config.z_theta;
242+
const z_q = config.z_q;
145243

146244
const nz = 25;
147245
let dz = h / nz;
@@ -169,11 +267,18 @@ export function getThermodynamicProfiles(
169267
soundingData.push({ p, T, Td });
170268

171269
// Free troposphere
270+
let z = zArrayMixedLayer.slice(-1)[0];
172271
dz = 200;
173272
while (p > 100) {
174-
theta += dz * gammaTheta;
175-
q += dz * gammaq;
273+
// Note: idx can exceed length of anchor points, then lapse becomes undefined and profile stops
274+
const idx_th = findInsertIndex(z_theta, z);
275+
const lapse_theta = gammaTheta[idx_th];
276+
const idx_q = findInsertIndex(z_q, z);
277+
const lapse_q = gammaq[idx_q];
278+
theta += dz * lapse_theta;
279+
q += dz * lapse_q;
176280
p += pressureDiff(T, q, p, dz);
281+
z += dz;
177282
T = thetaToT(theta, p);
178283
Td = dewpoint(q, p);
179284

@@ -194,10 +299,23 @@ export function observationsForProfile(obs: Observation, variable = "theta") {
194299
const p = obs.pressure[i];
195300
const theta = tToTheta(T, p);
196301
const q = calculateSpecificHumidity(T, p, rh);
197-
if (variable === "theta") {
198-
return { y: h, x: theta };
302+
const { u, v } = windSpeedDirectionToUV(
303+
obs.windSpeed[i],
304+
obs.windDirection[i],
305+
);
306+
307+
switch (variable) {
308+
case "theta":
309+
return { y: h, x: theta };
310+
case "q":
311+
return { y: h, x: q };
312+
case "u":
313+
return { y: h, x: u };
314+
case "v":
315+
return { y: h, x: v };
316+
default:
317+
throw new Error(`Unknown variable '${variable}'`);
199318
}
200-
return { y: h, x: q };
201319
}),
202320
};
203321
}
@@ -217,3 +335,15 @@ export function observationsForSounding(obs: Observation) {
217335
linestyle: "none",
218336
};
219337
}
338+
339+
function windSpeedDirectionToUV(
340+
speed: number,
341+
directionDeg: number,
342+
): { u: number; v: number } {
343+
const directionRad = (directionDeg * Math.PI) / 180;
344+
345+
const u = -speed * Math.sin(directionRad); // zonal (east-west)
346+
const v = -speed * Math.cos(directionRad); // meridional (north-south)
347+
348+
return { u, v };
349+
}

apps/class-solid/src/lib/runner.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Config } from "@classmodel/class/config";
2-
import type { ClassOutput, runClass } from "@classmodel/class/runner";
2+
import type { ClassOutput } from "@classmodel/class/output";
3+
import type { runClass } from "@classmodel/class/runner";
34
import { wrap } from "comlink";
45

56
const worker = new Worker(new URL("./worker.ts", import.meta.url), {

apps/class-solid/src/lib/store.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { createUniqueId } from "solid-js";
22
import { createStore, produce, unwrap } from "solid-js/store";
33

44
import type { Config } from "@classmodel/class/config";
5-
import type { ClassOutput } from "@classmodel/class/runner";
5+
import type { ClassOutput } from "@classmodel/class/output";
66

77
import {
88
mergeConfigurations,

0 commit comments

Comments
 (0)