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.
225 lines
11 KiB
TypeScript
225 lines
11 KiB
TypeScript
// ============================================================
|
||
// 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,
|
||
};
|
||
}
|