Skip to content
Merged
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
48 changes: 39 additions & 9 deletions apps/class-solid/src/components/Analysis.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import {
createMemo,
createUniqueId,
} from "solid-js";
import { getThermodynamicProfiles, getVerticalProfiles } from "~/lib/profiles";
import type { Observation } from "~/lib/experiment_config";
import {
getThermodynamicProfiles,
getVerticalProfiles,
observationsForProfile,
observationsForSounding,
} from "~/lib/profiles";
import {
type Analysis,
type ProfilesAnalysis,
Expand Down Expand Up @@ -68,7 +74,7 @@ const flatExperiments: () => FlatExperiment[] = createMemo(() => {
return experiments
.filter((e) => e.output.running === false) // skip running experiments
.flatMap((e, i) => {
const reference = {
const reference: FlatExperiment = {
color: colors[0],
linestyle: linestyles[i % 5],
label: e.config.reference.name,
Expand All @@ -90,6 +96,15 @@ const flatExperiments: () => FlatExperiment[] = createMemo(() => {
});
});

// Derived store for all observations of all experiments combined
const flatObservations: () => Observation[] = createMemo(() => {
return experiments
.filter((e) => e.config.observations)
.flatMap((e) => {
return e.config.observations || [];
});
});

const _allTimes = () =>
new Set(flatExperiments().flatMap((e) => e.output?.t ?? []));
const uniqueTimes = () => [...new Set(_allTimes())].sort((a, b) => a - b);
Expand Down Expand Up @@ -166,12 +181,23 @@ export function VerticalProfilePlot({
const classVariable = () =>
variableOptions[analysis.variable as keyof typeof variableOptions];

const allValues = () =>
flatExperiments().flatMap((e) =>
const observations = () =>
flatObservations().map((o) => observationsForProfile(o, classVariable()));
const obsAllX = () =>
observations().flatMap((obs) => obs.data.map((d) => d.x));
const obsAllY = () =>
observations().flatMap((obs) => obs.data.map((d) => d.y));

const allValues = () => [
...flatExperiments().flatMap((e) =>
e.output ? e.output[classVariable()] : [],
);
const allHeights = () =>
flatExperiments().flatMap((e) => (e.output ? e.output.h : []));
),
...obsAllX(),
];
const allHeights = () => [
...flatExperiments().flatMap((e) => (e.output ? e.output.h : [])),
...obsAllY(),
];

// TODO: better to include jump at top in extent calculation rather than adding random margin.
const xLim = () => getNiceAxisLimits(allValues(), 1);
Expand Down Expand Up @@ -199,11 +225,12 @@ export function VerticalProfilePlot({
<>
<div class="flex flex-col gap-2">
<ChartContainer>
<Legend entries={profileData} />
<Legend entries={() => [...profileData(), ...observations()]} />
<Chart title="Vertical profile plot">
<AxisBottom domain={xLim} label={analysis.variable} />
<AxisLeft domain={yLim} label="Height[m]" />
<For each={profileData()}>{(d) => Line(d)}</For>
<For each={observations()}>{(d) => Line(d)}</For>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why show as a dotted line?

This gives the impression that each dot is an observation event.

Also rendering as dotted line makes it look like second experiment (which is also a dotted line)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do this as part of #110 (comment)

</Chart>
</ChartContainer>
<Picker
Expand Down Expand Up @@ -307,9 +334,12 @@ export function ThermodynamicPlot({ analysis }: { analysis: SkewTAnalysis }) {
};
});

const observations = () =>
flatObservations().map((o) => observationsForSounding(o));

return (
<>
<SkewTPlot data={skewTData} />
<SkewTPlot data={() => [...skewTData(), ...observations()]} />
{TimeSlider(
() => analysis.time,
uniqueTimes,
Expand Down
1 change: 1 addition & 0 deletions apps/class-solid/src/lib/encode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function toPartial(config: ExperimentConfig): PartialExperimentConfig {
permutations: config.permutations.map((perm) =>
pruneConfig(perm, config.reference, preset.config),
),
observations: config.observations,
};
}

Expand Down
44 changes: 43 additions & 1 deletion apps/class-solid/src/lib/experiment_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ import { overwriteDefaultsInJsonSchema } from "@classmodel/form/utils";
import type { DefinedError, JSONSchemaType } from "ajv";
import { findPresetByName } from "./presets";

/*
Height (m AGL) Pressure (mb) Temperature (C) Relative humidity (%) Wind speed (m/s) Wind direction (true deg)
*/
export interface Observation {
name: string;
height: number[];
pressure: number[];
temperature: number[];
relativeHumidity: number[];
windSpeed: number[];
windDirection: number[];
}

/**
* An experiment configuration is a combination of a preset name and
* a reference configuration and a set of permutation configurations.
Expand All @@ -13,6 +26,7 @@ export interface ExperimentConfig<C = Config> {
preset: string;
reference: C;
permutations: C[];
observations?: Observation[];
}

/**
Expand Down Expand Up @@ -42,6 +56,30 @@ const jsonSchemaOfExperimentConfigBase = {
},
default: [],
},
observations: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
height: { type: "array", items: { type: "number" } },
pressure: { type: "array", items: { type: "number" } },
temperature: { type: "array", items: { type: "number" } },
relativeHumidity: { type: "array", items: { type: "number" } },
windSpeed: { type: "array", items: { type: "number" } },
windDirection: { type: "array", items: { type: "number" } },
},
required: [
"name",
"height",
"pressure",
"temperature",
"relativeHumidity",
"windSpeed",
"windDirection",
],
},
},
},
required: ["preset", "reference", "permutations"],
} as unknown as JSONSchemaType<ExperimentConfig<object>>;
Expand Down Expand Up @@ -89,9 +127,13 @@ export function parseExperimentConfig(input: unknown): ExperimentConfig {
}

const permutations = base.permutations.map(permParse);
return {
const config: ExperimentConfig = {
reference,
preset: base.preset,
permutations,
};
if (base.observations) {
config.observations = base.observations;
}
return config;
}
64 changes: 64 additions & 0 deletions apps/class-solid/src/lib/profiles.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Config } from "@classmodel/class/config";
import type { ClassOutput } from "@classmodel/class/runner";
import type { Point } from "~/components/plots/Line";
import type { Observation } from "./experiment_config";

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

/**
* Calculate potential temperature from temperature
*/
const tToTheta = (T: number, p: number, p0 = 1000) => {
const R = 287; // specific gas constant for dry air
const cp = 1004; // specific heat of dry air at constant pressure
const theta = T * (p / p0) ** -(R / cp);
return theta;
};

/**
* Calculate pressure difference over layer using hypsometric equation
* Wallace & Hobbs eq (3.23)
Expand All @@ -96,6 +107,24 @@ const thickness = (T: number, q: number, p: number, dp: number) => {
return dz;
};

function calculateSpecificHumidity(T: number, p: number, rh: number) {
// Constants
const epsilon = 0.622; // Ratio of gas constants for dry air and water vapor
const es0 = 6.112; // Reference saturation vapor pressure in hPa

// Calculate saturation vapor pressure (Tetens formula)
const es = es0 * Math.exp((17.67 * (T - 273.15)) / (T - 29.65));

// Actual vapor pressure
const e = (rh / 100) * es;

// Mixing ratio (kg/kg)
const w = (epsilon * e) / (p - e);

// Specific humidity
return w / (1 + w);
}

export function getThermodynamicProfiles(
output: ClassOutput | undefined,
config: Config,
Expand Down Expand Up @@ -153,3 +182,38 @@ export function getThermodynamicProfiles(

return soundingData;
}

export function observationsForProfile(obs: Observation, variable = "theta") {
return {
label: obs.name,
color: "red",
linestyle: "3,10",
data: obs.height.map((h, i) => {
const T = obs.temperature[i] + 273.15;
const rh = obs.relativeHumidity[i];
const p = obs.pressure[i];
const theta = tToTheta(T, p);
const q = calculateSpecificHumidity(T, p, rh);
if (variable === "theta") {
return { y: h, x: theta };
}
return { y: h, x: q };
}),
};
}

export function observationsForSounding(obs: Observation) {
return {
data: obs.height.map((h, i) => {
const T = obs.temperature[i] + 273.15;
const p = obs.pressure[i];
const rh = obs.relativeHumidity[i];
const q = calculateSpecificHumidity(T, p, rh);
const Td = dewpoint(q, p);
return { p, T, Td };
}),
label: obs.name,
color: "red",
linestyle: "3,10",
};
}
3 changes: 3 additions & 0 deletions apps/class-solid/src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export async function uploadExperiment(rawData: unknown) {
running: false,
},
};
if (upload.observations) {
experiment.config.observations = upload.observations;
}
setExperiments(experiments.length, experiment);
await runExperiment(experiments.length - 1);
}
Expand Down
82 changes: 82 additions & 0 deletions apps/class-solid/tests/config-with-observations.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"reference": {
"name": "EWED 1916",
"description": "Default config with observations"
},
"preset": "Default",
"permutations": [],
"observations": [
{
"name": "2025-02-10_1916.sounding-2.csv",
"height": [
20.0, 40.0, 60.0, 80.0, 100.0, 120.0, 140.0, 160.0, 180.0, 200.0, 220.0,
240.0, 260.0, 280.0, 300.0, 320.0, 340.0, 360.0, 380.0, 400.0, 420.0,
440.0, 460.0, 480.0, 500.0, 520.0, 540.0, 560.0, 580.0, 600.0, 620.0,
640.0, 660.0, 680.0, 700.0, 720.0, 740.0, 760.0, 780.0, 800.0, 820.0,
840.0, 860.0, 880.0, 900.0, 920.0, 940.0, 960.0, 980.0, 1000.0, 1020.0,
1040.0, 1060.0, 1080.0, 1100.0, 1120.0, 1140.0, 1160.0, 1180.0, 1200.0,
1220.0, 1240.0, 1260.0, 1280.0, 1300.0, 1320.0, 1340.0, 1360.0, 1380.0,
1400.0, 1420.0, 1440.0, 1460.0, 1480.0, 1500.0, 1520.0, 1540.0, 1560.0,
1580.0, 1600.0, 1620.0, 1640.0, 1660.0, 1680.0, 1700.0, 1720.0, 1740.0,
1760.0, 1780.0, 1800.0, 1820.0, 1840.0, 1860.0, 1880.0
],
"pressure": [
970.06, 967.87, 965.7, 963.52, 961.35, 959.19, 957.03, 954.87, 952.72,
950.57, 948.42, 946.28, 944.14, 942.0, 939.87, 937.74, 935.61, 933.49,
931.37, 929.25, 927.14, 925.03, 922.92, 920.82, 918.72, 916.62, 914.53,
912.44, 910.35, 908.27, 906.19, 904.11, 902.04, 899.97, 897.91, 895.84,
893.78, 891.73, 889.68, 887.63, 885.58, 883.54, 881.5, 879.47, 877.44,
875.41, 873.39, 871.37, 869.35, 867.34, 865.33, 863.32, 861.32, 859.32,
857.33, 855.34, 853.35, 851.37, 849.39, 847.42, 845.44, 843.47, 841.51,
839.54, 837.58, 835.63, 833.68, 831.73, 829.78, 827.84, 825.91, 823.97,
822.04, 820.12, 818.2, 816.28, 814.36, 812.45, 810.54, 808.63, 806.73,
804.83, 802.94, 801.04, 799.16, 797.27, 795.39, 793.51, 791.64, 789.76,
787.9, 786.03, 784.17, 782.32
],
"temperature": [
27.88, 27.98, 28.01, 27.88, 27.61, 27.39, 27.2, 27.05, 26.89, 26.71,
26.53, 26.3, 26.04, 25.85, 25.69, 25.54, 25.35, 25.08, 24.84, 24.66,
24.45, 24.23, 24.02, 23.8, 23.64, 23.49, 23.31, 23.1, 22.85, 22.63,
22.43, 22.21, 22.04, 21.92, 21.84, 21.73, 21.51, 21.28, 21.05, 20.83,
20.62, 20.52, 20.39, 20.2, 20.01, 19.86, 19.77, 19.75, 19.73, 19.63,
19.45, 19.29, 19.14, 19.03, 18.89, 18.68, 18.51, 18.36, 18.23, 18.12,
18.01, 17.95, 17.88, 17.71, 17.53, 17.44, 17.32, 17.18, 17.09, 17.09,
17.09, 17.02, 16.86, 16.73, 16.61, 16.44, 16.19, 15.98, 15.82, 15.63,
15.52, 15.35, 15.09, 14.91, 14.75, 14.63, 14.51, 14.37, 14.23, 14.18,
14.15, 14.06, 13.92, 13.85
],
"relativeHumidity": [
27.5, 26.9, 26.4, 26.5, 26.7, 27.0, 27.1, 27.2, 27.3, 27.5, 27.7, 27.8,
28.1, 28.4, 28.6, 28.8, 28.9, 29.1, 29.3, 29.3, 29.2, 29.0, 29.1, 29.3,
29.1, 28.9, 29.1, 29.5, 30.3, 30.7, 31.3, 32.1, 32.5, 31.9, 31.1, 30.2,
30.3, 30.3, 30.4, 30.5, 30.5, 30.6, 30.9, 31.5, 31.8, 31.5, 30.3, 28.4,
27.2, 27.3, 28.3, 29.2, 30.0, 30.1, 30.1, 30.1, 29.8, 28.5, 26.3, 24.2,
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,
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,
7.9, 7.9, 7.7, 7.6, 7.6, 7.8, 7.9
],
"windSpeed": [
2.79, 3.32, 3.65, 4.27, 4.63, 5.03, 5.27, 5.28, 5.34, 5.32, 5.43, 5.57,
5.55, 5.56, 5.45, 5.26, 5.08, 5.1, 5.21, 5.47, 5.83, 6.12, 6.36, 6.53,
6.61, 6.58, 6.6, 6.67, 6.76, 6.92, 7.06, 7.13, 7.13, 7.11, 7.05, 6.93,
6.83, 6.7, 6.58, 6.47, 6.34, 6.23, 6.14, 6.02, 5.87, 5.7, 5.46, 5.12,
4.83, 4.53, 4.33, 4.11, 3.97, 3.78, 3.61, 3.5, 3.36, 3.24, 3.16, 3.11,
3.14, 3.21, 3.38, 3.56, 3.72, 3.77, 3.82, 3.86, 3.86, 3.67, 3.33, 2.75,
2.27, 1.89, 1.78, 1.78, 1.83, 1.82, 1.69, 1.6, 1.49, 1.46, 1.62, 1.91,
2.06, 2.19, 2.32, 2.53, 2.75, 2.91, 3.02, 3.05, 3.02, 2.99
],
"windDirection": [
236.7, 230.2, 227.8, 229.9, 230.5, 227.0, 226.5, 228.3, 232.7, 232.8,
229.8, 227.8, 226.5, 223.8, 223.7, 225.0, 224.5, 221.8, 219.6, 219.0,
219.8, 221.5, 221.8, 220.7, 220.3, 219.8, 218.4, 218.8, 219.6, 219.5,
219.0, 219.6, 219.4, 218.3, 217.1, 216.1, 216.2, 216.4, 216.5, 216.7,
216.7, 215.8, 214.7, 213.4, 211.2, 208.7, 205.8, 202.0, 198.6, 196.2,
193.0, 191.2, 189.8, 189.1, 187.7, 186.6, 185.8, 187.4, 191.0, 195.0,
197.3, 202.4, 205.8, 209.8, 211.3, 213.4, 216.7, 221.9, 226.8, 231.6,
235.4, 242.8, 251.9, 259.2, 261.7, 267.7, 276.3, 284.3, 283.4, 275.0,
267.3, 271.0, 289.2, 304.3, 315.0, 319.3, 316.0, 315.0, 318.5, 324.0,
327.9, 331.2, 333.4, 334.0
]
}
]
}