Skip to content

Commit c97cba9

Browse files
Soundings in plot (#123)
* Show coords in top left of plot (fixes #82) * Added formatting to coord infobox * Upload observations + wip try to render but scales are not converting from domain to pixel, it stays domain Refs #68 * Fix type error * Display temp in kelvin so it renders on plot * Theta and q now shown correctly on profile plot * Also render sounding on skew-t plot and include in legend * remove observationpoint dead code * Include obs data in share URL * typo * Apply suggestions from code review Will check locally for remaining issues with optional vs required name Co-authored-by: Stefan Verhoeven <s.verhoeven@esciencecenter.nl> * format fix * remove unneeded fallback for unnamed obs --------- Co-authored-by: Peter Kalverla <peter.kalverla@gmx.com>
1 parent edf3e1f commit c97cba9

File tree

6 files changed

+232
-10
lines changed

6 files changed

+232
-10
lines changed

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

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ import {
1111
createMemo,
1212
createUniqueId,
1313
} from "solid-js";
14-
import { getThermodynamicProfiles, getVerticalProfiles } from "~/lib/profiles";
14+
import type { Observation } from "~/lib/experiment_config";
15+
import {
16+
getThermodynamicProfiles,
17+
getVerticalProfiles,
18+
observationsForProfile,
19+
observationsForSounding,
20+
} from "~/lib/profiles";
1521
import {
1622
type Analysis,
1723
type ProfilesAnalysis,
@@ -68,7 +74,7 @@ const flatExperiments: () => FlatExperiment[] = createMemo(() => {
6874
return experiments
6975
.filter((e) => e.output.running === false) // skip running experiments
7076
.flatMap((e, i) => {
71-
const reference = {
77+
const reference: FlatExperiment = {
7278
color: colors[0],
7379
linestyle: linestyles[i % 5],
7480
label: e.config.reference.name,
@@ -90,6 +96,15 @@ const flatExperiments: () => FlatExperiment[] = createMemo(() => {
9096
});
9197
});
9298

99+
// Derived store for all observations of all experiments combined
100+
const flatObservations: () => Observation[] = createMemo(() => {
101+
return experiments
102+
.filter((e) => e.config.observations)
103+
.flatMap((e) => {
104+
return e.config.observations || [];
105+
});
106+
});
107+
93108
const _allTimes = () =>
94109
new Set(flatExperiments().flatMap((e) => e.output?.t ?? []));
95110
const uniqueTimes = () => [...new Set(_allTimes())].sort((a, b) => a - b);
@@ -166,12 +181,23 @@ export function VerticalProfilePlot({
166181
const classVariable = () =>
167182
variableOptions[analysis.variable as keyof typeof variableOptions];
168183

169-
const allValues = () =>
170-
flatExperiments().flatMap((e) =>
184+
const observations = () =>
185+
flatObservations().map((o) => observationsForProfile(o, classVariable()));
186+
const obsAllX = () =>
187+
observations().flatMap((obs) => obs.data.map((d) => d.x));
188+
const obsAllY = () =>
189+
observations().flatMap((obs) => obs.data.map((d) => d.y));
190+
191+
const allValues = () => [
192+
...flatExperiments().flatMap((e) =>
171193
e.output ? e.output[classVariable()] : [],
172-
);
173-
const allHeights = () =>
174-
flatExperiments().flatMap((e) => (e.output ? e.output.h : []));
194+
),
195+
...obsAllX(),
196+
];
197+
const allHeights = () => [
198+
...flatExperiments().flatMap((e) => (e.output ? e.output.h : [])),
199+
...obsAllY(),
200+
];
175201

176202
// TODO: better to include jump at top in extent calculation rather than adding random margin.
177203
const xLim = () => getNiceAxisLimits(allValues(), 1);
@@ -199,11 +225,12 @@ export function VerticalProfilePlot({
199225
<>
200226
<div class="flex flex-col gap-2">
201227
<ChartContainer>
202-
<Legend entries={profileData} />
228+
<Legend entries={() => [...profileData(), ...observations()]} />
203229
<Chart title="Vertical profile plot">
204230
<AxisBottom domain={xLim} label={analysis.variable} />
205231
<AxisLeft domain={yLim} label="Height[m]" />
206232
<For each={profileData()}>{(d) => Line(d)}</For>
233+
<For each={observations()}>{(d) => Line(d)}</For>
207234
</Chart>
208235
</ChartContainer>
209236
<Picker
@@ -307,9 +334,12 @@ export function ThermodynamicPlot({ analysis }: { analysis: SkewTAnalysis }) {
307334
};
308335
});
309336

337+
const observations = () =>
338+
flatObservations().map((o) => observationsForSounding(o));
339+
310340
return (
311341
<>
312-
<SkewTPlot data={skewTData} />
342+
<SkewTPlot data={() => [...skewTData(), ...observations()]} />
313343
{TimeSlider(
314344
() => analysis.time,
315345
uniqueTimes,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export function toPartial(config: ExperimentConfig): PartialExperimentConfig {
5353
permutations: config.permutations.map((perm) =>
5454
pruneConfig(perm, config.reference, preset.config),
5555
),
56+
observations: config.observations,
5657
};
5758
}
5859

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ import { overwriteDefaultsInJsonSchema } from "@classmodel/form/utils";
55
import type { DefinedError, JSONSchemaType } from "ajv";
66
import { findPresetByName } from "./presets";
77

8+
/*
9+
Height (m AGL) Pressure (mb) Temperature (C) Relative humidity (%) Wind speed (m/s) Wind direction (true deg)
10+
*/
11+
export interface Observation {
12+
name: string;
13+
height: number[];
14+
pressure: number[];
15+
temperature: number[];
16+
relativeHumidity: number[];
17+
windSpeed: number[];
18+
windDirection: number[];
19+
}
20+
821
/**
922
* An experiment configuration is a combination of a preset name and
1023
* a reference configuration and a set of permutation configurations.
@@ -13,6 +26,7 @@ export interface ExperimentConfig<C = Config> {
1326
preset: string;
1427
reference: C;
1528
permutations: C[];
29+
observations?: Observation[];
1630
}
1731

1832
/**
@@ -42,6 +56,30 @@ const jsonSchemaOfExperimentConfigBase = {
4256
},
4357
default: [],
4458
},
59+
observations: {
60+
type: "array",
61+
items: {
62+
type: "object",
63+
properties: {
64+
name: { type: "string" },
65+
height: { type: "array", items: { type: "number" } },
66+
pressure: { type: "array", items: { type: "number" } },
67+
temperature: { type: "array", items: { type: "number" } },
68+
relativeHumidity: { type: "array", items: { type: "number" } },
69+
windSpeed: { type: "array", items: { type: "number" } },
70+
windDirection: { type: "array", items: { type: "number" } },
71+
},
72+
required: [
73+
"name",
74+
"height",
75+
"pressure",
76+
"temperature",
77+
"relativeHumidity",
78+
"windSpeed",
79+
"windDirection",
80+
],
81+
},
82+
},
4583
},
4684
required: ["preset", "reference", "permutations"],
4785
} as unknown as JSONSchemaType<ExperimentConfig<object>>;
@@ -89,9 +127,13 @@ export function parseExperimentConfig(input: unknown): ExperimentConfig {
89127
}
90128

91129
const permutations = base.permutations.map(permParse);
92-
return {
130+
const config: ExperimentConfig = {
93131
reference,
94132
preset: base.preset,
95133
permutations,
96134
};
135+
if (base.observations) {
136+
config.observations = base.observations;
137+
}
138+
return config;
97139
}

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Config } from "@classmodel/class/config";
22
import type { ClassOutput } from "@classmodel/class/runner";
33
import type { Point } from "~/components/plots/Line";
4+
import type { Observation } from "./experiment_config";
45

56
// Get vertical profiles for a single class run
67
export function getVerticalProfiles(
@@ -70,6 +71,16 @@ const thetaToT = (theta: number, p: number, p0 = 1000) => {
7071
return T;
7172
};
7273

74+
/**
75+
* Calculate potential temperature from temperature
76+
*/
77+
const tToTheta = (T: number, p: number, p0 = 1000) => {
78+
const R = 287; // specific gas constant for dry air
79+
const cp = 1004; // specific heat of dry air at constant pressure
80+
const theta = T * (p / p0) ** -(R / cp);
81+
return theta;
82+
};
83+
7384
/**
7485
* Calculate pressure difference over layer using hypsometric equation
7586
* Wallace & Hobbs eq (3.23)
@@ -96,6 +107,24 @@ const thickness = (T: number, q: number, p: number, dp: number) => {
96107
return dz;
97108
};
98109

110+
function calculateSpecificHumidity(T: number, p: number, rh: number) {
111+
// Constants
112+
const epsilon = 0.622; // Ratio of gas constants for dry air and water vapor
113+
const es0 = 6.112; // Reference saturation vapor pressure in hPa
114+
115+
// Calculate saturation vapor pressure (Tetens formula)
116+
const es = es0 * Math.exp((17.67 * (T - 273.15)) / (T - 29.65));
117+
118+
// Actual vapor pressure
119+
const e = (rh / 100) * es;
120+
121+
// Mixing ratio (kg/kg)
122+
const w = (epsilon * e) / (p - e);
123+
124+
// Specific humidity
125+
return w / (1 + w);
126+
}
127+
99128
export function getThermodynamicProfiles(
100129
output: ClassOutput | undefined,
101130
config: Config,
@@ -153,3 +182,38 @@ export function getThermodynamicProfiles(
153182

154183
return soundingData;
155184
}
185+
186+
export function observationsForProfile(obs: Observation, variable = "theta") {
187+
return {
188+
label: obs.name,
189+
color: "red",
190+
linestyle: "3,10",
191+
data: obs.height.map((h, i) => {
192+
const T = obs.temperature[i] + 273.15;
193+
const rh = obs.relativeHumidity[i];
194+
const p = obs.pressure[i];
195+
const theta = tToTheta(T, p);
196+
const q = calculateSpecificHumidity(T, p, rh);
197+
if (variable === "theta") {
198+
return { y: h, x: theta };
199+
}
200+
return { y: h, x: q };
201+
}),
202+
};
203+
}
204+
205+
export function observationsForSounding(obs: Observation) {
206+
return {
207+
data: obs.height.map((h, i) => {
208+
const T = obs.temperature[i] + 273.15;
209+
const p = obs.pressure[i];
210+
const rh = obs.relativeHumidity[i];
211+
const q = calculateSpecificHumidity(T, p, rh);
212+
const Td = dewpoint(q, p);
213+
return { p, T, Td };
214+
}),
215+
label: obs.name,
216+
color: "red",
217+
linestyle: "3,10",
218+
};
219+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ export async function uploadExperiment(rawData: unknown) {
104104
running: false,
105105
},
106106
};
107+
if (upload.observations) {
108+
experiment.config.observations = upload.observations;
109+
}
107110
setExperiments(experiments.length, experiment);
108111
await runExperiment(experiments.length - 1);
109112
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"reference": {
3+
"name": "EWED 1916",
4+
"description": "Default config with observations"
5+
},
6+
"preset": "Default",
7+
"permutations": [],
8+
"observations": [
9+
{
10+
"name": "2025-02-10_1916.sounding-2.csv",
11+
"height": [
12+
20.0, 40.0, 60.0, 80.0, 100.0, 120.0, 140.0, 160.0, 180.0, 200.0, 220.0,
13+
240.0, 260.0, 280.0, 300.0, 320.0, 340.0, 360.0, 380.0, 400.0, 420.0,
14+
440.0, 460.0, 480.0, 500.0, 520.0, 540.0, 560.0, 580.0, 600.0, 620.0,
15+
640.0, 660.0, 680.0, 700.0, 720.0, 740.0, 760.0, 780.0, 800.0, 820.0,
16+
840.0, 860.0, 880.0, 900.0, 920.0, 940.0, 960.0, 980.0, 1000.0, 1020.0,
17+
1040.0, 1060.0, 1080.0, 1100.0, 1120.0, 1140.0, 1160.0, 1180.0, 1200.0,
18+
1220.0, 1240.0, 1260.0, 1280.0, 1300.0, 1320.0, 1340.0, 1360.0, 1380.0,
19+
1400.0, 1420.0, 1440.0, 1460.0, 1480.0, 1500.0, 1520.0, 1540.0, 1560.0,
20+
1580.0, 1600.0, 1620.0, 1640.0, 1660.0, 1680.0, 1700.0, 1720.0, 1740.0,
21+
1760.0, 1780.0, 1800.0, 1820.0, 1840.0, 1860.0, 1880.0
22+
],
23+
"pressure": [
24+
970.06, 967.87, 965.7, 963.52, 961.35, 959.19, 957.03, 954.87, 952.72,
25+
950.57, 948.42, 946.28, 944.14, 942.0, 939.87, 937.74, 935.61, 933.49,
26+
931.37, 929.25, 927.14, 925.03, 922.92, 920.82, 918.72, 916.62, 914.53,
27+
912.44, 910.35, 908.27, 906.19, 904.11, 902.04, 899.97, 897.91, 895.84,
28+
893.78, 891.73, 889.68, 887.63, 885.58, 883.54, 881.5, 879.47, 877.44,
29+
875.41, 873.39, 871.37, 869.35, 867.34, 865.33, 863.32, 861.32, 859.32,
30+
857.33, 855.34, 853.35, 851.37, 849.39, 847.42, 845.44, 843.47, 841.51,
31+
839.54, 837.58, 835.63, 833.68, 831.73, 829.78, 827.84, 825.91, 823.97,
32+
822.04, 820.12, 818.2, 816.28, 814.36, 812.45, 810.54, 808.63, 806.73,
33+
804.83, 802.94, 801.04, 799.16, 797.27, 795.39, 793.51, 791.64, 789.76,
34+
787.9, 786.03, 784.17, 782.32
35+
],
36+
"temperature": [
37+
27.88, 27.98, 28.01, 27.88, 27.61, 27.39, 27.2, 27.05, 26.89, 26.71,
38+
26.53, 26.3, 26.04, 25.85, 25.69, 25.54, 25.35, 25.08, 24.84, 24.66,
39+
24.45, 24.23, 24.02, 23.8, 23.64, 23.49, 23.31, 23.1, 22.85, 22.63,
40+
22.43, 22.21, 22.04, 21.92, 21.84, 21.73, 21.51, 21.28, 21.05, 20.83,
41+
20.62, 20.52, 20.39, 20.2, 20.01, 19.86, 19.77, 19.75, 19.73, 19.63,
42+
19.45, 19.29, 19.14, 19.03, 18.89, 18.68, 18.51, 18.36, 18.23, 18.12,
43+
18.01, 17.95, 17.88, 17.71, 17.53, 17.44, 17.32, 17.18, 17.09, 17.09,
44+
17.09, 17.02, 16.86, 16.73, 16.61, 16.44, 16.19, 15.98, 15.82, 15.63,
45+
15.52, 15.35, 15.09, 14.91, 14.75, 14.63, 14.51, 14.37, 14.23, 14.18,
46+
14.15, 14.06, 13.92, 13.85
47+
],
48+
"relativeHumidity": [
49+
27.5, 26.9, 26.4, 26.5, 26.7, 27.0, 27.1, 27.2, 27.3, 27.5, 27.7, 27.8,
50+
28.1, 28.4, 28.6, 28.8, 28.9, 29.1, 29.3, 29.3, 29.2, 29.0, 29.1, 29.3,
51+
29.1, 28.9, 29.1, 29.5, 30.3, 30.7, 31.3, 32.1, 32.5, 31.9, 31.1, 30.2,
52+
30.3, 30.3, 30.4, 30.5, 30.5, 30.6, 30.9, 31.5, 31.8, 31.5, 30.3, 28.4,
53+
27.2, 27.3, 28.3, 29.2, 30.0, 30.1, 30.1, 30.1, 29.8, 28.5, 26.3, 24.2,
54+
22.0, 18.9, 16.5, 14.6, 13.4, 12.5, 11.7, 10.2, 8.3, 6.5, 5.5, 5.4, 5.4,
55+
5.5, 5.6, 5.6, 5.7, 5.8, 5.9, 6.0, 6.0, 6.2, 6.4, 6.7, 7.2, 7.7, 7.9,
56+
7.9, 7.9, 7.7, 7.6, 7.6, 7.8, 7.9
57+
],
58+
"windSpeed": [
59+
2.79, 3.32, 3.65, 4.27, 4.63, 5.03, 5.27, 5.28, 5.34, 5.32, 5.43, 5.57,
60+
5.55, 5.56, 5.45, 5.26, 5.08, 5.1, 5.21, 5.47, 5.83, 6.12, 6.36, 6.53,
61+
6.61, 6.58, 6.6, 6.67, 6.76, 6.92, 7.06, 7.13, 7.13, 7.11, 7.05, 6.93,
62+
6.83, 6.7, 6.58, 6.47, 6.34, 6.23, 6.14, 6.02, 5.87, 5.7, 5.46, 5.12,
63+
4.83, 4.53, 4.33, 4.11, 3.97, 3.78, 3.61, 3.5, 3.36, 3.24, 3.16, 3.11,
64+
3.14, 3.21, 3.38, 3.56, 3.72, 3.77, 3.82, 3.86, 3.86, 3.67, 3.33, 2.75,
65+
2.27, 1.89, 1.78, 1.78, 1.83, 1.82, 1.69, 1.6, 1.49, 1.46, 1.62, 1.91,
66+
2.06, 2.19, 2.32, 2.53, 2.75, 2.91, 3.02, 3.05, 3.02, 2.99
67+
],
68+
"windDirection": [
69+
236.7, 230.2, 227.8, 229.9, 230.5, 227.0, 226.5, 228.3, 232.7, 232.8,
70+
229.8, 227.8, 226.5, 223.8, 223.7, 225.0, 224.5, 221.8, 219.6, 219.0,
71+
219.8, 221.5, 221.8, 220.7, 220.3, 219.8, 218.4, 218.8, 219.6, 219.5,
72+
219.0, 219.6, 219.4, 218.3, 217.1, 216.1, 216.2, 216.4, 216.5, 216.7,
73+
216.7, 215.8, 214.7, 213.4, 211.2, 208.7, 205.8, 202.0, 198.6, 196.2,
74+
193.0, 191.2, 189.8, 189.1, 187.7, 186.6, 185.8, 187.4, 191.0, 195.0,
75+
197.3, 202.4, 205.8, 209.8, 211.3, 213.4, 216.7, 221.9, 226.8, 231.6,
76+
235.4, 242.8, 251.9, 259.2, 261.7, 267.7, 276.3, 284.3, 283.4, 275.0,
77+
267.3, 271.0, 289.2, 304.3, 315.0, 319.3, 316.0, 315.0, 318.5, 324.0,
78+
327.9, 331.2, 333.4, 334.0
79+
]
80+
}
81+
]
82+
}

0 commit comments

Comments
 (0)