// ============================================================ // 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> = { 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, 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> = { 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, }; }