pediatric-ai-scribe-v3/shared/clinical/fenton.ts
Daniel 941b73f9e5 feat(client): port final 5 Bedside panels — all 15 now run in React
Finishes Bedside parity. Neonatal, Respiratory, Ventilation, Sepsis,
and Burns replace their LegacyPanel fallbacks. All 15 vanilla
Bedside sub-modules are now real React components.

shared/clinical/fenton.ts — second, higher-accuracy Fenton table
  fentonLmsPeditools: 21 GA weeks × 2 sexes (22-42 in 1-week steps)
  ported verbatim from public/js/bedside/neonatal.js:20-37. This is
  the peditools-derived table that superseded the older hand-rounded
  version (still used by the Growth Charts calculator). Adds
  calcZNeonatal (|L|<0.001 threshold) + zToPercentileNeonatal
  (Abramowitz & Stegun erf form) so neonatal output matches the
  vanilla Bedside numbers to 3 decimal places.
  neonatalAssess() returns { gaDecimal, gaClass, bwClass,
  weightClass, expectedWeight, z, percentile, L, M, S }.

scripts/capture-calc-vectors.js + e2e/fixtures/calc-vectors.json
  Adds 8 neonatal vectors (including the 40w5d male 3070g validated
  case from the vanilla file's own comment: z = -1.42).

shared/clinical/fenton.test.ts — now covers neonatal too (110 total
vitest cases pass: 19 calculators, 21 fenton, 70 bilirubin).

client/src/pages/BedsidePanels2.tsx — five new panels
  • NeonatalPanel — GA+weight+sex inputs drive the Fenton assessment
    (gestational-age class, weight-for-GA class, birth-weight
    category, Z-score, percentile, expected M). Full NRP pathway
    cards (birth→HR<100→HR<60 escalation), NRP drug table scaled
    by weight (epi IV/IO/ETT, NS bolus, D10), and 5-element Apgar
    scorer with reassuring/moderately-depressed/severely-depressed
    guidance.
  • RespiratoryPanel — four sub-modes: Asthma (mild/moderate/
    severe with full drug tables + "when to intubate" / ABG /
    heliox clinical decision boxes), PRAM scorer (0-12),
    Westley croup scorer (0-17 with severity-tiered treatment),
    Bronchiolitis admission decision tree (age / SpO₂ / hydration
    / distress) with AAP "NOT recommended" list.
  • VentilationPanel — target SpO₂ table by population, 6-step
    escalation ladder (NC → FM → NRB → HFNC → NIV → intubation)
    with live-scaled HFNC flow (1-2 L/kg/min), BVM how-to,
    mechanical vent starting settings (TV, rate by age, PEEP,
    FiO₂, I:E), gas-exchange adjustment table, and the
    oxygenation-vs-ventilation mental model.
  • SepsisPanel — Phoenix Sepsis Criteria (JAMA 2024), red-flag
    list, age-banded workup + empirical therapy (neonate /
    infant / child abx drugs keyed to weight), SSC 2020 first-hour
    bundle (0-5 min recognize → >60 min refractory-shock
    vasoactive), and resuscitation targets.
  • BurnsPanel — full 19-region Lund-Browder age-adjusted table
    (ported verbatim), with per-region % input + live TBSA
    computation + override. Parkland formula (4 × kg × TBSA,
    8h/16h split + per-hr rates), 4-2-1 maintenance, UOP targets,
    pearl list (palm rule, no first-degree, analgesia, tetanus),
    and ABA burn-center referral criteria.

client/src/pages/BedsidePanels.tsx — dispatch extended to all 15
pills. REAL_BEDSIDE_PANELS now contains every Bedside pill id, so
the legacy-link fallback is dead code that can be pruned later.

Client tsc + vite build + 110/110 vitest tests all green.
2026-04-24 02:00:50 +02:00

225 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// FENTON 2013 — preterm growth LMS for weight-for-GA. Table
// ported VERBATIM from public/js/calculators.js:1168-1183
// (15 GA weeks × 2 sexes × {L,M,S}). Parity against the vanilla
// implementation pinned by e2e/fixtures/calc-vectors.json +
// fenton.test.ts.
//
// Reference:
// Fenton TR, Kim JH. A systematic review and meta-analysis
// to revise the Fenton growth chart for preterm infants.
// BMC Pediatr 2013;13:59.
// ============================================================
export type Sex = 'male' | 'female';
export interface Lms { L: number; M: number; S: number }
export interface FentonResult {
L: number;
M: number;
S: number;
z: number;
percentile: number;
}
// Verbatim from calculators.js:1168-1183.
export const fentonLMS: Record<Sex, Record<number, Lms>> = {
male: {
22: { L: 0.21, M: 496, S: 0.17 }, 24: { L: 0.21, M: 660, S: 0.17 }, 26: { L: 0.21, M: 870, S: 0.16 },
28: { L: 0.20, M: 1124, S: 0.15 }, 30: { L: 0.18, M: 1430, S: 0.14 }, 32: { L: 0.15, M: 1795, S: 0.14 },
34: { L: 0.12, M: 2230, S: 0.13 }, 36: { L: 0.08, M: 2710, S: 0.13 }, 38: { L: 0.04, M: 3195, S: 0.12 },
40: { L: 0.01, M: 3530, S: 0.12 }, 42: { L: -0.02, M: 3820, S: 0.12 }, 44: { L: -0.04, M: 4200, S: 0.12 },
46: { L: -0.06, M: 4680, S: 0.12 }, 48: { L: -0.07, M: 5200, S: 0.12 }, 50: { L: -0.08, M: 5760, S: 0.12 },
},
female: {
22: { L: 0.23, M: 474, S: 0.17 }, 24: { L: 0.22, M: 610, S: 0.17 }, 26: { L: 0.22, M: 810, S: 0.16 },
28: { L: 0.21, M: 1040, S: 0.15 }, 30: { L: 0.19, M: 1330, S: 0.14 }, 32: { L: 0.16, M: 1680, S: 0.14 },
34: { L: 0.12, M: 2090, S: 0.13 }, 36: { L: 0.08, M: 2540, S: 0.13 }, 38: { L: 0.04, M: 3000, S: 0.12 },
40: { L: 0.01, M: 3340, S: 0.12 }, 42: { L: -0.02, M: 3630, S: 0.12 }, 44: { L: -0.04, M: 4010, S: 0.12 },
46: { L: -0.06, M: 4470, S: 0.12 }, 48: { L: -0.07, M: 4970, S: 0.12 }, 50: { L: -0.08, M: 5510, S: 0.12 },
},
};
// Verbatim from calculators.js:2299-2311.
export function interpolateLMS(table: Record<number, Lms>, val: number): Lms {
const keys = Object.keys(table).map(Number).sort((a, b) => a - b);
if (val <= keys[0]) return table[keys[0]];
if (val >= keys[keys.length - 1]) return table[keys[keys.length - 1]];
for (let i = 0; i < keys.length - 1; i++) {
if (val >= keys[i] && val <= keys[i + 1]) {
const t = (val - keys[i]) / (keys[i + 1] - keys[i]);
const lms1 = table[keys[i]];
const lms2 = table[keys[i + 1]];
return {
L: lms1.L + t * (lms2.L - lms1.L),
M: lms1.M + t * (lms2.M - lms1.M),
S: lms1.S + t * (lms2.S - lms1.S),
};
}
}
return table[keys[0]];
}
// Verbatim from calculators.js:2313-2316.
export function calcZ(value: number, L: number, M: number, S: number): number {
if (L === 0) return Math.log(value / M) / S;
return (Math.pow(value / M, L) - 1) / (L * S);
}
// Abramowitz & Stegun normal CDF approximation, verbatim from
// calculators.js:2318-2326.
export function zToPercentile(z: number): number {
const t = 1 / (1 + 0.2316419 * Math.abs(z));
const d = 0.3989423 * Math.exp(-z * z / 2);
let p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
if (z > 0) p = 1 - p;
return p * 100;
}
export function fentonWeightForAge(gaWeeks: number, weightGrams: number, sex: Sex): FentonResult {
const lms = interpolateLMS(fentonLMS[sex], gaWeeks);
const z = calcZ(weightGrams, lms.L, lms.M, lms.S);
const percentile = zToPercentile(z);
return { L: lms.L, M: lms.M, S: lms.S, z, percentile };
}
// Clinical classification — per AAP 2017 / Fenton 2013:
// SGA: <10th percentile, LGA: >90th, AGA: between.
export type SizeForAge = 'SGA' | 'AGA' | 'LGA';
export function classifySizeForAge(percentile: number): SizeForAge {
if (percentile < 10) return 'SGA';
if (percentile > 90) return 'LGA';
return 'AGA';
}
// ============================================================
// Fenton 2013 — peditools-derived higher-accuracy table used by
// the Bedside Neonatal module. Ported VERBATIM from
// public/js/bedside/neonatal.js:20-37. Comment from that file:
// "LMS parameters derived empirically from peditools.org/
// fenton2013 — peditools is widely used and consistent with
// the published Fenton TR, Kim JH. BMC Pediatrics 2013;13:59
// reference. Each week's triple was fit against 6 probe weights
// per week (RMSE < 0.005 z-score units). Replaces an earlier
// hand-rounded table whose z-scores drifted ~0.05 SD from
// peditools/Epic near term."
//
// This table is 21 GA weeks (22-42) in 1-week steps, more granular
// than the coarser 15-week version in fentonLMS above. Use
// fentonLMS for the Calculators / Growth Charts pill (matches the
// vanilla calc output). Use fentonLmsPeditools for the Bedside
// Neonatal sub-module (matches peditools.org z-scores to 3 decimal
// places and is what Daniel uses clinically).
// ============================================================
export const fentonLmsPeditools: Record<Sex, Record<number, Lms>> = {
male: {
22: { L: 0.5885, M: 496, S: 0.12802 }, 23: { L: 0.7565, M: 571, S: 0.14547 },
24: { L: 0.9128, M: 651, S: 0.16235 }, 25: { L: 1.0544, M: 741, S: 0.17765 },
26: { L: 1.1862, M: 841, S: 0.19029 }, 27: { L: 1.3051, M: 953, S: 0.19989 },
28: { L: 1.3699, M: 1079, S: 0.20777 }, 29: { L: 1.4165, M: 1223, S: 0.21163 },
30: { L: 1.4172, M: 1388, S: 0.21185 }, 31: { L: 1.3755, M: 1578, S: 0.20785 },
32: { L: 1.2952, M: 1790, S: 0.20112 }, 33: { L: 1.1974, M: 2018, S: 0.19143 },
34: { L: 1.0743, M: 2255, S: 0.18119 }, 35: { L: 0.9583, M: 2493, S: 0.16992 },
36: { L: 0.8460, M: 2726, S: 0.16001 }, 37: { L: 0.7543, M: 2947, S: 0.15072 },
38: { L: 0.6650, M: 3156, S: 0.14304 }, 39: { L: 0.5881, M: 3360, S: 0.13641 },
40: { L: 0.5237, M: 3568, S: 0.13173 }, 41: { L: 0.4691, M: 3785, S: 0.12863 },
42: { L: 0.4216, M: 4014, S: 0.12735 },
},
female: {
22: { L: -0.0868, M: 481, S: 0.13605 }, 23: { L: 0.2119, M: 537, S: 0.14635 },
24: { L: 0.5281, M: 606, S: 0.16134 }, 25: { L: 0.8258, M: 694, S: 0.18077 },
26: { L: 1.0501, M: 792, S: 0.19889 }, 27: { L: 1.2084, M: 899, S: 0.21323 },
28: { L: 1.2599, M: 1017, S: 0.22437 }, 29: { L: 1.2539, M: 1152, S: 0.22982 },
30: { L: 1.2262, M: 1306, S: 0.23082 }, 31: { L: 1.1223, M: 1482, S: 0.22733 },
32: { L: 1.0122, M: 1681, S: 0.21846 }, 33: { L: 0.8746, M: 1897, S: 0.20681 },
34: { L: 0.7299, M: 2126, S: 0.19407 }, 35: { L: 0.5929, M: 2362, S: 0.18059 },
36: { L: 0.4534, M: 2602, S: 0.17028 }, 37: { L: 0.3462, M: 2835, S: 0.16139 },
38: { L: 0.2636, M: 3050, S: 0.15513 }, 39: { L: 0.2069, M: 3239, S: 0.15004 },
40: { L: 0.1670, M: 3415, S: 0.14649 }, 41: { L: 0.1517, M: 3596, S: 0.14359 },
42: { L: 0.1308, M: 3787, S: 0.14127 },
},
};
// Neonatal-specific Z — uses |L|<0.001 threshold instead of L===0
// to handle the interpolated near-zero L values cleanly.
// Verbatim from public/js/bedside/neonatal.js:53-56.
export function calcZNeonatal(value: number, L: number, M: number, S: number): number {
if (Math.abs(L) < 0.001) return Math.log(value / M) / S;
return (Math.pow(value / M, L) - 1) / (L * S);
}
// Abramowitz & Stegun normal CDF (Erf form), verbatim from
// public/js/bedside/neonatal.js:58-66. This is a different
// approximation from zToPercentile above (which uses the Pythagoras
// polynomial form) — same asymptotic answer, different rounding.
// Kept as a separate function so the Neonatal numbers match the
// vanilla Bedside output exactly.
export function zToPercentileNeonatal(z: number): number {
const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741;
const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911;
const sign = z < 0 ? -1 : 1;
const x = Math.abs(z) / Math.sqrt(2);
const t = 1 / (1 + p * x);
const y = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x);
return Math.round(((1 + sign * y) / 2) * 1000) / 10;
}
export interface NeonatalGaClass { label: string; color: string; icon: string }
export interface NeonatalBwClass { label: string; color: string }
export interface NeonatalWeightClass { label: string; color: string; detail: string }
export function classifyGA(weeks: number, days = 0): NeonatalGaClass {
const total = weeks + days / 7;
if (total < 28) return { label: 'Extremely Preterm', color: '#dc2626', icon: 'triangle-exclamation' };
if (total < 32) return { label: 'Very Preterm', color: '#ea580c', icon: 'triangle-exclamation' };
if (total < 34) return { label: 'Moderate Preterm', color: '#d97706', icon: 'circle-exclamation' };
if (total < 37) return { label: 'Late Preterm', color: '#ca8a04', icon: 'circle-info' };
if (total < 39) return { label: 'Early Term', color: '#2563eb', icon: 'circle-info' };
if (total < 41) return { label: 'Full Term', color: '#16a34a', icon: 'circle-check' };
if (total < 42) return { label: 'Late Term', color: '#d97706', icon: 'circle-info' };
return { label: 'Post Term', color: '#dc2626', icon: 'triangle-exclamation' };
}
export function classifyBirthWeight(grams: number): NeonatalBwClass {
if (grams < 1000) return { label: 'Extremely Low Birth Weight (ELBW)', color: '#dc2626' };
if (grams < 1500) return { label: 'Very Low Birth Weight (VLBW)', color: '#ea580c' };
if (grams < 2500) return { label: 'Low Birth Weight (LBW)', color: '#d97706' };
if (grams <= 4000) return { label: 'Normal Birth Weight', color: '#16a34a' };
return { label: 'Macrosomia (>4000g)', color: '#ea580c' };
}
export function classifyWeightPercentile(percentile: number): NeonatalWeightClass {
if (percentile < 3) return { label: 'Severely SGA', color: '#dc2626', detail: '<3rd percentile' };
if (percentile < 10) return { label: 'SGA', color: '#ea580c', detail: '<10th percentile' };
if (percentile > 97) return { label: 'Severely LGA', color: '#dc2626', detail: '>97th percentile' };
if (percentile > 90) return { label: 'LGA', color: '#ea580c', detail: '>90th percentile' };
return { label: 'AGA', color: '#16a34a', detail: '10th-90th percentile' };
}
// Main neonatal assessment using the peditools-accurate Fenton table.
export interface NeonatalAssessment {
gaDecimal: number;
gaClass: NeonatalGaClass;
bwClass: NeonatalBwClass;
weightClass: NeonatalWeightClass;
expectedWeight: number;
z: number;
percentile: number;
L: number; M: number; S: number;
}
export function neonatalAssess(weeks: number, days: number, weightGrams: number, sex: Sex): NeonatalAssessment {
const gaDecimal = weeks + days / 7;
const lms = interpolateLMS(fentonLmsPeditools[sex], gaDecimal);
const z = calcZNeonatal(weightGrams, lms.L, lms.M, lms.S);
const percentile = zToPercentileNeonatal(z);
return {
gaDecimal,
gaClass: classifyGA(weeks, days),
bwClass: classifyBirthWeight(weightGrams),
weightClass: classifyWeightPercentile(percentile),
expectedWeight: Math.round(lms.M),
z,
percentile,
L: lms.L, M: lms.M, S: lms.S,
};
}