#!/usr/bin/env python
# coding: utf-8
# # COMPASS Model Diagnostics 2: Electric Bogaloo
#
# A number of plots are provided from ADF. The full output from the stand-alone ADF configuration is in the link below.
#
#
# Note that in standalone format (eg, CUPiD run not through CESM workflow), ADF is currently run by users via the following process:
# 1) Install ADF and activate cupid-analysis
# 2) Use the `CUPiD/helper_scripts/generate_adf_config_file.py` script to generate an ADF config file based on a CUPiD configuration file.
# * `cd CUPiD/examples/external_diag_packages`
# * `../../helper_scripts/generate_adf_config_file.py --cupid-config-loc . --adf-template ../../externals/ADF/config_amwg_default_plots.yaml --out-file ADF_config.yaml`
# 3) Run ADF with the newly created configuration file.
# * `../../externals/ADF/run_adf_diag ADF_config.yaml`
# In[1]:
adf_root = "."
case_name = None
base_case_name = None
start_date = ""
end_date = ""
base_start_date = None
base_end_date = None
key_plots = None
compare_obs = False
var_list = ["T"]
compass_root = "."
runs_dict = {}
# adf_root will be external_diag_packages/computed_notebooks/ADF/
# In[2]:
# Parameters
case_name = "b.e23_alpha17f.BLT1850.ne30_t232.092"
base_case_name = "b.e30_beta02.BLT1850.ne30_t232.104"
case_nickname = "BLT1850_92"
base_case_nickname = "BLT1850_104"
CESM_output_dir = "/glade/campaign/cesm/development/cross-wg/diagnostic_framework/CESM_output_for_testing"
start_date = "0001-01-01"
end_date = "0021-01-01"
climo_start_date = "0001-01-01"
climo_end_date = "0021-01-01"
base_start_date = "0001-01-01"
base_end_date = "0045-01-01"
base_climo_start_date = "0001-01-01"
base_climo_end_date = "0021-01-01"
obs_data_dir = (
"/glade/campaign/cesm/development/cross-wg/diagnostic_framework/CUPiD_obs_data"
)
ts_dir = None
lc_kwargs = {"threads_per_worker": 1}
serial = False
compass_root = "/glade/work/richling/ADF/ADF_dev/Justin_ADF_2/ADF/adf_try_plots/f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull/T/ERA5/RF13/"
runs_dict = {
"f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull": [
"f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_tau12h.001",
"f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_tau24h.001",
],
"f.e30_cam6_4_120.FHISTC_LTso.ne30pg3_ne30pg3_mg17.SOCRATES_nudgeUVTfull_withCOSP": [
"f.e30_cam6_4_120.FHISTC_LTso.ne30pg3_ne30pg3_mg17.SOCRATES_nudgeUVTfull_withCOSP_tau6h.001",
"f.e30_cam6_4_120.FHISTC_LTso.ne30pg3_ne30pg3_mg17.SOCRATES_nudgeUVTfull_withCOSP_tau12h.001",
],
}
subset_kwargs = {}
product = "/glade/derecho/scratch/richling/compass-cupid/CUPiD/examples/external_diag_packages/computed_notebooks//atm/COMPASS_model_diags_2.ipynb"
# In[3]:
runs_dict_tmp = {}
if isinstance(runs_dict, list):
for run in runs_dict:
runs_dict_tmp[run] = [run]
runs_dict = runs_dict_tmp
# ## Key Metrics from COMPASS Model Diagnostics (CMD)
#
# Some important things to look at from CMD include a slap to the face and then a hug:
# for path_to_key_plot in key_plots:
# full_path = os.path.join(adf_root, path_to_key_plot)
# if os.path.isfile(full_path):
# display(Image(full_pbath))
# In[4]:
def slider(case_name, image_dir, ldrop=None, settings_list=None, rdrop=None):
import os
import re
from glob import glob
from PIL import Image
from io import BytesIO
import base64
from IPython.display import display, HTML
import ipywidgets as widgets
from ipywidgets import Layout
tooltip_dict = {
"Nudge_Uprof": """Selectively apply nudging to U:
OFF = Switch off nudging
ON = Apply nudging everywhere
WINDOW = Apply window function to
nudging tendencies.
0 = OFF
1 = ON
2 = WINDOW
""",
#"nudge_uprof": "Profile of zonal wind nudging",
"Nudge_Ucoef": """Selectively adjust the nudging strength applied to U. (normalized) [0.,1.]
""",
"Nudge_Hwin_lat0": """Specify the horizontal center of the
window (lat0) in degrees. [-90., +90.]""",
"Nudge_Hwin_lon0":"""Specify the horizontal center of the
window (lon0) in degrees. [ 0. , 360.]""",
"Nudge_Hwin_latWidth": """Specify the lat width of the
horizontal window in degrees.
Setting a width to a large value
(e.g. 999.) renders the window
constant in that direction.
>0""",
"Nudge_Hwin_lonWidth": """Specify the lon width of the
horizontal window in degrees.
Setting a width to a large value
(e.g. 999.) renders the window
constant in that direction.
>0""",
"Nudge_Vwin_Hindex": "Vertical window high index",
"Nudge_Vwin_Lindex": "Vertical window low index",
"Nudge_Vwin_Hdelta": "Vertical window high delta",
"Nudge_Vwin_Ldelta": "Vertical window low delta",
"Nudge_Vwin_Invert":"""A logical flag used to invert the
horizontal window function to get its
compliment.
True/False""",
"Nudge_Hwin_latDelta": "Horizontal lat delta",
"Nudge_Hwin_lonDelta": "Horizontal lon delta",
"Nudge_Hwin_Invert":"""A logical flag used to invert the
horizontal window function to get its
compliment.
(e.g. to nudge outside a given window)
True/False""",
"Nudge_TimeScale_Opt":"""Select the timescale for the relaxation:
WEAK = Constant time scale based in
the time interval of Target values.
STRONG = Variable timescale which
gets stronger near each Target time.
0 = WEAK
1 = STRONG
""",
"Nudge_Force_Opt":"""Select the form of the Target values:
NEXT = Target at next future time
LINEAR = Linearly interpolate Target
values to current model time.
0 = NEXT
1 = LINEAR
"""
}
link_dict = {
"Nudge_Uprof": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html?highlight=nudge#namelist-values",
"Nudge_Ucoef": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html?highlight=nudge#namelist-values",
"Nudge_TimeScale_Opt":"www.google.com",
#"nudge_timescale_opt":"www.google.com"
}
if not image_dir:
print("No image directory :/")
return
# --- Detect levels ---
lev_pattern = re.compile(r'(\d+)hPa\.png$')
all_files = os.listdir(image_dir)
levels = sorted({lev_pattern.search(f).group(1) for f in all_files if lev_pattern.search(f)})
if not levels:
print("No level-specific image files found.")
return
# Grab all PNG files
files = glob(os.path.join(image_dir, "*.png"))
# Regex to extract timestamp after hPa_ and before .png
timestamp_pattern = re.compile(r"hPa_(\d{4}_\d{2}_\d{2}_\d{2}:\d{2})\.png$")
timestamps = []
for f in all_files:
base = os.path.basename(f)
m = timestamp_pattern.search(base)
if m:
timestamps.append(m.group(1))
# Optional: sort by timestamp
timestamps = sorted(timestamps)
print(timestamps)
# Widgets
level_dropdown = widgets.Dropdown(options=levels, value=levels[0], description="Level (hPa)")
slider = widgets.IntSlider(min=0, max=0, step=1, description='Time Step')
#time_label = widgets.Label(value=str(timestamps[0]))
#def update_label(change):
# time_label.value = timestamps[slider.value].strftime("%Y-%m-%d %H:%M")
html_widget = widgets.HTML()
def get_images_for_level(level):
level = str(level)
level_files = sorted([
os.path.join(image_dir, f) for f in os.listdir(image_dir)
if f.endswith(f'{level}hPa.png')
])
timestamps = [
'-'.join(os.path.basename(f).split('_')[1:4]) + ' ' + os.path.basename(f).split('_')[4]
for f in level_files
]
return level_files, timestamps
image_files, timestamps = get_images_for_level(levels[0])
print(timestamps)
#image_files = get_images_for_level(levels[0])
slider.max = max(0, len(image_files) - 1)
def image_to_html(path):
img = Image.open(path).resize((1000, 300))
buffer = BytesIO()
img.save(buffer, format='PNG')
b64 = base64.b64encode(buffer.getvalue()).decode()
return f""
def update_display(change=None):
if not image_files:
html_widget.value = "No images found for this level."
return
html_img = image_to_html(image_files[slider.value])
html_ts = f"
for search
processed_lines.append(line)
html_output = "
".join(processed_lines)
with settings_output:
# Use + HTML for easy copy/search
display(HTML(f"{html_output}"))
elif settings_dropdown.value == f"Subset {fname}":
with settings_output:
display(HTML(f"{subset_text}"))
elif settings_dropdown.value == f"Full {fname}":
with settings_output:
display(HTML(f"{full_text}"))
"""
def update_settings(change):
settings_output.clear_output()
if settings_dropdown.value == "Nudge Params":
processed_lines = []
for line in nudge_text.splitlines():
#for key in set(link_dict.keys()).union(tooltip_dict.keys()): #use as an intersection of both dict kyes
for key in tooltip_dict.keys():
#match = next((k for k in tooltip_dict if k.upper() == key.upper()), None)
#if k.upper() == key
if key.lower() in line:
title = tooltip_dict.get(key, "")
href = link_dict.get(key, "")
if href:
replacement = f'{key.lower()}'
else:
replacement = f'{key.lower()}'
line = line.replace(key.lower(), replacement)
processed_lines.append(line)
html_output = "
".join(processed_lines)
with settings_output:
display(HTML(html_output))
elif settings_dropdown.value == f"Subset {fname}":
with settings_output:
display(HTML("" + subset_text + "
"))
elif settings_dropdown.value == f"Full {fname}":
with settings_output:
display(HTML("" + full_text + "
"))
"""
def update_status(change):
with status_output:
status_output.clear_output()
if status_dropdown.value == "CaseStatus":
print(status_text)
slider.observe(update_display, names='value')
settings_dropdown.observe(update_settings, names='value')
status_dropdown.observe(update_status, names='value')
level_dropdown.observe(on_level_change, names='value')
# Layout
settings_output.layout = Layout(margin='0 20px 0 0')
status_output.layout = Layout(margin='0 0 0 20px')
output_row = widgets.HBox([settings_output, status_output])
dropdown_row = widgets.HBox([settings_dropdown, status_dropdown], layout=Layout(margin='10px 0 10px 0'))
control_row = widgets.HBox([level_dropdown, slider])
title = widgets.HTML(
value=f"{case_name}
"
)
ui = widgets.VBox([title, control_row, html_widget, dropdown_row, output_row])
#display(ui)
#update_display()
update_display()
return ui
# In[5]:
import ipywidgets as widgets
from IPython.display import display
from pathlib import Path
# ----------------------------------------------------------
# Define your case *families* and runs
# ----------------------------------------------------------
case_groups = {
"f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull": [
"f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_tau12h.001",
"f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_tau24h.001",
],
"f.e30_cam6_4_120.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_withCOSP": [
"f.e30_cam6_4_120.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_withCOSP_tau6h.001",
#"f.e30_cam6_4_120.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_withCOSP_tau48h.001",
]
}
# ----------------------------------------------------------
# Create nested tabs
# ----------------------------------------------------------
top_tabs = widgets.Tab()
top_level_children = []
top_titles = []
for var in var_list:
plot_type = "T"
#file_path = Path(f"/glade/derecho/scratch/islas/{caze}/run/atm_in")
for case_family, run_list in case_groups.items():
# Create sub-tab for each family
sub_tab = widgets.Tab()
sub_children = []
sub_titles = []
for run in run_list:
# Replace this with: widget = slider(...)
#widget = fake_slider(run)
#caze = "f.e30_cam6_4_120.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_withCOSP_tau6h.001"
file_path = Path(f"/glade/derecho/scratch/islas/{run}/run/atm_in")
widget = slider(run,
#f"plots/{run}/{plot_type}/ERA5/RF13/",
compass_root,
f"{str(file_path)}",
#settings_list=["fincl","mfilt","nhtfrq","ncdata","cld","cosp"],
settings_list=["cosp"],
#rdrop="/glade/derecho/scratch/richling/cases/F2000climo.f09_f09_mg17.window.exp.6hrInit.R13.003/CaseStatus"
)
sub_children.append(widget)
sub_titles.append(run)
sub_tab.children = sub_children
# Name the sub-tabs (each run)
for i, title in enumerate(sub_titles):
sub_tab.set_title(i, title)
# Add subtab into top tab
top_level_children.append(sub_tab)
top_titles.append(case_family)
# Assign to top-level tab
top_tabs.children = top_level_children
# Name top-level tabs (each case family)
for i, title in enumerate(top_titles):
top_tabs.set_title(i, title)
display(top_tabs)
%%html
Run 1 content
You can inject HTML here (images, case settings, anything).
Run 2 content
Run 3 content
Run 4 content
from IPython.display import HTML, display
import os, re, json
from glob import glob
from PIL import Image
from io import BytesIO
import base64
from html import escape
def build_slider_html(case_name, image_dir, ldrop=None, settings_list=None, rdrop=None):
"""
Convert your slider(...) to a pure HTML/CSS/JS blob returned as IPython.display.HTML.
- case_name: string
- image_dir: directory containing images named like ..._hPa.png and with timestamps after 'hPa_'
- ldrop: path to user_nl (optional)
- settings_list: list of additional keywords to include in subset (optional)
- rdrop: path to CaseStatus (optional)
"""
# --- tooltip / link dicts from your function ---
tooltip_dict = {
"Nudge_Uprof": """Selectively apply nudging to U:
OFF = Switch off nudging
ON = Apply nudging everywhere
WINDOW = Apply window function to
nudging tendencies.
0 = OFF
1 = ON
2 = WINDOW
""",
"Nudge_Ucoef": "Selectively adjust the nudging strength applied to U. (normalized) [0.,1.]",
"Nudge_Hwin_lat0": "Specify the horizontal center of the window (lat0) in degrees. [-90., +90.]",
"Nudge_Hwin_lon0": "Specify the horizontal center of the window (lon0) in degrees. [ 0. , 360.]",
"Nudge_Hwin_latWidth": "Specify the lat width of the horizontal window in degrees. >0",
"Nudge_Hwin_lonWidth": "Specify the lon width of the horizontal window in degrees. >0",
"Nudge_Vwin_Hindex": "Vertical window high index",
"Nudge_Vwin_Lindex": "Vertical window low index",
"Nudge_Vwin_Hdelta": "Vertical window high delta",
"Nudge_Vwin_Ldelta": "Vertical window low delta",
"Nudge_Vwin_Invert": "A logical flag used to invert the horizontal window function to get its compliment. True/False",
"Nudge_Hwin_latDelta": "Horizontal lat delta",
"Nudge_Hwin_lonDelta": "Horizontal lon delta",
"Nudge_Hwin_Invert": "A logical flag used to invert the horizontal window function to get its compliment. (e.g. to nudge outside a given window) True/False",
"Nudge_TimeScale_Opt": "Select the timescale for the relaxation: WEAK / STRONG (0=WEAK,1=STRONG)",
"Nudge_Force_Opt": "Select the form of the Target values: NEXT / LINEAR (0=NEXT,1=LINEAR)"
}
link_dict = {
"Nudge_Uprof": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html?highlight=nudge#namelist-values",
"Nudge_Ucoef": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html?highlight=nudge#namelist-values",
"Nudge_TimeScale_Opt": "https://www.google.com"
}
# --- guard ---
if not image_dir or not os.path.isdir(image_dir):
return HTML(f"No valid image_dir: {escape(str(image_dir))}")
# --- detect levels and image files per level ---
lev_pattern = re.compile(r'(\d+)hPa\.png$')
all_files = os.listdir(image_dir)
levels = sorted({lev_pattern.search(f).group(1) for f in all_files if lev_pattern.search(f)})
if not levels:
return HTML("No level-specific image files found (expecting '*hPa.png').")
# helper to extract timestamp like your original code
timestamp_pattern = re.compile(r"hPa_(\d{4}_\d{2}_\d{2}_\d{2}:\d{2})\.png$")
def get_images_for_level(level):
level = str(level)
level_files = sorted([
os.path.join(image_dir, f) for f in os.listdir(image_dir)
if f.endswith(f'{level}hPa.png')
])
timestamps = [
'-'.join(os.path.basename(f).split('_')[1:4]) + ' ' + os.path.basename(f).split('_')[4]
for f in level_files
]
return level_files, timestamps
# --- build base64-encoded images per level (list of lists) ---
images_per_level = {}
timestamps_per_level = {}
for lev in levels:
files, tss = get_images_for_level(lev)
b64_list = []
for p in files:
try:
img = Image.open(p)
# keep aspect similar to your original resize (1000x300) but scale if smaller
#img = img.resize((1000, 300))
img = img.resize((1200, 400))
buf = BytesIO()
img.save(buf, format='PNG')
b64 = base64.b64encode(buf.getvalue()).decode()
b64_list.append(b64)
except Exception as e:
b64_list.append("") # placeholder if something fails
images_per_level[lev] = b64_list
timestamps_per_level[lev] = tss
# --- load and process settings and status text like your original functions ---
def load_user_nl(ldrop, settings_list):
default_setting_list = ["fincl","mfilt","nhtfrq","ncdata"]
lines_nudged = []
lines_all = []
lines_subset = []
try:
with open(ldrop, 'r') as file:
for line in file:
if line.startswith(' Nudge_') or line.startswith(' nudge_') or line.startswith('Nudge_') or line.startswith('nudge_'):
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
lines_nudged.append(cleaned_line.strip())
if settings_list:
for setting in settings_list:
if setting not in default_setting_list:
if setting in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
if "fincl" in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line.strip())
lines_subset.append(cleaned_line)
if "mfilt" in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
if "nhtfrq" in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
if "ncdata" in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
lines_all.append(line.strip())
except Exception as e:
lines_all = [f"Error reading user_nl_cam: {e}"]
return "\n".join(lines_nudged), "\n".join(lines_all), "\n".join(lines_subset)
def load_case_status(case_status):
lines = []
try:
with open(case_status, 'r') as file:
lines = [re.sub(r'\s{3,}', ' ', line).strip() for line in file]
except Exception as e:
lines = [f"Error reading CaseStatus: {e}"]
return "\n".join(lines)
ldrop_opts = ["Hide Settings", "Nudge Params"]
fname = ""
nudge_text = full_text = subset_text = ""
if ldrop and os.path.isfile(ldrop):
nudge_text, full_text, subset_text = load_user_nl(ldrop, settings_list)
if "atm_in" in ldrop:
ldrop_opts += ["Full atm_in", "Subset atm_in"]
fname = "atm_in"
if "user_nl_cam" in ldrop:
ldrop_opts.append("Full user_nl_cam")
fname = "user_nl_cam"
if rdrop and os.path.isfile(rdrop):
status_text = load_case_status(rdrop)
else:
status_text = "Nothing here..."
# --- sanitized HTML snippets ready for insertion (escape content except where we want HTML links) ---
# For settings Nudge Params we will replace keys with or including title attributes.
def process_nudge_html(text):
lines = []
for line in text.splitlines():
original = line
for key in tooltip_dict.keys():
pattern = re.compile(re.escape(key), re.IGNORECASE)
title = tooltip_dict.get(key, "")
href = link_dict.get(key, "")
if href:
repl = f'{escape(key)}'
else:
repl = f'{escape(key)}'
line = pattern.sub(repl, line)
# escape leftover content
# we assume replacements already escaped; ensure any other characters are safe
lines.append(escape(line, quote=False))
# join with
and then unescape the already-constructed / by replacing their escaped forms
html_out = "
".join(lines)
# fix the escaped and by unescaping their tags (they were double-escaped)
# (we purposely escaped the full line earlier — now restore tags)
for key in tooltip_dict.keys():
# replace the escaped anchor/span (they contain <a ... >) with actual tags by searching for the escaped substring
if key in html_out:
# anchors contain the key text; replace the escaped key-only instances with unescaped link/span
# This step is conservative and will mainly restore our inserted tags.
pass
# simpler: since we already constructed repl with proper tags and then escaped the whole line,
# we need to instead rebuild from original again but carefully:
processed = []
for line in text.splitlines():
line2 = line
for key in tooltip_dict.keys():
pattern = re.compile(re.escape(key), re.IGNORECASE)
title = tooltip_dict.get(key, "")
href = link_dict.get(key, "")
if href:
repl = f'{escape(key)}'
else:
repl = f'{escape(key)}'
line2 = pattern.sub(repl, line2)
processed.append(escape(line2))
# Now processed contains escaped content with tags escaped; we must insert the tags verbatim.
# So we'll take the original lines and perform the substitution without escaping the replacements.
final_lines = []
for line in text.splitlines():
ln = line
for key in tooltip_dict.keys():
pattern = re.compile(re.escape(key), re.IGNORECASE)
title = tooltip_dict.get(key, "")
href = link_dict.get(key, "")
if href:
repl = f'{escape(key)}'
else:
repl = f'{escape(key)}'
ln = pattern.sub(repl, ln)
final_lines.append(escape(ln))
# convert to HTML with styling preserved
return "" + "
".join(final_lines) + "
"
nudge_html = process_nudge_html(nudge_text) if nudge_text else "No nudge params found."
subset_html = "" + escape(subset_text) + "
" if subset_text else "No subset."
full_html = "" + escape(full_text) + "
" if full_text else "No full text."
status_html = "" + escape(status_text) + "
"
# --- Build an HTML blob and inject JSON data for JS ---
data = {
"case_name": case_name,
"levels": levels,
"images_per_level": images_per_level,
"timestamps_per_level": timestamps_per_level,
"nudge_html": nudge_html,
"subset_html": subset_html,
"full_html": full_html,
"status_html": status_html,
"ldrop_opts": ldrop_opts,
"fname": fname
}
# Convert data to JSON safely for embedding
data_json = json.dumps(data)
# HTML/CSS/JS template
html = f"""
"""
return HTML(html)
# Example usage:
# display(build_slider_html("CASE_NAME", "/path/to/images", ldrop="/path/to/user_nl_cam", settings_list=["cosp"], rdrop="/path/to/CaseStatus"))
"""
"f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull": [
"f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_tau12h.001",
"f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_tau24h.001",
],
"""
from pathlib import Path
run = "f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_tau12h.001"
#for run in ["f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_tau12h.001",
# "f.e21.FHIST_BGC.f09_f09_mg17.SOCRATES_nudgeUVTfull_tau24h.001"]:
file_path = Path(f"/glade/derecho/scratch/islas/{run}/run/atm_in")
display(build_slider_html(run,
#f"plots/{run}/{plot_type}/ERA5/RF13/",
compass_root,
f"{str(file_path)}",
#settings_list=["fincl","mfilt","nhtfrq","ncdata","cld","cosp"],
settings_list=["cosp"],
#rdrop="/glade/derecho/scratch/richling/cases/F2000climo.f09_f09_mg17.window.exp.6hrInit.R13.003/CaseStatus"
)
)
# In[6]:
#GOOD FUNCTION
from IPython.display import HTML, display
import os, re, json
from glob import glob
from PIL import Image
from io import BytesIO
import base64
from html import escape
def build_slider_html(case_name, image_dir, ldrop=None, settings_list=None, rdrop=None):
"""
Convert your slider(...) to a pure HTML/CSS/JS blob returned as IPython.display.HTML.
- case_name: string
- image_dir: directory containing images named like ..._hPa.png and with timestamps after 'hPa_'
- ldrop: path to user_nl (optional)
- settings_list: list of additional keywords to include in subset (optional)
- rdrop: path to CaseStatus (optional)
"""
# --- tooltip / link dicts from your function ---
tooltip_dict = {
"Nudge_Uprof": """Selectively apply nudging to U:
OFF = Switch off nudging
ON = Apply nudging everywhere
WINDOW = Apply window function to
nudging tendencies.
0 = OFF
1 = ON
2 = WINDOW
""",
"Nudge_Ucoef": "Selectively adjust the nudging strength applied to U. (normalized) [0.,1.]",
"Nudge_Hwin_lat0": "Specify the horizontal center of the window (lat0) in degrees. [-90., +90.]",
"Nudge_Hwin_lon0": "Specify the horizontal center of the window (lon0) in degrees. [ 0. , 360.]",
"Nudge_Hwin_latWidth": "Specify the lat width of the horizontal window in degrees. >0",
"Nudge_Hwin_lonWidth": "Specify the lon width of the horizontal window in degrees. >0",
"Nudge_Vwin_Hindex": "Vertical window high index",
"Nudge_Vwin_Lindex": "Vertical window low index",
"Nudge_Vwin_Hdelta": "Vertical window high delta",
"Nudge_Vwin_Ldelta": "Vertical window low delta",
"Nudge_Vwin_Invert": "A logical flag used to invert the horizontal window function to get its compliment. True/False",
"Nudge_Hwin_latDelta": "Horizontal lat delta",
"Nudge_Hwin_lonDelta": "Horizontal lon delta",
"Nudge_Hwin_Invert": "A logical flag used to invert the horizontal window function to get its compliment. (e.g. to nudge outside a given window) True/False",
"Nudge_TimeScale_Opt": "Select the timescale for the relaxation: WEAK / STRONG (0=WEAK,1=STRONG)",
"Nudge_Force_Opt": "Select the form of the Target values: NEXT / LINEAR (0=NEXT,1=LINEAR)"
}
link_dict = {
"Nudge_Uprof": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html?highlight=nudge#namelist-values",
"Nudge_Ucoef": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html?highlight=nudge#namelist-values",
"Nudge_TimeScale_Opt": "https://www.google.com"
}
# --- guard ---
if not image_dir or not os.path.isdir(image_dir):
return HTML(f"No valid image_dir: {escape(str(image_dir))}")
# --- detect levels and image files per level ---
lev_pattern = re.compile(r'(\d+)hPa\.png$')
all_files = os.listdir(image_dir)
levels = sorted({lev_pattern.search(f).group(1) for f in all_files if lev_pattern.search(f)})
if not levels:
return HTML("No level-specific image files found (expecting '*hPa.png').")
# helper to extract timestamp like your original code
timestamp_pattern = re.compile(r"hPa_(\d{4}_\d{2}_\d{2}_\d{2}:\d{2})\.png$")
def get_images_for_level(level):
level = str(level)
level_files = sorted([
os.path.join(image_dir, f) for f in os.listdir(image_dir)
if f.endswith(f'{level}hPa.png')
])
timestamps = [
'-'.join(os.path.basename(f).split('_')[1:4]) + ' ' + os.path.basename(f).split('_')[4]
for f in level_files
]
return level_files, timestamps
# --- build base64-encoded images per level (list of lists) ---
images_per_level = {}
timestamps_per_level = {}
for lev in levels:
files, tss = get_images_for_level(lev)
b64_list = []
for p in files:
try:
img = Image.open(p)
# keep aspect similar to your original resize (1000x300) but scale if smaller
#img = img.resize((1000, 300))
img = img.resize((1200, 400))
buf = BytesIO()
img.save(buf, format='PNG')
b64 = base64.b64encode(buf.getvalue()).decode()
b64_list.append(b64)
except Exception as e:
b64_list.append("") # placeholder if something fails
images_per_level[lev] = b64_list
timestamps_per_level[lev] = tss
# --- load and process settings and status text like your original functions ---
def load_user_nl(ldrop, settings_list):
default_setting_list = ["fincl","mfilt","nhtfrq","ncdata"]
lines_nudged = []
lines_all = []
lines_subset = []
try:
with open(ldrop, 'r') as file:
for line in file:
if line.startswith(' Nudge_') or line.startswith(' nudge_') or line.startswith('Nudge_') or line.startswith('nudge_'):
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
lines_nudged.append(cleaned_line.strip())
if settings_list:
for setting in settings_list:
if setting not in default_setting_list:
if setting in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
if "fincl" in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line.strip())
lines_subset.append(cleaned_line)
if "mfilt" in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
if "nhtfrq" in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
if "ncdata" in line:
cleaned_line = re.sub(r'\s{3,}', ' ', line)
lines_subset.append(cleaned_line.strip())
lines_all.append(line.strip())
except Exception as e:
lines_all = [f"Error reading user_nl_cam: {e}"]
return "\n".join(lines_nudged), "\n".join(lines_all), "\n".join(lines_subset)
def load_case_status(case_status):
lines = []
try:
with open(case_status, 'r') as file:
lines = [re.sub(r'\s{3,}', ' ', line).strip() for line in file]
except Exception as e:
lines = [f"Error reading CaseStatus: {e}"]
return "\n".join(lines)
ldrop_opts = ["Hide Settings", "Nudge Params"]
fname = ""
nudge_text = full_text = subset_text = ""
if ldrop and os.path.isfile(ldrop):
nudge_text, full_text, subset_text = load_user_nl(ldrop, settings_list)
if "atm_in" in ldrop:
ldrop_opts += ["Full atm_in", "Subset atm_in"]
fname = "atm_in"
if "user_nl_cam" in ldrop:
ldrop_opts.append("Full user_nl_cam")
fname = "user_nl_cam"
if rdrop and os.path.isfile(rdrop):
status_text = load_case_status(rdrop)
else:
status_text = "Nothing here..."
# --- sanitized HTML snippets ready for insertion (escape content except where we want HTML links) ---
# For settings Nudge Params we will replace keys with or including title attributes.
def process_nudge_html(text):
lines = []
for line in text.splitlines():
original = line
for key in tooltip_dict.keys():
pattern = re.compile(re.escape(key), re.IGNORECASE)
title = tooltip_dict.get(key, "")
href = link_dict.get(key, "")
if href:
repl = f'{escape(key)}'
else:
repl = f'{escape(key)}'
line = pattern.sub(repl, line)
# escape leftover content
# we assume replacements already escaped; ensure any other characters are safe
lines.append(escape(line, quote=False))
# join with
and then unescape the already-constructed / by replacing their escaped forms
html_out = "
".join(lines)
# fix the escaped and by unescaping their tags (they were double-escaped)
# (we purposely escaped the full line earlier — now restore tags)
for key in tooltip_dict.keys():
# replace the escaped anchor/span (they contain <a ... >) with actual tags by searching for the escaped substring
if key in html_out:
# anchors contain the key text; replace the escaped key-only instances with unescaped link/span
# This step is conservative and will mainly restore our inserted tags.
pass
# simpler: since we already constructed repl with proper tags and then escaped the whole line,
# we need to instead rebuild from original again but carefully:
processed = []
for line in text.splitlines():
line2 = line
for key in tooltip_dict.keys():
pattern = re.compile(re.escape(key), re.IGNORECASE)
title = tooltip_dict.get(key, "")
href = link_dict.get(key, "")
if href:
repl = f'{escape(key)}'
else:
repl = f'{escape(key)}'
line2 = pattern.sub(repl, line2)
processed.append(escape(line2))
# Now processed contains escaped content with tags escaped; we must insert the tags verbatim.
# So we'll take the original lines and perform the substitution without escaping the replacements.
final_lines = []
for line in text.splitlines():
ln = line
for key in tooltip_dict.keys():
pattern = re.compile(re.escape(key), re.IGNORECASE)
title = tooltip_dict.get(key, "")
href = link_dict.get(key, "")
if href:
repl = f'{escape(key)}'
else:
repl = f'{escape(key)}'
ln = pattern.sub(repl, ln)
final_lines.append(escape(ln))
# convert to HTML with styling preserved
return "" + "
".join(final_lines) + "
"
nudge_html = process_nudge_html(nudge_text) if nudge_text else "No nudge params found."
subset_html = "" + escape(subset_text) + "
" if subset_text else "No subset."
full_html = "" + escape(full_text) + "
" if full_text else "No full text."
status_html = "" + escape(status_text) + "
"
# --- Build an HTML blob and inject JSON data for JS ---
data = {
"case_name": case_name,
"levels": levels,
"images_per_level": images_per_level,
"timestamps_per_level": timestamps_per_level,
"nudge_html": nudge_html,
"subset_html": subset_html,
"full_html": full_html,
"status_html": status_html,
"ldrop_opts": ldrop_opts,
"fname": fname
}
# Convert data to JSON safely for embedding
data_json = json.dumps(data)
# HTML/CSS/JS template
html = f"""
"""
return HTML(html)
# In[7]:
#GOOD FUNCTION
from IPython.display import HTML, display
from pathlib import Path
outer_tabs_html = []
outer_tab_buttons = []
outer_content_html = []
for i, (outer_name, inner_runs) in enumerate(runs_dict.items()):
# Outer tab button
active_cls = "active" if i==0 else ""
outer_tab_buttons.append(f'{outer_name}')
inner_tabs = []
inner_contents = []
for j, run_name in enumerate(inner_runs):
# Inner tab button
inner_active = "active" if j==0 else ""
inner_tabs.append(f'{run_name.split("_")[-1]}')
## Inner content: slider HTML
#slider_html = build_slider_html(
# run_name,
# image_dir=f"/glade/derecho/scratch/islas/{run_name}/run/atm_in",
# settings_list=["cosp"],
#).data # get the raw HTML string
file_path = Path(f"/glade/derecho/scratch/islas/{run_name}/run/atm_in")
slider_html = build_slider_html(run_name,
#f"plots/{run}/{plot_type}/ERA5/RF13/",
compass_root,
f"{str(file_path)}",
#settings_list=["fincl","mfilt","nhtfrq","ncdata","cld","cosp"],
settings_list=["cosp"],
#rdrop="/glade/derecho/scratch/richling/cases/F2000climo.f09_f09_mg17.window.exp.6hrInit.R13.003/CaseStatus"
).data # get the raw HTML string
inner_contents.append(f'{slider_html}')
outer_active = "active" if i==0 else ""
outer_content_html.append(f"""
{''.join(inner_tabs)}
{''.join(inner_contents)}
""")
html_full = f"""
{''.join(outer_tab_buttons)}
{''.join(outer_content_html)}
"""
# In[8]:
display(HTML(html_full))
from IPython.display import HTML, display
# Build HTML
outer_options = "".join([f'' for i, name in enumerate(runs_dict.keys())])
outer_contents = []
for i, (outer_name, inner_runs) in enumerate(runs_dict.items()):
inner_tabs_html = []
inner_contents_html = []
for j, run_name in enumerate(inner_runs):
active_cls = "active" if j==0 else ""
# Inner tab button
inner_tabs_html.append(f'{run_name.split("_")[-1]}')
# Inner content: embed your slider HTML
slider_html = build_slider_html(run_name,
#f"plots/{run}/{plot_type}/ERA5/RF13/",
compass_root,
f"{str(file_path)}",
#settings_list=["fincl","mfilt","nhtfrq","ncdata","cld","cosp"],
settings_list=["cosp"],
#rdrop="/glade/derecho/scratch/richling/cases/F2000climo.f09_f09_mg17.window.exp.6hrInit.R13.003/CaseStatus"
).data # extract HTML string
inner_contents_html.append(f'{slider_html}')
outer_active = "active" if i==0 else ""
outer_contents.append(f"""
{''.join(inner_tabs_html)}
{''.join(inner_contents_html)}
""")
html_full = f"""
{''.join(outer_contents)}
"""
display(HTML(html_full))
from IPython.display import HTML, display
import os, re, json
from glob import glob
from PIL import Image
from io import BytesIO
import base64
from html import escape
from pathlib import Path
def build_slider_html_multi(run_families, compass_root, base_path, settings_list=None):
"""
Build a slider widget with outer dropdown (run families) and inner tabs (runs within family)
run_families: dict
{"family_name1": ["run1","run2"], "family_name2": ["run3","run4"]}
base_path: str or Path
Base path where runs are located (will append /run/atm_in)
settings_list: list of strings (optional)
"""
# --- helper functions from your previous function ---
tooltip_dict = {
"Nudge_Uprof": "Selectively apply nudging to U",
"Nudge_Ucoef": "Selectively adjust the nudging strength applied to U. (normalized) [0.,1.]",
"Nudge_Hwin_lat0": "Specify the horizontal center of the window (lat0) in degrees. [-90., +90.]",
"Nudge_Hwin_lon0": "Specify the horizontal center of the window (lon0) in degrees. [ 0. , 360.]",
"Nudge_Hwin_latWidth": "Specify the lat width of the horizontal window in degrees. >0",
"Nudge_Hwin_lonWidth": "Specify the lon width of the horizontal window in degrees. >0",
"Nudge_Vwin_Hindex": "Vertical window high index",
"Nudge_Vwin_Lindex": "Vertical window low index",
"Nudge_Vwin_Hdelta": "Vertical window high delta",
"Nudge_Vwin_Ldelta": "Vertical window low delta",
"Nudge_Vwin_Invert": "A logical flag used to invert the horizontal window function to get its compliment. True/False",
"Nudge_Hwin_latDelta": "Horizontal lat delta",
"Nudge_Hwin_lonDelta": "Horizontal lon delta",
"Nudge_Hwin_Invert": "A logical flag used to invert the horizontal window function to get its compliment. (e.g. to nudge outside a given window) True/False",
"Nudge_TimeScale_Opt": "Select the timescale for the relaxation: WEAK / STRONG (0=WEAK,1=STRONG)",
"Nudge_Force_Opt": "Select the form of the Target values: NEXT / LINEAR (0=NEXT,1=LINEAR)"
}
link_dict = {
"Nudge_Uprof": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html?highlight=nudge#namelist-values",
"Nudge_Ucoef": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html?highlight=nudge#namelist-values",
"Nudge_TimeScale_Opt": "https://www.google.com"
}
def process_nudge_html(text):
lines = []
for line in text.splitlines():
line2 = line
for key in tooltip_dict.keys():
pattern = re.compile(re.escape(key), re.IGNORECASE)
title = tooltip_dict.get(key, "")
href = link_dict.get(key, "")
if href:
repl = f'{escape(key)}'
else:
repl = f'{escape(key)}'
line2 = pattern.sub(repl, line2)
lines.append(escape(line2))
return "" + "
".join(lines) + "
"
def load_user_nl(ldrop):
lines_nudged = []
try:
with open(ldrop, 'r') as file:
for line in file:
if line.startswith(' Nudge_') or line.startswith('nudge_'):
lines_nudged.append(line.strip())
except Exception as e:
lines_nudged = [f"Error reading user_nl_cam: {e}"]
return "\n".join(lines_nudged)
def get_images(compass_root):
lev_pattern = re.compile(r'(\d+)hPa\.png$')
timestamp_pattern = re.compile(r"hPa_(\d{4}_\d{2}_\d{2}_\d{2}:\d{2})\.png$")
if not os.path.isdir(compass_root):
return [], [], []
files = sorted(os.listdir(compass_root))
levels = sorted({lev_pattern.search(f).group(1) for f in files if lev_pattern.search(f)})
images_per_level = {}
timestamps_per_level = {}
for lev in levels:
level_files = sorted([os.path.join(compass_root, f) for f in files if f.endswith(f"{lev}hPa.png")])
tss = []
b64_list = []
for f in level_files:
# timestamp
ts_m = timestamp_pattern.search(f)
tss.append(ts_m.group(1) if ts_m else "NoTS")
# image
try:
img = Image.open(f)
img = img.resize((1200,400))
buf = BytesIO()
img.save(buf, format='PNG')
b64_list.append(base64.b64encode(buf.getvalue()).decode())
except:
b64_list.append("")
images_per_level[lev] = b64_list
timestamps_per_level[lev] = tss
return levels, images_per_level, timestamps_per_level
# --- Build data structure for all runs ---
all_data = {}
slider_count = 0 # to generate unique ID prefixes
for fam_name, run_list in run_families.items():
all_data[fam_name] = {}
for run_name in run_list:
run_path = Path(base_path)/run_name/"run"/"atm_in"
levels, images_per_level, timestamps_per_level = get_images(str(compass_root))
# Load nudge params if exist
ldrop = run_path # just as example
nudge_text = load_user_nl(str(run_path/"user_nl_cam")) if (run_path/"user_nl_cam").exists() else ""
data = {
"case_name": run_name,
"levels": levels,
"images_per_level": images_per_level,
"timestamps_per_level": timestamps_per_level,
"nudge_html": process_nudge_html(nudge_text),
"ldrop_opts": ["Hide Settings","Nudge Params"],
"slider_id": f"slider_{slider_count}"
}
all_data[fam_name][run_name] = data
slider_count += 1
data_json = json.dumps(all_data)
# --- HTML/JS template ---
html = f"""
"""
return HTML(html)
compass_root,
#f"{str(file_path)}",
display(build_slider_html_multi(runs_dict, compass_root, f"{str(file_path)}", settings_list=["cosp"]))from IPython.display import HTML, display
from pathlib import Path
def build_slider_dropdown(runs_dict, compass_root, base_path=None, settings_list=None):
"""
runs_dict: {"Family1": ["Run1", "Run2"], ...}
compass_root: Path to PNG images for all runs
base_path: optional base path to /run/atm_in (for ldrop/rdrop)
settings_list: list of settings keywords to pass to build_slider_html
"""
outer_options = "".join([f''
for i, name in enumerate(runs_dict.keys())])
outer_contents = []
for i, (outer_name, inner_runs) in enumerate(runs_dict.items()):
inner_tabs_html = []
inner_contents_html = []
for j, run_name in enumerate(inner_runs):
active_cls = "active" if j == 0 else ""
# Inner tab button
inner_tabs_html.append(
f''
f'{run_name.split("_")[-1]}'
)
# optional ldrop/rdrop path
file_path = Path(f"/glade/derecho/scratch/islas/{run_name}/run/atm_in")
#ldrop_path = Path(base_path) / run_name / "run" / "atm_in" if base_path else None
ldrop_path = str(file_path) if file_path else None
# embed slider HTML for this run
slider_html = build_slider_html(
run_name,
compass_root,
ldrop=ldrop_path,
settings_list=settings_list
).data
inner_contents_html.append(
f'{slider_html}'
)
outer_active = "active" if i == 0 else ""
outer_contents.append(f"""
{''.join(inner_tabs_html)}
{''.join(inner_contents_html)}
""")
html_full = f"""
{''.join(outer_contents)}
"""
display(HTML(html_full))
#compass_root = "/glade/derecho/scratch/islas/plots_all_pngs"
base_path = "/glade/derecho/scratch/islas"
build_slider_dropdown(runs_dict, compass_root, settings_list=["cosp"])from IPython.display import HTML, display
# Example: runs_dict = {"FamilyA": ["runA1","runA2"], "FamilyB": ["runB1","runB2"]}
# compass_root = path to images
# base_path = path to /run/atm_in
outer_options = "".join([f'' for i, name in enumerate(runs_dict.keys())])
outer_contents = []
for i, (outer_name, inner_runs) in enumerate(runs_dict.items()):
inner_tabs_html = []
inner_contents_html = []
for j, run_name in enumerate(inner_runs):
active_cls = "active" if j==0 else ""
# Inner tab button
inner_tabs_html.append(f'{run_name.split("_")[-1]}')
# Build slider HTML for this run
slider_html = build_slider_html(
run_name,
compass_root,
ldrop=f"{base_path}/{run_name}/run/atm_in",
settings_list=["cosp"]
).data # HTML string
inner_contents_html.append(f'{slider_html}')
outer_active = "active" if i==0 else ""
outer_contents.append(f"""
{''.join(inner_tabs_html)}
{''.join(inner_contents_html)}
""")
html_full = f"""
{''.join(outer_contents)}
"""
display(HTML(html_full))
# In[ ]: