What the points to observed in case of testing of fsh/ lh in pcos .
FSH LH ratio PCOS diagnosis
"All other frequently encountered manifestations offer less consistent findings and therefore qualify only as minor diagnostic criteria for PCOS. They include elevated LH-to-FSH ratio, IR, perimenarchal onset of hirsutism, and obesity."
- Berek & Novak's Gynecology, p. 1920
| Parameter | Recommendation |
|---|---|
| Day of cycle | Measure FSH and LH on Day 2 or Day 3 of the menstrual cycle (early follicular phase) |
| FSH Day 3 | Baseline FSH - elevated levels (>10 mIU/mL) suggest diminished ovarian reserve rather than PCOS |
| LH pulse variability | LH is secreted in pulses - a single random measurement may not be reliable |
| Fasting state | Preferred, especially if concurrent metabolic tests are ordered |
"Given the pulsatility of LH secretion, a random serum LH/FSH ratio is not included in the diagnostic criteria."
- Harrison's Principles 22E, p. 3178
"Lean oligo-ovulatory patients with PCOS generally have high LH levels in the presence of normal to low levels of FSH and estradiol" - Harrison's 22E
| Condition | FSH | LH |
|---|---|---|
| Primary ovarian failure/POI | Very HIGH | Very HIGH |
| Hypothalamic amenorrhea | Low or normal | Low or normal |
| PCOS (typical lean) | Normal / low-normal | ELEVATED (>LH:FSH >2-3) |
| Pituitary tumor | Variable | Variable |
| Point | Clinical Relevance |
|---|---|
| LH:FSH ratio >2-3 | Supportive but not diagnostic on its own |
| Measure on Day 2-3 | Baseline follicular phase is the standard timing |
| LH pulsatility | Single random value is unreliable - ratio may vary |
| Obese PCOS | May have normal LH:FSH - do not rule out PCOS on this alone |
| Elevated FSH + LH | Points to ovarian failure, NOT PCOS |
| Urinary LH kits unreliable | Baseline LH elevation causes false positives in PCOS |
| FSH/LH is for exclusion | Not a primary diagnostic criterion per Rotterdam 2023 |
Create a quick reference PDF on PCOS diagnostic workup including all hormone tests
mkdir -p /home/daytona/workspace/pcos-pdf && ls /home/daytona/workspace/
~/pcos-pdf/generate_pcos_pdf.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_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.platypus import PageBreak
from reportlab.lib.colors import HexColor
OUTPUT = "/home/daytona/workspace/pcos-pdf/PCOS_Diagnostic_Workup_Quick_Reference.pdf"
# ── Color palette ──────────────────────────────────────────────
C_TEAL = HexColor("#1A6B72") # header bar
C_TEAL_LT = HexColor("#D6EEF0") # section header bg
C_PURPLE = HexColor("#5B2C8D") # accent headings
C_PURPLE_LT = HexColor("#EDE7F6") # alternating row
C_GOLD = HexColor("#C8860A") # warning / note highlight
C_GOLD_LT = HexColor("#FFF8E1") # note box bg
C_RED = HexColor("#C0392B") # critical flags
C_GREEN = HexColor("#1E7D45") # normal range
C_GREY_BG = HexColor("#F4F6F8") # table alt row
C_GREY_LINE = HexColor("#B0BEC5") # grid lines
C_WHITE = colors.white
C_BLACK = HexColor("#1A1A2E")
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
topMargin=1.5*cm, bottomMargin=1.5*cm,
leftMargin=1.8*cm, rightMargin=1.8*cm
)
W = A4[0] - 3.6*cm # usable width
styles = getSampleStyleSheet()
# Custom styles
def ps(name, **kw):
base = kw.pop("parent", "Normal")
s = ParagraphStyle(name, parent=styles[base], **kw)
return s
TITLE = ps("TITLE", fontSize=22, textColor=C_WHITE, alignment=TA_CENTER, fontName="Helvetica-Bold", leading=28)
SUBTITLE= ps("SUBTITLE",fontSize=11, textColor=HexColor("#B2EBF2"), alignment=TA_CENTER, fontName="Helvetica", leading=16)
SEC_HDR = ps("SEC_HDR", fontSize=11, textColor=C_WHITE, alignment=TA_LEFT, fontName="Helvetica-Bold", leading=15, spaceBefore=6, spaceAfter=4)
BODY = ps("BODY", fontSize=9, textColor=C_BLACK, alignment=TA_JUSTIFY,fontName="Helvetica", leading=13, spaceBefore=2)
BOLD9 = ps("BOLD9", fontSize=9, textColor=C_BLACK, fontName="Helvetica-Bold", leading=13)
SMALL = ps("SMALL", fontSize=8, textColor=HexColor("#555555"), fontName="Helvetica", leading=12)
NOTE = ps("NOTE", fontSize=8.5,textColor=HexColor("#5D4037"),fontName="Helvetica-Oblique", leading=12)
CELL_H = ps("CELL_H", fontSize=8.5,textColor=C_WHITE, fontName="Helvetica-Bold", alignment=TA_CENTER, leading=12)
CELL = ps("CELL", fontSize=8.5,textColor=C_BLACK, fontName="Helvetica", alignment=TA_LEFT, leading=12)
CELL_C = ps("CELL_C", fontSize=8.5,textColor=C_BLACK, fontName="Helvetica", alignment=TA_CENTER, leading=12)
CELL_B = ps("CELL_B", fontSize=8.5,textColor=C_BLACK, fontName="Helvetica-Bold", alignment=TA_LEFT, leading=12)
FOOT = ps("FOOT", fontSize=7.5,textColor=HexColor("#757575"), alignment=TA_CENTER, fontName="Helvetica-Oblique", leading=10)
story = []
# ── Helper: section header banner ─────────────────────────────
def sec(title, color=C_TEAL):
tbl = Table([[Paragraph(f" {title}", SEC_HDR)]], colWidths=[W])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("ROWBACKGROUNDS",(0,0),(-1,-1),[color]),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),8),
("RIGHTPADDING",(0,0),(-1,-1),8),
]))
return tbl
def note_box(text, bg=C_GOLD_LT, border=C_GOLD):
tbl = Table([[Paragraph(text, NOTE)]], colWidths=[W])
tbl.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1), bg),
("BOX",(0,0),(-1,-1),1, border),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),8),
("RIGHTPADDING",(0,0),(-1,-1),8),
]))
return tbl
def std_table(headers, rows, col_widths, alt=True):
data = [[Paragraph(h, CELL_H) for h in headers]]
for i, row in enumerate(rows):
bg = C_GREY_BG if (alt and i % 2 == 1) else C_WHITE
data.append([Paragraph(str(c), CELL) for c in row])
tbl = Table(data, colWidths=col_widths, repeatRows=1)
style = [
("BACKGROUND",(0,0),(-1,0), C_TEAL),
("GRID",(0,0),(-1,-1),0.4, C_GREY_LINE),
("ROWBACKGROUNDS",(0,1),(-1,-1),[C_WHITE, C_GREY_BG]),
("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(TableStyle(style))
return tbl
# ═══════════════════════════════════════════════════════════════
# TITLE BLOCK
# ═══════════════════════════════════════════════════════════════
title_data = [[Paragraph("PCOS Diagnostic Workup", TITLE)],
[Paragraph("Hormone Tests · Quick Reference Guide", SUBTITLE)],
[Paragraph("Based on Rotterdam 2003 / Updated 2023 International Guidelines | Harrison's 22E · Berek & Novak · Henry's Clinical Lab Methods", SUBTITLE)]]
title_tbl = Table(title_data, colWidths=[W])
title_tbl.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1), C_TEAL),
("TOPPADDING",(0,0),(-1,0),14),
("BOTTOMPADDING",(0,-1),(-1,-1),12),
("TOPPADDING",(0,1),(-1,1),4),
("BOTTOMPADDING",(0,0),(-1,1),2),
("TOPPADDING",(0,2),(-1,2),2),
("LINEBELOW",(0,1),(-1,1),0.5, HexColor("#80CBC4")),
]))
story.append(title_tbl)
story.append(Spacer(1, 6))
# ═══════════════════════════════════════════════════════════════
# SECTION 1 – DIAGNOSTIC CRITERIA (Rotterdam 2023)
# ═══════════════════════════════════════════════════════════════
story.append(sec("1. DIAGNOSTIC CRITERIA — Rotterdam 2023 (2 of 3 required, after exclusion of other causes)"))
story.append(Spacer(1, 4))
crit_data = [
[Paragraph("Criterion", CELL_H), Paragraph("Details", CELL_H), Paragraph("Notes", CELL_H)],
[Paragraph("<b>1. Menstrual Irregularity</b>", CELL),
Paragraph("Oligomenorrhea (<8 cycles/yr) or amenorrhea", CELL),
Paragraph("In adolescents: wait ≥3 yrs post-menarche before diagnosing", CELL)],
[Paragraph("<b>2. Hyperandrogenism</b>", CELL),
Paragraph("Clinical (hirsutism, acne, alopecia) OR biochemical (↑ total/free testosterone)", CELL),
Paragraph("mFG score ≥4–6 depending on ethnicity", CELL)],
[Paragraph("<b>3. Polycystic Ovarian Morphology</b>", CELL),
Paragraph("≥20 antral follicles or ovarian volume ≥10 cm³ on USS OR elevated AMH", CELL),
Paragraph("Ultrasound criteria NOT used in adolescents", CELL)],
]
crit_tbl = Table(crit_data, colWidths=[3.8*cm, 8.2*cm, W-12*cm])
crit_tbl.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0), C_TEAL),
("GRID",(0,0),(-1,-1),0.4, C_GREY_LINE),
("ROWBACKGROUNDS",(0,1),(-1,-1),[C_WHITE, C_GREY_BG]),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),5),
("RIGHTPADDING",(0,0),(-1,-1),5),
("VALIGN",(0,0),(-1,-1),"TOP"),
]))
story.append(crit_tbl)
story.append(Spacer(1, 4))
story.append(note_box("⚠ PCOS is a DIAGNOSIS OF EXCLUSION. Always rule out: hypothyroidism, hyperprolactinemia, congenital adrenal hyperplasia (CAH), Cushing syndrome, androgen-secreting tumors, and primary ovarian insufficiency (POI) before confirming PCOS."))
story.append(Spacer(1, 8))
# ═══════════════════════════════════════════════════════════════
# SECTION 2 – COMPLETE HORMONE TEST PANEL
# ═══════════════════════════════════════════════════════════════
story.append(sec("2. COMPLETE HORMONE TEST PANEL"))
story.append(Spacer(1, 4))
panel_headers = ["Test", "Purpose in PCOS", "Timing", "Reference / Interpretation"]
panel_rows = [
# ── FSH/LH block ──
["FSH (Follicle-Stimulating Hormone)",
"Assess gonadotropin axis; rule out ovarian failure or hypothalamic-pituitary dysfunction",
"Day 2–3 of cycle (early follicular phase)",
"Normal: 3–10 mIU/mL. Low/normal in PCOS. FSH >10 mIU/mL → diminished ovarian reserve"],
["LH (Luteinizing Hormone)",
"Elevated LH drives ovarian androgen excess via thecal cell stimulation; key neuroendocrine marker",
"Day 2–3 of cycle (fasting preferred)",
"Lean PCOS: LH elevated (often 10–20 mIU/mL). Random value unreliable due to pulsatility"],
["LH : FSH Ratio",
"Classic PCOS pattern: GnRH pulse frequency skew toward LH",
"Same sample as above",
"Ratio >2:1 or >3:1 supportive of PCOS. Minor criterion only — not in Rotterdam 2023 criteria. Normal in obese PCOS."],
# ── Androgens ──
["Total Testosterone",
"Primary biochemical hyperandrogenism marker; required for Rotterdam criterion 2",
"8–10 AM fasting (diurnal variation); Day 2–5 preferred",
"PCOS: typically 100–200 ng/dL. >200 ng/dL raises suspicion of androgen-secreting tumor"],
["Free Testosterone",
"More sensitive than total T; useful when total T borderline normal",
"Same as total testosterone",
"Elevated free T with normal total T still confirms biochemical HA (Rotterdam criterion)"],
["DHEA-S (Dehydroepiandrosterone Sulfate)",
"Adrenal androgen marker; distinguishes adrenal vs ovarian source of androgens",
"Any time (relatively stable level)",
">700 µg/dL → adrenal tumor. Mildly elevated in PCOS (~50%). Very high → adrenal source"],
["17-OH Progesterone",
"Rule out non-classical congenital adrenal hyperplasia (21-hydroxylase deficiency)",
"Day 2–5, early AM",
"<2 ng/mL: normal. >2 ng/mL: proceed to ACTH stimulation test. Key exclusion test"],
# ── Prolactin/Thyroid ──
["Prolactin (PRL)",
"Rule out hyperprolactinemia as cause of anovulation and irregular menses",
"Fasting morning, no sexual activity prior",
">25–30 ng/mL → hyperprolactinemia. Mild elevation can be stress-related; recheck"],
["TSH (Thyroid Stimulating Hormone)",
"Rule out hypothyroidism mimicking PCOS (weight gain, irregular cycles, anovulation)",
"Any time (stable)",
"Normal: 0.4–4.0 mIU/L. TSH >4 → hypothyroidism. Address before confirming PCOS"],
# ── Insulin / Metabolic ──
["Fasting Insulin",
"Assess insulin resistance (IR); IR amplifies ovarian androgen production",
"8–12 hr fast",
"No universal cutoff. HOMA-IR = [fasting glucose (mmol/L) × fasting insulin (µU/mL)] / 22.5. HOMA-IR >2.5 suggests IR"],
["Fasting Blood Glucose / HbA1c",
"Screen for type 2 diabetes and prediabetes; PCOS carries significant metabolic risk",
"Fasting (8–12 hrs)",
"FBG ≥126 mg/dL or HbA1c ≥6.5% = diabetes. Impaired FBG: 100–125 mg/dL"],
["2-hr OGTT (Oral Glucose Tolerance Test)",
"Preferred over FBG in PCOS for detecting glucose intolerance; AES-PCOS Society recommended",
"Fasting; 75 g glucose load",
"2-hr glucose ≥200 mg/dL = diabetes; 140–199 mg/dL = impaired glucose tolerance"],
# ── AMH ──
["AMH (Anti-Mullerian Hormone)",
"Reflects antral follicle pool; now included in 2023 PCOS criteria as alternative to USS",
"Any day of cycle (stable across cycle)",
"Elevated in PCOS (reflects large small-antral follicle pool). Lab/assay-specific cutoffs — consult local lab"],
# ── Estradiol / Progesterone ──
["Estradiol (E2)",
"Assess follicular activity; Day 3 E2 with FSH helps interpret ovarian reserve",
"Day 2–3 of cycle",
"Day 3 E2 >80 pg/mL + elevated FSH = ovarian stimulation/failure. Low in hypothalamic amenorrhea"],
["Progesterone (midluteal)",
"Confirm ovulation occurred; useful in assessing anovulation",
"Day 21 (28-day cycle) or 7 days before expected next period",
">3 ng/mL = ovulation confirmed. <3 ng/mL = anovulatory cycle"],
# ── Cortisol ──
["24-hr Urinary Free Cortisol / 1 mg Overnight Dexamethasone Suppression Test",
"Rule out Cushing syndrome — can mimic PCOS with central obesity, hirsutism, irregular cycles",
"As directed",
"Post-dexamethasone cortisol <1.8 µg/dL = Cushing excluded (screening)"],
# ── Lipid profile ──
["Fasting Lipid Profile",
"Screen for dyslipidemia; part of metabolic risk assessment mandatory in PCOS",
"12-hr fast",
"Look for: ↑ LDL, ↑ triglycerides, ↓ HDL — metabolic syndrome pattern common in PCOS"],
]
col_w = [3.5*cm, 5.0*cm, 3.2*cm, W - 11.7*cm]
panel_data = [[Paragraph(h, CELL_H) for h in panel_headers]]
row_colors = [
C_TEAL_LT, C_TEAL_LT, C_PURPLE_LT, # FSH, LH, ratio
C_GREY_BG, C_WHITE, C_GREY_BG, C_WHITE, # androgens
C_GOLD_LT, C_GOLD_LT, # PRL, TSH
C_WHITE, C_GREY_BG, C_WHITE, # metabolic
C_TEAL_LT, # AMH
C_GREY_BG, C_WHITE, # E2, Prog
C_GOLD_LT, # Cortisol
C_WHITE, # Lipids
]
for i, row in enumerate(panel_rows):
panel_data.append([Paragraph(str(c), CELL) for c in row])
panel_tbl = Table(panel_data, colWidths=col_w, repeatRows=1)
row_style = [
("BACKGROUND",(0,0),(-1,0), C_TEAL),
("GRID",(0,0),(-1,-1),0.35, C_GREY_LINE),
("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),"TOP"),
]
for i, bg in enumerate(row_colors):
row_style.append(("BACKGROUND",(0,i+1),(-1,i+1), bg))
panel_tbl.setStyle(TableStyle(row_style))
story.append(panel_tbl)
story.append(Spacer(1, 8))
# ═══════════════════════════════════════════════════════════════
# SECTION 3 – FSH / LH INTERPRETATION TABLE
# ═══════════════════════════════════════════════════════════════
story.append(sec("3. FSH / LH PATTERN INTERPRETATION — Differential Diagnosis"))
story.append(Spacer(1, 4))
interp_headers = ["Condition", "FSH", "LH", "LH:FSH Ratio", "Key Distinguishing Features"]
interp_rows = [
["<b>PCOS (lean/typical)</b>", "Normal / Low-normal", "<b>↑ Elevated</b>", ">2:1 to 3:1",
"Testosterone ↑, irregular cycles, polycystic ovaries on USS, anovulation"],
["<b>PCOS (obese)</b>", "Normal", "Normal or mildly ↑", "Often normal",
"Obesity blunts LH; diagnosis relies more on testosterone + USS + AMH"],
["<b>Primary Ovarian Insufficiency (POI)</b>", "<b>↑↑ Very High</b>", "↑ Elevated", "<1:1 or near 1:1",
"FSH >25 mIU/mL (x2), estradiol low, age <40; anti-thyroid Ab, karyotype"],
["<b>Hypothalamic Amenorrhea</b>", "Low / Normal", "Low / Normal", "~1:1",
"Stress, weight loss, exercise; estradiol low; GnRH pulse frequency reduced"],
["<b>Hyperprolactinemia</b>", "Low-normal", "Low-normal", "Variable",
"Prolactin ↑; galactorrhea; MRI pituitary; LH/FSH suppressed by prolactin"],
["<b>Non-classical CAH</b>", "Normal", "Normal", "Normal",
"17-OH progesterone ↑ (>2 ng/mL Day 3); ACTH stim test positive"],
["<b>Hypothyroidism</b>", "Normal", "Normal", "Normal",
"TSH ↑, free T4 ↓; may cause hyperprolactinemia secondarily"],
["<b>Androgen-Secreting Tumor</b>", "Normal/Low", "Normal/Low", "Normal",
"Testosterone >200 ng/dL; rapid virilization; pelvic/adrenal imaging"],
]
col_w2 = [3.5*cm, 2.0*cm, 2.0*cm, 2.5*cm, W - 10*cm]
interp_data = [[Paragraph(h, CELL_H) for h in interp_headers]]
alt_colors2 = [C_WHITE, C_GREY_BG] * 10
for i, row in enumerate(interp_rows):
interp_data.append([Paragraph(str(c), CELL) for c in row])
interp_tbl = Table(interp_data, colWidths=col_w2, repeatRows=1)
interp_tbl.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0), C_PURPLE),
("GRID",(0,0),(-1,-1),0.35, C_GREY_LINE),
("ROWBACKGROUNDS",(0,1),(-1,-1),[C_WHITE, C_PURPLE_LT]),
("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),"TOP"),
]))
story.append(interp_tbl)
story.append(Spacer(1, 8))
# ═══════════════════════════════════════════════════════════════
# SECTION 4 – FSH/LH TESTING PITFALLS
# ═══════════════════════════════════════════════════════════════
story.append(sec("4. KEY PITFALLS IN FSH / LH TESTING IN PCOS", color=C_GOLD))
story.append(Spacer(1, 4))
pitfall_data = [
[Paragraph("Pitfall", CELL_H), Paragraph("Explanation", CELL_H), Paragraph("Action", CELL_H)],
[Paragraph("LH pulsatility", CELL_B),
Paragraph("LH secreted in pulses — a single random draw may miss elevation. LH can vary 2-3x within hours.", CELL),
Paragraph("Measure Day 2-3 fasting. Interpret ratio cautiously; single value unreliable.", CELL)],
[Paragraph("Obesity blunts LH", CELL_B),
Paragraph("Obese PCOS women often have normal LH:FSH due to adipose-mediated LH suppression.", CELL),
Paragraph("Do not rule out PCOS based on normal LH alone. Rely on testosterone + USS + AMH.", CELL)],
[Paragraph("Urinary LH kits (OPKs) unreliable", CELL_B),
Paragraph("Chronically elevated baseline LH in PCOS gives false-positive ovulation predictor kit results throughout the cycle.", CELL),
Paragraph("Use midluteal serum progesterone (Day 21) to confirm ovulation instead.", CELL)],
[Paragraph("LH:FSH ratio is minor criterion only", CELL_B),
Paragraph("Rotterdam 2023 guidelines do NOT include LH:FSH ratio as a primary diagnostic criterion.", CELL),
Paragraph("Treat ratio as supportive evidence. Diagnosis requires 2 of 3 Rotterdam criteria.", CELL)],
[Paragraph("Day of cycle matters", CELL_B),
Paragraph("FSH measured in mid/late follicular phase may appear falsely elevated. LH peaks at ovulation.", CELL),
Paragraph("Always measure FSH and LH on Day 2–3 of cycle for standardized interpretation.", CELL)],
[Paragraph("Oral contraceptives suppress FSH/LH", CELL_B),
Paragraph("OCP use suppresses gonadotropins — LH:FSH ratio will be falsely normal on OCP.", CELL),
Paragraph("Ideally discontinue OCP ≥3 months before testing. Note OCP use in lab request.", CELL)],
]
p_col_w = [3.5*cm, 7.0*cm, W - 10.5*cm]
pitfall_tbl = Table(pitfall_data, colWidths=p_col_w, repeatRows=1)
pitfall_tbl.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0), C_GOLD),
("GRID",(0,0),(-1,-1),0.35, C_GREY_LINE),
("ROWBACKGROUNDS",(0,1),(-1,-1),[C_WHITE, C_GOLD_LT]),
("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),"TOP"),
]))
story.append(pitfall_tbl)
story.append(Spacer(1, 8))
# ═══════════════════════════════════════════════════════════════
# SECTION 5 – STEP-BY-STEP DIAGNOSTIC ALGORITHM
# ═══════════════════════════════════════════════════════════════
story.append(sec("5. STEP-BY-STEP DIAGNOSTIC ALGORITHM"))
story.append(Spacer(1, 4))
steps = [
("Step 1 — History & Menstrual Pattern",
"Document cycle frequency, duration, last menstrual period. ≥1 year of irregular cycles required. In adolescents: irregular menses ≥2 years post-menarche."),
("Step 2 — Clinical Hyperandrogenism Assessment",
"Hirsutism: modified Ferriman-Gallwey (mFG) score ≥4–6. Also assess: acne, androgenic alopecia. Clinical HA alone can satisfy criterion 2."),
("Step 3 — Initial Hormone Screen (Exclusion Panel)",
"Order: TSH, Prolactin, 17-OH Progesterone (Day 2–5 AM). These exclude hypothyroidism, hyperprolactinemia, and non-classical CAH — three key PCOS mimics."),
("Step 4 — Androgen Testing",
"Total testosterone (AM fasting, Day 2–5). Free testosterone if total borderline. DHEA-S if adrenal source suspected (total T >200 ng/dL, rapid onset, virilization)."),
("Step 5 — FSH, LH & Estradiol",
"Day 2–3 of cycle. FSH & LH to assess gonadotropin axis. Elevated FSH + LH → POI (not PCOS). Low FSH + LH → hypothalamic amenorrhea. Elevated LH with normal-low FSH → supports PCOS. Estradiol Day 3 to assess follicular reserve."),
("Step 6 — Metabolic Screening",
"Fasting glucose, fasting insulin (HOMA-IR), HbA1c, fasting lipid profile. OGTT (75 g) preferred over FBG alone. Weight, BMI, waist circumference, blood pressure."),
("Step 7 — AMH",
"Elevated AMH now accepted as an alternative to ultrasound in the 2023 guidelines. Use assay with validated PCOS cutoffs. Stable across cycle days."),
("Step 8 — Pelvic Ultrasound",
"Transvaginal USS preferred. ≥20 antral follicles OR ovarian volume ≥10 cm³ in at least one ovary = polycystic morphology. Note: ultrasound NOT used to diagnose PCOS in adolescents."),
("Step 9 — Confirm Diagnosis",
"Apply Rotterdam 2023 criteria: 2 of 3 criteria met + all exclusions cleared = PCOS confirmed. Document PCOS phenotype (1–4) for management planning."),
]
for step_title, step_body in steps:
row = [[
Paragraph(f"<b>{step_title}</b>", CELL_B),
Paragraph(step_body, CELL)
]]
t = Table(row, colWidths=[4.5*cm, W - 4.5*cm])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(0,-1), C_TEAL_LT),
("BACKGROUND",(1,0),(1,-1), C_WHITE),
("BOX",(0,0),(-1,-1),0.5, C_GREY_LINE),
("LINEAFTER",(0,0),(0,-1),0.5, C_GREY_LINE),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),6),
("RIGHTPADDING",(0,0),(-1,-1),6),
("VALIGN",(0,0),(-1,-1),"TOP"),
]))
story.append(t)
story.append(Spacer(1, 2))
story.append(Spacer(1, 6))
# ═══════════════════════════════════════════════════════════════
# SECTION 6 – ADDITIONAL TESTS BY INDICATION
# ═══════════════════════════════════════════════════════════════
story.append(sec("6. ADDITIONAL TESTS BY INDICATION", color=C_PURPLE))
story.append(Spacer(1, 4))
add_headers = ["Indication", "Test(s) to Order", "What to Look For"]
add_rows = [
["Suspected Cushing syndrome\n(central obesity, striae, hypertension)",
"24-hr urinary free cortisol\n1 mg overnight dexamethasone suppression test\nLate-night salivary cortisol",
"Post-dex cortisol >1.8 µg/dL → Cushing not excluded → refer endocrinology"],
["Suspected androgen-secreting tumor\n(Testosterone >200 ng/dL, rapid virilization)",
"Total and free testosterone\nDHEA-S\nPelvic/adrenal MRI or CT",
"Very high T or DHEA-S; adrenal/ovarian mass on imaging → urgent referral"],
["Fertility evaluation\n(infertility workup, IVF planning)",
"AMH\nAntral Follicle Count (AFC) on USS\nFSH Day 3 + Estradiol Day 3",
"AMH + AFC assess ovarian reserve. FSH >10 = diminished reserve. High AMH in PCOS → OHSS risk with IVF"],
["Adolescent PCOS (puberty period)",
"Testosterone (total + free)\nLH, FSH\nDo NOT use USS or AMH for criteria",
"Diagnosis requires: irregular menses + hyperandrogenism only. Both criteria must be met."],
["Pregnancy or fertility treatment",
"Progesterone, hCG\nThyroid function (TSH + free T4)\nFasting glucose",
"Progesterone monitoring post-ovulation induction. Thyroid optimization before conception."],
["Monitoring on treatment (OCP/metformin)",
"Annual: fasting glucose, HbA1c, lipid profile, BP\nTestosterone if symptomatic",
"OCP suppresses FSH/LH — do not use to monitor gonadotropins on treatment"],
]
add_col_w = [4.5*cm, 5.0*cm, W - 9.5*cm]
story.append(std_table(add_headers, add_rows, add_col_w))
story.append(Spacer(1, 8))
# ═══════════════════════════════════════════════════════════════
# SECTION 7 – QUICK REFERENCE: PCOS PHENOTYPES
# ═══════════════════════════════════════════════════════════════
story.append(sec("7. PCOS PHENOTYPES (Rotterdam Classification)"))
story.append(Spacer(1, 4))
phen_headers = ["Phenotype", "HA (Clinical/Biochem)", "Ovulatory Dysfunction", "Polycystic Ovaries on USS", "Notes"]
phen_rows = [
["1 — Full PCOS", "✓", "✓", "✓", "Most common; classic presentation; highest metabolic risk"],
["2 — Classic without PCO", "✓", "✓", "✗", "Classic NIH 1990 phenotype; significant metabolic risk"],
["3 — Ovulatory PCOS", "✓", "✗", "✓", "Regular cycles but biochemical HA + polycystic USS; milder metabolic risk"],
["4 — Non-androgenic PCOS", "✗", "✓", "✓", "No hyperandrogenism; irregular cycles + polycystic USS; mildest phenotype; exclude other causes carefully"],
]
phen_col_w = [3.5*cm, 2.8*cm, 3.0*cm, 3.2*cm, W - 12.5*cm]
phen_data = [[Paragraph(h, CELL_H) for h in phen_headers]]
phen_alt = [C_WHITE, C_GREY_BG, C_TEAL_LT, C_PURPLE_LT]
for i, row in enumerate(phen_rows):
phen_data.append([Paragraph(str(c), CELL) for c in row])
phen_tbl = Table(phen_data, colWidths=phen_col_w, repeatRows=1)
phen_style = [
("BACKGROUND",(0,0),(-1,0), C_TEAL),
("GRID",(0,0),(-1,-1),0.35, C_GREY_LINE),
("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),"TOP"),
]
for i, bg in enumerate(phen_alt):
phen_style.append(("BACKGROUND",(0,i+1),(-1,i+1), bg))
phen_tbl.setStyle(TableStyle(phen_style))
story.append(phen_tbl)
story.append(Spacer(1, 8))
# ═══════════════════════════════════════════════════════════════
# FOOTER NOTE
# ═══════════════════════════════════════════════════════════════
story.append(HRFlowable(width=W, thickness=0.5, color=C_GREY_LINE))
story.append(Spacer(1, 4))
story.append(Paragraph(
"Sources: Harrison's Principles of Internal Medicine 22E (2025) · Berek & Novak's Gynecology · "
"Henry's Clinical Diagnosis & Management by Laboratory Methods (Tietz) · Dermatology 2-Volume Set 5e · "
"Textbook of Family Medicine 9e | Rotterdam Criteria (2003, updated 2023 International PCOS Guidelines) | "
"For educational / clinical reference use. Verify all reference ranges with your local laboratory.",
FOOT
))
# ── Build ──────────────────────────────────────────────────────
doc.build(story)
print(f"PDF saved: {OUTPUT}")
cd /home/daytona/workspace/pcos-pdf && python generate_pcos_pdf.py
ls -lh /home/daytona/workspace/pcos-pdf/PCOS_Diagnostic_Workup_Quick_Reference.pdf
PCOS Diagnostic Workup Quick Reference
PDF Document · PDF
| Section | Content |
|---|---|
| 1. Diagnostic Criteria | Rotterdam 2003 / 2023 table - all 3 criteria with notes + exclusion warning box |
| 2. Complete Hormone Test Panel | 16 tests: FSH, LH, LH:FSH ratio, Total T, Free T, DHEA-S, 17-OH Progesterone, Prolactin, TSH, Fasting Insulin, FBG/HbA1c, OGTT, AMH, Estradiol, Progesterone (midluteal), Cortisol, Lipid Profile - with timing, purpose, and reference ranges for each |
| 3. FSH/LH Pattern Interpretation | Differential diagnosis table: PCOS lean, PCOS obese, POI, hypothalamic amenorrhea, hyperprolactinemia, CAH, hypothyroidism, androgen tumor |
| 4. Key Pitfalls | 6 pitfalls: LH pulsatility, obesity effect, OPK false positives, LH:FSH as minor criterion, day of cycle, OCP suppression |
| 5. Step-by-Step Algorithm | 9-step sequential diagnostic workflow from history to PCOS confirmation |
| 6. Additional Tests by Indication | Tests for Cushing, androgen tumor, fertility workup, adolescent PCOS, pregnancy |
| 7. PCOS Phenotypes | All 4 Rotterdam phenotypes with HA/ovulation/USS status and clinical notes |
Create a quick reference PDF on PCOS diagnostic workup including all hormone tests
ls -lh /home/daytona/workspace/pcos-pdf/PCOS_Diagnostic_Workup_Quick_Reference.pdf
PCOS Diagnostic Workup Quick Reference
PDF Document · PDF
Generate a patient-friendly PCOS information handout in PDF format
~/pcos-pdf/generate_pcos_patient_handout.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_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.lib.colors import HexColor
from reportlab.graphics.shapes import Drawing, Rect, Circle, String, Line
from reportlab.graphics import renderPDF
OUTPUT = "/home/daytona/workspace/pcos-pdf/PCOS_Patient_Handout.pdf"
# ── Palette (soft, warm, approachable) ─────────────────────────
C_ROSE = HexColor("#C2185B") # header / accent
C_ROSE_LT = HexColor("#FCE4EC") # soft pink bg
C_TEAL = HexColor("#00796B") # section headers
C_TEAL_LT = HexColor("#E0F2F1") # teal light bg
C_PURPLE = HexColor("#6A1B9A") # accent 2
C_PURPLE_LT = HexColor("#F3E5F5")
C_AMBER = HexColor("#F57F17") # tips/notes
C_AMBER_LT = HexColor("#FFFDE7")
C_GREEN = HexColor("#2E7D32")
C_GREEN_LT = HexColor("#E8F5E9")
C_BLUE = HexColor("#1565C0")
C_BLUE_LT = HexColor("#E3F2FD")
C_GREY_LT = HexColor("#F5F5F5")
C_GREY_LINE = HexColor("#CFD8DC")
C_BLACK = HexColor("#212121")
C_DARK_GREY = HexColor("#424242")
C_WHITE = colors.white
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
topMargin=1.4*cm, bottomMargin=1.4*cm,
leftMargin=1.8*cm, rightMargin=1.8*cm
)
W = A4[0] - 3.6*cm
def ps(name, **kw):
base = kw.pop("parent", "Normal")
return ParagraphStyle(name, parent=getSampleStyleSheet()[base], **kw)
# Styles
TITLE = ps("TITLE", fontSize=26, textColor=C_WHITE, alignment=TA_CENTER, fontName="Helvetica-Bold", leading=32)
TAGLINE = ps("TAGLINE", fontSize=12, textColor=HexColor("#F8BBD0"), alignment=TA_CENTER, fontName="Helvetica-Oblique", leading=17)
SEC_H = ps("SEC_H", fontSize=12, textColor=C_WHITE, alignment=TA_LEFT, fontName="Helvetica-Bold", leading=16, spaceBefore=4, spaceAfter=2)
BODY = ps("BODY", fontSize=10, textColor=C_BLACK, alignment=TA_JUSTIFY,fontName="Helvetica", leading=15, spaceBefore=2)
BODY_L = ps("BODY_L", fontSize=10, textColor=C_BLACK, alignment=TA_LEFT, fontName="Helvetica", leading=15, spaceBefore=2)
BOLD10 = ps("BOLD10", fontSize=10, textColor=C_BLACK, fontName="Helvetica-Bold", leading=15)
CELL = ps("CELL", fontSize=9.5,textColor=C_BLACK, fontName="Helvetica", leading=14, alignment=TA_LEFT)
CELL_H = ps("CELL_H", fontSize=9.5,textColor=C_WHITE, fontName="Helvetica-Bold", leading=14, alignment=TA_LEFT)
CELL_C = ps("CELL_C", fontSize=9.5,textColor=C_BLACK, fontName="Helvetica", leading=14, alignment=TA_CENTER)
NOTE = ps("NOTE", fontSize=9, textColor=HexColor("#4E342E"), fontName="Helvetica-Oblique", leading=13)
TIP_H = ps("TIP_H", fontSize=10, textColor=C_AMBER, fontName="Helvetica-Bold", leading=14)
SMALL = ps("SMALL", fontSize=8, textColor=HexColor("#757575"), fontName="Helvetica", leading=11, alignment=TA_CENTER)
Q = ps("Q", fontSize=10, textColor=C_TEAL, fontName="Helvetica-Bold", leading=14)
ANS = ps("ANS", fontSize=10, textColor=C_BLACK, fontName="Helvetica", leading=15, alignment=TA_JUSTIFY)
BULLET = ps("BULLET", fontSize=10, textColor=C_BLACK, fontName="Helvetica", leading=15, leftIndent=12, firstLineIndent=-12)
EMPH = ps("EMPH", fontSize=10, textColor=C_PURPLE, fontName="Helvetica-Bold", leading=15, alignment=TA_CENTER)
story = []
# ══════════════════════════════════════════════════════════════
# TITLE BANNER
# ══════════════════════════════════════════════════════════════
def title_banner():
rows = [
[Paragraph("Understanding PCOS", TITLE)],
[Paragraph("Polycystic Ovary Syndrome", TAGLINE)],
[Paragraph("A guide for patients & families", TAGLINE)],
]
t = Table(rows, colWidths=[W])
t.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_ROSE),
("TOPPADDING", (0,0), (-1,0), 16),
("BOTTOMPADDING",(0,-1),(-1,-1), 14),
("TOPPADDING", (0,1), (-1,1), 3),
("BOTTOMPADDING",(0,0),(-1,1), 3),
("TOPPADDING", (0,2), (-1,2), 2),
]))
return t
story.append(title_banner())
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# Helper: section header
# ══════════════════════════════════════════════════════════════
def sec(icon, title, color=C_TEAL):
t = Table([[Paragraph(f" {icon} {title}", SEC_H)]], colWidths=[W])
t.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),6),
]))
return t
def info_box(text, bg=C_AMBER_LT, border=C_AMBER, style=NOTE):
t = Table([[Paragraph(text, style)]], colWidths=[W])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1), bg),
("BOX",(0,0),(-1,-1),1.2, border),
("TOPPADDING",(0,0),(-1,-1),7),
("BOTTOMPADDING",(0,0),(-1,-1),7),
("LEFTPADDING",(0,0),(-1,-1),10),
("RIGHTPADDING",(0,0),(-1,-1),10),
]))
return t
def two_col(left_content, right_content, lw=None, rw=None):
lw = lw or W * 0.48
rw = rw or W * 0.48
t = Table([[left_content, right_content]], colWidths=[lw, W - lw - 0.2*cm])
t.setStyle(TableStyle([
("VALIGN",(0,0),(-1,-1),"TOP"),
("LEFTPADDING",(0,0),(-1,-1),0),
("RIGHTPADDING",(0,0),(-1,-1),0),
("TOPPADDING",(0,0),(-1,-1),0),
("BOTTOMPADDING",(0,0),(-1,-1),0),
]))
return t
def bullet_table(items, bullet="•", color=C_TEAL, bg=C_WHITE):
rows = []
for item in items:
rows.append([
Paragraph(f'<font color="#{color.hexval()[2:]}"><b>{bullet}</b></font>', CELL),
Paragraph(item, CELL)
])
t = Table(rows, colWidths=[0.4*cm, W - 0.4*cm])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1), bg),
("TOPPADDING",(0,0),(-1,-1),2),
("BOTTOMPADDING",(0,0),(-1,-1),2),
("LEFTPADDING",(0,0),(-1,-1),4),
("RIGHTPADDING",(0,0),(-1,-1),4),
("VALIGN",(0,0),(-1,-1),"TOP"),
]))
return t
# ══════════════════════════════════════════════════════════════
# SECTION 1 – WHAT IS PCOS?
# ══════════════════════════════════════════════════════════════
story.append(sec("❓", "What Is PCOS?", C_ROSE))
story.append(Spacer(1, 5))
story.append(Paragraph(
"PCOS (Polycystic Ovary Syndrome) is one of the most common hormonal conditions in women of "
"reproductive age, affecting about <b>1 in 10 women worldwide</b>. It affects how the ovaries work "
"and causes an imbalance in hormones — particularly higher-than-normal levels of male hormones "
"(androgens). Despite its name, you do not need to have cysts on your ovaries to have PCOS.",
BODY))
story.append(Spacer(1, 5))
story.append(info_box(
"💡 PCOS is NOT your fault. It is caused by a combination of genetic and hormonal factors. "
"It cannot be 'caught' and is not caused by anything you did or didn't do.",
bg=C_TEAL_LT, border=C_TEAL, style=NOTE))
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# SECTION 2 – SYMPTOMS
# ══════════════════════════════════════════════════════════════
story.append(sec("🩺", "Common Symptoms", C_ROSE))
story.append(Spacer(1, 5))
story.append(Paragraph(
"PCOS affects every woman differently. You may have a few symptoms or many. "
"Symptoms often start in your teens or early 20s.", BODY))
story.append(Spacer(1, 5))
sym_left = [
["<b>Symptom</b>", "<b>What It Means</b>"],
["Irregular or missed periods", "Ovulation does not happen regularly, causing unpredictable cycles"],
["Excess hair on face, chest, or back (hirsutism)", "Higher androgen (male hormone) levels stimulate hair follicles"],
["Acne or oily skin", "Androgens increase oil production in skin glands"],
["Thinning hair or hair loss on scalp", "Again related to elevated androgens acting on hair follicles"],
["Weight gain (especially around the belly)", "Insulin resistance makes it easier to gain and harder to lose weight"],
["Difficulty getting pregnant", "Irregular ovulation means fewer chances of conception each month"],
["Mood changes, anxiety, or low mood", "Hormonal imbalance and living with a chronic condition can affect mental health"],
["Fatigue", "Often linked to poor sleep, blood sugar swings, or low mood"],
]
sym_col_w = [4.5*cm, W - 4.5*cm]
sym_data = [[Paragraph(r[0], CELL_H if i==0 else CELL_C),
Paragraph(r[1], CELL_H if i==0 else CELL)]
for i, r in enumerate(sym_left)]
sym_tbl = Table(sym_data, colWidths=sym_col_w, repeatRows=1)
sym_tbl.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0), C_ROSE),
("ROWBACKGROUNDS",(0,1),(-1,-1),[C_WHITE, C_ROSE_LT]),
("GRID",(0,0),(-1,-1),0.35, C_GREY_LINE),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),6),
("RIGHTPADDING",(0,0),(-1,-1),6),
("VALIGN",(0,0),(-1,-1),"TOP"),
]))
story.append(sym_tbl)
story.append(Spacer(1, 5))
story.append(info_box(
"⚠️ Not all women with PCOS have all symptoms. Some women only discover they have PCOS "
"when they have difficulty getting pregnant. See your doctor if you have irregular periods or "
"any of the symptoms above.",
bg=C_AMBER_LT, border=C_AMBER))
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# SECTION 3 – HOW IS PCOS DIAGNOSED?
# ══════════════════════════════════════════════════════════════
story.append(sec("🔬", "How Is PCOS Diagnosed?", C_TEAL))
story.append(Spacer(1, 5))
story.append(Paragraph(
"There is no single test for PCOS. Your doctor will use a combination of your symptoms, "
"blood tests, and an ultrasound scan. According to international guidelines (<b>Rotterdam Criteria 2023</b>), "
"you need to have <b>2 out of the following 3 features</b>, after other conditions have been ruled out:", BODY))
story.append(Spacer(1, 5))
diag_rows = [
["Feature 1", "Irregular or absent periods\n(fewer than 8 menstrual cycles per year, or no periods)"],
["Feature 2", "Signs of high male hormones (androgens)\nEither on examination (e.g. excess facial hair, acne) OR in your blood tests"],
["Feature 3", "Polycystic-looking ovaries on ultrasound scan\nOR an elevated AMH blood test result"],
]
diag_col_w = [2.8*cm, W - 2.8*cm]
diag_data = [[Paragraph(f"<b>{r[0]}</b>", CELL), Paragraph(r[1], CELL)] for r in diag_rows]
diag_colors = [C_TEAL_LT, C_GREEN_LT, C_PURPLE_LT]
diag_tbl = Table(diag_data, colWidths=diag_col_w)
diag_tbl.setStyle(TableStyle([
("BOX",(0,0),(-1,-1),1, C_GREY_LINE),
("LINEBELOW",(0,0),(-1,-2),0.5, C_GREY_LINE),
("TOPPADDING",(0,0),(-1,-1),7),
("BOTTOMPADDING",(0,0),(-1,-1),7),
("LEFTPADDING",(0,0),(-1,-1),8),
("RIGHTPADDING",(0,0),(-1,-1),8),
("VALIGN",(0,0),(-1,-1),"MIDDLE"),
*[("BACKGROUND",(0,i),(-1,i), diag_colors[i]) for i in range(3)],
]))
story.append(diag_tbl)
story.append(Spacer(1, 5))
story.append(info_box(
"📋 PCOS is a diagnosis of exclusion — your doctor must first rule out other conditions "
"that can look similar, such as thyroid problems, raised prolactin hormone, or adrenal gland conditions.",
bg=C_BLUE_LT, border=C_BLUE))
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# SECTION 4 – BLOOD TESTS EXPLAINED
# ══════════════════════════════════════════════════════════════
story.append(sec("🩸", "Blood Tests Your Doctor May Order", C_TEAL))
story.append(Spacer(1, 5))
story.append(Paragraph(
"Blood tests help confirm the diagnosis and check for related health conditions. "
"Here is a plain-language guide to each test:", BODY))
story.append(Spacer(1, 5))
tests = [
("Testosterone (Total & Free)",
"Checks the level of the main 'male hormone' in your blood. Women normally have small "
"amounts, but in PCOS it is often higher than normal. This can cause excess hair and acne.",
C_ROSE_LT, C_ROSE),
("LH & FSH\n(Luteinising Hormone & Follicle-Stimulating Hormone)",
"These are 'messenger hormones' from your brain that tell your ovaries to release an egg. "
"In PCOS, LH is often higher than FSH, which disrupts ovulation. <i>Note: these are measured "
"on Day 2–3 of your period for best results.</i>",
C_TEAL_LT, C_TEAL),
("AMH\n(Anti-Müllerian Hormone)",
"Reflects the number of small egg follicles in your ovaries. Women with PCOS often have a "
"higher AMH level because they have more follicles than usual. AMH can be measured any day "
"of your cycle.",
C_PURPLE_LT, C_PURPLE),
("17-OH Progesterone",
"Rules out a condition called Congenital Adrenal Hyperplasia (CAH), which can look very "
"similar to PCOS. This is measured on Days 2–5 of your cycle, in the morning.",
C_GREEN_LT, C_GREEN),
("Prolactin",
"A hormone produced by the pituitary gland (in the brain). High prolactin can stop ovulation "
"and cause irregular periods — so it needs to be ruled out before confirming PCOS.",
C_BLUE_LT, C_BLUE),
("TSH (Thyroid Stimulating Hormone)",
"Checks your thyroid gland. An underactive thyroid can cause weight gain, irregular periods, "
"and tiredness — symptoms that overlap with PCOS. Can be tested any time of day.",
C_AMBER_LT, C_AMBER),
("Fasting Blood Glucose & HbA1c",
"Checks your blood sugar level and your average blood sugar over 3 months. Women with PCOS "
"are more likely to develop prediabetes or type 2 diabetes, especially with weight gain.",
C_ROSE_LT, C_ROSE),
("Fasting Insulin / HOMA-IR",
"Checks whether your body is responding properly to insulin (the hormone that controls blood "
"sugar). Many women with PCOS have 'insulin resistance' — their body makes extra insulin to "
"keep blood sugar normal, which worsens hormone imbalance.",
C_TEAL_LT, C_TEAL),
("DHEA-S",
"Another male hormone, but made by the adrenal glands (above the kidneys) rather than the "
"ovaries. Helps distinguish whether excess androgens come from the ovaries (PCOS) or adrenal "
"glands (adrenal condition).",
C_PURPLE_LT, C_PURPLE),
("Fasting Lipid Profile\n(Cholesterol & Triglycerides)",
"PCOS increases the risk of unhealthy cholesterol levels. This test checks LDL ('bad') "
"cholesterol, HDL ('good') cholesterol, and triglycerides (blood fats).",
C_GREEN_LT, C_GREEN),
]
for test_name, test_desc, bg, border in tests:
row = [[
Paragraph(f"<b>{test_name}</b>", BOLD10),
Paragraph(test_desc, CELL)
]]
t = Table(row, colWidths=[4.2*cm, W - 4.2*cm])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(0,-1), bg),
("BACKGROUND",(1,0),(1,-1), C_WHITE),
("BOX",(0,0),(-1,-1),0.8, border),
("LINEAFTER",(0,0),(0,-1),0.8, border),
("TOPPADDING",(0,0),(-1,-1),6),
("BOTTOMPADDING",(0,0),(-1,-1),6),
("LEFTPADDING",(0,0),(-1,-1),7),
("RIGHTPADDING",(0,0),(-1,-1),7),
("VALIGN",(0,0),(-1,-1),"TOP"),
]))
story.append(t)
story.append(Spacer(1, 3))
story.append(Spacer(1, 5))
story.append(info_box(
"📌 For hormone tests like LH, FSH, testosterone and 17-OH progesterone, timing matters. "
"Your doctor will usually ask you to come in on Day 2–5 of your menstrual cycle, fasting "
"(nothing to eat for 8–10 hours). If you are on the contraceptive pill, these tests may need "
"to wait until 3 months after stopping, as the pill suppresses hormone levels.",
bg=C_AMBER_LT, border=C_AMBER))
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# SECTION 5 – LONG-TERM HEALTH RISKS
# ══════════════════════════════════════════════════════════════
story.append(sec("❤️", "Long-Term Health Conditions to Be Aware Of", C_PURPLE))
story.append(Spacer(1, 5))
story.append(Paragraph(
"PCOS can increase the risk of certain health conditions over time. "
"The good news is that many of these risks can be <b>significantly reduced</b> with the "
"right lifestyle changes and medical treatment.", BODY))
story.append(Spacer(1, 5))
risk_rows = [
["Health Condition", "What Increases the Risk?", "How to Reduce Your Risk"],
["Type 2 Diabetes\n& Prediabetes",
"Insulin resistance, weight gain",
"Healthy diet, regular exercise, regular blood sugar checks"],
["Cardiovascular Disease\n(Heart & Blood Vessels)",
"Unhealthy cholesterol, high blood pressure, obesity",
"Exercise, healthy eating, weight management, stop smoking"],
["Endometrial Cancer\n(Womb Cancer)",
"Infrequent periods mean the womb lining builds up",
"Induce regular periods (with medication if needed); lose weight if overweight"],
["Obstructive Sleep Apnoea",
"Obesity and hormonal factors",
"Weight loss, sleep study if snoring or poor sleep"],
["Mental Health Problems\n(Anxiety, Depression)",
"Hormonal imbalance, body image concerns, fertility worries",
"Seek support early; therapy, peer support groups, medication if needed"],
["Difficulty Conceiving",
"Irregular ovulation reduces monthly chance of pregnancy",
"Fertility treatments available; lifestyle changes improve ovulation"],
]
risk_col_w = [3.8*cm, 4.5*cm, W - 8.3*cm]
risk_data = [[Paragraph(str(c), CELL_H if i==0 else CELL) for c in r] for i, r in enumerate(risk_rows)]
risk_tbl = Table(risk_data, colWidths=risk_col_w, repeatRows=1)
risk_tbl.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0), C_PURPLE),
("ROWBACKGROUNDS",(0,1),(-1,-1),[C_WHITE, C_PURPLE_LT]),
("GRID",(0,0),(-1,-1),0.35, C_GREY_LINE),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),6),
("RIGHTPADDING",(0,0),(-1,-1),6),
("VALIGN",(0,0),(-1,-1),"TOP"),
]))
story.append(risk_tbl)
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# SECTION 6 – TREATMENT OPTIONS
# ══════════════════════════════════════════════════════════════
story.append(sec("💊", "Treatment Options", C_GREEN))
story.append(Spacer(1, 5))
story.append(Paragraph(
"There is no cure for PCOS, but the symptoms can be very well managed. "
"Treatment is tailored to your specific symptoms and goals "
"(e.g. managing periods, reducing hair growth, getting pregnant, or improving metabolic health).",
BODY))
story.append(Spacer(1, 6))
# Lifestyle block
lifestyle_items = [
"<b>Healthy diet:</b> Low glycaemic index (GI) foods — wholegrains, legumes, vegetables, lean protein. Reduce processed foods and sugary drinks.",
"<b>Regular exercise:</b> Aim for at least 150 minutes of moderate activity per week. Both cardio and strength training are beneficial.",
"<b>Weight management:</b> Losing even 5–10% of body weight can restore ovulation, improve periods, and reduce androgen levels in overweight women.",
"<b>Stress reduction:</b> Chronic stress worsens insulin resistance. Sleep well (7–9 hours), try yoga, mindfulness, or relaxation techniques.",
]
lifestyle_tbl = Table([
[Paragraph("🥗 Lifestyle Changes (First-Line Treatment for ALL Women with PCOS)", TIP_H)],
[bullet_table(lifestyle_items, color=C_GREEN)],
], colWidths=[W])
lifestyle_tbl.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0), C_GREEN_LT),
("BACKGROUND",(0,1),(-1,1), C_WHITE),
("BOX",(0,0),(-1,-1),1, C_GREEN),
("TOPPADDING",(0,0),(-1,0),7),
("BOTTOMPADDING",(0,0),(-1,0),5),
("TOPPADDING",(0,1),(-1,1),4),
("BOTTOMPADDING",(0,1),(-1,1),6),
("LEFTPADDING",(0,0),(-1,-1),10),
("RIGHTPADDING",(0,0),(-1,-1),10),
]))
story.append(lifestyle_tbl)
story.append(Spacer(1, 6))
# Medications
med_rows = [
["Treatment", "What It Does", "Used For"],
["Combined Oral Contraceptive Pill (OCP)",
"Regulates periods, lowers androgen levels, improves acne and excess hair",
"Irregular periods, acne, hirsutism"],
["Metformin",
"Improves insulin resistance, helps regulate periods, may aid weight management",
"Insulin resistance, prediabetes, irregular cycles, fertility support"],
["Anti-androgens\n(e.g. spironolactone, cyproterone acetate)",
"Block the effect of male hormones on the skin and hair follicles",
"Hirsutism (excess hair), acne, hair loss"],
["Clomifene / Letrozole",
"Stimulates ovulation to help you get pregnant",
"Fertility treatment for women trying to conceive"],
["Gonadotrophins\n(FSH / LH injections)",
"Directly stimulate the ovaries to produce eggs",
"Fertility treatment when tablets have not worked"],
["Inositol\n(Myo-inositol / D-chiro-inositol)",
"Supplement that may improve insulin sensitivity and ovulation regularity",
"Insulin resistance, ovulation support — ask your doctor"],
["Eflornithine cream",
"Slows the growth of facial hair",
"Facial hirsutism"],
]
med_col_w = [3.8*cm, 5.5*cm, W - 9.3*cm]
med_data = [[Paragraph(str(c), CELL_H if i==0 else CELL) for c in r] for i, r in enumerate(med_rows)]
med_tbl = Table(med_data, colWidths=med_col_w, repeatRows=1)
med_tbl.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0), C_GREEN),
("ROWBACKGROUNDS",(0,1),(-1,-1),[C_WHITE, C_GREEN_LT]),
("GRID",(0,0),(-1,-1),0.35, C_GREY_LINE),
("TOPPADDING",(0,0),(-1,-1),5),
("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),6),
("RIGHTPADDING",(0,0),(-1,-1),6),
("VALIGN",(0,0),(-1,-1),"TOP"),
]))
story.append(med_tbl)
story.append(Spacer(1, 5))
story.append(info_box(
"💊 Always take medications only as prescribed by your doctor. Do NOT start or stop any "
"treatment without medical advice. Some medications interact with each other or are not safe "
"in pregnancy.",
bg=C_AMBER_LT, border=C_AMBER))
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# SECTION 7 – PCOS & FERTILITY
# ══════════════════════════════════════════════════════════════
story.append(sec("👶", "PCOS & Fertility — What You Need to Know", C_ROSE))
story.append(Spacer(1, 5))
story.append(Paragraph(
"One of the most common concerns for women with PCOS is fertility. "
"PCOS is a leading cause of irregular ovulation, which makes it harder — but <b>not impossible</b> — "
"to get pregnant.", BODY))
story.append(Spacer(1, 5))
fert_items = [
"Most women with PCOS <b>can and do</b> get pregnant, either naturally or with medical help.",
"Losing weight (if you are overweight) is one of the most effective ways to restore ovulation.",
"Letrozole is the first-line fertility medication for PCOS (recommended over clomifene in current guidelines).",
"If tablets do not work, FSH injections or IVF may be offered.",
"Women with PCOS undergoing IVF have a higher risk of Ovarian Hyperstimulation Syndrome (OHSS) — your fertility team will monitor this closely.",
"During pregnancy, PCOS is associated with a slightly higher risk of gestational diabetes and high blood pressure — regular antenatal checks are important.",
]
story.append(bullet_table(fert_items, color=C_ROSE, bg=C_ROSE_LT))
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# SECTION 8 – LOOKING AFTER YOUR MENTAL HEALTH
# ══════════════════════════════════════════════════════════════
story.append(sec("🧠", "Looking After Your Mental Health", C_PURPLE))
story.append(Spacer(1, 5))
story.append(Paragraph(
"Living with PCOS can be emotionally challenging. Body image concerns, fertility worries, "
"and the ongoing nature of the condition mean that many women with PCOS experience anxiety, "
"depression, or low self-esteem. <b>You are not alone, and support is available.</b>", BODY))
story.append(Spacer(1, 5))
mh_items = [
"<b>Talk to your doctor:</b> If you feel low, anxious, or overwhelmed, tell your GP or specialist. Mental health support is part of PCOS care.",
"<b>Connect with others:</b> PCOS support groups (online and in person) can be enormously helpful.",
"<b>Cognitive Behavioural Therapy (CBT):</b> Evidence-based talking therapy that helps with anxiety and depression.",
"<b>Body-positive thinking:</b> Focus on health and how you feel, not just appearance.",
"<b>Regular exercise:</b> One of the best evidence-based treatments for mild to moderate depression and anxiety.",
]
story.append(bullet_table(mh_items, color=C_PURPLE, bg=C_PURPLE_LT))
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# SECTION 9 – QUESTIONS TO ASK YOUR DOCTOR
# ══════════════════════════════════════════════════════════════
story.append(sec("💬", "Questions to Ask Your Doctor", C_TEAL))
story.append(Spacer(1, 5))
qa_pairs = [
("Which PCOS features do I have? What is my phenotype?",
"Understanding your phenotype helps personalise your treatment."),
("Do I have insulin resistance? Should I take metformin?",
"Metformin is very helpful for some women but not all — depends on your results."),
("What blood tests do I need and when should I repeat them?",
"Ongoing monitoring is important for metabolic health."),
("What are my fertility options?",
"Even if you're not trying to get pregnant now, it's good to plan ahead."),
("Should I be referred to a specialist (endocrinologist, gynaecologist, dietitian)?",
"PCOS is best managed by a team when possible."),
("Are there any clinical trials I could join?",
"New treatments for PCOS are being studied — ask if any are available near you."),
]
for q, a in qa_pairs:
row = [[Paragraph(f"Q: {q}", Q), Paragraph(f"A: {a}", ANS)]]
t = Table(row, colWidths=[W*0.46, W*0.54])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(0,-1), C_TEAL_LT),
("BACKGROUND",(1,0),(1,-1), C_WHITE),
("BOX",(0,0),(-1,-1),0.6, C_GREY_LINE),
("LINEAFTER",(0,0),(0,-1),0.6, C_TEAL),
("TOPPADDING",(0,0),(-1,-1),6),
("BOTTOMPADDING",(0,0),(-1,-1),6),
("LEFTPADDING",(0,0),(-1,-1),7),
("RIGHTPADDING",(0,0),(-1,-1),7),
("VALIGN",(0,0),(-1,-1),"TOP"),
]))
story.append(t)
story.append(Spacer(1, 3))
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# SECTION 10 – KEY FACTS AT A GLANCE
# ══════════════════════════════════════════════════════════════
story.append(sec("⭐", "Key Facts at a Glance", C_ROSE))
story.append(Spacer(1, 5))
facts = [
("1 in 10", "women of reproductive age have PCOS worldwide"),
("2 of 3", "Rotterdam criteria needed to diagnose PCOS"),
("50–70%", "of women with PCOS have insulin resistance"),
("5–10%", "weight loss can restore periods and ovulation"),
("70%", "of PCOS women in the US have hirsutism"),
("2–6×", "increased risk of endometrial cancer if periods are very infrequent"),
]
fact_col = W / 3 - 0.2*cm
fact_rows_top = facts[:3]
fact_rows_bot = facts[3:]
def fact_cell(num, desc):
t = Table([
[Paragraph(f'<font color="#{C_ROSE.hexval()[2:]}"><b>{num}</b></font>', ps("FN", fontSize=20, fontName="Helvetica-Bold", alignment=TA_CENTER, leading=24))],
[Paragraph(desc, ps("FD", fontSize=9, fontName="Helvetica", alignment=TA_CENTER, leading=13, textColor=C_DARK_GREY))],
], colWidths=[fact_col])
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,-1), C_ROSE_LT),
("BOX",(0,0),(-1,-1),0.8, C_ROSE),
("TOPPADDING",(0,0),(-1,-1),6),
("BOTTOMPADDING",(0,0),(-1,-1),6),
]))
return t
row1 = [[fact_cell(n, d) for n, d in fact_rows_top]]
row2 = [[fact_cell(n, d) for n, d in fact_rows_bot]]
gap = 0.3*cm
fact_tbl1 = Table(row1, colWidths=[fact_col, fact_col, fact_col],
rowHeights=None)
fact_tbl1.setStyle(TableStyle([
("LEFTPADDING",(0,0),(-1,-1),2),
("RIGHTPADDING",(0,0),(-1,-1),2),
("TOPPADDING",(0,0),(-1,-1),0),
("BOTTOMPADDING",(0,0),(-1,-1),0),
]))
fact_tbl2 = Table(row2, colWidths=[fact_col, fact_col, fact_col])
fact_tbl2.setStyle(TableStyle([
("LEFTPADDING",(0,0),(-1,-1),2),
("RIGHTPADDING",(0,0),(-1,-1),2),
("TOPPADDING",(0,0),(-1,-1),0),
("BOTTOMPADDING",(0,0),(-1,-1),0),
]))
story.append(fact_tbl1)
story.append(Spacer(1, 4))
story.append(fact_tbl2)
story.append(Spacer(1, 8))
# ══════════════════════════════════════════════════════════════
# FOOTER
# ══════════════════════════════════════════════════════════════
story.append(HRFlowable(width=W, thickness=0.6, color=C_GREY_LINE))
story.append(Spacer(1, 4))
story.append(Paragraph(
"This handout is for educational purposes only and does not replace advice from your doctor or healthcare team. "
"Information based on: Rotterdam Criteria 2023 · Harrison's Principles of Internal Medicine 22E · "
"Berek & Novak's Gynecology · Henry's Clinical Diagnosis and Management by Laboratory Methods.",
SMALL))
story.append(Spacer(1, 3))
story.append(Paragraph(
"If you have concerns about your health, please speak to your GP, gynaecologist, or endocrinologist.",
SMALL))
doc.build(story)
print(f"PDF saved: {OUTPUT}")
cd /home/daytona/workspace/pcos-pdf && python generate_pcos_patient_handout.py 2>&1
ls -lh /home/daytona/workspace/pcos-pdf/PCOS_Patient_Handout.pdf
PCOS Patient Handout
PDF Document · PDF
| Section | Content |
|---|---|
| 1. What Is PCOS? | Simple explanation, prevalence (1 in 10 women), reassurance that it is not the patient's fault |
| 2. Common Symptoms | Table of 8 symptoms with plain-English explanations (irregular periods, hirsutism, acne, hair loss, weight gain, fatigue, mood, fertility) |
| 3. How Is PCOS Diagnosed? | Rotterdam 2023 criteria explained in lay terms - 2 of 3 features required, colour-coded boxes |
| 4. Blood Tests Explained | 10 tests (LH, FSH, Testosterone, AMH, 17-OH Progesterone, Prolactin, TSH, Fasting glucose/HbA1c, Insulin/HOMA-IR, DHEA-S, Lipids) - each with a plain-language "what it checks and why" explanation. Timing tip box included. |
| 5. Long-Term Health Risks | Table: diabetes, cardiovascular disease, endometrial cancer, sleep apnoea, mental health, fertility - with risk reduction strategies |
| 6. Treatment Options | Lifestyle changes (first-line), plus medication table: OCP, Metformin, anti-androgens, Clomifene/Letrozole, FSH injections, Inositol, Eflornithine |
| 7. PCOS & Fertility | Reassuring bullets: most women can conceive, letrozole is first-line, IVF options, OHSS risk, pregnancy monitoring |
| 8. Mental Health | CBT, support groups, exercise, body-positive framing |
| 9. Questions to Ask Your Doctor | 6 ready-to-use Q&A pairs patients can bring to appointments |
| 10. Key Facts at a Glance | 6 visual statistic boxes (1 in 10, 2 of 3, 50-70%, 5-10%, 70%, 2-6×) |