Why Ife facial muscles are more affected lmn than u m n legions
| Part of Facial Nucleus | UMN Input | Controls |
|---|---|---|
| Rostral (upper) | Bilateral - from both contralateral AND ipsilateral motor cortex | Forehead, orbicularis oculi (upper face) |
| Caudal (lower) | Unilateral - only from contralateral motor cortex | Lower face (mouth, cheek, platysma) |

| Feature | UMN Lesion | LMN Lesion |
|---|---|---|
| Forehead | Spared | Involved |
| Eye closure | Mild weakness or normal | Weak, lagophthalmos |
| Distribution | Contralateral lower face only | Entire ipsilateral face |
| Associated signs | Arm/leg weakness, aphasia, sensory loss | Hyperacusis, dry eye, taste loss |
| Common causes | Stroke, brain tumour | Bell's palsy, Ramsay Hunt, trauma |
Make a study guide comparing all cranial nerve palsy patterns (CN V, IX, X, XII)
mkdir -p /home/daytona/workspace/cn-study-guide
~/cn-study-guide/generate_cn_guide.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame
# ── Color palette ──────────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1A2E4A")
TEAL = colors.HexColor("#0D7377")
LIGHT_TEAL = colors.HexColor("#14A7AC")
GOLD = colors.HexColor("#E8A020")
LIGHT_GREY = colors.HexColor("#F4F6F9")
MID_GREY = colors.HexColor("#D0D6DF")
WHITE = colors.white
RED_LIGHT = colors.HexColor("#FFF0F0")
RED_DARK = colors.HexColor("#C0392B")
GREEN_LIGHT= colors.HexColor("#F0FFF4")
GREEN_DARK = colors.HexColor("#1E8449")
AMBER_LIGHT= colors.HexColor("#FFFBF0")
AMBER_DARK = colors.HexColor("#D68910")
OUTPUT = "/home/daytona/workspace/cn-study-guide/CN_Palsy_Study_Guide.pdf"
# ── Styles ─────────────────────────────────────────────────────────────────────
styles = getSampleStyleSheet()
TITLE_STYLE = ParagraphStyle("Title", fontSize=26, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_CENTER, leading=32)
SUBTITLE_STYLE = ParagraphStyle("Subtitle", fontSize=13, textColor=LIGHT_TEAL,
fontName="Helvetica", alignment=TA_CENTER, leading=18)
H1 = ParagraphStyle("H1", fontSize=16, textColor=WHITE,
fontName="Helvetica-Bold", alignment=TA_LEFT, leading=22,
spaceAfter=4, spaceBefore=4)
H2 = ParagraphStyle("H2", fontSize=12, textColor=NAVY,
fontName="Helvetica-Bold", alignment=TA_LEFT, leading=16,
spaceAfter=3, spaceBefore=6)
BODY = ParagraphStyle("Body", fontSize=9, textColor=colors.HexColor("#222222"),
fontName="Helvetica", leading=13, alignment=TA_JUSTIFY)
BODY_BOLD = ParagraphStyle("BodyBold", fontSize=9, textColor=NAVY,
fontName="Helvetica-Bold", leading=13)
SMALL = ParagraphStyle("Small", fontSize=7.5, textColor=colors.HexColor("#555555"),
fontName="Helvetica", leading=11, alignment=TA_LEFT)
TABLE_HEAD = ParagraphStyle("TH", fontSize=8.5, textColor=WHITE,
fontName="Helvetica-Bold", leading=11, alignment=TA_CENTER)
TABLE_CELL = ParagraphStyle("TC", fontSize=8, textColor=colors.HexColor("#1A1A1A"),
fontName="Helvetica", leading=11, alignment=TA_LEFT)
TABLE_CELL_BOLD = ParagraphStyle("TCB", fontSize=8, textColor=NAVY,
fontName="Helvetica-Bold", leading=11, alignment=TA_LEFT)
MNEMONIC = ParagraphStyle("Mnem", fontSize=10, textColor=GOLD,
fontName="Helvetica-Bold", leading=14, alignment=TA_CENTER)
PEARL = ParagraphStyle("Pearl", fontSize=8.5, textColor=colors.HexColor("#333333"),
fontName="Helvetica-Oblique", leading=12, alignment=TA_LEFT)
# ── CN Data ────────────────────────────────────────────────────────────────────
# Each entry: (number, name, type, nucleus/exit, function, palsy_features, causes, uMN_vs_LMN, clinical_pearl)
CN_DATA = [
{
"num": "CN I",
"name": "Olfactory",
"type": "Sensory only",
"exit": "Cribriform plate of ethmoid",
"nucleus": "Olfactory bulb (no brainstem nucleus)",
"function": "Smell (special somatic sensory)",
"palsy": [
("Anosmia", "Complete loss of smell"),
("Hyposmia", "Reduced sense of smell"),
("Parosmia", "Distorted smell perception"),
],
"causes": [
"Skull base fracture (cribriform plate shear)",
"Frontal lobe tumour (meningioma)",
"Viral / post-COVID anosmia",
"Foster Kennedy syndrome (unilateral anosmia + ipsilateral optic atrophy + contralateral papilloedema - subfrontal meningioma)",
],
"umn_lmn": "Pure sensory nerve - UMN/LMN distinction does not apply",
"pearl": "Foster Kennedy syndrome: unilateral anosmia + ipsilateral optic atrophy + contralateral papilloedema = subfrontal meningioma until proven otherwise.",
"color": colors.HexColor("#EEF2FF"),
},
{
"num": "CN II",
"name": "Optic",
"type": "Sensory only",
"exit": "Optic canal (sphenoid bone)",
"nucleus": "Lateral geniculate nucleus (thalamus)",
"function": "Vision (special somatic sensory)",
"palsy": [
("Ipsilateral blindness", "Complete CN II lesion anterior to chiasm"),
("Bitemporal hemianopia", "Chiasmal lesion (pituitary adenoma)"),
("Homonymous hemianopia", "Post-chiasmal lesion (tract/radiation)"),
("Central scotoma", "Optic neuritis / MS"),
("RAPD (afferent pupillary defect)", "Relative afferent defect on swinging torch test"),
],
"causes": [
"Optic neuritis (MS - most common in young women)",
"Ischaemic optic neuropathy (AION/PION)",
"Pituitary adenoma (bitemporal hemianopia)",
"Trauma (traumatic optic neuropathy)",
"Raised ICP (papilloedema - late visual loss)",
"Glaucoma (arcuate field defect)",
],
"umn_lmn": "Visual field defects follow the rule: lesions anterior to chiasm = ipsilateral; at chiasm = bitemporal; post-chiasm = contralateral homonymous",
"pearl": "RAPD: shine torch between eyes repeatedly. The eye with the afferent defect causes BOTH pupils to dilate when the light hits it (Marcus Gunn pupil).",
"color": colors.HexColor("#F0FFF4"),
},
{
"num": "CN III",
"name": "Oculomotor",
"type": "Motor + Parasympathetic",
"exit": "Superior orbital fissure",
"nucleus": "Midbrain (at level of superior colliculus); Edinger-Westphal nucleus (parasympathetic)",
"function": "Levator palpebrae, SR, MR, IR, IO; pupil constriction; accommodation",
"palsy": [
("Complete ptosis", "Loss of levator palpebrae"),
("Eye 'down and out'", "Unopposed lateral rectus (VI) + superior oblique (IV)"),
("Diplopia", "All directions except lateral gaze"),
("Fixed dilated pupil", "Parasympathetic compression - surgical CN III palsy"),
("Pupil SPARING", "Microvascular (diabetic) CN III - pupil often spared"),
("Loss of accommodation", "Ciliary muscle paralysis"),
],
"causes": [
"SURGICAL: Posterior communicating artery (PCoA) aneurysm - PUPIL INVOLVED (compression of peripheral parasympathetic fibres)",
"MEDICAL: Diabetic / hypertensive microvascular ischaemia - PUPIL SPARED",
"Transtentorial herniation (coning - surgical emergency)",
"Cavernous sinus lesion (SOF syndrome)",
"Midbrain infarct / tumour",
"Meningitis, sarcoidosis",
],
"umn_lmn": "KEY RULE: Pupil-involving CN III = surgical emergency (aneurysm). Pupil-sparing = likely ischaemic/medical (but image if >50y, no DM, or pupil becomes involved).",
"pearl": "Diabetic CN III: painful, pupil-spared. PCoA aneurysm CN III: painful or painless, but PUPIL INVOLVED = get CT angiogram NOW.",
"color": colors.HexColor("#FFF8E7"),
},
{
"num": "CN IV",
"name": "Trochlear",
"type": "Motor only",
"exit": "Superior orbital fissure",
"nucleus": "Midbrain (at level of inferior colliculus); only CN to exit DORSALLY",
"function": "Superior oblique muscle: INtorsion + DEpression of adducted eye",
"palsy": [
("Vertical diplopia", "Worse on looking down and in (e.g. reading, stairs)"),
("Head tilt away from lesion", "Compensatory - to reduce diplopia (Bielschowsky test)"),
("Hypertropia", "Affected eye is higher than the other"),
("Extorsion of affected eye", "Seen on fundoscopy - fovea displaced outward"),
],
"causes": [
"Trauma (most common - longest intracranial course, vulnerable)",
"Congenital (most common cause of long-standing head tilt)",
"Microvascular ischaemia (DM, HTN)",
"Midbrain dorsal lesion",
"Cavernous sinus lesion",
],
"umn_lmn": "Sole CN to exit dorsal brainstem; decussates before exiting - a left nuclear/fascicular lesion causes right superior oblique palsy.",
"pearl": "Bielschowsky 3-step test: (1) Which eye is higher? (2) Worse on left or right gaze? (3) Worse on left or right head tilt? - localises to one of the four cyclovertical muscles.",
"color": colors.HexColor("#F0F8FF"),
},
{
"num": "CN V",
"name": "Trigeminal",
"type": "Mixed (Sensory dominant)",
"exit": "V1: Superior orbital fissure; V2: Foramen rotundum; V3: Foramen ovale",
"nucleus": "Principal sensory (touch), Spinal nucleus (pain/temp), Motor nucleus (mastication), Mesencephalic (proprioception)",
"function": "Sensation: face, scalp, cornea, anterior 2/3 tongue, meninges. Motor: muscles of mastication (masseter, temporalis, pterygoids), tensor tympani, tensor veli palatini",
"palsy": [
("Facial numbness", "Ipsilateral to lesion - all or V1/V2/V3 distribution"),
("Corneal reflex loss", "V1 afferent limb absent (efferent = CN VII)"),
("Jaw deviation TOWARD lesion", "Motor branch: pterygoids push jaw to weak side"),
("Jaw jerk absent/reduced", "Nuclear or nerve lesion"),
("Trigeminal neuralgia", "Electric-shock pain V2/V3 territory, trigger points"),
("Neuropathic facial pain", "Persistent burning/aching - nucleus caudalis lesion"),
],
"causes": [
"Trigeminal neuralgia: vascular compression of nerve root (SCA)",
"MS (demyelination of root entry zone)",
"CPA tumour (acoustic neuroma, meningioma)",
"Cavernous sinus lesion (V1 + V2 involved)",
"Herpes zoster (V1 = herpes zoster ophthalmicus)",
"Skull base fractures (specific division)",
],
"umn_lmn": "Motor: Jaw deviates TOWARD the side of LMN lesion (weak pterygoids). UMN lesions rarely cause jaw weakness due to bilateral cortical supply to trigeminal motor nucleus.",
"pearl": "CN V sensory loss + CN VII motor loss on SAME side = peripheral (brainstem or CPA). CN V sensory loss + CN VII upper motor neuron pattern = separate lesions or central.",
"color": colors.HexColor("#FFF0F5"),
},
{
"num": "CN VI",
"name": "Abducens",
"type": "Motor only",
"exit": "Superior orbital fissure",
"nucleus": "Caudal pons (floor of 4th ventricle - forms facial colliculus with CN VII fibers)",
"function": "Lateral rectus: abduction of the eye",
"palsy": [
("Inability to abduct eye", "Cannot look laterally on affected side"),
("Esotropia (convergent squint)", "Medial rectus pulls eye inward unopposed"),
("Horizontal diplopia", "Worst on ipsilateral gaze, images side by side"),
("Head turn toward lesion", "Compensatory to reduce diplopia"),
],
"causes": [
"Raised ICP (false localising sign - nerve stretched over petrous ridge)",
"Microvascular ischaemia (DM, HTN)",
"Wernicke's encephalopathy (thiamine deficiency)",
"CPA / petrous apex tumour (Gradenigo syndrome: CN V + VI + ear pain)",
"Cavernous sinus lesion",
"Pontine glioma (+ CN VII palsy + long tract signs)",
],
"umn_lmn": "Isolated CN VI palsy is the most common false localising sign in raised ICP. The long intracranial course makes it vulnerable to stretch from any cause of raised pressure.",
"pearl": "Gradenigo syndrome: petrous apex pathology causing CN V + VI palsy + ipsilateral ear pain/discharge. Think chronic otitis media or petrous apex cholesteatoma.",
"color": colors.HexColor("#F5F0FF"),
},
{
"num": "CN VII",
"name": "Facial",
"type": "Mixed",
"exit": "Stylomastoid foramen (internal auditory canal en route)",
"nucleus": "Facial nucleus (caudal pons). Rostral subnucleus (upper face) = bilateral UMN input. Caudal subnucleus (lower face) = contralateral UMN only",
"function": "Motor: muscles of facial expression, stapedius, posterior digastric. Parasympathetic: lacrimal, sublingual, submandibular glands. Sensory: taste anterior 2/3 tongue (chorda tympani), sensation near EAM",
"palsy": [
("UMN (central) palsy", "LOWER face only affected - forehead SPARED (bilateral cortical input)"),
("LMN (peripheral) palsy", "ENTIRE ipsilateral face paralysed including forehead"),
("Bell's palsy", "Acute LMN palsy: all divisions, +/- hyperacusis, taste loss, dry eye"),
("Hyperacusis", "Stapedius muscle paralysis - loud sounds distorted"),
("Loss of taste anterior 2/3 tongue", "Chorda tympani involvement"),
("Dry eye", "Parasympathetic to lacrimal gland compromised"),
],
"causes": [
"UMN: Stroke, brain tumour, MS",
"LMN: Bell's palsy (most common - presumed HSV reactivation)",
"Ramsay Hunt syndrome (VZV): LMN palsy + vesicles in ear + sensorineural hearing loss",
"Lyme disease (bilateral facial palsy)",
"Sarcoidosis (Heerfordt syndrome)",
"Parotid tumour / surgery",
"Petrous bone fracture",
],
"umn_lmn": "THE KEY DISTINCTION: UMN - forehead SPARED (bilateral cortical supply). LMN - WHOLE face including forehead. Test forehead wrinkling and eye closure to distinguish.",
"pearl": "Ramsay Hunt = LMN palsy + auricular vesicles + SNHL. Treat with aciclovir + prednisolone. Prognosis worse than Bell's palsy.",
"color": colors.HexColor("#FFF4E6"),
},
{
"num": "CN VIII",
"name": "Vestibulocochlear",
"type": "Sensory only",
"exit": "Internal auditory canal",
"nucleus": "Cochlear nuclei (hearing, dorsal pons); Vestibular nuclei (balance, lateral medulla/pons)",
"function": "Cochlear division: hearing. Vestibular division: balance, head position, VOR",
"palsy": [
("Sensorineural hearing loss", "Cochlear division: discrimination poor, Weber to opposite side, Rinne AC>BC both sides"),
("Tinnitus", "Often accompanies cochlear lesions"),
("Vertigo", "Vestibular division: episodic or constant rotatory vertigo"),
("Nystagmus", "Fast phase away from lesion (peripheral); direction-changing (central)"),
("Positive HINTS exam", "Head impulse normal + direction-changing nystagmus + skew = CENTRAL"),
("Positive Dix-Hallpike", "BPPV - peripheral vestibular"),
],
"causes": [
"Acoustic neuroma (CN VIII schwannoma): unilateral SNHL + tinnitus + vertigo",
"BPPV (benign paroxysmal positional vertigo): most common vestibular disorder",
"Labyrinthitis / vestibular neuritis",
"Meniere's disease: episodic vertigo + SNHL + tinnitus + aural fullness",
"Ototoxic drugs: aminoglycosides, cisplatin, loop diuretics",
"Ramsay Hunt (VZV), CPA tumours",
],
"umn_lmn": "Peripheral CN VIII: unidirectional nystagmus, inhibited by fixation, positive head impulse test, no neurological signs. Central: direction-changing, not inhibited, abnormal HINTS.",
"pearl": "HINTS exam for acute vertigo: Head Impulse (normal in central), Nystagmus (direction-changing = central), Test of Skew (skew deviation = central). One central feature = stroke until proven otherwise.",
"color": colors.HexColor("#EFF9FF"),
},
{
"num": "CN IX",
"name": "Glossopharyngeal",
"type": "Mixed",
"exit": "Jugular foramen",
"nucleus": "Nucleus ambiguus (motor); Inferior salivatory nucleus (parasympathetic); Nucleus solitarius (taste/visceral sensory); Spinal trigeminal nucleus (somatic sensory)",
"function": "Motor: stylopharyngeus (elevates pharynx). Sensory: posterior 1/3 tongue (taste + touch), pharynx, middle ear, carotid body (baro/chemoreceptors). Parasympathetic: parotid gland",
"palsy": [
("Loss of gag reflex (afferent limb)", "IX is AFFERENT of gag; CN X is EFFERENT (palate elevation)"),
("Dysphagia", "Pharyngeal muscle weakness (mild - stylopharyngeus only)"),
("Loss of taste posterior 1/3 tongue", "Ipsilateral"),
("Glossopharyngeal neuralgia", "Lancinating pain in throat/ear triggered by swallowing"),
("Carotid sinus hypersensitivity", "Baroreceptor dysfunction - syncope on neck pressure"),
],
"causes": [
"Jugular foramen lesion (often IX + X + XI together - Vernet syndrome)",
"Glossopharyngeal neuralgia: vascular compression of nerve",
"CPA tumour",
"Neck surgery / trauma",
],
"umn_lmn": "CN IX almost never presents in isolation. Look for jugular foramen syndrome: IX + X + XI together. Gag reflex: IX = afferent, X = efferent - test both components separately.",
"pearl": "Glossopharyngeal neuralgia: swallow-triggered electric shock pain in throat radiating to ear. Occasionally causes cardiac syncope (Hering's nerve - CN IX branch to carotid sinus). Carbamazepine is first-line.",
"color": colors.HexColor("#F0FFF4"),
},
{
"num": "CN X",
"name": "Vagus",
"type": "Mixed (most widely distributed)",
"exit": "Jugular foramen",
"nucleus": "Nucleus ambiguus (branchial motor - pharynx/larynx); Dorsal motor nucleus (parasympathetic); Nucleus solitarius (visceral/taste sensory); Spinal trigeminal (somatic sensory)",
"function": "Motor: pharyngeal muscles (swallowing), laryngeal muscles (voice). Parasympathetic: heart, lungs, gut to splenic flexure. Sensory: larynx, trachea, oesophagus, thoracic/abdominal viscera, taste epiglottis",
"palsy": [
("Hoarseness / dysphonia", "Recurrent laryngeal nerve (RLN) lesion - vocal cord paralysis"),
("Dysphagia", "Pharyngeal weakness; nasal regurgitation"),
("Uvula deviation AWAY from lesion", "Ipsilateral soft palate drops, uvula pulled to normal side"),
("Nasal voice / regurgitation", "Velopharyngeal incompetence"),
("Loss of gag reflex (efferent limb)", "Palate does not elevate; IX provides afferent"),
("Cardiovascular: tachycardia", "Loss of parasympathetic slowing"),
],
"causes": [
"RLN palsy: lung cancer (left RLN loops under aortic arch - LEFT more common), aortic aneurysm, thyroid surgery",
"Nucleus ambiguus lesion: lateral medullary (Wallenberg) syndrome",
"Jugular foramen syndrome (IX+X+XI)",
"Vagal neuroma, skull base tumour",
"Bilateral vagal palsy: causes aspiration, aphonia (emergency)",
],
"umn_lmn": "UMN lesion (bilateral cortical supply to nucleus ambiguus): palate weakness can be masked. LMN (unilateral vagal palsy): uvula deviates AWAY from affected side, ipsilateral palate droops.",
"pearl": "Left RLN palsy is more common than right due to its longer course looping under the aortic arch. Causes: lung apex tumour, enlarged mediastinal nodes, aortic aneurysm. Always get a CXR.",
"color": colors.HexColor("#FFF5F5"),
},
{
"num": "CN XI",
"name": "Spinal Accessory",
"type": "Motor only",
"exit": "Jugular foramen (cranial root from nucleus ambiguus + spinal root from C1-C5)",
"nucleus": "Spinal nucleus C1-C5 (spinal root); nucleus ambiguus (cranial root joins vagus)",
"function": "Motor: sternocleidomastoid (SCM) and trapezius muscles",
"palsy": [
("SCM weakness", "Difficulty turning head AWAY from the lesion (SCM turns head to opposite side)"),
("Trapezius weakness", "Shoulder droop, inability to shrug, winging of scapula (inferior angle)"),
("Shoulder drop", "Ipsilateral shoulder lower than normal"),
("Ipsilateral scapular winging", "Inferior angle displaced laterally - serratus anterior type winging"),
],
"causes": [
"Jugular foramen syndrome (IX + X + XI)",
"Posterior triangle neck surgery / biopsy / lymph node dissection (most common iatrogenic cause)",
"Penetrating neck trauma",
"Cervical lymphadenopathy compressing nerve in posterior triangle",
],
"umn_lmn": "SCM gets contralateral UMN supply (turning head to opposite side). Trapezius gets ipsilateral UMN supply. Therefore: UMN lesion causes contralateral trapezius weakness (ipsilateral SCM spared).",
"pearl": "CN XI is most vulnerable in the posterior triangle of the neck where it is superficial. Iatrogenic injury during lymph node biopsy is a common medicolegal cause of shoulder disability.",
"color": colors.HexColor("#F5FFF5"),
},
{
"num": "CN XII",
"name": "Hypoglossal",
"type": "Motor only",
"exit": "Hypoglossal canal (occipital condyle)",
"nucleus": "Hypoglossal nucleus (floor of 4th ventricle, medulla)",
"function": "Motor: all intrinsic tongue muscles + extrinsic tongue (genioglossus, hyoglossus, styloglossus)",
"palsy": [
("Tongue deviation TOWARD lesion", "Weak genioglossus - normal side pushes tongue toward lesion"),
("Ipsilateral tongue atrophy + fasciculation", "LMN lesion - denervation"),
("Dysarthria", "Slurred, lingual sounds (l, r, n, t, d) most affected"),
("Dysphagia", "Impaired tongue bolus control"),
],
"causes": [
"Medullary lesion (Wallenberg syndrome - ipsilateral XII involvement)",
"Skull base tumour (hypoglossal canal)",
"Dissection of internal carotid artery (ICA) - XII nerve injured near ICA",
"Head and neck cancer / cervical lymphadenopathy",
"Motor neurone disease (bilateral wasting + fasciculation)",
],
"umn_lmn": "UMN CN XII: tongue deviates AWAY from lesion (contralateral = affected). NO atrophy, NO fasciculations. LMN CN XII: tongue deviates TOWARD lesion (ipsilateral = affected). WITH atrophy and fasciculations.",
"pearl": "LMN vs UMN tongue: LMN = atrophy + fasciculations + deviates TOWARD lesion side. UMN = no wasting, deviates AWAY from lesion (same as hemiplegia side - part of hemispheric stroke).",
"color": colors.HexColor("#FFF9E6"),
},
]
# ── Jugular Foramen Syndromes ──────────────────────────────────────────────────
SYNDROMES = [
("Vernet (jugular foramen)", "IX, X, XI", "Jugular foramen tumour (glomus, meningioma, metastasis)", "Dysphagia, hoarseness, shoulder weakness, loss of gag"),
("Collet-Sicard", "IX, X, XI, XII", "Skull base/retropharyngeal tumour", "As above + ipsilateral tongue atrophy"),
("Villaret", "IX, X, XI, XII + Horner's", "Retropharyngeal space / parotid lesion", "As above + ipsilateral Horner syndrome (ptosis, miosis, anhidrosis)"),
("Tapia", "X (RLN) + XII", "Retropharyngeal / neck lesion", "Hoarseness + ipsilateral tongue deviation, no palatal palsy"),
("Gradenigo", "V + VI + ear pain", "Petrous apex (chronic otitis media, cholesteatoma)", "Facial pain, lateral gaze palsy, ipsilateral ear pain/discharge"),
("Cavernous sinus", "III, IV, V1/V2, VI + Horner's", "Cavernous sinus thrombosis, aneurysm, tumour, fistula", "Multiple ophthalmoplegia + facial numbness + Horner"),
("Superior orbital fissure", "III, IV, V1, VI", "Tumour, trauma, inflammation at SOF", "Complete ophthalmoplegia + V1 sensory loss, pupil involved"),
("Orbital apex", "III, IV, V1, VI + CN II", "Extends from SOF to apex; tumour, mucormycosis", "SOF syndrome + visual loss"),
("Wallenberg (lateral medullary)", "IX, X + ipsilateral V, Horner, cerebellar", "PICA territory infarct", "Dysphagia, hoarseness, ipsilateral face numbness, contralateral body hemianalgesia, vertigo, ataxia"),
("Foster Kennedy", "CN I + II + raised ICP signs", "Subfrontal meningioma", "Unilateral anosmia + ipsilateral optic atrophy + contralateral papilloedema"),
]
def make_header(canvas, doc):
canvas.saveState()
w, h = A4
canvas.setFillColor(NAVY)
canvas.rect(0, h - 1.2*cm, w, 1.2*cm, fill=1, stroke=0)
canvas.setFillColor(GOLD)
canvas.setFont("Helvetica-Bold", 8)
canvas.drawString(1.5*cm, h - 0.8*cm, "CRANIAL NERVE PALSY - STUDY GUIDE")
canvas.setFillColor(WHITE)
canvas.setFont("Helvetica", 7.5)
canvas.drawRightString(w - 1.5*cm, h - 0.8*cm, f"Page {doc.page}")
canvas.restoreState()
def make_footer(canvas, doc):
canvas.saveState()
w, h = A4
canvas.setFillColor(MID_GREY)
canvas.rect(0, 0, w, 0.8*cm, fill=1, stroke=0)
canvas.setFillColor(colors.HexColor("#555555"))
canvas.setFont("Helvetica-Oblique", 7)
canvas.drawCentredString(w/2, 0.25*cm, "Sources: Neuroanatomy Through Clinical Cases 3e | Bradley & Daroff's Neurology | Rosen's Emergency Medicine | Localization in Clinical Neurology 8e")
canvas.restoreState()
def header_footer(canvas, doc):
make_header(canvas, doc)
make_footer(canvas, doc)
# ── Document builder ───────────────────────────────────────────────────────────
def build_pdf():
doc = SimpleDocTemplate(
OUTPUT, pagesize=A4,
topMargin=2*cm, bottomMargin=1.5*cm,
leftMargin=1.5*cm, rightMargin=1.5*cm
)
story = []
# ── Cover banner ──────────────────────────────────────────────────────────
cover_data = [[Paragraph("CRANIAL NERVE PALSY", TITLE_STYLE)],
[Paragraph("Comprehensive Study Guide: All 12 Cranial Nerves", SUBTITLE_STYLE)],
[Paragraph("UMN vs LMN Patterns · Clinical Features · Causes · Named Syndromes", SUBTITLE_STYLE)]]
cover_table = Table(cover_data, colWidths=[18*cm])
cover_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("ROWBACKGROUNDS", (0,0), (-1,-1), [NAVY]),
("TOPPADDING", (0,0), (-1,-1), 10),
("BOTTOMPADDING", (0,0), (-1,-1), 10),
("LEFTPADDING", (0,0), (-1,-1), 14),
("RIGHTPADDING", (0,0), (-1,-1), 14),
("ROUNDEDCORNERS", [6]),
]))
story.append(Spacer(1, 0.2*cm))
story.append(cover_table)
story.append(Spacer(1, 0.5*cm))
# ── Quick Mnemonic ────────────────────────────────────────────────────────
mn_data = [[
Paragraph("OH OH OH TO TOUCH AND FEEL VERY GOOD VELVET, AH HEAVEN!", MNEMONIC),
]]
mn_table = Table(mn_data, colWidths=[18*cm])
mn_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS", [4]),
]))
story.append(mn_table)
story.append(Spacer(1, 0.15*cm))
story.append(Paragraph(
"CN I(O) II(H) III(O) IV(T) V(T) VI(A) VII(F) VIII(V) IX(G) X(V) XI(A) XII(H)",
ParagraphStyle("sub_mn", fontSize=8, textColor=colors.HexColor("#555555"),
fontName="Helvetica", alignment=TA_CENTER, leading=11)
))
story.append(Spacer(1, 0.3*cm))
# ── Type overview table ───────────────────────────────────────────────────
story.append(Paragraph("Cranial Nerve Overview", H2))
overview_header = [
Paragraph("CN", TABLE_HEAD), Paragraph("Name", TABLE_HEAD),
Paragraph("Type", TABLE_HEAD), Paragraph("Exit Foramen", TABLE_HEAD),
Paragraph("Key Function", TABLE_HEAD)
]
overview_rows = [overview_header]
for cn in CN_DATA:
row = [
Paragraph(f"<b>{cn['num']}</b>", TABLE_CELL_BOLD),
Paragraph(cn["name"], TABLE_CELL_BOLD),
Paragraph(cn["type"], TABLE_CELL),
Paragraph(cn["exit"].split(";")[0], SMALL),
Paragraph(cn["function"][:80] + ("..." if len(cn["function"]) > 80 else ""), TABLE_CELL),
]
overview_rows.append(row)
ov_table = Table(overview_rows, colWidths=[1.2*cm, 2.8*cm, 3.2*cm, 3.8*cm, 7*cm])
ov_style = TableStyle([
("BACKGROUND", (0,0), (-1,0), TEAL),
("GRID", (0,0), (-1,-1), 0.4, MID_GREY),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_GREY]),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
])
ov_table.setStyle(ov_style)
story.append(ov_table)
story.append(PageBreak())
# ── Individual CN sections ─────────────────────────────────────────────────
for cn in CN_DATA:
# Section header bar
hdr_data = [[Paragraph(f"{cn['num']}: {cn['name']} Nerve | {cn['type']}", H1)]]
hdr_table = Table(hdr_data, colWidths=[18*cm])
hdr_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), TEAL),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS", [4]),
]))
story.append(KeepTogether([hdr_table, Spacer(1, 0.15*cm)]))
# Info row: nucleus + exit
info_data = [[
Paragraph(f"<b>Nucleus/Origin:</b> {cn['nucleus']}", SMALL),
Paragraph(f"<b>Exit:</b> {cn['exit']}", SMALL),
]]
info_table = Table(info_data, colWidths=[10*cm, 8*cm])
info_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), LIGHT_GREY),
("GRID", (0,0), (-1,-1), 0.3, MID_GREY),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(info_table)
story.append(Spacer(1, 0.15*cm))
# Two-column: Palsy Features | Causes
palsy_items = [Paragraph("<b>PALSY FEATURES</b>", TABLE_CELL_BOLD)]
for sign, detail in cn["palsy"]:
palsy_items.append(Paragraph(f"<b>• {sign}:</b> {detail}", TABLE_CELL))
causes_items = [Paragraph("<b>CAUSES</b>", TABLE_CELL_BOLD)]
for c in cn["causes"]:
causes_items.append(Paragraph(f"• {c}", TABLE_CELL))
max_rows = max(len(palsy_items), len(causes_items))
while len(palsy_items) < max_rows:
palsy_items.append(Paragraph("", TABLE_CELL))
while len(causes_items) < max_rows:
causes_items.append(Paragraph("", TABLE_CELL))
two_col_data = [[p, c] for p, c in zip(palsy_items, causes_items)]
two_col_table = Table(two_col_data, colWidths=[9*cm, 9*cm])
two_col_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,0), LIGHT_TEAL),
("BACKGROUND", (1,0), (1,0), LIGHT_TEAL),
("TEXTCOLOR", (0,0), (-1,0), WHITE),
("BACKGROUND", (0,1), (0,-1), cn["color"]),
("BACKGROUND", (1,1), (1,-1), colors.HexColor("#FAFAFA")),
("GRID", (0,0), (-1,-1), 0.3, MID_GREY),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 3),
("LEFTPADDING", (0,0), (-1,-1), 6),
("RIGHTPADDING", (0,0), (-1,-1), 6),
]))
story.append(two_col_table)
story.append(Spacer(1, 0.12*cm))
# UMN/LMN bar
umn_data = [[
Paragraph("UMN vs LMN / Localisation:", TABLE_CELL_BOLD),
Paragraph(cn["umn_lmn"], TABLE_CELL),
]]
umn_table = Table(umn_data, colWidths=[4*cm, 14*cm])
umn_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), AMBER_LIGHT),
("LINEBELOW", (0,0), (-1,-1), 0.5, GOLD),
("LINETOP", (0,0), (-1,-1), 0.5, GOLD),
("TOPPADDING", (0,0), (-1,-1), 4),
("BOTTOMPADDING", (0,0), (-1,-1), 4),
("LEFTPADDING", (0,0), (-1,-1), 6),
("VALIGN", (0,0), (-1,-1), "TOP"),
]))
story.append(umn_table)
story.append(Spacer(1, 0.1*cm))
# Clinical pearl
pearl_data = [[
Paragraph("CLINICAL PEARL", ParagraphStyle("PearlHdr", fontSize=8,
textColor=WHITE, fontName="Helvetica-Bold", leading=11)),
Paragraph(cn["pearl"], PEARL),
]]
pearl_table = Table(pearl_data, colWidths=[2.5*cm, 15.5*cm])
pearl_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (0,-1), GOLD),
("BACKGROUND", (1,0), (1,-1), colors.HexColor("#FFFCE8")),
("LINEABOVE", (0,0), (-1,0), 0.5, GOLD),
("LINEBELOW", (0,0), (-1,-1), 0.5, GOLD),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 6),
]))
story.append(pearl_table)
story.append(Spacer(1, 0.4*cm))
# ── Named Syndromes ────────────────────────────────────────────────────────
story.append(PageBreak())
syn_hdr = Table([[Paragraph("NAMED CRANIAL NERVE SYNDROMES", H1)]], colWidths=[18*cm])
syn_hdr.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), NAVY),
("TOPPADDING", (0,0), (-1,-1), 8),
("BOTTOMPADDING", (0,0), (-1,-1), 8),
("LEFTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS", [4]),
]))
story.append(syn_hdr)
story.append(Spacer(1, 0.2*cm))
syn_header_row = [
Paragraph("Syndrome", TABLE_HEAD),
Paragraph("CNs Involved", TABLE_HEAD),
Paragraph("Site / Cause", TABLE_HEAD),
Paragraph("Key Features", TABLE_HEAD),
]
syn_rows = [syn_header_row]
for s in SYNDROMES:
syn_rows.append([
Paragraph(f"<b>{s[0]}</b>", TABLE_CELL_BOLD),
Paragraph(s[1], TABLE_CELL),
Paragraph(s[2], TABLE_CELL),
Paragraph(s[3], TABLE_CELL),
])
syn_table = Table(syn_rows, colWidths=[4*cm, 2.5*cm, 5.5*cm, 6*cm])
syn_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("GRID", (0,0), (-1,-1), 0.4, MID_GREY),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_GREY]),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
]))
story.append(syn_table)
# ── Quick comparison table ─────────────────────────────────────────────────
story.append(Spacer(1, 0.5*cm))
comp_hdr = Table([[Paragraph("MOTOR DEVIATION QUICK REFERENCE", H1)]], colWidths=[18*cm])
comp_hdr.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,-1), TEAL),
("TOPPADDING", (0,0), (-1,-1), 6),
("BOTTOMPADDING", (0,0), (-1,-1), 6),
("LEFTPADDING", (0,0), (-1,-1), 10),
("ROUNDEDCORNERS", [4]),
]))
story.append(comp_hdr)
story.append(Spacer(1, 0.15*cm))
dev_header = [
Paragraph("Structure", TABLE_HEAD),
Paragraph("LMN Deviation", TABLE_HEAD),
Paragraph("UMN Deviation", TABLE_HEAD),
Paragraph("Extra Feature", TABLE_HEAD),
]
dev_rows = [
dev_header,
[Paragraph("<b>Jaw (CN V)</b>", TABLE_CELL_BOLD),
Paragraph("TOWARD lesion (weak pterygoids)", TABLE_CELL),
Paragraph("Usually SPARED (bilateral cortical supply)", TABLE_CELL),
Paragraph("Jaw jerk absent in LMN, brisk in bilateral UMN", TABLE_CELL)],
[Paragraph("<b>Face (CN VII)</b>", TABLE_CELL_BOLD),
Paragraph("ENTIRE ipsilateral face + forehead", TABLE_CELL),
Paragraph("LOWER face only - forehead SPARED", TABLE_CELL),
Paragraph("LMN: taste loss, dry eye, hyperacusis", TABLE_CELL)],
[Paragraph("<b>Uvula (CN X)</b>", TABLE_CELL_BOLD),
Paragraph("AWAY from lesion (normal side pulls)", TABLE_CELL),
Paragraph("May be masked by bilateral supply", TABLE_CELL),
Paragraph("Gag reflex: IX afferent, X efferent", TABLE_CELL)],
[Paragraph("<b>Tongue (CN XII)</b>", TABLE_CELL_BOLD),
Paragraph("TOWARD lesion + atrophy + fasciculation", TABLE_CELL),
Paragraph("AWAY from lesion, no wasting", TABLE_CELL),
Paragraph("LMN: tongue looks 'half-shrivelled'", TABLE_CELL)],
[Paragraph("<b>SCM (CN XI)</b>", TABLE_CELL_BOLD),
Paragraph("Weakness turning head AWAY from lesion", TABLE_CELL),
Paragraph("Contralateral SCM weak (UMN to SCM is crossed)", TABLE_CELL),
Paragraph("Trapezius: shoulder droop ipsilateral", TABLE_CELL)],
]
dev_table = Table(dev_rows, colWidths=[3*cm, 4.5*cm, 4.5*cm, 6*cm])
dev_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), TEAL),
("GRID", (0,0), (-1,-1), 0.4, MID_GREY),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_GREY]),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
]))
story.append(dev_table)
story.append(Spacer(1, 0.5*cm))
# ── Sensory modalities summary ─────────────────────────────────────────────
story.append(Paragraph("CN Sensory Testing Summary", H2))
sens_rows = [
[Paragraph("CN", TABLE_HEAD), Paragraph("What to Test", TABLE_HEAD), Paragraph("How", TABLE_HEAD), Paragraph("Lesion Sign", TABLE_HEAD)],
[Paragraph("I", TABLE_CELL_BOLD), Paragraph("Smell", TABLE_CELL), Paragraph("Coffee/mint each nostril", TABLE_CELL), Paragraph("Anosmia / hyposmia ipsilateral", TABLE_CELL)],
[Paragraph("II", TABLE_CELL_BOLD), Paragraph("Vision, colour, fields", TABLE_CELL), Paragraph("Snellen, Ishihara, confrontation, fundoscopy", TABLE_CELL), Paragraph("Field defect, RAPD, reduced acuity", TABLE_CELL)],
[Paragraph("V1-3", TABLE_CELL_BOLD), Paragraph("Facial sensation (light touch + pin)", TABLE_CELL), Paragraph("3 divisions: forehead/cheek/jaw bilaterally + corneal reflex", TABLE_CELL), Paragraph("Dermatomal sensory loss; absent corneal reflex afferent", TABLE_CELL)],
[Paragraph("VII", TABLE_CELL_BOLD), Paragraph("Taste anterior 2/3 tongue", TABLE_CELL), Paragraph("Sweet/salty/sour each side of anterior tongue", TABLE_CELL), Paragraph("Ipsilateral taste loss (LMN)", TABLE_CELL)],
[Paragraph("VIII", TABLE_CELL_BOLD), Paragraph("Hearing (Weber + Rinne) + vestibular (Dix-Hallpike, HINTS)", TABLE_CELL), Paragraph("512Hz tuning fork; Hallpike; head impulse", TABLE_CELL), Paragraph("SNHL: Weber to normal side; BPPV: positive Hallpike", TABLE_CELL)],
[Paragraph("IX", TABLE_CELL_BOLD), Paragraph("Gag reflex (afferent); taste posterior 1/3 tongue", TABLE_CELL), Paragraph("Touch posterior pharynx each side", TABLE_CELL), Paragraph("Absent gag sensation ipsilateral", TABLE_CELL)],
[Paragraph("X", TABLE_CELL_BOLD), Paragraph("Gag reflex (efferent); palate elevation", TABLE_CELL), Paragraph("Say 'aah' - watch uvula", TABLE_CELL), Paragraph("Uvula deviates AWAY from lesion", TABLE_CELL)],
]
sens_table = Table(sens_rows, colWidths=[1.2*cm, 3.5*cm, 6.5*cm, 6.8*cm])
sens_table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), NAVY),
("GRID", (0,0), (-1,-1), 0.4, MID_GREY),
("ROWBACKGROUNDS", (0,1), (-1,-1), [WHITE, LIGHT_GREY]),
("VALIGN", (0,0), (-1,-1), "TOP"),
("TOPPADDING", (0,0), (-1,-1), 5),
("BOTTOMPADDING", (0,0), (-1,-1), 5),
("LEFTPADDING", (0,0), (-1,-1), 5),
("RIGHTPADDING", (0,0), (-1,-1), 5),
]))
story.append(sens_table)
# Build
doc.build(story, onFirstPage=header_footer, onLaterPages=header_footer)
print(f"PDF written to: {OUTPUT}")
build_pdf()
cd /home/daytona/workspace/cn-study-guide && python generate_cn_guide.py 2>&1
ls -lh /home/daytona/workspace/cn-study-guide/CN_Palsy_Study_Guide.pdf
CN Palsy Study Guide
PDF Document · PDF
Why is mylohyoid known as safety muscle?
mylohyoid "safety muscle" floor of mouth anatomy clinical significance
https://www.intjmorphol.com/wp-content/uploads/2022/10/Art_0…
"Ludwig's angina is a life-threatening airway complication of submandibular space infection, characterized by rapid onset of cellulitis of the floor of mouth tissues, causing swelling, induration and tongue elevation resulting in a compromised airway."
| Property | What the Mylohyoid Does | What Happens When Breached |
|---|---|---|
| Structural floor | Supports tongue from below | Tongue drops - airway at risk |
| Space divider | Keeps sublingual & submandibular spaces separate | Infection spreads freely between spaces |
| Swelling containment | Directs swelling upward/medially | Swelling spreads into neck and mediastinum |
| Organ containment | Keeps salivary gland cysts above | Plunging ranula herniates into neck |
| Condition | Mylohyoid Role |
|---|---|
| Ludwig's angina | Infection crosses free posterior border of mylohyoid - floor of mouth cellulitis - airway emergency |
| Plunging ranula | Sublingual gland cyst herniates through mylohyoid into neck |
| Dental implants (lingual) | Implant perforation of mylohyoid can injure sublingual artery - life-threatening haematoma of floor of mouth |
| Periapical abscess (lower molars) | Root apices sit BELOW mylohyoid line - infection goes directly to submandibular space, not sublingual |
| Mandibular dentures | Mylohyoid ridge is the posterior lingual limit for denture extension |
Upper limb exam guide
~/cn-study-guide/upper_limb_guide.py
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import cm
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, PageBreak, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
OUTPUT = "/home/daytona/workspace/cn-study-guide/Upper_Limb_Anatomy_Guide.pdf"
# ── Palette ────────────────────────────────────────────────────────────────────
NAVY = colors.HexColor("#1A2E4A")
TEAL = colors.HexColor("#0D7377")
LT_TEAL = colors.HexColor("#14A7AC")
GOLD = colors.HexColor("#E8A020")
LT_GREY = colors.HexColor("#F4F6F9")
MID_GREY = colors.HexColor("#D0D6DF")
WHITE = colors.white
AMBER_LT = colors.HexColor("#FFFBF0")
AMBER_DK = colors.HexColor("#D68910")
RED_LT = colors.HexColor("#FFF3F3")
GREEN_LT = colors.HexColor("#F0FFF4")
BLUE_LT = colors.HexColor("#EFF9FF")
PURPLE_LT = colors.HexColor("#F5F0FF")
# ── Styles ─────────────────────────────────────────────────────────────────────
def S(name, **kw):
base = dict(fontName="Helvetica", fontSize=9, leading=13,
textColor=colors.HexColor("#222"), alignment=TA_LEFT)
base.update(kw)
return ParagraphStyle(name, **base)
TITLE = S("T", fontSize=24, fontName="Helvetica-Bold", textColor=WHITE, alignment=TA_CENTER, leading=30)
SUBT = S("ST", fontSize=12, textColor=LT_TEAL, alignment=TA_CENTER, leading=16)
H1 = S("H1", fontSize=15, fontName="Helvetica-Bold", textColor=WHITE, leading=20)
H2 = S("H2", fontSize=11, fontName="Helvetica-Bold", textColor=NAVY, leading=15, spaceBefore=6)
H3 = S("H3", fontSize=9.5, fontName="Helvetica-Bold", textColor=TEAL, leading=13)
BODY = S("B", fontSize=8.5, leading=12, alignment=TA_JUSTIFY)
SMALL = S("Sm", fontSize=7.5, textColor=colors.HexColor("#555"), leading=11)
TH = S("TH", fontSize=8, fontName="Helvetica-Bold", textColor=WHITE, alignment=TA_CENTER, leading=11)
TC = S("TC", fontSize=7.8, leading=11)
TCB = S("TCB", fontSize=7.8, fontName="Helvetica-Bold", textColor=NAVY, leading=11)
TCS = S("TCS", fontSize=7.2, textColor=colors.HexColor("#444"), leading=10)
PEARL = S("Pearl", fontSize=8, fontName="Helvetica-Oblique", leading=12)
MNEM = S("Mn", fontSize=10, fontName="Helvetica-Bold", textColor=GOLD, alignment=TA_CENTER, leading=14)
def hdr_footer(canvas, doc):
w, h = A4
canvas.saveState()
canvas.setFillColor(NAVY); canvas.rect(0, h-1.2*cm, w, 1.2*cm, fill=1, stroke=0)
canvas.setFillColor(GOLD); canvas.setFont("Helvetica-Bold", 8)
canvas.drawString(1.5*cm, h-0.8*cm, "UPPER LIMB ANATOMY — EXAM GUIDE")
canvas.setFillColor(WHITE); canvas.setFont("Helvetica", 7.5)
canvas.drawRightString(w-1.5*cm, h-0.8*cm, f"Page {doc.page}")
canvas.setFillColor(MID_GREY); canvas.rect(0, 0, w, 0.8*cm, fill=1, stroke=0)
canvas.setFillColor(colors.HexColor("#555")); canvas.setFont("Helvetica-Oblique", 7)
canvas.drawCentredString(w/2, 0.25*cm,
"Sources: Gray's Anatomy for Students | Bradley & Daroff's Neurology | Das Manual on Clinical Surgery | Neuroanatomy Through Clinical Cases 3e")
canvas.restoreState()
def section_bar(title, bg=TEAL):
t = Table([[Paragraph(title, H1)]], colWidths=[18*cm])
t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),bg),
("TOPPADDING",(0,0),(-1,-1),6),("BOTTOMPADDING",(0,0),(-1,-1),6),
("LEFTPADDING",(0,0),(-1,-1),10),("ROUNDEDCORNERS",[4])]))
return t
def pearl_box(text):
t = Table([[Paragraph("PEARL", S("ph",fontSize=8,fontName="Helvetica-Bold",textColor=WHITE,leading=11)),
Paragraph(text, PEARL)]], colWidths=[1.8*cm,16.2*cm])
t.setStyle(TableStyle([("BACKGROUND",(0,0),(0,-1),GOLD),
("BACKGROUND",(1,0),(1,-1),colors.HexColor("#FFFCE8")),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),6),("VALIGN",(0,0),(-1,-1),"MIDDLE"),
("LINEABOVE",(0,0),(-1,0),0.5,GOLD),("LINEBELOW",(0,0),(-1,-1),0.5,GOLD)]))
return t
def two_col(left_content, right_content, lw=9*cm, rw=9*cm, lbg=LT_GREY, rbg=WHITE):
t = Table([[left_content, right_content]], colWidths=[lw, rw])
t.setStyle(TableStyle([("BACKGROUND",(0,0),(0,-1),lbg),("BACKGROUND",(1,0),(1,-1),rbg),
("VALIGN",(0,0),(-1,-1),"TOP"),("GRID",(0,0),(-1,-1),0.3,MID_GREY),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),6),("RIGHTPADDING",(0,0),(-1,-1),6)]))
return t
def std_table(rows, col_widths, header_bg=NAVY):
t = Table(rows, colWidths=col_widths)
t.setStyle(TableStyle([
("BACKGROUND",(0,0),(-1,0),header_bg),
("GRID",(0,0),(-1,-1),0.4,MID_GREY),
("ROWBACKGROUNDS",(0,1),(-1,-1),[WHITE,LT_GREY]),
("VALIGN",(0,0),(-1,-1),"TOP"),
("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),5),("RIGHTPADDING",(0,0),(-1,-1),5)]))
return t
# ══════════════════════════════════════════════════════════════════════════════
def build():
doc = SimpleDocTemplate(OUTPUT, pagesize=A4,
topMargin=2*cm, bottomMargin=1.5*cm, leftMargin=1.5*cm, rightMargin=1.5*cm)
story = []
# ── Cover ─────────────────────────────────────────────────────────────────
cover = Table([[Paragraph("UPPER LIMB", TITLE)],
[Paragraph("Anatomy-Based Exam Study Guide", SUBT)],
[Paragraph("Brachial Plexus · Muscles · Nerves · Dermatomes · Reflexes · Nerve Injuries", SUBT)]],
colWidths=[18*cm])
cover.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),NAVY),
("TOPPADDING",(0,0),(-1,-1),10),("BOTTOMPADDING",(0,0),(-1,-1),10),
("LEFTPADDING",(0,0),(-1,-1),14),("ROUNDEDCORNERS",[6])]))
story += [Spacer(1,0.2*cm), cover, Spacer(1,0.4*cm)]
# ── Quick root mnemonic ───────────────────────────────────────────────────
mn = Table([[Paragraph("ROOT VALUES: C5 = Shoulder abduction | C6 = Elbow flexion / wrist extension | C7 = Elbow extension / wrist flexion | C8 = Finger flexion | T1 = Finger abduction/adduction", MNEM)]],
colWidths=[18*cm])
mn.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),NAVY),
("TOPPADDING",(0,0),(-1,-1),7),("BOTTOMPADDING",(0,0),(-1,-1),7),
("LEFTPADDING",(0,0),(-1,-1),8),("ROUNDEDCORNERS",[4])]))
story += [mn, Spacer(1,0.3*cm)]
# ══ 1. BRACHIAL PLEXUS ════════════════════════════════════════════════════
story += [section_bar("1. BRACHIAL PLEXUS"), Spacer(1,0.15*cm)]
story.append(Paragraph("Structure: Roots → Trunks → Divisions → Cords → Branches (mnemonic: Real Teenagers Drink Cold Beer)", H2))
story.append(Spacer(1,0.1*cm))
plexus_rows = [
[Paragraph("Level", TH), Paragraph("Components", TH), Paragraph("Roots", TH), Paragraph("Notes", TH)],
[Paragraph("<b>Roots</b>", TCB), Paragraph("C5, C6, C7, C8, T1 anterior rami", TC),
Paragraph("C5–T1", TC), Paragraph("Pass between anterior & middle scalene muscles", TC)],
[Paragraph("<b>Trunks</b>", TCB),
Paragraph("Superior (C5+C6)\nMiddle (C7)\nInferior (C8+T1)", TC),
Paragraph("C5–T1", TC), Paragraph("Superior trunk most vulnerable in Erb's palsy. Inferior trunk in Klumpke's palsy", TC)],
[Paragraph("<b>Divisions</b>", TCB), Paragraph("Each trunk → anterior + posterior division (6 total)", TC),
Paragraph("C5–T1", TC), Paragraph("Anterior = flexor compartments; Posterior = extensor compartments", TC)],
[Paragraph("<b>Cords</b>", TCB),
Paragraph("Lateral cord (ant. sup+mid)\nMedial cord (ant. inf)\nPosterior cord (all post.)", TC),
Paragraph("Lateral: C5–C7\nMedial: C8–T1\nPosterior: C5–T1", TC),
Paragraph("Named by relation to axillary artery. Posterior cord → all extensor nerves", TC)],
[Paragraph("<b>Terminal Branches</b>", TCB),
Paragraph("Musculocutaneous (lat)\nMedian (lat+med roots)\nUlnar (med)\nRadial (post)\nAxillary (post)", TC),
Paragraph("C5–C7\nC5–T1\nC8–T1\nC5–T1\nC5–C6", TC),
Paragraph("Median nerve = lat + med cord union (M shape on brachial plexus)", TC)],
]
story += [std_table(plexus_rows, [3*cm,5.5*cm,3*cm,6.5*cm]), Spacer(1,0.15*cm)]
# Cord branches summary
story.append(Paragraph("Key Cord Branches (non-terminal)", H3))
cord_rows = [
[Paragraph("Cord", TH), Paragraph("Branch", TH), Paragraph("Roots", TH), Paragraph("Supply", TH)],
[Paragraph("Lateral", TCB), Paragraph("Lateral pectoral nerve", TC), Paragraph("C5–C7", TC), Paragraph("Pectoralis major (clavicular head)", TC)],
[Paragraph("Medial", TCB), Paragraph("Medial pectoral nerve", TC), Paragraph("C8–T1", TC), Paragraph("Pectoralis minor + sternal pectoralis major", TC)],
[Paragraph("Medial", TCB), Paragraph("Medial cutaneous nerve of arm", TC), Paragraph("T1", TC), Paragraph("Medial arm skin", TC)],
[Paragraph("Medial", TCB), Paragraph("Medial cutaneous nerve of forearm", TC), Paragraph("C8–T1", TC), Paragraph("Medial forearm skin", TC)],
[Paragraph("Posterior", TCB), Paragraph("Axillary nerve", TC), Paragraph("C5–C6", TC), Paragraph("Deltoid, teres minor; lateral arm skin (regimental badge)", TC)],
[Paragraph("Posterior", TCB), Paragraph("Thoracodorsal nerve", TC), Paragraph("C6–C8", TC), Paragraph("Latissimus dorsi", TC)],
[Paragraph("Posterior", TCB), Paragraph("Upper/lower subscapular", TC), Paragraph("C5–C6", TC), Paragraph("Subscapularis, teres major", TC)],
[Paragraph("Posterior", TCB), Paragraph("Long thoracic nerve (of Bell)", TC), Paragraph("C5–C7", TC), Paragraph("Serratus anterior — injury = medial scapular winging", TC)],
[Paragraph("Upper trunk", TCB), Paragraph("Suprascapular nerve", TC), Paragraph("C5–C6", TC), Paragraph("Supraspinatus, infraspinatus (shoulder abduction + ER)", TC)],
[Paragraph("Upper trunk", TCB), Paragraph("Nerve to subclavius", TC), Paragraph("C5–C6", TC), Paragraph("Subclavius", TC)],
]
story += [std_table(cord_rows, [2.5*cm,4.5*cm,2.5*cm,8.5*cm]), Spacer(1,0.3*cm)]
story += [pearl_box("The 'M' of the brachial plexus: the musculocutaneous, median, and medial root of median nerve form an 'M' shape around the axillary artery on cross-section — helps identify the median nerve intraoperatively."), Spacer(1,0.4*cm)]
# ══ 2. MUSCLES BY REGION ══════════════════════════════════════════════════
story += [PageBreak(), section_bar("2. MUSCLES OF THE UPPER LIMB — REGION BY REGION"), Spacer(1,0.15*cm)]
# -- Shoulder --
story.append(Paragraph("2a. Shoulder Muscles", H2))
sh_rows = [
[Paragraph("Muscle", TH), Paragraph("Origin", TH), Paragraph("Insertion", TH), Paragraph("Action", TH), Paragraph("Nerve", TH), Paragraph("Root", TH)],
[Paragraph("Deltoid", TCB), Paragraph("Clavicle (lat 1/3), acromion, spine of scapula", TCS), Paragraph("Deltoid tuberosity of humerus", TCS), Paragraph("Abduction (middle), flexion (ant), extension (post)", TCS), Paragraph("Axillary", TC), Paragraph("C5, C6", TC)],
[Paragraph("Supraspinatus", TCB), Paragraph("Supraspinous fossa", TCS), Paragraph("Greater tubercle (sup facet)", TCS), Paragraph("Initiates abduction (0–15°), stabilises GHJ", TCS), Paragraph("Suprascapular", TC), Paragraph("C5, C6", TC)],
[Paragraph("Infraspinatus", TCB), Paragraph("Infraspinous fossa", TCS), Paragraph("Greater tubercle (mid facet)", TCS), Paragraph("External rotation, stabilises GHJ", TCS), Paragraph("Suprascapular", TC), Paragraph("C5, C6", TC)],
[Paragraph("Teres minor", TCB), Paragraph("Lateral border scapula", TCS), Paragraph("Greater tubercle (inf facet)", TCS), Paragraph("External rotation, weak adduction", TCS), Paragraph("Axillary", TC), Paragraph("C5, C6", TC)],
[Paragraph("Subscapularis", TCB), Paragraph("Subscapular fossa", TCS), Paragraph("Lesser tubercle of humerus", TCS), Paragraph("Internal rotation, adduction", TCS), Paragraph("Upper + lower subscapular", TC), Paragraph("C5–C7", TC)],
[Paragraph("Teres major", TCB), Paragraph("Inferior angle of scapula", TCS), Paragraph("Medial lip of intertubercular sulcus", TCS), Paragraph("Internal rotation, adduction, extension", TCS), Paragraph("Lower subscapular", TC), Paragraph("C5–C7", TC)],
[Paragraph("Coracobrachialis", TCB), Paragraph("Coracoid process", TCS), Paragraph("Mid-medial humerus shaft", TCS), Paragraph("Flexion + adduction of arm", TCS), Paragraph("Musculocutaneous", TC), Paragraph("C5–C7", TC)],
]
story += [std_table(sh_rows, [2.8*cm,3*cm,3*cm,3.5*cm,2.7*cm,1.5*cm]), Spacer(1,0.2*cm)]
story += [pearl_box("SITS = Rotator cuff: Supraspinatus, Infraspinatus, Teres minor, Subscapularis. All insert into the greater/lesser tubercle and stabilise the glenohumeral joint. Only subscapularis = INTERNAL rotator; the other 3 = external rotators."), Spacer(1,0.2*cm)]
# -- Arm --
story.append(Paragraph("2b. Arm Muscles (Humerus to Elbow)", H2))
arm_rows = [
[Paragraph("Muscle", TH), Paragraph("Compartment", TH), Paragraph("Action", TH), Paragraph("Nerve", TH), Paragraph("Root", TH)],
[Paragraph("Biceps brachii", TCB), Paragraph("Anterior", TC), Paragraph("Elbow flexion (supinated), forearm supination, shoulder flexion (long head)", TC), Paragraph("Musculocutaneous", TC), Paragraph("C5, C6", TC)],
[Paragraph("Brachialis", TCB), Paragraph("Anterior", TC), Paragraph("Elbow flexion (prime mover, all positions)", TC), Paragraph("Musculocutaneous (mainly); small radial contribution", TC), Paragraph("C5, C6", TC)],
[Paragraph("Coracobrachialis", TCB), Paragraph("Anterior", TC), Paragraph("Shoulder flexion + adduction", TC), Paragraph("Musculocutaneous", TC), Paragraph("C5–C7", TC)],
[Paragraph("Triceps brachii", TCB), Paragraph("Posterior", TC), Paragraph("Elbow extension (prime mover); long head assists shoulder extension/adduction", TC), Paragraph("Radial", TC), Paragraph("C6, C7, C8", TC)],
[Paragraph("Anconeus", TCB), Paragraph("Posterior", TC), Paragraph("Elbow extension (assists triceps)", TC), Paragraph("Radial", TC), Paragraph("C7, C8", TC)],
]
story += [std_table(arm_rows, [3.2*cm,2.5*cm,5.8*cm,3.5*cm,3*cm]), Spacer(1,0.2*cm)]
# -- Forearm --
story.append(Paragraph("2c. Forearm Muscles — Anterior (Flexor) Compartment", H2))
fa_ant_rows = [
[Paragraph("Layer", TH), Paragraph("Muscle", TH), Paragraph("Action", TH), Paragraph("Nerve", TH), Paragraph("Root", TH)],
[Paragraph("Superficial", TCB), Paragraph("Pronator teres", TC), Paragraph("Pronation, weak elbow flexion", TC), Paragraph("Median", TC), Paragraph("C6, C7", TC)],
[Paragraph("Superficial", TCB), Paragraph("Flexor carpi radialis (FCR)", TC), Paragraph("Wrist flexion + radial deviation", TC), Paragraph("Median", TC), Paragraph("C6, C7", TC)],
[Paragraph("Superficial", TCB), Paragraph("Palmaris longus", TC), Paragraph("Weak wrist flexion (absent in 14%)", TC), Paragraph("Median", TC), Paragraph("C7, C8", TC)],
[Paragraph("Superficial", TCB), Paragraph("Flexor carpi ulnaris (FCU)", TC), Paragraph("Wrist flexion + ulnar deviation", TC), Paragraph("Ulnar", TC), Paragraph("C7, C8", TC)],
[Paragraph("Superficial", TCB), Paragraph("Flexor digitorum superficialis (FDS)", TC), Paragraph("Flex middle phalanx digits 2–5; assists wrist flexion", TC), Paragraph("Median", TC), Paragraph("C7, C8, T1", TC)],
[Paragraph("Deep", TCB), Paragraph("Flexor digitorum profundus (FDP)", TC), Paragraph("Flex distal phalanx digits 2–5 (FDP 2–3 = median; FDP 4–5 = ulnar)", TC), Paragraph("Median (AIN) + Ulnar", TC), Paragraph("C8, T1", TC)],
[Paragraph("Deep", TCB), Paragraph("Flexor pollicis longus (FPL)", TC), Paragraph("Flex distal phalanx of thumb", TC), Paragraph("Median (AIN)", TC), Paragraph("C8, T1", TC)],
[Paragraph("Deep", TCB), Paragraph("Pronator quadratus", TC), Paragraph("Pronation (main pronator)", TC), Paragraph("Median (AIN)", TC), Paragraph("C8, T1", TC)],
[Paragraph("Lateral", TCB), Paragraph("Brachioradialis", TC), Paragraph("Elbow flexion (forearm in neutral/semiprone)", TC), Paragraph("Radial (before bifurcation)", TC), Paragraph("C5, C6", TC)],
]
story += [std_table(fa_ant_rows, [2.5*cm,4*cm,5*cm,3.5*cm,3*cm]), Spacer(1,0.15*cm)]
story.append(Paragraph("2d. Forearm Muscles — Posterior (Extensor) Compartment", H2))
fa_post_rows = [
[Paragraph("Layer", TH), Paragraph("Muscle", TH), Paragraph("Action", TH), Paragraph("Nerve", TH), Paragraph("Root", TH)],
[Paragraph("Superficial", TCB), Paragraph("Extensor carpi radialis longus (ECRL)", TC), Paragraph("Wrist extension + radial deviation", TC), Paragraph("Radial (before bifurcation)", TC), Paragraph("C6, C7", TC)],
[Paragraph("Superficial", TCB), Paragraph("Extensor carpi radialis brevis (ECRB)", TC), Paragraph("Wrist extension + radial deviation", TC), Paragraph("PIN (radial)", TC), Paragraph("C7, C8", TC)],
[Paragraph("Superficial", TCB), Paragraph("Extensor digitorum communis (EDC)", TC), Paragraph("Extend MCP joints 2–5 via extensor expansion", TC), Paragraph("PIN (radial)", TC), Paragraph("C7, C8", TC)],
[Paragraph("Superficial", TCB), Paragraph("Extensor digiti minimi", TC), Paragraph("Extend little finger", TC), Paragraph("PIN (radial)", TC), Paragraph("C7, C8", TC)],
[Paragraph("Superficial", TCB), Paragraph("Extensor carpi ulnaris (ECU)", TC), Paragraph("Wrist extension + ulnar deviation", TC), Paragraph("PIN (radial)", TC), Paragraph("C7, C8", TC)],
[Paragraph("Deep", TCB), Paragraph("Supinator", TC), Paragraph("Supination (with elbow extended)", TC), Paragraph("PIN (radial)", TC), Paragraph("C6, C7", TC)],
[Paragraph("Deep", TCB), Paragraph("Abductor pollicis longus (APL)", TC), Paragraph("Abduct + extend thumb (CMC joint)", TC), Paragraph("PIN (radial)", TC), Paragraph("C7, C8", TC)],
[Paragraph("Deep", TCB), Paragraph("Extensor pollicis brevis (EPB)", TC), Paragraph("Extend thumb MCP joint", TC), Paragraph("PIN (radial)", TC), Paragraph("C7, C8", TC)],
[Paragraph("Deep", TCB), Paragraph("Extensor pollicis longus (EPL)", TC), Paragraph("Extend thumb IP joint; ulnar deviation of wrist", TC), Paragraph("PIN (radial)", TC), Paragraph("C7, C8", TC)],
[Paragraph("Deep", TCB), Paragraph("Extensor indicis", TC), Paragraph("Extend index finger independently", TC), Paragraph("PIN (radial)", TC), Paragraph("C7, C8", TC)],
]
story += [std_table(fa_post_rows, [2.5*cm,4*cm,5*cm,3.5*cm,3*cm]), Spacer(1,0.2*cm)]
story += [pearl_box("'Anatomical snuffbox' boundaries: EPL (ulnar), EPB + APL (radial), scaphoid floor. Contents: radial artery, superficial radial nerve. Tenderness = scaphoid fracture until proven otherwise."), Spacer(1,0.4*cm)]
# -- Hand --
story += [PageBreak(), section_bar("2e. HAND MUSCLES"), Spacer(1,0.15*cm)]
story.append(Paragraph("Thenar Muscles (Median nerve, C8–T1)", H2))
thenar_rows = [
[Paragraph("Muscle", TH), Paragraph("Action", TH), Paragraph("Nerve", TH), Paragraph("Root", TH)],
[Paragraph("Abductor pollicis brevis (APB)", TCB), Paragraph("Abducts thumb (away from palm plane)", TC), Paragraph("Median (recurrent branch)", TC), Paragraph("C8, T1", TC)],
[Paragraph("Flexor pollicis brevis (FPB)", TCB), Paragraph("Flexes thumb MCP; superficial head = median, deep head = ulnar", TC), Paragraph("Median (mainly)", TC), Paragraph("C8, T1", TC)],
[Paragraph("Opponens pollicis (OP)", TCB), Paragraph("Opposition of thumb (rotation + flexion at CMC)", TC), Paragraph("Median (recurrent branch)", TC), Paragraph("C8, T1", TC)],
[Paragraph("Adductor pollicis", TCB), Paragraph("Adducts thumb across palm (Froment's sign)", TC), Paragraph("Ulnar (deep branch)", TC), Paragraph("C8, T1", TC)],
]
story += [std_table(thenar_rows, [4.5*cm,6.5*cm,4*cm,3*cm]), Spacer(1,0.15*cm)]
story.append(Paragraph("Hypothenar Muscles (Ulnar nerve, C8–T1)", H2))
hypo_rows = [
[Paragraph("Muscle", TH), Paragraph("Action", TH), Paragraph("Nerve", TH)],
[Paragraph("Abductor digiti minimi (ADM)", TCB), Paragraph("Abducts little finger", TC), Paragraph("Ulnar (deep branch)", TC)],
[Paragraph("Flexor digiti minimi (FDM)", TCB), Paragraph("Flexes little finger MCP", TC), Paragraph("Ulnar (deep branch)", TC)],
[Paragraph("Opponens digiti minimi (ODM)", TCB), Paragraph("Opposes little finger toward thumb", TC), Paragraph("Ulnar (deep branch)", TC)],
[Paragraph("Palmaris brevis", TCB), Paragraph("Wrinkles palm skin (protects ulnar nerve in Guyon's canal)", TC), Paragraph("Ulnar (superficial branch)", TC)],
]
story += [std_table(hypo_rows, [4.5*cm,8.5*cm,5*cm]), Spacer(1,0.15*cm)]
story.append(Paragraph("Intrinsic Muscles — Lumbricals & Interossei", H2))
int_rows = [
[Paragraph("Muscle", TH), Paragraph("Number", TH), Paragraph("Action", TH), Paragraph("Nerve", TH), Paragraph("Root", TH)],
[Paragraph("Lumbricals 1–2", TCB), Paragraph("2", TC), Paragraph("Flex MCP, extend IP joints (digits 2–3)", TC), Paragraph("Median", TC), Paragraph("C8, T1", TC)],
[Paragraph("Lumbricals 3–4", TCB), Paragraph("2", TC), Paragraph("Flex MCP, extend IP joints (digits 4–5)", TC), Paragraph("Ulnar (deep)", TC), Paragraph("C8, T1", TC)],
[Paragraph("Palmar interossei", TCB), Paragraph("3 (PAD)", TC), Paragraph("ADduct fingers toward middle finger (PAD)", TC), Paragraph("Ulnar (deep)", TC), Paragraph("C8, T1", TC)],
[Paragraph("Dorsal interossei", TCB), Paragraph("4 (DAB)", TC), Paragraph("ABduct fingers from middle finger (DAB)", TC), Paragraph("Ulnar (deep)", TC), Paragraph("C8, T1", TC)],
]
story += [std_table(int_rows, [3.5*cm,2*cm,5.5*cm,3.5*cm,3.5*cm]), Spacer(1,0.2*cm)]
story += [pearl_box("PAD-DAB: Palmar interossei ADduct (PAD), Dorsal interossei ABduct (DAB). Both innervated by ulnar nerve deep branch. Lumbrical action = 'intrinsic plus position' = MCP flex + IP extend = writing position."), Spacer(1,0.4*cm)]
# ══ 3. DERMATOMES ═════════════════════════════════════════════════════════
story += [PageBreak(), section_bar("3. DERMATOMES & CUTANEOUS NERVE DISTRIBUTION"), Spacer(1,0.15*cm)]
derm_rows = [
[Paragraph("Root", TH), Paragraph("Surface Area", TH), Paragraph("Test Point", TH), Paragraph("Peripheral Nerve", TH)],
[Paragraph("C3", TCB), Paragraph("Posterior neck, upper chest", TC), Paragraph("Over clavicle / posterior neck", TC), Paragraph("Supraclavicular nerves", TC)],
[Paragraph("<b>C4</b>", TCB), Paragraph("Over clavicle, upper shoulder", TC), Paragraph("Tip of shoulder", TC), Paragraph("Supraclavicular nerves", TC)],
[Paragraph("<b>C5</b>", TCB), Paragraph("Lateral arm (deltoid badge area)", TC), Paragraph("Lateral deltoid / lateral arm", TC), Paragraph("Axillary nerve (upper lateral cutaneous of arm)", TC)],
[Paragraph("<b>C6</b>", TCB), Paragraph("Lateral forearm, thumb + index finger", TC), Paragraph("Thumb pulp / dorsal 1st web space", TC), Paragraph("Lateral cutaneous nerve of forearm (musculocutaneous); superficial radial nerve", TC)],
[Paragraph("<b>C7</b>", TCB), Paragraph("Middle finger, dorsal forearm", TC), Paragraph("Middle finger pulp / dorsal middle finger", TC), Paragraph("Superficial radial nerve; median nerve", TC)],
[Paragraph("<b>C8</b>", TCB), Paragraph("Ring + little finger, medial forearm", TC), Paragraph("Little finger pulp", TC), Paragraph("Ulnar nerve; medial cutaneous nerve of forearm", TC)],
[Paragraph("<b>T1</b>", TCB), Paragraph("Medial forearm / medial arm near elbow", TC), Paragraph("Medial elbow / medial forearm", TC), Paragraph("Medial cutaneous nerve of forearm", TC)],
[Paragraph("T2", TCB), Paragraph("Axilla and medial upper arm", TC), Paragraph("Medial upper arm", TC), Paragraph("Intercostobrachial + medial cutaneous of arm", TC)],
]
story += [std_table(derm_rows, [1.5*cm,4*cm,4*cm,8.5*cm]), Spacer(1,0.2*cm)]
story.append(Paragraph("Named Peripheral Nerve Sensory Areas", H2))
cut_rows = [
[Paragraph("Nerve", TH), Paragraph("Sensory Territory", TH), Paragraph("Autonomous Zone (lost with isolated injury)", TH)],
[Paragraph("Axillary", TCB), Paragraph("Lateral arm ('regimental badge' area)", TC), Paragraph("Lateral deltoid region", TC)],
[Paragraph("Musculocutaneous → Lateral cutaneous of forearm", TCB), Paragraph("Lateral forearm (anterior + posterior surfaces)", TC), Paragraph("Lateral forearm near elbow crease", TC)],
[Paragraph("Medial cutaneous of arm", TCB), Paragraph("Medial arm skin", TC), Paragraph("Distal medial arm", TC)],
[Paragraph("Medial cutaneous of forearm", TCB), Paragraph("Medial forearm (ant + post surfaces)", TC), Paragraph("Medial forearm", TC)],
[Paragraph("Radial (superficial branch)", TCB), Paragraph("Dorsal lateral hand, dorsum thumb, dorsum digits 1–3½ (to PIP)", TC), Paragraph("Dorsal 1st web space / dorsum of thumb", TC)],
[Paragraph("Median", TCB), Paragraph("Palmar: thumb + 2½ fingers (radial side). Dorsal: fingertips of same digits", TC), Paragraph("Tip of index finger", TC)],
[Paragraph("Ulnar", TCB), Paragraph("Palmar: medial 1½ fingers. Dorsal: medial 1½ fingers + dorsal ulnar hand", TC), Paragraph("Tip of little finger", TC)],
[Paragraph("Intercostobrachial (T2)", TCB), Paragraph("Axilla, medial upper arm", TC), Paragraph("Medial axilla wall", TC)],
]
story += [std_table(cut_rows, [4*cm,7*cm,7*cm]), Spacer(1,0.3*cm)]
story += [pearl_box("Autonomous zone vs territory: the autonomous zone is the only area supplied by that nerve alone (no overlap). Test here for isolated nerve injuries. For median = index tip. For ulnar = little finger tip. For radial = dorsal 1st web space."), Spacer(1,0.4*cm)]
# ══ 4. TENDON REFLEXES ════════════════════════════════════════════════════
story += [section_bar("4. UPPER LIMB TENDON REFLEXES"), Spacer(1,0.15*cm)]
ref_rows = [
[Paragraph("Reflex", TH), Paragraph("Root", TH), Paragraph("Nerve", TH), Paragraph("Technique", TH), Paragraph("Response", TH)],
[Paragraph("<b>Biceps</b>", TCB), Paragraph("C5, C6", TC), Paragraph("Musculocutaneous", TC), Paragraph("Thumb over biceps tendon in cubital fossa, tap thumb", TC), Paragraph("Elbow flexion + supination", TC)],
[Paragraph("<b>Brachioradialis (Supinator)</b>", TCB), Paragraph("C5, C6", TC), Paragraph("Radial", TC), Paragraph("Tap styloid process / brachioradialis tendon, forearm in neutral", TC), Paragraph("Elbow flexion + slight supination", TC)],
[Paragraph("<b>Triceps</b>", TCB), Paragraph("C6, C7", TC), Paragraph("Radial", TC), Paragraph("Elbow at 90°, tap triceps tendon above olecranon", TC), Paragraph("Elbow extension", TC)],
[Paragraph("Finger flexion (Hoffmann's)", TCB), Paragraph("C8", TC), Paragraph("Median/Ulnar", TC), Paragraph("Flick middle fingernail — watch thumb flex (UMN sign)", TC), Paragraph("Thumb IP flexion = positive (UMN lesion)", TC)],
[Paragraph("Jaw jerk (for comparison)", TCB), Paragraph("CN V (pons)", TC), Paragraph("Trigeminal", TC), Paragraph("Tap jaw — brisk = UMN lesion above pons", TC), Paragraph("Mandible elevates", TC)],
]
story += [std_table(ref_rows, [3.5*cm,2*cm,3.5*cm,5*cm,4*cm]), Spacer(1,0.2*cm)]
inv_box = Table([[Paragraph("<b>INVERSION OF REFLEXES:</b>", TCB),
Paragraph("Tapping brachioradialis (C5/C6) produces finger flexion instead of elbow flexion = absent C5/C6 reflex + brisk C7/C8 finger flexors. Indicates C5/C6 lesion with intact C7/C8 below. Classic in cervical spondylotic myelopathy.", BODY)]],
colWidths=[4*cm,14*cm])
inv_box.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),colors.HexColor("#FFF0F5")),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),6),("VALIGN",(0,0),(-1,-1),"TOP"),
("LINEABOVE",(0,0),(-1,0),1,colors.HexColor("#C0392B")),
("LINEBELOW",(0,0),(-1,-1),1,colors.HexColor("#C0392B"))]))
story += [inv_box, Spacer(1,0.4*cm)]
# ══ 5. NERVE INJURIES ═════════════════════════════════════════════════════
story += [PageBreak(), section_bar("5. MAJOR NERVE INJURIES — PATTERNS & CLINICAL FEATURES"), Spacer(1,0.15*cm)]
nerves = [
{
"name": "RADIAL NERVE (C5–T1, Posterior Cord)",
"bg": PURPLE_LT,
"course": "Axilla → spiral groove of humerus (posterior) → lateral epicondyle → bifurcates into superficial radial nerve (sensory) + posterior interosseous nerve (PIN, deep motor branch)",
"levels": [
("Axilla (above spiral groove)", "All muscles lost: triceps, brachioradialis, ECRL, + all PIN. Wrist drop + finger drop + triceps paralysis + elbow weakness", "Saturday night palsy (crutch, or falling asleep with arm over chair back)"),
("Spiral groove (mid-humerus)", "Triceps SPARED (branch given off above groove). Wrist drop + finger drop. Brachioradialis weak. Sensory: dorsal hand/thumb", "Humeral shaft fracture (Holstein-Lewis), 'Saturday night palsy'"),
("Posterior interosseous nerve (PIN) at radial tunnel", "Wrist drop (ECRL spared = wrist deviated radially). Finger extension lost. NO sensory loss (PIN is purely motor)", "Radial tunnel syndrome, supinator arcade compression, Monteggia fracture-dislocation"),
("Superficial radial nerve (wrist)", "Purely sensory loss over dorsal lateral hand / 1st web space. No motor loss", "Handcuff neuropathy (Cheiralgia paraesthetica), tight wristwatch"),
],
"sign": "Wrist drop (inability to extend wrist against gravity). Finger drop (cannot extend MCPs). Brachioradialis: test in neutral forearm position = prominent tendon in radial nerve lesions above this point.",
"pearl": "Radial nerve wrist drop: interphalangeal extension is PRESERVED (interossei via ulnar nerve extend IPs through extensor expansion). Do not confuse IP extension for recovery!"
},
{
"name": "MEDIAN NERVE (C5–T1, Lateral + Medial Cord)",
"bg": BLUE_LT,
"course": "Axilla → anteromedial arm (no arm branches) → cubital fossa → between heads of pronator teres → AIN branch → carpal tunnel → thenar muscles + lumbricals 1–2",
"levels": [
("High lesion (elbow/above)", "All median muscles: pronators, FCR, FDS, FDP 1–2, FPL lost. Benediction hand on attempted grip (MCP flex preserved by interossei). Loss of thumb opposition. Sensory: radial 3½ fingers", "Supracondylar fracture (children), elbow dislocation, penetrating wounds"),
("Anterior interosseous nerve (AIN)", "FDP 1–2 + FPL + pronator quadratus paralysed. NO sensory loss. Cannot make 'OK sign' (tip pinch): distal phalanx of thumb + index cannot flex", "AIN syndrome (Kiloh-Nevin): spontaneous or after forearm injury"),
("Carpal tunnel (wrist)", "Thenar wasting (APB, OP, FPB superficial head). Loss of thumb opposition and abduction. Sensory: radial 3½ fingers palmar side (dorsal fingertip sensation also lost). Palmar branch preserved (branches before carpal tunnel) = thenar skin sensation intact", "Carpal tunnel syndrome — most common entrapment neuropathy"),
],
"sign": "Ape hand (thenar wasting, thumb adducted in same plane as palm). Carpal tunnel: Phalen's test (wrist flexion >60s = paresthesiae), Tinel's sign (percussion over carpal tunnel), Durkan's test (direct compression).",
"pearl": "Pope's blessing / Benediction hand = high median nerve injury (grip attempt). Ape hand = chronic thenar wasting (carpal tunnel). OK sign inability = AIN lesion. Palmar branch of median nerve (sensation over thenar skin) given off BEFORE carpal tunnel — so preserved in carpal tunnel syndrome."
},
{
"name": "ULNAR NERVE (C8–T1, Medial Cord)",
"bg": GREEN_LT,
"course": "Medial cord → medial arm (no branches) → posterior to medial epicondyle in cubital tunnel → between FCU heads → Guyon's canal at wrist → superficial (sensory) + deep (motor) branches",
"levels": [
("High lesion (above elbow)", "FCU + FDP 4–5 paralysed + all intrinsics (interossei, hypothenar, adductor pollicis, FPB deep head, lumbricals 3–4). Paradoxically LESS claw deformity than low lesion (FDP also weak, so less unopposed IP flexion)", "Medial epicondyle fracture, cubital tunnel syndrome, elbow dislocation"),
("Cubital tunnel (elbow)", "Most common entrapment site. FCU + FDP 4–5 + all intrinsics. Classic ulnar neuropathy at elbow (UNE)", "Cubital tunnel syndrome — 2nd most common entrapment after carpal tunnel"),
("Guyon's canal (wrist)", "All intrinsics but FCU + FDP 4–5 are SPARED (supplied above wrist). Claw deformity digits 4–5 more marked (FDP intact, flexes IPs strongly). Sensory: depends on level within canal", "Handlebar palsy (cyclists), hook of hamate fracture, ganglion cyst"),
],
"sign": "Claw hand (digits 4–5): hyperextension at MCP (lost lumbrical), flexion at IP (FDP intact). Froment's sign: ask patient to hold paper between thumb + index → thumb IP flexes to compensate for weak adductor pollicis (uses FPL via median nerve instead). Wartenberg's sign: little finger abduction at rest (no PAD to counteract EDM).",
"pearl": "Ulnar paradox: HIGH lesions produce LESS clawing than LOW lesions because FDP 4–5 are also paralysed, so less flexor force at IPs. The lower the lesion, the more dramatic the claw."
},
{
"name": "MUSCULOCUTANEOUS NERVE (C5–C7, Lateral Cord)",
"bg": AMBER_LT,
"course": "Lateral cord → pierces coracobrachialis → between biceps + brachialis → exits as lateral cutaneous nerve of forearm",
"levels": [
("Any level", "Weak elbow flexion (biceps + brachialis). Weak supination (biceps). Absent biceps reflex. Sensory: lateral forearm", "Isolated injury rare — usually part of brachial plexus injury, shoulder dislocation, or vigorous exercise (spontaneous)"),
],
"sign": "Elbow flexion weakest in supinated position (biceps). Brachialis still partially acts (partly innervated by radial). Lateral forearm sensory loss.",
"pearl": "The musculocutaneous nerve pierces coracobrachialis — a contracted or hypertrophied coracobrachialis (e.g. bodybuilders) can compress it."
},
{
"name": "AXILLARY NERVE (C5–C6, Posterior Cord)",
"bg": colors.HexColor("#FFF0F0"),
"course": "Posterior cord → exits axilla through quadrilateral space with posterior circumflex humeral artery → anterior branch (deltoid) + posterior branch (teres minor + lateral arm skin)",
"levels": [
("Surgical neck of humerus / quadrilateral space", "Deltoid paralysis: arm abduction lost (note: supraspinatus initiates first 15°). Teres minor weakness (external rotation). Sensory loss: regimental badge area (lateral deltoid skin)", "Anterior shoulder dislocation (most common), surgical neck humeral fracture, quadrilateral space syndrome"),
],
"sign": "Cannot abduct arm beyond 15° (deltoid paralysed). Shoulder appears flat/squared off in deltoid wasting. Regimental badge area numbness.",
"pearl": "Deltoid wasting after shoulder dislocation = axillary nerve injury until proven otherwise. Abduction 0–15° (supraspinatus, via suprascapular nerve) preserved. Abduction 15–90° (deltoid) lost."
},
]
for nerve in nerves:
hdr = Table([[Paragraph(nerve["name"], S("NH",fontSize=11,fontName="Helvetica-Bold",textColor=WHITE,leading=15))]],
colWidths=[18*cm])
hdr.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),NAVY),
("TOPPADDING",(0,0),(-1,-1),5),("BOTTOMPADDING",(0,0),(-1,-1),5),
("LEFTPADDING",(0,0),(-1,-1),8),("ROUNDEDCORNERS",[3])]))
story += [hdr, Spacer(1,0.1*cm)]
course_t = Table([[Paragraph("<b>Course:</b>", TCB), Paragraph(nerve["course"], TC)]],
colWidths=[1.8*cm,16.2*cm])
course_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),LT_GREY),
("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),6),("VALIGN",(0,0),(-1,-1),"TOP")]))
story += [course_t, Spacer(1,0.1*cm)]
lvl_rows = [[Paragraph("Level of Injury", TH), Paragraph("Motor / Sensory Loss", TH), Paragraph("Common Causes", TH)]]
for lvl, deficit, cause in nerve["levels"]:
lvl_rows.append([Paragraph(f"<b>{lvl}</b>", TCB), Paragraph(deficit, TC), Paragraph(cause, TC)])
lvl_t = Table(lvl_rows, colWidths=[4*cm,7.5*cm,6.5*cm])
lvl_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,0),TEAL),
("GRID",(0,0),(-1,-1),0.4,MID_GREY),
("ROWBACKGROUNDS",(0,1),(-1,-1),[nerve["bg"],WHITE]),
("VALIGN",(0,0),(-1,-1),"TOP"),
("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),5),("RIGHTPADDING",(0,0),(-1,-1),5)]))
story += [lvl_t, Spacer(1,0.1*cm)]
sign_t = Table([[Paragraph("<b>Key Clinical Signs:</b>", TCB), Paragraph(nerve["sign"], TC)]],
colWidths=[3.5*cm,14.5*cm])
sign_t.setStyle(TableStyle([("BACKGROUND",(0,0),(-1,-1),AMBER_LT),
("TOPPADDING",(0,0),(-1,-1),4),("BOTTOMPADDING",(0,0),(-1,-1),4),
("LEFTPADDING",(0,0),(-1,-1),6),("VALIGN",(0,0),(-1,-1),"TOP"),
("LINEABOVE",(0,0),(-1,0),0.5,GOLD),("LINEBELOW",(0,0),(-1,-1),0.5,GOLD)]))
story += [sign_t, Spacer(1,0.1*cm), pearl_box(nerve["pearl"]), Spacer(1,0.3*cm)]
# ══ 6. BRACHIAL PLEXUS INJURIES ═══════════════════════════════════════════
story += [PageBreak(), section_bar("6. BRACHIAL PLEXUS INJURY PATTERNS"), Spacer(1,0.15*cm)]
bpi_rows = [
[Paragraph("Injury", TH), Paragraph("Roots", TH), Paragraph("Motor Loss", TH), Paragraph("Sensory Loss", TH), Paragraph("Mechanism", TH)],
[Paragraph("<b>Erb's (upper) palsy</b>", TCB), Paragraph("C5, C6", TC),
Paragraph("Shoulder abduction, ER, elbow flexion, forearm supination lost → 'Waiter's tip' posture (arm: adducted + IR, elbow extended + pronated)", TC),
Paragraph("Lateral arm (C5), lateral forearm + thumb (C6)", TC),
Paragraph("Shoulder dystocia (obstetric), violent neck-shoulder separation, diving", TC)],
[Paragraph("<b>Middle trunk</b>", TCB), Paragraph("C7", TC),
Paragraph("Wrist + finger extension weak (rare isolated middle trunk injury)", TC),
Paragraph("Middle finger dorsum", TC), Paragraph("Usually combined with other levels", TC)],
[Paragraph("<b>Klumpke's (lower) palsy</b>", TCB), Paragraph("C8, T1", TC),
Paragraph("Intrinsic hand muscles, finger flexors (FDP 4–5, FPL). Claw hand. + Horner's syndrome (T1 sympathetics: ptosis, miosis, anhidrosis)", TC),
Paragraph("Medial forearm, little + ring fingers", TC),
Paragraph("Arm pulled upward (grabbing overhead during fall), cervical rib, Pancoast tumour", TC)],
[Paragraph("<b>Total plexus palsy</b>", TCB), Paragraph("C5–T1", TC),
Paragraph("Complete flail arm — no motor function throughout limb", TC),
Paragraph("Complete upper limb anaesthesia", TC),
Paragraph("High-velocity trauma (motorcycle accidents), avulsion injuries", TC)],
]
story += [std_table(bpi_rows, [3.5*cm,1.8*cm,4.7*cm,3.5*cm,4.5*cm]), Spacer(1,0.2*cm)]
story += [pearl_box("Horner's syndrome in brachial plexus injury = T1 root avulsion (preganglionic) — T1 carries sympathetic fibres to face. Avulsion (from cord, not nerve) = no recovery possible without nerve grafting; contrast with post-ganglionic rupture (better prognosis)."), Spacer(1,0.3*cm)]
# ══ 7. QUICK COMPARISON TABLE ═════════════════════════════════════════════
story += [section_bar("7. HAND DEFORMITY QUICK COMPARISON", bg=NAVY), Spacer(1,0.15*cm)]
deform_rows = [
[Paragraph("Deformity", TH), Paragraph("Nerve", TH), Paragraph("Level", TH), Paragraph("Appearance", TH), Paragraph("Mechanism", TH)],
[Paragraph("<b>Wrist drop</b>", TCB), Paragraph("Radial", TC), Paragraph("≥ Spiral groove", TC),
Paragraph("Wrist droops in flexion; fingers partially flexed", TC),
Paragraph("Loss of ECRLs + ECU + EDC (wrist + finger extensors)", TC)],
[Paragraph("<b>Claw hand (ring + little)</b>", TCB), Paragraph("Ulnar", TC), Paragraph("At or below elbow", TC),
Paragraph("MCP hyperextension + IP flexion digits 4–5", TC),
Paragraph("Lost lumbricals 3–4 (MCP flexion) + intact FDP (IP flexion)", TC)],
[Paragraph("<b>Ape hand</b>", TCB), Paragraph("Median", TC), Paragraph("Carpal tunnel / high", TC),
Paragraph("Thumb lies flat in plane of palm; thenar wasting", TC),
Paragraph("Lost thenar muscles (especially opponens pollicis + APB)", TC)],
[Paragraph("<b>Benediction hand</b>", TCB), Paragraph("Median", TC), Paragraph("High (above elbow)", TC),
Paragraph("On attempted grip: digits 1–3 cannot flex; digits 4–5 flex (ulnar FDP)", TC),
Paragraph("Lost FDS + FDP 1–2 + FPL; intrinsics intact in unopposed fingers", TC)],
[Paragraph("<b>Waiter's tip</b>", TCB), Paragraph("Upper plexus (Erb)", TC), Paragraph("C5, C6", TC),
Paragraph("Arm adducted, internally rotated, elbow extended + pronated", TC),
Paragraph("Lost shoulder abductors/ER, elbow flexors, supinators", TC)],
[Paragraph("<b>Claw hand (all digits)</b>", TCB), Paragraph("Ulnar + Median", TC), Paragraph("Combined low lesion", TC),
Paragraph("All 4 fingers clawed; thumb also ape hand", TC),
Paragraph("All lumbricals + all intrinsics lost", TC)],
]
story += [std_table(deform_rows, [3*cm,2.8*cm,3.2*cm,4.5*cm,4.5*cm]), Spacer(1,0.4*cm)]
doc.build(story, onFirstPage=hdr_footer, onLaterPages=hdr_footer)
print(f"Written: {OUTPUT}")
build()
cd /home/daytona/workspace/cn-study-guide && python upper_limb_guide.py 2>&1
ls -lh /home/daytona/workspace/cn-study-guide/Upper_Limb_Anatomy_Guide.pdf
Upper Limb Anatomy Guide
PDF Document · PDF