// ============================================================ // BMI-FOR-AGE — CDC 2000 LMS. Table + math + classification // ported VERBATIM from public/js/calculators.js:739-1017. // Vitest parity driven by calc-vectors.json. // // CDC reference: // Kuczmarski RJ, Ogden CL, Guo SS, et al. 2000 CDC Growth // Charts for the United States: methods and development. // Vital Health Stat 11. 2002;(246):1-190. // ============================================================ import type { Sex, Lms } from './fenton'; import { interpolateLMS } from './fenton'; // 37 age points (24-240 months, 6-mo steps) × 2 sexes. Verbatim from // calculators.js:739. Values are floating-point to 6+ decimals; the // test vectors pin every one. export const bmiLMS: Record> = { male: { 24: { L: -1.982374, M: 16.5478, S: 0.080127 }, 30: { L: -1.642107, M: 16.2497, S: 0.075499 }, 36: { L: -1.419991, M: 16.0003, S: 0.072634 }, 42: { L: -1.438165, M: 15.7941, S: 0.071495 }, 48: { L: -1.714869, M: 15.6282, S: 0.071889 }, 54: { L: -2.155348, M: 15.5026, S: 0.073491 }, 60: { L: -2.615166, M: 15.4191, S: 0.075992 }, 66: { L: -2.981797, M: 15.3795, S: 0.079211 }, 72: { L: -3.211705, M: 15.3835, S: 0.083048 }, 78: { L: -3.314769, M: 15.429, S: 0.0874 }, 84: { L: -3.323189, M: 15.5129, S: 0.092131 }, 90: { L: -3.270455, M: 15.6317, S: 0.097082 }, 96: { L: -3.183058, M: 15.7823, S: 0.102091 }, 102: { L: -3.079383, M: 15.9617, S: 0.107013 }, 108: { L: -2.971148, M: 16.1671, S: 0.111721 }, 114: { L: -2.865311, M: 16.3961, S: 0.116113 }, 120: { L: -2.765648, M: 16.6461, S: 0.120112 }, 126: { L: -2.673903, M: 16.9151, S: 0.123664 }, 132: { L: -2.59056, M: 17.2009, S: 0.126735 }, 138: { L: -2.51532, M: 17.5014, S: 0.129309 }, 144: { L: -2.447426, M: 17.8146, S: 0.131389 }, 150: { L: -2.385858, M: 18.1387, S: 0.132991 }, 156: { L: -2.329457, M: 18.4718, S: 0.134141 }, 162: { L: -2.277017, M: 18.812, S: 0.13488 }, 168: { L: -2.227362, M: 19.1576, S: 0.135251 }, 174: { L: -2.179426, M: 19.5067, S: 0.135309 }, 180: { L: -2.132345, M: 19.8577, S: 0.13511 }, 186: { L: -2.085574, M: 20.2086, S: 0.134718 }, 192: { L: -2.039015, M: 20.5576, S: 0.134198 }, 198: { L: -1.99315, M: 20.9029, S: 0.13362 }, 204: { L: -1.949135, M: 21.2425, S: 0.133057 }, 210: { L: -1.908831, M: 21.5742, S: 0.132585 }, 216: { L: -1.87467, M: 21.8959, S: 0.132286 }, 222: { L: -1.849323, M: 22.2054, S: 0.132249 }, 228: { L: -1.835138, M: 22.5007, S: 0.132566 }, 234: { L: -1.833401, M: 22.7799, S: 0.133339 }, 240: { L: -1.843581, M: 23.0414, S: 0.134675 }, }, female: { 24: { L: -1.024497, M: 16.388, S: 0.085026 }, 30: { L: -1.534542, M: 16.0059, S: 0.080932 }, 36: { L: -2.096829, M: 15.6992, S: 0.078605 }, 42: { L: -2.618733, M: 15.4647, S: 0.077904 }, 48: { L: -3.018522, M: 15.2985, S: 0.078713 }, 54: { L: -3.2593, M: 15.1961, S: 0.080904 }, 60: { L: -3.350078, M: 15.1519, S: 0.0843 }, 66: { L: -3.325522, M: 15.1606, S: 0.08868 }, 72: { L: -3.225607, M: 15.2169, S: 0.093803 }, 78: { L: -3.084291, M: 15.3161, S: 0.099427 }, 84: { L: -2.926187, M: 15.4536, S: 0.105325 }, 90: { L: -2.76731, M: 15.6252, S: 0.111295 }, 96: { L: -2.617192, M: 15.827, S: 0.117159 }, 102: { L: -2.480952, M: 16.0552, S: 0.122771 }, 108: { L: -2.360921, M: 16.3061, S: 0.128014 }, 114: { L: -2.257782, M: 16.5763, S: 0.132797 }, 120: { L: -2.171296, M: 16.8623, S: 0.137057 }, 126: { L: -2.100749, M: 17.161, S: 0.140754 }, 132: { L: -2.045235, M: 17.4691, S: 0.143868 }, 138: { L: -2.003802, M: 17.7836, S: 0.146399 }, 144: { L: -1.975521, M: 18.1015, S: 0.148361 }, 150: { L: -1.95952, M: 18.42, S: 0.149783 }, 156: { L: -1.954978, M: 18.7364, S: 0.150705 }, 162: { L: -1.9611, M: 19.0481, S: 0.151176 }, 168: { L: -1.977074, M: 19.3526, S: 0.151256 }, 174: { L: -2.002014, M: 19.6475, S: 0.15101 }, 180: { L: -2.034893, M: 19.9306, S: 0.150512 }, 186: { L: -2.07446, M: 20.1998, S: 0.149843 }, 192: { L: -2.119157, M: 20.4533, S: 0.14909 }, 198: { L: -2.167045, M: 20.6891, S: 0.148349 }, 204: { L: -2.215738, M: 20.9058, S: 0.147723 }, 210: { L: -2.262382, M: 21.1016, S: 0.147323 }, 216: { L: -2.303688, M: 21.2753, S: 0.147269 }, 222: { L: -2.336038, M: 21.4255, S: 0.147689 }, 228: { L: -2.355678, M: 21.5508, S: 0.148724 }, 234: { L: -2.35898, M: 21.6501, S: 0.150521 }, 240: { L: -2.342797, M: 21.7219, S: 0.153241 }, }, }; // Abramowitz & Stegun erf-form normal CDF (0-1 range). Verbatim from // calculators.js:754-761. export function normalCDF(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 0.5 * (1 + sign * y); } // Verbatim from calculators.js:741-752. export interface BmiZ { z: number; percentile: number } export function calcBmiPercentile(bmi: number, L: number, M: number, S: number): BmiZ { let z: number; if (Math.abs(L) < 0.001) z = Math.log(bmi / M) / S; else z = (Math.pow(bmi / M, L) - 1) / (L * S); const p = normalCDF(z); return { z, percentile: Math.round(p * 10000) / 100 }; } // Verbatim from calculators.js:1000-1017. export type BmiClassification = | 'Underweight' | 'Healthy Weight' | 'Overweight' | 'Obese (Class 1)' | 'Class 2 Severe Obesity' | 'Class 3 Severe Obesity'; export interface BmiClass { label: BmiClassification; color: string; bg: string; pctOf95: number; bmi95: number; } export function classifyBMI(percentile: number, bmi: number, lms: Lms): BmiClass { const p95z = 1.645; const bmi95 = Math.abs(lms.L) < 0.001 ? lms.M * Math.exp(lms.S * p95z) : lms.M * Math.pow(1 + lms.L * lms.S * p95z, 1 / lms.L); const pctOf95 = (bmi / bmi95) * 100; if (percentile >= 95) { if (pctOf95 >= 140) return { label: 'Class 3 Severe Obesity', color: '#7f1d1d', bg: '#fecaca', pctOf95, bmi95 }; if (pctOf95 >= 120) return { label: 'Class 2 Severe Obesity', color: '#dc2626', bg: '#fee2e2', pctOf95, bmi95 }; return { label: 'Obese (Class 1)', color: '#ef4444', bg: '#fee2e2', pctOf95, bmi95 }; } if (percentile >= 85) return { label: 'Overweight', color: '#f97316', bg: '#ffedd5', pctOf95, bmi95 }; if (percentile >= 5) return { label: 'Healthy Weight', color: '#10b981', bg: '#d1fae5', pctOf95, bmi95 }; return { label: 'Underweight', color: '#f59e0b', bg: '#fef3c7', pctOf95, bmi95 }; } // Top-level helper combining interpolate + math + classification. // Mirrors the click-handler flow in calculators.js:1019-1038. export interface BmiResult extends BmiZ { bmi: number; L: number; M: number; S: number; classification: BmiClass; } export function computeBmi(weightKg: number, heightCm: number, ageMonths: number, sex: Sex): BmiResult { const bmi = weightKg / Math.pow(heightCm / 100, 2); const clamped = Math.max(24, Math.min(240, ageMonths)); const lms = interpolateLMS(bmiLMS[sex], clamped); const { z, percentile } = calcBmiPercentile(bmi, lms.L, lms.M, lms.S); const classification = classifyBMI(percentile, bmi, lms); return { bmi, z, percentile, L: lms.L, M: lms.M, S: lms.S, classification }; }