Nst in detail
"non-stress test" AND fetal surveillance

| Cause | Notes |
|---|---|
| Fetal sleep cycle | Most common cause; sleep cycles last 20-40 min, occasionally up to 2 hours near term |
| Prematurity | Preterm fetuses have immature autonomic responses |
| Fetal hypoxia / acidemia | Genuine compromise - requires urgent further evaluation |
| Maternal narcotics / opioids | Blunt fetal CNS responses |
| Magnesium sulfate | Depresses fetal neuromuscular activity |
| Fetal CNS or cardiac anomalies | Structural reasons for absent acceleration |
| Extreme prematurity | <28 weeks: criteria modification needed |
| Metric | Value |
|---|---|
| False-negative rate (reactive NST) | ~1.9:1000 (fetal death within 1 week after reactive result) - Creasy & Resnik's |
| False-negative rate (modified BPP = NST + AFI) | ~0.8:1000 (better than NST alone) |
| Positive predictive value of nonreactive NST | 15% (post-term) to 69% (IUGR) - so false-positives are common |
Nonreactive NST
|
↓
Extend monitoring to 40-90 min
(exclude fetal sleep state)
|
Still nonreactive?
|
┌───┴───────────┐
↓ ↓
Vibroacoustic Further testing:
Stimulation - BPP or modified BPP
(VAS) - CST
- Consider delivery if mature
+ favorable cervix
| Component | Normal (2 pts) | Abnormal (0 pts) |
|---|---|---|
| NST | ≥2 accelerations ≥15 bpm above baseline for ≥15 sec | <2 accelerations of sufficient height/duration |
| Amniotic fluid volume | ≥1 pocket ≥2×2 cm | No 2×2 cm pocket or AFI <5 |
| Fetal breathing movements | ≥30 sec sustained breathing | <30 sec |
| Fetal body movements | ≥3 limb/body movements | <3 movements |
| Fetal tone | Extension + return to flexion | Stays extended; no return to flexion |
Generate a clinical reference guide for NST interpretation
ls -la /tmp/workspace/ 2>/dev/null || mkdir -p /tmp/workspace && echo "workspace ready"
mkdir -p /tmp/workspace/nst-guide && python3 - <<'PYEOF' from PIL import Image, ImageDraw, ImageFont import os # Canvas: A3 portrait at 150 DPI = 1240 x 1754 px (good for printing) W, H = 1240, 1754 img = Image.new("RGB", (W, H), "#FAFAFA") draw = ImageDraw.Draw(img) # ── Colour palette ────────────────────────────────────────────────────────── NAVY = "#1B3A6B" TEAL = "#007A8C" GREEN = "#1A7A4A" RED = "#C0392B" AMBER = "#D4850A" WHITE = "#FFFFFF" LGRAY = "#E8EDF2" DGRAY = "#4A4A4A" MGRAY = "#7A7A7A" BORDER = "#B0BEC5" # ── Font helper ───────────────────────────────────────────────────────────── def font(size, bold=False): paths = [ f"/usr/share/fonts/truetype/dejavu/DejaVuSans{'-Bold' if bold else ''}.ttf", f"/usr/share/fonts/truetype/liberation/LiberationSans{'-Bold' if bold else ''}.ttf", ] for p in paths: if os.path.exists(p): return ImageFont.truetype(p, size) return ImageFont.load_default() # ── Rounded rectangle helper ──────────────────────────────────────────────── def rrect(draw, xy, fill, radius=12, outline=None, width=2): x0,y0,x1,y1 = xy draw.rounded_rectangle([x0,y0,x1,y1], radius=radius, fill=fill, outline=outline, width=width) # ── Text wrap helper ──────────────────────────────────────────────────────── def wrap_text(text, font_obj, max_width): words = text.split() lines, line = [], "" for w in words: test = (line + " " + w).strip() bbox = font_obj.getbbox(test) if bbox[2] - bbox[0] <= max_width: line = test else: if line: lines.append(line) line = w if line: lines.append(line) return lines def draw_wrapped(draw, text, xy, font_obj, fill, max_width): x, y = xy lines = wrap_text(text, font_obj, max_width) for ln in lines: draw.text((x, y), ln, font=font_obj, fill=fill) y += font_obj.getbbox("Ag")[3] + 4 return y # ══════════════════════════════════════════════════════════════════════════════ # HEADER # ══════════════════════════════════════════════════════════════════════════════ rrect(draw, [0, 0, W, 100], fill=NAVY) draw.text((W//2, 28), "NON-STRESS TEST (NST)", font=font(38, True), fill=WHITE, anchor="mm") draw.text((W//2, 72), "CLINICAL INTERPRETATION REFERENCE GUIDE", font=font(18), fill="#90CAF9", anchor="mm") # Thin accent bar draw.rectangle([0, 100, W, 108], fill=TEAL) # ══════════════════════════════════════════════════════════════════════════════ # SECTION A — What is NST + Indications (two columns) # ══════════════════════════════════════════════════════════════════════════════ y_sec = 120 COL_L = 30 COL_R = W//2 + 15 COL_W = W//2 - 45 # --- What is NST --- rrect(draw, [COL_L, y_sec, COL_L+COL_W, y_sec+200], fill=LGRAY, outline=BORDER) draw.text((COL_L+14, y_sec+10), "WHAT IS THE NST?", font=font(14, True), fill=NAVY) draw.line([COL_L+14, y_sec+30, COL_L+COL_W-14, y_sec+30], fill=TEAL, width=2) info_lines = [ "• External cardiotocograph (CTG) recording of", " fetal heart rate (FHR) & uterine activity", "• Primary antepartum fetal surveillance tool", "• Reflects fetal autonomic nervous system", " integrity & uteroplacental function", "• No contraindications • Non-invasive", "• Patient semi-recumbent (avoids supine hypotension)", "• Mother presses button on fetal movement", ] fy = y_sec + 38 for ln in info_lines: draw.text((COL_L+14, fy), ln, font=font(12), fill=DGRAY) fy += 19 # --- Indications --- rrect(draw, [COL_R, y_sec, COL_R+COL_W, y_sec+200], fill=LGRAY, outline=BORDER) draw.text((COL_R+14, y_sec+10), "INDICATIONS", font=font(14, True), fill=NAVY) draw.line([COL_R+14, y_sec+30, COL_R+COL_W-14, y_sec+30], fill=TEAL, width=2) ind_lines = [ "• Gestational diabetes (GDM) — uncontrolled", "• Chronic hypertension / pre-eclampsia", "• Intrauterine growth restriction (IUGR/FGR)", "• Post-term pregnancy (≥41–42 weeks)", "• Decreased fetal movement (DFM)", "• Multiple gestation with complications", "• Antiphospholipid syndrome / thyroid disease", " START: 32 wks (low-risk) — earlier if high-risk", ] fy = y_sec + 38 for ln in ind_lines: draw.text((COL_R+14, fy), ln, font=font(12), fill=DGRAY) fy += 19 # ══════════════════════════════════════════════════════════════════════════════ # SECTION B — Interpretation Criteria (full width, 3-column boxes) # ══════════════════════════════════════════════════════════════════════════════ y_crit = y_sec + 215 draw.text((W//2, y_crit), "INTERPRETATION CRITERIA", font=font(17, True), fill=NAVY, anchor="mm") y_crit += 24 THIRD_W = (W - 60) // 3 GAP = 15 # --- REACTIVE --- rx0 = 30 rrect(draw, [rx0, y_crit, rx0+THIRD_W, y_crit+155], fill="#E8F5E9", outline=GREEN, width=3) draw.text((rx0+THIRD_W//2, y_crit+14), "✓ REACTIVE", font=font(15, True), fill=GREEN, anchor="mm") draw.line([rx0+12, y_crit+28, rx0+THIRD_W-12, y_crit+28], fill=GREEN, width=2) react_lines = [ "≥ 2 accelerations in 20 min", "", "Each acceleration must have:", " Peak ≥ 15 bpm above baseline", " Duration ≥ 15 seconds", " (Duration < 2 minutes)", "", "Preterm < 32 wks: ≥10 bpm × ≥10 sec", " (with normal FHR variability)", ] fy = y_crit + 34 for ln in react_lines: draw.text((rx0+12, fy), ln, font=font(11), fill="#1B5E20") fy += 14 # --- NONREACTIVE --- nx0 = rx0 + THIRD_W + GAP rrect(draw, [nx0, y_crit, nx0+THIRD_W, y_crit+155], fill="#FFF3E0", outline=AMBER, width=3) draw.text((nx0+THIRD_W//2, y_crit+14), "⚠ NONREACTIVE", font=font(15, True), fill=AMBER, anchor="mm") draw.line([nx0+12, y_crit+28, nx0+THIRD_W-12, y_crit+28], fill=AMBER, width=2) nonreact_lines = [ "Fails to meet reactive criteria", "within the monitoring period", "", "~10–12% at 30 min (drops to", "<6% by 40 min) — 3rd trimester", "", "Does NOT = fetal jeopardy", "→ Indicates need for further", " evaluation, not immediate action", ] fy = y_crit + 34 for ln in nonreact_lines: draw.text((nx0+12, fy), ln, font=font(11), fill="#7B4400") fy += 14 # --- UNSATISFACTORY --- ux0 = nx0 + THIRD_W + GAP rrect(draw, [ux0, y_crit, ux0+THIRD_W, y_crit+155], fill="#FDECEA", outline=RED, width=3) draw.text((ux0+THIRD_W//2, y_crit+14), "✗ UNSATISFACTORY", font=font(14, True), fill=RED, anchor="mm") draw.line([ux0+12, y_crit+28, ux0+THIRD_W-12, y_crit+28], fill=RED, width=2) unsat_lines = [ "Inadequate tracing quality:", "• Persistent poor signal", "• Cannot assess FHR baseline", "• Unable to detect accelerations", "", "Action:", "• Reposition patient", "• Reapply transducer", "• Repeat test / escalate", ] fy = y_crit + 34 for ln in unsat_lines: draw.text((ux0+12, fy), ln, font=font(11), fill="#8B0000") fy += 14 # ══════════════════════════════════════════════════════════════════════════════ # SECTION C — Causes of Nonreactive NST (full width table) # ══════════════════════════════════════════════════════════════════════════════ y_cause = y_crit + 170 rrect(draw, [30, y_cause, W-30, y_cause+195], fill="#EEF2F7", outline=BORDER) draw.text((W//2, y_cause+12), "CAUSES OF NONREACTIVE NST", font=font(15, True), fill=NAVY, anchor="mm") draw.line([30, y_cause+28, W-30, y_cause+28], fill=TEAL, width=2) causes = [ ("Fetal Sleep Cycle", "MOST COMMON • Quiet sleep 20-40 min (up to 2 hrs near term)\n→ Extend monitoring; use VAS to wake fetus"), ("Prematurity (<32 wks)", "Immature autonomic nervous system\n→ Use modified criteria (10×10 rule)"), ("Fetal Hypoxia/Acidemia", "Genuine compromise — CNS depression from hypoxemia\n→ Urgent BPP or delivery assessment"), ("Maternal Medications", "Opioids/narcotics, Magnesium sulfate, Beta-blockers,\nSteroids — all blunt fetal CNS/cardiac responses"), ("Fetal CNS/Cardiac\nAnomalies", "Structural reasons for absent acceleration\n→ Ultrasound assessment"), ] cx = 40; cy = y_cause + 36 col_gap = (W - 60) // len(causes) for title, desc in causes: draw.text((cx, cy), title, font=font(11, True), fill=NAVY) dy = cy + 16 for dline in desc.split("\n"): draw.text((cx, dy), dline, font=font(10), fill=DGRAY) dy += 14 # vertical divider if cx + col_gap < W - 30: draw.line([cx + col_gap - 8, y_cause+32, cx + col_gap - 8, y_cause+185], fill=BORDER, width=1) cx += col_gap # ══════════════════════════════════════════════════════════════════════════════ # SECTION D — Management Flowchart # ══════════════════════════════════════════════════════════════════════════════ y_flow = y_cause + 208 draw.text((W//2, y_flow), "MANAGEMENT ALGORITHM", font=font(17, True), fill=NAVY, anchor="mm") y_flow += 22 # Flowchart boxes BOX_H = 38 BOX_W = 240 cx_fc = W // 2 def flow_box(y, text, color, text_color=WHITE, w=BOX_W): x0 = cx_fc - w//2; x1 = cx_fc + w//2 rrect(draw, [x0, y, x1, y+BOX_H], fill=color, radius=8) draw.text((cx_fc, y+BOX_H//2), text, font=font(12, True), fill=text_color, anchor="mm") return y + BOX_H def arrow(y_from, y_to, label=""): draw.line([cx_fc, y_from, cx_fc, y_to], fill=DGRAY, width=2) # arrowhead draw.polygon([(cx_fc-7, y_to-10),(cx_fc+7, y_to-10),(cx_fc, y_to)], fill=DGRAY) if label: draw.text((cx_fc+10, (y_from+y_to)//2 - 7), label, font=font(10), fill=MGRAY) yf = y_flow yf_end = flow_box(yf, "NST Ordered (≥32 weeks)", NAVY) arrow(yf_end, yf_end+18) yf = yf_end + 18 yf_end = flow_box(yf, "Monitor 20 Minutes", TEAL) arrow(yf_end, yf_end+14) yf = yf_end + 14 # Decision diamond (simulate with rotated box) def diamond(cx, cy, text, fill_col, w=200, h=44): pts = [(cx, cy),(cx+w//2, cy+h//2),(cx, cy+h),(cx-w//2, cy+h//2)] draw.polygon(pts, fill=fill_col) draw.polygon(pts, outline="#555", width=2) draw.text((cx, cy+h//2), text, font=font(11, True), fill=WHITE, anchor="mm") return cy + h d_bot = diamond(cx_fc, yf, "REACTIVE?", AMBER, w=220, h=48) yf_dbot = d_bot # YES branch (left) draw.line([cx_fc - 110, yf + 24, cx_fc - 180, yf + 24], fill=GREEN, width=2) draw.line([cx_fc - 180, yf + 24, cx_fc - 180, yf + 90], fill=GREEN, width=2) rrect(draw, [cx_fc-300, yf+90, cx_fc-60, yf+140], fill="#E8F5E9", outline=GREEN, width=2) draw.text((cx_fc-180, yf+115), "✓ REACTIVE", font=font(12, True), fill=GREEN, anchor="mm") draw.text((cx_fc-180, yf+130), "Reassuring — routine follow-up", font=font(10), fill=GREEN, anchor="mm") draw.text((cx_fc-155, yf+8), "YES", font=font(10, True), fill=GREEN) # NO branch (right) draw.line([cx_fc + 110, yf + 24, cx_fc + 190, yf + 24], fill=RED, width=2) draw.line([cx_fc + 190, yf + 24, cx_fc + 190, yf + 90], fill=RED, width=2) rrect(draw, [cx_fc+60, yf+90, cx_fc+320, yf+140], fill="#FDECEA", outline=RED, width=2) draw.text((cx_fc+190, yf+107), "Extend to 40 min", font=font(11, True), fill=RED, anchor="mm") draw.text((cx_fc+190, yf+124), "Try Vibroacoustic Stim (VAS)", font=font(10), fill=RED, anchor="mm") draw.text((cx_fc+130, yf+8), "NO", font=font(10, True), fill=RED) # After VAS — still nonreactive? yf_vas = yf + 155 arrow(d_bot + 14, yf_vas, "Still nonreactive") d2_bot = diamond(cx_fc, yf_vas, "STILL NONREACTIVE?", RED, w=240, h=48) # Left: BPP / modified BPP draw.line([cx_fc - 120, yf_vas + 24, cx_fc - 200, yf_vas + 24], fill=AMBER, width=2) draw.line([cx_fc - 200, yf_vas + 24, cx_fc - 200, yf_vas + 90], fill=AMBER, width=2) rrect(draw, [cx_fc-325, yf_vas+90, cx_fc-75, yf_vas+155], fill="#FFF3E0", outline=AMBER, width=2) draw.text((cx_fc-200, yf_vas+107), "Modified BPP", font=font(11, True), fill=AMBER, anchor="mm") draw.text((cx_fc-200, yf_vas+122), "(NST + AFI)", font=font(10), fill=AMBER, anchor="mm") draw.text((cx_fc-200, yf_vas+137), "or Full BPP / CST", font=font(10), fill=AMBER, anchor="mm") draw.text((cx_fc-155, yf_vas+8), "YES", font=font(10, True), fill=AMBER) # Right: Delivery draw.line([cx_fc + 120, yf_vas + 24, cx_fc + 200, yf_vas + 24], fill=GREEN, width=2) draw.line([cx_fc + 200, yf_vas + 24, cx_fc + 200, yf_vas + 90], fill=GREEN, width=2) rrect(draw, [cx_fc+75, yf_vas+90, cx_fc+325, yf_vas+155], fill="#E8F5E9", outline=GREEN, width=2) draw.text((cx_fc+200, yf_vas+107), "✓ Reassuring", font=font(11, True), fill=GREEN, anchor="mm") draw.text((cx_fc+200, yf_vas+122), "Continue routine", font=font(10), fill=GREEN, anchor="mm") draw.text((cx_fc+200, yf_vas+137), "surveillance plan", font=font(10), fill=GREEN, anchor="mm") draw.text((cx_fc+128, yf_vas+8), "NO", font=font(10, True), fill=GREEN) # ══════════════════════════════════════════════════════════════════════════════ # SECTION E — Performance Metrics (bottom, 3 boxes) # ══════════════════════════════════════════════════════════════════════════════ y_metrics = yf_vas + 170 rrect(draw, [30, y_metrics, W-30, y_metrics+130], fill="#EEF2F7", outline=BORDER) draw.text((W//2, y_metrics+10), "PERFORMANCE METRICS & PPV/NPV", font=font(14, True), fill=NAVY, anchor="mm") draw.line([30, y_metrics+26, W-30, y_metrics+26], fill=TEAL, width=2) metrics = [ ("False-Negative Rate\n(Reactive NST)", "~1.9 / 1000\n(fetal death within 1 week)", GREEN), ("False-Negative Rate\n(Modified BPP)", "~0.8 / 1000\n(NST + AFI combined)", TEAL), ("PPV of Nonreactive NST", "15% (post-term)\n→ 69% (IUGR)\n(false-positives are COMMON)", RED), ] mx = 50 for title, val, col in metrics: rrect(draw, [mx, y_metrics+34, mx+350, y_metrics+120], fill=WHITE, outline=col, width=2) ty = y_metrics + 42 for tl in title.split("\n"): draw.text((mx+175, ty), tl, font=font(11, True), fill=col, anchor="mm") ty += 16 ty += 4 for vl in val.split("\n"): draw.text((mx+175, ty), vl, font=font(11), fill=DGRAY, anchor="mm") ty += 15 mx += 375 # ══════════════════════════════════════════════════════════════════════════════ # SECTION F — Modified BPP + BPP Components (side by side) # ══════════════════════════════════════════════════════════════════════════════ y_bpp = y_metrics + 143 # Modified BPP box rrect(draw, [30, y_bpp, 580, y_bpp+175], fill="#F0F4FF", outline=NAVY, width=2) draw.text((305, y_bpp+10), "MODIFIED BPP", font=font(14, True), fill=NAVY, anchor="mm") draw.line([44, y_bpp+26, 566, y_bpp+26], fill=TEAL, width=2) mbpp = [ "= NST + Amniotic Fluid Index (AFI)", "", "NST: Acute fetal status (CNS integrity)", "AFI: Chronic uteroplacental reserve", "", "AFI Normal: ≥ 5 cm (or MVP ≥ 2 cm)", "AFI Low: < 5 cm → Oligohydramnios", "", "False-negative rate: 0.8 / 1000", "As effective as full BPP — preferred first-line", ] fy = y_bpp + 34 for ln in mbpp: draw.text((44, fy), ln, font=font(11), fill=DGRAY) fy += 15 # BPP components box rrect(draw, [595, y_bpp, W-30, y_bpp+175], fill="#F9F0FF", outline="#7B1FA2", width=2) draw.text(((595+W-30)//2, y_bpp+10), "BPP COMPONENTS (each = 2 pts)", font=font(13, True), fill="#7B1FA2", anchor="mm") draw.line([609, y_bpp+26, W-44, y_bpp+26], fill="#7B1FA2", width=2) bpp_items = [ ("NST", "≥2 accel ≥15 bpm × ≥15 sec"), ("Breathing", "≥30 sec sustained in 30 min"), ("Movement", "≥3 body/limb movements"), ("Tone", "Extension + return to flexion"), ("AFV", "≥1 pocket ≥2×2 cm"), ] fy = y_bpp + 34 for comp, crit in bpp_items: draw.text((609, fy), f"• {comp}:", font=font(11, True), fill="#7B1FA2") draw.text((685, fy), crit, font=font(11), fill=DGRAY) fy += 18 draw.line([609, fy+4, W-44, fy+4], fill=BORDER, width=1) fy += 10 scores = [("8-10", "Reassuring", GREEN), ("6", "Suspicious — re-evaluate", AMBER), ("≤ 4", "Ominous — immediate action", RED)] for sc, meaning, col in scores: draw.text((609, fy), f"Score {sc}: ", font=font(11, True), fill=DGRAY) draw.text((680, fy), meaning, font=font(11), fill=col) fy += 15 # ══════════════════════════════════════════════════════════════════════════════ # SECTION G — VAS + Tips (2 narrow boxes) # ══════════════════════════════════════════════════════════════════════════════ y_vas = y_bpp + 188 HALF = (W - 75) // 2 rrect(draw, [30, y_vas, 30+HALF, y_vas+145], fill="#E0F7FA", outline=TEAL, width=2) draw.text((30+HALF//2, y_vas+10), "VIBROACOUSTIC STIMULATION (VAS)", font=font(13, True), fill=TEAL, anchor="mm") draw.line([44, y_vas+26, 30+HALF-14, y_vas+26], fill=TEAL, width=2) vas_lines = [ "• Handheld electro-larynx on maternal abdomen", "• 1–3 sec burst of vibration + sound (0.1–10 kHz)", "• Goal: State change (quiet → active sleep)", "• Vibration sense: fully developed by 22–24 wks", "• Auditory response: ~25–26 weeks onward", "• Significantly shortens testing time", "• VAS-generated accelerations = equal predictive", " value to spontaneous accelerations", "• Risk: Rare fetal bradycardia reported", ] fy = y_vas + 34 for ln in vas_lines: draw.text((44, fy), ln, font=font(11), fill="#004D40") fy += 13 # Tips box tx0 = 30 + HALF + 15 rrect(draw, [tx0, y_vas, tx0+HALF, y_vas+145], fill="#FFFDE7", outline=AMBER, width=2) draw.text((tx0+HALF//2, y_vas+10), "CLINICAL TIPS & PITFALLS", font=font(13, True), fill=AMBER, anchor="mm") draw.line([tx0+14, y_vas+26, tx0+HALF-14, y_vas+26], fill=AMBER, width=2) tips = [ "⚑ Semi-recumbent position — NEVER supine", "⚑ Extend to 40–90 min before calling nonreactive", "⚑ Fetal sleep is most common cause of NR NST", "⚑ Check maternal meds (opioids, MgSO₄)", "⚑ Variable decelerations in reactive NST → AFI", "⚑ NR NST in IUGR: PPV 69% — act quickly", "⚑ NR NST post-term: PPV only 15% — extend test", "⚑ Combine with AFI (modified BPP) for best accuracy", "⚑ Document all accelerations, baseline, variability", ] fy = y_vas + 34 for ln in tips: draw.text((tx0+14, fy), ln, font=font(11), fill="#5D4037") fy += 13 # ══════════════════════════════════════════════════════════════════════════════ # FOOTER # ══════════════════════════════════════════════════════════════════════════════ y_foot = H - 38 draw.rectangle([0, y_foot, W, H], fill=NAVY) draw.text((W//2, y_foot+10), "Sources: Textbook of Family Medicine 9e • Creasy & Resnik's Maternal-Fetal Medicine • Pfenninger & Fowler's Procedures for Primary Care", font=font(9), fill="#90CAF9", anchor="mm") draw.text((W//2, y_foot+24), "For educational use only — clinical decisions should be individualised based on current guidelines and patient context.", font=font(9), fill="#90CAF9", anchor="mm") # Save out_path = "/tmp/workspace/nst-guide/NST_Clinical_Reference_Guide.png" img.save(out_path, "PNG", dpi=(150, 150)) print(f"Saved: {out_path} ({W}x{H} px)") PYEOF
python3 - <<'PYEOF' from PIL import Image, ImageDraw, ImageFont import os # A3 @ 200 DPI = 1654 x 2339 px — bigger canvas for more breathing room W, H = 1654, 2339 img = Image.new("RGB", (W, H), "#F7F9FC") draw = ImageDraw.Draw(img) # Palette NAVY = "#1B3A6B" TEAL = "#006E7F" GREEN = "#1A7A4A" RED = "#C0392B" AMBER = "#C97A00" WHITE = "#FFFFFF" LGRAY = "#EDF1F6" DGRAY = "#3A3A3A" MGRAY = "#8A8A8A" BORDER = "#C5D0DA" PURPLE = "#6A1B9A" def font(size, bold=False): for name in ["DejaVuSans", "LiberationSans"]: suffix = "-Bold" if bold else "" for base in ["/usr/share/fonts/truetype/dejavu/", "/usr/share/fonts/truetype/liberation/"]: p = base + name + suffix + ".ttf" if os.path.exists(p): return ImageFont.truetype(p, size) return ImageFont.load_default() def rrect(draw, xy, fill, radius=14, outline=None, width=2): draw.rounded_rectangle(xy, radius=radius, fill=fill, outline=outline, width=width) def txt(draw, pos, text, fnt, fill, anchor="la"): draw.text(pos, text, font=fnt, fill=fill, anchor=anchor) def hline(draw, x0, y, x1, col=BORDER, w=1): draw.line([(x0,y),(x1,y)], fill=col, width=w) def vline(draw, x, y0, y1, col=BORDER, w=1): draw.line([(x,y0),(x,y1)], fill=col, width=w) # ──────────────────────────────────────────────────────────────────────────── # HEADER # ──────────────────────────────────────────────────────────────────────────── rrect(draw, [0,0,W,130], fill=NAVY, radius=0) txt(draw, (W//2,38), "NON-STRESS TEST (NST)", font(50,True), WHITE, "mm") txt(draw, (W//2,88), "CLINICAL INTERPRETATION · REFERENCE GUIDE", font(24), "#90CAF9", "mm") draw.rectangle([0,130,W,142], fill=TEAL) # ──────────────────────────────────────────────────────────────────────────── # ROW 1 — What is NST | Indications # ──────────────────────────────────────────────────────────────────────────── y0 = 154; PAD = 32; MID = W//2 + 8 COL = MID - PAD - 32 # column width for half-page boxes def half_box(x, y, w, h, title, lines, hdr_col, body_col, text_col, border_col): rrect(draw, [x, y, x+w, y+h], fill=body_col, outline=border_col, width=2) rrect(draw, [x, y, x+w, y+38], fill=hdr_col, radius=12, outline=None) draw.rectangle([x, y+26, x+w, y+38], fill=hdr_col) # square off bottom of header txt(draw, (x+w//2, y+19), title, font(17,True), WHITE, "mm") fy = y + 46 for ln in lines: txt(draw, (x+16, fy), ln, font(14), text_col) fy += 22 return fy lft_lines = [ "· External cardiotocograph (CTG) — FHR upper tracing,", " uterine activity on lower tracing", "· Primary antepartum fetal surveillance tool", "· Reflects fetal autonomic nervous system integrity", "· Reflects adequacy of uteroplacental function", "· NO contraindications · Non-invasive · Inexpensive", "· Patient position: semi-recumbent (never flat supine)", "· Patient presses button when fetal movement felt", ] rgt_lines = [ "· Gestational diabetes (GDM) — uncontrolled/new", "· Chronic hypertension / pre-eclampsia", "· Intrauterine growth restriction (IUGR / FGR)", "· Post-term pregnancy (≥ 41–42 weeks)", "· Decreased fetal movement (DFM)", "· Multiple gestation with complications", "· Antiphospholipid syndrome / thyroid disease", " START: 32 wks (low-risk) · earlier if high-risk", ] BOX1H = 230 half_box(PAD, y0, COL, BOX1H, "WHAT IS THE NST?", lft_lines, NAVY, LGRAY, DGRAY, BORDER) half_box(MID, y0, COL, BOX1H, "INDICATIONS", rgt_lines, TEAL, LGRAY, DGRAY, BORDER) # ──────────────────────────────────────────────────────────────────────────── # ROW 2 — Interpretation Criteria (3 columns) # ──────────────────────────────────────────────────────────────────────────── y2 = y0 + BOX1H + 22 txt(draw, (W//2, y2), "INTERPRETATION CRITERIA", font(20,True), NAVY, "mm") y2 += 28 CRIT_H = 210 C3W = (W - PAD*2 - 28) // 3 # Reactive rx = PAD rrect(draw, [rx, y2, rx+C3W, y2+CRIT_H], fill="#E8F5E9", outline=GREEN, width=3) rrect(draw, [rx, y2, rx+C3W, y2+40], fill=GREEN, radius=12, outline=None) draw.rectangle([rx, y2+28, rx+C3W, y2+40], fill=GREEN) txt(draw, (rx+C3W//2, y2+20), "✓ REACTIVE (NORMAL)", font(16,True), WHITE, "mm") react = [ "≥ 2 accelerations in a 20-min window", "", "Each acceleration must:", " · Peak ≥ 15 bpm above baseline FHR", " · Last ≥ 15 seconds", " · Last < 2 minutes", "", "Preterm < 32 wks (modified):", " · ≥ 10 bpm above baseline", " · Duration ≥ 10 seconds", " · Normal FHR variability present", ] fy = y2+50 for ln in react: txt(draw, (rx+16, fy), ln, font(13), "#1B5E20") fy += 16 # Nonreactive nx = rx + C3W + 14 rrect(draw, [nx, y2, nx+C3W, y2+CRIT_H], fill="#FFF8E1", outline=AMBER, width=3) rrect(draw, [nx, y2, nx+C3W, y2+40], fill=AMBER, radius=12, outline=None) draw.rectangle([nx, y2+28, nx+C3W, y2+40], fill=AMBER) txt(draw, (nx+C3W//2, y2+20), "⚠ NONREACTIVE", font(16,True), WHITE, "mm") nonr = [ "Fails to meet reactive criteria", "within the monitoring period", "", "~10–12% of 3rd-trimester fetuses", "at 30 min → < 6% by 40 min", "", "Does NOT equal fetal jeopardy", "", "→ Indicates need for further", " evaluation, not immediate action", "", "→ Extend monitoring & consider VAS", ] fy = y2+50 for ln in nonr: txt(draw, (nx+16, fy), ln, font(13), "#6D4C00") fy += 16 # Unsatisfactory ux = nx + C3W + 14 rrect(draw, [ux, y2, ux+C3W, y2+CRIT_H], fill="#FDECEA", outline=RED, width=3) rrect(draw, [ux, y2, ux+C3W, y2+40], fill=RED, radius=12, outline=None) draw.rectangle([ux, y2+28, ux+C3W, y2+40], fill=RED) txt(draw, (ux+C3W//2, y2+20), "✗ UNSATISFACTORY", font(16,True), WHITE, "mm") unsat = [ "Inadequate tracing quality —", "cannot be interpreted", "", "Causes:", " · Persistent poor signal", " · Cannot assess FHR baseline", " · Unable to detect accelerations", "", "Action:", " · Reposition patient", " · Reapply transducer", " · Repeat / escalate", ] fy = y2+50 for ln in unsat: txt(draw, (ux+16, fy), ln, font(13), "#7F0000") fy += 16 # ──────────────────────────────────────────────────────────────────────────── # ROW 3 — Causes of Nonreactive NST # ──────────────────────────────────────────────────────────────────────────── y3 = y2 + CRIT_H + 22 rrect(draw, [PAD, y3, W-PAD, y3+220], fill=LGRAY, outline=BORDER, width=2) rrect(draw, [PAD, y3, W-PAD, y3+38], fill=NAVY, radius=12, outline=None) draw.rectangle([PAD, y3+26, W-PAD, y3+38], fill=NAVY) txt(draw, (W//2, y3+19), "CAUSES OF A NONREACTIVE NST", font(18,True), WHITE, "mm") causes = [ ("Fetal Sleep Cycle", ["MOST COMMON", "Quiet sleep 20–40 min", "(up to 2 hrs near term)", "→ Extend monitoring", "→ Use VAS to wake"]), ("Prematurity\n< 32 Weeks", ["Immature autonomic", "nervous system", "→ Use modified 10×10", "criteria"]), ("Fetal Hypoxia /\nAcidemia", ["Genuine compromise", "CNS depression from", "hypoxemia/acidosis", "→ Urgent BPP or", "delivery assessment"]), ("Maternal\nMedications", ["Opioids / narcotics", "Magnesium sulfate", "Beta-blockers", "Corticosteroids", "→ All blunt CNS/cardiac"]), ("Fetal CNS /\nCardiac Anomalies", ["Structural reasons for", "absent acceleration", "→ Targeted ultrasound", "assessment"]), ] CW5 = (W - PAD*2 - 4*14) // 5 cx5 = PAD + 12 for i,(title,desc) in enumerate(causes): fy = y3 + 50 # Title for tl in title.split("\n"): txt(draw, (cx5, fy), tl, font(14,True), NAVY) fy += 20 hline(draw, cx5, fy, cx5+CW5-16, TEAL, 1) fy += 8 for dl in desc: txt(draw, (cx5, fy), dl, font(12), DGRAY) fy += 17 if i < len(causes)-1: vline(draw, cx5+CW5+6, y3+42, y3+215, BORDER, 1) cx5 += CW5 + 14 # ──────────────────────────────────────────────────────────────────────────── # ROW 4 — Management Flowchart # ──────────────────────────────────────────────────────────────────────────── y4 = y3 + 234 txt(draw, (W//2, y4), "MANAGEMENT ALGORITHM", font(20,True), NAVY, "mm") y4 += 28 FCW = 290 # flowchart box width FCH = 44 cx_fc = W // 2 def fc_box(draw, cy, text, col, w=FCW, h=FCH, text_col=WHITE): x0 = cx_fc - w//2; x1 = cx_fc + w//2 rrect(draw, [x0, cy, x1, cy+h], fill=col, radius=10, outline=None) txt(draw, (cx_fc, cy+h//2), text, font(14,True), text_col, "mm") return cy+h def arrow_down(draw, y_from, y_to, label="", col=DGRAY): draw.line([(cx_fc, y_from),(cx_fc, y_to-10)], fill=col, width=2) draw.polygon([(cx_fc-8,y_to-12),(cx_fc+8,y_to-12),(cx_fc,y_to)], fill=col) if label: txt(draw, (cx_fc+12, (y_from+y_to)//2-10), label, font(11), MGRAY) def diamond(draw, cy, text, fill_col, w=260, h=52): pts = [(cx_fc,cy),(cx_fc+w//2,cy+h//2),(cx_fc,cy+h),(cx_fc-w//2,cy+h//2)] draw.polygon(pts, fill=fill_col) draw.polygon(pts, outline="#555", width=2) txt(draw, (cx_fc, cy+h//2), text, font(14,True), WHITE, "mm") return cy+h def side_box(draw, cx, cy, text1, text2, col, w=280, h=72): rrect(draw, [cx-w//2, cy, cx+w//2, cy+h], fill=col+"22", outline=col, width=2) txt(draw, (cx, cy+20), text1, font(13,True), col, "mm") txt(draw, (cx, cy+40), text2, font(12), DGRAY, "mm") yf = y4 yf_end = fc_box(draw, yf, "NST Ordered (≥ 32 weeks)", NAVY) arrow_down(draw, yf_end, yf_end+20) yf = yf_end + 20 yf_end = fc_box(draw, yf, "Monitor for 20 Minutes", TEAL) arrow_down(draw, yf_end, yf_end+20) yf = yf_end + 20 d1_top = yf d1_bot = diamond(draw, yf, "REACTIVE?", AMBER, w=280, h=52) # YES — left LX = cx_fc - 320 draw.line([(cx_fc-140, d1_top+26),(LX, d1_top+26)], fill=GREEN, width=2) draw.line([(LX, d1_top+26),(LX, d1_top+110)], fill=GREEN, width=2) rrect(draw, [LX-130, d1_top+110, LX+130, d1_top+170], fill="#E8F5E9", outline=GREEN, width=2) txt(draw, (LX, d1_top+135), "✓ REACTIVE", font(14,True), GREEN, "mm") txt(draw, (LX, d1_top+154), "Reassuring — routine follow-up", font(12), "#2E7D32", "mm") txt(draw, (cx_fc-175, d1_top+10), "YES", font(12,True), GREEN) # NO — right RX = cx_fc + 320 draw.line([(cx_fc+140, d1_top+26),(RX, d1_top+26)], fill=RED, width=2) draw.line([(RX, d1_top+26),(RX, d1_top+90)], fill=RED, width=2) rrect(draw, [RX-145, d1_top+90, RX+145, d1_top+170], fill="#FDECEA", outline=RED, width=2) txt(draw, (RX, d1_top+110), "Extend to 40–90 min", font(13,True), RED, "mm") txt(draw, (RX, d1_top+132), "Try Vibroacoustic", font(12), DGRAY, "mm") txt(draw, (RX, d1_top+150), "Stimulation (VAS)", font(12), DGRAY, "mm") txt(draw, (cx_fc+148, d1_top+10), "NO", font(12,True), RED) # Still nonreactive arrow down arrow_down(draw, d1_bot, d1_bot+30, "Still nonreactive", RED) yf = d1_bot + 30 d2_top = yf d2_bot = diamond(draw, yf, "STILL NONREACTIVE?", RED, w=300, h=52) # YES — left (BPP) draw.line([(cx_fc-150, d2_top+26),(LX, d2_top+26)], fill=AMBER, width=2) draw.line([(LX, d2_top+26),(LX, d2_top+110)], fill=AMBER, width=2) rrect(draw, [LX-155, d2_top+110, LX+155, d2_top+185], fill="#FFF8E1", outline=AMBER, width=2) txt(draw, (LX, d2_top+128), "Modified BPP", font(14,True), AMBER, "mm") txt(draw, (LX, d2_top+148), "(NST + AFI)", font(13), DGRAY, "mm") txt(draw, (LX, d2_top+166), "or Full BPP / CST", font(13), DGRAY, "mm") txt(draw, (cx_fc-178, d2_top+10), "YES", font(12,True), AMBER) # NO — right (reassuring) draw.line([(cx_fc+150, d2_top+26),(RX, d2_top+26)], fill=GREEN, width=2) draw.line([(RX, d2_top+26),(RX, d2_top+110)], fill=GREEN, width=2) rrect(draw, [RX-145, d2_top+110, RX+145, d2_top+185], fill="#E8F5E9", outline=GREEN, width=2) txt(draw, (RX, d2_top+130), "✓ Reassuring", font(14,True), GREEN, "mm") txt(draw, (RX, d2_top+150), "Continue routine", font(12), "#2E7D32", "mm") txt(draw, (RX, d2_top+168), "surveillance plan", font(12), "#2E7D32", "mm") txt(draw, (cx_fc+155, d2_top+10), "NO", font(12,True), GREEN) yf_after = d2_bot + 20 # ──────────────────────────────────────────────────────────────────────────── # ROW 5 — Performance Metrics # ──────────────────────────────────────────────────────────────────────────── y5 = yf_after + 10 rrect(draw, [PAD, y5, W-PAD, y5+155], fill=LGRAY, outline=BORDER, width=2) rrect(draw, [PAD, y5, W-PAD, y5+38], fill=NAVY, radius=12, outline=None) draw.rectangle([PAD, y5+26, W-PAD, y5+38], fill=NAVY) txt(draw, (W//2, y5+19), "PERFORMANCE METRICS", font(18,True), WHITE, "mm") metrics = [ ("False-Negative Rate\n(Reactive NST alone)", "~1.9 / 1,000", "fetal death within 1 week", GREEN), ("False-Negative Rate\n(Modified BPP = NST+AFI)", "~0.8 / 1,000", "NST + AFI combined", TEAL), ("PPV of Nonreactive NST\n(post-term pregnancies)", "~15%", "False-positives are COMMON", RED), ("PPV of Nonreactive NST\n(IUGR/FGR pregnancies)", "~69%", "Act promptly in IUGR", AMBER), ] M4W = (W - PAD*2 - 3*16) // 4 mx = PAD + 10 for title, val, sub, col in metrics: rrect(draw, [mx, y5+46, mx+M4W, y5+145], fill=WHITE, outline=col, width=2) ty = y5+58 for tl in title.split("\n"): txt(draw, (mx+M4W//2, ty), tl, font(13,True), col, "mm") ty += 20 txt(draw, (mx+M4W//2, ty+4), val, font(22,True), col, "mm") txt(draw, (mx+M4W//2, ty+32), sub, font(11), MGRAY, "mm") mx += M4W + 16 # ──────────────────────────────────────────────────────────────────────────── # ROW 6 — Modified BPP | BPP Components # ──────────────────────────────────────────────────────────────────────────── y6 = y5 + 168 BPP_H = 230 HALF_W = (W - PAD*2 - 20) // 2 # Modified BPP rrect(draw, [PAD, y6, PAD+HALF_W, y6+BPP_H], fill="#EEF2FF", outline=NAVY, width=2) rrect(draw, [PAD, y6, PAD+HALF_W, y6+38], fill=NAVY, radius=12, outline=None) draw.rectangle([PAD, y6+26, PAD+HALF_W, y6+38], fill=NAVY) txt(draw, (PAD+HALF_W//2, y6+19), "MODIFIED BPP", font(17,True), WHITE, "mm") mbpp = [ ("= NST + Amniotic Fluid Index (AFI)", True), ("", False), ("NST → Acute fetal CNS status (short-term)", False), ("AFI → Chronic uteroplacental reserve (long-term)", False), ("", False), ("AFI Normal: ≥ 5 cm (or MVP ≥ 2 cm)", False), ("AFI Low: < 5 cm → Oligohydramnios", False), ("", False), ("False-negative rate: 0.8 / 1,000", True), ("As effective as full BPP — preferred first-line", False), ] fy = y6 + 48 for text, bold in mbpp: txt(draw, (PAD+16, fy), text, font(13,bold), DGRAY if not bold else NAVY) fy += 18 # BPP Components bx = PAD + HALF_W + 20 rrect(draw, [bx, y6, bx+HALF_W, y6+BPP_H], fill="#F9F0FF", outline=PURPLE, width=2) rrect(draw, [bx, y6, bx+HALF_W, y6+38], fill=PURPLE, radius=12, outline=None) draw.rectangle([bx, y6+26, bx+HALF_W, y6+38], fill=PURPLE) txt(draw, (bx+HALF_W//2, y6+19), "BPP COMPONENTS (each = 2 points)", font(16,True), WHITE, "mm") bpp_comps = [ ("NST", "≥ 2 accel ≥15 bpm × ≥15 sec in 30 min window"), ("Breathing", "≥ 30 sec sustained fetal breathing in 30 min"), ("Movement", "≥ 3 discrete body / limb movements in 30 min"), ("Tone", "Extension + return to flexion of extremity / spine"), ("AFV", "≥ 1 amniotic fluid pocket ≥ 2×2 cm"), ] fy = y6 + 50 for comp, crit in bpp_comps: txt(draw, (bx+16, fy), f"· {comp}:", font(13,True), PURPLE) txt(draw, (bx+130, fy), crit, font(13), DGRAY) fy += 22 hline(draw, bx+16, fy+4, bx+HALF_W-16, BORDER, 1) fy += 14 scores = [("8 – 10", "Reassuring — continue plan", GREEN), ("6", "Suspicious — re-evaluate urgently", AMBER), ("≤ 4", "Ominous — immediate intervention", RED)] for sc, meaning, col in scores: txt(draw, (bx+16, fy), f"Score {sc}:", font(13,True), DGRAY) txt(draw, (bx+120, fy), meaning, font(13), col) fy += 20 # ──────────────────────────────────────────────────────────────────────────── # ROW 7 — VAS | Clinical Tips # ──────────────────────────────────────────────────────────────────────────── y7 = y6 + BPP_H + 18 VAS_H = 220 # VAS box rrect(draw, [PAD, y7, PAD+HALF_W, y7+VAS_H], fill="#E0F7FA", outline=TEAL, width=2) rrect(draw, [PAD, y7, PAD+HALF_W, y7+38], fill=TEAL, radius=12, outline=None) draw.rectangle([PAD, y7+26, PAD+HALF_W, y7+38], fill=TEAL) txt(draw, (PAD+HALF_W//2, y7+19), "VIBROACOUSTIC STIMULATION (VAS)", font(16,True), WHITE, "mm") vas = [ "· Handheld electro-larynx applied to maternal abdomen", "· 1–3 sec burst of vibration + broad-band sound (0.1–10 kHz)", "· Goal: state change from quiet sleep → active sleep", "· Vibration sense: fully developed by 22–24 weeks", "· Auditory response: ~25–26 weeks onward", "· Significantly shortens total monitoring time", "· VAS-generated accelerations carry EQUAL predictive", " value to spontaneous accelerations", "· On occasion, fetal bradycardia has been reported", "· NOT recommended if fetus already in active state", ] fy = y7 + 50 for ln in vas: txt(draw, (PAD+16, fy), ln, font(13), "#004D40") fy += 17 # Tips box tx = PAD + HALF_W + 20 rrect(draw, [tx, y7, tx+HALF_W, y7+VAS_H], fill="#FFFDE7", outline=AMBER, width=2) rrect(draw, [tx, y7, tx+HALF_W, y7+38], fill=AMBER, radius=12, outline=None) draw.rectangle([tx, y7+26, tx+HALF_W, y7+38], fill=AMBER) txt(draw, (tx+HALF_W//2, y7+19), "CLINICAL TIPS & PITFALLS", font(16,True), WHITE, "mm") tips = [ "⚑ Semi-recumbent position — NEVER supine", "⚑ Extend to 40–90 min before calling nonreactive", "⚑ Fetal sleep is the most common cause of NR NST", "⚑ Check all maternal medications before interpreting", "⚑ Variable decelerations in reactive NST → check AFI", "⚑ NR NST in IUGR: PPV 69% — escalate quickly", "⚑ NR NST post-term: PPV only 15% — extend test first", "⚑ Combine NST + AFI (modified BPP) for best accuracy", "⚑ Always document: baseline FHR, variability, accels,", " decelerations, fetal movement marks", ] fy = y7 + 50 for ln in tips: txt(draw, (tx+16, fy), ln, font(13), "#4E3100") fy += 17 # ──────────────────────────────────────────────────────────────────────────── # FOOTER # ──────────────────────────────────────────────────────────────────────────── y_foot = H - 50 draw.rectangle([0, y_foot, W, H], fill=NAVY) txt(draw, (W//2, y_foot+10), "Sources: Textbook of Family Medicine 9e · Creasy & Resnik's Maternal-Fetal Medicine · Pfenninger & Fowler's Procedures for Primary Care, 3rd ed.", font(12), "#90CAF9", "mm") txt(draw, (W//2, y_foot+30), "For educational use only — clinical decisions must be individualised based on current guidelines, gestational age, and patient context.", font(12), "#90CAF9", "mm") out = "/tmp/workspace/nst-guide/NST_Clinical_Reference_Guide.png" img.save(out, "PNG", dpi=(200,200)) print(f"Saved: {out} {W}x{H}px") PYEOF
python3 - <<'PYEOF' from PIL import Image, ImageDraw, ImageFont import os W, H = 1654, 2100 # trimmed height img = Image.new("RGB", (W, H), "#F7F9FC") draw = ImageDraw.Draw(img) NAVY="#1B3A6B"; TEAL="#006E7F"; GREEN="#1A7A4A"; RED="#C0392B" AMBER="#C97A00"; WHITE="#FFFFFF"; LGRAY="#EDF1F6"; DGRAY="#3A3A3A" MGRAY="#8A8A8A"; BORDER="#C5D0DA"; PURPLE="#6A1B9A" def font(size, bold=False): for name in ["DejaVuSans","LiberationSans"]: sfx="-Bold" if bold else "" for base in ["/usr/share/fonts/truetype/dejavu/","/usr/share/fonts/truetype/liberation/"]: p=base+name+sfx+".ttf" if os.path.exists(p): return ImageFont.truetype(p,size) return ImageFont.load_default() def rrect(d,xy,fill,r=14,ol=None,w=2): d.rounded_rectangle(xy,radius=r,fill=fill,outline=ol,width=w) def txt(d,pos,t,f,fill,a="la"): d.text(pos,t,font=f,fill=fill,anchor=a) def hdr(d,x,y,w,h,title,hcol): rrect(d,[x,y,x+w,y+h],fill=hcol,r=12); d.rectangle([x,y+h-12,x+w,y+h],fill=hcol) txt(d,(x+w//2,y+h//2),title,font(15,True),WHITE,"mm") PAD=32; MID=W//2+8; COL=MID-PAD-32 # ═══════════ HEADER ═══════════ rrect(draw,[0,0,W,120],fill=NAVY,r=0) txt(draw,(W//2,36),"NON-STRESS TEST (NST)",font(46,True),WHITE,"mm") txt(draw,(W//2,82),"CLINICAL INTERPRETATION · REFERENCE GUIDE",font(22),"#90CAF9","mm") draw.rectangle([0,120,W,130],fill=TEAL) # ═══════════ ROW 1 — What/Indications ═══════════ y0=142; BOX1H=215 for xb,title,lines,hc in [ (PAD,"WHAT IS THE NST?",[ "· External CTG — FHR upper tracing, UA lower tracing", "· Primary antepartum fetal surveillance tool", "· Reflects fetal autonomic nervous system integrity", "· Reflects adequacy of uteroplacental function", "· NO contraindications · Non-invasive · Inexpensive", "· Semi-recumbent position (never flat supine)", "· Mother presses button when fetal movement perceived", ],NAVY), (MID,"INDICATIONS",[ "· Gestational diabetes (GDM) — uncontrolled / new", "· Chronic hypertension / pre-eclampsia", "· Intrauterine growth restriction (IUGR / FGR)", "· Post-term pregnancy (≥ 41–42 weeks)", "· Decreased fetal movement (DFM)", "· Multiple gestation with complications", "· START: 32 wks low-risk · earlier for high-risk", ],TEAL), ]: rrect(draw,[xb,y0,xb+COL,y0+BOX1H],fill=LGRAY,ol=BORDER) hdr(draw,xb,y0,COL,36,title,hc) fy=y0+44 for ln in lines: txt(draw,(xb+14,fy),ln,font(13),DGRAY); fy+=22 # ═══════════ ROW 2 — Criteria ═══════════ y2=y0+BOX1H+18 txt(draw,(W//2,y2),"INTERPRETATION CRITERIA",font(19,True),NAVY,"mm"); y2+=24 CRIT_H=200; C3W=(W-PAD*2-28)//3 for xc,title,tcol,bgcol,lines in [ (PAD,"✓ REACTIVE (NORMAL)",GREEN,"#E8F5E9",[ "≥ 2 accelerations in a 20-min window","", "Each acceleration must have:", " · Peak ≥ 15 bpm above baseline FHR", " · Duration ≥ 15 seconds (< 2 minutes)","", "Preterm < 32 wks (modified criteria):", " · Peak ≥ 10 bpm above baseline", " · Duration ≥ 10 seconds", " · Normal FHR variability present", ]), (PAD+C3W+14,"⚠ NONREACTIVE",AMBER,"#FFF8E1",[ "Fails to meet reactive criteria","within the monitoring period","", "~10–12% of 3rd-trimester fetuses","at 30 min → < 6% by 40 min","", "Does NOT equal fetal jeopardy","", "→ Extend monitoring & apply VAS", "→ Nonreactive ≠ immediate delivery", ]), (PAD+2*(C3W+14),"✗ UNSATISFACTORY",RED,"#FDECEA",[ "Inadequate quality — uninterpretable","", "Causes:", " · Persistent poor signal", " · Cannot assess FHR baseline", " · Cannot detect accelerations","", "Action:", " · Reposition · Reapply probe", " · Repeat test · Escalate", ]), ]: rrect(draw,[xc,y2,xc+C3W,y2+CRIT_H],fill=bgcol,ol=tcol,w=3) hdr(draw,xc,y2,C3W,36,title,tcol) fy=y2+44 for ln in lines: txt(draw,(xc+14,fy),ln,font(12),DGRAY); fy+=15 # ═══════════ ROW 3 — Causes ═══════════ y3=y2+CRIT_H+18 rrect(draw,[PAD,y3,W-PAD,y3+200],fill=LGRAY,ol=BORDER) hdr(draw,PAD,y3,W-PAD*2,36,"CAUSES OF A NONREACTIVE NST",NAVY) causes=[ ("Fetal Sleep Cycle",["MOST COMMON","Quiet sleep 20–40 min","(up to 2 hrs near term)","→ Extend monitoring","→ Use VAS"]), ("Prematurity\n< 32 Weeks",["Immature autonomic","nervous system","→ Modified 10×10","criteria"]), ("Fetal Hypoxia /\nAcidemia",["Genuine compromise","CNS depression","→ Urgent BPP or","delivery assessment"]), ("Maternal\nMedications",["Opioids / narcotics","Magnesium sulfate","Beta-blockers","Corticosteroids"]), ("Fetal CNS /\nCardiac Anomaly",["Structural reason","for absent accel.","→ Targeted USS"]), ] CW5=(W-PAD*2-4*14)//5; cx5=PAD+12 for i,(title,desc) in enumerate(causes): fy=y3+46 for tl in title.split("\n"): txt(draw,(cx5,fy),tl,font(13,True),NAVY); fy+=18 draw.line([(cx5,fy),(cx5+CW5-16,fy)],fill=TEAL,width=1); fy+=8 for dl in desc: txt(draw,(cx5,fy),dl,font(12),DGRAY); fy+=15 if i<4: draw.line([(cx5+CW5+6,y3+40),(cx5+CW5+6,y3+192)],fill=BORDER,width=1) cx5+=CW5+14 # ═══════════ ROW 4 — Flowchart (compact) ═══════════ y4=y3+215 txt(draw,(W//2,y4),"MANAGEMENT ALGORITHM",font(19,True),NAVY,"mm"); y4+=24 FCW=280; FCH=40; cx_fc=W//2 def fc_box(cy,text,col,w=FCW,h=FCH): x0=cx_fc-w//2; x1=cx_fc+w//2 rrect(draw,[x0,cy,x1,cy+h],fill=col,r=10) txt(draw,(cx_fc,cy+h//2),text,font(13,True),WHITE,"mm"); return cy+h def arr(yf,yt,col=DGRAY,lbl=""): draw.line([(cx_fc,yf),(cx_fc,yt-10)],fill=col,width=2) draw.polygon([(cx_fc-7,yt-12),(cx_fc+7,yt-12),(cx_fc,yt)],fill=col) if lbl: txt(draw,(cx_fc+10,(yf+yt)//2-7),lbl,font(10),MGRAY) def diam(cy,text,col,w=280,h=50): pts=[(cx_fc,cy),(cx_fc+w//2,cy+h//2),(cx_fc,cy+h),(cx_fc-w//2,cy+h//2)] draw.polygon(pts,fill=col); draw.polygon(pts,outline="#555",width=2) txt(draw,(cx_fc,cy+h//2),text,font(13,True),WHITE,"mm"); return cy+h yf=y4 yf=fc_box(yf,"NST Ordered (≥ 32 weeks)",NAVY)+0; arr(yf,yf+16); yf+=16 yf=fc_box(yf,"Monitor for 20 Minutes",TEAL)+0; arr(yf,yf+16); yf+=16 d1t=yf; d1b=diam(yf,"REACTIVE?",AMBER) LX=cx_fc-310; RX=cx_fc+310 # YES draw.line([(cx_fc-140,d1t+25),(LX,d1t+25)],fill=GREEN,width=2) draw.line([(LX,d1t+25),(LX,d1t+90)],fill=GREEN,width=2) rrect(draw,[LX-130,d1t+90,LX+130,d1t+145],fill="#E8F5E9",ol=GREEN,w=2) txt(draw,(LX,d1t+107),"✓ REACTIVE",font(13,True),GREEN,"mm") txt(draw,(LX,d1t+127),"Reassuring — continue plan",font(11),"#1B5E20","mm") txt(draw,(cx_fc-175,d1t+8),"YES",font(11,True),GREEN) # NO draw.line([(cx_fc+140,d1t+25),(RX,d1t+25)],fill=RED,width=2) draw.line([(RX,d1t+25),(RX,d1t+90)],fill=RED,width=2) rrect(draw,[RX-140,d1t+90,RX+140,d1t+155],fill="#FDECEA",ol=RED,w=2) txt(draw,(RX,d1t+107),"Extend to 40–90 min",font(13,True),RED,"mm") txt(draw,(RX,d1t+128),"Apply Vibroacoustic Stim (VAS)",font(11),DGRAY,"mm") txt(draw,(cx_fc+145,d1t+8),"NO",font(11,True),RED) arr(d1b,d1b+22,RED,"Still nonreactive"); yf=d1b+22 d2t=yf; d2b=diam(yf,"STILL NONREACTIVE?",RED,w=300) # YES draw.line([(cx_fc-150,d2t+25),(LX,d2t+25)],fill=AMBER,width=2) draw.line([(LX,d2t+25),(LX,d2t+90)],fill=AMBER,width=2) rrect(draw,[LX-140,d2t+90,LX+140,d2t+160],fill="#FFF8E1",ol=AMBER,w=2) txt(draw,(LX,d2t+107),"Modified BPP",font(13,True),AMBER,"mm") txt(draw,(LX,d2t+127),"(NST + AFI)",font(12),DGRAY,"mm") txt(draw,(LX,d2t+145),"or Full BPP / CST",font(12),DGRAY,"mm") txt(draw,(cx_fc-178,d2t+8),"YES",font(11,True),AMBER) # NO draw.line([(cx_fc+150,d2t+25),(RX,d2t+25)],fill=GREEN,width=2) draw.line([(RX,d2t+25),(RX,d2t+90)],fill=GREEN,width=2) rrect(draw,[RX-140,d2t+90,RX+140,d2t+160],fill="#E8F5E9",ol=GREEN,w=2) txt(draw,(RX,d2t+110),"✓ Reassuring",font(13,True),GREEN,"mm") txt(draw,(RX,d2t+130),"Continue routine plan",font(12),"#1B5E20","mm") txt(draw,(cx_fc+155,d2t+8),"NO",font(11,True),GREEN) y5=d2b+18 # ═══════════ ROW 5 — Metrics ═══════════ rrect(draw,[PAD,y5,W-PAD,y5+140],fill=LGRAY,ol=BORDER) hdr(draw,PAD,y5,W-PAD*2,36,"PERFORMANCE METRICS",NAVY) metrics=[ ("False-Neg Rate\n(Reactive NST)","~1.9 / 1,000","fetal death within 1 wk",GREEN), ("False-Neg Rate\n(Modified BPP)","~0.8 / 1,000","NST + AFI combined",TEAL), ("PPV Nonreactive NST\n(post-term)","~15%","False-positives COMMON",RED), ("PPV Nonreactive NST\n(IUGR/FGR)","~69%","Act promptly in IUGR",AMBER), ] M4W=(W-PAD*2-3*16)//4; mx=PAD+8 for title,val,sub,col in metrics: rrect(draw,[mx,y5+44,mx+M4W,y5+132],fill=WHITE,ol=col,w=2) ty=y5+54 for tl in title.split("\n"): txt(draw,(mx+M4W//2,ty),tl,font(12,True),col,"mm"); ty+=18 txt(draw,(mx+M4W//2,ty+2),val,font(20,True),col,"mm") txt(draw,(mx+M4W//2,ty+28),sub,font(11),MGRAY,"mm") mx+=M4W+16 # ═══════════ ROW 6 — Mod BPP + BPP ═══════════ y6=y5+152; HALF_W=(W-PAD*2-20)//2; BPP_H=215 rrect(draw,[PAD,y6,PAD+HALF_W,y6+BPP_H],fill="#EEF2FF",ol=NAVY,w=2) hdr(draw,PAD,y6,HALF_W,36,"MODIFIED BPP = NST + AFI",NAVY) mbpp_lines=[("= NST + Amniotic Fluid Index (AFI)",True),("",False), ("NST → Acute fetal CNS status (short-term)",False), ("AFI → Chronic uteroplacental reserve (long-term)",False),("",False), ("AFI Normal: ≥ 5 cm (or MVP ≥ 2 cm)",False), ("AFI Low: < 5 cm → Oligohydramnios",False),("",False), ("False-negative rate: 0.8 / 1,000",True), ("As effective as full BPP — preferred first-line",False),] fy=y6+46 for t,b in mbpp_lines: txt(draw,(PAD+14,fy),t,font(13,b),NAVY if b else DGRAY); fy+=17 bx=PAD+HALF_W+20 rrect(draw,[bx,y6,bx+HALF_W,y6+BPP_H],fill="#F9F0FF",ol=PURPLE,w=2) hdr(draw,bx,y6,HALF_W,36,"BPP COMPONENTS (each = 2 points)",PURPLE) bpp_comps=[("NST","≥ 2 accel ≥15 bpm × ≥15 sec in 30 min"), ("Breathing","≥ 30 sec sustained fetal breathing"), ("Movement","≥ 3 discrete body / limb movements"), ("Tone","Extension + return to flexion of extremity"), ("AFV","≥ 1 amniotic fluid pocket ≥ 2×2 cm"),] fy=y6+46 for comp,crit in bpp_comps: txt(draw,(bx+14,fy),f"· {comp}:",font(13,True),PURPLE) txt(draw,(bx+120,fy),crit,font(13),DGRAY); fy+=20 draw.line([(bx+14,fy+4),(bx+HALF_W-14,fy+4)],fill=BORDER,width=1); fy+=14 for sc,meaning,col in [("8–10","Reassuring — continue plan",GREEN), ("6","Suspicious — re-evaluate urgently",AMBER), ("≤ 4","Ominous — immediate intervention",RED)]: txt(draw,(bx+14,fy),f"Score {sc}:",font(13,True),DGRAY) txt(draw,(bx+110,fy),meaning,font(13),col); fy+=18 # ═══════════ ROW 7 — VAS + Tips ═══════════ y7=y6+BPP_H+16; VAS_H=210 rrect(draw,[PAD,y7,PAD+HALF_W,y7+VAS_H],fill="#E0F7FA",ol=TEAL,w=2) hdr(draw,PAD,y7,HALF_W,36,"VIBROACOUSTIC STIMULATION (VAS)",TEAL) vas_lines=["· Handheld electro-larynx on maternal abdomen", "· 1–3 sec burst: vibration + sound (0.1–10 kHz)", "· Goal: quiet sleep → active sleep (state change)", "· Vibration sense developed by 22–24 weeks GA", "· Auditory response from ~25–26 weeks onward", "· Significantly shortens total monitoring time", "· VAS accelerations = equal predictive value to", " spontaneous accelerations (outcome equivalent)", "· Rare: fetal bradycardia reported", "· Avoid if fetus already in active state",] fy=y7+46 for ln in vas_lines: txt(draw,(PAD+14,fy),ln,font(13),"#004D40"); fy+=17 tx=PAD+HALF_W+20 rrect(draw,[tx,y7,tx+HALF_W,y7+VAS_H],fill="#FFFDE7",ol=AMBER,w=2) hdr(draw,tx,y7,HALF_W,36,"CLINICAL TIPS & PITFALLS",AMBER) tips_lines=["⚑ Semi-recumbent — NEVER flat supine", "⚑ Extend to 40–90 min before labelling nonreactive", "⚑ Fetal sleep is the MOST COMMON cause of NR NST", "⚑ Review all maternal medications before interpreting", "⚑ Variable decels in reactive NST → always check AFI", "⚑ NR NST in IUGR: PPV 69% — escalate quickly", "⚑ NR NST post-term: PPV 15% — extend before acting", "⚑ NST + AFI (modified BPP) gives best accuracy", "⚑ Always document baseline FHR, variability,", " accelerations, decelerations, fetal movement marks",] fy=y7+46 for ln in tips_lines: txt(draw,(tx+14,fy),ln,font(13),"#4E3100"); fy+=17 # ═══════════ FOOTER ═══════════ yf=H-46; draw.rectangle([0,yf,W,H],fill=NAVY) txt(draw,(W//2,yf+8), "Sources: Textbook of Family Medicine 9e · Creasy & Resnik's Maternal-Fetal Medicine · Pfenninger & Fowler's Procedures for Primary Care, 3rd ed.", font(11),"#90CAF9","mm") txt(draw,(W//2,yf+27), "For educational use only — clinical decisions must be individualised based on current guidelines, gestational age, and clinical context.", font(11),"#90CAF9","mm") out="/tmp/workspace/nst-guide/NST_Clinical_Reference_Guide.png" img.save(out,"PNG",dpi=(200,200)) print(f"Saved {out} {W}x{H}") PYEOF
python3 - <<'PYEOF' from PIL import Image img = Image.open("/tmp/workspace/nst-guide/NST_Clinical_Reference_Guide.png") # Crop to remove blank space at bottom - tips end around y=1950 cropped = img.crop((0, 0, 1654, 1970)) cropped.save("/tmp/workspace/nst-guide/NST_Clinical_Reference_Guide.png", "PNG", dpi=(200,200)) print(f"Cropped to: {cropped.size}") PYEOF
NST Clinical Reference Guide
Image · PNG
| Section | Contents |
|---|---|
| What is the NST / Indications | Procedure basics, start timing, clinical contexts |
| Interpretation Criteria | Reactive · Nonreactive · Unsatisfactory — colour-coded green/amber/red |
| Causes of Nonreactive NST | 5-column breakdown: sleep, prematurity, hypoxia, medications, anomalies |
| Management Algorithm | Step-by-step flowchart from order → reactive/nonreactive → VAS → BPP |
| Performance Metrics | 4 key stats: FNR reactive NST, FNR modified BPP, PPV post-term, PPV IUGR |
| Modified BPP | NST + AFI, AFI thresholds, false-negative rate |
| BPP Components | All 5 components with normal criteria + scoring key |
| VAS | Device, mechanism, developmental milestones, evidence note |
| Clinical Tips & Pitfalls | 10 key bedside reminders |
What are deceleration?

| Feature | Detail |
|---|---|
| Timing | Onset, nadir, and recovery coincide with beginning, peak, and end of contraction |
| Onset | Gradual (≥30 sec from onset to nadir) |
| Shape | Mirror image of the contraction - smooth, uniform |
| Depth | Usually shallow |
| Mechanism | Vagal response to fetal head compression during contraction → transient changes in intracranial pressure and/or cerebral blood flow |
| Clinical significance | BENIGN - not associated with fetal hypoxia, acidemia, or hypoxic-ischemic neurologic injury |
| Action | None required - reassuring finding |
"Early decelerations occur secondary to changes in vagal tone after brief hypoxic episodes or head compression and are benign." - The Harriet Lane Handbook, 23rd ed.
| Feature | Detail |
|---|---|
| Timing | Onset, nadir, and recovery all occur AFTER the beginning, peak, and ending of the contraction |
| Onset | Gradual (≥30 sec from onset to nadir) |
| Shape | Smooth, uniform curve - delayed mirror of contraction |
| Depth | Often shallow but significant regardless of amplitude |
| Mechanism | Two subtypes (see below) |
| Clinical significance | WORRISOME / OMINOUS - indicate uteroplacental insufficiency and fetal distress |


"Late decelerations result from uteroplacental insufficiency and indicate fetal distress." - Harriet Lane Handbook, 23rd ed.
| Feature | Detail |
|---|---|
| Timing | No uniform temporal relationship to contractions - onset, depth, and duration vary with successive contractions |
| Onset | Abrupt (<30 sec from onset to beginning of nadir) |
| Depth | ≥15 bpm below baseline |
| Duration | ≥15 seconds but <2 minutes (baseline to baseline) |
| Mechanism | Umbilical cord compression → acute increase in fetal blood pressure → baroreceptor-mediated vagal response |
| Appearance | Characteristic "V" or "W" shape; may have "shoulders" (brief acceleration before and after) |
| Clinical significance | Variable - depends on depth, duration, and FHR variability between decelerations |
"Variable decelerations represent umbilical cord compression... considered severe when heart rate drops to <60 bpm for about 60 seconds, with a slow recovery to baseline." - Harriet Lane Handbook, 23rd ed.
| Feature | Detail |
|---|---|
| Definition | Abrupt decrease ≥15 bpm below baseline lasting ≥2 minutes but <10 minutes |
| If ≥10 minutes | Reclassified as a baseline change (bradycardia) |
| Mechanism | Same physiologic mechanisms as late or variable decelerations but sustained longer - interrupting fetal oxygenation for an extended period |
| Clinical significance | Associated with increasing acidemia risk as duration extends; if bradycardia develops → increased risk of NICU admission |
| Feature | Early | Late | Variable | Prolonged |
|---|---|---|---|---|
| Onset speed | Gradual | Gradual | Abrupt (<30s) | Abrupt |
| Timing vs. contraction | Coincident | After peak (delayed) | Variable / irregular | Any time |
| Mechanism | Head compression / vagal | Uteroplacental insufficiency | Cord compression | Late/variable sustained |
| Variability | Normal | Normal (reflex) or Absent (nonreflex) | Often normal between | Varies |
| Clinical danger | Benign | Worrisome/Ominous | Depends on severity | Concerning |
| Action | None | Urgent evaluation | Context-dependent | Urgent evaluation |