From 2af7dcb80d85b8bd75d33af13a2dc6daa665d4a9 Mon Sep 17 00:00:00 2001 From: St3451 Date: Thu, 28 May 2026 17:53:01 +0200 Subject: [PATCH 1/7] feat: enhance robustness of ChimeraX plotting by handling subprocess failures --- scripts/plotting/chimerax_plot.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/scripts/plotting/chimerax_plot.py b/scripts/plotting/chimerax_plot.py index b63ba41..ab76128 100644 --- a/scripts/plotting/chimerax_plot.py +++ b/scripts/plotting/chimerax_plot.py @@ -16,7 +16,19 @@ logger = daiquiri.getLogger(__logger_name__ + ".plotting.chimerax_plot") -def create_attribute_file(path_to_file, +def _run_chimerax(command, label): + """Run a ChimeraX subprocess. Surface failures as warnings so one bad image + doesn't abort the whole loop, but the user actually sees something went wrong.""" + result = subprocess.run(command, shell=True, capture_output=True, text=True) + if result.returncode != 0: + logger.warning( + f"ChimeraX failed for {label} " + f"(rc={result.returncode}): {result.stderr.strip()}" + ) + return result + + +def create_attribute_file(path_to_file, df, attribute_col, pos_col="Pos", @@ -233,9 +245,9 @@ def generate_chimerax_plot(output_dir, cohort, pixelsize=pixel_size, transparent_bg=transparent_bg) - subprocess.run(chimerax_command, shell=True) + _run_chimerax(chimerax_command, f"{gene} ({attribute})") logger.debug(chimerax_command) - + if attribute == "score" or attribute == "logscore": chimerax_command = get_chimerax_command(chimerax_bin, pdb_path, @@ -252,7 +264,7 @@ def generate_chimerax_plot(output_dir, clusters=clusters, pixelsize=pixel_size, transparent_bg=transparent_bg) - subprocess.run(chimerax_command, shell=True) + _run_chimerax(chimerax_command, f"{gene} ({attribute}, clusters)") logger.debug(chimerax_command) else: From 64309f7dad7e9207c60c6b3243bb4fc295e38901 Mon Sep 17 00:00:00 2001 From: St3451 Date: Thu, 28 May 2026 17:56:19 +0200 Subject: [PATCH 2/7] feat: enhance robustness of plotting by capping infinite scores in results --- scripts/plotting/chimerax_plot.py | 3 ++- scripts/plotting/plot.py | 2 ++ scripts/plotting/utils.py | 34 +++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/scripts/plotting/chimerax_plot.py b/scripts/plotting/chimerax_plot.py index ab76128..15ab038 100644 --- a/scripts/plotting/chimerax_plot.py +++ b/scripts/plotting/chimerax_plot.py @@ -11,7 +11,7 @@ import daiquiri from scripts import __logger_name__ -from scripts.plotting.utils import detect_af_version +from scripts.plotting.utils import cap_inf_scores, detect_af_version logger = daiquiri.getLogger(__logger_name__ + ".plotting.chimerax_plot") @@ -163,6 +163,7 @@ def generate_chimerax_plot(output_dir, result = pd.read_csv(pos_result_path) if "Ratio_obs_sim" in result.columns: result = result.rename(columns={"Ratio_obs_sim" : "Score_obs_sim"}) + result = cap_inf_scores(result) result["Logscore_obs_sim"] = np.log(result["Score_obs_sim"]) # Detect the AlphaFold version actually present in the dataset; the diff --git a/scripts/plotting/plot.py b/scripts/plotting/plot.py index 4e809da..6face81 100644 --- a/scripts/plotting/plot.py +++ b/scripts/plotting/plot.py @@ -17,6 +17,7 @@ import daiquiri from scripts.plotting.utils import ( + cap_inf_scores, get_broad_consequence, save_annotated_result, get_enriched_result, @@ -2308,6 +2309,7 @@ def generate_plots(gene_result_path, gene_result = pd.read_csv(gene_result_path) pos_result = pd.read_csv(pos_result_path) + pos_result = cap_inf_scores(pos_result) maf = pd.read_csv(maf_path, sep="\t") miss_prob_dict = json.load(open(miss_prob_path)) seq_df = pd.read_csv(seq_df_path, sep="\t") diff --git a/scripts/plotting/utils.py b/scripts/plotting/utils.py index 35bca05..2e45274 100644 --- a/scripts/plotting/utils.py +++ b/scripts/plotting/utils.py @@ -17,6 +17,40 @@ logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING) +def cap_inf_scores(pos_result, col="Score_obs_sim"): + """ + Replace +inf values in a score column with a saturated finite value. + + `Score_obs_sim` is +inf for extreme hotspot positions where the observed + anomaly score is at the mathematical limit (the high-precision Decimal + fallback in score_and_simulations.get_dcm_anomaly_score returns +inf when + even 600-digit precision underflows). +inf breaks downstream plotting: + normalization (sum=inf), log transforms, and axis auto-scaling. + + We cap +inf at 1.5 * max(finite) so the point stays visible at the top of + the y-axis without distorting the rest of the track. + """ + if col not in pos_result.columns: + return pos_result + + inf_mask = np.isinf(pos_result[col]) + if not inf_mask.any(): + return pos_result + + finite_values = pos_result.loc[~inf_mask, col] + finite_max = finite_values.max() if not finite_values.empty else np.nan + cap = finite_max * 1.5 if pd.notna(finite_max) and finite_max > 0 else 1.0 + + n_inf = int(inf_mask.sum()) + logger.warning( + f"Capping {n_inf} `{col}` value(s) at {cap:.4f} for plotting " + f"(originally +inf — extreme hotspot positions)" + ) + pos_result = pos_result.copy() + pos_result.loc[inf_mask, col] = cap + return pos_result + + _AF_VERSION_RE = re.compile(r"-model_v(\d+)\.pdb(?:\.gz)?$") From 5281a472be431cae0b66d231fc4df55e67f014ab Mon Sep 17 00:00:00 2001 From: St3451 Date: Thu, 28 May 2026 18:12:52 +0200 Subject: [PATCH 3/7] feat: enhance robustness of score capping by checking for positive infinity only --- scripts/plotting/utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/plotting/utils.py b/scripts/plotting/utils.py index 2e45274..20ed174 100644 --- a/scripts/plotting/utils.py +++ b/scripts/plotting/utils.py @@ -33,7 +33,10 @@ def cap_inf_scores(pos_result, col="Score_obs_sim"): if col not in pos_result.columns: return pos_result - inf_mask = np.isinf(pos_result[col]) + # Only +inf is expected here (the score is bounded below by 0); use the + # positive-only check so any future -inf sentinel is preserved instead of + # silently rewritten. + inf_mask = np.isposinf(pos_result[col]) if not inf_mask.any(): return pos_result From e0a5f2a132f9e0f5b4d08d487a9bee0c74ccecc8 Mon Sep 17 00:00:00 2001 From: St3451 Date: Thu, 28 May 2026 18:15:19 +0200 Subject: [PATCH 4/7] feat: enhance robustness of ChimeraX plotting by using argv list for subprocess commands --- scripts/plotting/chimerax_plot.py | 38 ++++++++++++++++++------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/scripts/plotting/chimerax_plot.py b/scripts/plotting/chimerax_plot.py index 15ab038..bc8580a 100644 --- a/scripts/plotting/chimerax_plot.py +++ b/scripts/plotting/chimerax_plot.py @@ -2,7 +2,8 @@ Use ChimeraX to generate 3D structures colored by metrics """ -import os +import os +import shlex import subprocess import math @@ -18,8 +19,12 @@ def _run_chimerax(command, label): """Run a ChimeraX subprocess. Surface failures as warnings so one bad image - doesn't abort the whole loop, but the user actually sees something went wrong.""" - result = subprocess.run(command, shell=True, capture_output=True, text=True) + doesn't abort the whole loop, but the user actually sees something went wrong. + + `command` is an argv list (no shell interpretation), so paths and labels + with spaces or shell metacharacters pass through verbatim. + """ + result = subprocess.run(command, capture_output=True, text=True) if result.returncode != 0: logger.warning( f"ChimeraX failed for {label} " @@ -112,11 +117,10 @@ def get_chimerax_command(chimerax_bin, transparent_bg=False): palette = get_palette(intervals, type="diverging") if attribute == "logscore" else get_palette(intervals, type="sequential") - transparent_bg = " transparentBackground true" if transparent_bg else "" - - chimerax_command = ( - f"{chimerax_bin} --nogui --offscreen --silent --cmd " - f"\"open {pdb_path}; " + transparent_bg_suffix = " transparentBackground true" if transparent_bg else "" + + chimerax_script = ( + f"open {pdb_path}; " "set bgColor white; " "color lightgray; " f"open {attr_file_path}; " @@ -130,18 +134,20 @@ def get_chimerax_command(chimerax_bin, "graphics silhouettes true width 1.3;" "zoom;" ) - + if clusters is not None and len(clusters) > 0: for pos in clusters: - chimerax_command += f"marker #10 position :{pos} color #dacae961 radius 5.919;" + chimerax_script += f"marker #10 position :{pos} color #dacae961 radius 5.919;" cluster_tag = "_clusters" else: cluster_tag = "" - + output_path = os.path.join(chimera_output_path, f"{cohort}_{i}_{gene}_{attribute}{cluster_tag}.png") - chimerax_command += f"save {output_path} pixelSize {pixelsize} supersample 3{transparent_bg};exit\"" - - return chimerax_command + chimerax_script += f"save {output_path} pixelSize {pixelsize} supersample 3{transparent_bg_suffix};exit" + + # Return as an argv list so the caller can use subprocess.run(..., shell=False) + # and avoid shell interpretation of paths/labels with spaces or metacharacters. + return [chimerax_bin, "--nogui", "--offscreen", "--silent", "--cmd", chimerax_script] def generate_chimerax_plot(output_dir, @@ -247,7 +253,7 @@ def generate_chimerax_plot(output_dir, pixelsize=pixel_size, transparent_bg=transparent_bg) _run_chimerax(chimerax_command, f"{gene} ({attribute})") - logger.debug(chimerax_command) + logger.debug(shlex.join(chimerax_command)) if attribute == "score" or attribute == "logscore": chimerax_command = get_chimerax_command(chimerax_bin, @@ -266,7 +272,7 @@ def generate_chimerax_plot(output_dir, pixelsize=pixel_size, transparent_bg=transparent_bg) _run_chimerax(chimerax_command, f"{gene} ({attribute}, clusters)") - logger.debug(chimerax_command) + logger.debug(shlex.join(chimerax_command)) else: tried_files = ', '.join(os.path.basename(path) for path in pdb_candidates) From 230c4262852c0c4f49aeacafffdf82a1150ab2a4 Mon Sep 17 00:00:00 2001 From: St3451 Date: Thu, 28 May 2026 18:17:15 +0200 Subject: [PATCH 5/7] feat: enhance robustness of _run_chimerax by redirecting stdout and capturing stderr --- scripts/plotting/chimerax_plot.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/plotting/chimerax_plot.py b/scripts/plotting/chimerax_plot.py index bc8580a..8bc4863 100644 --- a/scripts/plotting/chimerax_plot.py +++ b/scripts/plotting/chimerax_plot.py @@ -24,7 +24,14 @@ def _run_chimerax(command, label): `command` is an argv list (no shell interpretation), so paths and labels with spaces or shell metacharacters pass through verbatim. """ - result = subprocess.run(command, capture_output=True, text=True) + # Discard stdout (never read) but keep stderr for the failure warning; + # capture_output=True would buffer both into memory across many genes. + result = subprocess.run( + command, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + ) if result.returncode != 0: logger.warning( f"ChimeraX failed for {label} " From def4aae65160fb97bdde6142a0b23f8e71ffff58 Mon Sep 17 00:00:00 2001 From: St3451 Date: Thu, 28 May 2026 18:18:33 +0200 Subject: [PATCH 6/7] feat: enhance robustness of cap_inf_scores by preventing overflow when capping scores --- scripts/plotting/utils.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/plotting/utils.py b/scripts/plotting/utils.py index 20ed174..4951e71 100644 --- a/scripts/plotting/utils.py +++ b/scripts/plotting/utils.py @@ -42,7 +42,14 @@ def cap_inf_scores(pos_result, col="Score_obs_sim"): finite_values = pos_result.loc[~inf_mask, col] finite_max = finite_values.max() if not finite_values.empty else np.nan - cap = finite_max * 1.5 if pd.notna(finite_max) and finite_max > 0 else 1.0 + if pd.notna(finite_max) and finite_max > 0: + cap = finite_max * 1.5 + # Guard against finite_max being so large that *1.5 overflows back to inf + # (would defeat the purpose of capping). Fall back to finite_max itself. + if not np.isfinite(cap): + cap = finite_max + else: + cap = 1.0 n_inf = int(inf_mask.sum()) logger.warning( From 446e4189567cce17de56f41637735ced34aaf30b Mon Sep 17 00:00:00 2001 From: St3451 Date: Thu, 28 May 2026 18:33:53 +0200 Subject: [PATCH 7/7] feat: enhance robustness of _run_chimerax by handling OSError exceptions during execution --- scripts/plotting/chimerax_plot.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/plotting/chimerax_plot.py b/scripts/plotting/chimerax_plot.py index 8bc4863..193d2d9 100644 --- a/scripts/plotting/chimerax_plot.py +++ b/scripts/plotting/chimerax_plot.py @@ -26,12 +26,18 @@ def _run_chimerax(command, label): """ # Discard stdout (never read) but keep stderr for the failure warning; # capture_output=True would buffer both into memory across many genes. - result = subprocess.run( - command, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - text=True, - ) + try: + result = subprocess.run( + command, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + ) + except OSError as exc: + # E.g. FileNotFoundError when chimerax_bin is missing or not executable. + # Treat it like a failed run so the loop continues across other genes. + logger.warning(f"ChimeraX could not be executed for {label}: {exc}") + return None if result.returncode != 0: logger.warning( f"ChimeraX failed for {label} "