metabolic encephalopathy via kidney
uremic encephalopathy pathophysiology treatment
| Test | Finding |
|---|---|
| EEG | Generalized slowing with excess theta and delta activity; bilateral spike-wave complexes possible; EEG severity tracks clinical status and improves with treatment |
| CSF | Increased protein (<1 g/L); slight pleocytosis (<25 cells/mL); pressure normal |
| Brain MRI | White matter lesions (small vessel disease); brain edema on DWI; decreased brain volume in chronic cases; reduced frontal cortical perfusion |
| Brain imaging vs liver | In hepatic encephalopathy, characteristic signal changes (T1 hyperintensity in globus pallidus); in uremia, imaging is largely non-specific |
| Serum Ca, Mg | Check - hypocalcemia and hypomagnesemia can mimic uremic encephalopathy |
| Feature | Uremic Encephalopathy | Hepatic Encephalopathy |
|---|---|---|
| Primary toxins | Guanidino compounds | Ammonia |
| Receptor effect | NMDA agonism + GABA antagonism (excitatory) | GABAergic enhancement (inhibitory) |
| Motor | Myoclonus, twitch-convulsive syndrome prominent | Asterixis prominent |
| Brain imaging | Non-specific, no characteristic signal change | T1 hyperintensity globus pallidus (Mn deposition) |
| Brain inflammation | Prominent (vascular permeability increased) | Less prominent |
| Treatment | RRT / transplant | Lactulose, rifaximin, liver transplant |
copd
"chronic obstructive pulmonary disease"[MeSH Terms] AND management
| Subtype | Location | Primary Cause |
|---|---|---|
| Centriacinar (centrilobular) | Central acinus, respiratory bronchioles; upper lobe predominant | Cigarette smoking (most common type) |
| Panacinar (panlobular) | Entire acinus uniformly; lower lobe predominant | Alpha-1 antitrypsin deficiency |
| Paraseptal | Distal acinus, subpleural | Associated with spontaneous pneumothorax in young adults |
| Feature | "Pink Puffer" (Emphysema) | "Blue Bloater" (Chronic Bronchitis) |
|---|---|---|
| Build | Thin, cachectic | Overweight |
| Color | Pink (no cyanosis at rest) | Cyanotic ("blue") |
| Cough | Minimal | Prominent, productive |
| Dyspnea | Severe | Moderate |
| PaO2 | Near normal at rest | Low |
| PaCO2 | Low or normal | Elevated |
| Cor pulmonale | Late | Early |
| Polycythemia | Absent | Present |
| GOLD Grade | Severity | FEV1 % Predicted |
|---|---|---|
| GOLD 1 | Mild | ≥ 80% |
| GOLD 2 | Moderate | 50-79% |
| GOLD 3 | Severe | 30-49% |
| GOLD 4 | Very Severe | < 30% |
| Drug Class | Examples | Notes |
|---|---|---|
| Short-acting β2 agonist (SABA) | Albuterol (salbutamol) | Rescue therapy; also effective in COPD despite incomplete reversibility |
| Short-acting anticholinergic (SAMA) | Ipratropium bromide | Blocks muscarinic receptors; SABA + SAMA combination more effective than either alone |
| Long-acting β2 agonist (LABA) | Salmeterol, formoterol, indacaterol | For persistent dyspnea; can combine with LAMA |
| Long-acting anticholinergic/antimuscarinic (LAMA) | Tiotropium, umeclidinium, glycopyrronium | Preferred long-acting bronchodilator in COPD; reduces exacerbations more than LABA alone |
| LABA + LAMA combination | Indacaterol/glycopyrronium, etc. | Superior to monotherapy; first-line for Group B and many Group E |
| Intervention | Evidence/Notes |
|---|---|
| Smoking cessation | Most effective intervention to slow decline; reduces FEV1 loss rate |
| Long-term oxygen therapy (LTOT) | For PaO2 ≤55 mmHg (or ≤59 mmHg with cor pulmonale); improves survival in hypoxemic COPD |
| Pulmonary rehabilitation | Improves exercise tolerance, dyspnea, and quality of life; reduces hospitalizations |
| Vaccination | Influenza, pneumococcal, COVID-19, RSV (newer evidence) |
| Non-invasive ventilation (NIV/NIPPV) | For chronic hypercapnia, especially nocturnal; reduces persistent hypercapnia |
| Lung volume reduction surgery (LVRS) | Selected patients with upper-lobe emphysema and low exercise capacity after rehabilitation |
| Endobronchial valves | Bronchoscopic lung volume reduction for suitable emphysema |
| Lung transplantation | End-stage disease; improves quality of life |
give both in a pdf
mkdir -p /tmp/workspace/medical-notes && ls /tmp/workspace/medical-notes/
~/medical-notes/generate_notes_pdf.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, KeepTogether
)
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
from reportlab.platypus import ListFlowable, ListItem
OUTPUT = "/tmp/workspace/medical-notes/Medical_Notes_UE_COPD.pdf"
doc = SimpleDocTemplate(
OUTPUT,
pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm,
topMargin=2.2*cm, bottomMargin=2.2*cm,
title="Medical Notes: Uremic Encephalopathy & COPD",
author="Orris Medical Assistant"
)
W = A4[0] - 4*cm # usable width
# ── Colour palette ──────────────────────────────────────────────────────────
C_DARK_BLUE = colors.HexColor("#1a3a5c")
C_MID_BLUE = colors.HexColor("#2c6fad")
C_LIGHT_BLUE = colors.HexColor("#d6e8f8")
C_ACCENT = colors.HexColor("#e8f4e8")
C_ORANGE = colors.HexColor("#e07b39")
C_TBL_HDR = colors.HexColor("#2c6fad")
C_TBL_ALT = colors.HexColor("#eef5fb")
C_RULE = colors.HexColor("#2c6fad")
C_BOX_BG = colors.HexColor("#fff8e1")
C_BOX_BORDER = colors.HexColor("#f0a500")
# ── Styles ───────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
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
sTitle = make_style("sTitle", fontName="Helvetica-Bold", fontSize=22,
textColor=C_DARK_BLUE, alignment=TA_CENTER, spaceAfter=4)
sSubtitle = make_style("sSubtitle", fontName="Helvetica", fontSize=11,
textColor=C_MID_BLUE, alignment=TA_CENTER, spaceAfter=14)
sChapterHead = make_style("sChapterHead",fontName="Helvetica-Bold", fontSize=17,
textColor=colors.white, spaceBefore=6, spaceAfter=6, leading=22)
sH1 = make_style("sH1", fontName="Helvetica-Bold", fontSize=13,
textColor=C_DARK_BLUE, spaceBefore=12, spaceAfter=4, leading=17)
sH2 = make_style("sH2", fontName="Helvetica-Bold", fontSize=11,
textColor=C_MID_BLUE, spaceBefore=8, spaceAfter=3, leading=15)
sBody = make_style("sBody", fontName="Helvetica", fontSize=9.5,
leading=14, spaceAfter=4, alignment=TA_JUSTIFY)
sBullet = make_style("sBullet", fontName="Helvetica", fontSize=9.5,
leading=13, spaceAfter=2, leftIndent=14, firstLineIndent=-10)
sNote = make_style("sNote", fontName="Helvetica-Oblique",fontSize=9,
textColor=colors.HexColor("#555555"), leading=13, spaceAfter=4)
sSource = make_style("sSource", fontName="Helvetica-Oblique",fontSize=8.5,
textColor=colors.HexColor("#444444"), leading=12, spaceAfter=2)
sTableHdr = make_style("sTableHdr", fontName="Helvetica-Bold", fontSize=9,
textColor=colors.white, alignment=TA_CENTER, leading=12)
sTableCell = make_style("sTableCell", fontName="Helvetica", fontSize=9,
leading=12, alignment=TA_LEFT)
sTableCellC = make_style("sTableCellC", fontName="Helvetica", fontSize=9,
leading=12, alignment=TA_CENTER)
# ── Helpers ───────────────────────────────────────────────────────────────────
def rule(color=C_RULE, thickness=1.2):
return HRFlowable(width="100%", thickness=thickness, color=color, spaceAfter=6, spaceBefore=2)
def h1(txt):
return [Spacer(1, 4), Paragraph(txt, sH1), rule(C_MID_BLUE, 0.8)]
def h2(txt):
return [Paragraph(txt, sH2)]
def body(txt):
return Paragraph(txt, sBody)
def bullet(items):
rows = []
for it in items:
rows.append(Paragraph(f"• {it}", sBullet))
return rows
def spacer(h=6):
return Spacer(1, h)
def chapter_banner(text, color=C_DARK_BLUE):
"""Full-width coloured banner for chapter heading."""
tbl = Table([[Paragraph(text, sChapterHead)]], colWidths=[W])
tbl.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), color),
("LEFTPADDING", (0,0), (-1,-1), 12),
("RIGHTPADDING", (0,0), (-1,-1), 12),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING",(0,0), (-1,-1), 8),
("ROUNDEDCORNERS", [4]),
]))
return tbl
def make_table(headers, rows, col_widths=None, alt=True):
data = [[Paragraph(h, sTableHdr) for h in headers]]
for i, row in enumerate(rows):
data.append([Paragraph(str(c), sTableCell) for c in row])
if col_widths is None:
col_widths = [W / len(headers)] * len(headers)
tbl = Table(data, colWidths=col_widths, repeatRows=1)
style = [
("BACKGROUND", (0,0), (-1,0), C_TBL_HDR),
("ROWBACKGROUNDS",(0,1), (-1,-1), [colors.white, C_TBL_ALT] if alt else [colors.white]),
("GRID", (0,0), (-1,-1), 0.4, colors.HexColor("#bbbbbb")),
("VALIGN", (0,0), (-1,-1), "TOP"),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
]
tbl.setStyle(TableStyle(style))
return tbl
def key_box(label, text):
inner = Table([
[Paragraph(f"<b>{label}</b>", sTableCell)],
[Paragraph(text, sTableCell)],
], colWidths=[W-16])
inner.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), C_BOX_BG),
("BOX", (0,0), (-1,-1), 1.2, C_BOX_BORDER),
("LEFTPADDING", (0,0), (-1,-1), 8),
("RIGHTPADDING", (0,0), (-1,-1), 8),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING",(0,0), (-1,-1), 5),
]))
return inner
# ════════════════════════════════════════════════════════════════════════════
# CONTENT
# ════════════════════════════════════════════════════════════════════════════
story = []
# ── Cover ────────────────────────────────────────────────────────────────────
story.append(Spacer(1, 3*cm))
story.append(Paragraph("Medical Notes", sTitle))
story.append(Paragraph("Uremic Encephalopathy | COPD", sSubtitle))
story.append(Spacer(1, 0.5*cm))
story.append(rule(C_DARK_BLUE, 2))
story.append(Spacer(1, 0.3*cm))
story.append(Paragraph(
"Sources: Comprehensive Clinical Nephrology 7th Ed. · Brenner & Rector's The Kidney · "
"Adams & Victor's Neurology 12th Ed. · Bradley & Daroff's Neurology · "
"Fishman's Pulmonary Diseases · Robbins & Kumar Basic Pathology · "
"Katzung's Pharmacology 16th Ed. · Costanzo Physiology 7th Ed.", sSource))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════════
# PART 1 – UREMIC ENCEPHALOPATHY
# ════════════════════════════════════════════════════════════════════════════
story.append(chapter_banner("PART 1 — UREMIC ENCEPHALOPATHY (Metabolic Encephalopathy via Kidney)", C_DARK_BLUE))
story.append(spacer(10))
story.append(body(
"Uremic encephalopathy (UE) is a syndrome of brain dysfunction resulting from renal failure — "
"either acute kidney injury (AKI) or end-stage kidney disease (ESKD). It ranges from "
"imperceptible cognitive changes to coma and death. Importantly, <b>the degree of azotemia "
"alone correlates poorly</b> with the presence or severity of encephalopathy, and symptoms "
"characteristically fluctuate hour to hour or day to day."
))
story.append(spacer())
# Pathophysiology
story += h1("1. Pathophysiology")
story += h2("1.1 Uremic Toxin Accumulation")
story.append(body(
"Among the 75 solutes in the European Uremic Toxin (EUTox) database, ~9% (7 solutes) are "
"directly associated with neurologic effects. The most important are the <b>guanidino compounds</b>:"
))
story += bullet([
"Guanidinouscinic acid and methylguanidine are increased <b>100-fold</b> in uremic brain tissue and CSF.",
"Antagonize <b>GABA receptors</b> (inhibitory) while simultaneously acting as <b>NMDA receptor agonists</b> (excitatory) → enhanced cortical excitability.",
"Guanidinosuccinic acid, methylguanidine, and homoarginine can directly induce seizures via NMDA receptors and calcium channel modulation.",
"Gut microbial metabolites (phenylalanine, benzoate, glutamate metabolites) also linked to cognitive impairment in dialysis patients.",
])
story.append(spacer())
story += h2("1.2 Blood-Brain Barrier Disruption")
story += bullet([
"Kidney injury activates inflammatory cytokines that cross or disrupt the BBB.",
"Animal models show increased brain inflammation + vascular permeability in uremic encephalopathy (distinct pattern from hepatic encephalopathy).",
"<b>Asymmetric dimethylarginine (ADMA)</b> — elevated in CKD — inhibits eNOS and correlates with cerebrovascular complications.",
"Indoxyl sulfate, polyamines, myoinositol, carnitine disrupt solute transport and neuronal membrane permeability.",
])
story.append(spacer())
story += h2("1.3 Secondary Hyperparathyroidism")
story += bullet([
"Brain calcium content doubles within days of acute renal failure onset.",
"PTH elevates alkaline phosphatase in brain → promotes tau binding to muscarinic receptors in hippocampus → increased intracellular Ca²⁺ → neuronal cell death.",
"EEG slowing correlates with N-terminal PTH fragment levels.",
"1,25-dihydroxyvitamin D treatment improves EEG and reduces PTH.",
"High FGF-23 (via α-klotho interaction) and low α-klotho both associated with increased dementia risk.",
])
story.append(spacer())
story += h2("1.4 Neurotransmitter Disruption")
story += bullet([
"Norepinephrine depletion and central <b>dopamine suppression</b> — impairs motor activity.",
"High tryptophan entry across BBB → excess serotonin synthesis → anorexia.",
"Neuropeptide Y (NPY) levels dysregulated — promotes endothelial dysfunction.",
])
story.append(spacer())
story += h2("1.5 Drug Accumulation")
story.append(body("Reduced renal clearance → toxic accumulation of:"))
story += bullet([
"<b>Meperidine metabolites</b> (renal cation secretory transport impairment)",
"<b>Cimetidine, acyclovir</b> (OAT3 inhibition)",
"<b>Metoclopramide, phenothiazines, gabapentin, opioids</b> — provoke asterixis and myoclonus in CKD",
])
story.append(spacer(10))
# Clinical Manifestations
story += h1("2. Clinical Manifestations")
story.append(make_table(
["Stage", "Mental Features", "Motor Features"],
[
["Early", "Mood swings, irritability, apathy, fatigue\nImpaired concentration, loss of recent memory\nInsomnia, depression", "Fine action tremor\nAsterixis (intermittent loss of antigravity muscle tone)\nMyoclonus (lightning-quick arrhythmic jerks)\nHyperreflexia, dysarthria, altered gait"],
["Late / Severe", "Confusion, delirium, psychosis\nHallucinations, delusions\nSeizures (uremic twitch-convulsive syndrome)\nStupor → quiet coma", "Incessant myoclonic twitches (wakefulness and sleep)\nCheyne-Stokes breathing\nKussmaul breathing (metabolic acidosis)"],
],
col_widths=[2.5*cm, 7.5*cm, 7.5*cm]
))
story.append(spacer(4))
story.append(Paragraph(
"About <b>30%</b> of dialysis patients have mild neuropsychiatric symptoms; ~<b>10%</b> exhibit severe impairment. "
"Advanced UE with confusion/coma is now predominantly seen in patients where a decision has been made not to start dialysis.",
sNote))
story.append(spacer(10))
# Investigations
story += h1("3. Investigations")
story.append(make_table(
["Test", "Finding"],
[
["EEG", "Generalised slowing with excess theta and delta activity; bilateral spike-wave complexes; severity tracks clinical status; normalises with treatment"],
["CSF", "Increased protein (<1 g/L); slight pleocytosis (<25 cells/mL); pressure normal"],
["Brain MRI", "Non-specific; white matter lesions (small vessel disease); brain oedema on DWI; decreased brain volume in chronic disease; reduced frontal perfusion on perfusion imaging"],
["Serum Ca/Mg", "Must check — hypocalcaemia and hypomagnesaemia can mimic UE"],
["Serum BUN/Creatinine", "Elevated but degree does NOT directly correlate with encephalopathy severity"],
],
col_widths=[3.5*cm, 14*cm]
))
story.append(spacer(10))
# Diagnosis
story += h1("4. Diagnosis")
story.append(key_box("Diagnostic Criteria",
"1. Characteristic symptoms (as above) in a patient with severe renal dysfunction. "
"2. Exclusion of other causes. "
"3. Confirmation: symptoms disappear with successful renal replacement therapy."))
story.append(spacer(6))
story.append(body("<b>Key differential:</b> Hypertensive encephalopathy (PRES) — the myoclonic-twitch syndrome "
"is NOT a feature of hypertensive encephalopathy; its presence points to true uremia. "
"Volhard introduced 'pseudouremia' for hypertensive cerebral effects. "
"Also exclude: subdural haematoma (coagulopathy/hypertension in uremia), meningitis, drug toxicity, metabolic acidosis."))
story.append(spacer(10))
# Comparison with Hepatic Encephalopathy
story += h1("5. Uremic vs Hepatic Encephalopathy")
story.append(make_table(
["Feature", "Uremic Encephalopathy", "Hepatic Encephalopathy"],
[
["Primary toxins", "Guanidino compounds", "Ammonia"],
["Receptor effect", "NMDA agonism + GABA antagonism (excitatory)", "GABAergic enhancement (inhibitory)"],
["Dominant motor sign", "Myoclonus; uremic twitch-convulsive syndrome", "Asterixis prominent"],
["Brain MRI", "Non-specific; no characteristic signal change", "T1 hyperintensity globus pallidus (Mn deposition)"],
["Brain inflammation", "Prominent; increased vascular permeability", "Less prominent"],
["Treatment", "Renal replacement therapy / transplant", "Lactulose, rifaximin, liver transplant"],
],
col_widths=[4*cm, 8*cm, 5.5*cm]
))
story.append(spacer(10))
# Treatment
story += h1("6. Treatment")
story += h2("6.1 Renal Replacement Therapy (Primary Treatment)")
story += bullet([
"<b>Haemodialysis or peritoneal dialysis</b> — symptoms typically regress within days to weeks.",
"<b>Renal transplantation</b> — most effective; resolution within days; eliminates uremic solutes across a wide molecular-weight range.",
"Mild symptoms may persist even after adequate dialysis.",
])
story.append(spacer(4))
story += h2("6.2 Supportive Management")
story += bullet([
"Correct metabolic acidosis",
"Treat secondary hyperparathyroidism (1,25-dihydroxyvitamin D)",
"Treat anaemia with erythropoietin",
"Avoid or dose-adjust renally-cleared neurotoxic drugs (especially meperidine, gabapentin, metoclopramide)",
"Antiepileptics for seizures — use with caution and dose-adjust for renal failure",
])
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════════
# PART 2 – COPD
# ════════════════════════════════════════════════════════════════════════════
story.append(chapter_banner("PART 2 — CHRONIC OBSTRUCTIVE PULMONARY DISEASE (COPD)", C_MID_BLUE))
story.append(spacer(10))
story.append(body(
"COPD is a common, preventable, and treatable disease characterised by <b>persistent, "
"not fully reversible airflow limitation</b> that is progressive and associated with an "
"enhanced chronic inflammatory response to noxious particles or gases — primarily cigarette smoke. "
"It is the <b>third most common cause of death</b> in the United States and accounts for "
">$40 billion/year in direct and indirect healthcare costs."
))
story.append(spacer())
# Definition
story += h1("1. Definition & Subtypes")
story.append(make_table(
["Subtype", "Definition", "Key Features"],
[
["Chronic Bronchitis", "Persistent productive cough ≥3 months/year for ≥2 consecutive years", "Mucus hypersecretion from large airway gland hyperplasia; obstruction from small airway inflammation (chronic bronchiolitis)"],
["Emphysema", "Enlargement of airspaces distal to terminal bronchioles with alveolar wall destruction", "Loss of elastic recoil; air trapping; increased lung compliance; barrel chest"],
],
col_widths=[3.5*cm, 6*cm, 8*cm]
))
story.append(spacer(6))
story.append(Paragraph(
"Most patients have a mixture of both subtypes. The two classic phenotypes are the "
"'<b>Pink Puffer</b>' (emphysema-predominant: thin, non-cyanotic, severe dyspnoea, near-normal PaO₂) "
"and the '<b>Blue Bloater</b>' (bronchitis-predominant: cyanotic, productive cough, elevated PaCO₂, cor pulmonale).",
sNote))
story.append(spacer(10))
# Epidemiology & Risk Factors
story += h1("2. Risk Factors")
story += bullet([
"<b>Cigarette smoking</b> — major risk factor; radiographic evidence of progressive changes even in smokers with normal spirometry.",
"<b>Alpha-1 antitrypsin (A1AT) deficiency</b> — A1AT inhibits neutrophil elastase; deficiency → unchecked protease activity → panacinar emphysema (lower lobe predominant). Screen all patients with persistent airflow obstruction, especially young patients/non-smokers.",
"Air pollution, occupational dusts and fumes",
"Recurrent respiratory infections in childhood",
"Asthma (risk factor for accelerated FEV₁ decline)",
])
story.append(spacer(10))
# Pathology
story += h1("3. Pathology")
story += h2("3.1 Emphysema Subtypes")
story.append(make_table(
["Subtype", "Location", "Cause"],
[
["Centriacinar (centrilobular)", "Central acinus, respiratory bronchioles; upper lobe predominant", "Cigarette smoking (most common type)"],
["Panacinar (panlobular)", "Entire acinus uniformly; lower lobe predominant", "Alpha-1 antitrypsin deficiency"],
["Paraseptal", "Distal acinus, subpleural", "Associated with spontaneous pneumothorax in young adults"],
],
col_widths=[4.5*cm, 6.5*cm, 6.5*cm]
))
story.append(spacer(6))
story += h2("3.2 Mechanism — Protease-Antiprotease Imbalance")
story += bullet([
"Cigarette smoke recruits <b>neutrophils and macrophages</b> → release of <b>neutrophil elastase</b> and matrix metalloproteinases (MMPs).",
"These destroy <b>elastin</b> in alveolar walls.",
"A1AT normally counterbalances this; when balance is disrupted → emphysema.",
])
story.append(spacer(6))
story += h2("3.3 Chronic Bronchitis Pathology")
story += bullet([
"Hyperplasia of mucus-secreting glands (Reid index increased — ratio of gland thickness to wall thickness).",
"Goblet cell metaplasia in small airways.",
"Chronic bronchiolitis causes obstruction (not the large-airway mucus gland hypertrophy).",
"MUC5AC concentration increased <b>10-fold</b> and MUC5B <b>3-fold</b> in severe COPD.",
"Small airway mucus occlusion correlates with degree of airflow obstruction and <b>predicts longevity</b>.",
])
story.append(spacer(10))
# Pathophysiology
story += h1("4. Pathophysiology")
story += h2("4.1 Airflow Obstruction")
story += bullet([
"Reduced FEV₁ with normal or near-normal FVC → reduced <b>FEV₁/FVC ratio (<0.70 post-bronchodilator)</b> = diagnostic criterion.",
"Emphysema: loss of elastic recoil → dynamic airway collapse on expiration → air trapping.",
"Chronic bronchitis: luminal narrowing from inflammation, mucus, and wall remodelling.",
])
story.append(spacer(4))
story += h2("4.2 Hyperinflation")
story += bullet([
"<b>Static hyperinflation:</b> Increased lung compliance raises equilibrium volume of respiratory system.",
"<b>Dynamic hyperinflation:</b> During exercise, insufficient expiratory time → air trapping → EELV fails to decline → IRV shrinks → tidal volume augmentation impaired.",
"Results in: increased work of breathing, <b>diaphragm flattening</b> (mechanical disadvantage), 'neuromechanical uncoupling'.",
"Diaphragm muscle changes: reduced force/cross-sectional area, reduced myosin heavy chain content, decreased Ca²⁺ sensitivity, slower cross-bridge cycling kinetics.",
"Classic sign: <b>barrel chest</b> (increased AP diameter).",
])
story.append(spacer(4))
story += h2("4.3 V/Q Mismatch & Gas Exchange")
story += bullet([
"Primary cause of hypoxemia in COPD — underventilated alveoli with continued perfusion.",
"Widened <b>A-a gradient</b>; PaO₂ falls below calculated PAO₂.",
"Exercise-related desaturation more common in emphysema-predominant than bronchitis-predominant disease.",
"<b>Type 1 RF</b> (hypoxaemia alone): early/moderate COPD; patient hyperventilates → PaCO₂ normal or low.",
"<b>Type 2 RF</b> (hypoxaemia + hypercapnia): PaCO₂ does not usually rise until FEV₁ falls to <b>20-25% of predicted</b>.",
"Persistent hypercapnia = poor prognostic sign; survival shorter than normocapnic COPD.",
])
story.append(spacer(4))
story.append(key_box("ABG Example — Moderate COPD with Hyperventilation",
"pH 7.47 (alkalotic) | PaO₂ 60 mmHg | PaCO₂ 30 mmHg | SaO₂ 90%\n"
"Interpretation: Hypoxaemia from V/Q mismatch + compensatory respiratory alkalosis "
"driven by hypoxaemia stimulating peripheral chemoreceptors."))
story.append(spacer(10))
# Spirometry / GOLD
story += h1("5. Spirometry & GOLD Classification")
story.append(body("<b>Diagnosis requires:</b> Post-bronchodilator FEV₁/FVC < 0.70"))
story.append(spacer(4))
story.append(make_table(
["GOLD Grade", "Severity", "FEV₁ % Predicted"],
[
["GOLD 1", "Mild", "≥ 80%"],
["GOLD 2", "Moderate", "50–79%"],
["GOLD 3", "Severe", "30–49%"],
["GOLD 4", "Very Severe", "< 30%"],
],
col_widths=[4*cm, 6*cm, 7.5*cm]
))
story.append(spacer(6))
story.append(make_table(
["GOLD Group", "Symptoms (mCAT/mMRC)", "Exacerbation History"],
[
["Group A", "Low (mCAT<10 or mMRC<2)", "0–1 exacerbation (no hospitalisation)"],
["Group B", "High (mCAT≥10 or mMRC≥2)", "0–1 exacerbation (no hospitalisation)"],
["Group E", "Any", "≥2 exacerbations OR ≥1 leading to hospitalisation"],
],
col_widths=[3*cm, 7*cm, 7.5*cm]
))
story.append(spacer(10))
# Clinical Features
story += h1("6. Clinical Features")
story += h2("Symptoms")
story += bullet([
"Progressive exertional <b>dyspnoea</b> (hallmark)",
"Chronic productive cough (especially morning sputum in bronchitis)",
"Wheezing",
"Exercise intolerance",
])
story += h2("Signs")
story += bullet([
"Barrel chest, increased AP diameter",
"Hyperresonance to percussion",
"Diminished breath sounds",
"Prolonged expiratory phase, expiratory wheeze",
"Use of accessory muscles",
"Cyanosis (bronchitic type)",
"Asterixis (in hypercapnic encephalopathy)",
"Signs of cor pulmonale: elevated JVP, peripheral oedema, loud P₂",
])
story.append(spacer(10))
# Complications
story += h1("7. Complications")
story.append(make_table(
["Complication", "Mechanism / Notes"],
[
["Cor pulmonale", "Pulmonary hypertension from hypoxic vasoconstriction → RV hypertrophy/failure"],
["Polycythaemia", "Compensatory erythrocytosis from chronic hypoxaemia"],
["Acute exacerbations (AECOPD)", "Usually viral URTI (rhinovirus) or bacterial (H. influenzae, S. pneumoniae, M. catarrhalis), air pollution, PE"],
["Spontaneous pneumothorax", "Emphysema with bullae rupture"],
["Respiratory failure (Type 1 or 2)", "Progressive airflow obstruction and muscle fatigue"],
["Lung cancer", "Risk independently elevated; smoking common aetiology"],
["Cardiovascular disease", "Major comorbidity and leading cause of death in mild-moderate COPD"],
["Cachexia / muscle wasting", "Systemic inflammation, hypoxaemia, nutritional depletion"],
],
col_widths=[5*cm, 12.5*cm]
))
story.append(spacer(10))
# Treatment
story += h1("8. Treatment")
story += h2("8.1 Pharmacological — Stable COPD")
story.append(make_table(
["Drug Class", "Examples", "Role"],
[
["SABA", "Albuterol (salbutamol)", "Rescue; acute symptom relief"],
["SAMA", "Ipratropium bromide", "Rescue; blocks muscarinic receptors; combines with SABA"],
["LABA", "Salmeterol, formoterol, indacaterol", "Persistent dyspnoea; combine with LAMA for additive benefit"],
["LAMA", "Tiotropium, umeclidinium, glycopyrronium", "Preferred long-acting bronchodilator; reduces exacerbations more than LABA alone"],
["LABA + LAMA", "Indacaterol/glycopyrronium etc.", "Superior to monotherapy; first-line for Groups B and E"],
["ICS", "Fluticasone, budesonide", "Only in severe obstruction, frequent exacerbations, or asthma overlap. Use blood eosinophils to guide: ≥300/µL = benefit; low = avoid (pneumonia risk)"],
["Roflumilast (PDE4 inhibitor)", "Roflumilast", "Severe COPD with chronic bronchitis phenotype; reduces exacerbation frequency"],
["Azithromycin (long-term)", "Azithromycin", "Reduces exacerbations in selected patients; monitor for hearing loss, arrhythmia, resistance"],
],
col_widths=[4*cm, 4.5*cm, 9*cm]
))
story.append(spacer(6))
story += h2("8.2 Non-Pharmacological")
story.append(make_table(
["Intervention", "Notes"],
[
["Smoking cessation", "Most effective; slows FEV₁ decline rate"],
["Long-term O₂ therapy (LTOT)", "PaO₂ ≤55 mmHg (or ≤59 mmHg with cor pulmonale); improves survival"],
["Pulmonary rehabilitation", "Improves exercise tolerance, dyspnoea, QoL; reduces hospitalisations"],
["Vaccination", "Influenza, pneumococcal, COVID-19, RSV"],
["NIV / NIPPV (BiPAP)", "For chronic hypercapnia (nocturnal); reduces persistent CO₂ retention"],
["Lung volume reduction surgery", "Selected patients: upper-lobe emphysema + low exercise capacity post-rehabilitation"],
["Endobronchial valves", "Bronchoscopic lung volume reduction in suitable emphysema"],
["Lung transplantation", "End-stage disease; improves QoL"],
],
col_widths=[5*cm, 12.5*cm]
))
story.append(spacer(6))
story += h2("8.3 Acute Exacerbations (AECOPD)")
story += bullet([
"<b>Short-acting bronchodilators</b> (SABA ± SAMA) — increase dose and frequency.",
"<b>Systemic corticosteroids</b> — 5-day oral prednisolone course (equivalent to longer courses).",
"<b>Antibiotics</b> — routinely given (unlike asthma); β-lactams, macrolides, doxycycline, or fluoroquinolones.",
"<b>Controlled O₂</b> — target SpO₂ <b>88–92%</b> to avoid suppressing hypoxic drive in chronic CO₂ retainers.",
"<b>NIV (BiPAP)</b> — for hypercapnic respiratory failure; reduces need for intubation and mortality.",
"Mortality of AECOPD greater than asthma exacerbations due to older age and cardiovascular comorbidities.",
])
story.append(spacer(10))
# Prognosis
story += h1("9. Prognosis — BODE Index")
story.append(body(
"The <b>BODE Index</b> is a better predictor of mortality than FEV₁ alone. It incorporates:"
))
story += bullet([
"<b>B</b>ody mass index (low BMI = worse prognosis)",
"airflow <b>O</b>bstruction (FEV₁ % predicted)",
"<b>D</b>yspnoea (mMRC score)",
"<b>E</b>xercise capacity (6-minute walk distance)",
])
story.append(spacer(4))
story.append(Paragraph(
"Persistent hypercapnia after an acute exacerbation is an independent poor prognostic indicator. "
"Survival in hypercapnic COPD is shorter than in normocapnic COPD.",
sNote))
story.append(spacer(16))
story.append(rule(C_DARK_BLUE, 1.5))
story.append(Paragraph(
"Sources: Comprehensive Clinical Nephrology 7th Ed. · Brenner & Rector's The Kidney · "
"Adams & Victor's Principles of Neurology 12th Ed. · Bradley & Daroff's Neurology · "
"Fishman's Pulmonary Diseases and Disorders · Robbins & Kumar Basic Pathology · "
"Katzung's Basic & Clinical Pharmacology 16th Ed. · Costanzo Physiology 7th Ed. · "
"Murray & Nadel's Textbook of Respiratory Medicine · GOLD Guidelines (goldcopd.org)",
sSource))
# ── Build ─────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"PDF saved to {OUTPUT}")
cd /tmp/workspace/medical-notes && python generate_notes_pdf.py 2>&1
ls -lh /tmp/workspace/medical-notes/Medical_Notes_UE_COPD.pdf
Medical Notes UE COPD
PDF Document · PDF
common ecg pattern
| Wave/Interval | What It Represents | Normal Value |
|---|---|---|
| P wave | Atrial depolarization (SA node → AV node) | <120 ms, upright in II, inverted in aVR |
| PR interval | AV conduction time (atria → ventricles) | 120-200 ms |
| QRS complex | Ventricular depolarization | <120 ms (narrow) |
| ST segment | Early ventricular repolarization | Isoelectric (flat) |
| T wave | Ventricular repolarization | Upright in most leads |
| QT interval | Total ventricular electrical activity | Corrected QTc <440 ms (men), <460 ms (women) |



| ECG Leads with ST Elevation | Infarct Territory | Culprit Artery |
|---|---|---|
| V1-V4 (anterior) | Anterior wall | LAD (left anterior descending) |
| II, III, aVF (inferior) | Inferior wall | RCA or LCx |
| I, aVL, V5-V6 (lateral) | Lateral wall | LCx or diagonal branch of LAD |
| V1-V2 tall R wave (posterior) | Posterior wall | RCA or LCx |
| V4R (right-sided lead) | Right ventricle | Proximal RCA |


| Condition | Key ECG Finding |
|---|---|
| Hyperkalaemia | Peaked T waves → widened QRS → sine wave → VF/asystole |
| Hypokalaemia | Flattened/inverted T waves, prominent U waves, prolonged QU |
| Hypercalcaemia | Short QT interval |
| Hypocalcaemia | Prolonged QT interval |
| Digoxin toxicity | "Salvador Dali moustache" (scooped ST depression), bradycardia, AV blocks |
| Hypothermia | Osborn (J) waves — positive deflection at J point, bradycardia |
| Pericarditis | Diffuse saddle-shaped ST elevation in all leads except aVR; PR depression |
| Pattern | Rate | Rhythm | P Waves | QRS | Key Clue |
|---|---|---|---|---|---|
| Normal sinus | 60-100 | Regular | Upright in II, every QRS | Narrow | Baseline |
| Atrial flutter | 75-150 (V rate) | Regular | Sawtooth ~300/min | Narrow | 150 bpm tachycardia |
| Atrial fibrillation | Variable | Irregularly irregular | Absent/chaotic | Narrow | No two R-R equal |
| 1° AV block | Normal | Regular | Normal | Narrow | PR >200 ms |
| Mobitz I | Normal | Regularly irregular | Group beating | Narrow | Progressive PR → dropped beat |
| Mobitz II | Normal/slow | Regularly irregular | Fixed PR then dropped | Wide | Sudden dropped beat |
| Complete heart block | Slow escape | Regular (2 independent) | Dissociated from QRS | Wide/narrow escape | P unrelated to QRS |
| STEMI | Variable | Variable | Variable | ST elevated | Contiguous leads, reciprocal changes |
| VT | >100 | Regular | Dissociated (AV dissoc.) | Wide (>120 ms) | Fusion/capture beats |
| Torsades | 200-250 | Irregular | Absent | Wide, twisting | Long QT precedes it |
| RBBB | Normal | Regular | Normal | Wide: RSR' in V1 | Terminal R in V1 |
| LBBB | Normal | Regular | Normal | Wide: broad R I/V6 | No Q in lateral leads |
explain me with all basic and neccessary details as i am a new student



| Axis | What It Measures | Small Box (1 mm) | Large Box (5 mm) |
|---|---|---|---|
| Horizontal (X-axis) | Time | 0.04 seconds (40 ms) | 0.20 seconds (200 ms) |
| Vertical (Y-axis) | Voltage (amplitude) | 0.1 mV | 0.5 mV |


Why is QRS narrow even though ventricles are large? Because the His-Purkinje system conducts extremely fast — it distributes the impulse to all parts of the ventricles almost simultaneously. Costanzo Physiology

| Lead | Position |
|---|---|
| V1 | Right sternal border, 4th intercostal space |
| V2 | Left sternal border, 4th intercostal space |
| V3 | Midway between V2 and V4 |
| V4 | Left midclavicular line, 5th intercostal space |
| V5 | Left anterior axillary line, same horizontal level as V4 |
| V6 | Left midaxillary line, same horizontal level as V4 |
| Lead | View | Part of Heart Seen |
|---|---|---|
| I | Left (0°) | Lateral wall |
| II | Inferior-left (60°) | Inferior wall — best for P waves |
| III | Inferior-right (120°) | Inferior wall |
| aVR | Right arm (−150°) | Cavity/right side — normally negative |
| aVL | Left arm (−30°) | Lateral wall |
| aVF | Feet/inferior (90°) | Inferior wall |
| Lead | Part of Heart Seen |
|---|---|
| V1-V2 | Right ventricle + anterior septum |
| V3-V4 | Anterior wall (left ventricle, LAD territory) |
| V5-V6 | Lateral wall (left ventricle) |
| Large boxes between R waves | Heart Rate |
|---|---|
| 1 | 300 bpm |
| 2 | 150 bpm |
| 3 | 100 bpm |
| 4 | 75 bpm |
| 5 | 60 bpm |
| 6 | 50 bpm |
100 = Tachycardia (fast heart rate)
| Parameter | Normal Value | What Happens If Abnormal |
|---|---|---|
| Heart rate | 60-100 bpm | <60 = bradycardia; >100 = tachycardia |
| P wave duration | <120 ms | Wide = atrial enlargement |
| P wave axis | Upright in II, inverted in aVR | Otherwise = ectopic origin |
| PR interval | 120-200 ms | Long = AV block; Short = pre-excitation |
| QRS duration | <120 ms | Wide = BBB or ventricular beat |
| ST segment | Isoelectric (flat) | Elevated = injury; Depressed = ischaemia |
| T wave | Upright in most leads | Inverted = ischaemia; Peaked = hyperkalaemia |
| QTc | <440 ms (M), <460 ms (F) | Prolonged = risk of Torsades |
SA node fires → P wave (atria depolarize → atria contract)
↓
AV node delay → PR segment (flat line = waiting at the "gate")
↓
Ventricles fire → QRS complex (ventricles depolarize → ventricles contract)
↓
Ventricles rest → ST segment (flat — whole ventricle still depolarized)
↓
Ventricles recover → T wave (ventricles repolarize → ventricles relax)
give me also this in a pdf
ls /tmp/workspace/medical-notes/ && python3 -c "import requests; print('requests ok')" && python3 -c "from reportlab.platypus import Image; print('reportlab ok')"
~/medical-notes/generate_ecg_pdf.py
import os, io, requests
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, Image, KeepTogether
)
from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY
OUTPUT = "/tmp/workspace/medical-notes/ECG_Beginner_Guide.pdf"
IMG_DIR = "/tmp/workspace/medical-notes/ecg_imgs"
os.makedirs(IMG_DIR, exist_ok=True)
# ── Palette ──────────────────────────────────────────────────────────────────
C_NAVY = colors.HexColor("#1a3a5c")
C_BLUE = colors.HexColor("#2c6fad")
C_LIGHT_BLUE = colors.HexColor("#d6e8f8")
C_TEAL = colors.HexColor("#1a7a6e")
C_LIGHT_TEAL = colors.HexColor("#d0efec")
C_ORANGE = colors.HexColor("#c75b1e")
C_LIGHT_ORG = colors.HexColor("#fde8da")
C_TBL_HDR = colors.HexColor("#2c6fad")
C_TBL_ALT = colors.HexColor("#eef5fb")
C_BOX_BG = colors.HexColor("#fffbea")
C_BOX_BORDER = colors.HexColor("#e0a800")
C_TIP_BG = colors.HexColor("#e8f5e9")
C_TIP_BORDER = colors.HexColor("#388e3c")
W = A4[0] - 4*cm
# ── Styles ───────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
def ms(name, parent="Normal", **kw):
s = ParagraphStyle(name, parent=styles[parent])
for k, v in kw.items(): setattr(s, k, v)
return s
sTitle = ms("sTitle", fontName="Helvetica-Bold", fontSize=24, textColor=C_NAVY, alignment=TA_CENTER, spaceAfter=4)
sSub = ms("sSub", fontName="Helvetica", fontSize=12, textColor=C_BLUE, alignment=TA_CENTER, spaceAfter=10)
sBanner = ms("sBanner", fontName="Helvetica-Bold", fontSize=16, textColor=colors.white, leading=22, spaceBefore=2, spaceAfter=2)
sH1 = ms("sH1", fontName="Helvetica-Bold", fontSize=13, textColor=C_NAVY, spaceBefore=10, spaceAfter=3, leading=17)
sH2 = ms("sH2", fontName="Helvetica-Bold", fontSize=11, textColor=C_BLUE, spaceBefore=7, spaceAfter=3, leading=15)
sBody = ms("sBody", fontName="Helvetica", fontSize=9.5, leading=14, spaceAfter=4, alignment=TA_JUSTIFY)
sBullet = ms("sBullet", fontName="Helvetica", fontSize=9.5, leading=13, spaceAfter=2, leftIndent=14, firstLineIndent=-10)
sCaption = ms("sCap", fontName="Helvetica-Oblique",fontSize=8.5, textColor=colors.HexColor("#444"), leading=12, spaceAfter=6, alignment=TA_CENTER)
sNote = ms("sNote", fontName="Helvetica-Oblique",fontSize=9, textColor=colors.HexColor("#555"), leading=13, spaceAfter=4)
sCode = ms("sCode", fontName="Courier", fontSize=9, leading=13, spaceAfter=4, leftIndent=10)
sSource = ms("sSource", fontName="Helvetica-Oblique",fontSize=8, textColor=colors.HexColor("#555"), leading=12, spaceAfter=2)
sTblHdr = ms("sTblHdr", fontName="Helvetica-Bold", fontSize=9, textColor=colors.white, alignment=TA_CENTER, leading=12)
sTblCell = ms("sTblCell",fontName="Helvetica", fontSize=9, leading=12)
sTblCellC = ms("sTblCC", fontName="Helvetica", fontSize=9, leading=12, alignment=TA_CENTER)
# ── Helpers ───────────────────────────────────────────────────────────────────
def rule(c=C_BLUE, t=1.0): return HRFlowable(width="100%", thickness=t, color=c, spaceAfter=6, spaceBefore=2)
def sp(h=6): return Spacer(1, h)
def banner(text, color=C_NAVY):
t = Table([[Paragraph(text, sBanner)]], colWidths=[W])
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), color),
("LEFTPADDING", (0,0),(-1,-1), 12),
("RIGHTPADDING", (0,0),(-1,-1), 12),
("TOPPADDING", (0,0),(-1,-1), 8),
("BOTTOMPADDING", (0,0),(-1,-1), 8),
]))
return t
def h1(txt): return [sp(4), Paragraph(txt, sH1), rule(C_BLUE, 0.8)]
def h2(txt): return [Paragraph(txt, sH2)]
def body(txt): return Paragraph(txt, sBody)
def bullets(items):
return [Paragraph(f"• {it}", sBullet) for it in items]
def tip_box(label, text, bg=C_BOX_BG, border=C_BOX_BORDER):
inner = Table([[Paragraph(f"<b>{label}</b>", sTblCell)],
[Paragraph(text, sTblCell)]], colWidths=[W-20])
inner.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,-1), bg),
("BOX", (0,0),(-1,-1), 1.2, border),
("LEFTPADDING", (0,0),(-1,-1), 10),
("RIGHTPADDING", (0,0),(-1,-1), 10),
("TOPPADDING", (0,0),(-1,-1), 6),
("BOTTOMPADDING", (0,0),(-1,-1), 6),
]))
return inner
def make_table(headers, rows, col_widths=None, alt=True):
data = [[Paragraph(h, sTblHdr) for h in headers]]
for row in rows:
data.append([Paragraph(str(c), sTblCell) for c in row])
if not col_widths:
col_widths = [W/len(headers)]*len(headers)
t = Table(data, colWidths=col_widths, repeatRows=1)
t.setStyle(TableStyle([
("BACKGROUND", (0,0),(-1,0), C_TBL_HDR),
("ROWBACKGROUNDS",(0,1),(-1,-1), ([colors.white, C_TBL_ALT] if alt else [colors.white])),
("GRID", (0,0),(-1,-1), 0.4, colors.HexColor("#bbbbbb")),
("VALIGN", (0,0),(-1,-1), "TOP"),
("LEFTPADDING", (0,0),(-1,-1), 6),
("RIGHTPADDING", (0,0),(-1,-1), 6),
("TOPPADDING", (0,0),(-1,-1), 4),
("BOTTOMPADDING", (0,0),(-1,-1), 4),
]))
return t
# ── Image downloader ─────────────────────────────────────────────────────────
def dl_img(url, fname):
path = os.path.join(IMG_DIR, fname)
if not os.path.exists(path):
r = requests.get(url, timeout=20)
with open(path,"wb") as f: f.write(r.content)
return path
def rl_img(url, fname, width=None, caption=None):
"""Return [Image, caption_para] or [] on failure."""
try:
path = dl_img(url, fname)
w = width or (W - 1*cm)
img = Image(path, width=w, kind='proportional')
items = [img]
if caption:
items.append(Paragraph(caption, sCaption))
return items
except Exception as e:
print(f" img failed {fname}: {e}")
return [Paragraph(f"[Image: {caption or fname}]", sCaption)]
# ── Download all images ───────────────────────────────────────────────────────
IMGS = {
"conduction.png": "https://cdn.orris.care/cdss_images/030588aeee4867ca308e5f1a0fcc6940480805730b60fd0ddf1014b5c2254def.png",
"depol_repol.png": "https://cdn.orris.care/cdss_images/3ff4ade6e91f416f4524029de46348fb5a7f4028e8eeb50fbec86abab89ca495.png",
"normal_ecg.png": "https://cdn.orris.care/cdss_images/68a6c0397e45eefcc080b0f11d262dc072c272ed0d5af899d58637b80a828f29.png",
"ecg_grid.png": "https://cdn.orris.care/cdss_images/ef815eeb06c673b405e3c7f9112d1e9ff6fbea96a00a2723b233d384156386bd.png",
"waves_diagram.png":"https://cdn.orris.care/cdss_images/042124084c74430362fb12de1758340e2dd3b129abd476a16698a7fda88e0376.png",
"leads_placement.png":"https://cdn.orris.care/cdss_images/34b5cf1bc5d662ff1987fcfcf9b03809ef79a5df151f04aef5c6801e52912fa4.png",
"af.png": "https://cdn.orris.care/cdss_images/a689a14cc2a98bf071158972c8f1347d8ea29e130b73423f1c5e4b08b55013e3.png",
"aflutter.png": "https://cdn.orris.care/cdss_images/f88a5b8505299d45830bb8150a7f9b0883e3b58f7dc8f674472c6cbf759d1bb7.png",
"vt.png": "https://cdn.orris.care/cdss_images/c0d1a542a338c6ef89618bdb32851c880180b009a3e5a183ba0a4c6d0ee4a4ae.png",
"torsades.png": "https://cdn.orris.care/cdss_images/30c4930e3e293fec6b7deb34cb1b92926ade8033cb34b8e94bda927c4149eb9a.png",
"av_blocks.png": "https://cdn.orris.care/cdss_images/a77f39e9879dd484b3b4417e45932db78c25cf99328cac41ff8f2f9c3937b4b3.png",
}
print("Downloading images...")
for fname, url in IMGS.items():
try:
dl_img(url, fname)
print(f" OK: {fname}")
except Exception as e:
print(f" FAIL: {fname} — {e}")
# ════════════════════════════════════════════════════════════════════════════
# BUILD STORY
# ════════════════════════════════════════════════════════════════════════════
doc = SimpleDocTemplate(OUTPUT, pagesize=A4,
leftMargin=2*cm, rightMargin=2*cm, topMargin=2.2*cm, bottomMargin=2.2*cm,
title="ECG for Beginners — Complete Foundation Guide",
author="Orris Medical Assistant")
story = []
# ── Cover ─────────────────────────────────────────────────────────────────────
story.append(sp(3*cm))
story.append(Paragraph("ECG for Beginners", sTitle))
story.append(Paragraph("Complete Foundation Guide — Waves, Intervals, Leads & Patterns", sSub))
story.append(sp(0.4*cm))
story.append(rule(C_NAVY, 2))
story.append(sp(0.3*cm))
story.append(Paragraph(
"Sources: Guyton & Hall Medical Physiology · Costanzo Physiology 7th Ed. · "
"Goldman-Cecil Medicine · Roberts & Hedges' Clinical Procedures · "
"Katzung's Pharmacology 16th Ed. · Tintinalli's Emergency Medicine · "
"Miller's Anesthesia 10th Ed. · Washington Manual of Medical Therapeutics",
sSource))
story.append(PageBreak())
# ════════════════════════════════════════════════════════════════════════════
# PART 1: WHAT IS AN ECG
# ════════════════════════════════════════════════════════════════════════════
story.append(banner("PART 1 — What Is an ECG and Why Do We Do It?", C_NAVY))
story.append(sp(8))
story.append(body(
"An <b>Electrocardiogram (ECG or EKG)</b> is a completely painless test that records the "
"<b>electrical activity of the heart</b> over time. Every heartbeat is triggered by an "
"electrical signal. By placing small sticky electrodes on the skin, we can 'listen' to "
"those signals and display them as waves on paper or a screen. "
"No electricity enters the body — the machine only reads signals coming out."
))
story.append(sp(4))
story.append(body("<b>The ECG can detect:</b>"))
story += bullets([
"Heart attacks (myocardial infarction — ST elevation, Q waves)",
"Abnormal heart rhythms (arrhythmias — atrial fibrillation, VT, heart block)",
"Blocked electrical pathways (bundle branch blocks, AV blocks)",
"Enlarged heart chambers (ventricular hypertrophy)",
"Electrolyte imbalances (potassium, calcium, magnesium problems)",
"Drug toxicity effects (digoxin, prolonged QT from medications)",
])
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 2: CONDUCTION SYSTEM
# ════════════════════════════════════════════════════════════════════════════
story.append(banner("PART 2 — The Heart's Electrical System", C_TEAL))
story.append(sp(8))
story.append(body(
"Before reading an ECG, you must understand <b>why</b> the waves appear. "
"Every wave corresponds to a specific electrical event in the heart. "
"The heart has its own built-in electrical wiring — the <b>cardiac conduction system</b>."
))
story.append(sp(6))
story += rl_img("conduction.png", "conduction.png", width=W-2*cm,
caption="The cardiac conduction system — SA node (pacemaker) → AV node → Bundle of His → Bundle branches → Purkinje fibers.\n(Costanzo Physiology, 7th Ed.)")
story.append(sp(8))
story += h1("The Electrical Pathway — Step by Step")
pathway = [
["Step", "Structure", "Function", "Speed"],
["1", "SA Node\n(Sinoatrial)", "Natural pacemaker. Fires spontaneously 60–100x/min.\nSpreads impulse through both atria.", "Slowest to start\n~0.05 m/s"],
["2", "AV Node\n(Atrioventricular)", "Deliberate DELAY of 0.1 second.\nGives ventricles time to fill with blood.\nThink of it as a traffic light.", "Slowest node\n~0.05 m/s"],
["3", "Bundle of His", "Conducts impulse from AV node down\nthe interventricular septum.", "~1 m/s"],
["4", "Bundle Branches\n(Left + Right)", "Left branch → left ventricle\nRight branch → right ventricle", "~2 m/s"],
["5", "Purkinje Fibers", "Fine network spreading impulse\nthroughout all of both ventricles.\nFastest conduction — ensures simultaneous contraction.", "~4 m/s\n(fastest)"],
]
story.append(make_table(pathway[0], pathway[1:], col_widths=[1.2*cm, 3.5*cm, 8.3*cm, 3.5*cm]))
story.append(sp(6))
story.append(tip_box("Key Concept",
"The AV node delay is protective — it ensures atria finish contracting and filling the ventricles "
"BEFORE the ventricles contract. If the AV node conducts too fast, cardiac output drops.",
C_TIP_BG, C_TIP_BORDER))
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 3: DEPOLARIZATION & REPOLARIZATION
# ════════════════════════════════════════════════════════════════════════════
story.append(banner("PART 3 — Depolarization and Repolarization", C_ORANGE))
story.append(sp(8))
story.append(body(
"These are the two fundamental processes that produce ALL ECG waves. "
"Understanding them makes every wave logical, not memorised."
))
story.append(sp(6))
story += rl_img("depol_repol.png", "depol_repol.png", width=W-1*cm,
caption="Top: ventricular action potential — depolarization (upstroke) triggers contraction; repolarization (return) = relaxation.\n"
"Bottom: corresponding ECG waves P, QRS (depolarization), T (repolarization). (Guyton & Hall Medical Physiology)")
story.append(sp(6))
story.append(make_table(
["Process", "What Happens at Cell Level", "What Triggers", "ECG Appearance"],
[
["Depolarization", "Na⁺ rushes INTO the cell → membrane charge reverses from negative to positive", "Muscle CONTRACTION", "P wave (atria), QRS complex (ventricles)"],
["Repolarization", "K⁺ flows OUT → membrane returns to negative resting state", "Muscle RELAXATION", "T wave (ventricles)\n(Atrial repolarization buried inside QRS)"],
],
col_widths=[3.5*cm, 5.5*cm, 4*cm, 4.5*cm]
))
story.append(sp(6))
story.append(tip_box("Simple Rule",
"Depolarization = contraction → upward deflection on ECG (if wave moves toward the electrode).\n"
"Repolarization = relaxation → T wave. No current when heart is fully depolarized or fully repolarized → flat line (ST segment).",
C_BOX_BG, C_BOX_BORDER))
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 4: THE ECG PAPER & GRID
# ════════════════════════════════════════════════════════════════════════════
story.append(banner("PART 4 — The ECG Paper and Grid", C_NAVY))
story.append(sp(8))
story.append(body(
"Before reading any wave, you must understand the paper. "
"The ECG grid has two axes: the <b>horizontal axis measures TIME</b> and "
"the <b>vertical axis measures VOLTAGE (amplitude)</b>."
))
story.append(sp(6))
story += rl_img("ecg_grid.png", "ecg_grid.png", width=W-1*cm,
caption="Standard ECG paper. Horizontal: 1 small box = 0.04 sec; 1 large box = 0.2 sec.\n"
"Vertical: 1 small box = 0.1 mV; 1 large box = 0.5 mV. (Goldman-Cecil Medicine)")
story.append(sp(6))
story.append(make_table(
["Axis", "What It Measures", "Small Box (1 mm)", "Large Box (5 mm)"],
[
["Horizontal (X-axis)", "TIME", "0.04 seconds (40 ms)", "0.20 seconds (200 ms)"],
["Vertical (Y-axis)", "VOLTAGE (Amplitude)", "0.1 mV", "0.5 mV"],
],
col_widths=[4*cm, 4*cm, 5*cm, 4.5*cm]
))
story.append(sp(4))
story.append(Paragraph(
"<b>Standard recording speed = 25 mm/second</b> — this is the same worldwide so measurements are universal.",
sNote))
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 5: WAVES AND INTERVALS
# ════════════════════════════════════════════════════════════════════════════
story.append(banner("PART 5 — The ECG Waves and Intervals Explained", C_TEAL))
story.append(sp(8))
story += rl_img("normal_ecg.png", "normal_ecg.png", width=W,
caption="Normal ECG showing two complete cardiac cycles — P wave, Q, R, S, T, and all intervals labeled.\n"
"PR interval = 0.16 sec; QRS < 0.12 sec; RR interval shows heart rate. (Guyton & Hall Medical Physiology)")
story.append(sp(6))
story += rl_img("waves_diagram.png", "waves_diagram.png", width=W-2*cm,
caption="ECG components from lead II: P wave, PR interval, QRS complex, ST segment, T wave, QT interval, J point. (Costanzo Physiology, 7th Ed.)")
story.append(sp(8))
story += h1("Each Wave — What It Is and What It Means")
story += h2("P Wave")
story += bullets([
"<b>Electrical event:</b> Atrial depolarization (SA node fires → impulse spreads through both atria)",
"<b>Physical event:</b> Atria contract, pushing blood into ventricles (atrial kick)",
"<b>Normal appearance:</b> Small, rounded, upright in lead II; duration <120 ms (3 small boxes)",
"<b>Abnormal:</b> Wide P = atrial enlargement; Absent P = atrial fibrillation or junctional rhythm; Inverted P in II = ectopic atrial origin",
])
story.append(sp(6))
story += h2("PR Interval")
story += bullets([
"<b>Measured from:</b> Start of P wave → Start of QRS complex",
"<b>Represents:</b> Total conduction time through atria + AV node + His bundle (the 'AV node delay' is the flat PR segment)",
"<b>Normal: 120–200 ms (3–5 small boxes)</b>",
"<b>Short PR (<120 ms):</b> Pre-excitation syndrome (e.g., Wolff-Parkinson-White — a bypass tract bypasses the AV node)",
"<b>Long PR (>200 ms):</b> First-degree heart block (AV node conducts slowly)",
"Sympathetic stimulation shortens PR; parasympathetic (vagal) stimulation lengthens it",
])
story.append(sp(6))
story += h2("QRS Complex")
story += bullets([
"<b>Electrical event:</b> Ventricular depolarization — both ventricles activate via the His-Purkinje system",
"<b>Physical event:</b> Ventricles contract powerfully, pumping blood to lungs (right) and body (left)",
"<b>Q wave:</b> First negative (downward) deflection — small initial septal activation",
"<b>R wave:</b> Tall upward spike — main ventricular activation wave",
"<b>S wave:</b> Downward deflection after R — terminal forces moving away from the electrode",
"<b>Normal QRS duration: <120 ms (3 small boxes)</b>",
"<b>Wide QRS (≥120 ms):</b> Bundle branch block (signal not using the fast Purkinje system) OR ventricular ectopic beat",
"<b>Pathological Q waves (wide >40 ms AND deep >25% QRS height):</b> Completed heart attack (scar tissue cannot depolarize)",
])
story.append(sp(4))
story.append(tip_box("Why is QRS narrow despite large ventricular mass?",
"The His-Purkinje system conducts at ~4 m/s — it distributes the impulse to ALL parts of both ventricles "
"almost simultaneously. Compare this to the AV node (0.05 m/s) — 80x slower! "
"Fast, simultaneous activation = narrow, efficient QRS. (Costanzo Physiology)"))
story.append(sp(6))
story += h2("ST Segment")
story += bullets([
"<b>What it is:</b> Flat line between end of QRS (J point) and start of T wave",
"<b>Why it's flat:</b> The entire ventricle is uniformly depolarized — no current flows between regions → no deflection",
"<b>Normal: Isoelectric (flat — at the baseline)</b>",
"<b>ST Elevation:</b> Acute transmural (full-thickness) injury → STEMI (heart attack), pericarditis, Brugada syndrome",
"<b>ST Depression:</b> Subendocardial ischaemia (inner layer starved of oxygen) → NSTEMI, digoxin effect",
"The J point is the exact junction of QRS end and ST start",
])
story.append(sp(6))
story += h2("T Wave")
story += bullets([
"<b>Electrical event:</b> Ventricular repolarization — ventricles reset electrically",
"<b>Physical event:</b> Ventricles relax (diastole begins)",
"<b>Normal:</b> Rounded, upright in most leads; inverted in aVR normally",
"<b>Peaked (tall, narrow) T waves:</b> Hyperkalaemia (high potassium) — first sign",
"<b>Inverted T waves:</b> Ischaemia, ventricular hypertrophy, bundle branch block, pulmonary embolism",
"<b>Atrial repolarization:</b> Occurs during this time too but is hidden inside the QRS complex (too small to see)",
])
story.append(sp(6))
story += h2("QT Interval")
story += bullets([
"<b>Measured from:</b> Start of QRS → End of T wave",
"<b>Represents:</b> Total ventricular electrical activity (depolarization + repolarization)",
"<b>Must be corrected for heart rate</b> (faster heart rate = shorter QT naturally) → QTc",
"<b>Normal QTc: <440 ms (men), <460 ms (women)</b>",
"<b>Prolonged QTc:</b> Risk of fatal arrhythmia (Torsades de Pointes). Causes: drugs (antiarrhythmics, antipsychotics, some antibiotics), low K⁺/Mg²⁺/Ca²⁺, congenital long QT syndrome",
"<b>Short QTc:</b> Hypercalcaemia",
])
story.append(sp(6))
story += h2("U Wave")
story += bullets([
"Small, positive wave after the T wave (not always visible)",
"Thought to represent repolarization of Purkinje fibers or papillary muscles",
"<b>Prominent U waves = hypokalaemia</b> (low potassium) or bradycardia",
])
story.append(sp(6))
story += h2("RR Interval")
story += bullets([
"Distance between two consecutive R wave peaks",
"Used to calculate heart rate: <b>Heart Rate = 1 / (RR interval in seconds)</b>",
"Or use the quick 300 Rule (explained in Part 7)",
])
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 6: THE 12 LEADS
# ════════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(banner("PART 6 — The 12 Leads: Different Camera Angles of the Heart", C_ORANGE))
story.append(sp(8))
story.append(body(
"A standard ECG uses <b>12 leads</b>, but only <b>10 physical electrodes</b> are placed. "
"Each lead looks at the heart from a different angle — like cameras surrounding the heart. "
"The 12 views are mathematically derived from the 10 electrode signals."
))
story.append(sp(6))
story += h1("Chest (Precordial) Electrode Placement — V1 to V6")
story += rl_img("leads_placement.png", "leads_placement.png", width=W-1*cm,
caption="Precordial electrode placement V1–V6. Start at the sternal angle to find the 4th intercostal space.\n"
"(Roberts & Hedges' Clinical Procedures in Emergency Medicine)")
story.append(sp(6))
story.append(make_table(
["Lead", "Exact Position"],
[
["V1", "Right sternal border, 4th intercostal space"],
["V2", "Left sternal border, 4th intercostal space"],
["V3", "Midway between V2 and V4"],
["V4", "Left midclavicular line, 5th intercostal space"],
["V5", "Left anterior axillary line, same horizontal level as V4"],
["V6", "Left midaxillary line, same horizontal level as V4 and V5"],
],
col_widths=[2.5*cm, 15*cm]
))
story.append(sp(4))
story.append(tip_box("How to find the 4th intercostal space",
"Feel the sternal angle (the bony ridge on the breastbone, about 5 cm below the top notch). "
"Just lateral to it is the 2nd rib. Count downward: 2nd rib → 2nd space → 3rd rib → 3rd space → 4th rib → 4th space. "
"V1 and V2 go here. V4–V6 are at the same horizontal level (NOT all in the 5th intercostal space)."))
story.append(sp(8))
story += h1("What Each Lead Group 'Sees'")
story.append(make_table(
["Lead Group", "Leads Included", "Wall of Heart Viewed", "Artery at Risk"],
[
["Inferior", "II, III, aVF", "Inferior (bottom) wall of LV", "Right coronary artery (RCA) or LCx"],
["Anterior", "V1, V2, V3, V4", "Anterior (front) wall of LV + septum", "Left anterior descending (LAD)"],
["Lateral", "I, aVL, V5, V6", "Lateral (side) wall of LV", "Left circumflex (LCx) or diagonal branch of LAD"],
["Septal", "V1, V2", "Interventricular septum", "LAD (septal branches)"],
["Right Ventricle", "V1, V3R, V4R (special right-sided leads)", "Right ventricular wall", "Proximal RCA"],
],
col_widths=[2.8*cm, 4*cm, 5.2*cm, 5.5*cm]
))
story.append(sp(6))
story += h1("The Limb Leads")
story.append(make_table(
["Lead", "View (Angle)", "Normal Appearance"],
[
["I", "Lateral (0°)", "Upright P, R, T"],
["II", "Inferior-left (60°)", "Best lead for P waves; upright P, R, T — most used for rhythm monitoring"],
["III", "Inferior-right (120°)","Upright P, R, T"],
["aVR", "Right arm (–150°)", "Normally NEGATIVE (looking into the heart from upper right — opposite direction of main current)"],
["aVL", "Left arm (–30°)", "Lateral view; may have small Q wave"],
["aVF", "Inferior (90°)", "Looking from below; inferior wall"],
],
col_widths=[1.8*cm, 4.5*cm, 11.2*cm]
))
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 7: HEART RATE
# ════════════════════════════════════════════════════════════════════════════
story.append(banner("PART 7 — How to Calculate Heart Rate", C_TEAL))
story.append(sp(8))
story += h2("Method 1 — The 300 Rule (for regular rhythms)")
story.append(body(
"Count the number of <b>large boxes</b> between two consecutive R waves, "
"then divide 300 by that number."
))
story.append(sp(4))
story.append(make_table(
["Large Boxes between R–R", "Heart Rate", "Interpretation"],
[
["1", "300 bpm", "Very fast (rare)"],
["2", "150 bpm", "Tachycardia (e.g., atrial flutter 2:1)"],
["3", "100 bpm", "Upper limit of normal / mild tachycardia"],
["4", "75 bpm", "Normal"],
["5", "60 bpm", "Normal (lower limit)"],
["6", "50 bpm", "Bradycardia"],
["7+", "<43 bpm","Significant bradycardia"],
],
col_widths=[5.5*cm, 4*cm, 8*cm]
))
story.append(sp(4))
story.append(tip_box("Memory mnemonic",
"300 → 150 → 100 → 75 → 60 → 50 (one R-R large box at a time)"))
story.append(sp(6))
story += h2("Method 2 — Count QRS Complexes")
story += bullets([
"Find a 10-second strip (usually the bottom rhythm strip on a standard ECG)",
"Count all QRS complexes in that 10 seconds",
"Multiply by 6 → heart rate per minute",
"Use this for irregular rhythms (e.g., atrial fibrillation)",
])
story.append(sp(6))
story.append(make_table(
["Heart Rate Category", "Rate (bpm)", "Common Causes"],
[
["Normal Sinus Rhythm", "60–100", "Healthy baseline"],
["Sinus Bradycardia", "<60", "Athletes, vagal tone, beta-blockers, hypothyroidism, inferior MI, hypothermia"],
["Sinus Tachycardia", ">100", "Pain, fever, anaemia, anxiety, PE, thyrotoxicosis, heart failure"],
],
col_widths=[4.5*cm, 3.5*cm, 9.5*cm]
))
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 8: SYSTEMATIC APPROACH
# ════════════════════════════════════════════════════════════════════════════
story.append(banner("PART 8 — Systematic Approach to Reading Any ECG", C_NAVY))
story.append(sp(8))
story.append(body(
"Always read an ECG in the <b>same order every single time</b>. This prevents you from "
"being distracted by one obvious finding and missing something equally important."
))
story.append(sp(6))
story.append(make_table(
["Step", "What to Assess", "Normal", "Abnormal = Think..."],
[
["1", "RATE\nCount large boxes between R-R", "60–100 bpm", "<60 = bradycardia\n>100 = tachycardia"],
["2", "RHYTHM\nAre all R-R intervals equal?\nIs it regular or irregular?", "Regular; all R-R equal", "Irregular = AF, heart block, ectopics\nRegularly irregular = 2nd degree block"],
["3", "P WAVES\nPresent? One before every QRS?\nUpright in lead II?", "Yes, Yes, Yes", "Absent = AF, junctional rhythm\nNo P before QRS = AV block, ectopic ventricular beat"],
["4", "PR INTERVAL\nMeasure onset P to onset QRS", "120–200 ms\n(3–5 small boxes)", "Short = WPW; Long = 1st degree block\nMissing/variable = 2nd/3rd degree block"],
["5", "QRS COMPLEX\nDuration? Morphology?", "<120 ms (narrow)", "Wide ≥120 ms = BBB or ventricular beat\nDeep Q waves = old MI"],
["6", "ST SEGMENT & T WAVES\nFlat baseline? T wave direction?", "ST isoelectric\nT upright in most leads", "ST elevation = STEMI, pericarditis\nST depression = ischaemia, NSTEMI, digoxin\nT inversion = ischaemia, PE, hypertrophy"],
["7", "QT INTERVAL\nMeasure QRS start to T wave end", "QTc <440 ms (M)\nQTc <460 ms (F)", "Prolonged = Torsades risk\nShort = hypercalcaemia"],
],
col_widths=[0.8*cm, 4.5*cm, 3.8*cm, 8.4*cm]
))
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 9: COMMON PATTERNS (with images)
# ════════════════════════════════════════════════════════════════════════════
story.append(PageBreak())
story.append(banner("PART 9 — Common ECG Patterns (With Real Tracings)", C_ORANGE))
story.append(sp(8))
story += h1("9.1 Atrial Flutter")
story += bullets([
"Sawtooth (saw-blade) flutter waves — most visible in leads II, III, aVF and V1",
"Atrial rate: ~300 bpm (range 250–350 bpm)",
"Ventricular rate depends on AV block ratio: 2:1 = 150 bpm, 3:1 = 100 bpm",
"<b>Key clue: A regular narrow tachycardia at ~150 bpm almost always = atrial flutter with 2:1 block</b>",
])
story.append(sp(4))
story += rl_img("aflutter.png", "aflutter.png", width=W,
caption="Atrial flutter — sawtooth flutter waves clearly visible in lead V1 (black arrows). (Miller's Anesthesia, 10th Ed.)")
story.append(sp(8))
story += h1("9.2 Atrial Fibrillation (AF)")
story += bullets([
"<b>Absent P waves</b> — no organised atrial activity; chaotic or flat baseline",
"<b>Irregularly irregular</b> ventricular rhythm — no two R-R intervals are equal",
"Narrow QRS (unless bundle branch block or pre-excitation)",
"Causes: ischaemia, valvular disease, alcohol, thyrotoxicosis, heart failure",
"Risk: thromboembolism — blood pools in atrium → clot → stroke",
])
story.append(sp(4))
story += rl_img("af.png", "af.png", width=W,
caption="Atrial fibrillation — irregularly irregular rhythm with no identifiable P waves. (Miller's Anesthesia, 10th Ed.)")
story.append(sp(8))
story += h1("9.3 AV Heart Blocks")
story += rl_img("av_blocks.png", "av_blocks.png", width=W,
caption="AV block examples: A = 1st degree (long PR), B = Mobitz I/Wenckebach (progressive PR then dropped beat),\n"
"C = Mobitz II (sudden dropped beat, constant PR), D = 2:1 block, E = 3rd degree/complete heart block.\n(Washington Manual of Medical Therapeutics)")
story.append(sp(6))
story.append(make_table(
["Type", "ECG Finding", "Site of Block", "Risk / Action"],
[
["1st Degree", "PR interval >200 ms every beat. No dropped beats.", "AV node (slow conduction)", "Benign. Monitor. No treatment usually needed."],
["2nd Degree\nMobitz I\n(Wenckebach)", "Progressive PR prolongation → one dropped QRS → cycle repeats. Group beating pattern.", "Within AV node (proximal)", "Usually benign. Rarely needs pacemaker."],
["2nd Degree\nMobitz II", "Constant PR interval → sudden dropped QRS without warning. Often wide QRS.", "Below AV node (distal His-Purkinje)", "Serious. High risk of progressing to complete block. Pacemaker often required."],
["3rd Degree\n(Complete)", "Complete AV dissociation. Atria and ventricles beat independently. More P waves than QRS complexes.", "Complete block at any level", "Emergency. Haemodynamic compromise. Temporary then permanent pacemaker needed."],
],
col_widths=[2.8*cm, 5.5*cm, 4*cm, 5.2*cm]
))
story.append(sp(8))
story += h1("9.4 Ventricular Tachycardia (VT)")
story += bullets([
"≥3 consecutive beats originating from the ventricles at rate >100 bpm",
"<b>Wide QRS (≥120 ms)</b> — signal not using the fast Purkinje system",
"AV dissociation (P waves unrelated to QRS) — pathognomonic when visible",
"Fusion beats and capture beats confirm VT",
"Can cause haemodynamic compromise; may degenerate to VF (cardiac arrest)",
"Sustained VT: lasts >30 seconds; Non-sustained: stops within 30 seconds spontaneously",
])
story.append(sp(4))
story += rl_img("vt.png", "vt.png", width=W,
caption="Ventricular tachycardia — broad, regular complexes at >100 bpm. (Miller's Anesthesia, 10th Ed.)")
story.append(sp(8))
story += h1("9.5 Torsades de Pointes")
story += bullets([
"A form of polymorphic VT (QRS morphology changes from beat to beat)",
"QRS complexes appear to 'twist' around the isoelectric baseline",
"Rate: 200–250 bpm",
"Preceded by prolonged QT interval — always check QT before initiating certain drugs",
"Causes: drugs (class Ia/III antiarrhythmics, antipsychotics, some antibiotics), low K⁺/Mg²⁺, congenital long QT",
"Treatment: IV magnesium sulphate; correct electrolytes; temporary pacing if recurrent",
])
story.append(sp(4))
story += rl_img("torsades.png", "torsades.png", width=W,
caption="Torsades de Pointes — initiating beats with prolonged QT then twisting polymorphic VT. (Miller's Anesthesia, 10th Ed.)")
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 10: NORMAL VALUES REFERENCE TABLE
# ════════════════════════════════════════════════════════════════════════════
story.append(banner("PART 10 — Normal Values Quick Reference", C_TEAL))
story.append(sp(8))
story.append(make_table(
["Parameter", "Normal Value", "What Abnormal Means"],
[
["Heart Rate", "60–100 bpm", "<60 = bradycardia; >100 = tachycardia"],
["P wave duration", "<120 ms", "Wide = atrial enlargement or inter-atrial block"],
["P wave axis", "Upright in II, inverted in aVR", "Inverted in II = ectopic/junctional origin"],
["PR interval", "120–200 ms", "<120 = pre-excitation (WPW); >200 = 1st degree AV block"],
["QRS duration", "<120 ms", "≥120 ms = bundle branch block or ventricular beat"],
["QRS axis", "–30° to +90°", "Left axis = LBBB, LVH, inferior MI; Right axis = RVH, PE, RBBB"],
["ST segment", "Isoelectric (flat)", "Elevation = STEMI, pericarditis; Depression = ischaemia, digoxin"],
["T wave", "Upright in I, II, V3–V6", "Peaked = hyperkalaemia; Inverted = ischaemia, PE, hypertrophy"],
["QTc (men)", "<440 ms", "Prolonged = risk of Torsades de Pointes"],
["QTc (women)", "<460 ms", "Women have slightly longer QT physiologically"],
["U wave", "Small, upright (may be absent)", "Prominent = hypokalaemia; inverted = ischaemia"],
],
col_widths=[4*cm, 4.5*cm, 9*cm]
))
story.append(sp(10))
# ════════════════════════════════════════════════════════════════════════════
# PART 11: THE STORY OF ONE HEARTBEAT
# ════════════════════════════════════════════════════════════════════════════
story.append(banner("PART 11 — The Story of One Heartbeat on ECG", C_NAVY))
story.append(sp(8))
story.append(body(
"Think of the ECG as telling a sequential story from top to bottom of the heart each time the heart beats:"
))
story.append(sp(6))
story.append(make_table(
["Event", "ECG Feature", "What the Heart Is Doing"],
[
["SA node fires", "No visible deflection", "Pacemaker triggers a signal — starts the beat"],
["Atria depolarize", "P wave", "Atria contract, pushing blood into the ventricles"],
["AV node delays signal", "PR segment (flat line)", "Traffic light pause — ventricles fill with blood"],
["Ventricles depolarize", "QRS complex", "Both ventricles contract powerfully, ejecting blood"],
["Ventricles fully excited","ST segment (flat)", "All ventricular cells depolarized — brief plateau"],
["Ventricles repolarize", "T wave", "Ventricles relax, refill — ready for next beat"],
["Heart at rest", "TP segment (flat baseline)","Electrical silence between beats (diastole)"],
],
col_widths=[4.5*cm, 4*cm, 9*cm]
))
story.append(sp(8))
story.append(tip_box("The Golden Memory Trick",
"P = Pump atria • QRS = Squeeze ventricles • T = Time to recover\n"
"PR flat = Gate closed at AV node • ST flat = Whole ventricle lit up • TP flat = Heart resting"))
story.append(sp(16))
story.append(rule(C_NAVY, 1.5))
story.append(Paragraph(
"Sources: Guyton & Hall Textbook of Medical Physiology · Costanzo Physiology 7th Ed. · "
"Goldman-Cecil Medicine · Roberts & Hedges' Clinical Procedures · "
"Katzung's Basic & Clinical Pharmacology 16th Ed. · Tintinalli's Emergency Medicine · "
"Miller's Anesthesia 10th Ed. · Washington Manual of Medical Therapeutics",
sSource))
# ── Build ─────────────────────────────────────────────────────────────────────
doc.build(story)
print(f"\nPDF saved: {OUTPUT}")
cd /tmp/workspace/medical-notes && python generate_ecg_pdf.py 2>&1
Image helper — kind='proportional' needs both width and height. Let me fix the image helper to set explicit height.~/medical-notes/generate_ecg_pdf.py
Successfully replaced 1 instance(s) of the string in '/tmp/workspace/medical-notes/generate_ecg_pdf.py'
cd /tmp/workspace/medical-notes && python generate_ecg_pdf.py 2>&1
ls -lh /tmp/workspace/medical-notes/ECG_Beginner_Guide.pdf
ECG Beginner Guide
PDF Document · PDF