#!/usr/bin/env python3 import pandas as pd import numpy as np import html import argparse from pathlib import Path ############################################################################### # COLOR HELPERS ############################################################################### def color_scale(value, vmin, vmax, invert=False): if pd.isna(value): return "#ffffff" if abs(vmax - vmin) < 1e-9: return "rgba(0,255,0,0.35)" # All same → green x = (value - vmin) / (vmax - vmin + 1e-9) x = max(0.0, min(1.0, x)) if invert: x = 1.0 - x r = int(255 * (1 - x)) g = int(255 * x) return f"rgba({r},{g},0,0.35)" ############################################################################### # MAIN TABLE FORMATTER ############################################################################### def dataframe_to_colored_html(df): """ Flexible: If AF2 columns exist → include them. Otherwise show seq-only metrics. """ # if "scfvtools_score" in df.columns: # df = df.sort_values("scfvtools_score", ascending=False) preferred_order = [ "Accession", "pLDDT", "pAE_mean", "pTM", "ipTM", "scfvtools_score", # moved up "scfvtools_blosum_score", "scfvtools_blosum_diff_score", "Prob. of Solubility", # moved down "log_likelihood_target", "log_likelihood" ] # Keep only columns present cols = [c for c in preferred_order if c in df.columns] df = df[cols] numeric_cols = df.select_dtypes(include=[np.number]).columns col_ranges = { col: (df[col].min(), df[col].max()) for col in numeric_cols } # Build HTML rows = [] header = "".join( f"{html.escape(col)}" for col in df.columns ) rows.append(f"{header}") for _, row in df.iterrows(): cells = [] for col, value in row.items(): text = html.escape(str(value)) if col in numeric_cols: inv = ("pae" in col.lower()) vmin, vmax = col_ranges[col] bg = color_scale(value, vmin, vmax, invert=inv) cells.append(f"{text}") else: cells.append(f"{text}") rows.append(f"{''.join(cells)}") return ( "" + "".join(rows) + "
" ) ############################################################################### # SMALL TABLE FORMATTER FOR TOP / BOTTOM / RANDOM ############################################################################### def simple_table(csv_path, title): df = pd.read_csv(csv_path) return ( f"

{html.escape(title)}

\n" + df.to_html(index=False, escape=True) ) ############################################################################### # MAIN APPENDER ############################################################################### def append_scores_to_summary(main_csv, summary_html, out=None, top_csv=None, bottom_csv=None, random_csv=None): df = pd.read_csv(main_csv) main_table = dataframe_to_colored_html(df) block = ( "
" "
Design Ranking Scores
" "
" f"{main_table}" "
" "
\n" ) # # Add stacked Top / Bottom / Random if present # if top_csv and Path(top_csv).exists(): # block += simple_table(top_csv, "Top Designs") + "\n" # if bottom_csv and Path(bottom_csv).exists(): # block += simple_table(bottom_csv, "Bottom Designs") + "\n" # if random_csv and Path(random_csv).exists(): # block += simple_table(random_csv, "Random Designs") + "\n" # Inject block before html_text = Path(summary_html).read_text() new_html = html_text.replace("", block + "") out_path = out or summary_html Path(out_path).write_text(new_html) print(f"[append_scores_to_summary] Added tables → {out_path}") ############################################################################### # CLI ############################################################################### if __name__ == "__main__": ap = argparse.ArgumentParser() ap.add_argument("--csv", required=True, help="Main score table (AF2 merged or pre-AF2)") ap.add_argument("--summary", required=True) ap.add_argument("--out", default=None) ap.add_argument("--top") ap.add_argument("--bottom") ap.add_argument("--random") args = ap.parse_args() append_scores_to_summary( args.csv, args.summary, args.out, top_csv=args.top, bottom_csv=args.bottom, random_csv=args.random )