A complete EDIC Part II station on Type 1 Respiratory Failure progressing to Severe ARDS. approach it exactly as an EDIC examiner would. Topics include: Progressive CCS vignette discussion CBS interpretation Chest X-ray ABG analysis Ventilator graphics ARDS diagnosis Lung-protective ventilation PEEP optimisation Prone ventilation Common examiner traps Examiner commentary on every question. kindly make my CCS based on this real EDIC 2 exam . seperate Q and A sheet same as Real Edic , add Logo, same font , same style,, based on latest guideline of all including ESICM ARDS, Sepsis 2026 etc,,
ESICM ARDS definition guidelines 2023 2024 Berlin criteria update lung protective ventilation PEEP
cd /home/daytona/workspace/attachments/3f5c04ee-ee1b-4efe-87f8-974bd1164660/ && python3 -c " import pdfplumber print('=== CCS 1 Q.pdf ===') with pdfplumber.open('CCS 1 Q.pdf') as pdf: for i, page in enumerate(pdf.pages): print(f'--- PAGE {i+1} ---') print(page.extract_text()) print() "
cd /home/daytona/workspace/attachments/3f5c04ee-ee1b-4efe-87f8-974bd1164660/ && python3 -c " import pdfplumber print('=== CCS 1 Answer sheet.pdf ===') with pdfplumber.open('CCS 1 Answer sheet.pdf') as pdf: for i, page in enumerate(pdf.pages): print(f'--- PAGE {i+1} ---') print(page.extract_text()) print() "
ls /home/daytona/workspace/ && python3 -c "import reportlab; print('reportlab OK'); import PIL; print('PIL OK')" 2>&1
mkdir -p /home/daytona/workspace/edic-ards-ccs cat << 'PYEOF' > /home/daytona/workspace/edic-ards-ccs/build_question_sheet.py """ EDIC Part II - CCS Question Sheet Type 1 Respiratory Failure → Severe ARDS Exactly matching the style of the real EDIC Nov 2025 exam papers. """ from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.units import mm, cm from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable, KeepTogether, PageBreak) from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY from reportlab.platypus import Flowable from reportlab.pdfgen import canvas import os # ── Colour palette (matches ESICM / EDIC branding) ─────────────────────────── ESICM_BLUE = colors.HexColor('#003D73') # deep navy ESICM_TEAL = colors.HexColor('#007B9E') # header accent LIGHT_GREY = colors.HexColor('#F5F5F5') MID_GREY = colors.HexColor('#D0D0D0') DARK_GREY = colors.HexColor('#555555') TABLE_HDR = colors.HexColor('#D6E4F0') TABLE_ALT = colors.HexColor('#EEF5FB') WHITE = colors.white BLACK = colors.black PAGE_W, PAGE_H = A4 # 595.3 × 841.9 pts L_MARGIN = R_MARGIN = 18 * mm T_MARGIN = 22 * mm B_MARGIN = 22 * mm # ── Style helper ────────────────────────────────────────────────────────────── def make_styles(): styles = getSampleStyleSheet() base = ParagraphStyle('base', fontName='Helvetica', fontSize=9, leading=13, textColor=BLACK, spaceBefore=2, spaceAfter=2) title = ParagraphStyle('title', parent=base, fontName='Helvetica-Bold', fontSize=13, textColor=WHITE, alignment=TA_CENTER, spaceBefore=0, spaceAfter=0) subtitle = ParagraphStyle('subtitle', parent=base, fontName='Helvetica-Bold', fontSize=10, textColor=WHITE, alignment=TA_CENTER) section_hdr = ParagraphStyle('section_hdr', parent=base, fontName='Helvetica-Bold', fontSize=10, textColor=ESICM_BLUE, spaceBefore=6, spaceAfter=3) body = ParagraphStyle('body', parent=base, fontSize=9, leading=14, textColor=BLACK, spaceAfter=4) body_bold = ParagraphStyle('body_bold', parent=body, fontName='Helvetica-Bold') vignette_title = ParagraphStyle('vignette_title', parent=base, fontName='Helvetica-Bold', fontSize=10, textColor=WHITE, alignment=TA_LEFT) small = ParagraphStyle('small', parent=base, fontSize=8, textColor=DARK_GREY) footer = ParagraphStyle('footer', parent=base, fontSize=8, textColor=DARK_GREY, alignment=TA_CENTER) label = ParagraphStyle('label', parent=base, fontName='Helvetica-Bold', fontSize=9, textColor=ESICM_BLUE) table_hdr = ParagraphStyle('table_hdr', parent=base, fontName='Helvetica-Bold', fontSize=8, textColor=ESICM_BLUE, alignment=TA_CENTER) table_cell = ParagraphStyle('table_cell', parent=base, fontSize=8, alignment=TA_CENTER) table_cell_l = ParagraphStyle('table_cell_l', parent=base, fontSize=8, alignment=TA_LEFT) note = ParagraphStyle('note', parent=base, fontSize=8, textColor=DARK_GREY, leftIndent=6, borderPad=4) return dict(title=title, subtitle=subtitle, section_hdr=section_hdr, body=body, body_bold=body_bold, vignette_title=vignette_title, small=small, footer=footer, label=label, table_hdr=table_hdr, table_cell=table_cell, table_cell_l=table_cell_l, note=note) # ── Coloured banner (used for section titles / vignette headers) ────────────── class ColourBanner(Flowable): def __init__(self, text, bg=ESICM_BLUE, fg=WHITE, height=18, font='Helvetica-Bold', font_size=10, width=None, left_pad=6): super().__init__() self._text = text self._bg = bg self._fg = fg self._h = height self._font = font self._fs = font_size self._w = width # None = fill available width self._lp = left_pad def wrap(self, availW, availH): self.width = self._w or availW self.height = self._h return self.width, self.height def draw(self): c = self.canv c.setFillColor(self._bg) c.rect(0, 0, self.width, self.height, stroke=0, fill=1) c.setFillColor(self._fg) c.setFont(self._font, self._fs) c.drawString(self._lp, (self.height - self._fs) / 2, self._text) # ── Thin coloured rule ──────────────────────────────────────────────────────── def rule(colour=ESICM_TEAL, width=1.5): return HRFlowable(width='100%', thickness=width, color=colour, spaceAfter=4, spaceBefore=4) # ── Lab table builder ───────────────────────────────────────────────────────── def lab_table(headers, rows, st): col_count = len(headers) col_w = [(PAGE_W - L_MARGIN - R_MARGIN) / col_count] * col_count data = [[Paragraph(h, st['table_hdr']) for h in headers]] for i, row in enumerate(rows): styled = [Paragraph(str(c), st['table_cell']) if j != 0 else Paragraph(str(c), st['table_cell_l']) for j, c in enumerate(row)] data.append(styled) style = TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), TABLE_HDR), ('GRID', (0, 0), (-1, -1), 0.4, MID_GREY), ('ROWBACKGROUNDS', (0, 1), (-1, -1), [WHITE, TABLE_ALT]), ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), ('TOPPADDING', (0, 0), (-1, -1), 3), ('BOTTOMPADDING', (0, 0), (-1, -1), 3), ('LEFTPADDING', (0, 0), (0, -1), 4), ]) return Table(data, colWidths=col_w, style=style, repeatRows=1) # ── Header / Footer callbacks ───────────────────────────────────────────────── def make_page_template(title_text, page_num_text): """Returns an onFirstPage / onLaterPages callable pair.""" def _draw(canv, doc, is_first): canv.saveState() w = PAGE_W # ── top blue bar ── canv.setFillColor(ESICM_BLUE) canv.rect(0, PAGE_H - 38, w, 38, stroke=0, fill=1) # ESICM text logo (left) canv.setFillColor(WHITE) canv.setFont('Helvetica-Bold', 14) canv.drawString(L_MARGIN, PAGE_H - 22, 'ESICM') canv.setFont('Helvetica', 9) canv.drawString(L_MARGIN, PAGE_H - 32, 'European Society of Intensive Care Medicine') # title (centre) canv.setFont('Helvetica-Bold', 11) tw = canv.stringWidth(title_text, 'Helvetica-Bold', 11) canv.drawString((w - tw) / 2, PAGE_H - 20, title_text) # subtitle (centre) sub = 'EDIC Part II Examination' canv.setFont('Helvetica', 8) sw = canv.stringWidth(sub, 'Helvetica', 8) canv.drawString((w - sw) / 2, PAGE_H - 31, sub) # teal rule under header canv.setStrokeColor(ESICM_TEAL) canv.setLineWidth(2) canv.line(0, PAGE_H - 40, w, PAGE_H - 40) # ── footer ── canv.setStrokeColor(MID_GREY) canv.setLineWidth(0.5) canv.line(L_MARGIN, B_MARGIN - 4, w - R_MARGIN, B_MARGIN - 4) canv.setFillColor(DARK_GREY) canv.setFont('Helvetica', 7.5) canv.drawString(L_MARGIN, B_MARGIN - 14, 'EDIC Part II Exam • July 2026 • Strictly Confidential') pg = f'{doc.page}' pw = canv.stringWidth(pg, 'Helvetica', 7.5) canv.drawString(w - R_MARGIN - pw, B_MARGIN - 14, pg) canv.restoreState() first = lambda c, d: _draw(c, d, True) later = lambda c, d: _draw(c, d, False) return first, later # ═══════════════════════════════════════════════════════════════════════════════ # BUILD QUESTION SHEET # ═══════════════════════════════════════════════════════════════════════════════ def build_question_sheet(out_path): doc = SimpleDocTemplate( out_path, pagesize=A4, leftMargin=L_MARGIN, rightMargin=R_MARGIN, topMargin=T_MARGIN + 30, bottomMargin=B_MARGIN + 10, title='EDIC Part II – CCS 2 – Question Sheet', author='ESICM EDIC') st = make_styles() story = [] add = story.append # ── CASE TITLE ──────────────────────────────────────────────────────────── add(ColourBanner( 'Clinical Case Scenario 2 (A and B)', bg=ESICM_BLUE, height=22, font_size=12)) add(Spacer(1, 6)) # ── BACKGROUND CASE A ───────────────────────────────────────────────────── add(ColourBanner('Background Case A', bg=ESICM_TEAL, height=16, font_size=9)) add(Spacer(1, 4)) # Two-column layout: narrative left, labs right narrative_A = """ <b>A 58-year-old woman</b> (height 162 cm, weight 78 kg, IBW 57 kg) with a 30-pack-year smoking history, type 2 diabetes mellitus (on metformin), and known mild COPD (GOLD II, FEV₁ 68% predicted) presented to the Emergency Department with a 5-day history of fever, productive cough, and progressive dyspnoea.<br/><br/> On arrival she was <b>alert but distressed</b>, with a respiratory rate of 34/min, SpO₂ 84% on room air, temperature 39.2 °C, and heart rate 118 bpm.<br/><br/> She was commenced on high-flow nasal cannula (HFNC) at <b>FiO₂ 1.0, flow 60 L/min</b>. Despite 30 minutes of HFNC, SpO₂ remained at 90% and work of breathing did not improve. She was therefore <b>intubated and mechanically ventilated</b>.<br/><br/> <b>Initial ventilator settings (Volume-Controlled Ventilation):</b><br/> Tidal volume 480 mL, RR 18/min, PEEP 8 cmH₂O, FiO₂ 0.8, I:E 1:2.<br/><br/> <b>Peak airway pressure:</b> 38 cmH₂O <b>Plateau pressure:</b> 32 cmH₂O<br/> <b>Driving pressure:</b> 24 cmH₂O """ narrative_col = [Paragraph(narrative_A, st['body'])] # ABG table abg_data_A = [ ['Parameter', 'Value', 'Normal Range'], ['pH', '7.31', '7.35 – 7.45'], ['pCO₂', '48 mmHg / 6.4 kPa', '35 – 45 mmHg / 4.8 – 6.0 kPa'], ['pO₂', '58 mmHg / 7.7 kPa', '75 – 100 mmHg / 10 – 13.5 kPa'], ['HCO₃⁻', '24 mmol/L', '22 – 26 mmol/L'], ['Base Excess', '-1', '-2 / +2 mmol/L'], ['Lactate', '2.8 mmol/L', '0.5 – 2 mmol/L'], ['SaO₂', '89%', '95 – 99%'], ['FiO₂', '0.8', '—'], ['P/F Ratio', '72.5 mmHg', '> 300 mmHg'], ['SpO₂/FiO₂', '112', '> 315'], ] abg_table_A = Table( [[Paragraph(c, st['table_hdr'] if r == 0 else (st['table_cell_l'] if i == 0 else st['table_cell'])) for i, c in enumerate(row)] for r, row in enumerate(abg_data_A)], colWidths=[48*mm, 38*mm, 52*mm], style=TableStyle([ ('BACKGROUND', (0,0), (-1,0), TABLE_HDR), ('GRID', (0,0), (-1,-1), 0.4, MID_GREY), ('ROWBACKGROUNDS', (0,1), (-1,-1), [WHITE, TABLE_ALT]), ('VALIGN', (0,0), (-1,-1), 'MIDDLE'), ('TOPPADDING', (0,0), (-1,-1), 3), ('BOTTOMPADDING', (0,0), (-1,-1), 3), ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'), ('FONTSIZE', (0,0), (-1,-1), 8), ]) ) add(Paragraph('<b>Arterial Blood Gas (VCV, FiO₂ 0.8, PEEP 8 cmH₂O):</b>', st['label'])) add(Spacer(1, 2)) add(abg_table_A) add(Spacer(1, 5)) # Lab investigations add(Paragraph('<b>Laboratory Investigations:</b>', st['label'])) add(Spacer(1, 2)) lab_data_A = [ ['Parameter', 'Value', 'Normal Range', 'Parameter', 'Value', 'Normal Range'], ['Haemoglobin', '11.2 g/dL', '11–17 g/dL', 'Sodium', '138 mmol/L', '135–145 mmol/L'], ['WBC', '18.6 ×10⁹/L', '4–10 ×10⁹/L', 'Potassium', '3.9 mmol/L', '3.5–5 mmol/L'], ['Neutrophils', '15.2 ×10⁹/L', '2–5 ×10⁹/L', 'Chloride', '100 mmol/L', '95–110 mmol/L'], ['Platelets', '310 ×10⁹/L', '150–400 ×10⁹/L', 'Creatinine', '108 µmol/L', '70–105 µmol/L'], ['CRP', '312 mg/L', '< 5 mg/L', 'Urea', '9.1 mmol/L', '2.8–7.1 mmol/L'], ['Procalcitonin', '18.4 ng/mL', '< 0.5 ng/mL', 'Albumin', '22 g/L', '35–50 g/L'], ['INR', '1.42', '0.8–1.2', 'LDH', '540 IU/L', '85–230 IU/L'], ['Fibrinogen', '5.8 g/L', '1.5–4 g/L', 'BNP', '210 pg/mL', '< 100 pg/mL'], ] lw = (PAGE_W - L_MARGIN - R_MARGIN) cw = [lw*0.22, lw*0.12, lw*0.16, lw*0.22, lw*0.12, lw*0.16] lab_t_A = Table( [[Paragraph(str(c), st['table_hdr'] if r == 0 else (st['table_cell_l'] if i in (0,3) else st['table_cell'])) for i, c in enumerate(row)] for r, row in enumerate(lab_data_A)], colWidths=cw, style=TableStyle([ ('BACKGROUND', (0,0), (-1,0), TABLE_HDR), ('GRID', (0,0), (-1,-1), 0.4, MID_GREY), ('ROWBACKGROUNDS', (0,1), (-1,-1), [WHITE, TABLE_ALT]), ('VALIGN', (0,0), (-1,-1), 'MIDDLE'), ('TOPPADDING', (0,0), (-1,-1), 3), ('BOTTOMPADDING', (0,0), (-1,-1), 3), ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'), ('FONTSIZE', (0,0), (-1,-1), 8), ]) ) add(lab_t_A) add(Spacer(1, 5)) # Full narrative paragraph add(Paragraph(narrative_A, st['body'])) add(Spacer(1, 4)) # Chest X-ray description box cxr_box_data = [[ Paragraph('<b>Chest X-ray (PA view, post-intubation):</b><br/>' 'ET tube tip 4 cm above carina. <i>Bilateral diffuse alveolar and ' 'interstitial opacities, more confluent in the lower zones with ' 'air bronchograms bilaterally. No pneumothorax. Heart size ' 'upper-normal. No significant pleural effusion.</i>', st['body']) ]] cxr_box = Table(cxr_box_data, colWidths=[PAGE_W - L_MARGIN - R_MARGIN], style=TableStyle([ ('BOX', (0,0), (-1,-1), 1, ESICM_TEAL), ('BACKGROUND', (0,0), (-1,-1), TABLE_ALT), ('TOPPADDING', (0,0), (-1,-1), 6), ('BOTTOMPADDING', (0,0), (-1,-1), 6), ('LEFTPADDING', (0,0), (-1,-1), 8), ])) add(cxr_box) add(Spacer(1, 5)) # Ventilator graphics description vent_box_data = [[ Paragraph('<b>Ventilator Waveform Description (VCV mode):</b><br/>' 'Flow-time curve: square wave inspiratory flow; expiratory flow does not ' 'return to zero before next breath (auto-PEEP pattern). ' 'Pressure-time curve: rapid pressure rise to peak, with visible plateau. ' 'Volume-time curve: incomplete expiratory return. ' '<b>Measured auto-PEEP: 4 cmH₂O.</b>', st['body']) ]] vent_box = Table(vent_box_data, colWidths=[PAGE_W - L_MARGIN - R_MARGIN], style=TableStyle([ ('BOX', (0,0), (-1,-1), 1, ESICM_BLUE), ('BACKGROUND', (0,0), (-1,-1), TABLE_ALT), ('TOPPADDING', (0,0), (-1,-1), 6), ('BOTTOMPADDING', (0,0), (-1,-1), 6), ('LEFTPADDING', (0,0), (-1,-1), 8), ])) add(vent_box) add(Spacer(1, 4)) add(Paragraph('<i>EDIC Part II Exam • July 2026</i>', st['footer'])) add(PageBreak()) # ── VIGNETTE 1A ─────────────────────────────────────────────────────────── add(ColourBanner('Clinical Case Scenario 2 (A and B)', bg=ESICM_BLUE, height=22, font_size=12)) add(Spacer(1, 6)) add(ColourBanner('Vignette 1A', bg=ESICM_TEAL, height=16, font_size=9)) add(Spacer(1, 6)) v1a_text = """ The team re-evaluates the patient 2 hours after intubation.<br/><br/> Repeat ABG on <b>FiO₂ 1.0, PEEP 10 cmH₂O, RR 20/min, Vt 420 mL</b>: """ add(Paragraph(v1a_text, st['body'])) add(Spacer(1, 3)) abg_v1a = [ ['Parameter', 'Value', 'Normal Range'], ['pH', '7.26', '7.35 – 7.45'], ['pCO₂', '52 mmHg / 6.9 kPa', '35–45 mmHg / 4.8–6.0 kPa'], ['pO₂', '61 mmHg / 8.1 kPa', '75–100 mmHg / 10–13.5 kPa'], ['HCO₃⁻', '23 mmol/L', '22–26 mmol/L'], ['Base Excess', '-3', '-2 / +2 mmol/L'], ['Lactate', '3.2 mmol/L', '0.5–2 mmol/L'], ['FiO₂', '1.0', '—'], ['P/F Ratio', '61 mmHg', '> 300 mmHg'], ] abg_t_v1a = Table( [[Paragraph(str(c), st['table_hdr'] if r==0 else (st['table_cell_l'] if i==0 else st['table_cell'])) for i,c in enumerate(row)] for r,row in enumerate(abg_v1a)], colWidths=[55*mm, 42*mm, 60*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,0), TABLE_HDR), ('GRID',(0,0),(-1,-1), 0.4, MID_GREY), ('ROWBACKGROUNDS',(0,1),(-1,-1),[WHITE,TABLE_ALT]), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'), ('FONTSIZE',(0,0),(-1,-1),8), ]) ) add(abg_t_v1a) add(Spacer(1,5)) v1a_continued = """ <b>Echocardiography (bedside, focused):</b><br/> • No significant pericardial effusion<br/> • Left ventricular function preserved (LVEF ~55%), no wall motion abnormalities<br/> • Right ventricle mildly dilated with <b>septal flattening (D-sign)</b> in systole and diastole<br/> • IVC dilated (2.4 cm), <i>partial</i> collapse with inspiration<br/> • Bilateral B-lines (> 3 per zone in all 6 lung zones); no lung sliding in left lower zone<br/> • No significant pleural effusion bilaterally<br/><br/> <b>Haemodynamics:</b><br/> Heart rate 108 bpm (sinus tachycardia) | BP 98/62 mmHg (MAP 74 mmHg)<br/> Norepinephrine 0.22 µg/kg/min | CVP 16 mmHg<br/> Urine output 28 mL/h over the last 2 hours<br/><br/> <b>Ventilator data (VCV, Vt 420 mL / 7.4 mL/kg IBW):</b><br/> Peak pressure 42 cmH₂O | Plateau pressure 34 cmH₂O | PEEP 10 cmH₂O<br/> Driving pressure 24 cmH₂O | Crs 17.5 mL/cmH₂O | Auto-PEEP 5 cmH₂O (measured) """ add(Paragraph(v1a_continued, st['body'])) add(Spacer(1, 4)) add(Paragraph('<i>EDIC Part II Exam • July 2026</i>', st['footer'])) add(PageBreak()) # ── VIGNETTE 2A ─────────────────────────────────────────────────────────── add(ColourBanner('Clinical Case Scenario 2 (A and B)', bg=ESICM_BLUE, height=22, font_size=12)) add(Spacer(1, 6)) add(ColourBanner('Vignette 2A', bg=ESICM_TEAL, height=16, font_size=9)) add(Spacer(1, 6)) v2a_text = """ Following further management, the patient is now on <b>FiO₂ 0.7, PEEP 14 cmH₂O, VCV (Vt 350 mL / 6.1 mL/kg IBW), RR 22/min</b>. A recruitment manoeuvre was performed 1 hour ago.<br/><br/> She received a <b>cisatracurium infusion</b> for 48 hours (started at time of intubation).<br/><br/> The physiotherapist asks about positioning. The FiO₂ requirement has not improved below 0.65 despite PEEP titration using a decremental PEEP trial (optimal PEEP was identified as 14 cmH₂O based on best compliance).<br/><br/> <b>Current ABG (FiO₂ 0.7, PEEP 14 cmH₂O):</b> """ add(Paragraph(v2a_text, st['body'])) add(Spacer(1, 3)) abg_v2a = [ ['Parameter', 'Value', 'Normal Range'], ['pH', '7.33', '7.35–7.45'], ['pCO₂', '50 mmHg / 6.7 kPa', '35–45 mmHg / 4.8–6.0 kPa'], ['pO₂', '68 mmHg / 9.1 kPa', '75–100 mmHg / 10–13.5 kPa'], ['HCO₃⁻', '26 mmol/L', '22–26 mmol/L'], ['FiO₂', '0.7', '—'], ['P/F Ratio', '97 mmHg', '> 300 mmHg'], ['SpO₂/FiO₂', '136 (SpO₂ 95%)', '> 315'], ] abg_t_v2a = Table( [[Paragraph(str(c), st['table_hdr'] if r==0 else (st['table_cell_l'] if i==0 else st['table_cell'])) for i,c in enumerate(row)] for r,row in enumerate(abg_v2a)], colWidths=[55*mm, 42*mm, 60*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,0),TABLE_HDR), ('GRID',(0,0),(-1,-1),0.4,MID_GREY), ('ROWBACKGROUNDS',(0,1),(-1,-1),[WHITE,TABLE_ALT]), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'), ('FONTSIZE',(0,0),(-1,-1),8), ]) ) add(abg_t_v2a) add(Spacer(1, 5)) v2a_vent = """ <b>Ventilator data:</b><br/> Plateau pressure 28 cmH₂O | Driving pressure 14 cmH₂O | Crs 25 mL/cmH₂O<br/> Mechanical Power 22.4 J/min<br/><br/> <b>Haemodynamics:</b><br/> HR 96 bpm | BP 108/68 mmHg (MAP 81 mmHg)<br/> Norepinephrine 0.18 µg/kg/min | CVP 12 mmHg | Urine output 45 mL/h """ add(Paragraph(v2a_vent, st['body'])) add(Spacer(1, 4)) add(Paragraph('<i>EDIC Part II Exam • July 2026</i>', st['footer'])) add(PageBreak()) # ── 5 MINUTE BREAK SLIDE ───────────────────────────────────────────────── add(ColourBanner('Clinical Case Scenario 2 (A and B)', bg=ESICM_BLUE, height=22, font_size=12)) add(Spacer(1, 80)) wait_box = [[Paragraph( 'NOW YOU HAVE 5 MINUTES TO<br/>PREPARE THE BACKGROUND FOR<br/>CASE 2B', ParagraphStyle('wait', fontName='Helvetica-Bold', fontSize=14, textColor=ESICM_BLUE, alignment=TA_CENTER, leading=22))]] wt = Table(wait_box, colWidths=[PAGE_W - L_MARGIN - R_MARGIN], style=TableStyle([ ('BOX',(0,0),(-1,-1),2,ESICM_BLUE), ('BACKGROUND',(0,0),(-1,-1),TABLE_ALT), ('TOPPADDING',(0,0),(-1,-1),30), ('BOTTOMPADDING',(0,0),(-1,-1),30), ])) add(wt) add(Spacer(1, 4)) add(Paragraph('<i>EDIC Part II Exam • July 2026</i>', st['footer'])) add(PageBreak()) # ── BACKGROUND CASE B ───────────────────────────────────────────────────── add(ColourBanner('Clinical Case Scenario 2 (A and B)', bg=ESICM_BLUE, height=22, font_size=12)) add(Spacer(1, 6)) add(ColourBanner('Background Case B', bg=ESICM_TEAL, height=16, font_size=9)) add(Spacer(1, 4)) narrative_B = """ <b>A 44-year-old man</b> (height 175 cm, weight 92 kg, IBW 70 kg) with a background of alcohol excess and no other past medical history presents with a 3-day history of worsening dyspnoea and fever following a 10-day illness with productive cough. He has no regular medications.<br/><br/> On arrival to the ICU (directly from ED after failed NIV trial of 1 hour): <b>RR 36/min, SpO₂ 82% on FiO₂ 1.0 HFNC, HR 128 bpm, BP 88/55 mmHg (MAP 66 mmHg), Temperature 38.9°C.</b><br/><br/> He is intubated using RSI and placed on <b>Pressure-Controlled Ventilation (PCV)</b>:<br/> Driving pressure 18 cmH₂O above PEEP, PEEP 10 cmH₂O, FiO₂ 1.0, RR 22/min, I:E 1:1.5. """ add(Paragraph(narrative_B, st['body'])) add(Spacer(1, 4)) add(Paragraph('<b>Arterial Blood Gas (on HFNC FiO₂ 1.0, just before intubation):</b>', st['label'])) add(Spacer(1, 2)) abg_B_pre = [ ['Parameter', 'Value', 'Normal Range'], ['pH', '7.22', '7.35–7.45'], ['pCO₂', '54 mmHg / 7.2 kPa', '35–45 mmHg / 4.8–6.0 kPa'], ['pO₂', '50 mmHg / 6.7 kPa', '75–100 mmHg / 10–13.5 kPa'], ['HCO₃⁻', '22 mmol/L', '22–26 mmol/L'], ['Base Excess', '-5', '-2/+2 mmol/L'], ['Lactate', '4.1 mmol/L', '0.5–2 mmol/L'], ['FiO₂', '1.0 (HFNC)', '—'], ['P/F Ratio', '50 mmHg', '> 300 mmHg'], ] add(Table( [[Paragraph(str(c), st['table_hdr'] if r==0 else (st['table_cell_l'] if i==0 else st['table_cell'])) for i,c in enumerate(row)] for r,row in enumerate(abg_B_pre)], colWidths=[55*mm, 42*mm, 60*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,0),TABLE_HDR), ('GRID',(0,0),(-1,-1),0.4,MID_GREY), ('ROWBACKGROUNDS',(0,1),(-1,-1),[WHITE,TABLE_ALT]), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'), ('FONTSIZE',(0,0),(-1,-1),8), ]) )) add(Spacer(1, 5)) add(Paragraph('<b>Laboratory Investigations:</b>', st['label'])) add(Spacer(1, 2)) lab_B = [ ['Parameter', 'Value', 'Normal Range', 'Parameter', 'Value', 'Normal Range'], ['Haemoglobin', '9.8 g/dL', '11–17 g/dL', 'Sodium', '132 mmol/L', '135–145 mmol/L'], ['WBC', '22.4 ×10⁹/L', '4–10 ×10⁹/L', 'Potassium', '4.8 mmol/L', '3.5–5 mmol/L'], ['Neutrophils', '19.6 ×10⁹/L', '2–5 ×10⁹/L', 'Chloride', '94 mmol/L', '95–110 mmol/L'], ['Platelets', '88 ×10⁹/L', '150–400 ×10⁹/L', 'Creatinine', '198 µmol/L', '70–105 µmol/L'], ['CRP', '428 mg/L', '< 5 mg/L', 'Urea', '14.2 mmol/L', '2.8–7.1 mmol/L'], ['Procalcitonin', '32.1 ng/mL', '< 0.5 ng/mL', 'Albumin', '18 g/L', '35–50 g/L'], ['INR', '1.88', '0.8–1.2', 'ALT', '112 IU/L', '7–56 IU/L'], ['Fibrinogen', '6.4 g/L', '1.5–4 g/L', 'GGT', '310 IU/L', '< 50 IU/L'], ['D-Dimer', '4.8 µg/mL', '< 0.5 µg/mL', 'Glucose', '9.4 mmol/L', '3.9–5.6 mmol/L'], ] add(Table( [[Paragraph(str(c), st['table_hdr'] if r==0 else (st['table_cell_l'] if i in (0,3) else st['table_cell'])) for i,c in enumerate(row)] for r,row in enumerate(lab_B)], colWidths=cw, style=TableStyle([ ('BACKGROUND',(0,0),(-1,0),TABLE_HDR), ('GRID',(0,0),(-1,-1),0.4,MID_GREY), ('ROWBACKGROUNDS',(0,1),(-1,-1),[WHITE,TABLE_ALT]), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'), ('FONTSIZE',(0,0),(-1,-1),8), ]) )) add(Spacer(1, 4)) cxr_B = [[Paragraph( '<b>Chest X-ray (AP, post-intubation):</b><br/>' 'ET tube 3.5 cm above carina. <i>Bilateral extensive white-out of both lung fields. ' 'Air bronchograms bilaterally. No pneumothorax identified. ' 'Heart size cannot be accurately assessed. Minimal right-sided pleural effusion.</i>', st['body'])]] add(Table(cxr_B, colWidths=[PAGE_W-L_MARGIN-R_MARGIN], style=TableStyle([ ('BOX',(0,0),(-1,-1),1,ESICM_TEAL), ('BACKGROUND',(0,0),(-1,-1),TABLE_ALT), ('TOPPADDING',(0,0),(-1,-1),6), ('BOTTOMPADDING',(0,0),(-1,-1),6), ('LEFTPADDING',(0,0),(-1,-1),8), ]))) add(Spacer(1, 4)) add(Paragraph('<i>EDIC Part II Exam • July 2026</i>', st['footer'])) add(PageBreak()) # ── VIGNETTE 1B ─────────────────────────────────────────────────────────── add(ColourBanner('Clinical Case Scenario 2 (A and B)', bg=ESICM_BLUE, height=22, font_size=12)) add(Spacer(1, 6)) add(ColourBanner('Vignette 1B', bg=ESICM_TEAL, height=16, font_size=9)) add(Spacer(1, 6)) v1b = """ The patient is now 6 hours post-intubation. An arterial line and central venous catheter have been placed. Transpulmonary thermodilution monitoring (PiCCO) is inserted.<br/><br/> <b>Current PCV settings:</b> Driving pressure 20 cmH₂O, PEEP 14 cmH₂O, FiO₂ 0.9, RR 24/min<br/> <b>Measured Vt:</b> 430 mL (6.1 mL/kg IBW)<br/> <b>Plateau pressure:</b> 34 cmH₂O | <b>Driving pressure:</b> 20 cmH₂O | <b>Crs:</b> 21.5 mL/cmH₂O<br/><br/> <b>PiCCO data:</b><br/> Cardiac Index 4.8 L/min/m² | SVRI 1180 dyn·s·cm⁻⁵·m² (low) | GEDVI 820 mL/m² (normal) | EVLWI 18 mL/kg (elevated; normal < 10) | PVPI 3.8 (elevated; normal < 3)<br/><br/> <b>Haemodynamics:</b><br/> HR 118 bpm | MAP 68 mmHg | Norepinephrine 0.35 µg/kg/min | CVP 18 mmHg | Urine output 15 mL/h<br/><br/> <b>ABG (FiO₂ 0.9, PEEP 14 cmH₂O):</b> """ add(Paragraph(v1b, st['body'])) add(Spacer(1, 3)) abg_v1b = [ ['Parameter', 'Value', 'Normal Range'], ['pH', '7.19', '7.35–7.45'], ['pCO₂', '58 mmHg / 7.7 kPa', '35–45 mmHg / 4.8–6.0 kPa'], ['pO₂', '59 mmHg / 7.9 kPa', '75–100 mmHg / 10–13.5 kPa'], ['HCO₃⁻', '22 mmol/L', '22–26 mmol/L'], ['Lactate', '5.8 mmol/L', '0.5–2 mmol/L'], ['FiO₂', '0.9', '—'], ['P/F Ratio', '65.6 mmHg', '> 300 mmHg'], ['SvO₂ (central)', '58%', '65–75%'], ] add(Table( [[Paragraph(str(c), st['table_hdr'] if r==0 else (st['table_cell_l'] if i==0 else st['table_cell'])) for i,c in enumerate(row)] for r,row in enumerate(abg_v1b)], colWidths=[55*mm, 42*mm, 60*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,0),TABLE_HDR), ('GRID',(0,0),(-1,-1),0.4,MID_GREY), ('ROWBACKGROUNDS',(0,1),(-1,-1),[WHITE,TABLE_ALT]), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'), ('FONTSIZE',(0,0),(-1,-1),8), ]) )) add(Spacer(1, 4)) add(Paragraph('<i>EDIC Part II Exam • July 2026</i>', st['footer'])) add(PageBreak()) # ── VIGNETTE 2B ─────────────────────────────────────────────────────────── add(ColourBanner('Clinical Case Scenario 2 (A and B)', bg=ESICM_BLUE, height=22, font_size=12)) add(Spacer(1, 6)) add(ColourBanner('Vignette 2B', bg=ESICM_TEAL, height=16, font_size=9)) add(Spacer(1, 6)) v2b = """ After 16 hours of <b>prone positioning</b> the patient is returned to supine.<br/> Salvage therapy with <b>inhaled nitric oxide (iNO) 20 ppm</b> and a course of <b>methylprednisolone</b> (1 mg/kg/day) were started at hour 12 of prone positioning.<br/><br/> The haematology and microbiology results are now available:<br/> • <b>BAL culture:</b> <i>Streptococcus pneumoniae</i> — sensitive to beta-lactams; also <i>Pneumocystis jirovecii</i> PCR <b>negative</b><br/> • <b>Blood cultures ×2:</b> <i>Streptococcus pneumoniae</i> (bacteraemic pneumonia)<br/> • <b>Urine pneumococcal antigen:</b> positive<br/> • <b>SARS-CoV-2 PCR:</b> negative | <b>Influenza A/B:</b> negative<br/><br/> <b>New ABG (18 hours after prone, back in supine, FiO₂ 0.65, PEEP 12 cmH₂O):</b> """ add(Paragraph(v2b, st['body'])) add(Spacer(1, 3)) abg_v2b = [ ['Parameter', 'Value', 'Normal Range'], ['pH', '7.38', '7.35–7.45'], ['pCO₂', '44 mmHg / 5.9 kPa', '35–45 mmHg / 4.8–6.0 kPa'], ['pO₂', '78 mmHg / 10.4 kPa', '75–100 mmHg / 10–13.5 kPa'], ['HCO₃⁻', '25.5 mmol/L', '22–26 mmol/L'], ['Lactate', '1.8 mmol/L', '0.5–2 mmol/L'], ['FiO₂', '0.65', '—'], ['P/F Ratio', '120 mmHg', '> 300 mmHg'], ['SvO₂ (central)', '68%', '65–75%'], ] add(Table( [[Paragraph(str(c), st['table_hdr'] if r==0 else (st['table_cell_l'] if i==0 else st['table_cell'])) for i,c in enumerate(row)] for r,row in enumerate(abg_v2b)], colWidths=[55*mm, 42*mm, 60*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,0),TABLE_HDR), ('GRID',(0,0),(-1,-1),0.4,MID_GREY), ('ROWBACKGROUNDS',(0,1),(-1,-1),[WHITE,TABLE_ALT]), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ('FONTNAME',(0,0),(-1,0),'Helvetica-Bold'), ('FONTSIZE',(0,0),(-1,-1),8), ]) )) add(Spacer(1,5)) v2b_extra = """ <b>Ventilator data:</b><br/> Plateau pressure 26 cmH₂O | Driving pressure 14 cmH₂O | Crs 28.6 mL/cmH₂O<br/><br/> <b>Haemodynamics:</b><br/> HR 88 bpm | MAP 76 mmHg | Norepinephrine 0.12 µg/kg/min | EVLWI 14 mL/kg | Urine output 55 mL/h<br/><br/> <b>Note:</b> The ECMO team has been consulted and is available. """ add(Paragraph(v2b_extra, st['body'])) add(Spacer(1, 4)) add(Paragraph('<i>EDIC Part II Exam • July 2026</i>', st['footer'])) # Build PDF first, later = make_page_template( 'Clinical Case Scenario 2 — Type 1 Respiratory Failure / Severe ARDS', 'Q') doc.build(story, onFirstPage=first, onLaterPages=later) print(f'Question sheet written: {out_path}') if __name__ == '__main__': out = '/home/daytona/workspace/edic-ards-ccs/CCS2_Question_Sheet.pdf' build_question_sheet(out) PYEOF python3 /home/daytona/workspace/edic-ards-ccs/build_question_sheet.py
cat << 'PYEOF' > /home/daytona/workspace/edic-ards-ccs/build_answer_sheet.py """ EDIC Part II – CCS 2 – EXAMINER ANSWER SHEET Type 1 Respiratory Failure → Severe ARDS Matching exact EDIC Nov 2025 format with full examiner commentary. """ from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.units import mm from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable, KeepTogether, PageBreak) from reportlab.lib.styles import ParagraphStyle from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT from reportlab.platypus import Flowable ESICM_BLUE = colors.HexColor('#003D73') ESICM_TEAL = colors.HexColor('#007B9E') LIGHT_GREY = colors.HexColor('#F5F5F5') MID_GREY = colors.HexColor('#D0D0D0') DARK_GREY = colors.HexColor('#555555') TABLE_HDR = colors.HexColor('#D6E4F0') TABLE_ALT = colors.HexColor('#EEF5FB') GOLD = colors.HexColor('#B8860B') GREEN_LIGHT = colors.HexColor('#E8F5E9') GREEN_DARK = colors.HexColor('#2E7D32') RED_LIGHT = colors.HexColor('#FFEBEE') RED_DARK = colors.HexColor('#C62828') AMBER_LIGHT = colors.HexColor('#FFF8E1') AMBER_DARK = colors.HexColor('#F57F17') WHITE = colors.white BLACK = colors.black PAGE_W, PAGE_H = A4 L_MARGIN = R_MARGIN = 18 * mm T_MARGIN = 22 * mm B_MARGIN = 22 * mm def S(name, **kw): defaults = dict(fontName='Helvetica', fontSize=9, leading=13, textColor=BLACK, spaceBefore=2, spaceAfter=2) defaults.update(kw) return ParagraphStyle(name, **defaults) st = { 'body': S('body', leading=13, spaceAfter=3), 'bold': S('bold', fontName='Helvetica-Bold'), 'label': S('label', fontName='Helvetica-Bold', fontSize=9, textColor=ESICM_BLUE), 'task_hdr': S('task_hdr', fontName='Helvetica-Bold', fontSize=10, textColor=WHITE, spaceBefore=0, spaceAfter=0), 'small': S('small', fontSize=8, textColor=DARK_GREY), 'footer': S('footer', fontSize=8, textColor=DARK_GREY, alignment=TA_CENTER), 'comment': S('comment', fontSize=8.5, textColor=DARK_GREY, leading=12, leftIndent=6, spaceBefore=3, spaceAfter=2), 'trap': S('trap', fontSize=8.5, textColor=RED_DARK, leading=12, leftIndent=6, fontName='Helvetica-Oblique'), 'answer': S('answer', fontSize=9, textColor=GREEN_DARK, leading=13, leftIndent=6), 'pts': S('pts', fontName='Helvetica-Bold', fontSize=9, textColor=ESICM_BLUE, alignment=TA_RIGHT), 'hdr_label': S('hdr_label', fontName='Helvetica-Bold', fontSize=9, textColor=ESICM_BLUE, spaceBefore=6), } class ColourBanner(Flowable): def __init__(self, text, bg=ESICM_BLUE, fg=WHITE, height=18, font='Helvetica-Bold', font_size=10, left_pad=6): super().__init__() self._text, self._bg, self._fg = text, bg, fg self._h, self._font, self._fs, self._lp = height, font, font_size, left_pad def wrap(self, aw, ah): self.width, self.height = aw, self._h return self.width, self.height def draw(self): c = self.canv c.setFillColor(self._bg) c.rect(0, 0, self.width, self.height, stroke=0, fill=1) c.setFillColor(self._fg) c.setFont(self._font, self._fs) c.drawString(self._lp, (self.height - self._fs) / 2, self._text) def make_page_template(): def _draw(canv, doc): canv.saveState() w = PAGE_W canv.setFillColor(ESICM_BLUE) canv.rect(0, PAGE_H-38, w, 38, stroke=0, fill=1) canv.setFillColor(WHITE) canv.setFont('Helvetica-Bold', 14) canv.drawString(L_MARGIN, PAGE_H-22, 'ESICM') canv.setFont('Helvetica', 9) canv.drawString(L_MARGIN, PAGE_H-32, 'European Society of Intensive Care Medicine') title = 'Clinical Case Scenario 2 — EXAMINER ANSWER SHEET' canv.setFont('Helvetica-Bold', 10) tw = canv.stringWidth(title, 'Helvetica-Bold', 10) canv.drawString((w-tw)/2, PAGE_H-20, title) sub = 'EDIC Part II Examination • STRICTLY CONFIDENTIAL' canv.setFont('Helvetica', 8) sw = canv.stringWidth(sub, 'Helvetica', 8) canv.drawString((w-sw)/2, PAGE_H-31, sub) canv.setStrokeColor(GOLD) canv.setLineWidth(2) canv.line(0, PAGE_H-40, w, PAGE_H-40) canv.setStrokeColor(MID_GREY) canv.setLineWidth(0.5) canv.line(L_MARGIN, B_MARGIN-4, w-R_MARGIN, B_MARGIN-4) canv.setFillColor(DARK_GREY) canv.setFont('Helvetica', 7.5) canv.drawString(L_MARGIN, B_MARGIN-14, 'EDIC Part II • July 2026 • EXAMINER USE ONLY • Strictly Confidential') pg = f'{doc.page} / {doc.page}' pw = canv.stringWidth(str(doc.page), 'Helvetica', 7.5) canv.drawString(w-R_MARGIN-20, B_MARGIN-14, str(doc.page)) canv.restoreState() return _draw, _draw def candidate_header(): """Prüfling / Prüfer / Datum row like real sheet.""" data = [[ Paragraph('<b>Prüfling (Candidate):</b> _______________________', st['body']), Paragraph('<b>Prüfer (Examiner):</b> _______________________', st['body']), Paragraph('<b>Datum (Date):</b> _______________', st['body']), ]] t = Table(data, colWidths=[(PAGE_W-L_MARGIN-R_MARGIN)/3]*3, style=TableStyle([ ('BOX',(0,0),(-1,-1),0.5,MID_GREY), ('INNERGRID',(0,0),(-1,-1),0.5,MID_GREY), ('BACKGROUND',(0,0),(-1,-1),LIGHT_GREY), ('TOPPADDING',(0,0),(-1,-1),5), ('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),6), ])) return t def task_block(number, title, max_pts, answers, commentary, traps=None, guideline_ref=None, show_task_label=True): """Build a complete task block with scoring box, answers, commentary.""" story = [] avail_w = PAGE_W - L_MARGIN - R_MARGIN # Task header row: Task N label | question text | (max. N) hdr_data = [[ Paragraph(f'Task {number}', st['task_hdr']), Paragraph(title, st['task_hdr']), Paragraph(f'(max. {max_pts})', ParagraphStyle( 'ptshdr', fontName='Helvetica-Bold', fontSize=9, textColor=WHITE, alignment=TA_RIGHT)), ]] hdr_t = Table(hdr_data, colWidths=[22*mm, avail_w-22*mm-22*mm, 22*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1),ESICM_BLUE), ('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.append(hdr_t) # Answer rows with checkbox + score column ans_rows = [] for pts, text in answers: ans_rows.append([ Paragraph('☐', ParagraphStyle('cb', fontSize=11, textColor=ESICM_TEAL, alignment=TA_CENTER)), Paragraph(text, st['answer']), Paragraph(str(pts), ParagraphStyle('pt', fontName='Helvetica-Bold', fontSize=9, textColor=ESICM_BLUE, alignment=TA_CENTER)), ]) if ans_rows: ans_t = Table(ans_rows, colWidths=[8*mm, avail_w-8*mm-14*mm, 14*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1),GREEN_LIGHT), ('GRID',(0,0),(-1,-1),0.3,MID_GREY), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ('LEFTPADDING',(0,1),(-1,-1),4), ])) story.append(ans_t) # Total score box total_data = [[ Paragraph('TOTAL:', ParagraphStyle('tot', fontName='Helvetica-Bold', fontSize=9, textColor=ESICM_BLUE)), Paragraph(f'_____ / {max_pts}', ParagraphStyle('tot2', fontName='Helvetica-Bold', fontSize=9, textColor=ESICM_BLUE, alignment=TA_RIGHT)), ]] tot_t = Table(total_data, colWidths=[avail_w-30*mm, 30*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1),TABLE_ALT), ('BOX',(0,0),(-1,-1),0.5,ESICM_TEAL), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ('LEFTPADDING',(0,0),(-1,-1),5), ('RIGHTPADDING',(0,0),(-1,-1),5), ])) story.append(tot_t) # Examiner commentary if commentary: comm_data = [[ Paragraph('💬 <b>Examiner Commentary:</b>', st['label']), ], [ Paragraph(commentary, st['comment']), ]] comm_t = Table(comm_data, colWidths=[avail_w], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1),AMBER_LIGHT), ('BOX',(0,0),(-1,-1),0.5,AMBER_DARK), ('LEFTPADDING',(0,0),(-1,-1),6), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ])) story.append(comm_t) if traps: trap_data = [[ Paragraph('⚠ <b>Common Examiner Trap:</b>', ParagraphStyle( 'trap_hdr', fontName='Helvetica-Bold', fontSize=8.5, textColor=RED_DARK)), ], [ Paragraph(traps, st['trap']), ]] trap_t = Table(trap_data, colWidths=[avail_w], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1),RED_LIGHT), ('BOX',(0,0),(-1,-1),0.5,RED_DARK), ('LEFTPADDING',(0,0),(-1,-1),6), ('TOPPADDING',(0,0),(-1,-1),3), ('BOTTOMPADDING',(0,0),(-1,-1),3), ])) story.append(trap_t) if guideline_ref: ref_data = [[Paragraph(f'📋 <i>{guideline_ref}</i>', ParagraphStyle('ref', fontSize=7.5, textColor=ESICM_TEAL, fontName='Helvetica-Oblique'))]] story.append(Table(ref_data, colWidths=[avail_w], style=TableStyle([ ('LEFTPADDING',(0,0),(-1,-1),4), ('TOPPADDING',(0,0),(-1,-1),2), ('BOTTOMPADDING',(0,0),(-1,-1),2), ]))) story.append(Spacer(1, 6)) return KeepTogether(story) def build_answer_sheet(out_path): doc = SimpleDocTemplate( out_path, pagesize=A4, leftMargin=L_MARGIN, rightMargin=R_MARGIN, topMargin=T_MARGIN+30, bottomMargin=B_MARGIN+10, title='EDIC Part II – CCS 2 – Examiner Answer Sheet', author='ESICM EDIC') story = [] add = story.append # ── Master title + candidate header ────────────────────────────────────── add(ColourBanner('CCS 2 — Type 1 Respiratory Failure → Severe ARDS', bg=ESICM_BLUE, height=22, font_size=12)) add(Spacer(1, 5)) add(candidate_header()) add(Spacer(1, 6)) # ══ CASE A — TASKS 1–14 ═══════════════════════════════════════════════════ add(ColourBanner('CASE A — Background Vignette Tasks', bg=ESICM_TEAL, height=16, font_size=9)) add(Spacer(1, 4)) add(Paragraph( '<i>Give the candidate 5 minutes to read Background Case A. Do not ask questions ' 'until reading time has elapsed.</i>', st['small'])) add(Spacer(1, 5)) # Task 1 add(task_block( 1, 'What are the two main clinical problems to address immediately in this patient?', max_pts=2, answers=[ (1, 'Type 1 (hypoxaemic) respiratory failure / ARDS'), (1, 'Sepsis / septic shock'), ], commentary=( 'The candidate must identify BOTH components. This patient has a P/F ratio of 72.5 mmHg ' '(severe hypoxaemia) and haemodynamic compromise requiring vasopressors. A candidate who ' 'mentions only respiratory failure without recognising the concurrent septic shock misses ' 'half the picture. The ABG shows a mixed picture: respiratory acidosis (pH 7.31, pCO₂ 48) ' 'with mild metabolic component — typical of early decompensation before compensatory mechanisms. ' 'Accept "pneumonia with severe hypoxaemia" as equivalent to Type 1 RF for this task.' ), traps=( 'Do NOT accept "ARDS" as the primary answer at this stage — ARDS can only be formally ' 'diagnosed after confirming all four Berlin/ESICM 2023 criteria. The candidate must ' 'recognise the process is evolving.' ), guideline_ref='ESICM ARDS Guidelines 2023 (Grasselli et al., Intensive Care Med 2023); ' 'Sepsis-3 (Singer et al., JAMA 2016)' )) # Task 2 add(task_block( 2, 'Interpret the ABG fully (Background Case A, VCV FiO₂ 0.8, PEEP 8 cmH₂O).', max_pts=5, answers=[ (1, 'Type 1 (hypoxaemic) respiratory failure: pO₂ 58 mmHg, P/F ratio 72.5 mmHg'), (1, 'Respiratory acidosis: pH 7.31, pCO₂ 48 mmHg'), (1, 'No significant metabolic component: HCO₃⁻ 24 mmol/L, BE -1 (acute respiratory process)'), (1, 'Hyperlactataemia: lactate 2.8 mmol/L — suggests early tissue hypoperfusion / hypoxia'), (1, 'P/F ratio 72.5 mmHg → severe ARDS range (Berlin: P/F < 100 on PEEP ≥ 5; ESICM 2023: P/F ≤ 100)'), ], commentary=( 'The ABG interpretation must be structured: (1) pH, (2) primary disorder, (3) compensation, ' '(4) oxygenation including P/F ratio, (5) additional markers. ' 'The candidate should calculate the P/F ratio (pO₂ ÷ FiO₂ = 58 ÷ 0.8 = 72.5 mmHg) — this is a ' 'frequently tested calculation. Note: pCO₂ of 48 mmHg with pH 7.31 in a patient breathing at ' 'RR 18/min signals impending ventilatory failure — the patient cannot compensate. ' 'The SpO₂/FiO₂ ratio = 89/0.8 = 111 also meets ESICM 2023 threshold (≤ 315 with SpO₂ ≤ 97%) ' 'for ARDS diagnosis. Award full marks only if P/F ratio is calculated or A-a gradient mentioned.' ), traps=( 'TRAP: Candidates often misidentify this as a "mixed" respiratory/metabolic acidosis. ' 'The HCO₃⁻ of 24 and BE of -1 are NORMAL — there is no primary metabolic acidosis here. ' 'The mild lactate is a secondary finding. Another trap: forgetting to state the PEEP requirement ' 'when interpreting P/F — P/F must always be stated with the PEEP level per Berlin/ESICM criteria.' ), guideline_ref='ESICM ARDS Guidelines 2023: New global ARDS definition includes SpO₂/FiO₂ ≤ 315 ' '(SpO₂ ≤ 97%) as alternative to P/F ratio.' )) # Task 3 add(task_block( 3, 'Using the Berlin/ESICM 2023 criteria, does this patient fulfil the diagnosis of ARDS? ' 'State each criterion.', max_pts=4, answers=[ (1, 'Timing: within 1 week of known clinical insult (community-acquired pneumonia) — FULFILLED'), (1, 'Chest imaging: bilateral opacities on CXR not fully explained by effusions, atelectasis, ' 'or nodules — FULFILLED (bilateral diffuse alveolar opacities with air bronchograms)'), (1, 'Origin of oedema: respiratory failure not fully explained by cardiac failure or fluid ' 'overload — BNP mildly elevated but echo shows preserved LV function, no cardiogenic cause — ' 'FULFILLED'), (1, 'Oxygenation: P/F ratio 72.5 mmHg on PEEP ≥ 5 cmH₂O → SEVERE ARDS (P/F ≤ 100); ' 'OR SpO₂/FiO₂ 112 ≤ 315 with SpO₂ ≤ 97% (ESICM 2023 new criterion) — FULFILLED'), ], commentary=( 'This is a HIGH-YIELD examiner task. The candidate MUST cite all four criteria by name and ' 'justify each against the case data. ESICM 2023 updated the definition to include: ' '(1) lung ultrasound as acceptable imaging modality (B-lines / subpleural consolidation); ' '(2) SpO₂/FiO₂ ≤ 315 when SpO₂ ≤ 97% as an alternative oxygenation criterion; ' '(3) non-invasive ventilation (HFNC/NIV with CPAP/PEEP ≥ 5) now acceptable in definition. ' 'Severity: Mild P/F 201-300; Moderate P/F 101-200; Severe P/F ≤ 100. ' 'This patient has SEVERE ARDS. The BNP of 210 pg/mL is mildly elevated — the candidate ' 'should not dismiss cardiac cause without mentioning the echo showing normal LV function.' ), traps=( 'TRAP: Do not confuse the AECC 1994 definition with Berlin 2012 or ESICM 2023. ' 'AECC used ALI (P/F 200-300) and ARDS (P/F < 200) — these terms are obsolete. ' 'The examiner will penalise use of "ALI" or citing AECC criteria as current. ' 'Another trap: stating "PEEP ≥ 5 cmH₂O on mechanical ventilation ONLY" — ' 'ESICM 2023 now accepts HFNC and NIV in the definition for mild/moderate ARDS.' ), guideline_ref='Grasselli G et al. ESICM guidelines on ARDS. Intensive Care Med 2023;49:727-759. ' 'Berlin Definition: Ranieri VM et al. JAMA 2012;307:2526-33.' )) # Task 4 add(task_block( 4, 'What are the initial lung-protective ventilation targets for this patient? ' 'Include tidal volume, plateau pressure, driving pressure, and PEEP strategy.', max_pts=5, answers=[ (1, 'Tidal volume: ≤ 6 mL/kg PREDICTED (ideal) body weight. ' 'IBW = 50 + 0.91 × (162-152.4) = 57 kg → target Vt ≤ 342 mL. ' 'Current Vt 480 mL (8.4 mL/kg IBW) is INJURIOUS — must be reduced.'), (1, 'Plateau pressure: < 30 cmH₂O (ESICM 2023 strong recommendation). ' 'Current Pplat 32 cmH₂O — exceeds target.'), (1, 'Driving pressure: < 15 cmH₂O (ESICM 2023). ' 'Current driving pressure 24 cmH₂O — greatly exceeds target; ' 'reflects very low compliance (Crs = Vt / DP = 480/24 = 20 mL/cmH₂O).'), (1, 'PEEP: higher PEEP strategy recommended in moderate-severe ARDS (ESICM 2023 conditional). ' 'Should be titrated by PEEP/FiO₂ table, best compliance, or oesophageal pressure.'), (1, 'Permissive hypercapnia: accept PaCO₂ up to 60-70 mmHg / pH ≥ 7.20 to achieve lung protection.'), ], commentary=( 'IBW calculation is MANDATORY: males = 50 + 0.91×(height cm - 152.4); ' 'females = 45.5 + 0.91×(height cm - 152.4). This patient is female, 162 cm → IBW 57 kg. ' 'The ARMA trial (NEJM 2000) demonstrated mortality reduction with 6 mL/kg IBW. ' 'Current settings are harmful: Vt 480 mL = 8.4 mL/kg IBW. ' 'ESICM 2023 makes a STRONG recommendation for Vt ≤ 6 mL/kg IBW and Pplat < 30 cmH₂O. ' 'Driving pressure (Pplat - PEEP) is an independent predictor of mortality (Amato 2015, NEJM) ' 'and ESICM 2023 recommends targeting DP < 15 cmH₂O. ' 'The examiner expects the candidate to recognise the auto-PEEP of 4 cmH₂O — total PEEP ' 'is therefore 8 + 4 = 12 cmH₂O (not just set PEEP of 8). ' 'Reducing RR and prolonging expiratory time will help clear auto-PEEP.' ), traps=( 'TRAP 1: Using actual body weight (78 kg) instead of IBW (57 kg) for Vt calculation — ' 'this is a classic and frequently fatal error in the exam. ' 'TRAP 2: Ignoring auto-PEEP — total PEEP determines lung recruitment, not set PEEP. ' 'TRAP 3: Forgetting that permissive hypercapnia is acceptable to achieve lung protection ' '— candidates often increase RR to normalise pCO₂, which perpetuates VILI.' ), guideline_ref='ESICM ARDS 2023 Strong Rec; ARMA Trial NEJM 2000; ' 'Amato et al. Driving pressure NEJM 2015;372:747-55.' )) # Task 5 add(task_block( 5, 'Interpret the chest X-ray and ventilator waveforms. What specific ventilator graphic ' 'abnormality is described, and what is its clinical significance?', max_pts=4, answers=[ (1, 'CXR: bilateral diffuse alveolar and interstitial opacities with air bronchograms — ' 'consistent with ARDS / diffuse alveolar damage. Not cardiomegaly. ' 'ET tube position acceptable (4 cm above carina).'), (1, 'Ventilator graphic: incomplete expiratory flow return to zero (flow-time curve) = ' 'dynamic hyperinflation / auto-PEEP'), (1, 'Clinical significance of auto-PEEP: intrinsic PEEP adds to applied PEEP, increasing ' 'total end-expiratory pressure, risk of haemodynamic compromise (reduced venous return) ' 'and barotrauma'), (1, 'Management of auto-PEEP: reduce RR, increase expiratory time (reduce I:E ratio), ' 'reduce tidal volume, ensure adequate bronchodilation'), ], commentary=( 'Ventilator graphics are a HIGH-FREQUENCY examiner topic. The flow-time curve is the most ' 'sensitive graphic for detecting auto-PEEP. On a flow-time curve, the expiratory limb should ' 'return to the zero baseline before the next inspiration — failure to do so indicates ' 'dynamic hyperinflation. The measured auto-PEEP (expiratory hold manoeuvre) in this case is ' '4 cmH₂O. In ARDS, auto-PEEP is less common than in obstructive disease, but can occur at ' 'high RR and small airways oedema. The candidate should demonstrate understanding of the ' 'flow-time, pressure-time, and volume-time curves independently.' ), traps=( 'TRAP: Focusing only on the CXR and ignoring the ventilator graphics — both are tested here. ' 'Another trap: recommending INCREASED RR to manage hypercapnia, which will WORSEN auto-PEEP. ' 'The examiner will specifically probe: "If you increase the RR to correct the CO₂, ' 'what happens to the auto-PEEP?"' ), guideline_ref='ESICM ARDS 2023; Slutsky AS, Ranieri VM. Ventilator-induced lung injury. ' 'NEJM 2013;369:2126.' )) add(Paragraph('<i>EDIC Part II Exam • July 2026 • EXAMINER USE ONLY</i>', st['footer'])) add(PageBreak()) add(ColourBanner('CCS 2 — Examiner Answer Sheet (continued)', bg=ESICM_BLUE, height=18, font_size=10)) add(Spacer(1,5)) # Task 6 add(task_block( 6, 'What PEEP optimisation strategies are available for this patient? Describe at least ' 'THREE methods and their advantages/disadvantages.', max_pts=4, answers=[ (1, 'PEEP/FiO₂ table (ARDSNet high-PEEP table): simple, widely validated, no extra equipment. ' 'Disadvantage: not individualised.'), (1, 'Decremental PEEP trial after recruitment manoeuvre: titrate to best static compliance ' '(Crs = Vt / [Pplat - PEEP]). Best respiratory mechanics approach. Widely used in ESICM 2023.'), (1, 'Oesophageal pressure (transpulmonary pressure) monitoring: measures pleural pressure, ' 'allows calculation of transpulmonary driving pressure. Particularly useful in obese patients. ' 'Limitation: invasive, requires oesophageal balloon, interpretation complex.'), (1, 'Electrical impedance tomography (EIT): real-time regional ventilation distribution, ' 'identifies PEEP that minimises collapse and overdistension. Most physiologically precise. ' 'Limitation: availability, expertise required.'), ], commentary=( 'ESICM 2023 gives a CONDITIONAL recommendation for higher PEEP in moderate-severe ARDS. ' 'No single PEEP titration strategy has demonstrated superiority in RCTs. ' 'The EPVent-2 trial (Talmor, NEJM 2008 / Beitler JAMA 2019) evaluated transpulmonary pressure ' 'with conflicting results. The ART trial (Lima, JAMA 2017) found HARM with aggressive ' 'recruitment manoeuvres — important context. ESICM 2023 recommends AGAINST routine ' 'recruitment manoeuvres (strong rec against sustained inflation RM). ' 'Accept any 3 of the 4 methods for full marks. The candidate should show understanding ' 'of why PEEP matters: prevention of de-recruitment at end-expiration, "baby lung" concept, ' 'and avoidance of overdistension.' ), traps=( 'TRAP: Recommending the ART-trial protocol (staircase RM to 40-60 cmH₂O) — this was shown ' 'to INCREASE mortality and is explicitly AGAINST ESICM 2023 guidelines. ' 'Another trap: stating "maximal PEEP = best" — overdistension worsens outcome. ' 'The examiner will ask: "What is the upper safe limit of PEEP and how do you know ' 'you have exceeded it?" (Answer: rising Pplat, falling compliance, haemodynamic instability).' ), guideline_ref='ESICM ARDS 2023: Strong rec AGAINST sustained inflation RM. ' 'Conditional rec for higher PEEP in moderate-severe ARDS. ' 'ART Trial: Lima JAMA 2017. EPVent-2: Beitler JAMA 2019.' )) # ── Vignette 1A tasks ───────────────────────────────────────────────────── add(ColourBanner('Give Vignette 1A to the candidate', bg=ESICM_TEAL, height=14, font_size=8)) add(Spacer(1,4)) # Task 7 add(task_block( 7, 'The repeat ABG shows P/F ratio 61 mmHg with PEEP 10. Echocardiography demonstrates ' 'a D-sign (septal flattening) and RV dilation. What does this indicate and how does it ' 'change management?', max_pts=4, answers=[ (1, 'Acute cor pulmonale (ACP) / acute RV failure secondary to ARDS and high airway pressures'), (1, 'The D-sign (septal flattening in both systole AND diastole) indicates pressure-overload ' 'pattern of RV failure (McConnell sign would suggest PE, but here context is ARDS)'), (1, 'Management modification: reduce RV afterload — reduce Pplat, consider prone positioning ' '(improves RV afterload by improving V/Q and reducing HPV); ' 'avoid hypercapnia, hypoxia, acidosis (all worsen pulmonary vascular resistance)'), (1, 'Vasopressor selection: norepinephrine preferred (maintains systemic vascular resistance); ' 'consider inhaled vasodilators (iNO/iloprost) to reduce PVR; ' 'vasopressin as second agent. Avoid aggressive fluid loading.'), ], commentary=( 'ACP complicates approximately 25-50% of severe ARDS and is an independent predictor of mortality. ' 'The D-sign on echo (septal flattening D-shape of LV on parasternal short axis) indicates ' 'elevated RV pressure. The IVC is dilated with partial inspiratory collapse, consistent with ' 'elevated CVP from RV failure. ' 'Key ACP triggers: hypercapnia, hypoxia, acidosis, high driving pressure — all cause ' 'pulmonary vasoconstriction and increase RV afterload. ' 'ESICM 2023: Prone positioning reduces ACP risk. Tidal volumes > 6 mL/kg and Pplat > 27 cmH₂O ' 'are associated with ACP development (Vieillard-Baron, ICM 2016). ' 'The candidate should NOT recommend aggressive RV preload augmentation (fluids) — ' 'in RV pressure overload, this worsens septal shift and LV filling.' ), traps=( 'TRAP: Confusing ACP with PE — both cause D-sign, but context (ARDS, no DVT risk, ' 'bilateral CXR infiltrates) points to ACP. CTPA is not immediately indicated. ' 'TRAP: Increasing PEEP to improve oxygenation when there is already ACP — ' 'this may worsen RV afterload further. Must balance oxygenation vs RV afterload.' ), guideline_ref='Vieillard-Baron A et al. Acute cor pulmonale in ARDS. ICM 2016. ' 'ESICM ARDS 2023.' )) # Task 8 add(task_block( 8, 'What is the role of neuromuscular blockade (NMB) in this patient? ' 'When should it be used and for how long?', max_pts=3, answers=[ (1, 'NMB (cisatracurium) for 48 hours is appropriate in early severe ARDS ' '(P/F < 150 mmHg) to reduce patient-self inflicted lung injury (P-SILI) ' 'and improve synchrony — ESICM 2023 conditional recommendation, ATS 2024 conditional.'), (1, 'Evidence: ACURASYS trial (Papazian 2010, NEJM) showed reduced 90-day mortality; ' 'ROSE trial (2019, NEJM) showed NO benefit when deep sedation was used in control arm — ' 'suggests NMB benefit may be via preventing P-SILI and allowing lung-protective ventilation.'), (1, 'Duration: maximum 48 hours. Beyond 48 hours, NMB-related complications increase ' '(ICU-acquired weakness, prolonged paralysis). Reassess daily. ' 'Use TOF (train-of-four) monitoring to titrate.'), ], commentary=( 'The ROSE trial (Moss 2019, NEJM) did NOT show benefit with early NMB compared to lighter ' 'sedation. This appears to contradict ACURASYS. The key difference: ROSE used deep sedation ' 'in the control arm (fentanyl/midazolam), whereas ACURASYS used lighter sedation. ' 'Current interpretation: NMB benefit is specific to preventing P-SILI in spontaneously ' 'breathing patients — if the patient is already deeply sedated with no respiratory effort, ' 'NMB adds little. ESICM 2023 and ATS 2024: suggest NMB for P/F < 150 if unable to achieve ' 'lung-protective ventilation and to prevent P-SILI. ' 'The cisatracurium infusion in this case (already started) is appropriate — the candidate ' 'must know to stop at 48 hours and assess.' ), traps=( 'TRAP: Stating "NMB is routinely recommended for all ARDS" — this is WRONG per ESICM 2023, ' 'which recommends AGAINST routine use. It is indicated selectively for P/F < 150 or ' 'difficult ventilator synchrony. ' 'Another trap: not mentioning TOF monitoring — examiner will probe this specifically.' ), guideline_ref='ESICM ARDS 2023: Rec AGAINST routine NMB, but conditional rec for severe ARDS. ' 'ACURASYS: Papazian NEJM 2010. ROSE: Moss NEJM 2019.' )) add(Paragraph('<i>EDIC Part II Exam • July 2026 • EXAMINER USE ONLY</i>', st['footer'])) add(PageBreak()) add(ColourBanner('CCS 2 — Examiner Answer Sheet (continued)', bg=ESICM_BLUE, height=18, font_size=10)) add(Spacer(1,5)) # Task 9 add(task_block( 9, 'The ventilator shows a driving pressure of 24 cmH₂O despite reducing Vt to 420 mL ' '(7.4 mL/kg IBW). The compliance is 17.5 mL/cmH₂O. What further steps do you take ' 'to reduce driving pressure and why?', max_pts=4, answers=[ (1, 'Further reduce tidal volume toward 6 mL/kg IBW (342 mL target). ' 'Accept 300-380 mL with permissive hypercapnia.'), (1, 'Optimise PEEP: increasing PEEP may improve compliance if recruitable lung exists ' '(open collapsed alveoli → greater Crs → lower DP for same Vt). ' 'PEEP titration to best compliance by decremental trial.'), (1, 'Prone positioning: improves recruitable lung in dorsal regions, increases Crs, ' 'reduces DP (confirmed in PROSEVA trial — 28-day mortality benefit). ' 'ESICM 2023 strong recommendation for P/F < 150.'), (1, 'Consider extracorporeal CO₂ removal (ECCO₂R) or VV-ECMO if DP remains > 15 cmH₂O ' 'and cannot achieve lung-protective targets (ESICM 2023 conditional recommendation for ECMO ' 'in refractory severe ARDS).'), ], commentary=( 'Driving pressure = Pplat - PEEP = Vt / Crs. It represents the stress applied to the ' '"baby lung" — the aerated portion of the ARDS lung. Amato et al. (NEJM 2015) showed that ' 'driving pressure was the ventilator variable most strongly associated with ARDS mortality. ' 'A driving pressure > 15 cmH₂O is associated with increased mortality. ' 'In this case: Crs = 17.5 mL/cmH₂O, which is severely reduced (normal 50-70 mL/cmH₂O). ' 'Increasing PEEP paradoxically reduces DP if it recruits alveoli (improves Crs). ' 'However, if lung is not recruitable, higher PEEP worsens overdistension and increases DP. ' 'This is why a decremental PEEP trial to find the PEEP of best compliance is superior ' 'to fixed tables in severe ARDS.' ), traps=( 'TRAP: Increasing RR to allow lower Vt without permissive hypercapnia — ' 'the mechanical power of ventilation (related to RR × Vt × driving pressure) must also ' 'be considered. ESICM 2023 introduces mechanical power as a target (< 17 J/min suggested). ' 'TRAP: Assuming all ARDS is recruitable — only ~50% of ARDS patients have significant ' 'recruitability (detected by CT-scan or pressure-volume curve).' ), guideline_ref='Amato MBA et al. DP and survival in ARDS. NEJM 2015;372:747. ' 'PROSEVA Trial: Guérin et al. NEJM 2013. ' 'ESICM ARDS 2023: strong rec for prone in P/F < 150.' )) # ── Vignette 2A ─────────────────────────────────────────────────────────── add(ColourBanner('Give Vignette 2A to the candidate', bg=ESICM_TEAL, height=14, font_size=8)) add(Spacer(1,4)) # Task 10 add(task_block( 10, 'The physiotherapist asks about prone positioning. What are the indications, ' 'contraindications, practical steps, and expected duration for prone positioning?', max_pts=5, answers=[ (1, 'Indication: P/F ratio < 150 mmHg (or < 100 mmHg as strong rec per ESICM 2023) ' 'despite optimised ventilation and FiO₂ ≥ 0.6. This patient qualifies (P/F 97 mmHg).'), (1, 'Contraindications: unstable spinal injury, open chest/abdomen, facial trauma, ' 'haemodynamic instability not responsive to vasopressors, raised ICP, pregnancy, ' 'recent sternotomy (relative — within 2 weeks).'), (1, 'Practical steps: adequate sedation ± NMB, minimum 5-person turn, protect eyes/pressure ' 'areas, secure ET tube/lines/catheters, prone with head rotation alternated every 2 hours, ' 'arms in "swimmer position".'), (1, 'Duration: minimum 16 hours per session (PROSEVA used 16-17 hours). ' 'Repeat daily until P/F > 150 consistently in supine, or no further improvement.'), (1, 'Expected response: ~70% of patients are "responders" — improvement in P/F by ≥ 20 mmHg. ' 'Non-responders still benefit in terms of homogenising ventilation and reducing ACP. ' 'PROSEVA: absolute mortality reduction 16% (28-day mortality 16% prone vs 32.8% supine).'), ], commentary=( 'PROSEVA (Guérin, NEJM 2013) is the landmark trial: 28-day mortality 16.0% vs 32.8% ' '(NNT = 6). The benefit was confirmed in the meta-analysis (Munshi, Lancet 2017). ' 'ESICM 2023: STRONG RECOMMENDATION for prone positioning in moderate-severe ARDS ' '(P/F < 150). This is one of the strongest recommendations in all of ARDS management. ' 'The candidate should understand the physiological rationale: ' '(1) recruitment of dorsal atelectatic regions; (2) homogenisation of stress distribution; ' '(3) improved V/Q matching; (4) reduced ACP; (5) improved secretion drainage. ' 'The timing matters: PROSEVA enrolled patients ≥12 hours after qualifying, ensuring ' 'stabilisation. ESICM 2023 suggests initiating within 12-24 hours of meeting criteria.' ), traps=( 'TRAP 1: Stating contraindication of "haemodynamic instability" without specifying ' '"not responsive to vasopressors" — patients on vasopressors CAN be proned if stable. ' 'TRAP 2: Insufficient duration — prone for < 16 hours (e.g., "8-12 hours") is not ' 'supported by evidence and will be challenged by the examiner. ' 'TRAP 3: Equating non-response with futility — non-responders still benefit from ' 'homogeneous ventilation.' ), guideline_ref='PROSEVA: Guérin NEJM 2013. ESICM ARDS 2023 Strong Recommendation (prone P/F<150). ' 'Munshi meta-analysis Lancet 2017.' )) # Task 11 add(task_block( 11, 'Calculate the mechanical power for this patient (Vignette 2A: Vt 350 mL, RR 22, ' 'Pplat 28, PEEP 14, driving pressure 14 cmH₂O). What is its clinical significance?', max_pts=3, answers=[ (1, 'Mechanical power formula (simplified): ' 'MP (J/min) = 0.098 × RR × Vt(L) × [ΔPinsp + PEEP] ' '= 0.098 × 22 × 0.35 × [14 + 14] = 0.098 × 22 × 0.35 × 28 ≈ 21.1 J/min. ' '(Stated in vignette as 22.4 J/min — accept any value 20-24 J/min as correct if ' 'method shown). This exceeds the suggested threshold of 17 J/min.'), (1, 'Significance: mechanical power is the total energy transferred to the lung per minute. ' 'Exceeding 17 J/min is associated with VILI and increased mortality (Serpa Neto 2018). ' 'It incorporates all ventilator variables: RR, Vt, driving pressure, PEEP.'), (1, 'To reduce mechanical power: reduce RR (most impactful), reduce Vt, reduce driving pressure, ' 'reduce PEEP if causing overdistension. Consider ECCO₂R to allow ultra-protective ventilation.'), ], commentary=( 'Mechanical power is increasingly recognised as a unifying concept for VILI. ' 'The formula used in clinical practice: MP = 0.098 × RR × Vt × (Pplat - PEEP/2 + PEEP) ' 'or the simplified Gattinoni version. Serpa Neto et al. (Anesthesiology 2018) showed that ' 'each unit increase in mechanical power above 17 J/min increases ARDS mortality. ' 'ESICM 2023 does not yet give a specific mechanical power target as a strong recommendation, ' 'but the concept is embedded in limiting Vt, DP, and RR simultaneously. ' 'This is a newer concept that distinguishes high-performing candidates from average ones.' ), traps=( 'TRAP: Inability to recall or derive the mechanical power formula under exam conditions — ' 'the examiner may accept a qualitative description if the components are named correctly. ' 'Key insight: "all the energy I put into the lung every minute" and naming all 4 components.' ), guideline_ref='Gattinoni L et al. Mechanical power and development of VILI. ' 'Intensive Care Med 2016;42:1567. Serpa Neto A et al. Anesthesiology 2018.' )) add(Paragraph('<i>EDIC Part II Exam • July 2026 • EXAMINER USE ONLY</i>', st['footer'])) add(PageBreak()) add(ColourBanner('CCS 2 — Examiner Answer Sheet (continued)', bg=ESICM_BLUE, height=18, font_size=10)) add(Spacer(1,5)) # ════ CASE B TASKS ════════════════════════════════════════════════════════ add(ColourBanner('CASE B — Background Vignette Tasks', bg=ESICM_TEAL, height=16, font_size=9)) add(Spacer(1,4)) add(Paragraph( '<i>Give the candidate 5 minutes to read Background Case B. Do not ask questions until ' 'reading time has elapsed.</i>', st['small'])) add(Spacer(1,5)) # Task 12 add(task_block( 12, 'Interpret the pre-intubation ABG of Case B (HFNC FiO₂ 1.0). ' 'What is the diagnosis and what does the combination of findings tell you?', max_pts=5, answers=[ (1, 'Severe hypoxaemia: pO₂ 50 mmHg on FiO₂ 1.0 → P/F ratio = 50 mmHg (SEVERE ARDS range)'), (1, 'Respiratory acidosis: pH 7.22, pCO₂ 54 mmHg — ventilatory failure / fatigue'), (1, 'Metabolic component: HCO₃⁻ 22 mmol/L (normal) but BE -5 suggests mild metabolic ' 'acidosis component contributing to low pH (lactate 4.1 = lactic acidosis)'), (1, 'Combined Type 1 and Type 2 respiratory failure: hypoxaemia + hypercapnia + ' 'acidosis = impending respiratory arrest. Immediate intubation indicated.'), (1, 'High anion gap: Na - (Cl + HCO₃) = 132 - (94+22) = 16 mEq/L — raised AGMA ' 'from lactic acidosis / possible alcohol-related acidosis'), ], commentary=( 'This ABG represents the worst-case scenario: severe Type 1 RF (P/F 50) PLUS Type 2 RF ' '(pCO₂ 54, pH 7.22). A rising pCO₂ in a patient with bilateral infiltrates is a ' '"cannot oxygenate, cannot ventilate" picture — this is the indication for immediate RSI. ' 'The failed NIV trial of 1 hour is important context: ESICM ARDS 2023 suggests that ' 'high-risk patients (P/F < 200, RR > 30, accessory muscle use) should not have prolonged ' 'NIV trials as delayed intubation worsens outcome. ' 'Note the alcohol history: GGT 310 and ALT elevation suggest hepatic involvement. ' 'The hyponatraemia (132) and thrombocytopaenia (88 ×10⁹) raise concern for sepsis-induced ' 'organ dysfunction — meets Sepsis-3 criteria (SOFA ≥ 2 from baseline).' ), traps=( 'TRAP: Not calculating the AGMA — hyponatraemia lowers the apparent bicarbonate ' 'but the anion gap calculation corrects for this. ' 'TRAP: Attributing all findings to alcohol — the P/F ratio of 50 is independently lethal ' 'regardless of aetiology and drives immediate management.' ), guideline_ref='Sepsis-3: Singer et al. JAMA 2016. ' 'ESICM ARDS 2023: HFNC for mild ARDS; early intubation for failing NIV/HFNC.' )) # Task 13 add(task_block( 13, 'What are your initial ventilator settings post-intubation for Case B ' '(175 cm, 70 kg IBW male)? Justify each setting.', max_pts=5, answers=[ (1, 'Mode: VCV or PCV acceptable. IBW = 50 + 0.91×(175-152.4) = 70.6 kg. ' 'Target Vt ≤ 6 mL/kg IBW = ≤ 424 mL. Starting at 6 mL/kg = 420 mL.'), (1, 'PEEP: start 10-12 cmH₂O for severe ARDS (P/F 50). Use ARDSNet high-PEEP/FiO₂ table ' '(FiO₂ 1.0 → PEEP 18-24 by table; clinical judgement to start 14-16 and titrate). ' 'Total PEEP must be measured with expiratory hold.'), (1, 'FiO₂: 1.0 initially, wean to target SpO₂ 92-96% (SaO₂ ≥ 88%). ' 'Avoid prolonged FiO₂ > 0.8 (oxygen toxicity).'), (1, 'RR: 18-22/min to target pH ≥ 7.20-7.25. Accept permissive hypercapnia. ' 'Monitor for auto-PEEP.'), (1, 'Inspiratory time: standard I:E 1:2 initially; avoid inverse ratio without close monitoring. ' 'Check Pplat within 30 min of initiation and target < 30 cmH₂O (ESICM strong rec).'), ], commentary=( 'The Surge-3 (now Sepsis-3 2016) definition of septic shock applies here: ' 'vasopressor requirement to maintain MAP ≥ 65, lactate > 2 mmol/L, and no hypovolaemia. ' 'The candidate must manage BOTH the respiratory failure AND septic shock simultaneously. ' 'Post-intubation haemodynamic collapse is common — expect a MAP drop after RSI. ' 'Have norepinephrine ready. Also: this patient has thrombocytopaenia (88×10⁹/L) and ' 'coagulopathy (INR 1.88) — be careful with invasive procedures. ' 'The elevated EVLWI (from PiCCO in later vignette) confirms severe ARDS. ' 'Initial ventilation: the key error candidates make is starting at "normal" Vt 500-600 mL ' 'for a 92 kg actual weight patient — MUST use IBW 70.6 kg.' ), traps=( 'TRAP: Setting PEEP too aggressively at initiation in a haemodynamically unstable patient. ' 'In septic shock, high PEEP reduces venous return and may precipitate cardiac arrest. ' 'Start at moderate PEEP (10-12) and titrate after haemodynamic stabilisation. ' 'TRAP: Forgetting post-intubation chest X-ray to confirm ET tube position.' ), guideline_ref='ARDSNet ARMA Trial NEJM 2000; ESICM ARDS 2023; ' 'Sepsis-3 guidelines JAMA 2016; Surviving Sepsis Campaign 2021.' )) # ── Vignette 1B ─────────────────────────────────────────────────────────── add(ColourBanner('Give Vignette 1B to the candidate', bg=ESICM_TEAL, height=14, font_size=8)) add(Spacer(1,4)) add(Paragraph('<i>EDIC Part II Exam • July 2026 • EXAMINER USE ONLY</i>', st['footer'])) add(PageBreak()) add(ColourBanner('CCS 2 — Examiner Answer Sheet (continued)', bg=ESICM_BLUE, height=18, font_size=10)) add(Spacer(1,5)) # Task 14 add(task_block( 14, 'Interpret the PiCCO data (Vignette 1B): CI 4.8, SVRI 1180, GEDVI 820, EVLWI 18, PVPI 3.8. ' 'What is the haemodynamic diagnosis and what does the EVLWI tell you?', max_pts=4, answers=[ (1, 'Distributive (septic) shock: elevated CI 4.8 L/min/m² with low SVRI 1180 ' '(normal 1700-2400) — vasodilatory state despite norepinephrine 0.35 µg/kg/min'), (1, 'Adequate preload: GEDVI 820 mL/m² (normal 680-800 — slightly elevated). ' 'Fluid challenge not indicated; patient is not hypovolaemic.'), (1, 'Elevated EVLWI 18 mL/kg (normal < 10 mL/kg): severe pulmonary oedema — ' 'fluid accumulation in the lung. Confirms ARDS pathophysiology. ' 'EVLWI > 15 mL/kg associated with mortality in ARDS.'), (1, 'PVPI 3.8 (normal < 3): elevated permeability index = non-cardiogenic (high-permeability) ' 'pulmonary oedema, consistent with ARDS. ' 'PVPI distinguishes cardiogenic (low PVPI) from permeability oedema (high PVPI).'), ], commentary=( 'PiCCO (transpulmonary thermodilution) provides GEDVI (global end-diastolic volume — ' 'a preload indicator), EVLWI (extravascular lung water — direct measurement of pulmonary oedema), ' 'PVPI (pulmonary vascular permeability index = EVLWI/pulmonary blood volume). ' 'EVLWI > 15 mL/kg is associated with 50-70% ICU mortality. ' 'A conservative fluid strategy targeting EVLWI reduction (rather than CVP) improves ' 'ventilator-free days (FACTT Trial, NEJM 2006). ' 'The candidate should recognise that CVP of 18 mmHg in this context reflects RV dysfunction ' 'and high PEEP, NOT genuine preload excess — CVP should not be used as the sole fluid guide. ' 'ESICM Sepsis-3 / Surviving Sepsis Campaign 2021: target conservative fluid strategy after ' 'initial resuscitation phase.' ), traps=( 'TRAP: Giving more fluids because "the patient is in shock and lactate is high" — ' 'the GEDVI shows adequate preload and the EVLWI is 18 (pulmonary flooding). ' 'More fluids will worsen ARDS. Target: EVLWI minimisation with vasopressors/diuretics. ' 'TRAP: Ignoring the PVPI — this is the key differentiator of cardiogenic vs ARDS oedema.' ), guideline_ref='FACTT Trial: Wiedemann NEJM 2006 (conservative vs liberal fluids in ARDS). ' 'SSC 2021: Surviving Sepsis Campaign guidelines.' )) # Task 15 add(task_block( 15, 'The central venous SvO₂ is 58% (Vignette 1B). Interpret this and describe your assessment ' 'of global oxygen balance.', max_pts=4, answers=[ (1, 'SvO₂ < 65% indicates impaired oxygen delivery (DO₂) relative to demand (VO₂). ' 'In distributive shock with elevated CI, low SvO₂ = high oxygen extraction ratio.'), (1, 'DO₂ = CI × CaO₂ = CI × (Hb × 1.34 × SaO₂ + 0.003 × PaO₂). ' 'Low Hb (9.8 g/dL) + SaO₂ ~89% + CI 4.8 = DO₂ is impaired despite high CO. ' 'Accept: reduced DO₂ due to anaemia and hypoxaemia despite preserved cardiac output.'), (1, 'Address oxygen delivery: consider red cell transfusion (Hb 9.8, target 8-10 g/dL in ARDS ' 'with concurrent shock — ESICM: transfuse if Hb < 7-8 g/dL generally, but higher ' 'threshold reasonable in oxygen-limited states). Optimise PEEP/FiO₂.'), (1, 'Venous-arterial CO₂ gap: pCO₂(v-a) gap = pCO₂ central - pCO₂ arterial = 58 - 58 = 0 mmHg. ' '(Using ABG pCO₂ 58 and central pCO₂ not explicitly stated — accept discussion of v-a gap ' 'concept as valid answer. Normal < 6 mmHg. Elevated gap indicates low flow / poor perfusion.)'), ], commentary=( 'The global oxygen balance is assessed via the Fick equation. In ARDS + septic shock, ' 'the oxygen delivery-demand mismatch occurs because: (1) DO₂ is limited by hypoxaemia AND ' 'anaemia AND (2) VO₂ is increased by fever, inflammation, work of breathing. ' 'SvO₂ 58% in the context of CI 4.8 L/min/m² (hyperdynamic) suggests the tissues are ' 'extracting more oxygen than normal — this indicates inadequate DO₂ rather than distributive ' 'mismatch. The venous-arterial CO₂ gap (> 6 mmHg) is a more reliable marker of tissue ' 'hypoperfusion than SvO₂ alone and is tested in EDIC consistently. ' 'Lactate 5.8 mmol/L confirms severe tissue hypoperfusion.' ), traps=( 'TRAP: Attributing low SvO₂ entirely to RV failure and giving fluids — ' 'the CI is 4.8 (normal-high). The problem is DO₂ not CO. ' 'TRAP: Transfusing to Hb > 10 g/dL without indication — SSC 2021 targets Hb ≥ 7 g/dL ' 'in stable patients. A threshold of 8-9 may be appropriate in oxygen-limited ARDS but ' 'must be justified.' ), guideline_ref='Surviving Sepsis Campaign 2021; SSC Transfusion threshold. ' 'Vincent JL. Understanding cardiac output. Crit Care 2008.' )) add(Paragraph('<i>EDIC Part II Exam • July 2026 • EXAMINER USE ONLY</i>', st['footer'])) add(PageBreak()) add(ColourBanner('CCS 2 — Examiner Answer Sheet (continued)', bg=ESICM_BLUE, height=18, font_size=10)) add(Spacer(1,5)) # ── Vignette 2B ─────────────────────────────────────────────────────────── add(ColourBanner('Give Vignette 2B to the candidate', bg=ESICM_TEAL, height=14, font_size=8)) add(Spacer(1,4)) # Task 16 add(task_block( 16, 'After 16 hours of prone positioning, the patient is returned to supine. ' 'The P/F has improved to 120 mmHg. Inhaled nitric oxide and methylprednisolone have been started. ' 'Evaluate the appropriateness of these interventions based on current evidence.', max_pts=4, answers=[ (1, 'Prone positioning: APPROPRIATE and evidence-based (PROSEVA, ESICM 2023 strong rec). ' 'The improvement in P/F from 65.6 to 120 (improvement of ~55 mmHg) confirms the patient ' 'is a prone responder. Continued sessions indicated.'), (1, 'Inhaled nitric oxide: CONDITIONAL — iNO reduces PVR and may transiently improve ' 'oxygenation (P/F improvement ~10-15 mmHg), but does NOT reduce mortality (Cochrane 2016). ' 'ESICM 2023: conditional recommendation as rescue therapy in severe ARDS with ACP or ' 'refractory hypoxaemia. Appropriate as a bridge to ECMO.'), (1, 'Corticosteroids (methylprednisolone 1 mg/kg/day): appropriate in early-moderate ARDS ' '(< 14 days). Dexamethasone 6 mg/day or methylprednisolone may reduce ventilator-free days. ' 'ESICM 2023: conditional recommendation. Evidence: Villar 2020 (dexamethasone), ' 'DEXA-ARDS trial, Meduri meta-analyses. Not recommended in late/fibroproliferative phase.'), (1, 'ECMO team consulted: appropriate. EOLIA criteria met: P/F < 80 for 3h, or < 80 for 6h, ' 'or pH < 7.25 for 6h despite optimal management. VV-ECMO is the rescue therapy of choice. ' 'ESICM 2023: conditional recommendation for VV-ECMO in refractory severe ARDS.'), ], commentary=( 'This task requires the candidate to critically evaluate each intervention against the evidence. ' 'iNO: the Cochrane review (Adhikari 2016) showed no mortality benefit and possible harm ' '(renal failure). Use only as a bridge or for RV rescue. Not routine. ' 'Corticosteroids: the DEXA-ARDS trial (Villar JAMA 2020) showed dexamethasone 20 mg×5 + ' '10 mg×5 days reduced ventilator-free days and 60-day mortality. ESICM 2023: conditional. ' 'The key: start EARLY (< 14 days), stop if no response, do NOT use late ARDS. ' 'EOLIA trial (Combes, NEJM 2018): primary endpoint (60-day mortality) not met p=0.09, ' 'but 28% of control arm required rescue ECMO. Bayesian re-analysis suggests 96% probability ' 'of benefit. ESICM 2023: VV-ECMO as conditional recommendation for refractory ARDS.' ), traps=( 'TRAP: Stating iNO "reduces mortality" — it does NOT (Cochrane evidence, strong). ' 'TRAP: Starting corticosteroids at day 14+ in fibroproliferative ARDS — evidence of harm. ' 'TRAP: Stating EOLIA "proved ECMO works" — the trial did NOT meet its primary endpoint. ' 'The candidate must say "did not meet primary endpoint but Bayesian analysis and crossover ' 'confounding support conditional recommendation".' ), guideline_ref='EOLIA Trial: Combes NEJM 2018. DEXA-ARDS: Villar JAMA 2020. ' 'ESICM ARDS 2023: VV-ECMO conditional rec; steroid conditional rec. ' 'iNO Cochrane Review: Adhikari 2016.' )) # Task 17 add(task_block( 17, 'The microbiology confirms S. pneumoniae bacteraemic pneumonia. ' 'How does this change your antibiotic management? Discuss de-escalation strategy.', max_pts=3, answers=[ (1, 'De-escalate to beta-lactam monotherapy: S. pneumoniae confirmed, sensitive to beta-lactams. ' 'Switch to IV amoxicillin-clavulanate, or ceftriaxone 2g/24h, or benzylpenicillin ' '(if fully sensitive). Discontinue broad-spectrum agents (meropenem, vancomycin not needed).'), (1, 'Duration: 5-7 days for bacteraemic pneumococcal pneumonia (IDSA 2019, SSC 2021). ' 'ESICM/SSC: target minimum effective duration; use procalcitonin to guide de-escalation ' '(stop antibiotics if PCT < 0.5 ng/mL or < 80% of peak value on ≥ day 3).'), (1, 'Source control: pneumonia is the source — no additional drainage required. ' 'Ensure adequate tissue penetration of chosen antibiotic in the context of severe ARDS. ' 'PK/PD optimisation: extended infusion beta-lactam to maximise %T > MIC.'), ], commentary=( 'Antibiotic stewardship and de-escalation are core EDIC topics. The initial broad-spectrum ' 'cover (meropenem + vancomycin in Case B) was appropriate given the unknown pathogen and ' 'severity. Once susceptibility is confirmed, de-escalation to targeted therapy is MANDATORY. ' 'Surviving Sepsis Campaign 2021: strong recommendation for de-escalation as soon as ' 'clinically possible. Use PCT to guide duration — multiple RCTs show PCT-guided therapy ' 'reduces antibiotic exposure without worsening outcomes. ' 'Note: in severe ARDS with impaired lung perfusion, standard antibiotic dosing may be ' 'insufficient — consider augmented dosing or continuous infusion.' ), traps=( 'TRAP: Continuing broad-spectrum antibiotics beyond 7 days without justification — ' 'the examiner will specifically ask "Why are you keeping meropenem?" ' 'TRAP: Using PCT as the ONLY criterion to stop antibiotics — clinical resolution ' '(fever, WBC trend, haemodynamics) must also be considered.' ), guideline_ref='SSC 2021: De-escalation strong rec. IDSA CAP 2019. ' 'PCT-guided therapy: de Jong JAMA 2016.' )) # Task 18 — Overall Performance add(Spacer(1, 8)) add(ColourBanner('Overall Performance Assessment', bg=ESICM_BLUE, height=16, font_size=9)) add(Spacer(1,4)) perf_data = [ ['Rating', 'Description', '☑'], ['Superior performance', 'Well above required competencies — unprompted advanced knowledge, ' 'integrates physiology with guideline evidence seamlessly', '☐'], ['Clearly satisfactory', 'Clearly meets required competencies — correct approach, ' 'guideline-compliant, handles probing questions', '☐'], ['Bare pass (Borderline)', 'On balance meets minimum requirements — passes core tasks but ' 'limited in depth or needs frequent prompting', '☐'], ['Bare fail', 'Just fails minimum requirements — significant gaps in core knowledge ' '(e.g. cannot calculate IBW, cannot cite ARDS criteria)', '☐'], ['Clearly unsatisfactory', 'Well below requirements — patient safety concerns', '☐'], ] avail_w = PAGE_W - L_MARGIN - R_MARGIN perf_t = Table( [[Paragraph(str(c), ParagraphStyle('ph', fontName='Helvetica-Bold' if r==0 else 'Helvetica', fontSize=8.5 if r > 0 else 9, textColor=WHITE if r==0 else BLACK)) for c in row] for r, row in enumerate(perf_data)], colWidths=[52*mm, avail_w-52*mm-15*mm, 15*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,0), ESICM_BLUE), ('ROWBACKGROUNDS',(0,1),(-1,-1), [WHITE, TABLE_ALT]), ('GRID',(0,0),(-1,-1),0.4, MID_GREY), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),5), ('FONTSIZE',(2,1),(-1,-1),14), ]) ) add(perf_t) add(Spacer(1,8)) # Score summary score_data = [ ['Section', 'Tasks', 'Max Points', 'Score'], ['Case A — Background + Vignettes', 'Tasks 1–11', '43', '___'], ['Case B — Background + Vignettes', 'Tasks 12–17', '25', '___'], ['', 'TOTAL', '68', '___'], ] score_t = Table( [[Paragraph(str(c), ParagraphStyle('sh', fontName='Helvetica-Bold' if r==0 else 'Helvetica', fontSize=9, textColor=WHITE if r==0 else BLACK, alignment=TA_CENTER if i > 1 else TA_LEFT)) for i, c in enumerate(row)] for r, row in enumerate(score_data)], colWidths=[70*mm, 50*mm, 35*mm, 25*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,0), ESICM_BLUE), ('BACKGROUND',(0,-1),(-1,-1), TABLE_HDR), ('GRID',(0,0),(-1,-1),0.5, MID_GREY), ('ROWBACKGROUNDS',(0,1),(-2,-2), [WHITE, TABLE_ALT]), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),5), ('FONTNAME',(0,-1),(1,-1),'Helvetica-Bold'), ]) ) add(score_t) add(Spacer(1,10)) add(Paragraph( '<i>Examiner signature: _______________________ ' ' Date: _______________</i>', ParagraphStyle('sig', fontSize=9, textColor=DARK_GREY, alignment=TA_CENTER))) add(Spacer(1, 4)) add(Paragraph('<i>EDIC Part II Exam • July 2026 • EXAMINER USE ONLY</i>', st['footer'])) first, later = make_page_template() doc.build(story, onFirstPage=first, onLaterPages=later) print(f'Answer sheet written: {out_path}') if __name__ == '__main__': out = '/home/daytona/workspace/edic-ards-ccs/CCS2_Answer_Sheet_Examiner.pdf' build_answer_sheet(out) PYEOF python3 /home/daytona/workspace/edic-ards-ccs/build_answer_sheet.py
cd /home/daytona/workspace/edic-ards-ccs/ && ls -lh *.pdf && python3 -c " from pypdf import PdfReader for f in ['CCS2_Question_Sheet.pdf', 'CCS2_Answer_Sheet_Examiner.pdf']: r = PdfReader(f) print(f'{f}: {len(r.pages)} pages') for i, page in enumerate(r.pages): txt = page.extract_text() print(f' Page {i+1}: {len(txt)} chars - {txt[:80].strip()!r}') "
are you done writing ?
CCS2 Question Sheet
PDF Document · PDF
CCS2 Answer Sheet Examiner
PDF Document · PDF
| Topic | Task |
|---|---|
| ABG interpretation (P/F, AGMA, permissive hypercapnia) | 2, 12 |
| ARDS diagnosis — ESICM 2023 criteria (SpO₂/FiO₂, lung USS) | 3 |
| IBW calculation + lung-protective ventilation targets | 4, 13 |
| Ventilator graphics — auto-PEEP on flow-time curve | 5 |
| PEEP optimisation (PEEP/FiO₂ table, decremental trial, EIT, oesophageal Ptp) | 6 |
| Acute cor pulmonale + RV management in ARDS | 7 |
| NMB — ACURASYS vs ROSE, current ESICM 2023 position | 8 |
| Driving pressure + mechanical power calculation | 9, 11 |
| Prone positioning — PROSEVA, indications, duration, contraindications | 10 |
| PiCCO interpretation — EVLWI, PVPI, conservative fluid strategy | 14 |
| Global O₂ balance, SvO₂, v-a CO₂ gap | 15 |
| iNO, corticosteroids, VV-ECMO evidence (EOLIA, DEXA-ARDS) | 16 |
| Antibiotic de-escalation + PCT-guided therapy | 17 |
Generate a study guide highlighting the 12 most common EDIC examiner traps
cat << 'PYEOF' > /home/daytona/workspace/edic-ards-ccs/build_study_guide.py """ EDIC Part II — Study Guide: 12 Most Common Examiner Traps Professional PDF matching ESICM/EDIC branding style. """ from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.units import mm from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, KeepTogether, PageBreak, HRFlowable) from reportlab.lib.styles import ParagraphStyle from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY from reportlab.platypus import Flowable # ── Palette ────────────────────────────────────────────────────────────────── ESICM_BLUE = colors.HexColor('#003D73') ESICM_TEAL = colors.HexColor('#007B9E') ESICM_LIGHT = colors.HexColor('#D6E4F0') MID_GREY = colors.HexColor('#D0D0D0') DARK_GREY = colors.HexColor('#555555') LIGHT_GREY = colors.HexColor('#F5F5F5') TABLE_ALT = colors.HexColor('#EEF5FB') RED_DARK = colors.HexColor('#C62828') RED_LIGHT = colors.HexColor('#FFEBEE') RED_MID = colors.HexColor('#EF5350') GREEN_DARK = colors.HexColor('#2E7D32') GREEN_LIGHT = colors.HexColor('#E8F5E9') GREEN_MID = colors.HexColor('#43A047') AMBER_DARK = colors.HexColor('#E65100') AMBER_LIGHT = colors.HexColor('#FFF3E0') AMBER_MID = colors.HexColor('#FB8C00') PURPLE_DARK = colors.HexColor('#4A148C') PURPLE_LIGHT = colors.HexColor('#F3E5F5') TEAL_LIGHT = colors.HexColor('#E0F7FA') GOLD = colors.HexColor('#F9A825') WHITE = colors.white BLACK = colors.black PAGE_W, PAGE_H = A4 L_MARGIN = R_MARGIN = 18 * mm T_MARGIN = 22 * mm B_MARGIN = 20 * mm AVAIL_W = PAGE_W - L_MARGIN - R_MARGIN # ── Styles ──────────────────────────────────────────────────────────────────── def S(name, **kw): d = dict(fontName='Helvetica', fontSize=9, leading=13, textColor=BLACK, spaceBefore=0, spaceAfter=0) d.update(kw) return ParagraphStyle(name, **d) STYLES = { 'body': S('body', leading=13, spaceAfter=2), 'bold': S('bold', fontName='Helvetica-Bold'), 'footer': S('footer', fontSize=7.5, textColor=DARK_GREY, alignment=TA_CENTER), 'trap_num': S('trap_num', fontName='Helvetica-Bold', fontSize=22, textColor=WHITE, alignment=TA_CENTER), 'trap_title': S('trap_title', fontName='Helvetica-Bold', fontSize=12, textColor=WHITE, leading=16), 'trap_sub': S('trap_sub', fontName='Helvetica-BoldOblique', fontSize=9, textColor=WHITE, leading=12), 'wrong_hdr': S('wrong_hdr', fontName='Helvetica-Bold', fontSize=8.5, textColor=RED_DARK), 'wrong_body': S('wrong_body', fontSize=9, textColor=RED_DARK, leading=13, fontName='Helvetica-Oblique'), 'right_hdr': S('right_hdr', fontName='Helvetica-Bold', fontSize=8.5, textColor=GREEN_DARK), 'right_body': S('right_body', fontSize=9, textColor=GREEN_DARK, leading=13), 'why_hdr': S('why_hdr', fontName='Helvetica-Bold', fontSize=8.5, textColor=AMBER_DARK), 'why_body': S('why_body', fontSize=8.5, textColor=AMBER_DARK, leading=12.5), 'probe_hdr': S('probe_hdr', fontName='Helvetica-Bold', fontSize=8.5, textColor=ESICM_BLUE), 'probe_body': S('probe_body', fontSize=8.5, textColor=ESICM_BLUE, leading=12.5, fontName='Helvetica-Oblique'), 'ref': S('ref', fontSize=7.5, textColor=DARK_GREY, fontName='Helvetica-Oblique'), 'tag': S('tag', fontName='Helvetica-Bold', fontSize=7.5, textColor=WHITE, alignment=TA_CENTER), 'intro': S('intro', fontSize=10, leading=15, textColor=BLACK, alignment=TA_JUSTIFY), 'toc_item': S('toc_item', fontSize=9, leading=14, textColor=ESICM_BLUE), 'summary_hdr': S('summary_hdr', fontName='Helvetica-Bold', fontSize=9, textColor=WHITE), 'summary_body':S('summary_body', fontSize=8.5, textColor=BLACK, leading=12), } # ── Flowables ───────────────────────────────────────────────────────────────── class PageHeader(Flowable): """Full-width top bar drawn on every page.""" def __init__(self, right_text=''): super().__init__() self._rt = right_text def wrap(self, aw, ah): return 0, 0 def draw(self): c = self.canv w = PAGE_W c.saveState() c.setFillColor(ESICM_BLUE) c.rect(0, PAGE_H-38, w, 38, stroke=0, fill=1) # ESICM wordmark c.setFillColor(WHITE) c.setFont('Helvetica-Bold', 14) c.drawString(L_MARGIN, PAGE_H-22, 'ESICM') c.setFont('Helvetica', 8.5) c.drawString(L_MARGIN, PAGE_H-32, 'European Society of Intensive Care Medicine') # Centre title title = 'EDIC Part II — Study Guide' c.setFont('Helvetica-Bold', 11) tw = c.stringWidth(title, 'Helvetica-Bold', 11) c.drawString((w-tw)/2, PAGE_H-20, title) sub = '12 Most Common Examiner Traps' c.setFont('Helvetica', 8) sw = c.stringWidth(sub, 'Helvetica', 8) c.drawString((w-sw)/2, PAGE_H-31, sub) # Gold accent line c.setStrokeColor(GOLD) c.setLineWidth(2.5) c.line(0, PAGE_H-40, w, PAGE_H-40) # Footer c.setStrokeColor(MID_GREY) c.setLineWidth(0.5) c.line(L_MARGIN, B_MARGIN-2, w-R_MARGIN, B_MARGIN-2) c.setFillColor(DARK_GREY) c.setFont('Helvetica', 7.5) c.drawString(L_MARGIN, B_MARGIN-12, 'EDIC Part II Study Guide • July 2026 • Based on ESICM ARDS 2023 / SSC 2021 / ATS 2024') pg = str(c._pageNumber) if hasattr(c, '_pageNumber') else '' c.restoreState() class ColourBanner(Flowable): def __init__(self, text, bg=ESICM_BLUE, fg=WHITE, height=18, font='Helvetica-Bold', font_size=10, left_pad=8): super().__init__() self._t, self._bg, self._fg = text, bg, fg self._h, self._f, self._fs, self._lp = height, font, font_size, left_pad def wrap(self, aw, ah): self.width, self.height = aw, self._h return self.width, self.height def draw(self): c = self.canv c.setFillColor(self._bg) c.rect(0, 0, self.width, self.height, stroke=0, fill=1) c.setFillColor(self._fg) c.setFont(self._f, self._fs) c.drawString(self._lp, (self.height-self._fs)/2 + 1, self._t) def make_header_footer(): def _draw(canv, doc): canv.saveState() w = PAGE_W canv.setFillColor(ESICM_BLUE) canv.rect(0, PAGE_H-38, w, 38, stroke=0, fill=1) canv.setFillColor(WHITE) canv.setFont('Helvetica-Bold', 14) canv.drawString(L_MARGIN, PAGE_H-22, 'ESICM') canv.setFont('Helvetica', 8.5) canv.drawString(L_MARGIN, PAGE_H-32, 'European Society of Intensive Care Medicine') title = 'EDIC Part II — Study Guide' canv.setFont('Helvetica-Bold', 11) tw = canv.stringWidth(title, 'Helvetica-Bold', 11) canv.drawString((w-tw)/2, PAGE_H-20, title) sub = '12 Most Common Examiner Traps' canv.setFont('Helvetica', 8) sw = canv.stringWidth(sub, 'Helvetica', 8) canv.drawString((w-sw)/2, PAGE_H-31, sub) canv.setStrokeColor(GOLD) canv.setLineWidth(2.5) canv.line(0, PAGE_H-40, w, PAGE_H-40) canv.setStrokeColor(MID_GREY) canv.setLineWidth(0.5) canv.line(L_MARGIN, B_MARGIN-2, w-R_MARGIN, B_MARGIN-2) canv.setFillColor(DARK_GREY) canv.setFont('Helvetica', 7.5) canv.drawString(L_MARGIN, B_MARGIN-12, 'EDIC Part II Study Guide • July 2026 • ESICM ARDS 2023 / SSC 2021 / ATS 2024') canv.setFont('Helvetica-Bold', 8) pg = str(doc.page) pw = canv.stringWidth(pg, 'Helvetica-Bold', 8) canv.setFillColor(ESICM_BLUE) canv.drawString(w-R_MARGIN-pw, B_MARGIN-12, pg) canv.restoreState() return _draw, _draw # ── Trap card builder ───────────────────────────────────────────────────────── TRAP_COLOURS = [ (RED_DARK, RED_LIGHT), # 1 (ESICM_BLUE, ESICM_LIGHT), # 2 (RED_DARK, RED_LIGHT), # 3 (AMBER_DARK, AMBER_LIGHT), # 4 (ESICM_BLUE, ESICM_LIGHT), # 5 (GREEN_DARK, GREEN_LIGHT), # 6 (RED_DARK, RED_LIGHT), # 7 (PURPLE_DARK, PURPLE_LIGHT), # 8 (AMBER_DARK, AMBER_LIGHT), # 9 (ESICM_BLUE, ESICM_LIGHT), # 10 (GREEN_DARK, GREEN_LIGHT), # 11 (RED_DARK, RED_LIGHT), # 12 ] CATEGORY_COLOURS = { 'VENTILATION': (ESICM_BLUE, WHITE), 'DIAGNOSIS': (RED_DARK, WHITE), 'HAEMODYNAMICS': (AMBER_DARK, WHITE), 'PHARMACOLOGY': (GREEN_DARK, WHITE), 'PHYSIOLOGY': (PURPLE_DARK, WHITE), 'PROGNOSIS': (colors.HexColor('#00695C'), WHITE), } def trap_card(number, category, title, subtitle, wrong, right, why, probe, ref): """Build one complete trap card.""" header_bg, _ = TRAP_COLOURS[number-1] cat_bg, cat_fg = CATEGORY_COLOURS.get(category, (ESICM_TEAL, WHITE)) story = [] # ── Card header: number + category tag + title ──────────────────────────── num_cell = Table( [[Paragraph(str(number), STYLES['trap_num'])]], colWidths=[14*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1), header_bg), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1),0), ('BOTTOMPADDING',(0,0),(-1,-1),0), ]) ) # Category pill + title block cat_pill = Table( [[Paragraph(category, STYLES['tag'])]], colWidths=[26*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1), cat_bg), ('TOPPADDING',(0,0),(-1,-1),2), ('BOTTOMPADDING',(0,0),(-1,-1),2), ('LEFTPADDING',(0,0),(-1,-1),4), ('RIGHTPADDING',(0,0),(-1,-1),4), ('ROUNDEDCORNERS',(0,0),(-1,-1),3), ]) ) title_cell = Table( [[cat_pill], [Paragraph(title, STYLES['trap_title'])], [Paragraph(subtitle, STYLES['trap_sub'])]], colWidths=[AVAIL_W - 14*mm - 6*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1), header_bg), ('TOPPADDING',(0,0),(-1,-1),2), ('BOTTOMPADDING',(0,0),(-1,-1),2), ('LEFTPADDING',(0,0),(-1,-1),8), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ]) ) header = Table( [[num_cell, title_cell]], colWidths=[14*mm, AVAIL_W - 14*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1), header_bg), ('LEFTPADDING',(0,0),(-1,-1),0), ('RIGHTPADDING',(0,0),(-1,-1),0), ('TOPPADDING',(0,0),(-1,-1),6), ('BOTTOMPADDING',(0,0),(-1,-1),6), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ]) ) story.append(header) # ── Body: three columns ─────────────────────────────────────────────────── # Wrong / Right / Why wrong_cell = [ [Paragraph('✗ WHAT CANDIDATES SAY', STYLES['wrong_hdr'])], [Paragraph(wrong, STYLES['wrong_body'])], ] right_cell = [ [Paragraph('✓ CORRECT ANSWER', STYLES['right_hdr'])], [Paragraph(right, STYLES['right_body'])], ] why_cell = [ [Paragraph('▶ WHY THIS MATTERS', STYLES['why_hdr'])], [Paragraph(why, STYLES['why_body'])], ] col_w = (AVAIL_W - 4*mm) / 3 wrong_t = Table(wrong_cell, colWidths=[col_w], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1), RED_LIGHT), ('BOX',(0,0),(-1,-1),0.5, RED_MID), ('TOPPADDING',(0,0),(-1,-1),5), ('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),6), ('RIGHTPADDING',(0,0),(-1,-1),6), ('VALIGN',(0,0),(-1,-1),'TOP'), ])) right_t = Table(right_cell, colWidths=[col_w], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1), GREEN_LIGHT), ('BOX',(0,0),(-1,-1),0.5, GREEN_MID), ('TOPPADDING',(0,0),(-1,-1),5), ('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),6), ('RIGHTPADDING',(0,0),(-1,-1),6), ('VALIGN',(0,0),(-1,-1),'TOP'), ])) why_t = Table(why_cell, colWidths=[col_w], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1), AMBER_LIGHT), ('BOX',(0,0),(-1,-1),0.5, AMBER_MID), ('TOPPADDING',(0,0),(-1,-1),5), ('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),6), ('RIGHTPADDING',(0,0),(-1,-1),6), ('VALIGN',(0,0),(-1,-1),'TOP'), ])) body_row = Table([[wrong_t, Spacer(2*mm,1), right_t, Spacer(2*mm,1), why_t]], colWidths=[col_w, 2*mm, col_w, 2*mm, col_w], style=TableStyle([ ('VALIGN',(0,0),(-1,-1),'TOP'), ('LEFTPADDING',(0,0),(-1,-1),0), ('RIGHTPADDING',(0,0),(-1,-1),0), ('TOPPADDING',(0,0),(-1,-1),0), ('BOTTOMPADDING',(0,0),(-1,-1),0), ])) story.append(body_row) # ── Examiner probe + reference ──────────────────────────────────────────── probe_cell = [[Paragraph('🎙 EXAMINER WILL ASK:', STYLES['probe_hdr'])], [Paragraph(probe, STYLES['probe_body'])]] ref_cell = [[Paragraph(f'📋 {ref}', STYLES['ref'])]] footer_data = [[ Table(probe_cell, colWidths=[AVAIL_W * 0.62], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1), TEAL_LIGHT), ('BOX',(0,0),(-1,-1),0.5, ESICM_TEAL), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),6), ])), Spacer(3*mm, 1), Table(ref_cell, colWidths=[AVAIL_W * 0.36], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1), LIGHT_GREY), ('BOX',(0,0),(-1,-1),0.5, MID_GREY), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),6), ])), ]] footer_row = Table(footer_data, colWidths=[AVAIL_W*0.62, 3*mm, AVAIL_W*0.36], style=TableStyle([ ('VALIGN',(0,0),(-1,-1),'TOP'), ('LEFTPADDING',(0,0),(-1,-1),0), ('RIGHTPADDING',(0,0),(-1,-1),0), ('TOPPADDING',(0,0),(-1,-1),0), ('BOTTOMPADDING',(0,0),(-1,-1),0), ])) story.append(footer_row) story.append(Spacer(1, 10)) return KeepTogether(story) # ══ The 12 Traps Data ═════════════════════════════════════════════════════════ TRAPS = [ dict( number=1, category='VENTILATION', title='IBW vs Actual Body Weight for Tidal Volume', subtitle='The single most common calculation error in EDIC Part II', wrong=( '"I will set the tidal volume at 6 mL/kg... of actual body weight. ' 'This patient is 92 kg so I\'ll use 552 mL."' ), right=( 'ALWAYS use Predicted (Ideal) Body Weight (IBW):\n' 'Male IBW = 50 + 0.91 × (height cm - 152.4)\n' 'Female IBW = 45.5 + 0.91 × (height cm - 152.4)\n' 'Target Vt ≤ 6 mL/kg IBW, acceptable range 4-8 mL/kg IBW.\n' 'For a 175 cm male: IBW = 70.6 kg → Vt ≤ 424 mL.' ), why=( 'The ARMA trial (2000) enrolled patients using IBW because lung ' 'volume correlates with height, NOT weight. Using actual body weight ' 'in an obese patient delivers a VILI-inducing tidal volume. ' 'This error directly causes patient harm and is an automatic fail item.' ), probe='"Calculate the ideal tidal volume for a 170 cm female patient weighing 110 kg."', ref='ARMA Trial, NEJM 2000; ESICM ARDS 2023 Strong Recommendation' ), dict( number=2, category='DIAGNOSIS', title='Using Obsolete ARDS Definitions (AECC 1994)', subtitle='Referring to "ALI" or quoting P/F < 200 as the ARDS threshold', wrong=( '"This patient has ALI because the P/F ratio is between 200 and 300." ' 'OR: "ARDS requires a P/F < 200 on mechanical ventilation."' ), right=( 'Use Berlin 2012 / ESICM 2023 definitions ONLY.\n' 'Severity by P/F (on PEEP ≥ 5 cmH₂O):\n' '• Mild: P/F 201-300 mmHg\n' '• Moderate: P/F 101-200 mmHg\n' '• Severe: P/F ≤ 100 mmHg\n' 'ESICM 2023 NEW: SpO₂/FiO₂ ≤ 315 (SpO₂ ≤ 97%) acceptable. ' 'Lung USS now valid imaging. HFNC/NIV accepted for mild/moderate.' ), why=( '"ALI" (Acute Lung Injury) was retired by the Berlin Definition in 2012. ' 'Using it signals outdated knowledge. ESICM 2023 further expanded the ' 'definition to include SpO₂/FiO₂ and non-invasive respiratory support — ' 'this is a high-frequency examiner update question.' ), probe='"What changed in the ESICM 2023 ARDS definition compared to Berlin 2012?"', ref='Berlin Def: Ranieri JAMA 2012; ESICM 2023: Grasselli ICM 2023;49:727' ), dict( number=3, category='VENTILATION', title='Increasing RR to Fix Hypercapnia in ARDS', subtitle='Chasing a "normal" PaCO₂ at the cost of VILI and auto-PEEP', wrong=( '"The pCO₂ is 55 mmHg and pH is 7.28 — I will increase the respiratory ' 'rate from 18 to 28/min to correct the CO₂."' ), right=( 'Accept permissive hypercapnia to maintain lung-protective ventilation:\n' '• Target pH ≥ 7.20 (some accept ≥ 7.15 in refractory cases)\n' '• PaCO₂ up to 60-70 mmHg is acceptable\n' '• Increasing RR: risks auto-PEEP, increases mechanical power, ' 'worsens dynamic hyperinflation\n' '• Treat the pH with NaHCO₃ if needed (pH < 7.20)\n' '• Contraindication: raised ICP (avoid hypercapnia)' ), why=( 'Mechanical power = 0.098 × RR × Vt × DP. Doubling RR doubles ' 'mechanical power and may cause auto-PEEP — which increases total PEEP, ' 'reduces venous return, and can cause haemodynamic collapse. ' 'The ARMA trial showed that permissive hypercapnia is safe and necessary ' 'for lung protection.' ), probe='"If you increase the RR from 18 to 28, what happens to the flow-time curve? ' 'What is auto-PEEP and how would you detect it?"', ref='ARMA NEJM 2000; Gattinoni MP formula ICM 2016; ESICM ARDS 2023' ), dict( number=4, category='HAEMODYNAMICS', title='Giving More Fluids to a Patient with High EVLWI', subtitle='Treating every shocked ARDS patient with fluid boluses', wrong=( '"The patient has septic shock and lactate is elevated — I will give ' 'a 500 mL fluid challenge to improve perfusion."' ), right=( 'After initial resuscitation, assess preload status:\n' '• EVLWI > 10 mL/kg = pulmonary flooding — fluids WORSEN ARDS\n' '• Use dynamic indices (PPV, SVV, PLR) rather than CVP\n' '• FACTT trial: conservative fluid strategy → more VFDs, ' 'no mortality difference\n' '• SSC 2021: no routine fluid boluses; reassess after 1-2 L initial\n' '• Target: EVLWI reduction with diuretics/vasopressors once stable' ), why=( 'The FACTT trial (NEJM 2006) compared conservative vs liberal fluid ' 'strategy in ARDS: conservative group had 2.5 more ventilator-free days. ' 'Each 1 mL/kg increase in EVLWI increases ICU mortality by ~5%. ' 'Septic shock with ARDS requires vasopressors, not volume.' ), probe='"The EVLWI is 18 mL/kg and CVP is 18 mmHg. The lactate is 4.1. ' 'Do you give fluids? What guides your decision?"', ref='FACTT Trial NEJM 2006; SSC 2021; ESICM ARDS 2023' ), dict( number=5, category='VENTILATION', title='Recommending Aggressive Recruitment Manoeuvres (ART Protocol)', subtitle='Applying sustained inflation RM to 40-60 cmH₂O in severe ARDS', wrong=( '"I will perform a recruitment manoeuvre using stepwise PEEP increases ' 'up to 40-50 cmH₂O (staircase RM) to re-open collapsed alveoli."' ), right=( 'ESICM 2023: STRONG recommendation AGAINST sustained inflation ' 'recruitment manoeuvres (40 cmH₂O for 40 seconds or staircase RM).\n' '• ART trial (Lima, JAMA 2017): staircase RM increased 28-day mortality\n' '• Acceptable alternative: brief CPAP 30-35 cmH₂O for ≤ 30s ' '(controversial, not routinely recommended)\n' '• Best evidence: optimise PEEP by decremental trial to best compliance\n' '• Prone positioning is the most evidence-based "recruitment" strategy' ), why=( 'The ART trial randomised patients to aggressive RM + PEEP titration vs ' 'standard care. The aggressive RM group had HIGHER 28-day mortality (55.3% ' 'vs 49.3%, p=0.04). The mechanism: haemodynamic compromise and ' 'barotrauma from sustained supraphysiological pressures.' ), probe='"You mention recruitment — which type and at what pressure? ' 'What did the ART trial show and why?"', ref='ART Trial: Lima JAMA 2017; ESICM ARDS 2023 Strong Rec AGAINST RM' ), dict( number=6, category='PHARMACOLOGY', title='Stating iNO or Steroids Reduce Mortality in ARDS', subtitle='Overselling salvage therapies without nuance', wrong=( '"I will start inhaled nitric oxide because it improves oxygenation ' 'and reduces mortality in ARDS." OR "Steroids improve survival in ARDS."' ), right=( 'iNO: improves oxygenation transiently (~10-15 mmHg P/F improvement) ' 'but does NOT reduce mortality (Cochrane 2016 — 13 RCTs). ' 'Use as rescue/bridge to ECMO or for ACP-related RV failure.\n\n' 'Corticosteroids: reduce ventilator days (DEXA-ARDS 2020, Villar JAMA). ' 'Mortality benefit only in early ARDS (< 14 days). ' 'No benefit (possible harm) if started late (> 14 days). ' 'ESICM 2023: conditional recommendation.' ), why=( 'Examiners specifically test whether candidates can distinguish ' '"improves oxygenation" from "improves survival." iNO consistently ' 'improves P/F but NEVER reduces mortality. Corticosteroids reduce ' 'ventilator days in early ARDS but the mortality signal is weaker. ' 'Citing them as mortality-reducing = fails this task.' ), probe='"iNO improved the P/F by 25 mmHg. Should you continue it? ' 'What is the mechanism and what does the Cochrane evidence say?"', ref='iNO Cochrane: Adhikari 2016; DEXA-ARDS: Villar JAMA 2020; ESICM ARDS 2023' ), dict( number=7, category='VENTILATION', title='Ignoring Auto-PEEP on Ventilator Graphics', subtitle='Not recognising incomplete expiratory flow return on the flow-time curve', wrong=( '"The set PEEP is 10 cmH₂O so the total PEEP is 10 cmH₂O." ' 'OR: "The flow-time curve looks normal to me."' ), right=( 'Always perform an expiratory hold manoeuvre to measure total PEEP.\n' 'Total PEEP = set PEEP + auto-PEEP (intrinsic PEEP).\n' 'On flow-time curve: if expiratory flow does NOT return to zero ' 'before next breath → dynamic hyperinflation / auto-PEEP.\n' 'Consequences: reduced venous return, haemodynamic instability, ' 'overestimated driving pressure if auto-PEEP not accounted for.\n' 'Rx: ↓ RR, ↑ expiratory time (lower I:E ratio), ↓ Vt, bronchodilate.' ), why=( 'Auto-PEEP adds to applied PEEP and causes lung overdistension and ' 'haemodynamic compromise. In ARDS patients on high RR, auto-PEEP ' 'is common and frequently missed. The driving pressure calculated as ' '(Pplat - set PEEP) is INCORRECT if auto-PEEP exists — must use ' 'Pplat - TOTAL PEEP.' ), probe='"Show me how you would detect and measure auto-PEEP at the bedside. ' 'What do you do with the RR if the patient is hypercapnic AND has auto-PEEP?"', ref='Slutsky & Ranieri NEJM 2013; ESICM ARDS 2023 (ventilator graphics section)' ), dict( number=8, category='PHYSIOLOGY', title='Confusing ACP D-sign with Pulmonary Embolism', subtitle='Ordering CTPA for a D-sign in an established ARDS patient', wrong=( '"The echo shows a D-sign with RV dilation — this is pulmonary embolism. ' 'I will request urgent CTPA and start anticoagulation."' ), right=( 'D-sign (septal flattening) causes:\n' '• Pulmonary embolism (PE) — McConnell sign: RV free wall akinesia ' 'with preserved apex\n' '• Acute cor pulmonale (ACP) in ARDS — due to high airway pressures, ' 'hypoxia, hypercapnia, acidosis\n' 'In ARDS context: ACP is far more likely (25-50% of severe ARDS).\n' 'ACP management: ↓ Pplat, prone positioning, iNO for PVR reduction, ' 'norepinephrine, avoid fluids, correct hypoxia/hypercapnia/acidosis.' ), why=( 'Misdiagnosing ACP as PE leads to: unnecessary contrast exposure, ' 'transport of a critically ill patient for CT, and inappropriate ' 'anticoagulation in a coagulopathic patient. ' 'ACP complicates ~25-50% of severe ARDS and is independently associated ' 'with mortality (Vieillard-Baron ICM 2016).' ), probe='"How do you distinguish ACP from PE echocardiographically? ' 'Would you anticoagulate this ARDS patient with a D-sign?"', ref='Vieillard-Baron ACP in ARDS ICM 2016; McConnell sign Am J Cardiol 1996' ), dict( number=9, category='HAEMODYNAMICS', title='Using CVP as the Sole Guide for Fluid Management', subtitle='Treating CVP < 8 as hypovolaemia requiring fluid loading', wrong=( '"The CVP is 6 mmHg — this is low, indicating the patient needs ' 'fluid resuscitation. I will give 500 mL crystalloid."' ), right=( 'CVP is a POOR predictor of fluid responsiveness (meta-analysis: ' 'AUC 0.56 — no better than coin flip). Do NOT use CVP to guide fluids.\n' 'Use dynamic preload indicators:\n' '• Pulse pressure variation (PPV) > 13% → fluid responsive\n' '• Stroke volume variation (SVV) > 10-15%\n' '• Passive leg raise (PLR) + cardiac output measurement\n' '• GEDVI (PiCCO) normal range 680-800 mL/m²\n' 'In ARDS: CVP elevation often reflects RV failure or high PEEP, ' 'NOT hypervolaemia.' ), why=( 'Marik et al. (Chest 2008): CVP does not predict fluid responsiveness ' '(r = 0.18). A high CVP in ARDS reflects high intrathoracic pressure ' 'and RV failure. Giving fluids to a CVP of 6 in ARDS with EVLWI 15 ' 'will flood the lungs further.' ), probe='"CVP is 5 mmHg and MAP is 58 — how do you assess fluid responsiveness ' 'in this ventilated ARDS patient?"', ref='Marik PE Chest 2008; SSC 2021 (dynamic over static markers); ' 'FACTT NEJM 2006' ), dict( number=10, category='VENTILATION', title='Stating NMB Is Routinely Indicated for All ARDS', subtitle='Overlooking the ROSE trial and ESICM 2023 conditional recommendation', wrong=( '"All patients with ARDS should receive neuromuscular blockade for ' '48 hours — this is proven to reduce mortality by the ACURASYS trial."' ), right=( 'ESICM 2023 & ATS 2024: AGAINST routine NMB for all ARDS.\n' 'Selective use: P/F < 150 with inability to achieve lung-protective ' 'ventilation, or severe P-SILI (patient self-inflicted lung injury).\n' 'ACURASYS (2010): showed benefit — but control arm used deep sedation.\n' 'ROSE (2019): NO benefit when light sedation used in control arm.\n' 'Reconciliation: NMB benefit is via preventing P-SILI and facilitating ' 'LPV — if already deeply sedated with no effort, NMB adds little.\n' 'Always use TOF monitoring; max 48h duration.' ), why=( 'The ROSE trial (Moss NEJM 2019) contradicted ACURASYS (Papazian 2010). ' 'Understanding both trials and their differences is a standard EDIC probe. ' 'Stating NMB "reduces mortality" without nuance = examiner challenge. ' 'Must also know: ICU-acquired weakness risk, TOF monitoring, duration.' ), probe='"The ROSE trial contradicts ACURASYS. How do you reconcile them? ' 'When exactly would you use NMB in ARDS today?"', ref='ACURASYS: Papazian NEJM 2010; ROSE: Moss NEJM 2019; ESICM ARDS 2023' ), dict( number=11, category='PHYSIOLOGY', title='Driving Pressure: Not Calculating or Not Targeting', subtitle='Focusing only on plateau pressure while ignoring compliance-adjusted stress', wrong=( '"Plateau pressure is 28 cmH₂O which is under 30 — ventilation is safe." ' '(Ignores driving pressure of 22 cmH₂O on PEEP 6.)' ), right=( 'Driving pressure (DP) = Plateau pressure - PEEP = Vt / Crs\n' 'Target DP < 15 cmH₂O (ESICM 2023).\n' 'DP represents stress on the "baby lung" — the aerated fraction.\n' 'Pplat < 30 with PEEP 6 → DP = 24 = UNSAFE despite "safe" Pplat.\n' 'Amato 2015: DP was the STRONGEST independent predictor of ARDS ' 'mortality across 9 RCTs (3562 patients).\n' 'To reduce DP: ↓ Vt, ↑ PEEP (if recruitable), prone positioning.' ), why=( 'Amato et al. (NEJM 2015) performed a landmark mediation analysis showing ' 'driving pressure mediated most of the survival benefit of lung-protective ' 'ventilation. A DP > 15 cmH₂O is associated with a 41% increase in ' 'mortality hazard per 1 cmH₂O increase. This is now a core EDIC topic.' ), probe='"Plateau pressure is 29, PEEP is 5. What is the driving pressure and ' 'is it acceptable? What does the Amato 2015 paper say?"', ref='Amato MBA et al. Driving pressure and survival. NEJM 2015;372:747-55' ), dict( number=12, category='PROGNOSIS', title='Stating EOLIA "Proved VV-ECMO Works" for ARDS', subtitle='Misrepresenting a trial that did NOT meet its primary endpoint', wrong=( '"The EOLIA trial proved that VV-ECMO reduces mortality in severe ARDS. ' 'This patient should be referred for ECMO immediately."' ), right=( 'EOLIA (Combes, NEJM 2018):\n' '• Primary endpoint (60-day mortality): 35% ECMO vs 46% control — ' 'did NOT reach statistical significance (p=0.09)\n' '• 28% of control arm received rescue ECMO — crossover contamination\n' '• Bayesian reanalysis: 96% probability of benefit\n' '• ESICM 2023: conditional recommendation for VV-ECMO in refractory ' 'severe ARDS (P/F < 80 for 3h, or < 80 for 6h, or pH < 7.25 despite LPV)\n' 'Correct language: "EOLIA suggests VV-ECMO may be beneficial, and ESICM ' '2023 gives a conditional recommendation based on totality of evidence."' ), why=( 'Examiners will specifically probe EOLIA because candidates routinely ' 'overstate its conclusions. Saying the trial "proved" ECMO works when ' 'the primary endpoint was not met demonstrates inability to critically ' 'appraise evidence — a core EDIC Part II competency. ' 'Must know EOLIA criteria for ECMO referral.' ), probe='"The EOLIA trial: what was the primary outcome, did it meet its endpoint, ' 'and why do we still use ECMO? What are the EOLIA inclusion criteria?"', ref='EOLIA: Combes NEJM 2018;378:1965; ESICM ARDS 2023 conditional rec for ECMO' ), ] # ══ Summary Cheat-Sheet Table ═════════════════════════════════════════════════ def summary_table(): hdrs = ['#', 'Trap', 'Category', 'Key Phrase to Use', 'Avoid Saying'] rows = [ ['1', 'IBW vs ABW', 'Ventilation', '"IBW = 50 + 0.91 × (ht - 152.4) for males"', '"6 mL/kg actual weight"'], ['2', 'AECC vs Berlin/ESICM 2023', 'Diagnosis', '"Berlin 2012 / ESICM 2023: mild/mod/severe"', '"ALI" or "P/F < 200 = ARDS"'], ['3', 'Permissive hypercapnia', 'Ventilation', '"Accept pH ≥ 7.20, PaCO₂ 60-70 mmHg"', '"I will increase RR to normalise CO₂"'], ['4', 'EVLWI + fluid strategy', 'Haemodynamics', '"Conservative fluid, EVLWI-guided"', '"Give 500 mL bolus for lactate elevation"'], ['5', 'Recruitment manoeuvres', 'Ventilation', '"AGAINST sustained inflation RM (ART trial)"', '"Staircase RM to 50 cmH₂O"'], ['6', 'iNO/steroid mortality', 'Pharmacology', '"iNO improves oxygenation NOT mortality"', '"iNO/steroids reduce ARDS mortality"'], ['7', 'Auto-PEEP', 'Ventilation', '"Expiratory hold; total PEEP = set + auto"', '"PEEP on monitor = total PEEP"'], ['8', 'D-sign = ACP not PE', 'Physiology', '"ACP in ARDS context; McConnell sign for PE"', '"Order CTPA; start anticoagulation"'], ['9', 'CVP as fluid guide', 'Haemodynamics', '"Use PPV/SVV/PLR; CVP AUC 0.56"', '"CVP < 8 = give fluids"'], ['10', 'Routine NMB in ARDS', 'Ventilation', '"Selective use P/F < 150; ROSE vs ACURASYS"', '"All ARDS → cisatracurium 48h"'], ['11', 'Driving pressure', 'Physiology', '"DP = Pplat - PEEP; target < 15; Amato 2015"', '"Pplat < 30 → safe ventilation"'], ['12', 'EOLIA misinterpretation', 'Prognosis', '"Did not meet p<0.05; conditional rec ECMO"', '"EOLIA proved ECMO works"'], ] data = [[Paragraph(str(c), ParagraphStyle('sh', fontName='Helvetica-Bold' if r==0 else 'Helvetica', fontSize=8 if r>0 else 8.5, textColor=WHITE if r==0 else BLACK, leading=11)) for c in row] for r, row in enumerate([hdrs]+rows)] cws = [8*mm, 30*mm, 22*mm, 55*mm, 55*mm] t = Table(data, colWidths=cws, style=TableStyle([ ('BACKGROUND',(0,0),(-1,0), ESICM_BLUE), ('ROWBACKGROUNDS',(0,1),(-1,-1), [WHITE, TABLE_ALT]), ('GRID',(0,0),(-1,-1), 0.4, MID_GREY), ('VALIGN',(0,0),(-1,-1), 'TOP'), ('TOPPADDING',(0,0),(-1,-1), 3), ('BOTTOMPADDING',(0,0),(-1,-1), 3), ('LEFTPADDING',(0,0),(-1,-1), 4), ('FONTNAME',(0,0),(-1,0), 'Helvetica-Bold'), ])) return t # ══ Build PDF ═════════════════════════════════════════════════════════════════ def build(out_path): doc = SimpleDocTemplate( out_path, pagesize=A4, leftMargin=L_MARGIN, rightMargin=R_MARGIN, topMargin=T_MARGIN+32, bottomMargin=B_MARGIN+14, title='EDIC Part II Study Guide — 12 Most Common Examiner Traps', author='ESICM EDIC') story = [] add = story.append # ── Cover / Introduction ────────────────────────────────────────────────── add(ColourBanner('EDIC Part II — The 12 Examiner Traps', bg=ESICM_BLUE, height=28, font_size=14)) add(Spacer(1, 4)) add(ColourBanner( 'Type 1 Respiratory Failure • ARDS • Lung-Protective Ventilation • Sepsis', bg=ESICM_TEAL, height=14, font_size=8)) add(Spacer(1, 8)) intro = ( 'This study guide distils the <b>12 most common errors</b> that cause candidates to fail the EDIC ' 'Part II Clinical Case Scenario (CCS) station on respiratory failure and ARDS. ' 'Each trap is presented as a three-panel card: what candidates typically say incorrectly, ' 'what the correct answer is, and why it matters clinically. ' 'The <i>Examiner Will Ask</i> box reflects real probing questions used in EDIC Part II stations. ' 'All content is aligned to the <b>ESICM ARDS Guidelines 2023</b>, ' '<b>ATS ARDS Clinical Practice Guidelines 2024</b>, and ' '<b>Surviving Sepsis Campaign 2021</b>.' ) add(Paragraph(intro, STYLES['intro'])) add(Spacer(1, 8)) # Quick colour legend legend_data = [[ Table([[Paragraph('VENTILATION', STYLES['tag'])]], colWidths=[28*mm], style=TableStyle([('BACKGROUND',(0,0),(-1,-1),ESICM_BLUE), ('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3)])), Table([[Paragraph('DIAGNOSIS', STYLES['tag'])]], colWidths=[24*mm], style=TableStyle([('BACKGROUND',(0,0),(-1,-1),RED_DARK), ('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3)])), Table([[Paragraph('HAEMODYNAMICS', STYLES['tag'])]], colWidths=[32*mm], style=TableStyle([('BACKGROUND',(0,0),(-1,-1),AMBER_DARK), ('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3)])), Table([[Paragraph('PHARMACOLOGY', STYLES['tag'])]], colWidths=[30*mm], style=TableStyle([('BACKGROUND',(0,0),(-1,-1),GREEN_DARK), ('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3)])), Table([[Paragraph('PHYSIOLOGY', STYLES['tag'])]], colWidths=[26*mm], style=TableStyle([('BACKGROUND',(0,0),(-1,-1),PURPLE_DARK), ('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3)])), Table([[Paragraph('PROGNOSIS', STYLES['tag'])]], colWidths=[26*mm], style=TableStyle([('BACKGROUND',(0,0),(-1,-1),colors.HexColor('#00695C')), ('TOPPADDING',(0,0),(-1,-1),3),('BOTTOMPADDING',(0,0),(-1,-1),3)])), ]] legend_t = Table(legend_data, colWidths=[28*mm,24*mm,32*mm,30*mm,26*mm,26*mm], style=TableStyle([ ('LEFTPADDING',(0,0),(-1,-1),3),('RIGHTPADDING',(0,0),(-1,-1),3), ('TOPPADDING',(0,0),(-1,-1),0),('BOTTOMPADDING',(0,0),(-1,-1),0), ])) add(Paragraph('<b>Category key:</b>', STYLES['body'])) add(Spacer(1,3)) add(legend_t) add(Spacer(1, 10)) add(HRFlowable(width='100%', thickness=1, color=GOLD)) add(Spacer(1, 8)) # ── Trap cards ──────────────────────────────────────────────────────────── for trap in TRAPS: add(trap_card(**trap)) # ── Summary Table ───────────────────────────────────────────────────────── add(PageBreak()) add(ColourBanner('Quick Reference Summary — All 12 Traps at a Glance', bg=ESICM_BLUE, height=18, font_size=10)) add(Spacer(1, 6)) add(summary_table()) add(Spacer(1, 10)) # ── Key Formulae Box ────────────────────────────────────────────────────── add(ColourBanner('Essential Formulae & Thresholds to Memorise', bg=ESICM_TEAL, height=16, font_size=9)) add(Spacer(1, 4)) formulae = [ ['Formula / Threshold', 'Value', 'Source'], ['IBW (male)', '50 + 0.91 × (height cm - 152.4) kg', 'ARMA 2000'], ['IBW (female)', '45.5 + 0.91 × (height cm - 152.4) kg', 'ARMA 2000'], ['Tidal Volume target', '≤ 6 mL/kg IBW (range 4-8 mL/kg)', 'ARMA; ESICM 2023'], ['Plateau pressure target', '< 30 cmH₂O', 'ESICM 2023 Strong Rec'], ['Driving pressure target', '< 15 cmH₂O', 'Amato 2015; ESICM 2023'], ['PEEP (severe ARDS)', 'Higher PEEP strategy (titrate to best Crs)', 'ESICM 2023 Conditional'], ['P/F ratio: Severe ARDS', '≤ 100 mmHg on PEEP ≥ 5 cmH₂O', 'Berlin 2012'], ['SpO₂/FiO₂ ARDS threshold', '≤ 315 when SpO₂ ≤ 97%', 'ESICM 2023 NEW'], ['Prone positioning indication', 'P/F < 150 mmHg (Strong Rec)', 'ESICM 2023; PROSEVA'], ['Prone duration per session', '≥ 16 hours', 'PROSEVA 2013'], ['Permissive hypercapnia', 'PaCO₂ 60-70 mmHg; pH ≥ 7.20', 'ARMA; standard practice'], ['Mechanical power formula', '0.098 × RR × Vt(L) × (DP + PEEP) J/min', 'Gattinoni ICM 2016'], ['Mechanical power threshold', '> 17 J/min associated with VILI', 'Serpa Neto 2018'], ['EVLWI normal', '< 10 mL/kg', 'PiCCO reference range'], ['PVPI normal', '< 3.0', 'PiCCO reference range'], ['VV-ECMO indication (EOLIA)', 'P/F < 80 ×3h, < 80 ×6h, or pH < 7.25 ×6h', 'EOLIA trial 2018'], ['NMB duration', 'Max 48 hours; selective use P/F < 150', 'ESICM 2023; ROSE 2019'], ['SpO₂ target (ARDS)', '92-96% (avoid hyperoxia)', 'ESICM 2023; ICU-ROX'], ['Transfusion threshold (ARDS)', 'Hb ≥ 7 g/dL (8-9 if oxygen-limited)', 'SSC 2021; TRICC'], ['Steroid timing in ARDS', 'Early (< 14 days); NOT late-phase', 'DEXA-ARDS; ESICM 2023'], ] fw = AVAIL_W form_t = Table( [[Paragraph(str(c), ParagraphStyle('fh', fontName='Helvetica-Bold' if r==0 else 'Helvetica', fontSize=8, textColor=WHITE if r==0 else BLACK, leading=11)) for c in row] for r, row in enumerate(formulae)], colWidths=[fw*0.42, fw*0.38, fw*0.20], style=TableStyle([ ('BACKGROUND',(0,0),(-1,0), ESICM_BLUE), ('ROWBACKGROUNDS',(0,1),(-1,-1), [WHITE, TABLE_ALT]), ('GRID',(0,0),(-1,-1), 0.4, MID_GREY), ('VALIGN',(0,0),(-1,-1), 'MIDDLE'), ('TOPPADDING',(0,0),(-1,-1), 3), ('BOTTOMPADDING',(0,0),(-1,-1), 3), ('LEFTPADDING',(0,0),(-1,-1), 5), ]) ) add(form_t) add(Spacer(1, 8)) add(Paragraph( '<i>Guide prepared by ESICM EDIC Preparation Series • July 2026 • ' 'Based on ESICM ARDS Guidelines 2023, ATS 2024, Surviving Sepsis Campaign 2021, ' 'and landmark RCTs (ARMA, PROSEVA, ACURASYS, ROSE, EOLIA, DEXA-ARDS, ART, FACTT).</i>', STYLES['footer'])) first, later = make_header_footer() doc.build(story, onFirstPage=first, onLaterPages=later) print(f'Study guide written: {out_path}') if __name__ == '__main__': build('/home/daytona/workspace/edic-ards-ccs/EDIC_Study_Guide_12_Traps.pdf') PYEOF python3 /home/daytona/workspace/edic-ards-ccs/build_study_guide.py
python3 -c " from pypdf import PdfReader r = PdfReader('/home/daytona/workspace/edic-ards-ccs/EDIC_Study_Guide_12_Traps.pdf') print(f'Pages: {len(r.pages)}') import os print(f'Size: {os.path.getsize(\"/home/daytona/workspace/edic-ards-ccs/EDIC_Study_Guide_12_Traps.pdf\")/1024:.1f} KB') for i, p in enumerate(r.pages): t = p.extract_text() print(f' P{i+1}: {t[:70].strip()!r}') "
EDIC Study Guide 12 Traps
PDF Document · PDF
| Panel | Colour | Content |
|---|---|---|
| ✗ What Candidates Say | Red | The wrong answer verbatim |
| ✓ Correct Answer | Green | Exact phrasing to use |
| ▶ Why This Matters | Amber | Clinical / trial rationale |
| 🎙 Examiner Will Ask | Teal | The follow-up probe question |
| # | Trap | Category | The Fatal Error |
|---|---|---|---|
| 1 | IBW vs Actual Weight | Ventilation | Using 92 kg instead of IBW 70 kg for Vt |
| 2 | Obsolete ARDS Definitions | Diagnosis | Saying "ALI" or quoting AECC 1994 |
| 3 | Chasing Normal PaCO₂ | Ventilation | Increasing RR → auto-PEEP + VILI |
| 4 | Fluids in High EVLWI | Haemodynamics | Bolusing a patient with EVLWI 18 mL/kg |
| 5 | Staircase Recruitment | Ventilation | Recommending ART-protocol RM (kills patients) |
| 6 | iNO/Steroids "Reduce Mortality" | Pharmacology | iNO improves P/F — it does NOT reduce mortality |
| 7 | Ignoring Auto-PEEP | Ventilation | Set PEEP ≠ Total PEEP on the flow-time curve |
| 8 | D-sign = PE | Physiology | Ordering CTPA for ACP in established ARDS |
| 9 | CVP as Fluid Guide | Haemodynamics | CVP AUC 0.56 — no better than a coin flip |
| 10 | Routine NMB for All ARDS | Ventilation | Ignoring ROSE trial; ESICM 2023 says selective only |
| 11 | Ignoring Driving Pressure | Physiology | Pplat 28 "safe" but DP 22 = injurious (Amato 2015) |
| 12 | EOLIA "Proved ECMO" | Prognosis | Primary endpoint NOT met (p=0.09) — conditional rec only |
Make a ventilator graphics flashcard deck (ABG curves, pressure/flow/volume loops) with ARDS examples
python3 -c "import matplotlib; import numpy; import reportlab; print('all OK')" 2>&1
cat << 'PYEOF' > /home/daytona/workspace/edic-ards-ccs/build_flashcards.py """ EDIC Part II — Ventilator Graphics Flashcard Deck Draws real waveforms with matplotlib, assembles into A4 PDF via ReportLab. Each card: FRONT (waveform + title) | BACK (interpretation + clinical pearls) """ import io, os, math import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.patches as mpatches from matplotlib.patches import FancyArrowPatch from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.units import mm from reportlab.platypus import (SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, KeepTogether, PageBreak, Image) from reportlab.lib.styles import ParagraphStyle from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY from reportlab.platypus import Flowable # ── palette ────────────────────────────────────────────────────────────────── NAVY = colors.HexColor('#003D73') TEAL = colors.HexColor('#007B9E') GOLD = colors.HexColor('#F9A825') RED_D = colors.HexColor('#C62828') RED_L = colors.HexColor('#FFEBEE') GRN_D = colors.HexColor('#2E7D32') GRN_L = colors.HexColor('#E8F5E9') AMB_D = colors.HexColor('#E65100') AMB_L = colors.HexColor('#FFF3E0') PUR_D = colors.HexColor('#4A148C') PUR_L = colors.HexColor('#F3E5F5') LGREY = colors.HexColor('#F5F5F5') MGREY = colors.HexColor('#D0D0D0') DGREY = colors.HexColor('#555555') TEAL_L = colors.HexColor('#E0F7FA') WHT = colors.white BLK = colors.black CARD_B = colors.HexColor('#EEF5FB') PAGE_W, PAGE_H = A4 LM = RM = 14*mm TM = BM = 14*mm AW = PAGE_W - LM - RM # matplotlib colours MC = {'norm':'#1565C0','ards':'#C62828','note':'#E65100', 'warn':'#AD1457','ok':'#2E7D32','annot':'#4A148C', 'bg':'#FAFAFA','grid':'#E0E0E0','ax':'#37474F'} def S(n,**k): d=dict(fontName='Helvetica',fontSize=9,leading=13, textColor=BLK,spaceBefore=0,spaceAfter=0) d.update(k); return ParagraphStyle(n,**d) ST={ 'card_title': S('ct',fontName='Helvetica-Bold',fontSize=13, textColor=WHT,alignment=TA_CENTER), 'card_sub': S('cs',fontName='Helvetica',fontSize=9, textColor=WHT,alignment=TA_CENTER), 'card_num': S('cn',fontName='Helvetica-Bold',fontSize=11, textColor=WHT,alignment=TA_CENTER), 'front_hint': S('fh',fontName='Helvetica-Oblique',fontSize=8.5, textColor=DGREY,alignment=TA_CENTER), 'body': S('b',fontSize=9,leading=13), 'bold': S('bo',fontName='Helvetica-Bold',fontSize=9), 'label': S('l',fontName='Helvetica-Bold',fontSize=9,textColor=NAVY), 'answer': S('a',fontSize=9,leading=13,textColor=GRN_D), 'warn': S('w',fontSize=8.5,leading=12,textColor=RED_D, fontName='Helvetica-Oblique'), 'pearl': S('p',fontSize=8.5,leading=12.5,textColor=AMB_D), 'ref': S('r',fontSize=7.5,textColor=DGREY, fontName='Helvetica-Oblique'), 'footer': S('fo',fontSize=7.5,textColor=DGREY,alignment=TA_CENTER), 'bk_title': S('bt',fontName='Helvetica-Bold',fontSize=11, textColor=NAVY,alignment=TA_CENTER), 'toc': S('tc',fontSize=9,leading=14,textColor=NAVY), } # ── header/footer ───────────────────────────────────────────────────────────── def hf(canv,doc): canv.saveState() w=PAGE_W canv.setFillColor(NAVY) canv.rect(0,PAGE_H-36,w,36,stroke=0,fill=1) canv.setFillColor(WHT) canv.setFont('Helvetica-Bold',13) canv.drawString(LM,PAGE_H-20,'ESICM') canv.setFont('Helvetica',8) canv.drawString(LM,PAGE_H-30,'European Society of Intensive Care Medicine') t='EDIC Part II — Ventilator Graphics Flashcard Deck' canv.setFont('Helvetica-Bold',10) tw=canv.stringWidth(t,'Helvetica-Bold',10) canv.drawString((w-tw)/2,PAGE_H-19,t) s='Pressure · Flow · Volume · Loops · ARDS Examples' canv.setFont('Helvetica',8) sw=canv.stringWidth(s,'Helvetica',8) canv.drawString((w-sw)/2,PAGE_H-29,s) canv.setStrokeColor(GOLD) canv.setLineWidth(2) canv.line(0,PAGE_H-38,w,PAGE_H-38) canv.setStrokeColor(MGREY) canv.setLineWidth(0.5) canv.line(LM,BM,w-RM,BM) canv.setFillColor(DGREY) canv.setFont('Helvetica',7) canv.drawString(LM,BM-10, 'EDIC Part II Flashcard Deck • July 2026 • ESICM ARDS 2023 / ARMA / PROSEVA') pn=str(doc.page) pw=canv.stringWidth(pn,'Helvetica-Bold',8) canv.setFont('Helvetica-Bold',8) canv.setFillColor(NAVY) canv.drawString(w-RM-pw,BM-10,pn) canv.restoreState() # ══════════════════════════════════════════════════════════════════════════════ # WAVEFORM DRAWING FUNCTIONS # ══════════════════════════════════════════════════════════════════════════════ def fig_to_img(fig, w_mm, h_mm, dpi=140): """Convert matplotlib figure to ReportLab Image.""" buf = io.BytesIO() fig.savefig(buf, format='png', dpi=dpi, bbox_inches='tight', facecolor=fig.get_facecolor()) buf.seek(0) plt.close(fig) return Image(buf, width=w_mm*mm, height=h_mm*mm) def setup_ax(ax, xlabel='', ylabel='', title='', xlim=None, ylim=None): ax.set_facecolor(MC['bg']) ax.grid(True, color=MC['grid'], linewidth=0.5, zorder=0) ax.spines['top'].set_visible(False) ax.spines['right'].set_visible(False) ax.spines['left'].set_color(MC['ax']) ax.spines['bottom'].set_color(MC['ax']) ax.tick_params(colors=MC['ax'], labelsize=8) ax.set_xlabel(xlabel, fontsize=8, color=MC['ax']) ax.set_ylabel(ylabel, fontsize=8, color=MC['ax']) if title: ax.set_title(title, fontsize=9, fontweight='bold', color=MC['ax'], pad=4) if xlim: ax.set_xlim(xlim) if ylim: ax.set_ylim(ylim) def one_vcv_breath(t0, ti=0.75, te=1.5, vt=0.5, peep=5, pp=25, pplat=20, flow=1.0, rr_offset=0): """Generate one VCV breath arrays: t, P, F, V""" t = np.linspace(t0, t0+ti+te, 200) P = []; F = []; V = [] for tt in t: rel = tt - t0 if rel <= ti: # Inspiration: square flow, pressure rises then plateaus frac = rel/ti P.append(peep + (pp - peep)*min(frac*2.5, 1.0)) F.append(flow) V.append(vt * rel/ti) else: # Expiration: exponential exp_t = rel - ti tau = te/3.5 P.append(peep + (pplat - peep)*np.exp(-exp_t/tau)) F.append(-flow * np.exp(-exp_t/(te/4)) * 0.8) V.append(vt * np.exp(-exp_t/tau)) return t, np.array(P), np.array(F), np.array(V) # ── Card 1: Normal VCV triple waveform ─────────────────────────────────────── def draw_card1_normal_vcv(): fig, axes = plt.subplots(3, 1, figsize=(7, 5.5), facecolor='white') fig.subplots_adjust(hspace=0.55, left=0.1, right=0.97, top=0.92, bottom=0.08) fig.suptitle('Normal VCV — Pressure / Flow / Volume Waveforms', fontsize=10, fontweight='bold', color=MC['ax']) t_all, P_all, F_all, V_all = [], [], [], [] for i in range(3): t0 = i * 2.25 t, P, F, V = one_vcv_breath(t0, ti=0.75, te=1.5, vt=0.5, peep=5, pp=28, pplat=22, flow=0.67) t_all.extend(t); P_all.extend(P); F_all.extend(F); V_all.extend(V) t_arr = np.array(t_all) axes[0].plot(t_arr, P_all, color=MC['norm'], lw=2) axes[0].axhline(22, color=MC['note'], lw=1, ls='--', alpha=0.7) axes[0].axhline(5, color=MC['ok'], lw=1, ls='--', alpha=0.7) axes[0].annotate('Pplat 22', xy=(0.5, 22), xytext=(0.7, 25), fontsize=7, color=MC['note'], arrowprops=dict(arrowstyle='->', color=MC['note'], lw=0.8)) axes[0].annotate('PEEP 5', xy=(0.1, 5), xytext=(0.3, 2), fontsize=7, color=MC['ok'], arrowprops=dict(arrowstyle='->', color=MC['ok'], lw=0.8)) setup_ax(axes[0], ylabel='Pressure\n(cmH₂O)', ylim=(-2, 35)) axes[1].plot(t_arr, F_all, color=MC['norm'], lw=2) axes[1].axhline(0, color=MC['ax'], lw=0.8, ls='-') axes[1].fill_between(t_arr, F_all, 0, where=np.array(F_all)>0, alpha=0.15, color=MC['norm']) axes[1].fill_between(t_arr, F_all, 0, where=np.array(F_all)<0, alpha=0.15, color=MC['warn']) axes[1].annotate('Square\ninspiratory\nflow', xy=(0.37, 0.55), fontsize=7, color=MC['norm'], ha='center') axes[1].annotate('Exp flow\nreturns to 0', xy=(1.5, -0.3), fontsize=7, color=MC['ok'], ha='center') setup_ax(axes[1], ylabel='Flow\n(L/s)', ylim=(-0.9, 1.0)) axes[2].plot(t_arr, V_all, color=MC['norm'], lw=2) axes[2].annotate('Vt 500 mL', xy=(0.75, 0.5), xytext=(1.0, 0.4), fontsize=7, color=MC['norm'], arrowprops=dict(arrowstyle='->', color=MC['norm'], lw=0.8)) setup_ax(axes[2], xlabel='Time (s)', ylabel='Volume\n(L)', ylim=(-0.05, 0.65)) return fig # ── Card 2: Normal PCV waveform ─────────────────────────────────────────────── def draw_card2_normal_pcv(): fig, axes = plt.subplots(3, 1, figsize=(7, 5.5), facecolor='white') fig.subplots_adjust(hspace=0.55, left=0.1, right=0.97, top=0.92, bottom=0.08) fig.suptitle('Normal PCV — Pressure / Flow / Volume Waveforms', fontsize=10, fontweight='bold', color=MC['ax']) t_all, P_all, F_all, V_all = [], [], [], [] for breath in range(3): t0 = breath * 2.25 ti, te = 0.75, 1.5 t = np.linspace(t0, t0+ti+te, 200) for tt in t: rel = tt - t0 if rel <= ti: # PCV: pressure square wave, flow decelerating tau = ti/4 P_all.append(5 + 20) # PEEP 5 + ΔP 20 = 25 cmH₂O decay = np.exp(-rel/tau) F_all.append(0.8 * decay) V_all.append(0.5*(1-np.exp(-rel/tau))) else: exp_t = rel - ti tau_e = te/3.5 P_all.append(5 + 20*np.exp(-exp_t/0.05)) F_all.append(-0.6*np.exp(-exp_t/(te/4))) V_all.append(0.5*np.exp(-exp_t/tau_e)) t_all.extend(t) t_arr = np.array(t_all) axes[0].plot(t_arr, P_all, color=MC['norm'], lw=2) axes[0].annotate('Pressure\nsquare wave\n(PCV)', xy=(0.37, 25), fontsize=7, color=MC['norm'], ha='center', bbox=dict(boxstyle='round,pad=0.2', facecolor='white', edgecolor=MC['norm'], alpha=0.8)) axes[0].annotate('PEEP 5', xy=(1.5, 5.5), fontsize=7, color=MC['ok']) setup_ax(axes[0], ylabel='Pressure\n(cmH₂O)', ylim=(-2, 32)) axes[1].plot(t_arr, F_all, color=MC['norm'], lw=2) axes[1].axhline(0, color=MC['ax'], lw=0.8) axes[1].fill_between(t_arr, F_all, 0, where=np.array(F_all)>0, alpha=0.15, color=MC['norm']) axes[1].annotate('Decelerating\ninsp flow\n(PCV pattern)', xy=(0.37, 0.4), fontsize=7, color=MC['norm'], ha='center') setup_ax(axes[1], ylabel='Flow\n(L/s)', ylim=(-0.8, 1.0)) axes[2].plot(t_arr, V_all, color=MC['norm'], lw=2) setup_ax(axes[2], xlabel='Time (s)', ylabel='Volume\n(L)', ylim=(-0.05, 0.65)) return fig # ── Card 3: Auto-PEEP / Dynamic Hyperinflation ──────────────────────────────── def draw_card3_auto_peep(): fig, axes = plt.subplots(2, 1, figsize=(7, 4.8), facecolor='white') fig.subplots_adjust(hspace=0.55, left=0.1, right=0.97, top=0.90, bottom=0.1) fig.suptitle('Auto-PEEP: Flow-Time & Volume-Time — INCOMPLETE EXPIRATION', fontsize=10, fontweight='bold', color=MC['warn']) # Generate 3 breaths with insufficient expiratory time (auto-PEEP) t_f, F_f, t_v, V_v = [], [], [], [] baseline_vol = 0.0 for breath in range(3): rr = 2.0 # only 2s breath cycle (fast) ti, te = 0.75, 1.25 # te too short tau_e = 0.9 # slow tau → incomplete exp t_b = np.linspace(0, ti+te, 180) + breath*rr f_b = [] v_b = [] for tt in t_b: rel = tt - breath*rr if rel <= ti: f_b.append(0.65) v_b.append(baseline_vol + 0.5*rel/ti) else: exp_t = rel - ti f_val = -0.65*np.exp(-exp_t/(tau_e)) f_b.append(f_val) v_val = baseline_vol + 0.5*np.exp(-exp_t/tau_e) v_b.append(v_val) # new baseline = vol at end of breath (incomplete return) baseline_vol = v_b[-1] t_f.extend(t_b); F_f.extend(f_b) t_v.extend(t_b); V_v.extend(v_b) t_farr = np.array(t_f) t_varr = np.array(t_v) axes[0].plot(t_farr, F_f, color=MC['ards'], lw=2) axes[0].axhline(0, color=MC['ax'], lw=1.2, ls='-') axes[0].fill_between(t_farr, F_f, 0, where=np.array(F_f)<0, alpha=0.15, color=MC['ards']) # Annotate the non-return for i, t0 in enumerate([0.75, 2.75, 4.75]): axes[0].annotate('', xy=(t0+1.2, 0), xytext=(t0+1.1, -0.22), arrowprops=dict(arrowstyle='->', color=MC['warn'], lw=1.2)) axes[0].text(1.3, -0.35, 'Exp flow does NOT\nreturn to zero → AUTO-PEEP', fontsize=8, color=MC['warn'], fontweight='bold') setup_ax(axes[0], ylabel='Flow (L/s)', ylim=(-0.9, 0.85)) axes[1].plot(t_varr, V_v, color=MC['ards'], lw=2) axes[1].axhline(0, color=MC['ok'], lw=1, ls='--', alpha=0.6) axes[1].text(3.5, 0.35, 'Progressive\ngas trapping', fontsize=8, color=MC['warn'], fontweight='bold') # Arrow showing progressive rise in baseline axes[1].annotate('', xy=(4.0, 0.28), xytext=(0.5, 0.02), arrowprops=dict(arrowstyle='->', color=MC['warn'], lw=1.5, connectionstyle='arc3,rad=0.2')) setup_ax(axes[1], xlabel='Time (s)', ylabel='Volume (L)', ylim=(-0.05, 0.75)) return fig # ── Card 4: ARDS Pressure-Time ───────────────────────────────────────────────── def draw_card4_ards_pressure(): fig, axes = plt.subplots(1, 2, figsize=(8, 4), facecolor='white') fig.subplots_adjust(wspace=0.45, left=0.08, right=0.97, top=0.88, bottom=0.12) fig.suptitle('Pressure-Time: Normal vs ARDS (Same Vt, Different Compliance)', fontsize=10, fontweight='bold', color=MC['ax']) for ax_i, (label, pp, pplat, peep, col, lbl) in enumerate([ ('Normal', 26, 21, 5, MC['norm'], 'Normal lung\nCrs ≈ 50 mL/cmH₂O\nDP = 16 cmH₂O'), ('ARDS', 38, 32, 8, MC['ards'], 'ARDS lung\nCrs ≈ 20 mL/cmH₂O\nDP = 24 cmH₂O ⚠'), ]): ax = axes[ax_i] t_all, P_all = [], [] for breath in range(3): t0 = breath * 2.25 t, P, _, _ = one_vcv_breath(t0, ti=0.75, te=1.5, vt=0.5, peep=peep, pp=pp, pplat=pplat) t_all.extend(t); P_all.extend(P) ax.plot(np.array(t_all), P_all, color=col, lw=2.5) ax.axhline(pplat, color=MC['note'], lw=1.2, ls='--', alpha=0.8) ax.axhline(peep, color=MC['ok'], lw=1, ls=':', alpha=0.8) ax.text(0.3, pplat+0.5, f'Pplat {pplat}', fontsize=7.5, color=MC['note'], fontweight='bold') ax.text(0.3, peep+0.5, f'PEEP {peep}', fontsize=7.5, color=MC['ok']) # Driving pressure brace ax.annotate('', xy=(5.5, pplat), xytext=(5.5, peep), arrowprops=dict(arrowstyle='<->', color=col, lw=1.5)) ax.text(5.55, (pplat+peep)/2, f'DP={pplat-peep}', fontsize=7.5, color=col, fontweight='bold', va='center') ax.text(3.5, pp-3, lbl, fontsize=7.5, color=col, bbox=dict(boxstyle='round', facecolor='white', edgecolor=col, alpha=0.85)) setup_ax(ax, xlabel='Time (s)', ylabel='Pressure (cmH₂O)', title=label, ylim=(-2, 44)) return fig # ── Card 5: P-V Loop Normal vs ARDS ────────────────────────────────────────── def draw_card5_pv_loop(): fig, axes = plt.subplots(1, 3, figsize=(10, 4.2), facecolor='white') fig.subplots_adjust(wspace=0.5, left=0.07, right=0.97, top=0.88, bottom=0.12) fig.suptitle('Pressure-Volume (P-V) Loop: Normal, ARDS, Overdistension', fontsize=10, fontweight='bold', color=MC['ax']) def pv_normal(ax): # Sigmoid with good compliance p = np.linspace(5, 28, 100) v_insp = 0.5*(1/(1+np.exp(-(p-16)/3.5))) + 0.02 p_exp = np.linspace(28, 5, 100) v_exp = 0.5*(1/(1+np.exp(-(p_exp-14)/3.5))) - 0.02 ax.plot(p, v_insp, color=MC['norm'], lw=2.5, label='Insp') ax.plot(p_exp, v_exp, color=MC['norm'], lw=2, ls='--', alpha=0.8, label='Exp') ax.fill_betweenx(np.concatenate([v_insp, v_exp[::-1]]), np.concatenate([p, p_exp[::-1]]), alpha=0.08, color=MC['norm']) ax.text(8, 0.3, 'Good\nslope\n(compliance)', fontsize=7, color=MC['norm']) setup_ax(ax, xlabel='Pressure (cmH₂O)', ylabel='Volume (L)', title='Normal\nCrs ≈ 50 mL/cmH₂O', ylim=(-0.05,0.65)) def pv_ards(ax): # Reduced slope, lower inflection, beaking p = np.linspace(5, 34, 100) # Lower compliance - less steep v_insp = 0.35*(1/(1+np.exp(-(p-19)/4.5))) + 0.01 p_exp = np.linspace(34, 5, 100) v_exp = 0.35*(1/(1+np.exp(-(p_exp-17)/4.5))) - 0.015 # Lower inflection point lip = 14 ax.axvline(lip, color=MC['annot'], lw=1, ls=':', alpha=0.7) ax.text(lip+0.2, 0.02, 'LIP\n~14', fontsize=7, color=MC['annot']) # Upper inflection / beaking uip = 30 ax.axvline(uip, color=MC['warn'], lw=1, ls=':', alpha=0.7) ax.text(uip+0.2, 0.28, 'UIP\n~30', fontsize=7, color=MC['warn']) ax.plot(p, v_insp, color=MC['ards'], lw=2.5) ax.plot(p_exp, v_exp, color=MC['ards'], lw=2, ls='--', alpha=0.8) ax.text(6, 0.28, 'Reduced\nslope\n(stiff lung)', fontsize=7, color=MC['ards']) setup_ax(ax, xlabel='Pressure (cmH₂O)', ylabel='Volume (L)', title='ARDS\nCrs ≈ 20 mL/cmH₂O', ylim=(-0.05,0.55)) def pv_overdist(ax): p = np.linspace(5, 40, 120) # Beaking at high pressure v = np.where(p<=30, 0.4*(1/(1+np.exp(-(p-18)/4))), 0.4*(1/(1+np.exp(-(30-18)/4))) + 0.005*(p-30)) ax.plot(p, v, color=MC['warn'], lw=2.5) ax.annotate('BEAKING\n(overdistension\nzone)', xy=(37, 0.32), xytext=(28, 0.42), fontsize=7.5, color=MC['warn'], fontweight='bold', arrowprops=dict(arrowstyle='->', color=MC['warn'], lw=1)) ax.axvspan(30, 40, alpha=0.1, color=MC['warn'], zorder=0) ax.text(6, 0.25, 'Normal\nzone', fontsize=7, color=MC['ok']) ax.text(32, 0.15, 'Danger\nzone', fontsize=7, color=MC['warn']) setup_ax(ax, xlabel='Pressure (cmH₂O)', ylabel='Volume (L)', title='Overdistension\n(VILI risk)', ylim=(-0.05,0.58)) pv_normal(axes[0]) pv_ards(axes[1]) pv_overdist(axes[2]) return fig # ── Card 6: Flow-Volume Loop ────────────────────────────────────────────────── def draw_card6_fv_loop(): fig, axes = plt.subplots(1, 3, figsize=(10, 4), facecolor='white') fig.subplots_adjust(wspace=0.55, left=0.07, right=0.97, top=0.88, bottom=0.12) fig.suptitle('Flow-Volume Loop: Normal, Obstructive (auto-PEEP), Restrictive (ARDS)', fontsize=10, fontweight='bold', color=MC['ax']) def fv_normal(ax): # Inspiration: negative flow (ventilator convention) v_i = np.linspace(0, 0.5, 60) f_i = np.full(60, 0.67) # square wave VCV v_e = np.linspace(0.5, 0, 100) # Passive exp: flow proportional to volume f_e = -v_e * 1.2 * np.exp(-np.linspace(0,2,100)*0.3) ax.plot(v_i, f_i, color=MC['norm'], lw=2.5) ax.plot(v_e, f_e, color=MC['norm'], lw=2.5) ax.axhline(0, color=MC['ax'], lw=0.8) ax.text(0.1, 0.3, 'Insp\n(→)', fontsize=7, color=MC['norm']) ax.text(0.3, -0.4, 'Exp\n(←)', fontsize=7, color=MC['norm']) ax.fill_between(v_e, f_e, 0, alpha=0.1, color=MC['norm']) setup_ax(ax, xlabel='Volume (L)', ylabel='Flow (L/s)', title='Normal', ylim=(-0.9,1.0)) def fv_obstructive(ax): v_i = np.linspace(0, 0.5, 60) f_i = np.full(60, 0.67) v_e = np.linspace(0.5, 0, 100) # Scalloping — flow limitation in expiration f_e = -(v_e * 0.7 + 0.05) * (1 + 0.5*np.sin(np.linspace(0,np.pi*2,100)*1.5)*0.2) f_e = np.clip(f_e, -0.5, 0) ax.plot(v_i, f_i, color=MC['ards'], lw=2.5) ax.plot(v_e, f_e, color=MC['ards'], lw=2.5) ax.axhline(0, color=MC['ax'], lw=0.8) ax.annotate('Scalloping\n(expiratory\nflow limitation)', xy=(0.25,-0.35), fontsize=7, color=MC['warn'], fontweight='bold', bbox=dict(boxstyle='round',facecolor='white', edgecolor=MC['warn'],alpha=0.8)) setup_ax(ax, xlabel='Volume (L)', ylabel='Flow (L/s)', title='Obstructive / Auto-PEEP', ylim=(-0.9,1.0)) def fv_restrictive(ax): # Narrow loop — small Vt, similar flow v_i = np.linspace(0, 0.28, 60) f_i = np.full(60, 0.67) v_e = np.linspace(0.28, 0, 100) f_e = -v_e * 2.0 f_e = np.clip(f_e, -0.8, 0) ax.plot(v_i, f_i, color=MC['annot'], lw=2.5) ax.plot(v_e, f_e, color=MC['annot'], lw=2.5) ax.axhline(0, color=MC['ax'], lw=0.8) ax.text(0.02, 0.3, 'Reduced Vt\n280 mL', fontsize=7.5, color=MC['annot'], fontweight='bold') ax.annotate('Steep exp\nslope (high\nelastic recoil)', xy=(0.15,-0.35), fontsize=7, color=MC['annot']) setup_ax(ax, xlabel='Volume (L)', ylabel='Flow (L/s)', title='Restrictive / ARDS\n(low Crs)', ylim=(-0.9,1.0)) fv_normal(axes[0]) fv_obstructive(axes[1]) fv_restrictive(axes[2]) return fig # ── Card 7: ARDS — three-panel waveform with annotations ───────────────────── def draw_card7_ards_full(): fig, axes = plt.subplots(3, 1, figsize=(7, 5.8), facecolor='white') fig.subplots_adjust(hspace=0.6, left=0.11, right=0.97, top=0.91, bottom=0.08) fig.suptitle('Severe ARDS — Pressure / Flow / Volume (VCV, PEEP 14, DP 22 ⚠)', fontsize=10, fontweight='bold', color=MC['ards']) t_all, P_all, F_all, V_all = [], [], [], [] for i in range(3): t0 = i * 2.5 t, P, F, V = one_vcv_breath(t0, ti=0.75, te=1.75, vt=0.42, peep=14, pp=40, pplat=36, flow=0.56) t_all.extend(t); P_all.extend(P); F_all.extend(F); V_all.extend(V) t_arr = np.array(t_all) axes[0].plot(t_arr, P_all, color=MC['ards'], lw=2.5) axes[0].axhline(36, color=MC['warn'], lw=1.2, ls='--', alpha=0.9) axes[0].axhline(14, color=MC['ok'], lw=1, ls='--', alpha=0.8) axes[0].axhspan(30, 44, alpha=0.07, color=MC['warn'], zorder=0) axes[0].text(0.2, 36.5, 'Pplat 36 ⚠ (>30)', fontsize=7.5, color=MC['warn'], fontweight='bold') axes[0].text(0.2, 12, 'PEEP 14', fontsize=7.5, color=MC['ok']) axes[0].annotate('', xy=(6.3, 36), xytext=(6.3, 14), arrowprops=dict(arrowstyle='<->', color=MC['ards'], lw=1.5)) axes[0].text(6.35, 25, 'DP=22\n⚠>15', fontsize=7.5, color=MC['ards'], fontweight='bold', va='center') setup_ax(axes[0], ylabel='Pressure\n(cmH₂O)', ylim=(0, 48)) axes[1].plot(t_arr, F_all, color=MC['ards'], lw=2) axes[1].axhline(0, color=MC['ax'], lw=0.8) axes[1].fill_between(t_arr, F_all, 0, where=np.array(F_all)>0, alpha=0.15, color=MC['ards']) axes[1].fill_between(t_arr, F_all, 0, where=np.array(F_all)<0, alpha=0.12, color=MC['warn']) setup_ax(axes[1], ylabel='Flow\n(L/s)', ylim=(-0.75, 0.75)) axes[2].plot(t_arr, V_all, color=MC['ards'], lw=2) axes[2].text(1.0, 0.35, 'Vt 420 mL\n(6 mL/kg IBW)', fontsize=7.5, color=MC['ok'], fontweight='bold', bbox=dict(boxstyle='round', facecolor='white', edgecolor=MC['ok'], alpha=0.85)) setup_ax(axes[2], xlabel='Time (s)', ylabel='Volume\n(L)', ylim=(-0.03, 0.6)) return fig # ── Card 8: Inspiratory pause — Pplat measurement ──────────────────────────── def draw_card8_pplat(): fig, ax = plt.subplots(figsize=(7.5, 4.2), facecolor='white') fig.subplots_adjust(left=0.1, right=0.97, top=0.88, bottom=0.12) ax.set_title('Inspiratory Hold Manoeuvre — Plateau Pressure Measurement', fontsize=10, fontweight='bold', color=MC['ax']) t = np.linspace(0, 5, 500) P = np.zeros(500) for i, tt in enumerate(t): if tt < 0.75: P[i] = 5 + 33 * min(tt/0.75 * 2, 1.0) # rapid rise to Peak elif tt < 0.75: P[i] = 38 elif tt < 1.5: # hold P[i] = 38 - (38-28)*(tt-0.75)/0.75 * 0.9 # fall to Pplat elif tt < 2.0: # pause (hold button) P[i] = 28 elif tt < 2.75: # expiration P[i] = 28 - (28-5)*((tt-2.0)/0.75)**0.5 elif tt < 3.25: # second breath P[i] = 5 + 33 * min((tt-2.75)/0.5 * 2, 1.0) elif tt < 3.8: P[i] = 38 - (38-28)*(tt-3.25)/0.55 * 0.9 elif tt < 5.0: P[i] = 5 + (28-5)*np.exp(-(tt-3.8)/0.5) ax.plot(t, P, color=MC['norm'], lw=2.5) # Annotations ax.axhline(38, color=MC['ards'], lw=1.2, ls='--', alpha=0.9) ax.axhline(28, color=MC['note'], lw=1.2, ls='--', alpha=0.9) ax.axhline(5, color=MC['ok'], lw=1, ls=':', alpha=0.7) ax.text(0.1, 38.5, 'Peak pressure\n(Ppeak) 38', fontsize=7.5, color=MC['ards']) ax.text(2.1, 28.8, 'Plateau pressure\n(Pplat) 28 ✓ <30', fontsize=7.5, color=MC['note'], fontweight='bold') ax.text(0.1, 2, 'PEEP 5', fontsize=7.5, color=MC['ok']) # Driving pressure brace ax.annotate('', xy=(4.8, 28), xytext=(4.8, 5), arrowprops=dict(arrowstyle='<->', color=MC['norm'], lw=1.5)) ax.text(4.85, 16, 'DP=23\n⚠', fontsize=8, color=MC['ards'], fontweight='bold', va='center') # Resistive component brace ax.annotate('', xy=(0.9, 38), xytext=(0.9, 28), arrowprops=dict(arrowstyle='<->', color=MC['warn'], lw=1.2)) ax.text(0.95, 33, 'Resistive\n(Ppeak-Pplat)\n=10', fontsize=7, color=MC['warn'], va='center') # Insp hold window shading ax.axvspan(1.5, 2.0, alpha=0.12, color=MC['note'], zorder=0) ax.text(1.62, 20, 'Insp\nHold', fontsize=7.5, color=MC['note'], fontweight='bold', ha='center') setup_ax(ax, xlabel='Time (s)', ylabel='Pressure (cmH₂O)', ylim=(-2, 46)) return fig # ── Card 9: Expiratory hold — auto-PEEP measurement ────────────────────────── def draw_card9_exp_hold(): fig, axes = plt.subplots(2, 1, figsize=(7, 4.8), facecolor='white') fig.subplots_adjust(hspace=0.6, left=0.1, right=0.97, top=0.89, bottom=0.1) fig.suptitle('Expiratory Hold — Auto-PEEP (Intrinsic PEEP) Measurement', fontsize=10, fontweight='bold', color=MC['note']) t = np.linspace(0, 7, 700) P = []; F = [] baseline = 0 for tt in t: if tt < 0.75: P.append(8 + 25) # PEEP 8 + breath F.append(0.6) elif tt < 0.75 + 1.5: exp_t = tt - 0.75 P.append(8 + 25*np.exp(-exp_t/0.4)) F.append(-0.55*np.exp(-exp_t/0.35)) # doesn't reach zero elif tt < 3.0: # Second breath starts before flow returns to zero rel = tt - 2.25 if rel < 0.75: P.append(8 + 25) F.append(0.6) else: exp_t2 = rel - 0.75 P.append(8 + 25*np.exp(-exp_t2/0.4)) F.append(-0.55*np.exp(-exp_t2/0.35)) elif tt < 3.5: # Expiratory hold (3.0-5.5) P.append(8 + 25*np.exp(-(tt-2.25-0.75)/0.4)) F.append(0.0) # flow zeroed by hold elif tt < 5.5: # DURING hold — pressure equilibrates to total PEEP t_hold = tt - 3.5 P.append(12 + (8+5-12)*np.exp(-t_hold/0.3)) # 13 = total PEEP F.append(0.0) else: # After hold release rel = tt - 5.5 if rel < 0.75: P.append(8+5 + 22) F.append(0.6) else: exp_t = rel-0.75 P.append(8+5 + 22*np.exp(-exp_t/0.4)) F.append(-0.55*np.exp(-exp_t/0.35)) axes[0].plot(t, P, color=MC['note'], lw=2) axes[0].axhline(8, color=MGREY.hexval() if hasattr(MGREY,'hexval') else 'grey', lw=1, ls=':', alpha=0.7) axes[0].axhline(13, color=MC['ok'], lw=1.2, ls='--', alpha=0.9) axes[0].axvspan(3.0, 5.5, alpha=0.12, color=MC['note'], zorder=0) axes[0].text(4.0, 3, 'Exp Hold', fontsize=8, color=MC['note'], fontweight='bold', ha='center') axes[0].text(5.6, 13.5, 'Total PEEP\n= 13 cmH₂O\n(set PEEP 8\n+ auto-PEEP 5)', fontsize=7.5, color=MC['ok'], fontweight='bold') axes[0].text(0.1, 5.5, 'Set PEEP = 8', fontsize=7, color='grey') setup_ax(axes[0], ylabel='Pressure\n(cmH₂O)', ylim=(-2, 40)) axes[1].plot(t, F, color=MC['note'], lw=2) axes[1].axhline(0, color=MC['ax'], lw=1) axes[1].axvspan(3.0, 5.5, alpha=0.12, color=MC['note'], zorder=0) axes[1].text(1.5, -0.28, 'Flow does not return to zero\nbefore next breath', fontsize=7.5, color=MC['ards'], fontweight='bold', ha='center') axes[1].fill_between(t, F, 0, where=np.array(F)<0, alpha=0.12, color=MC['ards']) setup_ax(axes[1], xlabel='Time (s)', ylabel='Flow\n(L/s)', ylim=(-0.75, 0.85)) return fig # ── Card 10: Patient-Ventilator Asynchrony ──────────────────────────────────── def draw_card10_asynch(): fig, axes = plt.subplots(2, 2, figsize=(9.5, 5.5), facecolor='white') fig.subplots_adjust(hspace=0.7, wspace=0.55, left=0.08, right=0.97, top=0.88, bottom=0.1) fig.suptitle('Patient-Ventilator Asynchrony — 4 Key Patterns', fontsize=10, fontweight='bold', color=MC['ax']) # (a) Double triggering ax = axes[0,0] t = np.linspace(0,3,300) P = np.zeros(300) for i,tt in enumerate(t): if tt<0.4: P[i]=5+30*min(tt/0.4*2,1) elif tt<0.75: P[i]=35-35*(tt-0.4)/0.35*0.8 elif tt<1.0: P[i]=5+30*min((tt-0.75)/0.25*2,1) # second trigger elif tt<1.3: P[i]=35-35*(tt-1.0)/0.3*0.8 else: P[i]=5+25*np.exp(-(tt-1.3)/0.4) ax.plot(t,P,color=MC['warn'],lw=2) ax.axvspan(0.7,1.05,alpha=0.15,color=MC['warn']) ax.text(0.75,-4,'Double\ntrigger',fontsize=7,color=MC['warn'], fontweight='bold',ha='center') ax.set_title('(a) Double Triggering',fontsize=8,color=MC['warn'],pad=3) setup_ax(ax,ylabel='Pressure\n(cmH₂O)',ylim=(-8,42)) # (b) Flow starvation ax = axes[0,1] t = np.linspace(0,3,300) P = np.zeros(300) for i,tt in enumerate(t): if tt<0.75: # Flow starvation: patient demand > set flow → pressure dips P[i] = 5 + 28*min(tt/0.75*2,1) - 4*np.sin(np.pi*tt/0.75)**2 elif tt<2.25: P[i] = 5+28*np.exp(-(tt-0.75)/0.5) else: P[i] = 5+28*min((tt-2.25)/0.75*2,1)-4*np.sin(np.pi*(tt-2.25)/0.75)**2 ax.plot(t,P,color=MC['note'],lw=2) ax.annotate('Pressure dips during\ninspiration = flow\nstarvation', xy=(0.37,15),fontsize=7,color=MC['note'],ha='center', bbox=dict(boxstyle='round',facecolor='white', edgecolor=MC['note'],alpha=0.85)) ax.set_title('(b) Flow Starvation (VCV)',fontsize=8,color=MC['note'],pad=3) setup_ax(ax,ylabel='Pressure\n(cmH₂O)',ylim=(-3,42)) # (c) Reverse triggering ax = axes[1,0] t = np.linspace(0,3,300) F = np.zeros(300) for i,tt in enumerate(t): if tt<0.75: F[i]=0.6 elif tt<0.75+0.4: # Patient effort during ventilator exhalation → brief insp flow spike exp_t=tt-0.75 F[i]=-0.5*np.exp(-exp_t/0.25)+0.25*np.exp(-exp_t/0.1)*np.sin(25*exp_t) elif tt<2.25: F[i]=-0.4*np.exp(-(tt-1.15)/0.3) elif tt<3.0: F[i]=0.6*(tt>2.25) ax.plot(t,F,color=MC['annot'],lw=2) ax.axhline(0,color=MC['ax'],lw=0.8) ax.annotate('Aberrant flow spike\nduring expiration =\nreverse triggering', xy=(1.0,-0.25),fontsize=7,color=MC['annot'], bbox=dict(boxstyle='round',facecolor='white', edgecolor=MC['annot'],alpha=0.85)) ax.set_title('(c) Reverse Triggering',fontsize=8,color=MC['annot'],pad=3) setup_ax(ax,xlabel='Time (s)',ylabel='Flow (L/s)',ylim=(-0.75,0.9)) # (d) Premature cycling ax = axes[1,1] t = np.linspace(0,3,300) F = np.zeros(300) for i,tt in enumerate(t): if tt<0.3: # breath cycled off too early F[i]=0.55 elif tt<0.6: # Immediate expiration F[i]=-0.5*np.exp(-(tt-0.3)/0.15) elif tt<0.9: # Continued patient effort against closed valve → negative deflection F[i]=-0.1+0.3*np.sin(np.pi*(tt-0.6)/0.3) elif tt<1.7: F[i]=-0.3*np.exp(-(tt-0.9)/0.25) elif tt<2.0: F[i]=0.55 elif tt<2.3: F[i]=-0.5*np.exp(-(tt-2.0)/0.15) else: F[i]=-0.3*np.exp(-(tt-2.3)/0.25) ax.plot(t,F,color=MC['ok'],lw=2) ax.axhline(0,color=MC['ax'],lw=0.8) ax.annotate('Early cycling:\npatient effort continues\nafter ventilator cuts off', xy=(0.7,0.25),fontsize=7,color=MC['ok'], bbox=dict(boxstyle='round',facecolor='white', edgecolor=MC['ok'],alpha=0.85)) ax.set_title('(d) Premature Cycling',fontsize=8,color=MC['ok'],pad=3) setup_ax(ax,xlabel='Time (s)',ylabel='Flow (L/s)',ylim=(-0.75,0.9)) return fig # ── Card 11: Recruitment — PEEP titration compliance curve ──────────────────── def draw_card11_peep_compliance(): fig, axes = plt.subplots(1, 2, figsize=(9, 4.2), facecolor='white') fig.subplots_adjust(wspace=0.5, left=0.09, right=0.97, top=0.88, bottom=0.12) fig.suptitle('PEEP Optimisation — Decremental Trial: Compliance vs PEEP', fontsize=10, fontweight='bold', color=MC['ax']) # Non-recruitable lung peep_vals = np.array([18,16,14,12,10,8,6]) crs_nr = np.array([22,23,24,25,24,22,19]) # compliance drops at both extremes # Recruitable lung crs_r = np.array([18,22,26,29,27,23,18]) axes[0].plot(peep_vals, crs_nr, 'o-', color=MC['norm'], lw=2, ms=7, label='Non-recruitable') axes[0].axvline(12, color=MC['ok'], lw=1.5, ls='--', alpha=0.8) axes[0].text(12.2, 26, 'Optimal\nPEEP = 12\n(best Crs)', fontsize=7.5, color=MC['ok'], fontweight='bold') axes[0].fill_betweenx([18,27], [10,10],[14,14], alpha=0.12, color=MC['ok']) setup_ax(axes[0], xlabel='PEEP (cmH₂O)', ylabel='Crs (mL/cmH₂O)', title='Non-Recruitable Lung\n(~50% of ARDS)', ylim=(15, 32)) axes[0].set_xticks(peep_vals) axes[1].plot(peep_vals, crs_r, 's-', color=MC['ards'], lw=2, ms=7, label='Recruitable') axes[1].axvline(14, color=MC['ok'], lw=1.5, ls='--', alpha=0.8) axes[1].text(14.2, 30, 'Optimal\nPEEP = 14\n(best Crs)', fontsize=7.5, color=MC['ok'], fontweight='bold') axes[1].fill_betweenx([22,31], [12,12],[16,16], alpha=0.12, color=MC['ok']) axes[1].text(7, 20, 'Derecruitment\nat low PEEP', fontsize=7, color=MC['ards']) axes[1].annotate('', xy=(7,18.5), xytext=(8,19.5), arrowprops=dict(arrowstyle='->', color=MC['ards'], lw=1)) setup_ax(axes[1], xlabel='PEEP (cmH₂O)', ylabel='Crs (mL/cmH₂O)', title='Recruitable Lung\n(~50% of ARDS)', ylim=(15, 34)) axes[1].set_xticks(peep_vals) return fig # ── Card 12: ARDS P-V loop with LIP + UIP ───────────────────────────────────── def draw_card12_ards_pv_annotated(): fig, ax = plt.subplots(figsize=(7, 5), facecolor='white') fig.subplots_adjust(left=0.12, right=0.97, top=0.88, bottom=0.1) ax.set_title('ARDS P-V Loop — Inflection Points, Safe Window & VILI Zones', fontsize=10, fontweight='bold', color=MC['ax']) # Main sigmoidal curve p = np.linspace(3, 42, 200) v_insp = 0.45*(1/(1+np.exp(-(p-20)/4.5))) p_exp = np.linspace(42, 3, 200) v_exp = 0.45*(1/(1+np.exp(-(p_exp-17)/4.5))) - 0.02 ax.plot(p, v_insp, color=MC['ards'], lw=3, label='Inspiration', zorder=5) ax.plot(p_exp, v_exp, color=MC['ards'], lw=2, ls='--', alpha=0.8, label='Expiration', zorder=5) ax.fill_betweenx(np.concatenate([v_insp, v_exp[::-1]]), np.concatenate([p, p_exp[::-1]]), alpha=0.07, color=MC['ards']) # Zones ax.axvspan(3, 14, alpha=0.12, color=MC['annot'], zorder=0) # atelectasis ax.axvspan(14, 30, alpha=0.10, color=MC['ok'], zorder=0) # safe window ax.axvspan(30, 42, alpha=0.12, color=MC['ards'], zorder=0) # overdistension ax.text(5, 0.42, 'ATELECTASIS\nZONE\n(derecruitment)', fontsize=7.5, color=MC['annot'], fontweight='bold', ha='center') ax.text(22, 0.43, 'SAFE WINDOW\n(target PEEP\nand DP here)', fontsize=7.5, color=MC['ok'], fontweight='bold', ha='center') ax.text(36, 0.43, 'OVERDISTENSION\nZONE\n(VILI)', fontsize=7.5, color=MC['ards'], fontweight='bold', ha='center') # LIP annotation lip_p = 14 lip_v = 0.45*(1/(1+np.exp(-(lip_p-20)/4.5))) ax.plot(lip_p, lip_v, 'o', color=MC['annot'], ms=9, zorder=6) ax.annotate('LIP\n(Lower Inflection\nPoint) ~14 cmH₂O\nOptimal PEEP >LIP', xy=(lip_p, lip_v), xytext=(5, 0.28), fontsize=7.5, color=MC['annot'], fontweight='bold', arrowprops=dict(arrowstyle='->', color=MC['annot'], lw=1.2)) # UIP annotation uip_p = 30 uip_v = 0.45*(1/(1+np.exp(-(uip_p-20)/4.5))) ax.plot(uip_p, uip_v, 's', color=MC['warn'], ms=9, zorder=6) ax.annotate('UIP\n(Upper Inflection\nPoint) ~30 cmH₂O\nPplat should be <UIP', xy=(uip_p, uip_v), xytext=(32, 0.22), fontsize=7.5, color=MC['warn'], fontweight='bold', arrowprops=dict(arrowstyle='->', color=MC['warn'], lw=1.2)) # Hysteresis label ax.text(20, 0.1, 'Hysteresis\n(area between curves\n= energy dissipated)', fontsize=7, color=DGREY.hexval() if hasattr(DGREY,'hexval') else 'grey', ha='center', style='italic') ax.legend(fontsize=8, loc='lower right', framealpha=0.9) setup_ax(ax, xlabel='Pressure (cmH₂O)', ylabel='Volume (L)', xlim=(0, 44), ylim=(-0.03, 0.52)) return fig # ══ CARD DATA ══════════════════════════════════════════════════════════════════ CARDS = [ { 'num': '01', 'category': 'NORMAL WAVEFORMS', 'cat_col': GRN_D, 'title': 'Normal VCV — Pressure / Flow / Volume', 'draw_fn': draw_card1_normal_vcv, 'q': 'Describe the expected pressure, flow, and volume waveforms during normal Volume-Controlled Ventilation (VCV). What shape is the inspiratory flow curve?', 'answer': [ ('Pressure-time', 'Rapid rise to Peak pressure → plateau at Pplat → fall at expiration. ' 'PEEP visible as baseline. DP = Pplat − PEEP.'), ('Flow-time', 'SQUARE WAVE (constant) inspiratory flow (key VCV feature). ' 'Passive exponential expiratory flow returns to ZERO before next breath.'), ('Volume-time', 'Ramp rise during inspiration (linear with square flow). ' 'Exponential return to zero during passive expiration.'), ], 'pearls': [ 'VCV = constant flow, variable pressure. PCV = constant pressure, variable (decelerating) flow.', 'Expiratory flow MUST return to zero — if it does not → auto-PEEP.', 'Pplat requires inspiratory hold (0.5–2 s pause). Ppeak − Pplat = resistive component.', ], 'trap': 'Confusing VCV (square flow) with PCV (decelerating flow) — examiner will ask you to identify the mode from the waveform alone.', 'ref': 'ESICM Ventilator Graphics Module; Nilsestuen & Hargett, Respir Care 2005', }, { 'num': '02', 'category': 'NORMAL WAVEFORMS', 'cat_col': GRN_D, 'title': 'Normal PCV — Pressure / Flow / Volume', 'draw_fn': draw_card2_normal_pcv, 'q': 'What are the characteristic waveform features of Pressure-Controlled Ventilation (PCV)? How does the flow pattern differ from VCV?', 'answer': [ ('Pressure-time', 'SQUARE pressure wave — pressure instantly rises to set level and holds throughout inspiration. Sharp fall at end-inspiration.'), ('Flow-time', 'DECELERATING inspiratory flow (key PCV feature) — peaks at breath onset, declines exponentially as lungs fill.'), ('Volume-time', 'Exponential ramp (follows flow integral). Vt varies with compliance and resistance — NOT fixed.'), ], 'pearls': [ 'PCV advantage: lower peak pressure (pressure-limited). Disadvantage: Vt varies — must monitor closely in ARDS.', 'If compliance worsens in ARDS (PCV mode) → Vt drops silently without alarming → check Vt frequently.', 'In PCV, a RISING Vt = improving compliance (good sign in ARDS recovery).', ], 'trap': 'In PCV, the Vt is NOT guaranteed — a candidate who says "PCV is safer because pressures are controlled" must also say "but I must monitor Vt to ensure ≤6 mL/kg IBW".', 'ref': 'ESICM ARDS 2023; Blanch et al. Respir Care 2007', }, { 'num': '03', 'category': 'AUTO-PEEP', 'cat_col': AMB_D, 'title': 'Auto-PEEP — Dynamic Hyperinflation', 'draw_fn': draw_card3_auto_peep, 'q': 'Identify the auto-PEEP pattern on the flow-time and volume-time curves. What causes it and what are the consequences?', 'answer': [ ('Flow-time sign', 'Expiratory flow does NOT return to zero baseline before the next breath begins — the defining graphic sign of auto-PEEP / dynamic hyperinflation.'), ('Volume-time sign', 'Progressive rise in end-expiratory volume between breaths — gas trapping.'), ('Causes', 'High RR (insufficient Te), high Vt, increased airway resistance (bronchospasm, secretions), reduced elastic recoil (COPD), small ET tube.'), ('Consequences', 'Total PEEP > set PEEP; reduced venous return → haemodynamic instability; barotrauma; increased WOB (trigger effort must overcome auto-PEEP).'), ], 'pearls': [ 'Measure auto-PEEP: expiratory hold (3–5 s) → pressure equilibrates to total PEEP. auto-PEEP = total PEEP − set PEEP.', 'In haemodynamically unstable ventilated patient → always rule out auto-PEEP.', 'Rx: ↓ RR, ↓ Vt, ↑ expiratory time (lower I:E ratio e.g. 1:3), bronchodilators, ETT suctioning.', ], 'trap': 'ARDS is NOT classically obstructive — but high RR to correct hypercapnia causes auto-PEEP in ARDS. The examiner will ask: "You increase RR to 28. What happens to the flow-time curve?"', 'ref': 'Tobin MJ, Lodato RF. Am Rev Respir Dis 1989; ESICM 2023', }, { 'num': '04', 'category': 'ARDS WAVEFORMS', 'cat_col': RED_D, 'title': 'ARDS vs Normal — Pressure-Time Comparison', 'draw_fn': draw_card4_ards_pressure, 'q': 'Compare the pressure-time waveform in a normal lung vs ARDS lung for the SAME tidal volume. What differences do you see and why?', 'answer': [ ('Normal', 'Pplat 21 cmH₂O, PEEP 5, DP = 16 cmH₂O. Crs = Vt/DP = 500/16 ≈ 31 mL/cmH₂O (slightly low for illustration).'), ('ARDS', 'Pplat 36 cmH₂O, PEEP 14, DP = 22 cmH₂O ⚠. Crs = Vt/DP = 420/22 ≈ 19 mL/cmH₂O (severely reduced).'), ('Key message', 'SAME tidal volume → much higher pressures in ARDS. MUST reduce Vt to ≤6 mL/kg IBW. ARDS lung is a "baby lung" — the aerated portion receives the full Vt stress.'), ], 'pearls': [ 'Driving pressure (Pplat − PEEP) is the stress per unit of aerated lung (baby lung stress).', 'Target DP < 15 cmH₂O (ESICM 2023; Amato NEJM 2015).', 'Crs formula: Crs = Vt / (Pplat − PEEP). Normal ≥ 50 mL/cmH₂O. ARDS < 30 mL/cmH₂O.', ], 'trap': 'Using actual body weight Vt in ARDS → even higher pressures than shown. IBW MUST be used.', 'ref': 'Gattinoni & Pesenti, ICM 2005; Amato NEJM 2015; ARMA NEJM 2000', }, { 'num': '05', 'category': 'LOOPS', 'cat_col': PUR_D, 'title': 'P-V Loop — Normal, ARDS, Overdistension', 'draw_fn': draw_card5_pv_loop, 'q': 'Describe the pressure-volume (P-V) loop in a normal lung, ARDS, and a lung with overdistension. What are the inflection points?', 'answer': [ ('Normal', 'Steep slope (high compliance ~50 mL/cmH₂O). Elliptical loop. Small hysteresis area.'), ('ARDS', 'Reduced slope (low compliance ~20 mL/cmH₂O). Lower Inflection Point (LIP) ~10-15 cmH₂O — below this, recruitment drops sharply. Upper Inflection Point (UIP) ~30 cmH₂O — above this, overdistension. Safe window between LIP and UIP.'), ('Overdistension', '"Beaking" pattern at high pressures — curve flattens (loss of compliance at high volumes). Represents VILI zone.'), ], 'pearls': [ 'Optimal PEEP should be set above the LIP to prevent end-expiratory derecruitment.', 'Pplat should stay below the UIP to avoid overdistension.', 'The area inside the loop = energy dissipated per breath (mechanical energy). Larger loop = more VILI.', ], 'trap': 'The P-V loop cannot be easily performed at bedside in most units — examiner may ask "how else do you identify the safe window?" Answer: decremental PEEP trial to best compliance.', 'ref': 'Gattinoni et al. ICM 2001; ESICM ARDS 2023', }, { 'num': '06', 'category': 'LOOPS', 'cat_col': PUR_D, 'title': 'Flow-Volume Loop — Normal, Obstructive, Restrictive/ARDS', 'draw_fn': draw_card6_fv_loop, 'q': 'Interpret flow-volume loops. What is "scalloping" and what does a narrow loop indicate in ARDS?', 'answer': [ ('Normal', 'Rectangular loop. Square inspiratory flow (VCV). Smooth expiratory limb. Full volume return.'), ('Obstructive / auto-PEEP', '"Scalloping" of the expiratory limb — concave expiratory curve indicating expiratory flow limitation. Incomplete volume return. Seen in bronchospasm, dynamic hyperinflation.'), ('Restrictive / ARDS', 'NARROW loop — reduced Vt (low compliance = less volume delivered). Steep expiratory limb (high elastic recoil). Normal or high flow rates relative to small volume.'), ], 'pearls': [ 'Scalloping specifically indicates expiratory flow limitation and predicts auto-PEEP.', 'In ARDS, the F-V loop is narrow and steep — reflects "stiff" low-compliance lung.', 'A sudden narrowing of the F-V loop = acute decrease in compliance → pneumothorax? Plugging? Tube migration?', ], 'trap': 'Scalloping is a sign of OBSTRUCTIVE disease — it does NOT occur in ARDS. An ARDS F-V loop is narrow but NOT scalloped.', 'ref': 'Nilsestuen & Hargett, Respir Care 2005; Blanch et al. 2007', }, { 'num': '07', 'category': 'ARDS WAVEFORMS', 'cat_col': RED_D, 'title': 'Severe ARDS — Full Three-Waveform Analysis', 'draw_fn': draw_card7_ards_full, 'q': 'A patient with severe ARDS is on VCV: PEEP 14, Vt 420 mL, RR 22. Pplat is 36, DP is 22. Interpret the three waveforms and identify what needs to change.', 'answer': [ ('Problem 1', 'Pplat 36 cmH₂O > 30 cmH₂O target (ESICM 2023 strong rec). → Reduce Vt.'), ('Problem 2', 'DP 22 cmH₂O >> 15 cmH₂O target (Amato 2015). → Further reduce Vt and/or optimise PEEP.'), ('Problem 3', 'Crs = 420/22 = 19 mL/cmH₂O — severely stiff lung. Baby lung concept applies.'), ('Actions', '↓ Vt toward 350 mL (5 mL/kg IBW 70 kg). Accept permissive hypercapnia (pH ≥ 7.20). Consider prone positioning. Reassess PEEP with decremental trial.'), ], 'pearls': [ 'IBW 70 kg (175 cm male) → 6 mL/kg = 420 mL → 5 mL/kg = 350 mL → 4 mL/kg = 280 mL.', 'Mechanical power = 0.098 × RR × Vt(L) × (DP + PEEP). At current settings: ≈ 22 J/min > 17 J/min threshold.', 'Prone positioning increases Crs by recruiting dorsal atelectatic zones → reduces DP.', ], 'trap': 'Pplat 36 "only just above 30" is still a strong-recommendation violation. Never accept Pplat > 30 in ARDS.', 'ref': 'ESICM ARDS 2023; Amato NEJM 2015; Gattinoni Mechanical Power ICM 2016', }, { 'num': '08', 'category': 'MEASUREMENTS', 'cat_col': TEAL, 'title': 'Inspiratory Hold — Plateau Pressure & Resistive Component', 'draw_fn': draw_card8_pplat, 'q': 'How do you measure plateau pressure? What does the difference between Peak pressure and Pplat represent? Draw what happens on the pressure-time curve.', 'answer': [ ('Method', 'Apply an inspiratory hold (0.5–2.0 s) at end-inspiration. Airflow ceases → pressure drops from Peak to Pplat (the equilibrated pressure reflecting elastic recoil only).'), ('Ppeak − Pplat', 'Represents the resistive component of airways (resistance × flow). Normal < 10 cmH₂O. Elevated in bronchospasm, secretions, kinked ETT.'), ('Pplat interpretation', 'Reflects elastic work of breathing. Target < 30 cmH₂O (ESICM). Determines driving pressure = Pplat − PEEP.'), ], 'pearls': [ 'High Ppeak with NORMAL Pplat → airway resistance problem (suction, bronchodilate).', 'High Ppeak WITH high Pplat → compliance problem (ARDS, pneumothorax, main-stem intubation).', 'The inspiratory hold must be done with the patient deeply sedated/paralysed — patient effort invalidates the manoeuvre.', ], 'trap': 'Examining Ppeak alone without Pplat is insufficient — they reflect different physiological components. Examiner will distinguish them.', 'ref': 'Tobin MJ. Respiratory Monitoring. JAMA 1990; ESICM 2023', }, { 'num': '09', 'category': 'MEASUREMENTS', 'cat_col': TEAL, 'title': 'Expiratory Hold — Auto-PEEP Measurement', 'draw_fn': draw_card9_exp_hold, 'q': 'How do you measure auto-PEEP using the expiratory hold manoeuvre? Describe what you see on the pressure and flow curves.', 'answer': [ ('Method', 'Apply an expiratory hold (3–5 s) at end-expiration. Exhalation valve closes → any trapped gas equilibrates, raising airway pressure above set PEEP.'), ('Flow curve', 'During hold, flow = 0 (held closed). Before hold: expiratory flow does not return to zero — confirms gas trapping.'), ('Pressure reading', 'Pressure rises above set PEEP to a new plateau = Total PEEP. auto-PEEP = Total PEEP − Set PEEP. In this example: 13 − 8 = 5 cmH₂O auto-PEEP.'), ], 'pearls': [ 'auto-PEEP must be added when calculating True DP: True DP = Pplat − Total PEEP (not Pplat − set PEEP).', 'In spontaneously breathing patients, expiratory hold underestimates auto-PEEP (patient effort reopens airways).', 'Clinical clue: unexplained hypotension in ventilated patient → check for auto-PEEP (disconnect from ventilator briefly — if MAP rises = auto-PEEP was causing reduced venous return).', ], 'trap': 'Using SET PEEP in the DP formula when auto-PEEP exists → UNDERESTIMATES True DP and gives false reassurance.', 'ref': 'Pepe & Marini, Am Rev Respir Dis 1982; ESICM 2023', }, { 'num': '10', 'category': 'ASYNCHRONY', 'cat_col': AMB_D, 'title': 'Patient-Ventilator Asynchrony — 4 Patterns', 'draw_fn': draw_card10_asynch, 'q': 'Identify the four patterns of patient-ventilator asynchrony shown. What is the clinical consequence of each and how is each managed?', 'answer': [ ('(a) Double triggering', 'Patient effort outlasts ventilator breath → triggers second breath. Delivers stacked Vt (effective Vt doubles) → VILI. Rx: ↑ Ti, ↑ sedation, switch to PSV.'), ('(b) Flow starvation', 'Patient demand exceeds set flow (VCV) → pressure dips below PEEP baseline during inspiration. Rx: ↑ inspiratory flow rate, switch to PCV/PSV.'), ('(c) Reverse triggering', 'Ventilator breath triggers patient diaphragmatic contraction in passive/sedated patient. Subtly increases delivered Vt and risk of P-SILI. Rx: deeper sedation ± NMB.'), ('(d) Premature cycling', 'Ventilator terminates breath before patient effort ends → patient "fights" against closing valve. Rx: ↑ Texp (lengthen breath), adjust flow-cycle threshold in PSV.'), ], 'pearls': [ 'Asynchrony index > 10% is associated with prolonged ventilation and increased mortality.', 'Double triggering is the MOST dangerous for ARDS — stacked breaths can double Vt and cause acute VILI.', 'Expiratory muscle activity during inspiration (abdominal EMG, oesophageal pressure swings) is the gold standard for detecting asynchrony.', ], 'trap': 'Simply increasing sedation for "fighting the ventilator" without identifying the asynchrony type is wrong — each type has a specific fix. Deep sedation alone delays liberation.', 'ref': 'Thille et al. ICM 2006; Vignaux et al. ICM 2009; ESICM 2023', }, { 'num': '11', 'category': 'PEEP TITRATION', 'cat_col': GRN_D, 'title': 'Decremental PEEP Trial — Compliance Curves', 'draw_fn': draw_card11_peep_compliance, 'q': 'Describe the decremental PEEP trial method. What do the two compliance-vs-PEEP curves represent and how do you identify optimal PEEP?', 'answer': [ ('Method', 'After a recruitment manoeuvre (or in its absence per ESICM 2023), PEEP is decreased in steps (e.g. 2 cmH₂O every 3–5 breaths from 20 to 6). Static compliance is calculated at each step (Crs = Vt/(Pplat−PEEP)). Optimal PEEP = PEEP of best Crs.'), ('Non-recruitable lung', 'Compliance peaks at a lower PEEP (~10-12 cmH₂O). Higher PEEP causes overdistension (Crs falls at high PEEP).'), ('Recruitable lung', 'Compliance continues to rise with increasing PEEP up to ~14 cmH₂O — reflecting progressive alveolar opening. Falls sharply at low PEEP (derecruitment).'), ], 'pearls': [ 'The optimal PEEP identified by best compliance balances recruitment vs overdistension.', 'After the decremental trial, increase PEEP back to the optimal level and re-measure Pplat.', 'ESICM 2023 recommends AGAINST performing a staircase RM before the decremental trial — just start the decremental trial.', ], 'trap': 'Assuming all ARDS is recruitable — ~50% are not. In non-recruitable lungs, high PEEP worsens DP. You must individualise PEEP.', 'ref': 'Suter PM et al. NEJM 1975; ESICM ARDS 2023; ART JAMA 2017', }, { 'num': '12', 'category': 'ARDS P-V LOOP', 'cat_col': RED_D, 'title': 'ARDS P-V Loop — LIP, UIP, Safe Window', 'draw_fn': draw_card12_ards_pv_annotated, 'q': 'Annotate the ARDS P-V loop. What are LIP and UIP? Where should PEEP be set and where should Pplat be maintained?', 'answer': [ ('LIP', 'Lower Inflection Point (~10-15 cmH₂O): point below which compliance drops sharply = derecruitment. Optimal PEEP should be SET ABOVE LIP by 2 cmH₂O.'), ('UIP', 'Upper Inflection Point (~28-32 cmH₂O): point above which compliance flattens = overdistension / VILI. Pplat should remain BELOW UIP.'), ('Safe window', 'Between LIP and UIP: alveoli remain open, no overdistension. This is the target zone for PEEP and Pplat in ARDS.'), ('Hysteresis', 'Area between inspiratory and expiratory limbs = energy dissipated per breath. Larger hysteresis = more energy delivered to lung = higher VILI risk.'), ], 'pearls': [ 'P-V loop is a QUASI-STATIC manoeuvre — requires slow inflation at very low flow (usually 10 L/min). Rarely performed at bedside; mostly research use.', 'Surrogate for LIP in clinical practice: PEEP of best compliance on decremental trial.', 'Surrogate for UIP: Pplat < 28-30 cmH₂O (ESICM strong recommendation).', ], 'trap': 'LIP does NOT equal optimal PEEP — set PEEP = LIP + 2 cmH₂O (to sit above LIP and maintain recruitment). Examiner will test this distinction.', 'ref': 'Gattinoni et al. ICM 2001; Rimensberger & Cheifetz Ped Crit Care 2011; ESICM 2023', }, ] # ══ PDF builder ═══════════════════════════════════════════════════════════════ def cat_banner(text, bg, w_mm=None): class Banner(Flowable): def __init__(self, t, bg, w): super().__init__(); self._t=t; self._bg=bg; self._w=w def wrap(self,aw,ah): self.width=self._w*mm if self._w else aw; self.height=13; return self.width,self.height def draw(self): c=self.canv; c.setFillColor(self._bg) c.rect(0,0,self.width,self.height,stroke=0,fill=1) c.setFillColor(WHT); c.setFont('Helvetica-Bold',8) c.drawString(6,(self.height-8)/2+1,self._t) return Banner(text,bg,w_mm) def build_flashcard_pdf(out_path): doc = SimpleDocTemplate(out_path, pagesize=A4, leftMargin=LM, rightMargin=RM, topMargin=TM+38, bottomMargin=BM+16, title='EDIC Part II Ventilator Graphics Flashcard Deck', author='ESICM EDIC') story = [] add = story.append # ── Cover page ──────────────────────────────────────────────────────────── add(Spacer(1, 8)) cov = [[Paragraph( 'EDIC Part II — Ventilator Graphics<br/>Flashcard Deck', ParagraphStyle('cov',fontName='Helvetica-Bold',fontSize=20, textColor=NAVY,alignment=TA_CENTER,leading=26))]] add(Table(cov,colWidths=[AW], style=TableStyle([('TOPPADDING',(0,0),(-1,-1),20), ('BOTTOMPADDING',(0,0),(-1,-1),20), ('BACKGROUND',(0,0),(-1,-1),CARD_B), ('BOX',(0,0),(-1,-1),2,NAVY)]))) add(Spacer(1,8)) add(Paragraph( 'Complete coverage of pressure, flow, and volume waveforms; ' 'P-V and F-V loops; ARDS-specific patterns; auto-PEEP; asynchrony; ' 'and PEEP titration — with real examiner-style questions and clinical pearls. ' 'Based on <b>ESICM ARDS Guidelines 2023</b> and <b>EDIC Part II syllabus</b>.', ParagraphStyle('ci',fontSize=10,leading=15,alignment=TA_JUSTIFY, textColor=BLK))) add(Spacer(1,10)) # TOC add(cat_banner('CONTENTS', NAVY)) add(Spacer(1,4)) toc_items = [ ('01-02', 'Normal Waveforms', 'VCV & PCV pressure / flow / volume', GRN_D), ('03', 'Auto-PEEP', 'Dynamic hyperinflation — flow & volume signs', AMB_D), ('04', 'ARDS Waveforms', 'Pressure-time normal vs ARDS comparison', RED_D), ('05-06', 'Loops', 'P-V loop & F-V loop — normal, ARDS, overdistension', PUR_D), ('07', 'ARDS Full', 'Severe ARDS three-waveform analysis', RED_D), ('08-09', 'Measurements', 'Inspiratory hold (Pplat) & expiratory hold (auto-PEEP)', TEAL), ('10', 'Asynchrony', 'Four asynchrony patterns', AMB_D), ('11', 'PEEP Titration', 'Decremental trial compliance curves', GRN_D), ('12', 'P-V Loop ARDS', 'LIP, UIP, safe window annotated', RED_D), ] for num, cat, desc, col in toc_items: row_data = [[ Paragraph(f'<b>Card {num}</b>', ParagraphStyle('tn',fontName='Helvetica-Bold', fontSize=9,textColor=WHT,alignment=TA_CENTER)), Paragraph(f'<b>{cat}</b> — {desc}', ParagraphStyle('td',fontSize=9,textColor=BLK,leading=13)), ]] add(Table(row_data, colWidths=[22*mm, AW-22*mm], style=TableStyle([ ('BACKGROUND',(0,0),(0,0),col), ('BACKGROUND',(1,0),(1,0),LGREY), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),6), ('BOTTOMPADDING',(0,0),(-1,-1),0), ('BOX',(0,0),(-1,-1),0.3,MGREY), ]))) add(Spacer(1,1)) add(Spacer(1,6)) add(Paragraph( '<i>Instructions: Each card has a FRONT (waveform + question) and BACK (answer + pearls). ' 'Cover the answer panel and attempt interpretation before reading the key.</i>', ParagraphStyle('ins',fontSize=8.5,textColor=DGREY,fontName='Helvetica-Oblique', alignment=TA_CENTER))) # ── Individual cards ────────────────────────────────────────────────────── for card in CARDS: add(PageBreak()) cat_col = card['cat_col'] # ── FRONT of card ───────────────────────────────────────────────────── # Header band hdr_data = [[ Paragraph(f'CARD {card["num"]}', ParagraphStyle('ch',fontName='Helvetica-Bold',fontSize=16, textColor=WHT,alignment=TA_CENTER)), Table([[Paragraph(card['category'], ParagraphStyle('cat',fontName='Helvetica-Bold',fontSize=10, textColor=WHT,alignment=TA_CENTER))], [Paragraph(card['title'], ParagraphStyle('ct2',fontName='Helvetica-Bold',fontSize=12, textColor=WHT,leading=16))], ], colWidths=[AW-20*mm], style=TableStyle([('BACKGROUND',(0,0),(-1,-1),cat_col), ('TOPPADDING',(0,0),(-1,-1),2), ('BOTTOMPADDING',(0,0),(-1,-1),2), ('LEFTPADDING',(0,0),(-1,-1),6)])), ]] add(Table(hdr_data, colWidths=[20*mm, AW-20*mm], style=TableStyle([ ('BACKGROUND',(0,0),(0,0),cat_col), ('BACKGROUND',(1,0),(1,0),cat_col), ('TOPPADDING',(0,0),(-1,-1),8), ('BOTTOMPADDING',(0,0),(-1,-1),8), ('LEFTPADDING',(0,0),(-1,-1),4), ('VALIGN',(0,0),(-1,-1),'MIDDLE'), ]))) # Question box add(Spacer(1,4)) q_box = [[Paragraph('<b>❓ QUESTION:</b>', ST['label'])], [Paragraph(card['q'], ST['body'])]] add(Table(q_box, colWidths=[AW], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1),TEAL_L), ('BOX',(0,0),(-1,-1),1,TEAL), ('TOPPADDING',(0,0),(-1,-1),5), ('BOTTOMPADDING',(0,0),(-1,-1),5), ('LEFTPADDING',(0,0),(-1,-1),8), ]))) # Waveform image add(Spacer(1,5)) fig = card['draw_fn']() img_w = 165 # mm img_h = img_w * (fig.get_figheight()/fig.get_figwidth()) img = fig_to_img(fig, img_w, img_h, dpi=130) # Centre the image add(Table([[img]],colWidths=[AW], style=TableStyle([('ALIGN',(0,0),(-1,-1),'CENTER'), ('LEFTPADDING',(0,0),(-1,-1),0), ('RIGHTPADDING',(0,0),(-1,-1),0), ('TOPPADDING',(0,0),(-1,-1),0), ('BOTTOMPADDING',(0,0),(-1,-1),0)]))) add(Paragraph('<i>↓ Answer on next section ↓</i>', ParagraphStyle('hint',fontSize=8,textColor=DGREY, alignment=TA_CENTER,fontName='Helvetica-Oblique'))) add(Spacer(1,4)) # ── BACK of card ────────────────────────────────────────────────────── back_hdr = [[Paragraph(f'Card {card["num"]} — ANSWER & CLINICAL PEARLS', ParagraphStyle('bh',fontName='Helvetica-Bold',fontSize=10, textColor=WHT,alignment=TA_CENTER))]] add(Table(back_hdr, colWidths=[AW], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1),cat_col), ('TOPPADDING',(0,0),(-1,-1),5), ('BOTTOMPADDING',(0,0),(-1,-1),5), ]))) add(Spacer(1,3)) # Answer rows ans_rows = [] for label, text in card['answer']: ans_rows.append([ Paragraph(f'<b>{label}</b>', ST['label']), Paragraph(text, ST['answer']), ]) if ans_rows: add(Table(ans_rows, colWidths=[32*mm, AW-32*mm], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1),GRN_L), ('BOX',(0,0),(-1,-1),0.5,GRN_D), ('INNERGRID',(0,0),(-1,-1),0.3,MGREY), ('VALIGN',(0,0),(-1,-1),'TOP'), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),6), ]))) add(Spacer(1,4)) # Pearls pearl_rows = [[Paragraph('💎 CLINICAL PEARLS', ST['label'])]] for p in card['pearls']: pearl_rows.append([Paragraph(f'• {p}', ST['pearl'])]) add(Table(pearl_rows, colWidths=[AW], style=TableStyle([ ('BACKGROUND',(0,0),(-1,-1),AMB_L), ('BOX',(0,0),(-1,-1),0.5,AMB_D), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),3), ('LEFTPADDING',(0,0),(-1,-1),8), ]))) add(Spacer(1,3)) # Trap + Ref trap_ref_data = [[ Table([[Paragraph('⚠ EXAMINER TRAP', ST['bold'])], [Paragraph(card['trap'], ST['warn'])]], colWidths=[AW*0.60], style=TableStyle([('BACKGROUND',(0,0),(-1,-1),RED_L), ('BOX',(0,0),(-1,-1),0.5,RED_D), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),6)])), Spacer(3*mm,1), Table([[Paragraph('📋 REFERENCE', ST['bold'])], [Paragraph(card['ref'], ST['ref'])]], colWidths=[AW*0.37], style=TableStyle([('BACKGROUND',(0,0),(-1,-1),LGREY), ('BOX',(0,0),(-1,-1),0.5,MGREY), ('TOPPADDING',(0,0),(-1,-1),4), ('BOTTOMPADDING',(0,0),(-1,-1),4), ('LEFTPADDING',(0,0),(-1,-1),6)])), ]] add(Table(trap_ref_data, colWidths=[AW*0.60,3*mm,AW*0.37], style=TableStyle([('VALIGN',(0,0),(-1,-1),'TOP'), ('LEFTPADDING',(0,0),(-1,-1),0), ('RIGHTPADDING',(0,0),(-1,-1),0), ('TOPPADDING',(0,0),(-1,-1),0), ('BOTTOMPADDING',(0,0),(-1,-1),0)]))) add(Spacer(1,4)) add(Paragraph(f'<i>EDIC Part II Flashcard Deck • Card {card["num"]} of 12 • July 2026</i>', ST['footer'])) doc.build(story, onFirstPage=hf, onLaterPages=hf) print(f'Done: {out_path}') if __name__ == '__main__': out = '/home/daytona/workspace/edic-ards-ccs/EDIC_Ventilator_Graphics_Flashcards.pdf' build_flashcard_pdf(out) PYEOF python3 /home/daytona/workspace/edic-ards-ccs/build_flashcards.py
sed -i "s/MGREY.hexval() if hasattr(MGREY,'hexval') else 'grey'/'#D0D0D0'/g" /home/daytona/workspace/edic-ards-ccs/build_flashcards.py sed -i "s/DGREY.hexval() if hasattr(DGREY,'hexval') else 'grey'/'#555555'/g" /home/daytona/workspace/edic-ards-ccs/build_flashcards.py python3 /home/daytona/workspace/edic-ards-ccs/build_flashcards.py
python3 -c " content = open('/home/daytona/workspace/edic-ards-ccs/build_flashcards.py').read() content = content.replace( \"axes[0].axhline(8, color=MGREY.hexval() if hasattr(MGREY,'hexval') else 'grey',\", \"axes[0].axhline(8, color='#D0D0D0',\" ) content = content.replace( \"ax.text(0.1, 5.5, 'Set PEEP = 8', fontsize=7, color='grey')\", \"ax.text(0.1, 5.5, 'Set PEEP = 8', fontsize=7, color='#888888')\" ) # Also fix DGREY usage in matplotlib content = content.replace( \"color=DGREY.hexval() if hasattr(DGREY,'hexval') else 'grey'\", \"color='#555555'\" ) open('/home/daytona/workspace/edic-ards-ccs/build_flashcards.py','w').write(content) print('patched') " python3 /home/daytona/workspace/edic-ards-ccs/build_flashcards.py
python3 -c " from pypdf import PdfReader import os r = PdfReader('/home/daytona/workspace/edic-ards-ccs/EDIC_Ventilator_Graphics_Flashcards.pdf') sz = os.path.getsize('/home/daytona/workspace/edic-ards-ccs/EDIC_Ventilator_Graphics_Flashcards.pdf') print(f'Pages: {len(r.pages)}, Size: {sz/1024:.0f} KB') for i,p in enumerate(r.pages): t=p.extract_text()[:60].strip().replace(chr(10),' ') print(f' P{i+1:02d}: {t!r}') "
EDIC Ventilator Graphics Flashcards
PDF Document · PDF
| Front | Back |
|---|---|
| Real matplotlib-plotted waveform | Labelled interpretation |
| Examiner-style question | Colour-coded answer rows (green) |
| Annotated axes with clinical values | Clinical Pearls (amber) |
| Examiner Trap (red) + Reference |
| Card | Topic | What's Shown |
|---|---|---|
| 01 | Normal VCV | Pressure-time, flow-time (square wave), volume-time with PEEP and Pplat labelled |
| 02 | Normal PCV | Decelerating inspiratory flow curve vs VCV square wave |
| 03 | Auto-PEEP | Flow-time: expiratory non-return to zero + progressive gas trapping on volume-time |
| 04 | ARDS Pressure-Time | Side-by-side normal vs ARDS: same Vt → Pplat 21 vs 36, DP 16 vs 22 — visual impact |
| 05 | P-V Loop | Three-panel: normal ellipse, ARDS with LIP/UIP, overdistension "beaking" |
| 06 | F-V Loop | Three-panel: normal rectangle, obstructive scalloping, narrow ARDS loop |
| 07 | Severe ARDS Full | Complete 3-waveform stack: Pplat 36 ⚠, DP 22 ⚠, Crs 19, mechanical power > 17 J/min |
| 08 | Inspiratory Hold | Ppeak 38 → hold → Pplat 28 drop; resistive vs elastic component; DP brace annotated |
| 09 | Expiratory Hold | Auto-PEEP measurement: flow zeroed during hold, pressure rises to total PEEP 13 (set 8 + auto 5) |
| 10 | Asynchrony | Four-panel grid: double triggering, flow starvation, reverse triggering, premature cycling |
| 11 | PEEP Titration | Compliance vs PEEP curves for recruitable vs non-recruitable lung — optimal PEEP identification |
| 12 | ARDS P-V Loop | Fully annotated: LIP, UIP, atelectasis zone (red), safe window (green), overdistension zone (red) |
CCS2 Question Sheet
PDF Document · PDF
CCS2 Answer Sheet Examiner
PDF Document · PDF
EDIC Study Guide 12 Traps
PDF Document · PDF
EDIC Ventilator Graphics Flashcards
PDF Document · PDF
Generate a comprehensive EDIC Part II mock exam with timed scenarios and scoring