Skip to content

Commit 4fc41dd

Browse files
authored
Merge pull request #165 from classmodel/utctime
Display UTC time
2 parents 48d2623 + a296609 commit 4fc41dd

File tree

9 files changed

+54
-17
lines changed

9 files changed

+54
-17
lines changed

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

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ const flatObservations: () => Observation[] = createMemo(() => {
119119
});
120120

121121
const _allTimes = () =>
122-
new Set(flatExperiments().flatMap((e) => e.output?.t ?? []));
122+
new Set(flatExperiments().flatMap((e) => e.output?.utcTime ?? []));
123123
const uniqueTimes = () => [...new Set(_allTimes())].sort((a, b) => a - b);
124124

125125
// TODO: could memoize all reactive elements here, would it make a difference?
@@ -140,9 +140,15 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
140140
e.output ? e.output[analysis.yVariable as OutputVariableKey] : [],
141141
);
142142

143-
const granularity = () => (analysis.xVariable === "t" ? 600 : undefined);
144-
const xLim = () => getNiceAxisLimits(allX(), 0, granularity());
145-
const yLim = () => getNiceAxisLimits(allY());
143+
const granularities: Record<string, number | undefined> = {
144+
t: 600, // 10 minutes in seconds
145+
utcTime: 60_000, // 1 minute in milliseconds
146+
default: undefined,
147+
};
148+
149+
const roundTo = (v: string) => granularities[v] ?? granularities.default;
150+
const xLim = () => getNiceAxisLimits(allX(), 0, roundTo(analysis.xVariable));
151+
const yLim = () => getNiceAxisLimits(allY(), 0, roundTo(analysis.yVariable));
146152

147153
const chartData = () =>
148154
flatExperiments().map((e) => {
@@ -185,11 +191,14 @@ export function TimeSeriesPlot({ analysis }: { analysis: TimeseriesAnalysis }) {
185191
setResetPlot(analysis.id);
186192
};
187193

188-
const formatX = () =>
189-
analysis.xVariable === "t" ? formatSeconds : d3.format(".4");
190-
const formatY = () =>
191-
analysis.yVariable === "t" ? formatSeconds : d3.format(".4");
192-
194+
// Define axis format
195+
const formatters: Record<string, (value: number) => string> = {
196+
t: formatSeconds,
197+
utcTime: formatUTC,
198+
default: d3.format(".4"),
199+
};
200+
const formatX = () => formatters[analysis.xVariable] ?? formatters.default;
201+
const formatY = () => formatters[analysis.yVariable] ?? formatters.default;
193202
return (
194203
<>
195204
{/* TODO: get label for yVariable from model config */}
@@ -264,7 +273,7 @@ export function VerticalProfilePlot({
264273
const profileData = () =>
265274
flatExperiments().map((e) => {
266275
const { config, output, ...formatting } = e;
267-
const t = output?.t.indexOf(uniqueTimes()[analysis.time]);
276+
const t = output?.utcTime.indexOf(uniqueTimes()[analysis.time]);
268277
if (config.sw_ml && output && t !== undefined && t !== -1) {
269278
const outputAtTime = getOutputAtTime(output, t);
270279
return { ...formatting, data: generateProfiles(config, outputAtTime) };
@@ -407,6 +416,10 @@ function formatSeconds(seconds: number): string {
407416
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
408417
}
409418

419+
function formatUTC(milliseconds: number): string {
420+
return d3.utcFormat("%H:%M")(new Date(milliseconds));
421+
}
422+
410423
function TimeSlider(
411424
time: Accessor<number>,
412425
timeOptions: Accessor<number[]>,
@@ -429,12 +442,12 @@ function TimeSlider(
429442
class="w-full max-w-md"
430443
>
431444
<div class="flex w-full items-center gap-5">
432-
<p>Time: </p>
445+
<p class="whitespace-nowrap">Time (UTC): </p>
433446
<SliderTrack>
434447
<SliderFill />
435448
<SliderThumb />
436449
</SliderTrack>
437-
<p>{formatSeconds(timeOptions()[time()])}</p>
450+
<p>{formatUTC(timeOptions()[time()])}</p>
438451
</div>
439452
</Slider>
440453
);
@@ -468,7 +481,7 @@ export function ThermodynamicPlot({ analysis }: { analysis: SkewTAnalysis }) {
468481
const profileData = () =>
469482
flatExperiments().map((e) => {
470483
const { config, output, ...formatting } = e;
471-
const t = output?.t.indexOf(uniqueTimes()[analysis.time]);
484+
const t = output?.utcTime.indexOf(uniqueTimes()[analysis.time]);
472485
if (config.sw_ml && output && t !== undefined && t !== -1) {
473486
const outputAtTime = getOutputAtTime(output, t);
474487
return { ...formatting, data: generateProfiles(config, outputAtTime) };

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@ export function addAnalysis(name: string) {
279279
description: "",
280280
type: "timeseries",
281281
name: "Timeseries",
282-
xVariable: "t",
282+
xVariable: "utcTime",
283283
yVariable: "h",
284284
} as TimeseriesAnalysis;
285285
break;

packages/class/src/class.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,13 @@ export class CLASS {
270270
return this.ml?.qt || 999;
271271
}
272272

273+
get utcTime() {
274+
// export time in milliseconds since epoch so bounds calculation and
275+
// rendering can happen on app side
276+
const t0 = new Date(this._cfg.t0).getTime();
277+
return t0 + this.t * 1000;
278+
}
279+
273280
/**
274281
* Retrieve a value by name, treating nested state (wind, ml) as if it's flat.
275282
*

packages/class/src/config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ const untypedSchema = {
2828
default: "",
2929
"ui:widget": "textarea",
3030
},
31+
// Start date / time in utc
32+
t0: {
33+
type: "string",
34+
title: "Start date and time (ISO 8601)",
35+
default: new Date().toISOString(),
36+
"ui:group": "Time Control",
37+
},
3138
dt: {
3239
type: "integer",
3340
unit: "s",
@@ -63,7 +70,7 @@ const untypedSchema = {
6370
default: false,
6471
},
6572
},
66-
required: ["name", "dt", "runtime"],
73+
required: ["name", "t0", "dt", "runtime"],
6774
allOf: [
6875
{
6976
if: {
@@ -479,6 +486,7 @@ const untypedSchema = {
479486
type GeneralConfig = {
480487
name: string;
481488
description?: string;
489+
t0: string;
482490
dt: number;
483491
runtime: number;
484492
};

packages/class/src/config_utils.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ describe("pruneConfig()", () => {
77
const preset = {
88
name: "Default",
99
description: "Default configuration",
10+
t0: "2025-09-08T14:30:00Z",
1011
theta_0: 323,
1112
h_0: 200,
1213
dtheta_0: 1,
@@ -26,6 +27,7 @@ describe("pruneConfig()", () => {
2627
const reference = {
2728
name: "Higher and Hotter",
2829
description: "Higher h_0",
30+
t0: "2025-09-08T14:30:00Z",
2931
h_0: 211,
3032
theta_0: 323,
3133
dtheta_0: 1,
@@ -45,6 +47,7 @@ describe("pruneConfig()", () => {
4547
const permutation = {
4648
name: "Higher",
4749
description: "",
50+
t0: "2025-09-08T14:30:00Z",
4851
h_0: 222,
4952
theta_0: 323,
5053
dtheta_0: 1,

packages/class/src/config_utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function pruneConfig(
7373
for (const key in config) {
7474
const k = key as keyof typeof config;
7575
const k2 = key as keyof typeof config;
76-
if (typeof config[k] === "string") {
76+
if (k === "name" || k === "description") {
7777
// Do not prune name and description
7878
continue;
7979
}

packages/class/src/output.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export const outputVariables = {
1010
unit: "s",
1111
symbol: "t",
1212
},
13+
utcTime: {
14+
title: "Time (UTC)",
15+
unit: "-",
16+
symbol: "UTC",
17+
},
1318
h: {
1419
title: "ABL height",
1520
unit: "m",

packages/class/src/runner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function runClass(config: Config, freq = 600): ClassOutput {
4242
writeOutput();
4343

4444
// Update loop
45-
while (model.t < config.runtime) {
45+
while (model.t <= config.runtime) {
4646
model.update();
4747

4848
if (model.t % freq === 0) {

packages/class/src/validate.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ describe("parse", () => {
5050
const output = parse(input);
5151

5252
const expected = {
53+
t0: output.t0, // dynamic default
5354
h: 200,
5455
theta: 288,
5556
dtheta: 1,

0 commit comments

Comments
 (0)