"""Downloadable candidate and variant reports.""" from io import BytesIO from datetime import datetime from reportlab.lib import colors from reportlab.lib.enums import TA_CENTER, TA_LEFT from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import ParagraphStyle from reportlab.lib.units import cm from reportlab.platypus import ( HRFlowable, Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle, ) NAVY = colors.HexColor("#1B365D") TEAL = colors.HexColor("#0E7C7B") LIGHT = colors.HexColor("#F4F7FB") WHITE = colors.white TEXT = colors.HexColor("#111827") MUTED = colors.HexColor("#4A5568") GREEN = colors.HexColor("#2F855A") RED = colors.HexColor("#C53030") ORANGE = colors.HexColor("#C45C26") def _styles(): return { "title": ParagraphStyle("t", fontName="Helvetica-Bold", fontSize=16, textColor=NAVY, alignment=TA_CENTER), "sub": ParagraphStyle("s", fontName="Helvetica", fontSize=9, textColor=colors.HexColor("#D6E0F0"), alignment=TA_CENTER), "h": ParagraphStyle("h", fontName="Helvetica-Bold", fontSize=11, textColor=NAVY, spaceBefore=10, spaceAfter=4), "b": ParagraphStyle("b", fontName="Helvetica", fontSize=9, textColor=TEXT, leading=12), "small": ParagraphStyle("sm", fontName="Helvetica", fontSize=8, textColor=MUTED, leading=11), } def _header_footer(canvas, doc): canvas.saveState() canvas.setFillColor(NAVY) canvas.rect(0, A4[1] - 52, A4[0], 52, fill=1, stroke=0) canvas.setFillColor(TEAL) canvas.rect(0, 0, A4[0], 22, fill=1, stroke=0) canvas.setFillColor(WHITE) canvas.setFont("Helvetica", 8) canvas.drawString(1.6 * cm, 8, "TP53 Mutant Discovery Platform | Computational prioritization, not clinical proof") canvas.restoreState() def generate_variant_report(variant_row, candidates, structure_row=None, pocket_row=None) -> bytes: styles = _styles() buf = BytesIO() doc = SimpleDocTemplate( buf, pagesize=A4, leftMargin=1.6 * cm, rightMargin=1.6 * cm, topMargin=2.4 * cm, bottomMargin=1.4 * cm, ) hgvs = variant_row.get("hgvs_p", "") story = [ Spacer(1, 6), Paragraph("TP53 Variant-to-Compound Discovery Report", styles["title"]), Paragraph(f"Project: Lung Cancer · Variant {hgvs} · {datetime.utcnow():%Y-%m-%d}", styles["sub"]), Spacer(1, 28), Paragraph("Mutation analysis", styles["h"]), Paragraph( f"{hgvs}  |  Exon {variant_row.get('exon','')}  |  " f"{variant_row.get('domain','')}  |  Inferred type: {variant_row.get('type_inferred','')}
" f"Source type: {variant_row.get('type_source','')}  |  Effect: {variant_row.get('effect_source','')}
" f"Allele frequency (observation): {variant_row.get('allele_frequency','')}  |  " f"QC: {variant_row.get('qc_flags','')}  |  Route: {variant_row.get('route','')}
" f"Priority score: {variant_row.get('priority_score','')}  |  " f"Hotspot: {variant_row.get('hotspot','')}", styles["b"], ), ] if structure_row is not None: story += [ Paragraph("Wild-type versus mutant structure", styles["h"]), Paragraph( f"ΔΔG proxy: {structure_row.get('ddg_kcal','')} kcal/mol  |  " f"Cα RMSD: {structure_row.get('ca_rmsd_A','')} Å  |  " f"Local RMSD: {structure_row.get('local_rmsd_A','')} Å
" f"Pocket volume WT→mut: {structure_row.get('pocket_vol_wt','')} → {structure_row.get('pocket_vol_mut','')} ų  |  " f"Structure quality: {structure_row.get('structure_quality','')}", styles["b"], ), ] if pocket_row is not None: story += [ Paragraph("Primary pocket", styles["h"]), Paragraph( f"{pocket_row.get('pocket_name','')} ({pocket_row.get('pocket_id','')})  |  " f"Druggability {pocket_row.get('druggability','')}  |  " f"Docking gate: {pocket_row.get('docking_gate','')}  |  " f"Volume Δ {pocket_row.get('volume_delta','')} ų", styles["b"], ), ] story.append(Paragraph("Top ranked candidates", styles["h"])) header = ["Rank", "Compound", "Status", "Dock mut", "Dock WT", "Sel.", "MD", "ADMET", "Score", "Rec."] data = [header] use = candidates.sort_values("rank").head(10) for _, r in use.iterrows(): data.append( [ str(int(r["rank"])), str(r["name"])[:18], str(r["status"])[:10], f"{r['dock_mut']:.1f}", f"{r['dock_wt']:.1f}", f"{r['Sselectivity']:.2f}", f"{r['MDstability']:.2f}", f"{r['ADMET']:.2f}", f"{r['rescue_score']:.2f}", str(r["recommendation"]), ] ) tbl = Table(data, repeatRows=1) tbl.setStyle( TableStyle( [ ("BACKGROUND", (0, 0), (-1, 0), NAVY), ("TEXTCOLOR", (0, 0), (-1, 0), WHITE), ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), ("FONTSIZE", (0, 0), (-1, -1), 7), ("BACKGROUND", (0, 1), (-1, -1), LIGHT), ("GRID", (0, 0), (-1, -1), 0.3, colors.HexColor("#D1D5DB")), ("ALIGN", (0, 0), (-1, -1), "CENTER"), ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), ("TOPPADDING", (0, 0), (-1, -1), 4), ("BOTTOMPADDING", (0, 0), (-1, -1), 4), ] ) ) story += [tbl, Spacer(1, 8)] if len(use): top = use.iloc[0] story += [ Paragraph("AI recommendation", styles["h"]), Paragraph( f"Top candidate {top['name']}  |  Rescue/Opportunity Score {top['rescue_score']:.2f} " f" |  Confidence {top['confidence']:.2f}
" f"Recommendation: {top['recommendation']}  |  Next: {top['next_experiment']}
" f"Reason codes: {top['reason_codes']}", styles["b"], ), ] story += [ Paragraph("Disclaimer", styles["h"]), Paragraph( "Scores are multi-objective computational estimates for prioritization. " "They are not proof of binding, rescue, or clinical efficacy. Experimental assays remain the validation layer.", styles["small"], ), ] # dummy title overlay: we already drew header bar in canvas; put title into flow with white space doc.build(story, onFirstPage=_header_footer, onLaterPages=_header_footer) return buf.getvalue()