"""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()