File size: 25,043 Bytes
353cc28 f340e6f a0e4229 f59ae9e f340e6f 353cc28 a0e4229 f340e6f a0e4229 f340e6f a0e4229 f340e6f a0e4229 f340e6f a0e4229 f340e6f a0e4229 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 a0e4229 353cc28 a0e4229 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f a0e4229 353cc28 a0e4229 f340e6f 353cc28 f340e6f 353cc28 a0e4229 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 a0e4229 f340e6f 353cc28 f340e6f a0e4229 353cc28 a0e4229 353cc28 a0e4229 353cc28 f340e6f a0e4229 353cc28 a0e4229 353cc28 f340e6f a0e4229 f340e6f a0e4229 353cc28 a0e4229 f340e6f a0e4229 353cc28 a0e4229 353cc28 a0e4229 f340e6f a0e4229 353cc28 a0e4229 f59ae9e a0e4229 353cc28 f340e6f a0e4229 353cc28 a0e4229 353cc28 a0e4229 353cc28 a0e4229 353cc28 a0e4229 f59ae9e 353cc28 a0e4229 353cc28 a0e4229 353cc28 a0e4229 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 f340e6f 353cc28 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 |
# %%
import xarray as xr
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import matplotlib.colors as mcolors
import streamlit as st
import datetime
import os
from utils import latlon_to_xy
import plotly.graph_objects as go
from matplotlib.colors import to_hex, LinearSegmentedColormap
from plotly.subplots import make_subplots
@st.cache_data(ttl=7200)
def load_data():
"""
Loads a NetCDF file containing forecast data. Example
<xarray.Dataset> Size: 483MB
Dimensions: (altitude: 34, time: 67, y: 81, x: 81)
Coordinates:
height float32 4B 0.0
hybrid (altitude) float64 272B 0.6784 0.6984 ... 0.9985
* x (x) float32 324B -5.101e+05 -5.076e+05 ... -3.101e+05
* y (y) float32 324B -2.825e+05 -2.8e+05 ... -8.252e+04
* time (time) datetime64[ns] 536B 2025-03-13T12:00:00 ... ...
longitude (y, x) float64 52kB 5.684 5.729 5.774 ... 8.919 8.967
latitude (y, x) float64 52kB 60.43 60.43 60.43 ... 62.42 62.43
* altitude (altitude) float64 272B 2.89e+03 2.684e+03 ... 11.66
Data variables:
ap (altitude) float64 272B 5.682e+03 5.01e+03 ... 0.0 0.0
b (altitude) float64 272B 0.6223 0.6489 ... 0.9985
air_temperature_ml (time, altitude, y, x) float32 60MB 252.5 ... 260.7
x_wind_ml (time, altitude, y, x) float32 60MB -1.6 ... 5.709
y_wind_ml (time, altitude, y, x) float32 60MB -11.19 ... -10.67
surface_air_pressure (time, y, x, altitude) float32 60MB 9.46e+04 ... 8....
elevation (y, x, altitude) float32 892kB 465.8 ... 1.543e+03
air_temperature_0m (time, y, x, altitude) float32 60MB 278.6 ... 261.1
wind_speed (time, altitude, y, x) float32 60MB 11.31 ... 12.1
thermal_temp_diff (time, y, x, altitude) float64 120MB 2.331 ... 0.3471
thermal_top (time, y, x) float64 4MB 2.89e+03 ... 2.89e+03
Attributes: (12/41)
min_time: 2025-03-13T12:00:00Z
geospatial_lat_min: 49.8
geospatial_lat_max: 75.2
geospatial_lon_min: -18.1
geospatial_lon_max: 54.2
comment: For more information, please visit https://g...
... ...
publisher_name: Norwegian Meteorological Institute
summary: This file contains model level parameters fr...
summary_no: Denne filen inneholder modelnivåparametere f...
title: Meps 2.5Km deterministic model level paramet...
title_no: Meps 2.5Km deterministisk modellnivåparamete...
related_dataset: no.met:8c94c7de-6328-4113-9e77-8f090999fab9 ...
"""
# Find all files in the forecasts directory
forecast_dir = "forecasts"
# Get a list of all NetCDF files
nc_files = [f for f in os.listdir(forecast_dir) if f.endswith(".nc")]
if not nc_files:
raise FileNotFoundError("No forecast files found in the 'forecasts' directory")
# Sort files by their timestamp, assuming the filenames contain the timestamp
nc_files.sort()
# Choose the latest file
latest_file = os.path.join(forecast_dir, nc_files[-1])
# Load the dataset from the latest file
subset = xr.open_dataset(latest_file)
return subset
def wind_and_temp_colorscales(wind_max=20, tempdiff_max=8):
# build colorscale for thermal temperature difference
wind_colors = ["grey", "blue", "green", "yellow", "red", "purple"]
wind_positions = [0, 0.5, 3, 7, 12, 20] # transition points
wind_positions_norm = [i / wind_max for i in wind_positions]
# Create the colormap
windcolors = mcolors.LinearSegmentedColormap.from_list(
"", list(zip(wind_positions_norm, wind_colors))
)
# build colorscale for thermal temperature difference
thermal_colors = ["white", "white", "red", "violet", "darkviolet"]
thermal_positions = [0, 0.2, 2.0, 4, 8]
thermal_positions_norm = [i / tempdiff_max for i in thermal_positions]
# Create the colormap
tempcolors = mcolors.LinearSegmentedColormap.from_list(
"", list(zip(thermal_positions_norm, thermal_colors))
)
return windcolors, tempcolors
@st.cache_data(ttl=60)
def create_wind_map(
_subset, x_target, y_target, altitude_max=4000, date_start=None, date_end=None
):
"""
_subset = subset
altitude_max = 3000
x_target = -422175.14005226345
y_target = -204279.84596708667
"""
subset = _subset
wind_min, wind_max = 0.3, 20
tempdiff_min, tempdiff_max = 0, 8
wind_colors = ["grey", "blue", "green", "yellow", "red", "purple"]
if date_start is None:
date_start = datetime.datetime.fromtimestamp(
subset.time.min().values.astype("int64") // 1e9
)
if date_end is None:
date_end = datetime.datetime.fromtimestamp(
subset.time.max().values.astype("int64") // 1e9
)
# Resample time and altitude for the wind plot data.
new_timestamps = pd.date_range(date_start, date_end, 20)
new_altitude = np.arange(subset.elevation.mean(), altitude_max, altitude_max / 20)
windplot_data = subset.sel(x=x_target, y=y_target, method="nearest")
windplot_data = windplot_data.interp(altitude=new_altitude, time=new_timestamps)
# Convert data for Plotly heatmap
thermal_diff = windplot_data["thermal_temp_diff"].T.values
times = [pd.Timestamp(time).strftime("%H:%M") for time in windplot_data.time.values]
altitudes = windplot_data.altitude.values
# Creating Plotly heatmap
fig = go.Figure(
data=go.Heatmap(
z=thermal_diff,
x=times,
y=altitudes,
colorscale="YlGn",
colorbar=dict(title="Thermal Temperature Difference (°C)"),
zmin=tempdiff_min,
zmax=tempdiff_max,
)
)
# Add wind quiver plots (Note: Plotly doesn't support quivers directly like matplotlib; consider using streamlines or other visualization methods for precise vector representation).
speed = np.sqrt(windplot_data["x_wind_ml"] ** 2 + windplot_data["y_wind_ml"] ** 2).T
fig.add_trace(
go.Scatter(
x=times,
y=altitudes,
mode="markers",
marker=dict(
size=8,
color=speed,
colorscale=wind_colors,
colorbar=dict(title="Wind Speed (m/s)"),
),
# text=[f"Speed: {s:.2f} m/s" for s in speed.squeeze()],
hoverinfo="text",
)
)
# Update layout
fig.update_layout(
title=f"Wind and Thermals Starting at {date_start.strftime('%Y-%m-%d')} (UTC)",
xaxis=dict(title="Time"),
yaxis=dict(title="Altitude (m)"),
)
return fig
# %%
@st.cache_data(ttl=7200)
def create_sounding(_subset, date, hour, x_target, y_target, altitude_max=3000):
"""
date = "2024-05-12"
hour = "15"
x_target = 5
y_target = 5
"""
subset = _subset
lapse_rate = 0.0098 # in degrees Celsius per meter
subset = subset.where(subset.altitude < altitude_max, drop=True)
# Create a figure object
fig, ax = plt.subplots()
# Define the dry adiabatic lapse rate
def add_dry_adiabatic_lines(ds):
# Define a range of temperatures at sea level
T0 = np.arange(-40, 40, 5) # temperatures from -40°C to 40°C in steps of 10°C
# Create a 2D grid of temperatures and altitudes
T0, altitude = np.meshgrid(T0, ds.altitude)
# Calculate the temperatures at each altitude
T_adiabatic = T0 - lapse_rate * altitude
# Plot the dry adiabatic lines
for i in range(T0.shape[1]):
ax.plot(T_adiabatic[:, i], ds.altitude, "r:", alpha=0.5)
# Plot the actual temperature profiles
time_str = f"{date} {hour}:00:00"
# find x and y values cloeset to given latitude and longitude
ds_time = subset.sel(time=time_str, x=x_target, y=y_target, method="nearest")
T = ds_time["air_temperature_ml"].values - 273.3 # in degrees Celsius
ax.plot(
T, ds_time.altitude, label=f"temp {pd.to_datetime(time_str).strftime('%H:%M')}"
)
# Define the surface temperature
T_surface = T[-1] + 3
T_parcel = T_surface - lapse_rate * ds_time.altitude
# Plot the temperature of the rising air parcel
filter = T_parcel > T
ax.plot(
T_parcel[filter],
ds_time.altitude[filter],
label="Rising air parcel",
color="green",
)
add_dry_adiabatic_lines(ds_time)
ax.set_xlabel("Temperature (°C)")
ax.set_ylabel("Altitude (m)")
ax.set_title(
f"Temperature Profile and Dry Adiabatic Lapse Rate for {date} {hour}:00"
)
ax.legend(title="Time")
xmin, xmax = (
ds_time["air_temperature_ml"].min().values - 273.3,
ds_time["air_temperature_ml"].max().values - 273.3 + 3,
)
ax.set_xlim(xmin, xmax)
ax.grid(True)
# Return the figure object
return fig
# %%
def date_controls(subset):
start_stop_time = [
subset.time.min().values.astype("M8[D]").astype("O"),
subset.time.max().values.astype("M8[D]").astype("O"),
]
now = datetime.datetime.now().replace(minute=0, second=0, microsecond=0).date()
if "forecast_date" not in st.session_state:
st.session_state.forecast_date = now
if "forecast_time" not in st.session_state:
st.session_state.forecast_time = datetime.time(14, 0)
if "altitude_max" not in st.session_state:
st.session_state.altitude_max = 3000
if "target_latitude" not in st.session_state:
st.session_state.target_latitude = 61.22908
if "target_longitude" not in st.session_state:
st.session_state.target_longitude = 7.09674
# Generate available days within the dataset's time range
available_days = pd.date_range(
start=start_stop_time[0], end=start_stop_time[1]
).date
day_cols = st.columns(len(available_days)) # Create columns for each available day
for i, day in enumerate(available_days):
label = day.strftime("%A") # Get day label
if day == now:
label += " (today)"
with day_cols[i]: # Place each button in its respective column
if st.button(label):
st.session_state.forecast_date = day
# Group hours into smaller rows for better layout
hours_per_row = 24 # Define how many hour buttons to display per row
available_hours = range(7, 22, 1) # 24-hour format
# Divide hours into batches
hour_batches = [
available_hours[i : i + hours_per_row]
for i in range(0, len(available_hours), hours_per_row)
]
# Display hour buttons in rows
for batch in hour_batches:
hour_cols = st.columns(len(batch))
for i, hour in enumerate(batch):
label = f"{hour:02}:00"
with hour_cols[i]:
if st.button(label):
st.session_state.forecast_time = datetime.time(hour, 0)
def build_map(_subset, date=None, hour=None):
subset = _subset
latitude_values = subset.latitude.values.flatten()
longitude_values = subset.longitude.values.flatten()
thermal_top_values = (
subset.thermal_top.sel(time=f"{date}T{hour}").values.flatten().round()
)
# Use Plotly's scattermap for visualization and enable click events
scatter_map = go.Scattermap(
lat=latitude_values,
lon=longitude_values,
mode="markers",
marker=go.scattermap.Marker(
size=9,
color=thermal_top_values,
colorscale="Viridis",
colorbar=dict(title="Thermal Height (m)"),
),
text=[f"Thermal Height: {ht} m" for ht in thermal_top_values],
hoverinfo="text",
)
fig = go.Figure(scatter_map)
fig.update_layout(
map_style="open-street-map",
map=dict(center=dict(lat=61.22908, lon=7.09674), zoom=9),
margin={"r": 0, "t": 0, "l": 0, "b": 0},
)
# Register click event callback
return fig
def interpolate_color(
wind_speed, thresholds=[2, 8, 14], colors=["white", "green", "red", "black"]
):
# Normalize thresholds to range [0, 1]
norm_thresholds = [t / max(thresholds) for t in thresholds]
norm_thresholds = [0] + norm_thresholds + [1]
# Extend color list to match normalized thresholds
extended_colors = [colors[0]] + colors + [colors[-1]]
# Create colormap
cmap = LinearSegmentedColormap.from_list(
"wind_speed_cmap", list(zip(norm_thresholds, extended_colors)), N=256
)
# Normalize wind speed to range [0, 1] and get color
norm_wind_speed = wind_speed / max(thresholds)
return to_hex(cmap(np.clip(norm_wind_speed, 0, 1)))
def create_daily_thermal_and_wind_airgram(subset, x_target, y_target, date):
"""
Create a Plotly subplot figure for a single day's thermal and wind data.
The top subplot shows wind data as arrows for direction and color for strength.
The bottom subplot shows thermal temperature differences.
"""
# Define the time window to display
display_start_hour = 7
display_end_hour = 21
# Extract the day that matches the provided date
start_date = pd.Timestamp(date).normalize()
end_date = start_date + pd.Timedelta(days=1)
# Select data for the given date
daily_data = subset.sel(time=slice(start_date, end_date))
# Create time mask for the given display window
time_values = pd.to_datetime(
daily_data.time.values
) # Convert numpy.datetime64 to datetime
mask = [(display_start_hour <= t.hour < display_end_hour) for t in time_values]
# Filter data within the specified hours
daily_data = daily_data.isel(time=mask)
# Select nearest points for the supplied x and y indices
location_data = daily_data.sel(x=x_target, y=y_target, method="nearest")
# Interpolating the data for visualization
new_timestamps = pd.date_range(
start=start_date, end=end_date, freq="h"
) # Every full hour
# Remove timestamps that are outside the range of the data
new_timestamps = new_timestamps[
(new_timestamps >= location_data.time.min().values)
& (new_timestamps <= location_data.time.max().values)
]
altitudes_thermal = np.arange(0, 3000, 200) # Every 200 meters
altitudes_thermal = altitudes_thermal[
(altitudes_thermal >= location_data.altitude.min().values)
& (altitudes_thermal <= location_data.altitude.max().values)
]
# Interpolate thermal temperature difference for the specified times and altitudes
thermal_diff = (
location_data["thermal_temp_diff"]
.interp(time=new_timestamps, altitude=altitudes_thermal)
.T.values
)
# Generating time labels for the x-axis
times = [t.strftime("%H:%M") for t in new_timestamps]
# Calculate wind data at 500m intervals
altitudes_wind = np.arange(0, 3000, 500)
altitudes_wind = altitudes_wind[
(altitudes_wind >= location_data.altitude.min().values)
& (altitudes_wind <= location_data.altitude.max().values)
]
x_wind = location_data["x_wind_ml"].interp(
time=new_timestamps, altitude=altitudes_wind
)
y_wind = location_data["y_wind_ml"].interp(
time=new_timestamps, altitude=altitudes_wind
)
# Calculate wind speed and direction
speed = np.sqrt(x_wind**2 + y_wind**2).T.values
angles = np.rad2deg(np.arctan2(y_wind, x_wind)).T.values # Convert to degrees
angles = angles = (angles + 90) % 360
# Create a subplot figure with shared x-axis
fig = make_subplots(
rows=2,
cols=1,
shared_xaxes=True,
row_heights=[0.3, 0.7],
vertical_spacing=0.05,
subplot_titles=("Wind Speed and Direction", "Thermal Temperature Difference"),
)
# Add wind data plot as rotated triangular markers with a common legend
for i, alt in enumerate(altitudes_wind):
fig.add_trace(
go.Scatter(
x=times,
y=[alt] * len(times),
mode="markers",
marker=dict(
symbol="arrow",
size=20,
angle=angles[i],
color=[interpolate_color(s) for s in speed[i]],
# colorscale="Viridis",
showscale=False, # Hide individual color scales
cmin=0,
cmax=20,
),
hoverinfo="text",
text=[
f"Alt: {alt} m, Speed: {spd:.1f} m/s, Direction: {angle:.1f}°"
for spd, angle in zip(speed[i], angles[i])
],
),
row=1,
col=1,
)
fig.update_layout(showlegend=False)
# Add a legend indicator for the wind speed at the right of the plots
fig.add_shape(
type="rect",
x0=1.05,
y0=0.2,
x1=1.10,
y1=0.8,
xref="paper",
yref="paper",
line=dict(width=0),
fillcolor="rgba(0,0,0,0)",
)
annotations = [
dict(
x=1.15,
y=y,
text=f"{int(s)} m/s",
xref="paper",
yref="paper",
showarrow=False,
)
for y, s in zip(np.linspace(0.2, 0.8, 5), range(0, 20, 5))
]
fig.update_layout(annotations=annotations)
# Add thermal data plot
fig.add_trace(
go.Heatmap(
z=thermal_diff,
x=times,
y=altitudes_thermal,
colorscale="YlGn",
colorbar=dict(
title="Thermal Temp Difference (°C)",
thickness=10,
ypad=75, # Moves the color bar vertically
),
zmin=0,
zmax=8,
text=thermal_diff.round(1),
texttemplate="%{text}",
textfont={"size": 12},
),
row=2,
col=1,
)
# Update layout
fig.update_layout(
height=800,
width=950,
title=f"Airgram for {start_date.strftime('%Y-%m-%d')}, lat/lon: {st.session_state.target_latitude:.2f}, {st.session_state.target_longitude:.2f}",
xaxis=dict(title="Time"),
yaxis=dict(title="Altitude (m)"),
xaxis2=dict(title="Time", tickangle=-45),
yaxis1=dict(title="Altitude (m)", range=[0, 3000]),
)
return fig
def create_daily_airgram(subset, x_target, y_target, date):
"""
Create a Plotly heatmap for a single day's wind and thermal data.
:param subset: xarray Dataset containing the weather data.
:param x_target: The x-coordinate index (not longitude) of the target location.
:param y_target: The y-coordinate index (not latitude) of the target location.
:param date: The specific date for which the data is visualized (datetime object).
:return: A Plotly figure object.
"""
# Define the time window to display
display_start_hour = 7
display_end_hour = 21
# Extract the day that matches the provided date
start_date = pd.Timestamp(date).normalize()
end_date = start_date + pd.Timedelta(days=1)
# Select data for the given date
daily_data = subset.sel(time=slice(start_date, end_date))
# Create time mask for the given display window
time_values = pd.to_datetime(
daily_data.time.values
) # Convert numpy.datetime64 to datetime
mask = [(display_start_hour <= t.hour < display_end_hour) for t in time_values]
# Filter data within the specified hours
daily_data = daily_data.isel(time=mask)
# Select nearest points for the supplied x and y indices
location_data = daily_data.sel(x=x_target, y=y_target, method="nearest")
# Interpolating the data for visualization
new_timestamps = pd.date_range(
start=start_date, end=end_date, freq="h"
) # Every full hour
# Remove timestamps that are outside the range of the data
new_timestamps = new_timestamps[
(new_timestamps >= location_data.time.min().values)
& (new_timestamps <= location_data.time.max().values)
]
altitudes = np.arange(0, 3000, 200) # Every 200 meters
# Remove altitude that are outside the range of the data
altitudes = altitudes[
(altitudes >= location_data.altitude.min().values)
& (altitudes <= location_data.altitude.max().values)
]
# Interpolate thermal temperature difference for the specified times and altitudes
thermal_diff = (
location_data["thermal_temp_diff"]
.interp(time=new_timestamps, altitude=altitudes)
.T.values
)
# Generating time labels for the x-axis
times = [t.strftime("%H:%M") for t in new_timestamps]
# Creating Plotly heatmap
fig = go.Figure(
data=go.Heatmap(
z=thermal_diff,
x=times,
y=altitudes,
colorscale="YlGn",
colorbar=dict(title="Thermal Temperature Difference (°C)"),
zmin=0,
zmax=8, # Adjusted for expected data range
text=thermal_diff.round(1),
texttemplate="%{text}",
textfont={"size": 12},
)
)
# Update layout
fig.update_layout(
title=f"Thermal Profiles for {start_date.strftime('%Y-%m-%d')}",
xaxis=dict(title="Time"),
yaxis=dict(title="Altitude (m)"),
xaxis_tickangle=-45,
)
return fig
def show_forecast():
subset = load_data()
date_controls(subset)
## MAP
with st.expander("Map", expanded=True):
map_fig = build_map(
_subset=subset,
date=st.session_state.forecast_date,
hour=st.session_state.forecast_time,
)
map_selection = st.plotly_chart(
map_fig,
use_container_width=True,
config={"scrollZoom": True, "displayModeBar": False},
on_select="rerun",
)
# Update lat lon if selection is made
selected_points = map_selection.get("selection").get("points")
if len(selected_points) > 0:
point = selected_points[0]
st.session_state.target_latitude = point["lat"]
st.session_state.target_longitude = point["lon"]
print("Updated lat lon")
x_target, y_target = latlon_to_xy(
st.session_state.target_latitude, st.session_state.target_longitude
)
wind_fig = create_daily_thermal_and_wind_airgram(
subset,
x_target=x_target,
y_target=y_target,
date=st.session_state.forecast_date,
)
# wind_fig = create_wind_map(
# subset,
# date_start=date_start,
# date_end=date_end,
# altitude_max=st.session_state.altitude_max,
# x_target=x_target,
# y_target=y_target,
# )
st.plotly_chart(wind_fig)
plt.close()
with st.expander("More settings", expanded=False):
st.session_state.altitude_max = st.number_input(
"Max altitude", 0, 4000, 3000, step=500
)
############################
######### SOUNDING #########
############################
st.markdown("---")
with st.expander("Sounding", expanded=False):
date = datetime.datetime.combine(
st.session_state.forecast_date, st.session_state.forecast_time
)
with st.spinner("Building sounding..."):
sounding_fig = create_sounding(
subset,
date=date.date(),
hour=date.hour,
altitude_max=st.session_state.altitude_max,
x_target=x_target,
y_target=y_target,
)
st.pyplot(sounding_fig)
plt.close()
st.markdown(
"Wind and sounding data from MEPS model (main model used by met.no), including the estimated ground temperature. Ive probably made many errors in this process."
)
if __name__ == "__main__":
run_streamlit = True
if run_streamlit:
st.set_page_config(page_title="PGWeather", page_icon="🪂", layout="wide")
show_forecast()
else:
lat = 61.22908
lon = 7.09674
x_target, y_target = latlon_to_xy(lat, lon)
build_map_overlays(subset, date="2024-05-14", hour="16")
wind_fig = create_wind_map(
subset, altitude_max=3000, x_target=x_target, y_target=y_target
)
# Plot thermal top on a map for a specific time
# subset.sel(time=subset.time.min()).thermal_top.plot()
sounding_fig = create_sounding(
subset, date="2024-05-12", hour=15, x_target=x_target, y_target=y_target
)
|