Create a blood cancer report of a unknown person
"multiple myeloma"[MeSH Terms] AND "diagnosis"[MeSH Terms]
mkdir -p /home/daytona/workspace/blood-cancer-report && ls /home/daytona/workspace/blood-cancer-report/
~/blood-cancer-report/generate_report.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm, mm
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY, TA_RIGHT
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.platypus import PageBreak
from reportlab.lib.colors import HexColor
import datetime
# ── Colour palette ──────────────────────────────────────────────────────────
DARK_BLUE = HexColor("#1a2e5a")
MID_BLUE = HexColor("#2c5282")
ACCENT_BLUE = HexColor("#3182ce")
LIGHT_BLUE = HexColor("#ebf4ff")
STEEL = HexColor("#4a5568")
LIGHT_GREY = HexColor("#f7fafc")
BORDER_GREY = HexColor("#cbd5e0")
RED = HexColor("#c53030")
AMBER = HexColor("#c05621")
GREEN = HexColor("#276749")
WHITE = colors.white
BLACK = colors.black
PAGE_W, PAGE_H = A4
MARGIN_L = 2.2 * cm
MARGIN_R = 2.2 * cm
MARGIN_T = 2.8 * cm
MARGIN_B = 2.5 * cm
OUTPUT = "/home/daytona/workspace/blood-cancer-report/Multiple_Myeloma_Clinical_Case_Report.pdf"
styles = getSampleStyleSheet()
# ── Custom paragraph styles ─────────────────────────────────────────────────
def make_style(name, parent="Normal", **kwargs):
s = ParagraphStyle(name, parent=styles[parent])
for k, v in kwargs.items():
setattr(s, k, v)
return s
cover_title = make_style("CoverTitle", fontSize=30, leading=38, textColor=WHITE,
spaceAfter=6, alignment=TA_CENTER, fontName="Helvetica-Bold")
cover_sub = make_style("CoverSub", fontSize=14, leading=20, textColor=HexColor("#bee3f8"),
spaceAfter=4, alignment=TA_CENTER)
cover_badge = make_style("CoverBadge", fontSize=11, leading=16, textColor=HexColor("#ebf8ff"),
spaceAfter=2, alignment=TA_CENTER)
h1 = make_style("H1", fontSize=14, leading=18, textColor=WHITE,
spaceBefore=0, spaceAfter=0, fontName="Helvetica-Bold",
alignment=TA_LEFT)
h2 = make_style("H2", fontSize=11, leading=15, textColor=DARK_BLUE,
spaceBefore=10, spaceAfter=3, fontName="Helvetica-Bold")
h3 = make_style("H3", fontSize=10, leading=14, textColor=MID_BLUE,
spaceBefore=6, spaceAfter=2, fontName="Helvetica-BoldOblique")
body = make_style("Body", fontSize=9.5, leading=14, textColor=STEEL,
spaceBefore=3, spaceAfter=3, alignment=TA_JUSTIFY)
body_bold = make_style("BodyBold", fontSize=9.5, leading=14, textColor=BLACK,
spaceBefore=3, spaceAfter=3, fontName="Helvetica-Bold")
bullet = make_style("Bullet", fontSize=9.5, leading=14, textColor=STEEL,
leftIndent=12, firstLineIndent=0, spaceBefore=1, spaceAfter=1,
bulletIndent=4)
label_style = make_style("Label", fontSize=8.5, leading=12, textColor=STEEL,
fontName="Helvetica-Bold")
value_style = make_style("Value", fontSize=9.5, leading=13, textColor=BLACK)
footer_style = make_style("Footer", fontSize=7.5, leading=10, textColor=BORDER_GREY,
alignment=TA_CENTER)
caption = make_style("Caption", fontSize=8, leading=11, textColor=STEEL,
alignment=TA_CENTER, fontName="Helvetica-Oblique")
disclaimer = make_style("Disclaimer", fontSize=8, leading=12, textColor=HexColor("#718096"),
alignment=TA_JUSTIFY)
# ── Helper builders ──────────────────────────────────────────────────────────
def section_header(title, color=DARK_BLUE):
"""Coloured banner row that acts as a section heading."""
tbl = Table([[Paragraph(title, h1)]], colWidths=[PAGE_W - MARGIN_L - MARGIN_R])
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), color),
("TOPPADDING", (0, 0), (-1, -1), 6),
("BOTTOMPADDING",(0, 0), (-1, -1), 6),
("LEFTPADDING", (0, 0), (-1, -1), 10),
("RIGHTPADDING", (0, 0), (-1, -1), 10),
("ROUNDEDCORNERS", [4]),
]))
return tbl
def info_table(rows, col_ratios=(0.38, 0.62)):
"""Two-column label/value table."""
usable = PAGE_W - MARGIN_L - MARGIN_R
widths = [usable * r for r in col_ratios]
data = [[Paragraph(lbl, label_style), Paragraph(val, value_style)] for lbl, val in rows]
tbl = Table(data, colWidths=widths, hAlign="LEFT")
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (0, -1), LIGHT_GREY),
("BACKGROUND", (1, 0), (1, -1), WHITE),
("GRID", (0, 0), (-1, -1), 0.4, BORDER_GREY),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]))
return tbl
def lab_table(headers, rows, flag_col=None):
"""Full-width styled lab results table."""
usable = PAGE_W - MARGIN_L - MARGIN_R
col_count = len(headers)
widths = [usable / col_count] * col_count
header_cells = [Paragraph(h, make_style(f"th{i}", fontSize=8.5, fontName="Helvetica-Bold",
textColor=WHITE, alignment=TA_CENTER)) for i, h in enumerate(headers)]
table_data = [header_cells]
for row in rows:
styled_row = []
for i, cell in enumerate(row):
flag = False
if flag_col and i == flag_col and cell not in ("", "—"):
flag = "H" in cell or "L" in cell or "↑" in cell or "↓" in cell or "*" in cell
cell_style = make_style(f"td_{i}", fontSize=9, leading=13,
textColor=RED if flag else STEEL, alignment=TA_CENTER)
styled_row.append(Paragraph(str(cell), cell_style))
table_data.append(styled_row)
tbl = Table(table_data, colWidths=widths, hAlign="LEFT", repeatRows=1)
style = TableStyle([
("BACKGROUND", (0, 0), (-1, 0), MID_BLUE),
("ROWBACKGROUNDS",(0, 1), (-1, -1), [WHITE, LIGHT_BLUE]),
("GRID", (0, 0), (-1, -1), 0.3, BORDER_GREY),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 5),
("RIGHTPADDING", (0, 0), (-1, -1), 5),
("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
])
tbl.setStyle(style)
return tbl
def highlight_box(text, bg=LIGHT_BLUE, border=ACCENT_BLUE):
"""Highlighted callout box."""
usable = PAGE_W - MARGIN_L - MARGIN_R
tbl = Table([[Paragraph(text, make_style("hb", fontSize=9.5, leading=14, textColor=DARK_BLUE,
alignment=TA_JUSTIFY))]],
colWidths=[usable])
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), bg),
("LEFTBORDERPADDING", (0, 0), (0, -1), 10),
("BOX", (0, 0), (-1, -1), 1.5, border),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
("LEFTPADDING", (0, 0), (-1, -1), 12),
("RIGHTPADDING", (0, 0), (-1, -1), 12),
]))
return tbl
def alert_box(text, bg=HexColor("#fff5f5"), border=RED, text_color=RED):
usable = PAGE_W - MARGIN_L - MARGIN_R
tbl = Table([[Paragraph(text, make_style("ab", fontSize=9.5, leading=14, textColor=text_color,
alignment=TA_JUSTIFY))]],
colWidths=[usable])
tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), bg),
("BOX", (0, 0), (-1, -1), 1.5, border),
("TOPPADDING", (0, 0), (-1, -1), 8),
("BOTTOMPADDING", (0, 0), (-1, -1), 8),
("LEFTPADDING", (0, 0), (-1, -1), 12),
("RIGHTPADDING", (0, 0), (-1, -1), 12),
]))
return tbl
def green_box(text):
return highlight_box(text, bg=HexColor("#f0fff4"), border=HexColor("#38a169"))
# ── Page callbacks ────────────────────────────────────────────────────────────
def on_page(canvas, doc):
"""Draw header stripe + footer on every page except the cover."""
page = canvas.getPageNumber()
canvas.saveState()
if page > 1:
# Thin top rule
canvas.setFillColor(DARK_BLUE)
canvas.rect(0, PAGE_H - 1.6*cm, PAGE_W, 1.6*cm, fill=1, stroke=0)
canvas.setFillColor(WHITE)
canvas.setFont("Helvetica-Bold", 9)
canvas.drawString(MARGIN_L, PAGE_H - 1.1*cm, "CLINICAL CASE REPORT | MULTIPLE MYELOMA")
canvas.setFont("Helvetica", 8)
canvas.drawRightString(PAGE_W - MARGIN_R, PAGE_H - 1.1*cm, f"Page {page}")
# Bottom footer
canvas.setFillColor(BORDER_GREY)
canvas.setFont("Helvetica-Oblique", 7)
canvas.drawCentredString(PAGE_W / 2, 1.2*cm,
"CONFIDENTIAL — For educational/clinical purposes only. Not a substitute for professional medical judgment.")
canvas.setStrokeColor(BORDER_GREY)
canvas.setLineWidth(0.5)
canvas.line(MARGIN_L, 1.6*cm, PAGE_W - MARGIN_R, 1.6*cm)
canvas.restoreState()
# ── Document content ──────────────────────────────────────────────────────────
def build():
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=MARGIN_L,
rightMargin=MARGIN_R,
topMargin=MARGIN_T + 0.4*cm, # extra space for header stripe
bottomMargin=MARGIN_B,
title="Multiple Myeloma – Clinical Case Report",
author="Orris Medical System",
subject="Haematology / Oncology",
)
story = []
sp = lambda n=1: Spacer(1, n * 0.35 * cm)
# ─────────────────────────────────────────────────────────────────────────
# COVER PAGE
# ─────────────────────────────────────────────────────────────────────────
usable = PAGE_W - MARGIN_L - MARGIN_R
# Full-width cover banner
cover_data = [[
Paragraph("CLINICAL CASE REPORT", cover_sub),
],[
Paragraph("Multiple Myeloma", cover_title),
],[
Paragraph("IgG-κ Type | Stage III (ISS) | High-Risk Cytogenetics", cover_badge),
],[
Paragraph("Haematology & Oncology Department", cover_badge),
]]
cover_tbl = Table(cover_data, colWidths=[usable])
cover_tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, -1), DARK_BLUE),
("TOPPADDING", (0, 0), (-1, -1), 10),
("BOTTOMPADDING", (0, 0), (-1, -1), 10),
("LEFTPADDING", (0, 0), (-1, -1), 20),
("RIGHTPADDING", (0, 0), (-1, -1), 20),
("ROUNDEDCORNERS", [6]),
]))
story.append(cover_tbl)
story.append(sp(2))
# Date / Institution row
meta_data = [
["Report Date:", "01 June 2026",
"Institution:", "University Teaching Hospital"],
["Case Reference:", "UKN-MM-2026-001",
"Treating Consultant:", "Dr. [Attending Physician]"],
["Speciality:", "Haematology / Oncology",
"Report Prepared by:", "Oncology MDT"],
]
meta_tbl = Table(meta_data,
colWidths=[usable*0.18, usable*0.32, usable*0.18, usable*0.32])
meta_tbl.setStyle(TableStyle([
("BACKGROUND", (0, 0), (0, -1), LIGHT_GREY),
("BACKGROUND", (2, 0), (2, -1), LIGHT_GREY),
("GRID", (0, 0), (-1, -1), 0.4, BORDER_GREY),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 8),
("RIGHTPADDING", (0, 0), (-1, -1), 8),
("FONTNAME", (0, 0), (0, -1), "Helvetica-Bold"),
("FONTNAME", (2, 0), (2, -1), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 8.5),
("TEXTCOLOR", (0, 0), (0, -1), STEEL),
("TEXTCOLOR", (2, 0), (2, -1), STEEL),
]))
story.append(meta_tbl)
story.append(sp(1.5))
# Disclaimer
story.append(highlight_box(
"<b>NOTICE:</b> This report concerns an <b>unknown / de-identified patient</b> and is produced "
"for <b>educational and illustrative purposes only</b>. All patient identifiers are fictional. "
"Clinical data are constructed to represent a typical presentation of Multiple Myeloma based on "
"current evidence-based guidelines (Goldman-Cecil Medicine, 26th ed.; Robbins & Kumar Basic "
"Pathology, 10th ed.; EHA-EMN Guidelines 2025).",
bg=HexColor("#fffbeb"), border=HexColor("#d69e2e")
))
story.append(sp(1.5))
# ─────────────────────────────────────────────────────────────────────────
# 1. PATIENT DEMOGRAPHICS
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("1. PATIENT DEMOGRAPHICS"))
story.append(sp(0.5))
demo_rows = [
("Patient ID", "UKN-MM-2026-001 (de-identified)"),
("Age", "67 years"),
("Sex", "Male"),
("Ethnicity", "African-American (note: incidence ~2× higher vs. White population)"),
("Occupation", "Retired industrial chemist (possible benzene exposure history)"),
("Date of Admission", "15 April 2026"),
("Presenting to", "Haematology Outpatient Clinic → Emergency Admission"),
("Referral Source", "GP / Primary Care Physician"),
("Next of Kin", "Spouse (consented to educational data use)"),
("Insurance / Payor", "Not applicable (anonymised)"),
]
story.append(info_table(demo_rows))
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 2. CHIEF COMPLAINT & PRESENTING HISTORY
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("2. CHIEF COMPLAINT & PRESENTING HISTORY"))
story.append(sp(0.5))
story.append(Paragraph("Chief Complaint", h2))
story.append(Paragraph(
"Severe back pain, progressive fatigue, and recurrent episodes of bacterial pneumonia "
"over the preceding 8 months.",
body))
story.append(Paragraph("History of Presenting Illness", h2))
story.append(Paragraph(
"The patient is a 67-year-old male who presented with an 8-month history of worsening "
"lumbar and thoracic back pain (rated 7/10, mechanical in character, worse at rest), "
"profound fatigue limiting daily activities, unintentional weight loss of approximately "
"8 kg, and two hospitalizations for community-acquired pneumonia within 6 months.",
body))
story.append(Paragraph(
"He noted increasing difficulty walking due to lower-limb weakness over the past 3 weeks. "
"On the morning of admission, he experienced acute worsening of back pain following a "
"minor fall at home, prompting emergency presentation.",
body))
story.append(Paragraph("Symptom Timeline", h2))
timeline_data = [
["Timeframe", "Symptom / Event"],
["~8 months prior", "Onset of back pain; GP prescribed analgesics"],
["~7 months prior", "First episode of community-acquired pneumonia (hospitalized)"],
["~5 months prior", "Progressive fatigue; blood tests ordered by GP — noted anaemia"],
["~4 months prior", "Second pneumonia; serum protein electrophoresis sent — M-spike noted"],
["~3 months prior", "Referred to haematology; serum-free light chain assay abnormal"],
["~3 weeks prior", "Lower-limb weakness onset; constipation; increased thirst (hypercalcaemia)"],
["Day of admission","Acute back pain after fall; imaging showed T8 vertebral compression fracture"],
]
t = Table(timeline_data, colWidths=[usable*0.28, usable*0.72])
t.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), MID_BLUE),
("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 8.5),
("ROWBACKGROUNDS",(0, 1), (-1, -1), [WHITE, LIGHT_BLUE]),
("GRID", (0, 0), (-1, -1), 0.3, BORDER_GREY),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 7),
("RIGHTPADDING", (0, 0), (-1, -1), 7),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]))
story.append(t)
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 3. PAST MEDICAL HISTORY
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("3. PAST MEDICAL HISTORY"))
story.append(sp(0.5))
pmh_rows = [
("Hypertension", "Diagnosed 12 years ago; on amlodipine 5 mg OD"),
("Type 2 Diabetes", "Diagnosed 9 years ago; HbA1c 7.2% — managed with metformin 1 g BD"),
("Hyperlipidaemia", "On atorvastatin 20 mg OD"),
("Monoclonal Gammopathy (MGUS)", "Detected incidentally 3 years prior; IgG-κ 1.2 g/dL; monitored annually — not treated"),
("Osteoporosis", "DEXA scan 2 years prior showed T-score −2.8 (spine); on calcium + vitamin D"),
("No prior malignancy", "No personal or family history of myeloma; no prior radiotherapy"),
("Surgical History", "Appendicectomy (age 32); right inguinal hernia repair (age 55)"),
("Allergies", "Penicillin — rash (documented). No other known drug allergies."),
]
story.append(info_table(pmh_rows))
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 4. PHYSICAL EXAMINATION
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("4. PHYSICAL EXAMINATION"))
story.append(sp(0.5))
story.append(Paragraph("Vital Signs", h2))
vitals_data = [
["Parameter", "Value", "Reference", "Flag"],
["Blood Pressure", "148/90 mmHg", "< 130/80", "↑ Elevated"],
["Heart Rate", "96 bpm", "60–100", "Normal"],
["Temperature", "37.8 °C", "36.1–37.2", "↑ Low-grade fever"],
["SpO₂ (room air)", "95%", "≥ 95%", "Borderline"],
["Respiratory Rate", "18/min", "12–20", "Normal"],
["Weight", "72 kg (↓8 kg over 8 mo)", "—", "↓ Significant loss"],
["Height", "175 cm", "—", "BMI 23.5"],
]
story.append(lab_table(vitals_data[0], vitals_data[1:], flag_col=3))
story.append(Paragraph("General Findings", h2))
exam_rows = [
("General", "Pale, fatigued-appearing male; in moderate distress due to back pain"),
("HEENT", "No lymphadenopathy; no mucosal pallor; no JVP elevation"),
("Respiratory", "Reduced breath sounds at right base; dull percussion — residual effusion (post-pneumonia)"),
("Cardiovascular", "Regular rate; no murmurs; peripheral pulses intact"),
("Abdomen", "Soft, non-tender; liver span 13 cm (borderline hepatomegaly); no splenomegaly"),
("Musculoskeletal", "Point tenderness over T8 and L3 vertebrae; kyphotic posture; restricted lumbar flexion"),
("Neurological", "Power 4/5 bilateral lower limbs; intact sensation; no bowel/bladder dysfunction currently"),
("Skin", "Pallor; no plasmacytomas or skin lesions noted"),
("Bone lesions", "No visible skull deformity; anterior chest wall tenderness over ribs bilaterally"),
]
story.append(info_table(exam_rows))
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 5. LABORATORY INVESTIGATIONS
# ─────────────────────────────────────────────────────────────────────────
story.append(PageBreak())
story.append(section_header("5. LABORATORY INVESTIGATIONS"))
story.append(sp(0.5))
story.append(Paragraph("5.1 Complete Blood Count (CBC)", h2))
cbc_headers = ["Test", "Result", "Reference Range", "Flag"]
cbc_rows = [
["Haemoglobin", "8.4 g/dL", "13.5–17.5 g/dL", "↓ LOW *"],
["MCV", "88 fL", "80–100 fL", "Normal"],
["WBC", "4.2 × 10⁹/L", "4.0–11.0 × 10⁹/L","Normal"],
["Platelets", "142 × 10⁹/L", "150–400 × 10⁹/L", "↓ LOW"],
["Reticulocyte count", "1.0%", "0.5–2.5%", "Normal"],
["Rouleaux formation", "Present", "Absent", "* ABNORMAL"],
]
story.append(lab_table(cbc_headers, cbc_rows, flag_col=3))
story.append(Paragraph("5.2 Comprehensive Metabolic Panel", h2))
cmp_headers = ["Test", "Result", "Reference Range", "Flag"]
cmp_rows = [
["Serum Calcium (corrected)", "12.8 mg/dL", "8.5–10.5 mg/dL", "↑ HIGH *"],
["Serum Creatinine", "2.1 mg/dL", "0.7–1.3 mg/dL", "↑ HIGH *"],
["eGFR", "31 mL/min", "≥ 60 mL/min", "↓ LOW *"],
["Total Protein", "10.2 g/dL", "6.0–8.3 g/dL", "↑ HIGH *"],
["Albumin", "2.9 g/dL", "3.5–5.0 g/dL", "↓ LOW"],
["LDH", "385 U/L", "135–225 U/L", "↑ HIGH"],
["Beta-2 Microglobulin", "7.4 mg/L", "< 2.5 mg/L", "↑ HIGH *"],
["Uric Acid", "8.1 mg/dL", "3.4–7.0 mg/dL", "↑ HIGH"],
["Serum Na / K", "136 / 4.2 mEq/L", "Normal ranges", "Normal"],
]
story.append(lab_table(cmp_headers, cmp_rows, flag_col=3))
story.append(Paragraph("5.3 Myeloma-Specific Laboratory Panel", h2))
mm_headers = ["Test", "Result", "Reference / Threshold", "Interpretation"]
mm_rows = [
["Serum Protein Electrophoresis (SPEP)", "M-spike 4.8 g/dL (IgG)", "No M-spike expected", "↑ ABNORMAL *"],
["Serum Immunofixation", "IgG-κ monoclonal band", "No monoclonal band", "POSITIVE *"],
["IgG level", "5,640 mg/dL", "700–1,600 mg/dL", "↑ HIGH *"],
["IgA level", "48 mg/dL", "70–400 mg/dL", "↓ LOW"],
["IgM level", "22 mg/dL", "40–230 mg/dL", "↓ LOW"],
["Serum Free Light Chains (κ)", "2,840 mg/L", "3.3–19.4 mg/L", "↑ ELEVATED *"],
["Serum Free Light Chains (λ)", "12 mg/L", "5.7–26.3 mg/L", "Normal"],
["κ/λ Ratio", "236.7", "0.26–1.65", "↑ ABNORMAL *"],
["24-hr Urine Protein", "2.2 g / 24 hr", "< 0.15 g / 24 hr", "↑ HIGH *"],
["Bence Jones Protein (urine IFE)", "Positive (κ light chains)", "Negative", "POSITIVE *"],
]
story.append(lab_table(mm_headers, mm_rows, flag_col=3))
story.append(sp(0.5))
story.append(highlight_box(
"<b>Key Point:</b> Serum protein electrophoresis detects an M protein in ~80% of myeloma patients. "
"Combined serum immunofixation and urine studies identify M protein in ~97% of cases. "
"The serum free light chain assay (κ/λ ratio) can also substitute for urine studies in the "
"diagnostic work-up. (Goldman-Cecil Medicine, 26th ed.)"
))
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 6. BONE MARROW BIOPSY
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("6. BONE MARROW BIOPSY & CYTOGENETICS"))
story.append(sp(0.5))
bm_rows = [
("Procedure", "Posterior iliac crest core biopsy + aspirate — 15 April 2026"),
("Plasma cell %", "38% of nucleated cells (clonal plasma cells) — markedly elevated (normal < 5%)"),
("Morphology", "Atypical plasma cells with prominent nucleoli, binucleated forms, and Russell bodies (cytoplasmic Ig inclusions)"),
("Immunohistochemistry", "CD138+, CD38+, CD45−, CD56+, κ light-chain restricted. CD20 negative (20% of MM express CD20)."),
("κ/λ IHC ratio", "κ:λ > 8:1 (confirms clonal κ population; normal 2–4:1)"),
("FISH Panel", "t(4;14)(p16;q32) positive — HIGH RISK cytogenetic abnormality; del(17p) detected in 22% of cells — HIGH RISK"),
("Conventional Karyotype", "46,XY, add(14)(q32) — complex karyotype; MYC rearrangement not detected"),
("CRAB Criteria met", "Hypercalcaemia (Ca 12.8), Renal failure (Cr 2.1), Anaemia (Hb 8.4), Bone lesions (vertebral + skull) — ALL FOUR present"),
]
story.append(info_table(bm_rows))
story.append(sp(0.5))
story.append(alert_box(
"⚠ HIGH-RISK CYTOGENETICS IDENTIFIED: t(4;14) and del(17p) are both independently associated "
"with significantly inferior progression-free and overall survival. Per IMWG/IMS 2025 Consensus "
"recommendations, this patient is classified as HIGH-RISK MULTIPLE MYELOMA. Intensified therapy "
"and closer monitoring protocols apply."
))
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 7. IMAGING
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("7. IMAGING STUDIES"))
story.append(sp(0.5))
img_rows = [
("Whole-body Low-dose CT\n(WBLDCT)",
"Multiple lytic ('punched-out') lesions: skull (3 lesions, largest 2.1 cm), ribs (bilateral), "
"T8 vertebral body compression fracture with 35% height loss, L3 lytic lesion, left proximal femur "
"cortical thinning. Pattern consistent with myeloma bone disease."),
("MRI Spine (Gadolinium)",
"T8 pathological compression fracture with posterior cortical bulge causing mild cord impingement "
"(no complete cord compression). T2 hyperintense signal in L1, L3, T10 vertebral bodies — diffuse "
"marrow infiltration pattern."),
("PET-CT (18F-FDG)",
"Diffuse skeletal uptake; SUVmax 4.8 at T8 lesion. No extramedullary disease or organ involvement "
"beyond skeletal system."),
("Chest X-Ray",
"Mild right-sided pleural effusion (resolving); no active consolidation; rib lucencies visible on "
"oblique views."),
("Echocardiogram",
"EF 58%; no cardiac amyloid features; mild LVH — attributed to hypertension."),
("Renal Ultrasound",
"Bilateral kidneys enlarged (right 13.2 cm, left 12.8 cm); increased echogenicity consistent with "
"myeloma nephropathy / cast nephropathy."),
]
story.append(info_table(img_rows, col_ratios=(0.28, 0.72)))
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 8. DIAGNOSIS & STAGING
# ─────────────────────────────────────────────────────────────────────────
story.append(PageBreak())
story.append(section_header("8. DIAGNOSIS & DISEASE STAGING"))
story.append(sp(0.5))
story.append(Paragraph("8.1 Formal Diagnosis", h2))
story.append(highlight_box(
"<b>CONFIRMED DIAGNOSIS:</b> Multiple Myeloma, IgG-κ type<br/>"
"Diagnostic criteria met (IMWG 2014 / Updated 2021): ≥ 10% clonal bone marrow plasma cells "
"(38% documented) PLUS M protein in serum (IgG 5,640 mg/dL; M-spike 4.8 g/dL) PLUS CRAB "
"features (all four: Hypercalcaemia, Renal insufficiency, Anaemia, Bone lesions).",
bg=HexColor("#ebf8ff"), border=DARK_BLUE
))
story.append(sp(0.5))
story.append(Paragraph("8.2 ISS (International Staging System)", h2))
iss_data = [
["ISS Criterion", "Patient Value", "Stage III Threshold", "Met?"],
["Serum Albumin", "2.9 g/dL", "< 3.5 g/dL", "Yes"],
["Beta-2 Microglobulin", "7.4 mg/L", "≥ 5.5 mg/L", "Yes"],
]
story.append(lab_table(iss_data[0], iss_data[1:], flag_col=3))
story.append(sp(0.3))
story.append(Paragraph(
"<b>ISS Stage:</b> <font color='#c53030'><b>III (High Risk)</b></font> — "
"β2-microglobulin ≥ 5.5 mg/L + serum albumin < 3.5 g/dL.",
make_style("iss_note", fontSize=9.5, textColor=DARK_BLUE)))
story.append(Paragraph("8.3 Revised ISS (R-ISS)", h2))
riss_data = [
["Component", "Finding", "Risk"],
["ISS Stage", "Stage III", "High"],
["t(4;14) by FISH", "Positive", "High-Risk Cytogenetic"],
["del(17p) by FISH", "Positive (22% of cells)", "High-Risk Cytogenetic"],
["Serum LDH", "385 U/L (> upper normal limit)", "Elevated"],
]
story.append(lab_table(riss_data[0], riss_data[1:], flag_col=2))
story.append(sp(0.3))
story.append(Paragraph(
"<b>R-ISS Stage: III</b> — Poor prognosis. Median OS approximately 33 months with conventional regimens; "
"significantly improved with quadruplet induction + ASCT in eligible patients.",
make_style("riss_note", fontSize=9.5, textColor=RED)))
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 9. PATHOPHYSIOLOGY SUMMARY
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("9. PATHOPHYSIOLOGY"))
story.append(sp(0.5))
story.append(Paragraph(
"Multiple myeloma originates from a malignant proliferation of terminally differentiated B cells "
"(plasma cells) in the bone marrow. Almost all cases evolve from a precursor monoclonal gammopathy "
"of undetermined significance (MGUS) — progressing at ~1% per year. This patient had a documented "
"MGUS (IgG-κ, 1.2 g/dL) 3 years prior.",
body))
story.append(Paragraph("Key Pathogenic Mechanisms", h2))
for item in [
("<b>Cytogenetic events:</b> t(4;14) fuses FGFR3/MMSET oncogenes to the IgH locus (14q32), "
"dysregulating cyclin D expression and driving plasma cell proliferation. del(17p) inactivates "
"TP53, a critical tumour suppressor, conferring genomic instability and chemo-resistance."),
("<b>IL-6 signalling:</b> Interleukin-6 from bone marrow stromal cells and fibroblasts is a "
"major myeloma cell growth factor. Autocrine and paracrine IL-6 loops sustain tumour survival."),
("<b>Bone destruction (CRAB – Bones):</b> Myeloma cells upregulate RANKL on stromal cells, "
"activating osteoclasts. Simultaneously, DKK1, IL-3, and IL-7 suppress osteoblasts → pure "
"osteolytic lesions with no reactive sclerosis, pathological fractures, and hypercalcaemia."),
("<b>Renal injury (CRAB – Renal):</b> κ Bence Jones proteins form obstructive casts in distal "
"tubules (myeloma cast nephropathy). Light chain deposition in glomeruli, hypercalcaemia-induced "
"nephrocalcinosis, and recurrent infections all compound renal damage."),
("<b>Immune suppression (CRAB – Anaemia / Infections):</b> Malignant plasma cells crowd out "
"normal haematopoiesis → normocytic anaemia. Functional immunoglobulin production is suppressed "
"despite elevated total protein → hypogammaglobulinaemia → recurrent bacterial infections."),
]:
story.append(Paragraph(f"• {item}", bullet))
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 10. TREATMENT PLAN
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("10. TREATMENT PLAN"))
story.append(sp(0.5))
story.append(Paragraph(
"The patient is a 67-year-old male with good performance status (ECOG 1), no major cardiac "
"comorbidity, and eGFR 31 mL/min. He is considered a <b>borderline transplant candidate</b>; "
"renal function recovery may improve eligibility. Treatment is guided by high-risk cytogenetics "
"and ISS III status.",
body))
story.append(Paragraph("10.1 Induction Therapy (Planned)", h2))
story.append(highlight_box(
"<b>Daratumumab + Bortezomib + Lenalidomide + Dexamethasone (Dara-VRd / D-VRd)</b><br/>"
"Quadruplet induction — 4 cycles (28-day cycles):<br/>"
"• Daratumumab 16 mg/kg IV (Days 1, 8, 15, 22 of cycles 1–2; then Days 1, 15 of cycles 3–4)<br/>"
"• Bortezomib 1.3 mg/m² SC (Days 1, 4, 8, 11)<br/>"
"• Lenalidomide 25 mg PO Days 1–21 (dose-reduced to 10 mg given eGFR 31)<br/>"
"• Dexamethasone 40 mg PO/IV weekly<br/>"
"<br/>"
"Rationale: High-risk cytogenetics [t(4;14) + del(17p)] warrant intensified upfront therapy. "
"Daratumumab-based quadruplets demonstrate superior MRD negativity rates and PFS vs. VRd triplet.",
bg=HexColor("#f0fff4"), border=HexColor("#276749")
))
story.append(Paragraph("10.2 Autologous Stem Cell Transplantation (ASCT)", h2))
story.append(Paragraph(
"Stem cell harvest planned after cycle 2 (mobilisation with G-CSF ± plerixafor). "
"If renal function improves (eGFR > 40), proceed to high-dose melphalan (200 mg/m² or "
"reduced-dose 140 mg/m²) conditioning followed by ASCT. ASCT remains standard of care for "
"eligible patients and improves event-free and overall survival vs. conventional chemotherapy alone. "
"(Goldman-Cecil Medicine, 26th ed.)",
body))
story.append(Paragraph("10.3 Maintenance Therapy", h2))
story.append(Paragraph(
"Post-ASCT: Lenalidomide maintenance (10 mg Days 1–21 of a 28-day cycle) until progression or "
"intolerance. For high-risk disease (del 17p), consider addition of bortezomib maintenance "
"(1.3 mg/m² every 2 weeks). Duration of maintenance: continuous until disease progression.",
body))
story.append(Paragraph("10.4 Supportive Care", h2))
supportive_data = [
["Issue", "Intervention"],
["Bone disease / pain", "Zoledronic acid 4 mg IV monthly (renally adjusted); vertebroplasty/kyphoplasty for T8 fracture if pain persists; orthopaedic referral for femur lesion"],
["Hypercalcaemia", "IV hydration (normal saline) + zoledronic acid; loop diuretic if fluid overloaded"],
["Renal failure", "Aggressive IV hydration; avoid NSAIDs and nephrotoxins; nephrology co-management; haemodialysis if renal function worsens"],
["Anaemia (Hb 8.4)", "Transfuse if symptomatic (target Hb > 9); erythropoiesis-stimulating agent consideration post-induction"],
["Infection prophylaxis","Aciclovir (VZV prophylaxis during bortezomib); co-trimoxazole (PCP prophylaxis); IVIG if recurrent severe infections"],
["DVT prophylaxis", "Aspirin or LMWH during lenalidomide therapy (IMiD-associated thrombosis risk)"],
["Neuropathy monitoring","Serial neurological assessments; reduce bortezomib dose/frequency if Grade ≥ 2 peripheral neuropathy"],
["Steroid side effects", "Gastric protection (PPI); blood glucose monitoring (known T2DM); bone protection"],
]
t2 = Table(supportive_data, colWidths=[usable*0.28, usable*0.72])
t2.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), MID_BLUE),
("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 8.5),
("ROWBACKGROUNDS",(0, 1), (-1, -1), [WHITE, LIGHT_BLUE]),
("GRID", (0, 0), (-1, -1), 0.3, BORDER_GREY),
("TOPPADDING", (0, 0), (-1, -1), 5),
("BOTTOMPADDING", (0, 0), (-1, -1), 5),
("LEFTPADDING", (0, 0), (-1, -1), 7),
("RIGHTPADDING", (0, 0), (-1, -1), 7),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]))
story.append(t2)
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 11. PROGNOSIS
# ─────────────────────────────────────────────────────────────────────────
story.append(PageBreak())
story.append(section_header("11. PROGNOSIS & MONITORING"))
story.append(sp(0.5))
story.append(Paragraph("Prognostic Assessment", h2))
prog_rows = [
("ISS Stage", "III — poor prognosis"),
("R-ISS Stage", "III — high-risk; estimated median OS ~33 months with triplet therapy"),
("Cytogenetics", "t(4;14) + del(17p) — dual high-risk; significantly inferior PFS and OS"),
("LDH", "Elevated (385 U/L) — adverse prognostic marker in R-ISS"),
("Renal function", "CKD Stage 3b (eGFR 31) — reversible component expected with hydration + treatment"),
("ECOG Performance Status", "1 — favourable for intensive treatment"),
("Age", "67 years — borderline for full-dose ASCT; feasible with careful patient selection"),
("MRD-negativity potential", "Target: MRD-negative state post-ASCT (associated with significantly improved outcomes)"),
]
story.append(info_table(prog_rows))
story.append(Paragraph("Response Monitoring Schedule", h2))
monitor_data = [
["Timepoint", "Assessment"],
["Every 28 days (on-treatment)",
"CBC, metabolic panel, SPEP, serum IFE, free light chains, urine protein"],
["After cycle 2", "Response assessment per IMWG criteria; stem cell mobilisation if CR/VGPR"],
["After cycle 4", "Pre-ASCT staging: SPEP, BM biopsy, MRD by next-generation flow or NGS-MRD"],
["Day +100 post-ASCT", "Response assessment, MRD, bone survey, quality-of-life evaluation"],
["Every 3 months (maintenance)", "SPEP + IFE; free light chains; CBC; renal/calcium panel"],
["Annually", "Bone survey / PET-CT; echocardiogram; DEXA scan; full restaging if clinical change"],
["At progression", "Repeat BM biopsy with FISH panel; consider next-generation therapy"],
]
t3 = Table(monitor_data, colWidths=[usable*0.3, usable*0.7])
t3.setStyle(TableStyle([
("BACKGROUND", (0, 0), (-1, 0), MID_BLUE),
("TEXTCOLOR", (0, 0), (-1, 0), WHITE),
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
("FONTSIZE", (0, 0), (-1, -1), 8.5),
("ROWBACKGROUNDS",(0, 1), (-1, -1), [WHITE, LIGHT_BLUE]),
("GRID", (0, 0), (-1, -1), 0.3, BORDER_GREY),
("TOPPADDING", (0, 0), (-1, -1), 4),
("BOTTOMPADDING", (0, 0), (-1, -1), 4),
("LEFTPADDING", (0, 0), (-1, -1), 7),
("RIGHTPADDING", (0, 0), (-1, -1), 7),
("VALIGN", (0, 0), (-1, -1), "TOP"),
]))
story.append(t3)
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 12. MDT DISCUSSION & PLAN
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("12. MDT DISCUSSION & SUMMARY PLAN"))
story.append(sp(0.5))
story.append(Paragraph(
"This case was discussed at the Haematology-Oncology MDT on 18 April 2026. The following "
"consensus management plan was agreed:",
body))
plan_items = [
"Confirm diagnosis — DONE (BM biopsy, FISH, SPEP, IFE, FLC all consistent with IgG-κ MM)",
"Urgent spinal MRI completed — T8 cord impingement identified; neurosurgery alerted",
"Orthopaedic input for T8 kyphoplasty — planned after 2 cycles of induction (tumour debulking first)",
"Nephrology co-management — aggressive IV hydration commenced; monitor for cast nephropathy recovery",
"Initiate D-VRd quadruplet induction (daratumumab + bortezomib + lenalidomide + dexamethasone)",
"Lenalidomide dose adjusted to 10 mg (renal impairment: eGFR 31 mL/min)",
"VZV, PCP, and antifungal prophylaxis commenced",
"Bone protection: zoledronic acid (dose-adjusted) — first infusion after hydration/renal stabilisation",
"Social work referral — home adaptations, pain management, palliative support discussion",
"Patient and family counselled re: diagnosis, high-risk status, treatment goals, and clinical trial options",
"ISRCTN clinical trial eligibility reviewed — patient may qualify for emerging CAR-T or bispecific Ab trial",
"Follow-up: 2 weeks post-cycle 1 for tolerability review + blood tests",
]
for i, item in enumerate(plan_items, 1):
story.append(Paragraph(f"{i}. {item}", bullet))
story.append(sp(1))
# ─────────────────────────────────────────────────────────────────────────
# 13. REFERENCES
# ─────────────────────────────────────────────────────────────────────────
story.append(section_header("13. REFERENCES & EVIDENCE BASE", color=STEEL))
story.append(sp(0.5))
refs = [
("1", "Goldman L, Cooney KA (eds). Goldman-Cecil Medicine, 26th Edition. Elsevier, 2020. "
"Chapter 173: Plasma Cell Disorders (Multiple Myeloma)."),
("2", "Kumar V, Abbas A, Aster J. Robbins & Kumar Basic Pathology, 10th Edition. Elsevier, 2022. "
"Chapter 10: Lymphoid Neoplasms — Multiple Myeloma."),
("3", "Tietz Textbook of Laboratory Medicine, 7th Edition. Elsevier, 2022. Chapter 30: Plasma Cell "
"Disorders and Cytogenetics."),
("4", "Dimopoulos MA, Terpos E, Boccadoro M, et al. EHA-EMN Evidence-Based Guidelines for diagnosis, "
"treatment and follow-up of patients with multiple myeloma. Nat Rev Clin Oncol. 2025 Sep. "
"PMID: 40624367."),
("5", "Avet-Loiseau H, Davies FE, Samur MK, et al. IMS/IMWG Consensus Recommendations on the "
"Definition of High-Risk Multiple Myeloma. J Clin Oncol. 2025 Aug 20. PMID: 40489728."),
("6", "Clarke SE, Fuller KA, Erber WN. Chromosomal defects in multiple myeloma. Blood Rev. "
"2024 Mar. PMID: 38212176."),
("7", "Gay F, Marchetti E, Bertuglia G, et al. Multiple Myeloma Unpacked. Hematol Oncol. "
"2025 Jun. PMID: 40517540."),
("8", "IMWG Diagnostic Criteria for Multiple Myeloma (Updated 2021): Rajkumar SV, et al. "
"International Myeloma Working Group updated criteria for the diagnosis of multiple myeloma. "
"Lancet Oncol. 2014;15(12):e538-e548."),
]
ref_tbl_data = [[Paragraph(f"[{n}]", label_style),
Paragraph(text, make_style(f"ref{n}", fontSize=8.5, leading=13, textColor=STEEL))]
for n, text in refs]
ref_tbl = Table(ref_tbl_data, colWidths=[usable*0.07, usable*0.93])
ref_tbl.setStyle(TableStyle([
("VALIGN", (0, 0), (-1, -1), "TOP"),
("TOPPADDING", (0, 0), (-1, -1), 3),
("BOTTOMPADDING", (0, 0), (-1, -1), 3),
("LEFTPADDING", (0, 0), (-1, -1), 4),
]))
story.append(ref_tbl)
story.append(sp(1.5))
# Final disclaimer
story.append(HRFlowable(width="100%", thickness=0.5, color=BORDER_GREY))
story.append(sp(0.5))
story.append(Paragraph(
"DISCLAIMER: This document is an illustrative clinical case report for a de-identified / unknown patient, "
"created for educational purposes only. Drug doses, laboratory values, and clinical details have been "
"constructed to represent a typical high-risk Multiple Myeloma presentation in alignment with published "
"guidelines. This document does not constitute medical advice and must not be used to guide real patient "
"care without independent clinical assessment by a qualified haematologist. Report generated by Orris "
"Medical AI — 01 June 2026.",
disclaimer))
# ─────────────────────────────────────────────────────────────────────────
# BUILD
# ─────────────────────────────────────────────────────────────────────────
doc.build(story, onFirstPage=on_page, onLaterPages=on_page)
print(f"PDF saved to: {OUTPUT}")
build()
cd /home/daytona/workspace/blood-cancer-report && python generate_report.py 2>&1
ls -lh /home/daytona/workspace/blood-cancer-report/Multiple_Myeloma_Clinical_Case_Report.pdf
Multiple Myeloma Clinical Case Report
PDF Document · PDF
| Section | Contents |
|---|---|
| Patient Demographics | Age, sex, ethnicity, admission info, referral source |
| Chief Complaint & History | 8-month symptom timeline from back pain to fracture |
| Past Medical History | HTN, T2DM, prior MGUS, osteoporosis, allergies |
| Physical Examination | Vital signs table + system-by-system findings |
| Lab Investigations | CBC, metabolic panel, full myeloma-specific panel (SPEP, IFE, FLC, κ/λ ratio, Bence Jones) with colour-coded flagging |
| Bone Marrow Biopsy | 38% plasma cells, CD138+/CD38+, FISH: t(4;14) + del(17p) - high risk |
| Imaging | WBLDCT lytic lesions, MRI spine T8 fracture, PET-CT, echo, renal USS |
| Diagnosis & Staging | IMWG criteria, ISS III, R-ISS III with rationale |
| Pathophysiology | RANKL/OPG axis, IL-6, TP53 deletion, renal cast nephropathy |
| Treatment Plan | D-VRd quadruplet induction + ASCT + lenalidomide maintenance + full supportive care table |
| Prognosis & Monitoring | Response criteria, MRD targets, surveillance schedule |
| MDT Plan | 12-point consensus management plan |
| References | 8 sources including EHA-EMN 2025 guidelines and IMWG 2025 consensus (PMID cited) |