Mahmudm commited on
Commit
b7fd771
Β·
verified Β·
1 Parent(s): e4be2c3

Upload 3 files

Browse files
Files changed (2) hide show
  1. app.py +590 -33
  2. default_model.zip +3 -0
app.py CHANGED
@@ -16,7 +16,9 @@ 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
@@ -69,12 +71,20 @@ def process_zip(event=None):
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
 
@@ -85,6 +95,10 @@ def process_zip(event=None):
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:
@@ -125,7 +139,12 @@ def restore_previous_session():
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"))):
@@ -249,35 +268,99 @@ def human_readable_size(size):
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")]
@@ -291,6 +374,7 @@ media_tab = pn.Tabs(
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())),
@@ -306,14 +390,486 @@ tab2d = pn.Column(
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
 
318
  restore_previous_session()
319
 
@@ -321,4 +877,5 @@ pn.template.FastListTemplate(
321
  title="NAME Visualizer Dashboard",
322
  sidebar=sidebar,
323
  main=[tabs],
324
- ).servable()
 
 
16
 
17
  pn.extension()
18
 
19
+ import tempfile
20
+
21
+ MEDIA_DIR = os.environ.get("NAME_MEDIA_DIR", os.path.join(tempfile.gettempdir(), "name_media"))
22
  os.makedirs(MEDIA_DIR, exist_ok=True)
23
 
24
  # Logging setup
 
71
  status.object = "βœ… ZIP uploaded and saved."
72
  else:
73
  zip_path = os.path.join(MEDIA_DIR, "default_model.zip")
74
+ if not os.path.exists(zip_path):
75
+ zip_path = "default_model.zip" # fallback to local directory
76
  if not os.path.exists(zip_path):
77
  status.object = "❌ No ZIP uploaded and default_model.zip not found."
78
  return
79
  status.object = "πŸ“¦ Using default_model.zip"
80
 
81
+
82
+ try:
83
+ output_dir = os.path.join("./", "ash_output")
84
+ os.makedirs(output_dir, exist_ok=True)
85
+ except PermissionError:
86
+ output_dir = os.path.join(tempfile.gettempdir(), "name_output")
87
+ os.makedirs(output_dir, exist_ok=True)
88
  shutil.rmtree(output_dir, ignore_errors=True)
89
  os.makedirs(output_dir, exist_ok=True)
90
 
 
95
  # animator_obj["3d"] = [xr.open_dataset(fp).load()
96
  # for fp in sorted(glob.glob(os.path.join(output_dir, "3D", "*.nc")))]
97
 
98
+ # animator_obj["3d"] = []
99
+ # for fp in sorted(glob.glob(os.path.join(output_dir, "3D", "*.nc"))):
100
+ # with xr.open_dataset(fp) as ds:
101
+ # animator_obj["3d"].append(ds.load())
102
  animator_obj["3d"] = []
103
  for fp in sorted(glob.glob(os.path.join(output_dir, "3D", "*.nc"))):
104
  with xr.open_dataset(fp) as ds:
 
139
  with open(state_file) as f:
140
  zip_path = f.read().strip()
141
  if os.path.exists(zip_path):
142
+ try:
143
+ output_dir = os.path.join("./", "ash_output")
144
+ os.makedirs(output_dir, exist_ok=True)
145
+ except PermissionError:
146
+ output_dir = os.path.join(tempfile.gettempdir(), "name_output")
147
+ os.makedirs(output_dir, exist_ok=True)
148
 
149
  animator_obj["3d"] = []
150
  for fp in sorted(glob.glob(os.path.join(output_dir, "3D", "*.nc"))):
 
268
  size /= 1024
269
  return f"{size:.1f} TB"
270
 
271
+ # def generate_output_gallery(base_folder):
272
+ # grouped = defaultdict(lambda: defaultdict(list))
273
+ # for root, _, files in os.walk(os.path.join(MEDIA_DIR, base_folder)):
274
+ # for file in files:
275
+ # ext = os.path.splitext(file)[1].lower()
276
+ # subfolder = os.path.relpath(root, MEDIA_DIR)
277
+ # grouped[subfolder][ext].append(os.path.join(root, file))
278
+
279
+ # folder_tabs = []
280
+ # for subfolder, ext_files in sorted(grouped.items()):
281
+ # type_tabs = []
282
+ # for ext, paths in sorted(ext_files.items()):
283
+ # previews = []
284
+ # for path in sorted(paths, key=os.path.getmtime, reverse=True):
285
+ # size = human_readable_size(os.path.getsize(path))
286
+ # mod = datetime.fromtimestamp(os.path.getmtime(path)).strftime("%Y-%m-%d %H:%M")
287
+ # title = f"**{os.path.basename(path)}**\\n_{size}, {mod}_"
288
+ # download = pn.widgets.FileDownload(label="⬇", file=path, filename=os.path.basename(path), width=60)
289
+ # if ext in [".gif", ".png", ".jpg", ".jpeg"]:
290
+ # preview = pn.pane.Image(path, width=320)
291
+ # else:
292
+ # with open(path, "r", errors="ignore") as f:
293
+ # content = f.read(2048)
294
+ # preview = pn.pane.PreText(content, width=320)
295
+ # card = pn.Card(pn.pane.Markdown(title), preview, pn.Row(download), width=360)
296
+ # previews.append(card)
297
+ # type_tabs.append((ext.upper(), pn.GridBox(*previews, ncols=2)))
298
+ # folder_tabs.append((subfolder, pn.Tabs(*type_tabs)))
299
+ # return pn.Tabs(*folder_tabs)
300
+
301
+
302
  def generate_output_gallery(base_folder):
303
+ preview_container = pn.Column(width=640, height=550)
304
+ preview_container.append(pn.pane.Markdown("πŸ‘ˆ Click a thumbnail to preview"))
305
+ folder_cards = []
306
+
307
+ def make_preview(file_path):
308
+ ext = os.path.splitext(file_path)[1].lower()
309
+ title = pn.pane.Markdown(f"### {os.path.basename(file_path)}", width=640)
310
+ download_button = pn.widgets.FileDownload(file=file_path, filename=os.path.basename(file_path),
311
+ label="⬇ Download", button_type="success", width=150)
312
+
313
+ if ext in [".gif", ".png", ".jpg", ".jpeg"]:
314
+ content = pn.pane.Image(file_path, width=640, height=450, sizing_mode="fixed")
315
+ else:
316
+ try:
317
+ with open(file_path, 'r', errors="ignore") as f:
318
+ text = f.read(2048)
319
+ content = pn.pane.PreText(text, width=640, height=450)
320
+ except:
321
+ content = pn.pane.Markdown("*Unable to preview this file.*")
322
+
323
+ return pn.Column(title, content, download_button)
324
+
325
+ grouped = defaultdict(list)
326
  for root, _, files in os.walk(os.path.join(MEDIA_DIR, base_folder)):
327
+ for file in sorted(files):
328
+ full_path = os.path.join(root, file)
329
+ if not os.path.exists(full_path):
330
+ continue
331
+ rel_folder = os.path.relpath(root, os.path.join(MEDIA_DIR, base_folder))
332
+ grouped[rel_folder].append(full_path)
333
+
334
+ for folder, file_paths in sorted(grouped.items()):
335
+ thumbnails = []
336
+ for full_path in file_paths:
337
+ filename = os.path.basename(full_path)
338
+ ext = os.path.splitext(full_path)[1].lower()
339
+
340
+ if ext in [".gif", ".png", ".jpg", ".jpeg"]:
341
+ img = pn.pane.Image(full_path, width=140, height=100)
342
+ else:
343
+ img = pn.pane.Markdown("πŸ“„", width=140, height=100)
344
+
345
+ view_button = pn.widgets.Button(name="πŸ‘", width=40, height=30, button_type="primary")
346
+
347
+ def click_handler(path=full_path):
348
+ def inner_click(event):
349
+ preview_container[:] = [make_preview(path)]
350
+ return inner_click
351
+
352
+ view_button.on_click(click_handler())
353
+
354
+ overlay = pn.Column(pn.Row(pn.Spacer(width=90), view_button), img, width=160)
355
+ label_md = pn.pane.Markdown(f"**{filename}**", width=140, height=35)
356
+ thumb_card = pn.Column(overlay, label_md, width=160)
357
+ thumbnails.append(thumb_card)
358
+
359
+ folder_card = pn.Card(pn.GridBox(*thumbnails, ncols=2), title=f"πŸ“ {folder}", width=400, collapsible=True)
360
+ folder_cards.append(folder_card)
361
+
362
+ folder_scroll = pn.Column(*folder_cards, scroll=True, height=600, width=420)
363
+ return pn.Row(preview_container, pn.Spacer(width=20), folder_scroll)
364
 
365
  def update_media_tabs():
366
  media_tab_2d.objects[:] = [generate_output_gallery("2D")]
 
374
  ("3D Outputs", media_tab_3d)
375
  )
376
 
377
+
378
  tab3d = pn.Column(
379
  threshold_slider_3d, zoom_slider_3d, fps_slider_3d, Altitude_slider, cmap_select_3d,
380
  pn.widgets.Button(name="🎞 Generate animation at selected altitude level", button_type="primary", on_click=lambda e: tab3d.append(plot_z_level())),
 
390
  pn.widgets.Button(name="πŸ’§ Animate Wet Deposition Rate", button_type="primary", on_click=lambda e: tab2d.append(plot_2d_field("wet_deposition_rate"))),
391
  )
392
 
393
+ help_tab = pn.Column(pn.pane.Markdown("""
394
+ ## ❓ How to Use the NAME Ash Visualizer
395
+
396
+ This dashboard allows users to upload and visualize outputs from the NAME ash dispersion model.
397
+
398
+ ### 🧭 Workflow
399
+ 1. **Upload ZIP** containing NetCDF files from the NAME model.
400
+ 2. Use **3D and 2D tabs** to configure and generate animations.
401
+ 3. Use **Media Viewer** to preview and download results.
402
+
403
+ ### 🧳 ZIP Structure
404
+ ```
405
+ ## πŸ—‚ How Uploaded ZIP is Processed
406
+
407
+ ```text
408
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
409
+ β”‚ Uploaded ZIP (.zip) β”‚
410
+ β”‚ (e.g. Taal_273070_20200112_scenario_*.zip)β”‚
411
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
412
+ β”‚
413
+ β–Ό
414
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
415
+ β”‚ Contains: raw .txt outputs β”‚
416
+ β”‚ - AQOutput_3DField_*.txt β”‚
417
+ β”‚ - AQOutput_horizontal_*.txt β”‚
418
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
419
+ β”‚
420
+ β–Ό
421
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
422
+ β”‚ NAMEDataProcessor.batch_process_zip()β”‚
423
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
424
+ β”‚
425
+ β–Ό
426
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
427
+ β”‚ Converts to NetCDF files β”‚
428
+ β”‚ - ash_output/3D/*.nc β”‚
429
+ β”‚ - ash_output/horizontal/*.nc β”‚
430
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
431
+ β”‚
432
+ β–Ό
433
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
434
+ β”‚ View & animate in 3D/2D tabs β”‚
435
+ β”‚ Download results in Media Viewer β”‚
436
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
437
+
438
+ ```
439
+
440
+ ### πŸ“’ Tips
441
+ - Reset the app with πŸ”„ if needed.
442
+ - View logs if an error occurs.
443
+ - Outputs are temporary per session.
444
+ """, sizing_mode="stretch_width", width=800))
445
+
446
  tabs = pn.Tabs(
447
  ("🧱 3D Field", tab3d),
448
  ("🌍 2D Field", tab2d),
449
+ ("πŸ“ Media Viewer", media_tab),
450
+ ("❓ Help", help_tab)
451
+ )
452
+
453
+ sidebar = pn.Column(
454
+ pn.pane.Markdown("## πŸŒ‹ NAME Ash Visualizer", sizing_mode="stretch_width"),
455
+ pn.Card(pn.Column(file_input, process_button, reset_button, sizing_mode="stretch_width"),
456
+ title="πŸ“‚ File Upload & Processing", collapsible=True, sizing_mode="stretch_width"),
457
+ pn.Card(pn.Column(download_button, log_link, sizing_mode="stretch_width"),
458
+ title="πŸ“ Downloads & Logs", collapsible=True, sizing_mode="stretch_width"),
459
+ pn.Card(status, title="πŸ“’ Status", collapsible=True, sizing_mode="stretch_width"),
460
+ sizing_mode="stretch_width", width=300
461
+ )
462
+
463
+ restore_previous_session()
464
+
465
+ pn.template.FastListTemplate(
466
+ title="NAME Visualizer Dashboard",
467
+ sidebar=sidebar,
468
+ main=[tabs],
469
+ ).servable()
470
+
471
+ '''
472
+ import os
473
+ import glob
474
+ import shutil
475
+ import io
476
+ import logging
477
+ import panel as pn
478
+ import xarray as xr
479
+ import numpy as np
480
+ from datetime import datetime
481
+ from types import SimpleNamespace
482
+ from collections import defaultdict
483
+ from ash_animator.converter import NAMEDataProcessor
484
+ from ash_animator.plot_3dfield_data import Plot_3DField_Data
485
+ from ash_animator.plot_horizontal_data import Plot_Horizontal_Data
486
+ from ash_animator import create_grid
487
+ import tempfile
488
+
489
+ pn.extension()
490
+
491
+ MEDIA_DIR = os.environ.get("NAME_MEDIA_DIR", os.path.join(tempfile.gettempdir(), "name_media"))
492
+ os.makedirs(MEDIA_DIR, exist_ok=True)
493
+
494
+ LOG_FILE = os.path.join(MEDIA_DIR, "app_errors.log")
495
+ logging.basicConfig(filename=LOG_FILE, level=logging.ERROR, format="%(asctime)s - %(levelname)s - %(message)s")
496
+
497
+ animator_obj = {}
498
+
499
+ file_input = pn.widgets.FileInput(accept=".zip")
500
+ process_button = pn.widgets.Button(name="πŸ“¦ Process ZIP", button_type="primary")
501
+ reset_button = pn.widgets.Button(name="πŸ”„ Reset App", button_type="danger")
502
+ status = pn.pane.Markdown("### Upload a NAME Model ZIP to begin")
503
+
504
+ download_button = pn.widgets.FileDownload(
505
+ label="⬇️ Download All Exports", filename="all_exports.zip", button_type="success",
506
+ callback=lambda: io.BytesIO(open(shutil.make_archive(os.path.join(MEDIA_DIR, "all_exports").replace(".zip", ""), "zip", MEDIA_DIR), 'rb').read())
507
  )
508
 
509
+ log_link = pn.widgets.FileDownload(label="πŸͺ΅ View Error Log", file=LOG_FILE, filename="app_errors.log", button_type="warning")
510
+
511
+ threshold_slider_3d = pn.widgets.FloatSlider(name='3D Threshold', start=0.0, end=1.0, step=0.05, value=0.1)
512
+ zoom_slider_3d = pn.widgets.IntSlider(name='3D Zoom Level', start=1, end=20, value=19)
513
+ cmap_select_3d = pn.widgets.Select(name='3D Colormap', options=["rainbow", "viridis", "plasma"])
514
+ fps_slider_3d = pn.widgets.IntSlider(name='3D FPS', start=1, end=10, value=2)
515
+ Altitude_slider = pn.widgets.IntSlider(name='Define Ash Altitude', start=1, end=15, value=1)
516
+
517
+ threshold_slider_2d = pn.widgets.FloatSlider(name='2D Threshold', start=0.0, end=1.0, step=0.01, value=0.005)
518
+ zoom_slider_2d = pn.widgets.IntSlider(name='2D Zoom Level', start=1, end=20, value=19)
519
+ fps_slider_2d = pn.widgets.IntSlider(name='2D FPS', start=1, end=10, value=2)
520
+ cmap_select_2d = pn.widgets.Select(name='2D Colormap', options=["rainbow", "viridis", "plasma"])
521
+
522
+ def process_zip(event=None):
523
+ if file_input.value:
524
+ zip_path = os.path.join(MEDIA_DIR, file_input.filename)
525
+ with open(zip_path, "wb") as f:
526
+ f.write(file_input.value)
527
+ status.object = "βœ… ZIP uploaded and saved."
528
+ else:
529
+ zip_path = os.path.join(MEDIA_DIR, "default_model.zip")
530
+ if not os.path.exists(zip_path):
531
+ zip_path = "default_model.zip"
532
+ if not os.path.exists(zip_path):
533
+ status.object = "❌ No ZIP uploaded and default_model.zip not found."
534
+ return
535
+ status.object = "πŸ“¦ Using default_model.zip"
536
+
537
+ try:
538
+ output_dir = os.path.join("./", "ash_output")
539
+ os.makedirs(output_dir, exist_ok=True)
540
+ except PermissionError:
541
+ output_dir = os.path.join(tempfile.gettempdir(), "name_output")
542
+ os.makedirs(output_dir, exist_ok=True)
543
+ shutil.rmtree(output_dir, ignore_errors=True)
544
+ os.makedirs(output_dir, exist_ok=True)
545
+
546
+ try:
547
+ processor = NAMEDataProcessor(output_root=output_dir)
548
+ processor.batch_process_zip(zip_path)
549
+
550
+ animator_obj["3d"] = [xr.open_dataset(fp).load() for fp in sorted(glob.glob(os.path.join(output_dir, "3D", "*.nc")))]
551
+ animator_obj["2d"] = [xr.open_dataset(fp).load() for fp in sorted(glob.glob(os.path.join(output_dir, "horizontal", "*.nc")))]
552
+
553
+ with open(os.path.join(MEDIA_DIR, "last_run.txt"), "w") as f:
554
+ f.write(zip_path)
555
+
556
+ status.object += f" | βœ… Loaded 3D: {len(animator_obj['3d'])} & 2D: {len(animator_obj['2d'])}"
557
+ update_media_tabs()
558
+ except Exception as e:
559
+ logging.exception("Error during ZIP processing")
560
+ status.object = f"❌ Processing failed: {e}"
561
 
562
+ def reset_app(event=None):
563
+ animator_obj.clear()
564
+ file_input.value = None
565
+ status.object = "πŸ”„ App has been reset."
566
+ for folder in ["ash_output", "2D", "3D"]:
567
+ shutil.rmtree(os.path.join(MEDIA_DIR, folder), ignore_errors=True)
568
+ if os.path.exists(os.path.join(MEDIA_DIR, "last_run.txt")):
569
+ os.remove(os.path.join(MEDIA_DIR, "last_run.txt"))
570
+ update_media_tabs()
571
+
572
+ def restore_previous_session():
573
+ try:
574
+ state_file = os.path.join(MEDIA_DIR, "last_run.txt")
575
+ if os.path.exists(state_file):
576
+ with open(state_file) as f:
577
+ zip_path = f.read().strip()
578
+ if os.path.exists(zip_path):
579
+ output_dir = os.path.join("./", "ash_output")
580
+ os.makedirs(output_dir, exist_ok=True)
581
+ animator_obj["3d"] = [xr.open_dataset(fp).load() for fp in sorted(glob.glob(os.path.join(output_dir, "3D", "*.nc")))]
582
+ animator_obj["2d"] = [xr.open_dataset(fp).load() for fp in sorted(glob.glob(os.path.join(output_dir, "horizontal", "*.nc")))]
583
+ status.object = f"πŸ” Restored previous session from {os.path.basename(zip_path)}"
584
+ update_media_tabs()
585
+ except Exception as e:
586
+ logging.exception("Error restoring previous session")
587
+ status.object = f"⚠️ Could not restore previous session: {e}"
588
+
589
+ process_button.on_click(process_zip)
590
+ reset_button.on_click(reset_app)
591
+
592
+ def build_animator_3d():
593
+ ds = animator_obj["3d"]
594
+ attrs = ds[0].attrs
595
+ lons, lats, grid = create_grid(attrs)
596
+ return SimpleNamespace(datasets=ds, levels=ds[0].altitude.values, lons=lons, lats=lats,
597
+ lon_grid=grid[0], lat_grid=grid[1])
598
+
599
+ def build_animator_2d():
600
+ ds = animator_obj["2d"]
601
+ lat_grid, lon_grid = xr.broadcast(ds[0]["latitude"], ds[0]["longitude"])
602
+ return SimpleNamespace(datasets=ds, lats=ds[0]["latitude"].values,
603
+ lons=ds[0]["longitude"].values,
604
+ lat_grid=lat_grid.values, lon_grid=lon_grid.values)
605
+
606
+ def plot_z_level():
607
+ try:
608
+ animator = build_animator_3d()
609
+ out = os.path.join(MEDIA_DIR, "3D")
610
+ os.makedirs(out, exist_ok=True)
611
+ Plot_3DField_Data(animator, out, cmap_select_3d.value, threshold_slider_3d.value,
612
+ zoom_slider_3d.value, fps_slider_3d.value).plot_single_z_level(
613
+ Altitude_slider.value, f"ash_altitude{Altitude_slider.value}km_runTimes.gif")
614
+ update_media_tabs()
615
+ status.object = "βœ… Z-Level animation created."
616
+ except Exception as e:
617
+ logging.exception("Error in plot_z_level")
618
+ status.object = f"❌ Error in Z-Level animation: {e}"
619
+
620
+ def plot_vertical_profile():
621
+ try:
622
+ animator = build_animator_3d()
623
+ out = os.path.join(MEDIA_DIR, "3D")
624
+ os.makedirs(out, exist_ok=True)
625
+ Plot_3DField_Data(animator, out, cmap_select_3d.value, fps_slider_3d.value,
626
+ threshold_slider_3d.value, zoom_level=zoom_slider_3d.value).plot_vertical_profile_at_time(
627
+ Altitude_slider.value - 1, f"T{Altitude_slider.value - 1}_profile.gif")
628
+ update_media_tabs()
629
+ status.object = "βœ… Vertical profile animation created."
630
+ except Exception as e:
631
+ logging.exception("Error in plot_vertical_profile")
632
+ status.object = f"❌ Error in vertical profile animation: {e}"
633
+
634
+ def animate_all_altitude_profiles():
635
+ try:
636
+ animator = build_animator_3d()
637
+ out = os.path.join(MEDIA_DIR, "3D")
638
+ Plot_3DField_Data(animator, out, cmap_select_3d.value, threshold_slider_3d.value,
639
+ zoom_slider_3d.value).animate_all_altitude_profiles()
640
+ update_media_tabs()
641
+ status.object = "βœ… All altitude profile animations created."
642
+ except Exception as e:
643
+ logging.exception("Error in animate_all_altitude_profiles")
644
+ status.object = f"❌ Error animating all altitude profiles: {e}"
645
+
646
+ def export_jpg_frames():
647
+ try:
648
+ animator = build_animator_3d()
649
+ out = os.path.join(MEDIA_DIR, "3D")
650
+ Plot_3DField_Data(animator, out, cmap_select_3d.value, threshold_slider_3d.value,
651
+ zoom_slider_3d.value).export_frames_as_jpgs(include_metadata=True)
652
+ update_media_tabs()
653
+ status.object = "βœ… JPG frames exported."
654
+ except Exception as e:
655
+ logging.exception("Error exporting JPG frames")
656
+ status.object = f"❌ Error exporting JPG frames: {e}"
657
+
658
+ def plot_2d_field(field):
659
+ try:
660
+ animator = build_animator_2d()
661
+ out = os.path.join(MEDIA_DIR, "2D")
662
+ Plot_Horizontal_Data(animator, out, cmap_select_2d.value, fps_slider_2d.value,
663
+ include_metadata=True, threshold=threshold_slider_2d.value,
664
+ zoom_width_deg=6.0, zoom_height_deg=6.0,
665
+ zoom_level=zoom_slider_2d.value,
666
+ static_frame_export=True).plot_single_field_over_time(field, f"{field}.gif")
667
+ update_media_tabs()
668
+ status.object = f"βœ… 2D field `{field}` animation created."
669
+ except Exception as e:
670
+ logging.exception(f"Error in plot_2d_field: {field}")
671
+ status.object = f"❌ Error in 2D field `{field}` animation: {e}"
672
+
673
+ def human_readable_size(size):
674
+ for unit in ['B', 'KB', 'MB', 'GB']:
675
+ if size < 1024: return f"{size:.1f} {unit}"
676
+ size /= 1024
677
+ return f"{size:.1f} TB"
678
+
679
+ # def generate_output_gallery(base_folder):
680
+ # grouped = defaultdict(lambda: defaultdict(list))
681
+ # for root, _, files in os.walk(os.path.join(MEDIA_DIR, base_folder)):
682
+ # for file in files:
683
+ # ext = os.path.splitext(file)[1].lower()
684
+ # subfolder = os.path.relpath(root, MEDIA_DIR)
685
+ # grouped[subfolder][ext].append(os.path.join(root, file))
686
+
687
+ # folder_panels = []
688
+ # for subfolder, ext_files in sorted(grouped.items()):
689
+ # section = []
690
+ # for ext, paths in sorted(ext_files.items()):
691
+ # previews = []
692
+ # for path in sorted(paths, key=os.path.getmtime, reverse=True):
693
+ # size = human_readable_size(os.path.getsize(path))
694
+ # mod = datetime.fromtimestamp(os.path.getmtime(path)).strftime("%Y-%m-%d %H:%M")
695
+ # title = f"**{os.path.basename(path)}**\n_{size}, {mod}_"
696
+ # download = pn.widgets.FileDownload(label="⬇", file=path, filename=os.path.basename(path), width=60)
697
+ # if ext in [".gif", ".png", ".jpg", ".jpeg"]:
698
+ # preview = pn.pane.Image(path, width=320)
699
+ # else:
700
+ # with open(path, "r", errors="ignore") as f:
701
+ # content = f.read(2048)
702
+ # preview = pn.pane.PreText(content, width=320)
703
+ # card = pn.Card(pn.pane.Markdown(title), preview, pn.Row(download), width=360)
704
+ # previews.append(card)
705
+ # section.append(pn.Column(f"### {ext.upper()}", pn.GridBox(*previews, ncols=2)))
706
+ # folder_section = pn.Card(f"πŸ“ {subfolder}", *section, collapsible=True, width_policy="max")
707
+ # folder_panels.append(folder_section)
708
+
709
+ # return pn.Column(*folder_panels, height=600, scroll=True, sizing_mode='stretch_width', styles={'overflow': 'auto'})
710
+
711
+
712
+
713
+ def generate_output_gallery(base_folder):
714
+ preview_container = pn.Column(width=640, height=550)
715
+ preview_container.append(pn.pane.Markdown("πŸ‘ˆ Click a thumbnail to preview"))
716
+ folder_cards = []
717
+
718
+ def make_preview(file_path):
719
+ ext = os.path.splitext(file_path)[1].lower()
720
+ title = pn.pane.Markdown(f"### {os.path.basename(file_path)}", width=640)
721
+ download_button = pn.widgets.FileDownload(file=file_path, filename=os.path.basename(file_path),
722
+ label="⬇ Download", button_type="success", width=150)
723
+
724
+ if ext in [".gif", ".png", ".jpg", ".jpeg"]:
725
+ content = pn.pane.Image(file_path, width=640, height=450, sizing_mode="fixed")
726
+ else:
727
+ try:
728
+ with open(file_path, 'r', errors="ignore") as f:
729
+ text = f.read(2048)
730
+ content = pn.pane.PreText(text, width=640, height=450)
731
+ except:
732
+ content = pn.pane.Markdown("*Unable to preview this file.*")
733
+
734
+ return pn.Column(title, content, download_button)
735
+
736
+ grouped = defaultdict(list)
737
+ for root, _, files in os.walk(os.path.join(MEDIA_DIR, base_folder)):
738
+ for file in sorted(files):
739
+ full_path = os.path.join(root, file)
740
+ if not os.path.exists(full_path):
741
+ continue
742
+ rel_folder = os.path.relpath(root, os.path.join(MEDIA_DIR, base_folder))
743
+ grouped[rel_folder].append(full_path)
744
+
745
+ for folder, file_paths in sorted(grouped.items()):
746
+ thumbnails = []
747
+ for full_path in file_paths:
748
+ filename = os.path.basename(full_path)
749
+ ext = os.path.splitext(full_path)[1].lower()
750
+
751
+ if ext in [".gif", ".png", ".jpg", ".jpeg"]:
752
+ img = pn.pane.Image(full_path, width=140, height=100)
753
+ else:
754
+ img = pn.pane.Markdown("πŸ“„", width=140, height=100)
755
+
756
+ view_button = pn.widgets.Button(name="πŸ‘", width=40, height=30, button_type="primary")
757
+
758
+ def click_handler(path=full_path):
759
+ def inner_click(event):
760
+ preview_container[:] = [make_preview(path)]
761
+ return inner_click
762
+
763
+ view_button.on_click(click_handler())
764
+
765
+ overlay = pn.Column(pn.Row(pn.Spacer(width=90), view_button), img, width=160)
766
+ label_md = pn.pane.Markdown(f"**{filename}**", width=140, height=35)
767
+ thumb_card = pn.Column(overlay, label_md, width=160)
768
+ thumbnails.append(thumb_card)
769
+
770
+ folder_card = pn.Card(pn.GridBox(*thumbnails, ncols=2), title=f"πŸ“ {folder}", width=400, collapsible=True)
771
+ folder_cards.append(folder_card)
772
+
773
+ folder_scroll = pn.Column(*folder_cards, scroll=True, height=600, width=420)
774
+ return pn.Row(preview_container, pn.Spacer(width=20), folder_scroll)
775
+
776
+ def update_media_tabs():
777
+ media_tab_2d.objects[:] = [generate_output_gallery("2D")]
778
+ media_tab_3d.objects[:] = [generate_output_gallery("3D")]
779
+
780
+ media_tab_2d = pn.Column(generate_output_gallery("2D"))
781
+ media_tab_3d = pn.Column(generate_output_gallery("3D"))
782
+
783
+ media_tab = pn.Tabs(
784
+ ("πŸ–Ό 2D Output Gallery", media_tab_2d),
785
+ ("πŸ–Ό 3D Output Gallery", media_tab_3d),
786
+ dynamic=True
787
+ )
788
+
789
+ tab3d = pn.Column(
790
+ threshold_slider_3d, zoom_slider_3d, fps_slider_3d, Altitude_slider, cmap_select_3d,
791
+ pn.widgets.Button(name="🎞 Generate animation at selected altitude level", button_type="primary", on_click=lambda e: tab3d.append(plot_z_level())),
792
+ pn.widgets.Button(name="πŸ“ˆ Generate vertical profile animation at time index", button_type="primary", on_click=lambda e: tab3d.append(plot_vertical_profile())),
793
+ pn.widgets.Button(name="πŸ“Š Generate all altitude level animations", button_type="primary", on_click=lambda e: tab3d.append(animate_all_altitude_profiles())),
794
+ pn.widgets.Button(name="πŸ–Ό Export all animation frames as JPG", button_type="primary", on_click=lambda e: tab3d.append(export_jpg_frames())),
795
+ )
796
+
797
+ tab2d = pn.Column(
798
+ threshold_slider_2d, zoom_slider_2d, fps_slider_2d, cmap_select_2d,
799
+ pn.widgets.Button(name="🌫 Animate Air Concentration", button_type="primary", on_click=lambda e: tab2d.append(plot_2d_field("air_concentration"))),
800
+ pn.widgets.Button(name="🌧 Animate Dry Deposition Rate", button_type="primary", on_click=lambda e: tab2d.append(plot_2d_field("dry_deposition_rate"))),
801
+ pn.widgets.Button(name="πŸ’§ Animate Wet Deposition Rate", button_type="primary", on_click=lambda e: tab2d.append(plot_2d_field("wet_deposition_rate"))),
802
+ )
803
+
804
+ help_tab = pn.Column(pn.pane.Markdown("""
805
+ ## ❓ How to Use the NAME Ash Visualizer
806
+
807
+ This dashboard allows users to upload and visualize outputs from the NAME ash dispersion model.
808
+
809
+ ### 🧭 Workflow
810
+ 1. **Upload ZIP** containing NetCDF files from the NAME model.
811
+ 2. Use **3D and 2D tabs** to configure and generate animations.
812
+ 3. Use **Media Viewer** to preview and download results.
813
+
814
+ ### 🧳 ZIP Structure
815
+ ```
816
+ ## πŸ—‚ How Uploaded ZIP is Processed
817
+
818
+ ```text
819
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
820
+ β”‚ Uploaded ZIP (.zip) β”‚
821
+ β”‚ (e.g. Taal_273070_20200112_scenario_*.zip)β”‚
822
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
823
+ β”‚
824
+ β–Ό
825
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
826
+ β”‚ Contains: raw .txt outputs β”‚
827
+ β”‚ - AQOutput_3DField_*.txt β”‚
828
+ β”‚ - AQOutput_horizontal_*.txt β”‚
829
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
830
+ β”‚
831
+ β–Ό
832
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€οΏ½οΏ½οΏ½β”€β”€β”€β”€β”€β”€β”€β”€β”
833
+ β”‚ NAMEDataProcessor.batch_process_zip()β”‚
834
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
835
+ β”‚
836
+ β–Ό
837
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
838
+ β”‚ Converts to NetCDF files β”‚
839
+ β”‚ - ash_output/3D/*.nc β”‚
840
+ β”‚ - ash_output/horizontal/*.nc β”‚
841
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
842
+ β”‚
843
+ β–Ό
844
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
845
+ β”‚ View & animate in 3D/2D tabs β”‚
846
+ β”‚ Download results in Media Viewer β”‚
847
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
848
+
849
+ ```
850
+
851
+ ### πŸ“’ Tips
852
+ - Reset the app with πŸ”„ if needed.
853
+ - View logs if an error occurs.
854
+ - Outputs are temporary per session.
855
+ """, sizing_mode="stretch_width", width=800))
856
+
857
+ tabs = pn.Tabs(
858
+ ("🧱 3D Field", tab3d),
859
+ ("🌍 2D Field", tab2d),
860
+ ("πŸ“ Media Viewer", media_tab),
861
+ ("❓ Help", help_tab)
862
+ )
863
+
864
+ sidebar = pn.Column(
865
+ pn.pane.Markdown("## πŸŒ‹ NAME Ash Visualizer", sizing_mode="stretch_width"),
866
+ pn.Card(pn.Column(file_input, process_button, reset_button, sizing_mode="stretch_width"),
867
+ title="πŸ“‚ File Upload & Processing", collapsible=True, sizing_mode="stretch_width"),
868
+ pn.Card(pn.Column(download_button, log_link, sizing_mode="stretch_width"),
869
+ title="πŸ“ Downloads & Logs", collapsible=True, sizing_mode="stretch_width"),
870
+ pn.Card(status, title="πŸ“’ Status", collapsible=True, sizing_mode="stretch_width"),
871
+ sizing_mode="stretch_width", width=300
872
+ )
873
 
874
  restore_previous_session()
875
 
 
877
  title="NAME Visualizer Dashboard",
878
  sidebar=sidebar,
879
  main=[tabs],
880
+ ).servable()
881
+ '''
default_model.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9bb340a75132c3008a149557ff85f8bc05b4a46e70eee027503e30b9573fdd39
3
+ size 181349