simeneide commited on
Commit
f340e6f
1 Parent(s): 4b25fb9
Files changed (2) hide show
  1. app.py +503 -0
  2. requirements.txt +19 -0
app.py ADDED
@@ -0,0 +1,503 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #%%
2
+ import xarray as xr
3
+ from siphon.catalog import TDSCatalog
4
+ import numpy as np
5
+ import matplotlib.pyplot as plt
6
+ 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
+ @st.cache_data(ttl=60)
16
+ def find_latest_meps_file():
17
+ # The MEPS dataset: https://github.com/metno/NWPdocs/wiki/MEPS-dataset
18
+ today = datetime.datetime.today()
19
+ catalog_url = f"https://thredds.met.no/thredds/catalog/meps25epsarchive/{today.year}/{today.month:02d}/{today.day:02d}/catalog.xml"
20
+ file_url_base = f"https://thredds.met.no/thredds/dodsC/meps25epsarchive/{today.year}/{today.month:02d}/{today.day:02d}"
21
+ # Get the datasets from the catalog
22
+ catalog = TDSCatalog(catalog_url)
23
+ datasets = [s for s in catalog.datasets if "meps_det_ml" in s]
24
+ file_path = f"{file_url_base}/{sorted(datasets)[-1]}"
25
+ return file_path
26
+
27
+
28
+ @st.cache_data()
29
+ def load_meps_for_location(file_path=None, altitude_min=0, altitude_max=3000):
30
+ """
31
+ file_path=None
32
+ altitude_min=0
33
+ altitude_max=3000
34
+ """
35
+
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]"
44
+
45
+ params = {
46
+ "x": x_range,
47
+ "y": y_range,
48
+ "time": time_range,
49
+ "hybrid": hybrid_range,
50
+ "height": height_range,
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}",
59
+ }
60
+
61
+ path = f"{file_path}?{','.join(f'{k}{v}' for k, v in params.items())}"
62
+
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,
70
+ "y": y_range,
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
90
+ """
91
+ # Constants
92
+ R = 287.05 # Gas constant for dry air
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
149
+ # 'elevation' y: 3 x: 3
150
+ # 'altitude' altitude: 4
151
+
152
+ # broadcast ground temperature to all altitudes, but let it decrease by lapse rate
153
+ altitude_diff = subset.altitude - subset.elevation
154
+ altitude_diff = altitude_diff.where(altitude_diff >= 0, 0)
155
+ temp_decrease = lapse_rate * altitude_diff
156
+ ground_parcel_temp = ground_temp - temp_decrease
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
+ """
246
+ date = "2024-05-12"
247
+ hour = "15"
248
+ x_target = 5
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
+
257
+ # Define the dry adiabatic lapse rate
258
+ def add_dry_adiabatic_lines(ds):
259
+ # Define a range of temperatures at sea level
260
+ T0 = np.arange(-40, 40, 5) # temperatures from -40°C to 40°C in steps of 10°C
261
+
262
+ # Create a 2D grid of temperatures and altitudes
263
+ T0, altitude = np.meshgrid(T0, ds.altitude)
264
+
265
+ # Calculate the temperatures at each altitude
266
+ T_adiabatic = T0 - lapse_rate * altitude
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
+ """
304
+ date = "2024-05-13"
305
+ hour = "15"
306
+ x_target=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
+ {
339
+ "grid_mapping_name": "lambert_conformal_conic",
340
+ "standard_parallel": [63.3, 63.3],
341
+ "longitude_of_central_meridian": 15.0,
342
+ "latitude_of_projection_origin": 63.3,
343
+ "earth_radius": 6371000.0,
344
+ }
345
+ )
346
+ # Transformer to project from ESPG:4368 (WGS:84) to our lambert_conformal_conic
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
+ @st.cache_data
357
+ def load_data(filepath):
358
+ local=False
359
+ if local:
360
+ subset = xr.open_dataset("subset.nc")
361
+ else:
362
+ subset = load_meps_for_location(filepath)
363
+ subset.to_netcdf("subset.nc")
364
+ return subset
365
+
366
+ if "file_path" not in st.session_state:
367
+ st.session_state.file_path = find_latest_meps_file()
368
+ subset = load_data(st.session_state.file_path)
369
+
370
+ def date_controls():
371
+
372
+ start_stop_time = [subset.time.min().values.astype('M8[ms]').astype('O'), subset.time.max().values.astype('M8[ms]').astype('O')]
373
+ now = datetime.datetime.now().replace(minute=0, second=0, microsecond=0)
374
+
375
+ if "forecast_date" not in st.session_state:
376
+ st.session_state.forecast_date = now.date()
377
+ if "forecast_time" not in st.session_state:
378
+ st.session_state.forecast_time = datetime.time(14,0)
379
+ if "forecast_length" not in st.session_state:
380
+ st.session_state.forecast_length = 1
381
+ if "altitude_max" not in st.session_state:
382
+ st.session_state.altitude_max = 3000
383
+ if "target_latitude" not in st.session_state:
384
+ st.session_state.target_latitude = 61.22908
385
+ if "target_longitude" not in st.session_state:
386
+ st.session_state.target_longitude = 7.09674
387
+ col1, col_date, col_time, col3 = st.columns([0.2,0.6,0.2,0.2])
388
+
389
+ with col1:
390
+ if st.button("⏮️", use_container_width=True):
391
+ st.session_state.forecast_date -= datetime.timedelta(days=1)
392
+ with col3:
393
+ if st.button("⏭️", use_container_width=True, disabled=(st.session_state.forecast_date == start_stop_time[1])):
394
+ st.session_state.forecast_date += datetime.timedelta(days=1)
395
+ with col_date:
396
+ st.session_state.forecast_date = st.date_input(
397
+ "Start date",
398
+ value=st.session_state.forecast_date,
399
+ min_value=start_stop_time[0],
400
+ max_value=start_stop_time[1],
401
+ label_visibility="collapsed",
402
+ disabled=True
403
+ )
404
+ with col_time:
405
+ st.session_state.forecast_time = st.time_input("Start time", value=st.session_state.forecast_time, step=3600,disabled=False,label_visibility="collapsed")
406
+
407
+ date_controls()
408
+ time_start = datetime.time(0, 0)
409
+ # convert subset.attrs['min_time']='2024-05-11T06:00:00Z' into datetime
410
+ min_time = datetime.datetime.strptime(subset.attrs['min_time'], "%Y-%m-%dT%H:%M:%SZ")
411
+ date_start = datetime.datetime.combine(st.session_state.forecast_date, time_start)
412
+ date_start = max(date_start, min_time)
413
+ date_end= datetime.datetime.combine(st.session_state.forecast_date+datetime.timedelta(days=st.session_state.forecast_length), datetime.time(0, 0))
414
+
415
+ ## MAP
416
+ with st.expander("Map", expanded=True):
417
+ from streamlit_folium import st_folium
418
+ st.cache_data(ttl=30)
419
+ def build_map(date, hour):
420
+ m = folium.Map(location=[61.22908, 7.09674], zoom_start=9, tiles="openstreetmap")
421
+ img_overlay, heightcolor = build_map_overlays(subset, date=date, hour=hour)
422
+
423
+ img_overlay.add_to(m)
424
+ m.add_child(heightcolor,name="Thermal Height (m)")
425
+ m.add_child(folium.LatLngPopup())
426
+ return m
427
+ m = build_map(date = st.session_state.forecast_date,hour=st.session_state.forecast_time)
428
+ map=st_folium(m)
429
+ def get_pos(lat,lng):
430
+ return lat,lng
431
+ if map['last_clicked'] is not None:
432
+ st.session_state.target_latitude, st.session_state.target_longitude = get_pos(map['last_clicked']['lat'],map['last_clicked']['lng'])
433
+
434
+ x_target, y_target = latlon_to_xy(st.session_state.target_latitude, st.session_state.target_longitude)
435
+ wind_fig = create_wind_map(
436
+ subset,
437
+ date_start=date_start,
438
+ date_end=date_end,
439
+ altitude_max=st.session_state.altitude_max,
440
+ x_target=x_target,
441
+ y_target=y_target)
442
+ st.pyplot(wind_fig)
443
+ plt.close()
444
+
445
+
446
+ with st.expander("More settings", expanded=False):
447
+ st.session_state.forecast_length = st.number_input("multiday", 1, 3, 1, step=1,)
448
+ st.session_state.altitude_max = st.number_input("Max altitude", 0, 4000, 3000, step=500)
449
+
450
+ ############################
451
+ ######### SOUNDING #########
452
+ ############################
453
+ st.markdown("---")
454
+ with st.expander("Sounding", expanded=False):
455
+ date = datetime.datetime.combine(st.session_state.forecast_date, st.session_state.forecast_time)
456
+
457
+ with st.spinner('Building sounding...'):
458
+ sounding_fig = create_sounding(
459
+ subset,
460
+ date=date.date(),
461
+ hour=date.hour,
462
+ altitude_max=st.session_state.altitude_max,
463
+ x_target=x_target,
464
+ y_target=y_target)
465
+ st.pyplot(sounding_fig)
466
+ plt.close()
467
+
468
+ 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.")
469
+
470
+ # Download new forecast if available
471
+ st.session_state.file_path = find_latest_meps_file()
472
+ subset = load_data(st.session_state.file_path)
473
+
474
+
475
+ if __name__ == "__main__":
476
+ run_streamlit = True
477
+ if run_streamlit:
478
+ st.set_page_config(page_title="PGWeather",page_icon="🪂", layout="wide")
479
+ show_forecast()
480
+ else:
481
+ lat = 61.22908
482
+ lon = 7.09674
483
+ x_target, y_target = latlon_to_xy(lat, lon)
484
+
485
+ dataset_file_path = find_latest_meps_file()
486
+ local=True
487
+ if local:
488
+ subset = xr.open_dataset("subset.nc")
489
+ else:
490
+ subset = load_meps_for_location()
491
+ subset.to_netcdf("subset.nc")
492
+
493
+ build_map_overlays(subset, date="2024-05-14", hour="16")
494
+
495
+ wind_fig = create_wind_map(subset, altitude_max=3000,x_target=x_target, y_target=y_target)
496
+
497
+
498
+ # Plot thermal top on a map for a specific time
499
+ #subset.sel(time=subset.time.min()).thermal_top.plot()
500
+ sounding_fig = create_sounding(subset, date="2024-05-12", hour=15, x_target=x_target, y_target=y_target)
501
+
502
+
503
+
requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ plotly>=4.12.0
2
+ requests>=2.24.0
3
+ streamlit>=0.85.1
4
+ pandas>=1.1.3
5
+ geopandas>=0.10.2
6
+ metar>=1.8.0
7
+ python-dateutil>=2.8.1
8
+ #netatmo #>=1.0.7
9
+ numpy
10
+ shapely
11
+ matplotlib
12
+ folium
13
+ streamlit-folium
14
+ windrose
15
+ xarray
16
+ siphon
17
+ netcdf4
18
+ scipy
19
+ bottleneck