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

formatting

Browse files
Files changed (1) hide show
  1. app.py +285 -173
app.py CHANGED
@@ -1,4 +1,4 @@
1
- #%%
2
  import xarray as xr
3
  from siphon.catalog import TDSCatalog
4
  import numpy as np
@@ -12,6 +12,7 @@ from scipy.interpolate import griddata
12
  import folium
13
  import branca.colormap as cm
14
 
 
15
  @st.cache_data(ttl=60)
16
  def find_latest_meps_file():
17
  # The MEPS dataset: https://github.com/metno/NWPdocs/wiki/MEPS-dataset
@@ -36,8 +37,8 @@ def load_meps_for_location(file_path=None, altitude_min=0, altitude_max=3000):
36
  if file_path is None:
37
  file_path = find_latest_meps_file()
38
 
39
- x_range= "[220:1:300]"
40
- y_range= "[420:1:500]"
41
  time_range = "[0:1:66]"
42
  hybrid_range = "[25:1:64]"
43
  height_range = "[0:1:0]"
@@ -51,8 +52,8 @@ def load_meps_for_location(file_path=None, altitude_min=0, altitude_max=3000):
51
  "longitude": f"{y_range}{x_range}",
52
  "latitude": f"{y_range}{x_range}",
53
  "air_temperature_ml": f"{time_range}{hybrid_range}{y_range}{x_range}",
54
- "ap" : f"{hybrid_range}",
55
- "b" : f"{hybrid_range}",
56
  "surface_air_pressure": f"{time_range}{height_range}{y_range}{x_range}",
57
  "x_wind_ml": f"{time_range}{hybrid_range}{y_range}{x_range}",
58
  "y_wind_ml": f"{time_range}{hybrid_range}{y_range}{x_range}",
@@ -63,7 +64,7 @@ def load_meps_for_location(file_path=None, altitude_min=0, altitude_max=3000):
63
  subset = xr.open_dataset(path, cache=True)
64
  subset.load()
65
 
66
- #%% get geopotential
67
  time_range_sfc = "[0:1:0]"
68
  surf_params = {
69
  "x": x_range,
@@ -71,19 +72,20 @@ def load_meps_for_location(file_path=None, altitude_min=0, altitude_max=3000):
71
  "time": f"{time_range}",
72
  "surface_geopotential": f"{time_range_sfc}[0:1:0]{y_range}{x_range}",
73
  "air_temperature_0m": f"{time_range}[0:1:0]{y_range}{x_range}",
74
- }
75
- file_path_surf = f"{file_path.replace('meps_det_ml','meps_det_sfc')}?{','.join(f'{k}{v}' for k, v in surf_params.items())}"
76
 
77
  # Load surface parameters and merge into the main dataset
78
  surf = xr.open_dataset(file_path_surf, cache=True)
79
  # Convert the surface geopotential to elevation
80
  elevation = (surf.surface_geopotential / 9.80665).squeeze()
81
- #elevation.plot()
82
- subset['elevation'] = elevation
83
  air_temperature_0m = surf.air_temperature_0m.squeeze()
84
- subset['air_temperature_0m'] = air_temperature_0m
 
85
  # subset.elevation.plot()
86
- #%%
87
  def hybrid_to_height(ds):
88
  """
89
  ds = subset
@@ -93,56 +95,58 @@ def load_meps_for_location(file_path=None, altitude_min=0, altitude_max=3000):
93
  g = 9.80665 # Gravitational acceleration
94
 
95
  # Calculate the pressure at each level
96
- p = ds['ap'] + ds['b'] * ds['surface_air_pressure']#.mean("ensemble_member")
97
 
98
  # Get the temperature at each level
99
- T = ds['air_temperature_ml']#.mean("ensemble_member")
100
 
101
  # Calculate the height difference between each level and the surface
102
- dp = ds['surface_air_pressure'] - p # Pressure difference
103
  dT = T - T.isel(hybrid=-1) # Temperature difference relative to the surface
104
  dT_mean = 0.5 * (T + T.isel(hybrid=-1)) # Mean temperature
105
 
106
  # Calculate the height using the hypsometric equation
107
- dz = (R * dT_mean / g) * np.log(ds['surface_air_pressure'] / p)
108
 
109
  return dz
110
-
111
-
112
  altitude = hybrid_to_height(subset).mean("time").squeeze().mean("x").mean("y")
113
- subset = subset.assign_coords(altitude=('hybrid', altitude.data))
114
- subset = subset.swap_dims({'hybrid': 'altitude'})
115
 
116
  # filter subset on altitude ranges
117
- subset = subset.where((subset.altitude >= altitude_min) & (subset.altitude <= altitude_max), drop=True).squeeze()
 
 
118
 
119
- wind_speed = np.sqrt(subset['x_wind_ml']**2 + subset['y_wind_ml']**2)
120
- subset = subset.assign(wind_speed=(('time', 'altitude','y','x'), wind_speed.data))
121
 
122
-
123
- subset['thermal_temp_diff'] = compute_thermal_temp_difference(subset)
124
- #subset = subset.assign(thermal_temp_diff=(('time', 'altitude','y','x'), thermal_temp_diff.data))
125
 
126
  # Find the indices where the thermal temperature difference is zero or negative
127
  # Create tiny value at ground level to avoid finding the ground as the thermal top
128
- thermal_temp_diff = subset['thermal_temp_diff']
129
  thermal_temp_diff = thermal_temp_diff.where(
130
- (thermal_temp_diff.sum("altitude")>0)|(subset['altitude']!=subset.altitude.min()),
131
- thermal_temp_diff + 1e-6)
 
 
132
  indices = (thermal_temp_diff > 0).argmax(dim="altitude")
133
  # Get the altitudes corresponding to these indices
134
  thermal_top = subset.altitude[indices]
135
- subset = subset.assign(thermal_top=(('time', 'y', 'x'), thermal_top.data))
136
  subset = subset.set_coords(["latitude", "longitude"])
137
 
138
  return subset
139
 
140
 
141
- #%%
142
  def compute_thermal_temp_difference(subset):
143
  lapse_rate = 0.0098
144
- ground_temp = subset.air_temperature_0m-273.3
145
- air_temp = (subset['air_temperature_ml']-273.3)#.ffill(dim='altitude')
146
 
147
  # dimensions
148
  # 'air_temperature_ml' altitude: 4 y: 3, x: 3
@@ -157,89 +161,111 @@ def compute_thermal_temp_difference(subset):
157
  thermal_temp_diff = (ground_parcel_temp - air_temp).clip(min=0)
158
  return thermal_temp_diff
159
 
 
160
  def wind_and_temp_colorscales(wind_max=20, tempdiff_max=8):
161
  # build colorscale for thermal temperature difference
162
- wind_colors = ["grey", "blue", "green", "yellow", "red", "purple"]
163
- wind_positions = [0, 0.5, 3, 7, 12, 20] # transition points
164
- wind_positions_norm = [i/wind_max for i in wind_positions]
165
 
166
  # Create the colormap
167
- windcolors = mcolors.LinearSegmentedColormap.from_list("", list(zip(wind_positions_norm, wind_colors)))
168
-
 
169
 
170
  # build colorscale for thermal temperature difference
171
- thermal_colors = ['white', 'white', 'red', 'violet', "darkviolet"]
172
- thermal_positions = [0, 0.2, 2.0, 4, 8]
173
- thermal_positions_norm = [i/tempdiff_max for i in thermal_positions]
174
 
175
  # Create the colormap
176
- tempcolors = mcolors.LinearSegmentedColormap.from_list("", list(zip(thermal_positions_norm, thermal_colors)))
 
 
177
  return windcolors, tempcolors
178
 
179
- @st.cache_data(ttl=60)
180
- def create_wind_map(_subset, x_target, y_target, altitude_max=4000, date_start=None, date_end=None):
181
- """
182
- altitude_max = 3000
183
- date_start = None
184
- date_end = None
185
- """
186
- subset = _subset
187
 
 
 
 
 
188
 
189
 
 
 
 
 
 
 
190
  wind_min, wind_max = 0.3, 20
191
  tempdiff_min, tempdiff_max = 0, 8
192
- windcolors, tempcolors = wind_and_temp_colorscales(wind_max, tempdiff_max)
193
- # Filter location
194
- windplot_data = subset.sel(x=x_target, y=y_target, method="nearest")
195
-
196
- # Filter time periods and altitudes
197
  if date_start is None:
198
- date_start = datetime.datetime.fromtimestamp(subset.time.min().values.astype('int64') / 1e9)
 
 
199
  if date_end is None:
200
- date_end = datetime.datetime.fromtimestamp(subset.time.max().values.astype('int64') / 1e9)
 
 
 
 
201
  new_timestamps = pd.date_range(date_start, date_end, 20)
202
-
203
- new_altitude = np.arange(windplot_data.elevation.mean(), altitude_max, altitude_max/20)
 
 
 
204
  windplot_data = windplot_data.interp(altitude=new_altitude, time=new_timestamps)
205
 
206
- # BUILD PLOT
207
- fig, ax = plt.subplots(figsize=(15, 7))
208
- contourf = ax.contourf(windplot_data.time, windplot_data.altitude, windplot_data.thermal_temp_diff.T, cmap=tempcolors, alpha=0.5, vmin=0, vmax=8)
209
- fig.colorbar(contourf, ax=ax, label='Thermal Temperature Difference (°C)', pad=0.01, orientation='vertical')
210
-
211
- # Wind quiver plot
212
- quiverplot = windplot_data.plot.quiver(
213
- x='time', y='altitude', u='x_wind_ml', v='y_wind_ml',
214
- hue="wind_speed",
215
- cmap = windcolors,
216
- vmin=wind_min, vmax=wind_max,
217
- alpha=0.5,
218
- pivot="middle",# headwidth=4, headlength=6,
219
- ax=ax # Add this line to plot on the created axes
 
 
220
  )
221
- quiverplot.colorbar.set_label("Wind Speed [m/s]")
222
- quiverplot.colorbar.pad = 0.01
223
-
224
- # fill bottom with brown color
225
- plt.ylim(bottom=0)
226
- ax.fill_between(windplot_data.time, 0, windplot_data.elevation.mean(), color="brown", alpha=0.5)
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
- ax.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M'))
230
- # normalize wind speed for color mapping
231
- norm = plt.Normalize(wind_min, wind_max)
 
 
 
232
 
233
- # add numerical labels to the plot
234
- for x, t in enumerate(windplot_data.time.values):
235
- for y, alt in enumerate(windplot_data.altitude.values):
236
- color = windcolors(norm(windplot_data.wind_speed[x,y]))
237
- ax.text(t+5, alt+20, f"{windplot_data.wind_speed[x,y]:.1f}", size=6, color=color)
238
- plt.title(f"Wind and thermals in point starting at {date_start.strftime('%Y-%m-%d')} (UTC)")
239
- plt.yscale("linear")
240
  return fig
241
 
242
- #%%
 
243
  @st.cache_data(ttl=7200)
244
  def create_sounding(_subset, date, hour, x_target, y_target, altitude_max=3000):
245
  """
@@ -249,8 +275,8 @@ def create_sounding(_subset, date, hour, x_target, y_target, altitude_max=3000):
249
  y_target = 5
250
  """
251
  subset = _subset
252
- lapse_rate = 0.0098 # in degrees Celsius per meter
253
- subset = subset.where(subset.altitude< altitude_max,drop=True)
254
  # Create a figure object
255
  fig, ax = plt.subplots()
256
 
@@ -267,37 +293,50 @@ def create_sounding(_subset, date, hour, x_target, y_target, altitude_max=3000):
267
 
268
  # Plot the dry adiabatic lines
269
  for i in range(T0.shape[1]):
270
- ax.plot(T_adiabatic[:, i], ds.altitude, 'r:', alpha=0.5)
271
 
272
  # Plot the actual temperature profiles
273
  time_str = f"{date} {hour}:00:00"
274
  # find x and y values cloeset to given latitude and longitude
275
 
276
- ds_time = subset.sel(time=time_str, x=x_target,y=y_target, method="nearest")
277
- T = (ds_time['air_temperature_ml'].values-273.3) # in degrees Celsius
278
- ax.plot(T, ds_time.altitude, label=f"temp {pd.to_datetime(time_str).strftime('%H:%M')}")
 
 
279
 
280
  # Define the surface temperature
281
- T_surface = T[-1]+3
282
  T_parcel = T_surface - lapse_rate * ds_time.altitude
283
 
284
  # Plot the temperature of the rising air parcel
285
- filter = T_parcel>T
286
- ax.plot(T_parcel[filter], ds_time.altitude[filter], label='Rising air parcel',color="green")
 
 
 
 
 
287
 
288
  add_dry_adiabatic_lines(ds_time)
289
 
290
- ax.set_xlabel('Temperature (°C)')
291
- ax.set_ylabel('Altitude (m)')
292
- ax.set_title(f'Temperature Profile and Dry Adiabatic Lapse Rate for {date} {hour}:00')
293
- ax.legend(title='Time')
294
- xmin, xmax = ds_time['air_temperature_ml'].min().values-273.3, ds_time['air_temperature_ml'].max().values-273.3+3
 
 
 
 
 
295
  ax.set_xlim(xmin, xmax)
296
  ax.grid(True)
297
 
298
  # Return the figure object
299
  return fig
300
 
 
301
  @st.cache_data(ttl=7200)
302
  def build_map_overlays(_subset, date=None, hour=None):
303
  """
@@ -307,32 +346,58 @@ def build_map_overlays(_subset, date=None, hour=None):
307
  y_target=None
308
  """
309
  subset = _subset
310
-
311
  # Get the latitude and longitude values from the dataset
312
  latitude_values = subset.latitude.values.flatten()
313
  longitude_values = subset.longitude.values.flatten()
314
  thermal_top_values = subset.thermal_top.sel(time=f"{date}T{hour}").values.flatten()
315
- #thermal_top_values = subset.elevation.mean("altitude").values.flatten()
316
  # Convert the irregular grid data into a regular grid
317
- step_lon, step_lat = subset.longitude.diff("x").quantile(0.1).values, subset.latitude.diff("y").quantile(0.1).values
318
- grid_x, grid_y = np.mgrid[min(latitude_values):max(latitude_values):step_lat, min(longitude_values):max(longitude_values):step_lon]
319
- grid_z = griddata((latitude_values, longitude_values), thermal_top_values, (grid_x, grid_y), method='linear')
 
 
 
 
 
 
 
 
 
 
 
320
  grid_z = np.nan_to_num(grid_z, copy=False, nan=0)
321
  # Normalize the grid data to a range suitable for image display
322
  heightcolor = cm.LinearColormap(
323
- colors = ['white', 'white', 'green', 'yellow', 'orange','red', 'darkblue'],
324
- index = [0, 500, 1000, 1500, 2000, 2500, 3000],
325
- vmin=0, vmax=3000,
326
- caption='Thermal Height (m)')
327
-
 
328
 
329
- bounds = [[min(latitude_values), min(longitude_values)], [max(latitude_values), max(longitude_values)]]
330
- img_overlay = folium.raster_layers.ImageOverlay(image=grid_z, bounds=bounds, colormap=heightcolor, opacity=0.4, mercator_project=True, origin="lower",pixelated=False)
 
 
 
 
 
 
 
 
 
 
 
331
 
332
  return img_overlay, heightcolor
333
 
334
- #%%
 
335
  import pyproj
 
 
336
  def latlon_to_xy(lat, lon):
337
  crs = pyproj.CRS.from_cf(
338
  {
@@ -347,25 +412,28 @@ def latlon_to_xy(lat, lon):
347
  proj = pyproj.Proj.from_crs(4326, crs, always_xy=True)
348
 
349
  # Compute projected coordinates of lat/lon point
350
- X,Y = proj.transform(lon,lat)
351
- return X,Y
 
 
352
  # %%
353
  def show_forecast():
354
-
355
- with st.spinner('Fetching data...'):
356
  if "file_path" not in st.session_state:
357
  st.session_state.file_path = find_latest_meps_file()
358
  subset = load_data(st.session_state.file_path)
359
 
360
  def date_controls():
361
-
362
- start_stop_time = [subset.time.min().values.astype('M8[ms]').astype('O'), subset.time.max().values.astype('M8[ms]').astype('O')]
 
 
363
  now = datetime.datetime.now().replace(minute=0, second=0, microsecond=0)
364
 
365
  if "forecast_date" not in st.session_state:
366
  st.session_state.forecast_date = (now + datetime.timedelta(days=1)).date()
367
  if "forecast_time" not in st.session_state:
368
- st.session_state.forecast_time = datetime.time(14,0)
369
  if "forecast_length" not in st.session_state:
370
  st.session_state.forecast_length = 1
371
  if "altitude_max" not in st.session_state:
@@ -374,96 +442,139 @@ def show_forecast():
374
  st.session_state.target_latitude = 61.22908
375
  if "target_longitude" not in st.session_state:
376
  st.session_state.target_longitude = 7.09674
377
- col1, col_date, col_time, col3 = st.columns([0.2,0.6,0.2,0.2])
378
 
379
  with col1:
380
  if st.button("⏮️", use_container_width=True):
381
  st.session_state.forecast_date -= datetime.timedelta(days=1)
382
  with col3:
383
- if st.button("⏭️", use_container_width=True, disabled=(st.session_state.forecast_date == start_stop_time[1])):
 
 
 
 
384
  st.session_state.forecast_date += datetime.timedelta(days=1)
385
  with col_date:
386
  st.session_state.forecast_date = st.date_input(
387
- "Start date",
388
- value=st.session_state.forecast_date,
389
- min_value=start_stop_time[0],
390
- max_value=start_stop_time[1],
391
  label_visibility="collapsed",
392
- disabled=True
393
- )
394
  with col_time:
395
- st.session_state.forecast_time = st.time_input("Start time", value=st.session_state.forecast_time, step=3600,disabled=False,label_visibility="collapsed")
 
 
 
 
 
 
396
 
397
  date_controls()
398
  time_start = datetime.time(0, 0)
399
  # convert subset.attrs['min_time']='2024-05-11T06:00:00Z' into datetime
400
- min_time = datetime.datetime.strptime(subset.attrs['min_time'], "%Y-%m-%dT%H:%M:%SZ")
 
 
401
  date_start = datetime.datetime.combine(st.session_state.forecast_date, time_start)
402
  date_start = max(date_start, min_time)
403
- date_end= datetime.datetime.combine(st.session_state.forecast_date+datetime.timedelta(days=st.session_state.forecast_length), datetime.time(0, 0))
 
 
 
 
404
 
405
  ## MAP
406
  with st.expander("Map", expanded=True):
407
  from streamlit_folium import st_folium
 
408
  st.cache_data(ttl=30)
 
409
  def build_map(date, hour):
410
- m = folium.Map(location=[61.22908, 7.09674], zoom_start=9, tiles="openstreetmap")
 
 
411
  img_overlay, heightcolor = build_map_overlays(subset, date=date, hour=hour)
412
-
413
  img_overlay.add_to(m)
414
- m.add_child(heightcolor,name="Thermal Height (m)")
415
  m.add_child(folium.LatLngPopup())
416
  return m
417
- m = build_map(date = st.session_state.forecast_date,hour=st.session_state.forecast_time)
418
- map=st_folium(m)
419
- def get_pos(lat,lng):
420
- return lat,lng
421
- if map['last_clicked'] is not None:
422
- st.session_state.target_latitude, st.session_state.target_longitude = get_pos(map['last_clicked']['lat'],map['last_clicked']['lng'])
423
-
424
- x_target, y_target = latlon_to_xy(st.session_state.target_latitude, st.session_state.target_longitude)
 
 
 
 
 
 
 
 
 
425
  wind_fig = create_wind_map(
426
- subset,
427
- date_start=date_start,
428
- date_end=date_end,
429
- altitude_max=st.session_state.altitude_max,
430
- x_target=x_target,
431
- y_target=y_target)
 
432
  st.pyplot(wind_fig)
433
  plt.close()
434
-
435
 
436
  with st.expander("More settings", expanded=False):
437
- st.session_state.forecast_length = st.number_input("multiday", 1, 3, 1, step=1,)
438
- st.session_state.altitude_max = st.number_input("Max altitude", 0, 4000, 3000, step=500)
439
-
 
 
 
 
 
 
 
 
440
  ############################
441
  ######### SOUNDING #########
442
  ############################
443
  st.markdown("---")
444
  with st.expander("Sounding", expanded=False):
445
- date = datetime.datetime.combine(st.session_state.forecast_date, st.session_state.forecast_time)
 
 
446
 
447
- with st.spinner('Building sounding...'):
448
  sounding_fig = create_sounding(
449
- subset,
450
- date=date.date(),
451
- hour=date.hour,
452
  altitude_max=st.session_state.altitude_max,
453
  x_target=x_target,
454
- y_target=y_target)
 
455
  st.pyplot(sounding_fig)
456
  plt.close()
457
 
458
- 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.")
 
 
459
 
460
  # Download new forecast if available
461
  st.session_state.file_path = find_latest_meps_file()
462
  subset = load_data(st.session_state.file_path)
463
 
 
464
  @st.cache_data
465
  def load_data(filepath):
466
- local=False
467
  if local:
468
  subset = xr.open_dataset("subset.nc")
469
  else:
@@ -471,27 +582,28 @@ def load_data(filepath):
471
  subset.to_netcdf("subset.nc")
472
  return subset
473
 
 
474
  if __name__ == "__main__":
475
  run_streamlit = True
476
  if run_streamlit:
477
- st.set_page_config(page_title="PGWeather",page_icon="🪂", layout="wide")
478
  show_forecast()
479
  else:
480
  lat = 61.22908
481
  lon = 7.09674
482
  x_target, y_target = latlon_to_xy(lat, lon)
483
-
484
  dataset_file_path = find_latest_meps_file()
485
  subset = load_data(dataset_file_path)
486
 
487
  build_map_overlays(subset, date="2024-05-14", hour="16")
488
 
489
- wind_fig = create_wind_map(subset, altitude_max=3000,x_target=x_target, y_target=y_target)
490
-
 
491
 
492
  # Plot thermal top on a map for a specific time
493
- #subset.sel(time=subset.time.min()).thermal_top.plot()
494
- sounding_fig = create_sounding(subset, date="2024-05-12", hour=15, x_target=x_target, y_target=y_target)
495
-
496
-
497
-
 
1
+ # %%
2
  import xarray as xr
3
  from siphon.catalog import TDSCatalog
4
  import numpy as np
 
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
 
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]"
 
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}",
 
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,
 
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
 
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
 
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):
166
  # build colorscale for thermal temperature difference
167
+ wind_colors = ["grey", "blue", "green", "yellow", "red", "purple"]
168
+ wind_positions = [0, 0.5, 3, 7, 12, 20] # transition points
169
+ wind_positions_norm = [i / wind_max for i in wind_positions]
170
 
171
  # Create the colormap
172
+ windcolors = mcolors.LinearSegmentedColormap.from_list(
173
+ "", list(zip(wind_positions_norm, wind_colors))
174
+ )
175
 
176
  # build colorscale for thermal temperature difference
177
+ thermal_colors = ["white", "white", "red", "violet", "darkviolet"]
178
+ thermal_positions = [0, 0.2, 2.0, 4, 8]
179
+ thermal_positions_norm = [i / tempdiff_max for i in thermal_positions]
180
 
181
  # Create the colormap
182
+ tempcolors = mcolors.LinearSegmentedColormap.from_list(
183
+ "", list(zip(thermal_positions_norm, thermal_colors))
184
+ )
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
202
+ wind_colors = ["grey", "blue", "green", "yellow", "red", "purple"]
203
+
 
 
 
204
  if date_start is None:
205
+ date_start = datetime.datetime.fromtimestamp(
206
+ subset.time.min().values.astype("int64") // 1e9
207
+ )
208
  if date_end is None:
209
+ date_end = datetime.datetime.fromtimestamp(
210
+ subset.time.max().values.astype("int64") // 1e9
211
+ )
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
223
+ thermal_diff = windplot_data["thermal_temp_diff"].T.values
224
+ times = [pd.Timestamp(time).strftime("%H:%M") for time in windplot_data.time.values]
225
+ altitudes = windplot_data.altitude.values
226
+
227
+ # Creating Plotly heatmap
228
+ fig = go.Figure(
229
+ data=go.Heatmap(
230
+ z=thermal_diff,
231
+ x=times,
232
+ y=altitudes,
233
+ colorscale="YlGn",
234
+ colorbar=dict(title="Thermal Temperature Difference (°C)"),
235
+ zmin=tempdiff_min,
236
+ zmax=tempdiff_max,
237
+ )
238
  )
 
 
 
 
 
 
239
 
240
+ # Add wind quiver plots (Note: Plotly doesn't support quivers directly like matplotlib; consider using streamlines or other visualization methods for precise vector representation).
241
+ speed = np.sqrt(windplot_data["x_wind_ml"] ** 2 + windplot_data["y_wind_ml"] ** 2).T
242
+ fig.add_trace(
243
+ go.Scatter(
244
+ x=times,
245
+ y=altitudes,
246
+ mode="markers",
247
+ marker=dict(
248
+ size=8,
249
+ color=speed,
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
+ )
257
 
258
+ # Update layout
259
+ fig.update_layout(
260
+ title=f"Wind and Thermals Starting at {date_start.strftime('%Y-%m-%d')} (UTC)",
261
+ xaxis=dict(title="Time"),
262
+ yaxis=dict(title="Altitude (m)"),
263
+ )
264
 
 
 
 
 
 
 
 
265
  return fig
266
 
267
+
268
+ # %%
269
  @st.cache_data(ttl=7200)
270
  def create_sounding(_subset, date, hour, x_target, y_target, altitude_max=3000):
271
  """
 
275
  y_target = 5
276
  """
277
  subset = _subset
278
+ lapse_rate = 0.0098 # in degrees Celsius per meter
279
+ subset = subset.where(subset.altitude < altitude_max, drop=True)
280
  # Create a figure object
281
  fig, ax = plt.subplots()
282
 
 
293
 
294
  # Plot the dry adiabatic lines
295
  for i in range(T0.shape[1]):
296
+ ax.plot(T_adiabatic[:, i], ds.altitude, "r:", alpha=0.5)
297
 
298
  # Plot the actual temperature profiles
299
  time_str = f"{date} {hour}:00:00"
300
  # find x and y values cloeset to given latitude and longitude
301
 
302
+ ds_time = subset.sel(time=time_str, x=x_target, y=y_target, method="nearest")
303
+ T = ds_time["air_temperature_ml"].values - 273.3 # in degrees Celsius
304
+ ax.plot(
305
+ T, ds_time.altitude, label=f"temp {pd.to_datetime(time_str).strftime('%H:%M')}"
306
+ )
307
 
308
  # Define the surface temperature
309
+ T_surface = T[-1] + 3
310
  T_parcel = T_surface - lapse_rate * ds_time.altitude
311
 
312
  # Plot the temperature of the rising air parcel
313
+ filter = T_parcel > T
314
+ ax.plot(
315
+ T_parcel[filter],
316
+ ds_time.altitude[filter],
317
+ label="Rising air parcel",
318
+ color="green",
319
+ )
320
 
321
  add_dry_adiabatic_lines(ds_time)
322
 
323
+ ax.set_xlabel("Temperature (°C)")
324
+ ax.set_ylabel("Altitude (m)")
325
+ ax.set_title(
326
+ f"Temperature Profile and Dry Adiabatic Lapse Rate for {date} {hour}:00"
327
+ )
328
+ ax.legend(title="Time")
329
+ xmin, xmax = (
330
+ ds_time["air_temperature_ml"].min().values - 273.3,
331
+ ds_time["air_temperature_ml"].max().values - 273.3 + 3,
332
+ )
333
  ax.set_xlim(xmin, xmax)
334
  ax.grid(True)
335
 
336
  # Return the figure object
337
  return fig
338
 
339
+
340
  @st.cache_data(ttl=7200)
341
  def build_map_overlays(_subset, date=None, hour=None):
342
  """
 
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
  {
 
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:
 
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
+ )
544
+
545
  ############################
546
  ######### SOUNDING #########
547
  ############################
548
  st.markdown("---")
549
  with st.expander("Sounding", expanded=False):
550
+ date = datetime.datetime.combine(
551
+ st.session_state.forecast_date, st.session_state.forecast_time
552
+ )
553
 
554
+ with st.spinner("Building sounding..."):
555
  sounding_fig = create_sounding(
556
+ subset,
557
+ date=date.date(),
558
+ hour=date.hour,
559
  altitude_max=st.session_state.altitude_max,
560
  x_target=x_target,
561
+ y_target=y_target,
562
+ )
563
  st.pyplot(sounding_fig)
564
  plt.close()
565
 
566
+ st.markdown(
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:
 
582
  subset.to_netcdf("subset.nc")
583
  return subset
584
 
585
+
586
  if __name__ == "__main__":
587
  run_streamlit = True
588
  if run_streamlit:
589
+ st.set_page_config(page_title="PGWeather", page_icon="🪂", layout="wide")
590
  show_forecast()
591
  else:
592
  lat = 61.22908
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(
602
+ subset, altitude_max=3000, x_target=x_target, y_target=y_target
603
+ )
604
 
605
  # Plot thermal top on a map for a specific time
606
+ # subset.sel(time=subset.time.min()).thermal_top.plot()
607
+ sounding_fig = create_sounding(
608
+ subset, date="2024-05-12", hour=15, x_target=x_target, y_target=y_target
609
+ )