simen commited on
Commit
a0e4229
·
1 Parent(s): 353cc28

rewrite progress

Browse files
Files changed (5) hide show
  1. app.py +496 -337
  2. preprocess_forecast.py +173 -0
  3. pyproject.toml +29 -0
  4. utils.py +19 -0
  5. uv.lock +0 -0
app.py CHANGED
@@ -7,159 +7,73 @@ import pandas as pd
7
  import matplotlib.colors as mcolors
8
  import streamlit as st
9
  import datetime
10
- import matplotlib.dates as mdates
11
  from scipy.interpolate import griddata
12
- import folium
13
  import branca.colormap as cm
 
 
 
 
 
14
 
15
 
16
- @st.cache_data(ttl=60)
17
- def find_latest_meps_file():
18
- # The MEPS dataset: https://github.com/metno/NWPdocs/wiki/MEPS-dataset
19
- today = datetime.datetime.today()
20
- catalog_url = f"https://thredds.met.no/thredds/catalog/meps25epsarchive/{today.year}/{today.month:02d}/{today.day:02d}/catalog.xml"
21
- file_url_base = f"https://thredds.met.no/thredds/dodsC/meps25epsarchive/{today.year}/{today.month:02d}/{today.day:02d}"
22
- # Get the datasets from the catalog
23
- catalog = TDSCatalog(catalog_url)
24
- datasets = [s for s in catalog.datasets if "meps_det_ml" in s]
25
- file_path = f"{file_url_base}/{sorted(datasets)[-1]}"
26
- return file_path
27
-
28
-
29
- @st.cache_data()
30
- def load_meps_for_location(file_path=None, altitude_min=0, altitude_max=3000):
31
  """
32
- file_path=None
33
- altitude_min=0
34
- altitude_max=3000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  """
 
 
 
 
 
 
36
 
37
- if file_path is None:
38
- file_path = find_latest_meps_file()
39
-
40
- x_range = "[220:1:300]"
41
- y_range = "[420:1:500]"
42
- time_range = "[0:1:66]"
43
- hybrid_range = "[25:1:64]"
44
- height_range = "[0:1:0]"
45
-
46
- params = {
47
- "x": x_range,
48
- "y": y_range,
49
- "time": time_range,
50
- "hybrid": hybrid_range,
51
- "height": height_range,
52
- "longitude": f"{y_range}{x_range}",
53
- "latitude": f"{y_range}{x_range}",
54
- "air_temperature_ml": f"{time_range}{hybrid_range}{y_range}{x_range}",
55
- "ap": f"{hybrid_range}",
56
- "b": f"{hybrid_range}",
57
- "surface_air_pressure": f"{time_range}{height_range}{y_range}{x_range}",
58
- "x_wind_ml": f"{time_range}{hybrid_range}{y_range}{x_range}",
59
- "y_wind_ml": f"{time_range}{hybrid_range}{y_range}{x_range}",
60
- }
61
-
62
- path = f"{file_path}?{','.join(f'{k}{v}' for k, v in params.items())}"
63
-
64
- subset = xr.open_dataset(path, cache=True)
65
- subset.load()
66
-
67
- # %% get geopotential
68
- time_range_sfc = "[0:1:0]"
69
- surf_params = {
70
- "x": x_range,
71
- "y": y_range,
72
- "time": f"{time_range}",
73
- "surface_geopotential": f"{time_range_sfc}[0:1:0]{y_range}{x_range}",
74
- "air_temperature_0m": f"{time_range}[0:1:0]{y_range}{x_range}",
75
- }
76
- file_path_surf = f"{file_path.replace('meps_det_ml', 'meps_det_sfc')}?{','.join(f'{k}{v}' for k, v in surf_params.items())}"
77
-
78
- # Load surface parameters and merge into the main dataset
79
- surf = xr.open_dataset(file_path_surf, cache=True)
80
- # Convert the surface geopotential to elevation
81
- elevation = (surf.surface_geopotential / 9.80665).squeeze()
82
- # elevation.plot()
83
- subset["elevation"] = elevation
84
- air_temperature_0m = surf.air_temperature_0m.squeeze()
85
- subset["air_temperature_0m"] = air_temperature_0m
86
-
87
- # subset.elevation.plot()
88
- # %%
89
- def hybrid_to_height(ds):
90
- """
91
- ds = subset
92
- """
93
- # Constants
94
- R = 287.05 # Gas constant for dry air
95
- g = 9.80665 # Gravitational acceleration
96
-
97
- # Calculate the pressure at each level
98
- p = ds["ap"] + ds["b"] * ds["surface_air_pressure"] # .mean("ensemble_member")
99
-
100
- # Get the temperature at each level
101
- T = ds["air_temperature_ml"] # .mean("ensemble_member")
102
-
103
- # Calculate the height difference between each level and the surface
104
- dp = ds["surface_air_pressure"] - p # Pressure difference
105
- dT = T - T.isel(hybrid=-1) # Temperature difference relative to the surface
106
- dT_mean = 0.5 * (T + T.isel(hybrid=-1)) # Mean temperature
107
-
108
- # Calculate the height using the hypsometric equation
109
- dz = (R * dT_mean / g) * np.log(ds["surface_air_pressure"] / p)
110
-
111
- return dz
112
-
113
- altitude = hybrid_to_height(subset).mean("time").squeeze().mean("x").mean("y")
114
- subset = subset.assign_coords(altitude=("hybrid", altitude.data))
115
- subset = subset.swap_dims({"hybrid": "altitude"})
116
-
117
- # filter subset on altitude ranges
118
- subset = subset.where(
119
- (subset.altitude >= altitude_min) & (subset.altitude <= altitude_max), drop=True
120
- ).squeeze()
121
-
122
- wind_speed = np.sqrt(subset["x_wind_ml"] ** 2 + subset["y_wind_ml"] ** 2)
123
- subset = subset.assign(wind_speed=(("time", "altitude", "y", "x"), wind_speed.data))
124
-
125
- subset["thermal_temp_diff"] = compute_thermal_temp_difference(subset)
126
- # subset = subset.assign(thermal_temp_diff=(('time', 'altitude','y','x'), thermal_temp_diff.data))
127
-
128
- # Find the indices where the thermal temperature difference is zero or negative
129
- # Create tiny value at ground level to avoid finding the ground as the thermal top
130
- thermal_temp_diff = subset["thermal_temp_diff"]
131
- thermal_temp_diff = thermal_temp_diff.where(
132
- (thermal_temp_diff.sum("altitude") > 0)
133
- | (subset["altitude"] != subset.altitude.min()),
134
- thermal_temp_diff + 1e-6,
135
- )
136
- indices = (thermal_temp_diff > 0).argmax(dim="altitude")
137
- # Get the altitudes corresponding to these indices
138
- thermal_top = subset.altitude[indices]
139
- subset = subset.assign(thermal_top=(("time", "y", "x"), thermal_top.data))
140
- subset = subset.set_coords(["latitude", "longitude"])
141
-
142
- return subset
143
-
144
 
145
- # %%
146
- def compute_thermal_temp_difference(subset):
147
- lapse_rate = 0.0098
148
- ground_temp = subset.air_temperature_0m - 273.3
149
- air_temp = subset["air_temperature_ml"] - 273.3 # .ffill(dim='altitude')
150
 
151
- # dimensions
152
- # 'air_temperature_ml' altitude: 4 y: 3, x: 3
153
- # 'elevation' y: 3 x: 3
154
- # 'altitude' altitude: 4
155
-
156
- # broadcast ground temperature to all altitudes, but let it decrease by lapse rate
157
- altitude_diff = subset.altitude - subset.elevation
158
- altitude_diff = altitude_diff.where(altitude_diff >= 0, 0)
159
- temp_decrease = lapse_rate * altitude_diff
160
- ground_parcel_temp = ground_temp - temp_decrease
161
- thermal_temp_diff = (ground_parcel_temp - air_temp).clip(min=0)
162
- return thermal_temp_diff
163
 
164
 
165
  def wind_and_temp_colorscales(wind_max=20, tempdiff_max=8):
@@ -185,17 +99,19 @@ def wind_and_temp_colorscales(wind_max=20, tempdiff_max=8):
185
  return windcolors, tempcolors
186
 
187
 
188
- import plotly.graph_objects as go
189
- import numpy as np
190
- import pandas as pd
191
- import datetime
192
-
193
-
194
  @st.cache_data(ttl=60)
195
  def create_wind_map(
196
- subset, x_target, y_target, altitude_max=4000, date_start=None, date_end=None
197
  ):
198
- subset_data = subset
 
 
 
 
 
 
 
 
199
 
200
  wind_min, wind_max = 0.3, 20
201
  tempdiff_min, tempdiff_max = 0, 8
@@ -212,11 +128,9 @@ def create_wind_map(
212
 
213
  # Resample time and altitude for the wind plot data.
214
  new_timestamps = pd.date_range(date_start, date_end, 20)
215
- new_altitude = np.arange(
216
- subset_data.elevation.mean(), altitude_max, altitude_max / 20
217
- )
218
 
219
- windplot_data = subset_data.sel(x=x_target, y=y_target, method="nearest")
220
  windplot_data = windplot_data.interp(altitude=new_altitude, time=new_timestamps)
221
 
222
  # Convert data for Plotly heatmap
@@ -250,7 +164,7 @@ def create_wind_map(
250
  colorscale=wind_colors,
251
  colorbar=dict(title="Wind Speed (m/s)"),
252
  ),
253
- text=[f"Speed: {s:.2f} m/s" for s in speed.flatten()],
254
  hoverinfo="text",
255
  )
256
  )
@@ -337,207 +251,470 @@ def create_sounding(_subset, date, hour, x_target, y_target, altitude_max=3000):
337
  return fig
338
 
339
 
340
- @st.cache_data(ttl=7200)
341
- def build_map_overlays(_subset, date=None, hour=None):
342
- """
343
- date = "2024-05-13"
344
- hour = "15"
345
- x_target=None
346
- y_target=None
347
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  subset = _subset
349
 
350
- # Get the latitude and longitude values from the dataset
351
  latitude_values = subset.latitude.values.flatten()
352
  longitude_values = subset.longitude.values.flatten()
353
- thermal_top_values = subset.thermal_top.sel(time=f"{date}T{hour}").values.flatten()
354
- # thermal_top_values = subset.elevation.mean("altitude").values.flatten()
355
- # Convert the irregular grid data into a regular grid
356
- step_lon, step_lat = (
357
- subset.longitude.diff("x").quantile(0.1).values,
358
- subset.latitude.diff("y").quantile(0.1).values,
359
  )
360
- grid_x, grid_y = np.mgrid[
361
- min(latitude_values) : max(latitude_values) : step_lat,
362
- min(longitude_values) : max(longitude_values) : step_lon,
363
- ]
364
- grid_z = griddata(
365
- (latitude_values, longitude_values),
366
- thermal_top_values,
367
- (grid_x, grid_y),
368
- method="linear",
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  )
370
- grid_z = np.nan_to_num(grid_z, copy=False, nan=0)
371
- # Normalize the grid data to a range suitable for image display
372
- heightcolor = cm.LinearColormap(
373
- colors=["white", "white", "green", "yellow", "orange", "red", "darkblue"],
374
- index=[0, 500, 1000, 1500, 2000, 2500, 3000],
375
- vmin=0,
376
- vmax=3000,
377
- caption="Thermal Height (m)",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  )
379
 
380
- bounds = [
381
- [min(latitude_values), min(longitude_values)],
382
- [max(latitude_values), max(longitude_values)],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  ]
384
- img_overlay = folium.raster_layers.ImageOverlay(
385
- image=grid_z,
386
- bounds=bounds,
387
- colormap=heightcolor,
388
- opacity=0.4,
389
- mercator_project=True,
390
- origin="lower",
391
- pixelated=False,
392
  )
393
 
394
- return img_overlay, heightcolor
 
395
 
 
 
 
 
 
 
396
 
397
- # %%
398
- import pyproj
399
-
400
-
401
- def latlon_to_xy(lat, lon):
402
- crs = pyproj.CRS.from_cf(
403
- {
404
- "grid_mapping_name": "lambert_conformal_conic",
405
- "standard_parallel": [63.3, 63.3],
406
- "longitude_of_central_meridian": 15.0,
407
- "latitude_of_projection_origin": 63.3,
408
- "earth_radius": 6371000.0,
409
- }
410
  )
411
- # Transformer to project from ESPG:4368 (WGS:84) to our lambert_conformal_conic
412
- proj = pyproj.Proj.from_crs(4326, crs, always_xy=True)
413
 
414
- # Compute projected coordinates of lat/lon point
415
- X, Y = proj.transform(lon, lat)
416
- return X, Y
 
 
 
 
 
 
 
 
 
 
417
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
418
 
419
- # %%
420
- def show_forecast():
421
- with st.spinner("Fetching data..."):
422
- if "file_path" not in st.session_state:
423
- st.session_state.file_path = find_latest_meps_file()
424
- subset = load_data(st.session_state.file_path)
425
-
426
- def date_controls():
427
- start_stop_time = [
428
- subset.time.min().values.astype("M8[ms]").astype("O"),
429
- subset.time.max().values.astype("M8[ms]").astype("O"),
430
- ]
431
- now = datetime.datetime.now().replace(minute=0, second=0, microsecond=0)
432
-
433
- if "forecast_date" not in st.session_state:
434
- st.session_state.forecast_date = (now + datetime.timedelta(days=1)).date()
435
- if "forecast_time" not in st.session_state:
436
- st.session_state.forecast_time = datetime.time(14, 0)
437
- if "forecast_length" not in st.session_state:
438
- st.session_state.forecast_length = 1
439
- if "altitude_max" not in st.session_state:
440
- st.session_state.altitude_max = 3000
441
- if "target_latitude" not in st.session_state:
442
- st.session_state.target_latitude = 61.22908
443
- if "target_longitude" not in st.session_state:
444
- st.session_state.target_longitude = 7.09674
445
- col1, col_date, col_time, col3 = st.columns([0.2, 0.6, 0.2, 0.2])
446
-
447
- with col1:
448
- if st.button("⏮️", use_container_width=True):
449
- st.session_state.forecast_date -= datetime.timedelta(days=1)
450
- with col3:
451
- if st.button(
452
- "⏭️",
453
- use_container_width=True,
454
- disabled=(st.session_state.forecast_date == start_stop_time[1]),
455
- ):
456
- st.session_state.forecast_date += datetime.timedelta(days=1)
457
- with col_date:
458
- st.session_state.forecast_date = st.date_input(
459
- "Start date",
460
- value=st.session_state.forecast_date,
461
- min_value=start_stop_time[0],
462
- max_value=start_stop_time[1],
463
- label_visibility="collapsed",
464
- disabled=True,
465
- )
466
- with col_time:
467
- st.session_state.forecast_time = st.time_input(
468
- "Start time",
469
- value=st.session_state.forecast_time,
470
- step=3600,
471
- disabled=False,
472
- label_visibility="collapsed",
473
- )
474
 
475
- date_controls()
476
- time_start = datetime.time(0, 0)
477
- # convert subset.attrs['min_time']='2024-05-11T06:00:00Z' into datetime
478
- min_time = datetime.datetime.strptime(
479
- subset.attrs["min_time"], "%Y-%m-%dT%H:%M:%SZ"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  )
481
- date_start = datetime.datetime.combine(st.session_state.forecast_date, time_start)
482
- date_start = max(date_start, min_time)
483
- date_end = datetime.datetime.combine(
484
- st.session_state.forecast_date
485
- + datetime.timedelta(days=st.session_state.forecast_length),
486
- datetime.time(0, 0),
 
 
 
 
487
  )
488
 
489
- ## MAP
490
- with st.expander("Map", expanded=True):
491
- from streamlit_folium import st_folium
492
 
493
- st.cache_data(ttl=30)
494
 
495
- def build_map(date, hour):
496
- m = folium.Map(
497
- location=[61.22908, 7.09674], zoom_start=9, tiles="openstreetmap"
498
- )
499
- img_overlay, heightcolor = build_map_overlays(subset, date=date, hour=hour)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
 
501
- img_overlay.add_to(m)
502
- m.add_child(heightcolor, name="Thermal Height (m)")
503
- m.add_child(folium.LatLngPopup())
504
- return m
 
 
 
 
 
 
 
 
 
505
 
506
- m = build_map(
507
- date=st.session_state.forecast_date, hour=st.session_state.forecast_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  )
509
- map = st_folium(m)
510
 
511
- def get_pos(lat, lng):
512
- return lat, lng
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
 
514
- if map["last_clicked"] is not None:
515
- st.session_state.target_latitude, st.session_state.target_longitude = (
516
- get_pos(map["last_clicked"]["lat"], map["last_clicked"]["lng"])
517
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
 
519
  x_target, y_target = latlon_to_xy(
520
  st.session_state.target_latitude, st.session_state.target_longitude
521
  )
522
- wind_fig = create_wind_map(
523
  subset,
524
- date_start=date_start,
525
- date_end=date_end,
526
- altitude_max=st.session_state.altitude_max,
527
  x_target=x_target,
528
  y_target=y_target,
 
529
  )
530
- st.pyplot(wind_fig)
 
 
 
 
 
 
 
 
531
  plt.close()
532
 
533
  with st.expander("More settings", expanded=False):
534
- st.session_state.forecast_length = st.number_input(
535
- "multiday",
536
- 1,
537
- 3,
538
- 1,
539
- step=1,
540
- )
541
  st.session_state.altitude_max = st.number_input(
542
  "Max altitude", 0, 4000, 3000, step=500
543
  )
@@ -567,21 +744,6 @@ def show_forecast():
567
  "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."
568
  )
569
 
570
- # Download new forecast if available
571
- st.session_state.file_path = find_latest_meps_file()
572
- subset = load_data(st.session_state.file_path)
573
-
574
-
575
- @st.cache_data
576
- def load_data(filepath):
577
- local = False
578
- if local:
579
- subset = xr.open_dataset("subset.nc")
580
- else:
581
- subset = load_meps_for_location(filepath)
582
- subset.to_netcdf("subset.nc")
583
- return subset
584
-
585
 
586
  if __name__ == "__main__":
587
  run_streamlit = True
@@ -593,9 +755,6 @@ if __name__ == "__main__":
593
  lon = 7.09674
594
  x_target, y_target = latlon_to_xy(lat, lon)
595
 
596
- dataset_file_path = find_latest_meps_file()
597
- subset = load_data(dataset_file_path)
598
-
599
  build_map_overlays(subset, date="2024-05-14", hour="16")
600
 
601
  wind_fig = create_wind_map(
 
7
  import matplotlib.colors as mcolors
8
  import streamlit as st
9
  import datetime
 
10
  from scipy.interpolate import griddata
 
11
  import branca.colormap as cm
12
+ import os
13
+ from utils import latlon_to_xy
14
+ import plotly.graph_objects as go
15
+ from matplotlib.colors import to_hex, LinearSegmentedColormap
16
+ from streamlit_plotly_events import plotly_events
17
 
18
 
19
+ @st.cache_data(ttl=7200)
20
+ def load_data():
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  """
22
+ Loads a NetCDF file containing forecast data. Example
23
+ <xarray.Dataset> Size: 483MB
24
+ Dimensions: (altitude: 34, time: 67, y: 81, x: 81)
25
+ Coordinates:
26
+ height float32 4B 0.0
27
+ hybrid (altitude) float64 272B 0.6784 0.6984 ... 0.9985
28
+ * x (x) float32 324B -5.101e+05 -5.076e+05 ... -3.101e+05
29
+ * y (y) float32 324B -2.825e+05 -2.8e+05 ... -8.252e+04
30
+ * time (time) datetime64[ns] 536B 2025-03-13T12:00:00 ... ...
31
+ longitude (y, x) float64 52kB 5.684 5.729 5.774 ... 8.919 8.967
32
+ latitude (y, x) float64 52kB 60.43 60.43 60.43 ... 62.42 62.43
33
+ * altitude (altitude) float64 272B 2.89e+03 2.684e+03 ... 11.66
34
+ Data variables:
35
+ ap (altitude) float64 272B 5.682e+03 5.01e+03 ... 0.0 0.0
36
+ b (altitude) float64 272B 0.6223 0.6489 ... 0.9985
37
+ air_temperature_ml (time, altitude, y, x) float32 60MB 252.5 ... 260.7
38
+ x_wind_ml (time, altitude, y, x) float32 60MB -1.6 ... 5.709
39
+ y_wind_ml (time, altitude, y, x) float32 60MB -11.19 ... -10.67
40
+ surface_air_pressure (time, y, x, altitude) float32 60MB 9.46e+04 ... 8....
41
+ elevation (y, x, altitude) float32 892kB 465.8 ... 1.543e+03
42
+ air_temperature_0m (time, y, x, altitude) float32 60MB 278.6 ... 261.1
43
+ wind_speed (time, altitude, y, x) float32 60MB 11.31 ... 12.1
44
+ thermal_temp_diff (time, y, x, altitude) float64 120MB 2.331 ... 0.3471
45
+ thermal_top (time, y, x) float64 4MB 2.89e+03 ... 2.89e+03
46
+ Attributes: (12/41)
47
+ min_time: 2025-03-13T12:00:00Z
48
+ geospatial_lat_min: 49.8
49
+ geospatial_lat_max: 75.2
50
+ geospatial_lon_min: -18.1
51
+ geospatial_lon_max: 54.2
52
+ comment: For more information, please visit https://g...
53
+ ... ...
54
+ publisher_name: Norwegian Meteorological Institute
55
+ summary: This file contains model level parameters fr...
56
+ summary_no: Denne filen inneholder modelnivåparametere f...
57
+ title: Meps 2.5Km deterministic model level paramet...
58
+ title_no: Meps 2.5Km deterministisk modellnivåparamete...
59
+ related_dataset: no.met:8c94c7de-6328-4113-9e77-8f090999fab9 ...
60
  """
61
+ # Find all files in the forecasts directory
62
+ forecast_dir = "forecasts"
63
+ # Get a list of all NetCDF files
64
+ nc_files = [f for f in os.listdir(forecast_dir) if f.endswith(".nc")]
65
+ if not nc_files:
66
+ raise FileNotFoundError("No forecast files found in the 'forecasts' directory")
67
 
68
+ # Sort files by their timestamp, assuming the filenames contain the timestamp
69
+ nc_files.sort()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ # Choose the latest file
72
+ latest_file = os.path.join(forecast_dir, nc_files[-1])
 
 
 
73
 
74
+ # Load the dataset from the latest file
75
+ subset = xr.open_dataset(latest_file)
76
+ return subset
 
 
 
 
 
 
 
 
 
77
 
78
 
79
  def wind_and_temp_colorscales(wind_max=20, tempdiff_max=8):
 
99
  return windcolors, tempcolors
100
 
101
 
 
 
 
 
 
 
102
  @st.cache_data(ttl=60)
103
  def create_wind_map(
104
+ _subset, x_target, y_target, altitude_max=4000, date_start=None, date_end=None
105
  ):
106
+ """
107
+ _subset = subset
108
+ altitude_max = 3000
109
+ x_target = -422175.14005226345
110
+ y_target = -204279.84596708667
111
+
112
+
113
+ """
114
+ subset = _subset
115
 
116
  wind_min, wind_max = 0.3, 20
117
  tempdiff_min, tempdiff_max = 0, 8
 
128
 
129
  # Resample time and altitude for the wind plot data.
130
  new_timestamps = pd.date_range(date_start, date_end, 20)
131
+ new_altitude = np.arange(subset.elevation.mean(), altitude_max, altitude_max / 20)
 
 
132
 
133
+ windplot_data = subset.sel(x=x_target, y=y_target, method="nearest")
134
  windplot_data = windplot_data.interp(altitude=new_altitude, time=new_timestamps)
135
 
136
  # Convert data for Plotly heatmap
 
164
  colorscale=wind_colors,
165
  colorbar=dict(title="Wind Speed (m/s)"),
166
  ),
167
+ # text=[f"Speed: {s:.2f} m/s" for s in speed.squeeze()],
168
  hoverinfo="text",
169
  )
170
  )
 
251
  return fig
252
 
253
 
254
+ # %%
255
+ def date_controls(subset):
256
+ start_stop_time = [
257
+ subset.time.min().values.astype("M8[D]").astype("O"),
258
+ subset.time.max().values.astype("M8[D]").astype("O"),
259
+ ]
260
+ now = datetime.datetime.now().replace(minute=0, second=0, microsecond=0).date()
261
+
262
+ if "forecast_date" not in st.session_state:
263
+ st.session_state.forecast_date = now
264
+ if "forecast_time" not in st.session_state:
265
+ st.session_state.forecast_time = datetime.time(14, 0)
266
+ if "altitude_max" not in st.session_state:
267
+ st.session_state.altitude_max = 3000
268
+ if "target_latitude" not in st.session_state:
269
+ st.session_state.target_latitude = 61.22908
270
+ if "target_longitude" not in st.session_state:
271
+ st.session_state.target_longitude = 7.09674
272
+
273
+ # Generate available days within the dataset's time range
274
+ available_days = pd.date_range(
275
+ start=start_stop_time[0], end=start_stop_time[1]
276
+ ).date
277
+
278
+ day_cols = st.columns(len(available_days)) # Create columns for each available day
279
+
280
+ for i, day in enumerate(available_days):
281
+ label = day.strftime("%A") # Get day label
282
+ if day == now:
283
+ label += " (today)"
284
+ with day_cols[i]: # Place each button in its respective column
285
+ if st.button(label):
286
+ st.session_state.forecast_date = day
287
+
288
+ # Group hours into smaller rows for better layout
289
+ hours_per_row = 24 # Define how many hour buttons to display per row
290
+ available_hours = range(7, 22, 1) # 24-hour format
291
+
292
+ # Divide hours into batches
293
+ hour_batches = [
294
+ available_hours[i : i + hours_per_row]
295
+ for i in range(0, len(available_hours), hours_per_row)
296
+ ]
297
+
298
+ # Display hour buttons in rows
299
+ for batch in hour_batches:
300
+ hour_cols = st.columns(len(batch))
301
+ for i, hour in enumerate(batch):
302
+ label = f"{hour:02}:00"
303
+ with hour_cols[i]:
304
+ if st.button(label):
305
+ st.session_state.forecast_time = datetime.time(hour, 0)
306
+
307
+
308
+ def build_map(_subset, date=None, hour=None):
309
  subset = _subset
310
 
 
311
  latitude_values = subset.latitude.values.flatten()
312
  longitude_values = subset.longitude.values.flatten()
313
+ thermal_top_values = (
314
+ subset.thermal_top.sel(time=f"{date}T{hour}").values.flatten().round()
 
 
 
 
315
  )
316
+
317
+ # Use Plotly's scattermap for visualization and enable click events
318
+ scatter_map = go.Scattermap(
319
+ lat=latitude_values,
320
+ lon=longitude_values,
321
+ mode="markers",
322
+ marker=go.scattermap.Marker(
323
+ size=9,
324
+ color=thermal_top_values,
325
+ colorscale="Viridis",
326
+ colorbar=dict(title="Thermal Height (m)"),
327
+ ),
328
+ text=[f"Thermal Height: {ht} m" for ht in thermal_top_values],
329
+ hoverinfo="text",
330
+ )
331
+
332
+ fig = go.Figure(scatter_map)
333
+
334
+ fig.update_layout(
335
+ map_style="open-street-map",
336
+ map=dict(center=dict(lat=61.22908, lon=7.09674), zoom=9),
337
+ margin={"r": 0, "t": 0, "l": 0, "b": 0},
338
  )
339
+
340
+ # Register click event callback
341
+
342
+ return fig
343
+
344
+
345
+ from plotly.subplots import make_subplots
346
+
347
+ import numpy as np
348
+ import pandas as pd
349
+ import plotly.graph_objects as go
350
+ from plotly.subplots import make_subplots
351
+ import numpy as np
352
+ import pandas as pd
353
+ import plotly.graph_objects as go
354
+ from plotly.subplots import make_subplots
355
+
356
+ import pandas as pd
357
+ import numpy as np
358
+ import plotly.graph_objects as go
359
+ from plotly.subplots import make_subplots
360
+ from plotly.subplots import make_subplots
361
+ import numpy as np
362
+ import pandas as pd
363
+ import plotly.graph_objects as go
364
+ from plotly.subplots import make_subplots
365
+ import numpy as np
366
+ import pandas as pd
367
+ import plotly.graph_objects as go
368
+
369
+
370
+ def interpolate_color(
371
+ wind_speed, thresholds=[2, 8, 14], colors=["white", "green", "red", "black"]
372
+ ):
373
+ # Normalize thresholds to range [0, 1]
374
+ norm_thresholds = [t / max(thresholds) for t in thresholds]
375
+ norm_thresholds = [0] + norm_thresholds + [1]
376
+
377
+ # Extend color list to match normalized thresholds
378
+ extended_colors = [colors[0]] + colors + [colors[-1]]
379
+
380
+ # Create colormap
381
+ cmap = LinearSegmentedColormap.from_list(
382
+ "wind_speed_cmap", list(zip(norm_thresholds, extended_colors)), N=256
383
  )
384
 
385
+ # Normalize wind speed to range [0, 1] and get color
386
+ norm_wind_speed = wind_speed / max(thresholds)
387
+ return to_hex(cmap(np.clip(norm_wind_speed, 0, 1)))
388
+
389
+
390
+ def create_daily_thermal_and_wind_airgram(subset, x_target, y_target, date):
391
+ """
392
+ Create a Plotly subplot figure for a single day's thermal and wind data.
393
+ The top subplot shows wind data as arrows for direction and color for strength.
394
+ The bottom subplot shows thermal temperature differences.
395
+ """
396
+ # Define the time window to display
397
+ display_start_hour = 7
398
+ display_end_hour = 21
399
+
400
+ # Extract the day that matches the provided date
401
+ start_date = pd.Timestamp(date).normalize()
402
+ end_date = start_date + pd.Timedelta(days=1)
403
+
404
+ # Select data for the given date
405
+ daily_data = subset.sel(time=slice(start_date, end_date))
406
+
407
+ # Create time mask for the given display window
408
+ time_values = pd.to_datetime(
409
+ daily_data.time.values
410
+ ) # Convert numpy.datetime64 to datetime
411
+
412
+ mask = [(display_start_hour <= t.hour < display_end_hour) for t in time_values]
413
+
414
+ # Filter data within the specified hours
415
+ daily_data = daily_data.isel(time=mask)
416
+
417
+ # Select nearest points for the supplied x and y indices
418
+ location_data = daily_data.sel(x=x_target, y=y_target, method="nearest")
419
+
420
+ # Interpolating the data for visualization
421
+ new_timestamps = pd.date_range(
422
+ start=start_date, end=end_date, freq="h"
423
+ ) # Every full hour
424
+
425
+ # Remove timestamps that are outside the range of the data
426
+ new_timestamps = new_timestamps[
427
+ (new_timestamps >= location_data.time.min().values)
428
+ & (new_timestamps <= location_data.time.max().values)
429
+ ]
430
+
431
+ altitudes_thermal = np.arange(0, 3000, 200) # Every 200 meters
432
+ altitudes_thermal = altitudes_thermal[
433
+ (altitudes_thermal >= location_data.altitude.min().values)
434
+ & (altitudes_thermal <= location_data.altitude.max().values)
435
  ]
436
+
437
+ # Interpolate thermal temperature difference for the specified times and altitudes
438
+ thermal_diff = (
439
+ location_data["thermal_temp_diff"]
440
+ .interp(time=new_timestamps, altitude=altitudes_thermal)
441
+ .T.values
 
 
442
  )
443
 
444
+ # Generating time labels for the x-axis
445
+ times = [t.strftime("%H:%M") for t in new_timestamps]
446
 
447
+ # Calculate wind data at 500m intervals
448
+ altitudes_wind = np.arange(0, 3000, 500)
449
+ altitudes_wind = altitudes_wind[
450
+ (altitudes_wind >= location_data.altitude.min().values)
451
+ & (altitudes_wind <= location_data.altitude.max().values)
452
+ ]
453
 
454
+ x_wind = location_data["x_wind_ml"].interp(
455
+ time=new_timestamps, altitude=altitudes_wind
456
+ )
457
+ y_wind = location_data["y_wind_ml"].interp(
458
+ time=new_timestamps, altitude=altitudes_wind
 
 
 
 
 
 
 
 
459
  )
 
 
460
 
461
+ # Calculate wind speed and direction
462
+ speed = np.sqrt(x_wind**2 + y_wind**2).T.values
463
+ angles = np.rad2deg(np.arctan2(y_wind, x_wind)).T.values # Convert to degrees
464
+ angles = angles = (angles + 90) % 360
465
+ # Create a subplot figure with shared x-axis
466
+ fig = make_subplots(
467
+ rows=2,
468
+ cols=1,
469
+ shared_xaxes=True,
470
+ row_heights=[0.3, 0.7],
471
+ vertical_spacing=0.05,
472
+ subplot_titles=("Wind Speed and Direction", "Thermal Temperature Difference"),
473
+ )
474
 
475
+ # Add wind data plot as rotated triangular markers with a common legend
476
+ for i, alt in enumerate(altitudes_wind):
477
+ fig.add_trace(
478
+ go.Scatter(
479
+ x=times,
480
+ y=[alt] * len(times),
481
+ mode="markers",
482
+ marker=dict(
483
+ symbol="arrow",
484
+ size=20,
485
+ angle=angles[i],
486
+ color=[interpolate_color(s) for s in speed[i]],
487
+ # colorscale="Viridis",
488
+ showscale=False, # Hide individual color scales
489
+ cmin=0,
490
+ cmax=20,
491
+ ),
492
+ hoverinfo="text",
493
+ text=[
494
+ f"Alt: {alt} m, Speed: {spd:.1f} m/s, Direction: {angle:.1f}°"
495
+ for spd, angle in zip(speed[i], angles[i])
496
+ ],
497
+ ),
498
+ row=1,
499
+ col=1,
500
+ )
501
+ fig.update_layout(showlegend=False)
502
+
503
+ # Add a legend indicator for the wind speed at the right of the plots
504
+ fig.add_shape(
505
+ type="rect",
506
+ x0=1.05,
507
+ y0=0.2,
508
+ x1=1.10,
509
+ y1=0.8,
510
+ xref="paper",
511
+ yref="paper",
512
+ line=dict(width=0),
513
+ fillcolor="rgba(0,0,0,0)",
514
+ )
515
 
516
+ annotations = [
517
+ dict(
518
+ x=1.15,
519
+ y=y,
520
+ text=f"{int(s)} m/s",
521
+ xref="paper",
522
+ yref="paper",
523
+ showarrow=False,
524
+ )
525
+ for y, s in zip(np.linspace(0.2, 0.8, 5), range(0, 20, 5))
526
+ ]
527
+ fig.update_layout(annotations=annotations)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
 
529
+ # Add thermal data plot
530
+ fig.add_trace(
531
+ go.Heatmap(
532
+ z=thermal_diff,
533
+ x=times,
534
+ y=altitudes_thermal,
535
+ colorscale="YlGn",
536
+ colorbar=dict(
537
+ title="Thermal Temp Difference (°C)",
538
+ thickness=10,
539
+ ypad=75, # Moves the color bar vertically
540
+ ),
541
+ zmin=0,
542
+ zmax=8,
543
+ text=thermal_diff.round(1),
544
+ texttemplate="%{text}",
545
+ textfont={"size": 12},
546
+ ),
547
+ row=2,
548
+ col=1,
549
  )
550
+
551
+ # Update layout
552
+ fig.update_layout(
553
+ height=800,
554
+ width=950,
555
+ title=f"Thermal and Wind Profiles for {start_date.strftime('%Y-%m-%d')}",
556
+ xaxis=dict(title="Time"),
557
+ yaxis=dict(title="Altitude (m)"),
558
+ xaxis2=dict(title="Time", tickangle=-45),
559
+ yaxis1=dict(title="Altitude (m)", range=[0, 3000]),
560
  )
561
 
562
+ return fig
 
 
563
 
 
564
 
565
+ def create_daily_airgram(subset, x_target, y_target, date):
566
+ """
567
+ Create a Plotly heatmap for a single day's wind and thermal data.
568
+
569
+ :param subset: xarray Dataset containing the weather data.
570
+ :param x_target: The x-coordinate index (not longitude) of the target location.
571
+ :param y_target: The y-coordinate index (not latitude) of the target location.
572
+ :param date: The specific date for which the data is visualized (datetime object).
573
+ :return: A Plotly figure object.
574
+ """
575
+ # Define the time window to display
576
+ display_start_hour = 7
577
+ display_end_hour = 21
578
+
579
+ # Extract the day that matches the provided date
580
+ start_date = pd.Timestamp(date).normalize()
581
+ end_date = start_date + pd.Timedelta(days=1)
582
+
583
+ # Select data for the given date
584
+ daily_data = subset.sel(time=slice(start_date, end_date))
585
+
586
+ # Create time mask for the given display window
587
+ time_values = pd.to_datetime(
588
+ daily_data.time.values
589
+ ) # Convert numpy.datetime64 to datetime
590
+ mask = [(display_start_hour <= t.hour < display_end_hour) for t in time_values]
591
+
592
+ # Filter data within the specified hours
593
+ daily_data = daily_data.isel(time=mask)
594
+ # Select nearest points for the supplied x and y indices
595
+ location_data = daily_data.sel(x=x_target, y=y_target, method="nearest")
596
+
597
+ # Interpolating the data for visualization
598
+ new_timestamps = pd.date_range(
599
+ start=start_date, end=end_date, freq="h"
600
+ ) # Every full hour
601
+
602
+ # Remove timestamps that are outside the range of the data
603
+ new_timestamps = new_timestamps[
604
+ (new_timestamps >= location_data.time.min().values)
605
+ & (new_timestamps <= location_data.time.max().values)
606
+ ]
607
 
608
+ altitudes = np.arange(0, 3000, 200) # Every 200 meters
609
+ # Remove altitude that are outside the range of the data
610
+ altitudes = altitudes[
611
+ (altitudes >= location_data.altitude.min().values)
612
+ & (altitudes <= location_data.altitude.max().values)
613
+ ]
614
+
615
+ # Interpolate thermal temperature difference for the specified times and altitudes
616
+ thermal_diff = (
617
+ location_data["thermal_temp_diff"]
618
+ .interp(time=new_timestamps, altitude=altitudes)
619
+ .T.values
620
+ )
621
 
622
+ # Generating time labels for the x-axis
623
+ times = [t.strftime("%H:%M") for t in new_timestamps]
624
+
625
+ # Creating Plotly heatmap
626
+ fig = go.Figure(
627
+ data=go.Heatmap(
628
+ z=thermal_diff,
629
+ x=times,
630
+ y=altitudes,
631
+ colorscale="YlGn",
632
+ colorbar=dict(title="Thermal Temperature Difference (°C)"),
633
+ zmin=0,
634
+ zmax=8, # Adjusted for expected data range
635
+ text=thermal_diff.round(1),
636
+ texttemplate="%{text}",
637
+ textfont={"size": 12},
638
  )
639
+ )
640
 
641
+ # Add wind speed information (if needed)
642
+ speed = (
643
+ np.sqrt(location_data["x_wind_ml"] ** 2 + location_data["y_wind_ml"] ** 2)
644
+ .interp(time=new_timestamps, altitude=altitudes)
645
+ .T.values
646
+ )
647
+ # fig.add_trace(
648
+ # go.Scatter(
649
+ # x=times,
650
+ # y=altitudes,
651
+ # mode="markers",
652
+ # marker=dict(
653
+ # size=8,
654
+ # color=speed,
655
+ # colorscale="Viridis",
656
+ # colorbar=dict(title="Wind Speed (m/s)"),
657
+ # cmin=0,
658
+ # cmax=20, # Adjusted for expected data range
659
+ # ),
660
+ # hoverinfo="text",
661
+ # text=[f"Speed: {s:.2f} m/s" for s in speed.flatten()],
662
+ # )
663
+ # )
664
 
665
+ # Update layout
666
+ fig.update_layout(
667
+ title=f"Thermal Profiles for {start_date.strftime('%Y-%m-%d')}",
668
+ xaxis=dict(title="Time"),
669
+ yaxis=dict(title="Altitude (m)"),
670
+ xaxis_tickangle=-45,
671
+ )
672
+
673
+ return fig
674
+
675
+
676
+ def show_forecast():
677
+ subset = load_data()
678
+
679
+ date_controls(subset)
680
+ # time_start = datetime.time(0, 0)
681
+ # # convert subset.attrs['min_time']='2024-05-11T06:00:00Z' into datetime
682
+ # min_time = datetime.datetime.strptime(
683
+ # subset.attrs["min_time"], "%Y-%m-%dT%H:%M:%SZ"
684
+ # )
685
+ # date_start = datetime.datetime.combine(st.session_state.forecast_date, time_start)
686
+ # date_start = max(date_start, min_time)
687
+
688
+ ## MAP
689
+ with st.expander("Map", expanded=True):
690
+ map_fig = build_map(
691
+ _subset=subset,
692
+ date=st.session_state.forecast_date,
693
+ hour=st.session_state.forecast_time,
694
+ )
695
+ st.plotly_chart(map_fig, use_container_width=True, config={"scrollZoom": True})
696
 
697
  x_target, y_target = latlon_to_xy(
698
  st.session_state.target_latitude, st.session_state.target_longitude
699
  )
700
+ wind_fig = create_daily_thermal_and_wind_airgram(
701
  subset,
 
 
 
702
  x_target=x_target,
703
  y_target=y_target,
704
+ date=st.session_state.forecast_date,
705
  )
706
+ # wind_fig = create_wind_map(
707
+ # subset,
708
+ # date_start=date_start,
709
+ # date_end=date_end,
710
+ # altitude_max=st.session_state.altitude_max,
711
+ # x_target=x_target,
712
+ # y_target=y_target,
713
+ # )
714
+ st.plotly_chart(wind_fig)
715
  plt.close()
716
 
717
  with st.expander("More settings", expanded=False):
 
 
 
 
 
 
 
718
  st.session_state.altitude_max = st.number_input(
719
  "Max altitude", 0, 4000, 3000, step=500
720
  )
 
744
  "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."
745
  )
746
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
747
 
748
  if __name__ == "__main__":
749
  run_streamlit = True
 
755
  lon = 7.09674
756
  x_target, y_target = latlon_to_xy(lat, lon)
757
 
 
 
 
758
  build_map_overlays(subset, date="2024-05-14", hour="16")
759
 
760
  wind_fig = create_wind_map(
preprocess_forecast.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import xarray as xr
2
+ from siphon.catalog import TDSCatalog
3
+ import numpy as np
4
+ import datetime
5
+ import re
6
+
7
+
8
+ # %%
9
+ def compute_thermal_temp_difference(subset):
10
+ lapse_rate = 0.0098
11
+ ground_temp = subset.air_temperature_0m - 273.3
12
+ air_temp = subset["air_temperature_ml"] - 273.3 # .ffill(dim='altitude')
13
+
14
+ # dimensions
15
+ # 'air_temperature_ml' altitude: 4 y: 3, x: 3
16
+ # 'elevation' y: 3 x: 3
17
+ # 'altitude' altitude: 4
18
+
19
+ # broadcast ground temperature to all altitudes, but let it decrease by lapse rate
20
+ altitude_diff = subset.altitude - subset.elevation
21
+ altitude_diff = altitude_diff.where(altitude_diff >= 0, 0)
22
+ temp_decrease = lapse_rate * altitude_diff
23
+ ground_parcel_temp = ground_temp - temp_decrease
24
+ thermal_temp_diff = (ground_parcel_temp - air_temp).clip(min=0)
25
+ return thermal_temp_diff
26
+
27
+
28
+ def extract_timestamp(filename):
29
+ # Define a regex pattern to capture the timestamp
30
+ pattern = r"(\d{4})(\d{2})(\d{2})T(\d{2})Z"
31
+ match = re.search(pattern, filename)
32
+
33
+ if match:
34
+ year, month, day, hour = match.groups()
35
+ return f"{year}-{month}-{day}T{hour}:00Z"
36
+ else:
37
+ return None
38
+
39
+
40
+ def find_latest_meps_file():
41
+ # The MEPS dataset: https://github.com/metno/NWPdocs/wiki/MEPS-dataset
42
+ today = datetime.datetime.today()
43
+ catalog_url = f"https://thredds.met.no/thredds/catalog/meps25epsarchive/{today.year}/{today.month:02d}/{today.day:02d}/catalog.xml"
44
+ file_url_base = f"https://thredds.met.no/thredds/dodsC/meps25epsarchive/{today.year}/{today.month:02d}/{today.day:02d}"
45
+ # Get the datasets from the catalog
46
+ catalog = TDSCatalog(catalog_url)
47
+ datasets = [s for s in catalog.datasets if "meps_det_ml" in s]
48
+ file_path = f"{file_url_base}/{sorted(datasets)[-1]}"
49
+ return file_path
50
+
51
+
52
+ def load_meps_for_location(file_path=None, altitude_min=0, altitude_max=3000):
53
+ """
54
+ file_path=None
55
+ altitude_min=0
56
+ altitude_max=3000
57
+ """
58
+
59
+ if file_path is None:
60
+ file_path = find_latest_meps_file()
61
+
62
+ x_range = "[220:1:300]"
63
+ y_range = "[420:1:500]"
64
+ time_range = "[0:1:66]"
65
+ hybrid_range = "[25:1:64]"
66
+ height_range = "[0:1:0]"
67
+
68
+ params = {
69
+ "x": x_range,
70
+ "y": y_range,
71
+ "time": time_range,
72
+ "hybrid": hybrid_range,
73
+ "height": height_range,
74
+ "longitude": f"{y_range}{x_range}",
75
+ "latitude": f"{y_range}{x_range}",
76
+ "air_temperature_ml": f"{time_range}{hybrid_range}{y_range}{x_range}",
77
+ "ap": f"{hybrid_range}",
78
+ "b": f"{hybrid_range}",
79
+ "surface_air_pressure": f"{time_range}{height_range}{y_range}{x_range}",
80
+ "x_wind_ml": f"{time_range}{hybrid_range}{y_range}{x_range}",
81
+ "y_wind_ml": f"{time_range}{hybrid_range}{y_range}{x_range}",
82
+ }
83
+
84
+ path = f"{file_path}?{','.join(f'{k}{v}' for k, v in params.items())}"
85
+
86
+ subset = xr.open_dataset(path, cache=True)
87
+ subset.load()
88
+
89
+ # get geopotential
90
+ time_range_sfc = "[0:1:0]"
91
+ surf_params = {
92
+ "x": x_range,
93
+ "y": y_range,
94
+ "time": f"{time_range}",
95
+ "surface_geopotential": f"{time_range_sfc}[0:1:0]{y_range}{x_range}",
96
+ "air_temperature_0m": f"{time_range}[0:1:0]{y_range}{x_range}",
97
+ }
98
+ file_path_surf = f"{file_path.replace('meps_det_ml', 'meps_det_sfc')}?{','.join(f'{k}{v}' for k, v in surf_params.items())}"
99
+
100
+ # Load surface parameters and merge into the main dataset
101
+ surf = xr.open_dataset(file_path_surf, cache=True)
102
+ # Convert the surface geopotential to elevation
103
+ elevation = (surf.surface_geopotential / 9.80665).squeeze()
104
+ # elevation.plot()
105
+ subset["elevation"] = elevation
106
+ air_temperature_0m = surf.air_temperature_0m.squeeze()
107
+ subset["air_temperature_0m"] = air_temperature_0m
108
+
109
+ # subset.elevation.plot()
110
+ def hybrid_to_height(ds):
111
+ """
112
+ ds = subset
113
+ """
114
+ # Constants
115
+ R = 287.05 # Gas constant for dry air
116
+ g = 9.80665 # Gravitational acceleration
117
+
118
+ # Calculate the pressure at each level
119
+ p = ds["ap"] + ds["b"] * ds["surface_air_pressure"] # .mean("ensemble_member")
120
+
121
+ # Get the temperature at each level
122
+ T = ds["air_temperature_ml"] # .mean("ensemble_member")
123
+
124
+ # Calculate the height difference between each level and the surface
125
+ dp = ds["surface_air_pressure"] - p # Pressure difference
126
+ dT = T - T.isel(hybrid=-1) # Temperature difference relative to the surface
127
+ dT_mean = 0.5 * (T + T.isel(hybrid=-1)) # Mean temperature
128
+
129
+ # Calculate the height using the hypsometric equation
130
+ dz = (R * dT_mean / g) * np.log(ds["surface_air_pressure"] / p)
131
+
132
+ return dz
133
+
134
+ altitude = hybrid_to_height(subset).mean("time").squeeze().mean("x").mean("y")
135
+ subset = subset.assign_coords(altitude=("hybrid", altitude.data))
136
+ subset = subset.swap_dims({"hybrid": "altitude"})
137
+
138
+ # filter subset on altitude ranges
139
+ subset = subset.where(
140
+ (subset.altitude >= altitude_min) & (subset.altitude <= altitude_max), drop=True
141
+ ).squeeze()
142
+
143
+ wind_speed = np.sqrt(subset["x_wind_ml"] ** 2 + subset["y_wind_ml"] ** 2)
144
+ subset = subset.assign(wind_speed=(("time", "altitude", "y", "x"), wind_speed.data))
145
+
146
+ subset["thermal_temp_diff"] = compute_thermal_temp_difference(subset)
147
+ # subset = subset.assign(thermal_temp_diff=(('time', 'altitude','y','x'), thermal_temp_diff.data))
148
+
149
+ # Find the indices where the thermal temperature difference is zero or negative
150
+ # Create tiny value at ground level to avoid finding the ground as the thermal top
151
+ thermal_temp_diff = subset["thermal_temp_diff"]
152
+ thermal_temp_diff = thermal_temp_diff.where(
153
+ (thermal_temp_diff.sum("altitude") > 0)
154
+ | (subset["altitude"] != subset.altitude.min()),
155
+ thermal_temp_diff + 1e-6,
156
+ )
157
+ indices = (thermal_temp_diff > 0).argmax(dim="altitude")
158
+ # Get the altitudes corresponding to these indices
159
+ thermal_top = subset.altitude[indices]
160
+ subset = subset.assign(thermal_top=(("time", "y", "x"), thermal_top.data))
161
+ subset = subset.set_coords(["latitude", "longitude"])
162
+ return subset
163
+
164
+
165
+ if __name__ == "__main__":
166
+ dataset_file_path = find_latest_meps_file()
167
+
168
+ subset = load_meps_for_location(dataset_file_path)
169
+
170
+ os.makedirs("forecasts", exist_ok=True)
171
+
172
+ timestmap = extract_timestamp(dataset_file_path.split("/")[-1])
173
+ subset.to_netcdf(f"forecasts/{timestmap}.nc")
pyproject.toml ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "pgweather"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ dependencies = [
8
+ "bottleneck>=1.4.2",
9
+ "folium>=0.19.4",
10
+ "geopandas>=0.10.2",
11
+ "ipykernel>=6.29.5",
12
+ "matplotlib>=3.9.4",
13
+ "metar>=1.8.0",
14
+ "netcdf4>=1.7.2",
15
+ "numpy>=2.0.2",
16
+ "pandas>=1.1.3",
17
+ "plotly>=4.12.0",
18
+ "python-dateutil>=2.8.1",
19
+ "requests>=2.24.0",
20
+ "scipy>=1.13.1",
21
+ "shapely>=2.0.7",
22
+ "siphon>=0.9",
23
+ "streamlit-folium>=0.24.0",
24
+ "streamlit>=0.85.1",
25
+ "windrose>=1.9.2",
26
+ "xarray>=2024.7.0",
27
+ "nbformat>=5.10.4",
28
+ "anywidget>=0.9.15",
29
+ ]
utils.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pyproj
2
+
3
+
4
+ def latlon_to_xy(lat, lon):
5
+ crs = pyproj.CRS.from_cf(
6
+ {
7
+ "grid_mapping_name": "lambert_conformal_conic",
8
+ "standard_parallel": [63.3, 63.3],
9
+ "longitude_of_central_meridian": 15.0,
10
+ "latitude_of_projection_origin": 63.3,
11
+ "earth_radius": 6371000.0,
12
+ }
13
+ )
14
+ # Transformer to project from ESPG:4368 (WGS:84) to our lambert_conformal_conic
15
+ proj = pyproj.Proj.from_crs(4326, crs, always_xy=True)
16
+
17
+ # Compute projected coordinates of lat/lon point
18
+ X, Y = proj.transform(lon, lat)
19
+ return X, Y
uv.lock ADDED
The diff for this file is too large to render. See raw diff