| """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"<b>{hgvs}</b> | Exon {variant_row.get('exon','')} | " |
| f"{variant_row.get('domain','')} | Inferred type: {variant_row.get('type_inferred','')}<br/>" |
| f"Source type: {variant_row.get('type_source','')} | Effect: {variant_row.get('effect_source','')}<br/>" |
| f"Allele frequency (observation): {variant_row.get('allele_frequency','')} | " |
| f"QC: {variant_row.get('qc_flags','')} | Route: {variant_row.get('route','')}<br/>" |
| 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','')} Å<br/>" |
| 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 <b>{top['name']}</b> | Rescue/Opportunity Score {top['rescue_score']:.2f} " |
| f" | Confidence {top['confidence']:.2f}<br/>" |
| f"Recommendation: <b>{top['recommendation']}</b> | Next: {top['next_experiment']}<br/>" |
| 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"], |
| ), |
| ] |
| |
| doc.build(story, onFirstPage=_header_footer, onLaterPages=_header_footer) |
| return buf.getvalue() |
|
|