#!/usr/bin/env python # coding: utf-8 # # COMPASS Model Diagnostics # # 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.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)) # ![Compass schematic](${compass_root}compass_schematic_2025_12_09.jpg) # In[4]: from IPython.display import Image Image(f"{compass_root}/compass_schematic_2025_12_09.jpg") # In[5]: get_ipython().run_cell_magic('html', '', '\n\n\n
\n \n
\n \n \n
\n\n \n
\n\n
\n \n \n
\n\n
\n

Run 1 content

\n

You can inject HTML here (images, case settings, anything).

\n
\n\n
\n

Run 2 content

\n
\n\n
\n\n \n
\n\n
\n \n \n
\n\n
\n

Run 3 content

\n
\n\n
\n

Run 4 content

\n
\n
\n
\n\n\n\n') # 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, unique_id="0"): """ 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) """ uid = unique_id.replace("-", "_") html = f"""
""" # --- 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"""
{escape(case_name)}
Choose Settings to view
CaseStatus output
""" return HTML(html) # In[7]: from IPython.display import HTML import os, re, json, base64 from io import BytesIO from PIL import Image from html import escape # ---------------------------------------------------- # Constants used for highlighting nudging parameters # ---------------------------------------------------- TOOLTIPS = { "Nudge_Uprof": """Selectively apply nudging to U: OFF = off ON = everywhere WINDOW = apply a window function 0=OFF 1=ON 2=WINDOW """, "Nudge_Ucoef": "Normalized nudging strength [0–1].", "Nudge_TimeScale_Opt": "Timescale option: 0=WEAK, 1=STRONG", "Nudge_Force_Opt": "Target form: 0=NEXT, 1=LINEAR", } LINKS = { "Nudge_Uprof": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html", "Nudge_Ucoef": "https://ncar.github.io/CAM/doc/build/html/users_guide/physics-modifications-via-the-namelist.html", "Nudge_TimeScale_Opt": "https://www.google.com", } # ---------------------------------------------------- # Highlight nudging keys (plain → or ) # ---------------------------------------------------- def highlight_nudge_text(text): if not text: return "No nudge parameters found." html_lines = [] for line in text.splitlines(): for key in TOOLTIPS: tooltip = escape(TOOLTIPS[key]) if key in LINKS: repl = f'{key}' else: repl = f'{key}' line = re.sub(key, repl, line) html_lines.append(line) return "
" + "
".join(html_lines) + "
" # ---------------------------------------------------- # Read user_nl_cam or atm_in # ---------------------------------------------------- def load_user_nl_file(path, settings_list): nudged, all_text, subset = [], [], [] default_keys = ["fincl", "mfilt", "nhtfrq", "ncdata"] try: with open(path) as f: for line in f: stripped = re.sub(r"\s{3,}", " ", line).strip() all_text.append(stripped) # nudging lines if stripped.lower().startswith(("nudge_", " nudge_")): nudged.append(stripped) subset.append(stripped) # extra settings if settings_list: for key in settings_list: if key not in default_keys and key in stripped: subset.append(stripped) # required default keys if any(k in stripped for k in default_keys): subset.append(stripped) except Exception as e: return "", f"Error reading {path}: {e}", "" return "\n".join(nudged), "\n".join(all_text), "\n".join(subset) # ---------------------------------------------------- # Read CaseStatus # ---------------------------------------------------- def load_casestatus(path): try: with open(path) as f: return "\n".join([re.sub(r"\s{3,}", " ", line).strip() for line in f]) except Exception as e: return f"Error reading CaseStatus: {e}" # ---------------------------------------------------- # Level detection + base64 image loading # ---------------------------------------------------- def detect_levels(image_dir): pat = re.compile(r"(\d+)hPa\.png$") files = os.listdir(image_dir) return sorted({pat.search(f).group(1) for f in files if pat.search(f)}) def load_level_images(image_dir, level): level = str(level) files = sorted([ os.path.join(image_dir, f) for f in os.listdir(image_dir) if f.endswith(f"{level}hPa.png") ]) ts = [ "-".join(os.path.basename(f).split("_")[1:4]) + " " + os.path.basename(f).split("_")[4] for f in files ] b64 = [] for f in files: try: img = Image.open(f).resize((1200, 400)) buf = BytesIO() img.save(buf, format="PNG") b64.append(base64.b64encode(buf.getvalue()).decode()) except: b64.append("") return b64, ts # ---------------------------------------------------- # CLEAN + COMPLETE MAIN FUNCTION # ---------------------------------------------------- def build_slider_html(case_name, image_dir, ldrop=None, settings_list=None, rdrop=None, unique_id="0"): uid = unique_id.replace("-", "_") if not os.path.isdir(image_dir): return HTML(f"
Invalid image_dir: {escape(image_dir)}
") # ---- gather all data ------------------------------------------------- levels = detect_levels(image_dir) if not levels: return HTML("
No *hPa.png files found.
") images_per_level = {} timestamps_per_level = {} for lev in levels: b64, ts = load_level_images(image_dir, lev) images_per_level[lev] = b64 timestamps_per_level[lev] = ts # --- user_nl_cam / atm_in text panels --- nudge_text = full_text = subset_text = "" ldrop_opts = ["Hide Settings", "Nudge Params"] fname = "" if ldrop and os.path.isfile(ldrop): nudge_text, full_text, subset_text = load_user_nl_file(ldrop, settings_list) if "atm_in" in ldrop: ldrop_opts += ["Full atm_in", "Subset atm_in"] fname = "atm_in" elif "user_nl_cam" in ldrop: ldrop_opts += ["Full user_nl_cam"] fname = "user_nl_cam" status_text = load_casestatus(rdrop) if (rdrop and os.path.isfile(rdrop)) else "Nothing here..." # ---- preformatted HTML chunks ---- nudge_html = highlight_nudge_text(nudge_text) subset_html = f"
{escape(subset_text)}
" full_html = f"
{escape(full_text)}
" status_html = f"
{escape(status_text)}
" # ---- pack into JSON for JS ---- data = dict( 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, ) data_json = json.dumps(data) # ---------------------------------------------------- # Clean HTML + JS template # ---------------------------------------------------- html = f"""
{escape(case_name)}
Select settings…
Hidden
""" return HTML(html) # In[8]: #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, compass_root, str(file_path), settings_list=["cosp"], ).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[9]: display(HTML(html_full)) # In[10]: from IPython.display import display, HTML from html import escape import json # This function should generate data_json for each run (replace with your real build_slider_html logic) def dummy_slider_data(run_name): # Minimal example of slider JSON for testing return { "case_name": run_name, "levels": [500, 700], "images_per_level": {"500": ["", ""], "700": ["", ""]}, "timestamps_per_level": {"500": ["2025-12-01 00:00","2025-12-01 06:00"], "700": ["2025-12-01 00:00","2025-12-01 06:00"]}, "ldrop_opts": ["Hide Settings", "Nudge Params"], "nudge_html": "Example nudge params", "full_html": "Full text", "subset_html": "Subset", "status_html": "CaseStatus info", } # Build nested tab HTML html = '
\n' # Top-level buttons html += '
\n' for family in runs_dict: html += f'\n' html += '
\n' # Tab content per family for family, run_list in runs_dict.items(): html += f'
\n' # Sub-tabs html += '
\n' for run in run_list: html += f'\n' html += '
\n' # Subtab content for run in run_list: data_json_str = json.dumps(dummy_slider_data(run)) slider_html = f"""

{escape(run)}

""" html += slider_html html += '
\n' html += '
' # end tab-container # Add CSS/JS for tabs html += """ """ display(HTML(html)) # In[11]: from IPython.display import display, HTML from html import escape import json # Example runs_dict (replace with your real runs_dict) # Example slider data per run (replace with your real build_slider_data logic) def build_slider_data(run_name): return { "case_name": run_name, "levels": [500, 700], "images_per_level": { "500": ["", ""], # Base64 strings for your images "700": ["", ""] }, "timestamps_per_level": { "500": ["2025-12-01 00:00", "2025-12-01 06:00"], "700": ["2025-12-01 00:00", "2025-12-01 06:00"] }, "ldrop_opts": ["Hide Settings", "Nudge Params", "Full Config", "Subset Config"], "nudge_html": "Nudge params example", "full_html": "Full configuration content", "subset_html": "Subset content", "status_html": "Case status info" } # Start building HTML html = '
\n' # Top-level buttons html += '
\n' for family in runs_dict: html += f'\n' html += '
\n' # Tab content per family for family, run_list in runs_dict.items(): html += f'
\n' # Sub-tabs html += '
\n' for run in run_list: html += f'\n' html += '
\n' # Subtab content for run in run_list: data_json_str = json.dumps(build_slider_data(run)) slider_html = f"""

{escape(run)}

""" html += slider_html html += '
\n' html += '
' # end tab-container # Add CSS/JS for tabs html += """ """ display(HTML(html)) # In[12]: from IPython.display import HTML import json # --- Your Python dictionary --- data = { "Region A": ["https://via.placeholder.com/400x300?text=A1", "https://via.placeholder.com/400x300?text=A2"], "Region B": ["https://via.placeholder.com/400x300?text=B1", "https://via.placeholder.com/400x300?text=B2", "https://via.placeholder.com/400x300?text=B3"], "Region C": ["https://via.placeholder.com/400x300?text=C1"] } html = f""" """ HTML(html)