Mahmudm commited on
Commit
15afaaa
Β·
verified Β·
1 Parent(s): 4f3c9d9

Upload 4 files

Browse files
NameModel_app.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:58ecfedfead9237ae58b93ed6351f6492703145dfae1349591f9d9ac489a8867
3
+ size 741076
Taal_273070_20200112_scenario_yizhou.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9bb340a75132c3008a149557ff85f8bc05b4a46e70eee027503e30b9573fdd39
3
+ size 181349
app.py CHANGED
@@ -1,108 +1,323 @@
 
 
 
 
 
1
  import panel as pn
2
- import pandas as pd
3
- import hvplot.pandas
 
 
 
 
 
 
 
4
 
5
- pn.extension("tabulator")
6
 
7
- ACCENT="teal"
 
8
 
9
- styles = {
10
- "box-shadow": "rgba(50, 50, 93, 0.25) 0px 6px 12px -2px, rgba(0, 0, 0, 0.3) 0px 3px 7px -3px",
11
- "border-radius": "4px",
12
- "padding": "10px",
13
- }
14
 
15
- image = pn.pane.JPG("https://assets.holoviz.org/panel/tutorials/wind_turbines_sunset.png")
16
 
17
- # Extract Data
 
 
 
 
18
 
19
- @pn.cache() # only download data once
20
- def get_data():
21
- return pd.read_csv("https://assets.holoviz.org/panel/tutorials/turbines.csv.gz")
22
-
23
- # Transform Data
 
 
 
 
 
 
24
 
25
- source_data = get_data()
26
- min_year = int(source_data["p_year"].min())
27
- max_year = int(source_data["p_year"].max())
28
- top_manufacturers = (
29
- source_data.groupby("t_manu").p_cap.sum().sort_values().iloc[-10:].index.to_list()
30
  )
31
 
32
- def filter_data(t_manu, year):
33
- data = source_data[(source_data.t_manu == t_manu) & (source_data.p_year <= year)]
34
- return data
 
 
35
 
36
- # Filters
 
 
 
37
 
38
- t_manu = pn.widgets.Select(
39
- name="Manufacturer",
40
- value="Vestas",
41
- options=sorted(top_manufacturers),
42
- description="The name of the manufacturer",
43
- )
44
- p_year = pn.widgets.IntSlider(name="Year", value=max_year, start=min_year, end=max_year)
45
-
46
- # Transform Data 2
47
-
48
- df = pn.rx(filter_data)(t_manu=t_manu, year=p_year)
49
- count = df.rx.len()
50
- total_capacity = df.t_cap.sum()
51
- avg_capacity = df.t_cap.mean()
52
- avg_rotor_diameter = df.t_rd.mean()
53
-
54
- # Plot Data
55
-
56
- fig = (
57
- df[["p_year", "t_cap"]].groupby("p_year").sum() / 10**6
58
- ).hvplot.bar(
59
- title="Capacity Change",
60
- rot=90,
61
- ylabel="Capacity (GW)",
62
- xlabel="Year",
63
- xlim=(min_year, max_year),
64
- color=ACCENT,
65
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
- # Display Data
68
-
69
- indicators = pn.FlexBox(
70
- pn.indicators.Number(
71
- value=count, name="Count", format="{value:,.0f}", styles=styles
72
- ),
73
- pn.indicators.Number(
74
- value=total_capacity / 1e6,
75
- name="Total Capacity (GW)",
76
- format="{value:,.1f}",
77
- styles=styles,
78
- ),
79
- pn.indicators.Number(
80
- value=avg_capacity/1e3,
81
- name="Avg. Capacity (MW)",
82
- format="{value:,.1f}",
83
- styles=styles,
84
- ),
85
- pn.indicators.Number(
86
- value=avg_rotor_diameter,
87
- name="Avg. Rotor Diameter (m)",
88
- format="{value:,.1f}",
89
- styles=styles,
90
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  )
92
 
93
- plot = pn.pane.HoloViews(fig, sizing_mode="stretch_both", name="Plot")
94
- table = pn.widgets.Tabulator(df, sizing_mode="stretch_both", name="Table")
 
 
 
 
 
95
 
96
- # Layout Data
 
 
 
 
 
97
 
98
  tabs = pn.Tabs(
99
- plot, table, styles=styles, sizing_mode="stretch_width", height=500, margin=10
 
 
100
  )
101
 
 
 
 
 
102
  pn.template.FastListTemplate(
103
- title="Wind Turbine Dashboard",
104
- sidebar=[image, t_manu, p_year],
105
- main=[pn.Column(indicators, tabs, sizing_mode="stretch_both")],
106
- main_layout=None,
107
- accent=ACCENT,
108
  ).servable()
 
1
+ import os
2
+ import glob
3
+ import shutil
4
+ import io
5
+ import logging
6
  import panel as pn
7
+ import xarray as xr
8
+ import numpy as np
9
+ from datetime import datetime
10
+ from types import SimpleNamespace
11
+ from collections import defaultdict
12
+ from ash_animator.converter import NAMEDataProcessor
13
+ from ash_animator.plot_3dfield_data import Plot_3DField_Data
14
+ from ash_animator.plot_horizontal_data import Plot_Horizontal_Data
15
+ from ash_animator import create_grid
16
 
17
+ pn.extension()
18
 
19
+ MEDIA_DIR = "media"
20
+ os.makedirs(MEDIA_DIR, exist_ok=True)
21
 
22
+ # Logging setup
23
+ LOG_FILE = os.path.join(MEDIA_DIR, "app_errors.log")
24
+ logging.basicConfig(filename=LOG_FILE, level=logging.ERROR,
25
+ format="%(asctime)s - %(levelname)s - %(message)s")
 
26
 
27
+ animator_obj = {}
28
 
29
+ # ---------------- Widgets ----------------
30
+ file_input = pn.widgets.FileInput(accept=".zip")
31
+ process_button = pn.widgets.Button(name="πŸ“¦ Process ZIP", button_type="primary")
32
+ reset_button = pn.widgets.Button(name="πŸ”„ Reset App", button_type="danger")
33
+ status = pn.pane.Markdown("### Upload a NAME Model ZIP to begin")
34
 
35
+ download_button = pn.widgets.FileDownload(
36
+ label="⬇️ Download All Exports",
37
+ filename="all_exports.zip",
38
+ button_type="success",
39
+ callback=lambda: io.BytesIO(
40
+ open(shutil.make_archive(
41
+ os.path.join(MEDIA_DIR, "all_exports").replace(".zip", ""),
42
+ "zip", MEDIA_DIR
43
+ ), 'rb').read()
44
+ )
45
+ )
46
 
47
+ log_link = pn.widgets.FileDownload(
48
+ label="πŸͺ΅ View Error Log", file=LOG_FILE,
49
+ filename="app_errors.log", button_type="warning"
 
 
50
  )
51
 
52
+ threshold_slider_3d = pn.widgets.FloatSlider(name='3D Threshold', start=0.0, end=1.0, step=0.05, value=0.1)
53
+ zoom_slider_3d = pn.widgets.IntSlider(name='3D Zoom Level', start=1, end=20, value=19)
54
+ cmap_select_3d = pn.widgets.Select(name='3D Colormap', options=["rainbow", "viridis", "plasma"])
55
+ fps_slider_3d = pn.widgets.IntSlider(name='3D FPS', start=1, end=10, value=2)
56
+ Altitude_slider = pn.widgets.IntSlider(name='Define Ash Altitude', start=1, end=15, value=1)
57
 
58
+ threshold_slider_2d = pn.widgets.FloatSlider(name='2D Threshold', start=0.0, end=1.0, step=0.01, value=0.005)
59
+ zoom_slider_2d = pn.widgets.IntSlider(name='2D Zoom Level', start=1, end=20, value=19)
60
+ fps_slider_2d = pn.widgets.IntSlider(name='2D FPS', start=1, end=10, value=2)
61
+ cmap_select_2d = pn.widgets.Select(name='2D Colormap', options=["rainbow", "viridis", "plasma"])
62
 
63
+ # ---------------- Core Functions ----------------
64
+ def process_zip(event=None):
65
+ if file_input.value:
66
+ zip_path = os.path.join(MEDIA_DIR, file_input.filename)
67
+ with open(zip_path, "wb") as f:
68
+ f.write(file_input.value)
69
+ status.object = "βœ… ZIP uploaded and saved."
70
+ else:
71
+ zip_path = os.path.join(MEDIA_DIR, "default_model.zip")
72
+ if not os.path.exists(zip_path):
73
+ status.object = "❌ No ZIP uploaded and default_model.zip not found."
74
+ return
75
+ status.object = "πŸ“¦ Using default_model.zip"
76
+
77
+ output_dir = os.path.join("./", "ash_output")
78
+ shutil.rmtree(output_dir, ignore_errors=True)
79
+ os.makedirs(output_dir, exist_ok=True)
80
+
81
+ try:
82
+ processor = NAMEDataProcessor(output_root=output_dir)
83
+ processor.batch_process_zip(zip_path)
84
+
85
+ # animator_obj["3d"] = [xr.open_dataset(fp).load()
86
+ # for fp in sorted(glob.glob(os.path.join(output_dir, "3D", "*.nc")))]
87
+
88
+ animator_obj["3d"] = []
89
+ for fp in sorted(glob.glob(os.path.join(output_dir, "3D", "*.nc"))):
90
+ with xr.open_dataset(fp) as ds:
91
+ animator_obj["3d"].append(ds.load())
92
+
93
+ animator_obj["2d"] = []
94
+ for fp in sorted(glob.glob(os.path.join(output_dir, "horizontal", "*.nc"))):
95
+ with xr.open_dataset(fp) as ds:
96
+ animator_obj["2d"].append(ds.load())
97
+
98
+
99
+ # animator_obj["2d"] = [xr.open_dataset(fp).load()
100
+ # for fp in sorted(glob.glob(os.path.join(output_dir, "horizontal", "*.nc")))]
101
+
102
+ with open(os.path.join(MEDIA_DIR, "last_run.txt"), "w") as f:
103
+ f.write(zip_path)
104
+
105
+ status.object += f" | βœ… Loaded 3D: {len(animator_obj['3d'])} & 2D: {len(animator_obj['2d'])}"
106
+ update_media_tabs()
107
+ except Exception as e:
108
+ logging.exception("Error during ZIP processing")
109
+ status.object = f"❌ Processing failed: {e}"
110
+
111
+ def reset_app(event=None):
112
+ animator_obj.clear()
113
+ file_input.value = None
114
+ status.object = "πŸ”„ App has been reset."
115
+ for folder in ["ash_output", "2D", "3D"]:
116
+ shutil.rmtree(os.path.join(MEDIA_DIR, folder), ignore_errors=True)
117
+ if os.path.exists(os.path.join(MEDIA_DIR, "last_run.txt")):
118
+ os.remove(os.path.join(MEDIA_DIR, "last_run.txt"))
119
+ update_media_tabs()
120
+
121
+ def restore_previous_session():
122
+ try:
123
+ state_file = os.path.join(MEDIA_DIR, "last_run.txt")
124
+ if os.path.exists(state_file):
125
+ with open(state_file) as f:
126
+ zip_path = f.read().strip()
127
+ if os.path.exists(zip_path):
128
+ output_dir = os.path.join("./", "ash_output")
129
+
130
+ animator_obj["3d"] = []
131
+ for fp in sorted(glob.glob(os.path.join(output_dir, "3D", "*.nc"))):
132
+ with xr.open_dataset(fp) as ds:
133
+ animator_obj["3d"].append(ds.load())
134
+
135
+ animator_obj["2d"] = []
136
+ for fp in sorted(glob.glob(os.path.join(output_dir, "horizontal", "*.nc"))):
137
+ with xr.open_dataset(fp) as ds:
138
+ animator_obj["2d"].append(ds.load())
139
+
140
+ status.object = f"πŸ” Restored previous session from {os.path.basename(zip_path)}"
141
+ update_media_tabs()
142
+ except Exception as e:
143
+ logging.exception("Error restoring previous session")
144
+ status.object = f"⚠️ Could not restore previous session: {e}"
145
+
146
+ process_button.on_click(process_zip)
147
+ reset_button.on_click(reset_app)
148
 
149
+ # ---------------- Animator Builders ----------------
150
+ def build_animator_3d():
151
+ ds = animator_obj["3d"]
152
+ attrs = ds[0].attrs
153
+ lons, lats, grid = create_grid(attrs)
154
+ return SimpleNamespace(
155
+ datasets=ds,
156
+ levels=ds[0].altitude.values,
157
+ lons=lons,
158
+ lats=lats,
159
+ lon_grid=grid[0],
160
+ lat_grid=grid[1],
161
+ )
162
+
163
+ def build_animator_2d():
164
+ ds = animator_obj["2d"]
165
+ lat_grid, lon_grid = xr.broadcast(ds[0]["latitude"], ds[0]["longitude"])
166
+ return SimpleNamespace(
167
+ datasets=ds,
168
+ lats=ds[0]["latitude"].values,
169
+ lons=ds[0]["longitude"].values,
170
+ lat_grid=lat_grid.values,
171
+ lon_grid=lon_grid.values,
172
+ )
173
+
174
+ # ---------------- Plot Functions ----------------
175
+ def plot_z_level():
176
+ try:
177
+ animator = build_animator_3d()
178
+ out = os.path.join(MEDIA_DIR, "3D")
179
+ os.makedirs(out, exist_ok=True)
180
+ Plot_3DField_Data(animator, out, cmap_select_3d.value,
181
+ threshold_slider_3d.value, zoom_slider_3d.value,
182
+ fps_slider_3d.value).plot_single_z_level(
183
+ Altitude_slider.value, f"ash_altitude{Altitude_slider.value}km_runTimes.gif")
184
+ update_media_tabs()
185
+ status.object = "βœ… Z-Level animation created."
186
+ except Exception as e:
187
+ logging.exception("Error in plot_z_level")
188
+ status.object = f"❌ Error in Z-Level animation: {e}"
189
+
190
+ def plot_vertical_profile():
191
+ try:
192
+ animator = build_animator_3d()
193
+ out = os.path.join(MEDIA_DIR, "3D")
194
+ os.makedirs(out, exist_ok=True)
195
+ plotter = Plot_3DField_Data(animator, out, cmap_select_3d.value, fps_slider_3d.value,
196
+ threshold_slider_3d.value, zoom_level=zoom_slider_3d.value,
197
+ basemap_type='basemap')
198
+ plotter.plot_vertical_profile_at_time(Altitude_slider.value - 1,
199
+ filename=f"T{Altitude_slider.value - 1}_profile.gif")
200
+ update_media_tabs()
201
+ status.object = "βœ… Vertical profile animation created."
202
+ except Exception as e:
203
+ logging.exception("Error in plot_vertical_profile")
204
+ status.object = f"❌ Error in vertical profile animation: {e}"
205
+
206
+ def animate_all_altitude_profiles():
207
+ try:
208
+ animator = build_animator_3d()
209
+ out = os.path.join(MEDIA_DIR, "3D")
210
+ Plot_3DField_Data(animator, out, cmap_select_3d.value,
211
+ threshold_slider_3d.value, zoom_slider_3d.value).animate_all_altitude_profiles()
212
+ update_media_tabs()
213
+ status.object = "βœ… All altitude profile animations created."
214
+ except Exception as e:
215
+ logging.exception("Error in animate_all_altitude_profiles")
216
+ status.object = f"❌ Error animating all altitude profiles: {e}"
217
+
218
+ def export_jpg_frames():
219
+ try:
220
+ animator = build_animator_3d()
221
+ out = os.path.join(MEDIA_DIR, "3D")
222
+ Plot_3DField_Data(animator, out, cmap_select_3d.value,
223
+ threshold_slider_3d.value, zoom_slider_3d.value).export_frames_as_jpgs(include_metadata=True)
224
+ update_media_tabs()
225
+ status.object = "βœ… JPG frames exported."
226
+ except Exception as e:
227
+ logging.exception("Error exporting JPG frames")
228
+ status.object = f"❌ Error exporting JPG frames: {e}"
229
+
230
+ def plot_2d_field(field):
231
+ try:
232
+ animator = build_animator_2d()
233
+ out = os.path.join(MEDIA_DIR, "2D")
234
+ Plot_Horizontal_Data(animator, out, cmap_select_2d.value, fps_slider_2d.value,
235
+ include_metadata=True, threshold=threshold_slider_2d.value,
236
+ zoom_width_deg=6.0, zoom_height_deg=6.0,
237
+ zoom_level=zoom_slider_2d.value,
238
+ static_frame_export=True).plot_single_field_over_time(field, f"{field}.gif")
239
+ update_media_tabs()
240
+ status.object = f"βœ… 2D field `{field}` animation created."
241
+ except Exception as e:
242
+ logging.exception(f"Error in plot_2d_field: {field}")
243
+ status.object = f"❌ Error in 2D field `{field}` animation: {e}"
244
+
245
+ # ---------------- Layout ----------------
246
+ def human_readable_size(size):
247
+ for unit in ['B', 'KB', 'MB', 'GB']:
248
+ if size < 1024: return f"{size:.1f} {unit}"
249
+ size /= 1024
250
+ return f"{size:.1f} TB"
251
+
252
+ def generate_output_gallery(base_folder):
253
+ grouped = defaultdict(lambda: defaultdict(list))
254
+ for root, _, files in os.walk(os.path.join(MEDIA_DIR, base_folder)):
255
+ for file in files:
256
+ ext = os.path.splitext(file)[1].lower()
257
+ subfolder = os.path.relpath(root, MEDIA_DIR)
258
+ grouped[subfolder][ext].append(os.path.join(root, file))
259
+
260
+ folder_tabs = []
261
+ for subfolder, ext_files in sorted(grouped.items()):
262
+ type_tabs = []
263
+ for ext, paths in sorted(ext_files.items()):
264
+ previews = []
265
+ for path in sorted(paths, key=os.path.getmtime, reverse=True):
266
+ size = human_readable_size(os.path.getsize(path))
267
+ mod = datetime.fromtimestamp(os.path.getmtime(path)).strftime("%Y-%m-%d %H:%M")
268
+ title = f"**{os.path.basename(path)}**\\n_{size}, {mod}_"
269
+ download = pn.widgets.FileDownload(label="⬇", file=path, filename=os.path.basename(path), width=60)
270
+ if ext in [".gif", ".png", ".jpg", ".jpeg"]:
271
+ preview = pn.pane.Image(path, width=320)
272
+ else:
273
+ with open(path, "r", errors="ignore") as f:
274
+ content = f.read(2048)
275
+ preview = pn.pane.PreText(content, width=320)
276
+ card = pn.Card(pn.pane.Markdown(title), preview, pn.Row(download), width=360)
277
+ previews.append(card)
278
+ type_tabs.append((ext.upper(), pn.GridBox(*previews, ncols=2)))
279
+ folder_tabs.append((subfolder, pn.Tabs(*type_tabs)))
280
+ return pn.Tabs(*folder_tabs)
281
+
282
+ def update_media_tabs():
283
+ media_tab_2d.objects[:] = [generate_output_gallery("2D")]
284
+ media_tab_3d.objects[:] = [generate_output_gallery("3D")]
285
+
286
+ media_tab_2d = pn.Column(generate_output_gallery("2D"))
287
+ media_tab_3d = pn.Column(generate_output_gallery("3D"))
288
+
289
+ media_tab = pn.Tabs(
290
+ ("2D Outputs", media_tab_2d),
291
+ ("3D Outputs", media_tab_3d)
292
  )
293
 
294
+ tab3d = pn.Column(
295
+ threshold_slider_3d, zoom_slider_3d, fps_slider_3d, Altitude_slider, cmap_select_3d,
296
+ pn.widgets.Button(name="🎞 Generate animation at selected altitude level", button_type="primary", on_click=lambda e: tab3d.append(plot_z_level())),
297
+ pn.widgets.Button(name="πŸ“ˆ Generate vertical profile animation at time index", button_type="primary", on_click=lambda e: tab3d.append(plot_vertical_profile())),
298
+ pn.widgets.Button(name="πŸ“Š Generate all altitude level animations", button_type="primary", on_click=lambda e: tab3d.append(animate_all_altitude_profiles())),
299
+ pn.widgets.Button(name="πŸ–Ό Export all animation frames as JPG", button_type="primary", on_click=lambda e: tab3d.append(export_jpg_frames())),
300
+ )
301
 
302
+ tab2d = pn.Column(
303
+ threshold_slider_2d, zoom_slider_2d, fps_slider_2d, cmap_select_2d,
304
+ pn.widgets.Button(name="🌫 Animate Air Concentration", button_type="primary", on_click=lambda e: tab2d.append(plot_2d_field("air_concentration"))),
305
+ pn.widgets.Button(name="🌧 Animate Dry Deposition Rate", button_type="primary", on_click=lambda e: tab2d.append(plot_2d_field("dry_deposition_rate"))),
306
+ pn.widgets.Button(name="πŸ’§ Animate Wet Deposition Rate", button_type="primary", on_click=lambda e: tab2d.append(plot_2d_field("wet_deposition_rate"))),
307
+ )
308
 
309
  tabs = pn.Tabs(
310
+ ("🧱 3D Field", tab3d),
311
+ ("🌍 2D Field", tab2d),
312
+ ("πŸ“ Media Viewer", media_tab)
313
  )
314
 
315
+ sidebar = pn.Column("## πŸŒ‹ NAME Ash Visualizer", file_input, process_button, reset_button, download_button, log_link, status)
316
+
317
+ restore_previous_session()
318
+
319
  pn.template.FastListTemplate(
320
+ title="NAME Visualizer Dashboard",
321
+ sidebar=sidebar,
322
+ main=[tabs],
 
 
323
  ).servable()
requirements.txt CHANGED
@@ -1,3 +1,13 @@
1
  panel
2
- pandas
3
- hvplot
 
 
 
 
 
 
 
 
 
 
 
1
  panel
2
+ xarray
3
+ numpy
4
+ matplotlib
5
+ netCDF4
6
+ pillow
7
+ scipy
8
+ cartopy
9
+ contextily
10
+ geopy
11
+ adjustText
12
+ basemap
13
+ imageio