Ships the AAP 2022 phototherapy/exchange nomograms, the Bhutani 1999
risk zones, and the Fenton 2013 preterm weight-for-GA chart as real
React calculators. These were the high-risk "table-driven" ports the
migration checkpoint flagged as needing captured vectors before
landing — the whole vector-capture workflow now exists and can be
reused for BP / BMI / growth-beyond-Fenton next.
Workflow, so future calculator ports have a template:
scripts/capture-calc-vectors.js
Standalone Node script. Data tables + math inlined VERBATIM from
public/js/calculators.js (no rewriting, no reformatting). Picks 83
carefully chosen test cases — edge-of-domain, table-key-exact,
interpolated-between-keys, and clinical-decision-boundary values —
and emits e2e/fixtures/calc-vectors.json with
{ inputs, output } pairs produced by the authoritative math.
e2e/fixtures/calc-vectors.json
12 Bhutani cases, 58 AAP 2022 cases, 13 Fenton cases. Re-run
capture-calc-vectors.js whenever the vanilla file changes.
shared/clinical/bilirubin.ts
• 17 HourTable constants ported byte-for-byte from calculators.js
lines 1489-1512: photo 35w/36w/37w/38w/39w/40+ (low risk) +
35w/36w/37w/38+ (medium risk), same 8 for exchange.
• Bhutani zones (p95/p75/p40) from lines 1644-1651.
• interpolateThreshold helper (lines 1514-1525).
• classifyBhutani + classifyAapBili functions mirror the vanilla
click-handler logic.
shared/clinical/fenton.ts
• 15-week × 2-sex LMS table from lines 1168-1183 preserved entry-
for-entry.
• interpolateLMS + calcZ + zToPercentile (Abramowitz & Stegun
normal CDF, lines 2299-2326) ported byte-for-byte.
• fentonWeightForAge returns { L, M, S, z, percentile };
classifySizeForAge labels SGA/<10 / AGA / LGA/>90.
shared/clinical/bilirubin.test.ts + fenton.test.ts
Vitest suites that import e2e/fixtures/calc-vectors.json and assert
classifyBhutani / classifyAapBili / fentonWeightForAge match
every captured vector to 10 decimal places for threshold values
and 6 decimals for the Fenton M (grams, so 6 is >= 1e-3 g).
All 102 tests pass locally (19 prior + 70 bili + 13 Fenton).
client/src/pages/Calculators.tsx
• BiliPanel with AAP 2022 / Bhutani mode switch, GA + risk-factor
dropdowns, color-coded status / zone badges, both threshold
pairs surfaced in the result grid.
• GrowthPanel runs Fenton with sex + GA + weight inputs, emits
Z-score, percentile, L/M/S reference, and SGA/AGA/LGA label.
• PILLS flags bili + growth as ported: true; ActivePanel routes to
the new components. BP / BMI / vitals / resus / equipment
remain legacy-linked until their own vectors land.
e2e/tests/calculators-react.spec.js
Three new parity tests covering above-phototherapy/above-exchange
transitions (38w 72h TSB 20 → phototherapy; TSB 26 → exchange),
Bhutani high-risk classification at 36h TSB 13, and Fenton 32w
male 1795g landing exactly at 50th percentile AGA.
Backend tsc + client tsc + vite build + vitest (102/102) all green.
Bundle 619 kB / 178 kB gz (+10 kB for the bili tables).
266 lines
24 KiB
JavaScript
266 lines
24 KiB
JavaScript
#!/usr/bin/env node
|
|
// ============================================================
|
|
// CAPTURE-CALC-VECTORS — writes e2e/fixtures/calc-vectors.json with
|
|
// authoritative outputs from the vanilla calculator math.
|
|
//
|
|
// The math + data tables below are lifted VERBATIM from
|
|
// public/js/calculators.js (with the IIFE wrapper stripped). No
|
|
// modifications, no reformatting — any rewrite here would defeat
|
|
// the whole point of the capture script.
|
|
//
|
|
// Each entry in the generated JSON has:
|
|
// { name, inputs, output }
|
|
//
|
|
// Run with: node scripts/capture-calc-vectors.js
|
|
// Re-run whenever the vanilla file's math or tables change.
|
|
// ============================================================
|
|
|
|
'use strict';
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// ── Bhutani 1999 zones ─────────────────────────────────────
|
|
// From public/js/calculators.js lines 1644-1651.
|
|
const bhutaniZones = {
|
|
p95: { 6:6.0, 12:7.2, 18:8.5, 24:9.6, 30:11.2, 36:12.8, 42:13.8, 48:14.8,
|
|
54:15.6, 60:16.2, 66:16.8, 72:17.4, 84:18.0, 96:18.4, 108:18.8, 120:19.0 },
|
|
p75: { 6:4.5, 12:5.5, 18:6.6, 24:7.8, 30:9.2, 36:10.6, 42:11.6, 48:12.6,
|
|
54:13.4, 60:14.0, 66:14.6, 72:15.0, 84:15.4, 96:15.6, 108:15.8, 120:16.0 },
|
|
p40: { 6:3.0, 12:4.0, 18:5.0, 24:6.2, 30:7.2, 36:8.4, 42:9.2, 48:10.0,
|
|
54:10.6, 60:11.2, 66:11.8, 72:12.2, 84:12.6, 96:12.8, 108:13.0, 120:13.2 }
|
|
};
|
|
|
|
// ── AAP 2022 phototherapy + exchange tables (verbatim lines 1489-1512) ──
|
|
const photoThresholds35 = {12:8.5,13:8.7,14:8.9,15:9.0,16:9.2,17:9.4,18:9.6,19:9.8,20:9.9,21:10.1,22:10.3,23:10.4,24:10.6,25:10.8,26:10.9,27:11.1,28:11.3,29:11.4,30:11.6,31:11.7,32:11.9,33:12.0,34:12.2,35:12.3,36:12.5,37:12.6,38:12.8,39:12.9,40:13.1,41:13.2,42:13.4,43:13.5,44:13.6,45:13.8,46:13.9,47:14.0,48:14.2,49:14.3,50:14.4,51:14.5,52:14.7,53:14.8,54:14.9,55:15.0,56:15.1,57:15.3,58:15.4,59:15.5,60:15.6,61:15.7,62:15.8,63:15.9,64:16.0,65:16.1,66:16.2,67:16.3,68:16.4,69:16.5,70:16.6,71:16.7,72:16.8,73:16.9,74:17.0,75:17.1,76:17.2,77:17.3,78:17.4,79:17.5,80:17.5,81:17.6,82:17.7,83:17.8,84:17.8,85:17.9,86:18.0,87:18.1,88:18.1,89:18.2,90:18.3,91:18.3,92:18.4,93:18.5,94:18.5,95:18.6,96:18.6};
|
|
const photoThresholds36 = {12:9.0,13:9.2,14:9.4,15:9.6,16:9.8,17:9.9,18:10.1,19:10.3,20:10.5,21:10.6,22:10.8,23:11.0,24:11.2,25:11.3,26:11.5,27:11.7,28:11.8,29:12.0,30:12.1,31:12.3,32:12.5,33:12.6,34:12.8,35:12.9,36:13.1,37:13.2,38:13.4,39:13.5,40:13.7,41:13.8,42:13.9,43:14.1,44:14.2,45:14.4,46:14.5,47:14.6,48:14.8,49:14.9,50:15.0,51:15.1,52:15.3,53:15.4,54:15.5,55:15.6,56:15.8,57:15.9,58:16.0,59:16.1,60:16.2,61:16.3,62:16.5,63:16.6,64:16.7,65:16.8,66:16.9,67:17.0,68:17.1,69:17.2,70:17.3,71:17.4,72:17.5,73:17.6,74:17.7,75:17.8,76:17.9,77:17.9,78:18.0,79:18.1,80:18.2,81:18.3,82:18.4,83:18.4,84:18.5,85:18.6,86:18.7,87:18.8,88:18.8,89:18.9,90:19.0,91:19.0,92:19.1,93:19.2,94:19.2,95:19.3,96:19.3};
|
|
const photoThresholds37 = {12:9.6,13:9.8,14:9.9,15:10.1,16:10.3,17:10.5,18:10.7,19:10.8,20:11.0,21:11.2,22:11.4,23:11.5,24:11.7,25:11.9,26:12.1,27:12.2,28:12.4,29:12.5,30:12.7,31:12.9,32:13.0,33:13.2,34:13.3,35:13.5,36:13.6,37:13.8,38:13.9,39:14.1,40:14.2,41:14.4,42:14.5,43:14.7,44:14.8,45:15.0,46:15.1,47:15.2,48:15.4,49:15.5,50:15.6,51:15.8,52:15.9,53:16.0,54:16.1,55:16.3,56:16.4,57:16.5,58:16.6,59:16.7,60:16.9,61:17.0,62:17.1,63:17.2,64:17.3,65:17.4,66:17.5,67:17.6,68:17.7,69:17.8,70:17.9,71:18.0,72:18.1,73:18.2,74:18.3,75:18.4,76:18.5,77:18.6,78:18.7,79:18.8,80:18.9,81:19.0,82:19.0,83:19.1,84:19.2,85:19.3,86:19.4,87:19.4,88:19.5,89:19.6,90:19.7,91:19.7,92:19.8,93:19.9,94:19.9,95:20.0,96:20.0};
|
|
const photoThresholds38 = {12:10.1,13:10.3,14:10.5,15:10.7,16:10.8,17:11.0,18:11.2,19:11.4,20:11.6,21:11.7,22:11.9,23:12.1,24:12.3,25:12.4,26:12.6,27:12.8,28:12.9,29:13.1,30:13.3,31:13.4,32:13.6,33:13.8,34:13.9,35:14.1,36:14.2,37:14.4,38:14.5,39:14.7,40:14.8,41:15.0,42:15.1,43:15.3,44:15.4,45:15.6,46:15.7,47:15.8,48:16.0,49:16.1,50:16.2,51:16.4,52:16.5,53:16.6,54:16.8,55:16.9,56:17.0,57:17.1,58:17.3,59:17.4,60:17.5,61:17.6,62:17.7,63:17.8,64:17.9,65:18.1,66:18.2,67:18.3,68:18.4,69:18.5,70:18.6,71:18.7,72:18.8,73:18.9,74:19.0,75:19.1,76:19.2,77:19.3,78:19.4,79:19.5,80:19.5,81:19.6,82:19.7,83:19.8,84:19.9,85:20.0,86:20.0,87:20.1,88:20.2,89:20.3,90:20.3,91:20.4,92:20.5,93:20.6,94:20.6,95:20.7,96:20.7};
|
|
const photoThresholds39 = {12:10.6,13:10.8,14:11.0,15:11.2,16:11.4,17:11.6,18:11.8,19:11.9,20:12.1,21:12.3,22:12.5,23:12.7,24:12.8,25:13.0,26:13.2,27:13.3,28:13.5,29:13.7,30:13.8,31:14.0,32:14.2,33:14.3,34:14.5,35:14.7,36:14.8,37:15.0,38:15.1,39:15.3,40:15.4,41:15.6,42:15.7,43:15.9,44:16.0,45:16.2,46:16.3,47:16.4,48:16.6,49:16.7,50:16.8,51:17.0,52:17.1,53:17.2,54:17.4,55:17.5,56:17.6,57:17.8,58:17.9,59:18.0,60:18.1,61:18.2,62:18.4,63:18.5,64:18.6,65:18.7,66:18.8,67:18.9,68:19.0,69:19.1,70:19.2,71:19.3,72:19.5,73:19.6,74:19.7,75:19.7,76:19.8,77:19.9,78:20.0,79:20.1,80:20.2,81:20.3,82:20.4,83:20.5,84:20.6,85:20.6,86:20.7,87:20.8,88:20.9,89:21.0,90:21.0,91:21.1,92:21.2,93:21.3,94:21.3,95:21.4,96:21.5};
|
|
const photoThresholds40 = {12:11.2,13:11.3,14:11.5,15:11.7,16:11.9,17:12.1,18:12.3,19:12.5,20:12.7,21:12.8,22:13.0,23:13.2,24:13.4,25:13.6,26:13.7,27:13.9,28:14.1,29:14.2,30:14.4,31:14.6,32:14.7,33:14.9,34:15.1,35:15.2,36:15.4,37:15.6,38:15.7,39:15.9,40:16.0,41:16.2,42:16.3,43:16.5,44:16.6,45:16.8,46:16.9,47:17.1,48:17.2,49:17.4,50:17.5,51:17.6,52:17.8,53:17.9,54:18.1,55:18.2,56:18.3,57:18.5,58:18.6,59:18.7,60:18.9,61:19.0,62:19.1,63:19.3,64:19.4,65:19.5,66:19.6,67:19.7,68:19.9,69:20.0,70:20.1,71:20.2,72:20.3,73:20.4,74:20.5,75:20.6,76:20.7,77:20.8,78:20.9,79:21.0,80:21.1,81:21.2,82:21.3,83:21.4,84:21.5,85:21.5,86:21.6,87:21.7,88:21.8,89:21.9,90:21.9,91:22.0,92:22.1,93:22.2,94:22.2,95:22.3,96:22.3};
|
|
|
|
const photoThresholds35risk = {12:6.9,13:7.1,14:7.2,15:7.4,16:7.6,17:7.7,18:7.9,19:8.1,20:8.2,21:8.4,22:8.6,23:8.7,24:8.9,25:9.0,26:9.2,27:9.3,28:9.5,29:9.6,30:9.8,31:9.9,32:10.1,33:10.2,34:10.3,35:10.5,36:10.6,37:10.8,38:10.9,39:11.0,40:11.2,41:11.3,42:11.4,43:11.5,44:11.7,45:11.8,46:11.9,47:12.0,48:12.2,49:12.3,50:12.4,51:12.5,52:12.6,53:12.7,54:12.8,55:13.0,56:13.1,57:13.2,58:13.3,59:13.4,60:13.5,61:13.6,62:13.7,63:13.8,64:13.9,65:14.0,66:14.1,67:14.2,68:14.2,69:14.3,70:14.4,71:14.5,72:14.6,73:14.7,74:14.8,75:14.8,76:14.9,77:15.0,78:15.1,79:15.1,80:15.2,81:15.3,82:15.3,83:15.4,84:15.5,85:15.5,86:15.6,87:15.7,88:15.7,89:15.8,90:15.8,91:15.9,92:15.9,93:16.0,94:16.1,95:16.1,96:16.1};
|
|
const photoThresholds36risk = {12:7.4,13:7.6,14:7.8,15:8.0,16:8.1,17:8.3,18:8.5,19:8.6,20:8.8,21:9.0,22:9.1,23:9.3,24:9.4,25:9.6,26:9.8,27:9.9,28:10.1,29:10.2,30:10.4,31:10.5,32:10.7,33:10.8,34:11.0,35:11.1,36:11.2,37:11.4,38:11.5,39:11.7,40:11.8,41:11.9,42:12.1,43:12.2,44:12.3,45:12.5,46:12.6,47:12.7,48:12.8,49:13.0,50:13.1,51:13.2,52:13.3,53:13.4,54:13.5,55:13.7,56:13.8,57:13.9,58:14.0,59:14.1,60:14.2,61:14.3,62:14.4,63:14.5,64:14.6,65:14.7,66:14.8,67:14.9,68:15.0,69:15.1,70:15.2,71:15.3,72:15.4,73:15.4,74:15.5,75:15.6,76:15.7,77:15.8,78:15.8,79:15.9,80:16.0,81:16.1,82:16.1,83:16.2,84:16.3,85:16.4,86:16.4,87:16.5,88:16.6,89:16.6,90:16.7,91:16.7,92:16.8,93:16.8,94:16.9,95:17.0,96:17.0};
|
|
const photoThresholds37risk = {12:8.0,13:8.1,14:8.3,15:8.5,16:8.7,17:8.9,18:9.0,19:9.2,20:9.4,21:9.5,22:9.7,23:9.9,24:10.0,25:10.2,26:10.4,27:10.5,28:10.7,29:10.8,30:11.0,31:11.1,32:11.3,33:11.4,34:11.6,35:11.7,36:11.9,37:12.0,38:12.2,39:12.3,40:12.4,41:12.6,42:12.7,43:12.9,44:13.0,45:13.1,46:13.2,47:13.4,48:13.5,49:13.6,50:13.8,51:13.9,52:14.0,53:14.1,54:14.2,55:14.4,56:14.5,57:14.6,58:14.7,59:14.8,60:14.9,61:15.0,62:15.1,63:15.2,64:15.3,65:15.4,66:15.5,67:15.6,68:15.7,69:15.8,70:15.9,71:16.0,72:16.1,73:16.2,74:16.3,75:16.4,76:16.5,77:16.6,78:16.6,79:16.7,80:16.8,81:16.9,82:17.0,83:17.0,84:17.1,85:17.2,86:17.2,87:17.3,88:17.4,89:17.4,90:17.5,91:17.6,92:17.6,93:17.7,94:17.8,95:17.8,96:17.9};
|
|
const photoThresholds38risk = {12:8.5,13:8.6,14:8.8,15:9.0,16:9.2,17:9.4,18:9.5,19:9.7,20:9.9,21:10.0,22:10.2,23:10.4,24:10.5,25:10.7,26:10.8,27:11.0,28:11.2,29:11.3,30:11.5,31:11.6,32:11.8,33:11.9,34:12.1,35:12.2,36:12.4,37:12.5,38:12.7,39:12.8,40:12.9,41:13.1,42:13.2,43:13.3,44:13.5,45:13.6,46:13.7,47:13.9,48:14.0,49:14.1,50:14.2,51:14.4,52:14.5,53:14.6,54:14.7,55:14.8,56:14.9,57:15.1,58:15.2,59:15.3,60:15.4,61:15.5,62:15.6,63:15.7,64:15.8,65:15.9,66:16.0,67:16.1,68:16.2,69:16.3,70:16.4,71:16.5,72:16.6,73:16.6,74:16.7,75:16.8,76:16.9,77:17.0,78:17.1,79:17.1,80:17.2,81:17.3,82:17.4,83:17.4,84:17.5,85:17.6,86:17.6,87:17.7,88:17.8,89:17.8,90:17.9,91:18.0,92:18.0,93:18.1,94:18.1,95:18.2,96:18.2};
|
|
|
|
const exchangeThresholds35 = {12:16.4,13:16.5,14:16.6,15:16.8,16:16.9,17:17.0,18:17.2,19:17.3,20:17.4,21:17.5,22:17.7,23:17.8,24:17.9,25:18.0,26:18.2,27:18.3,28:18.4,29:18.5,30:18.7,31:18.8,32:18.9,33:19.0,34:19.1,35:19.2,36:19.4,37:19.5,38:19.6,39:19.7,40:19.8,41:19.9,42:20.0,43:20.1,44:20.2,45:20.3,46:20.5,47:20.6,48:20.7,49:20.8,50:20.9,51:21.0,52:21.1,53:21.2,54:21.3,55:21.4,56:21.5,57:21.6,58:21.7,59:21.7,60:21.8,61:21.9,62:22.0,63:22.1,64:22.2,65:22.3,66:22.4,67:22.5,68:22.6,69:22.6,70:22.7,71:22.8,72:22.9,73:23.0,74:23.1,75:23.1,76:23.2,77:23.3,78:23.4,79:23.4,80:23.5,81:23.6,82:23.7,83:23.7,84:23.8,85:23.9,86:23.9,87:24.0,88:24.1,89:24.1,90:24.2,91:24.3,92:24.3,93:24.4,94:24.4,95:24.5,96:24.5};
|
|
const exchangeThresholds36 = {12:17.5,13:17.7,14:17.8,15:17.9,16:18.1,17:18.2,18:18.3,19:18.5,20:18.6,21:18.7,22:18.9,23:19.0,24:19.1,25:19.2,26:19.4,27:19.5,28:19.6,29:19.7,30:19.9,31:20.0,32:20.1,33:20.2,34:20.4,35:20.5,36:20.6,37:20.7,38:20.8,39:20.9,40:21.0,41:21.2,42:21.3,43:21.4,44:21.5,45:21.6,46:21.7,47:21.8,48:21.9,49:22.0,50:22.1,51:22.2,52:22.3,53:22.4,54:22.5,55:22.6,56:22.7,57:22.8,58:22.9,59:23.0,60:23.1,61:23.2,62:23.2,63:23.3,64:23.4,65:23.5,66:23.6,67:23.7,68:23.8,69:23.8,70:23.9,71:24.0,72:24.1,73:24.1,74:24.2,75:24.3,76:24.4,77:24.4,78:24.5,79:24.6,80:24.6,81:24.7,82:24.8,83:24.8,84:24.9,85:25.0,86:25.0,87:25.1,88:25.2,89:25.2,90:25.3,91:25.3,92:25.4,93:25.4,94:25.5,95:25.5,96:25.5};
|
|
const exchangeThresholds37 = {12:18.7,13:18.8,14:18.9,15:19.1,16:19.2,17:19.4,18:19.5,19:19.6,20:19.8,21:19.9,22:20.1,23:20.2,24:20.3,25:20.5,26:20.6,27:20.7,28:20.8,29:21.0,30:21.1,31:21.2,32:21.3,33:21.5,34:21.6,35:21.7,36:21.8,37:21.9,38:22.1,39:22.2,40:22.3,41:22.4,42:22.5,43:22.6,44:22.7,45:22.8,46:22.9,47:23.0,48:23.1,49:23.2,50:23.3,51:23.4,52:23.5,53:23.6,54:23.7,55:23.8,56:23.9,57:24.0,58:24.1,59:24.2,60:24.3,61:24.4,62:24.5,63:24.5,64:24.6,65:24.7,66:24.8,67:24.9,68:24.9,69:25.0,70:25.1,71:25.2,72:25.2,73:25.3,74:25.4,75:25.5,76:25.5,77:25.6,78:25.7,79:25.7,80:25.8,81:25.8,82:25.9,83:26.0,84:26.0,85:26.1,86:26.1,87:26.2,88:26.2,89:26.3,90:26.3,91:26.4,92:26.4,93:26.5,94:26.5,95:26.5,96:26.6};
|
|
const exchangeThresholds38 = {12:19.7,13:19.9,14:20.0,15:20.1,16:20.3,17:20.4,18:20.6,19:20.7,20:20.8,21:21.0,22:21.1,23:21.2,24:21.4,25:21.5,26:21.6,27:21.7,28:21.9,29:22.0,30:22.1,31:22.2,32:22.3,33:22.4,34:22.6,35:22.7,36:22.8,37:22.9,38:23.0,39:23.1,40:23.2,41:23.3,42:23.4,43:23.5,44:23.6,45:23.7,46:23.8,47:23.9,48:24.0,49:24.1,50:24.2,51:24.3,52:24.4,53:24.5,54:24.6,55:24.7,56:24.7,57:24.8,58:24.9,59:25.0,60:25.1,61:25.2,62:25.2,63:25.3,64:25.4,65:25.5,66:25.5,67:25.6,68:25.7,69:25.7,70:25.8,71:25.9,72:25.9,73:26.0,74:26.0,75:26.1,76:26.2,77:26.2,78:26.3,79:26.3,80:26.4,81:26.4,82:26.5,83:26.5,84:26.6,85:26.6,86:26.7,87:26.7,88:26.7,89:26.8,90:26.8,91:26.9,92:26.9,93:26.9,94:27.0,95:27.0,96:27.0};
|
|
|
|
const exchangeThresholds35risk = {12:14.6,13:14.8,14:14.9,15:15.0,16:15.1,17:15.3,18:15.4,19:15.5,20:15.6,21:15.8,22:15.9,23:16.0,24:16.1,25:16.2,26:16.3,27:16.4,28:16.5,29:16.6,30:16.8,31:16.9,32:17.0,33:17.1,34:17.2,35:17.3,36:17.4,37:17.5,38:17.6,39:17.7,40:17.7,41:17.8,42:17.9,43:18.0,44:18.1,45:18.2,46:18.3,47:18.4,48:18.5,49:18.5,50:18.6,51:18.7,52:18.8,53:18.9,54:18.9,55:19.0,56:19.1,57:19.2,58:19.2,59:19.3,60:19.4,61:19.4,62:19.5,63:19.6,64:19.6,65:19.7,66:19.8,67:19.8,68:19.9,69:19.9,70:20.0,71:20.1,72:20.1,73:20.2,74:20.2,75:20.3,76:20.3,77:20.4,78:20.4,79:20.5,80:20.5,81:20.6,82:20.6,83:20.6,84:20.7,85:20.7,86:20.8,87:20.8,88:20.8,89:20.9,90:20.9,91:20.9,92:21.0,93:21.0,94:21.0,95:21.1,96:21.1};
|
|
const exchangeThresholds36risk = {12:15.2,13:15.3,14:15.4,15:15.6,16:15.7,17:15.8,18:15.9,19:16.1,20:16.2,21:16.3,22:16.4,23:16.5,24:16.6,25:16.8,26:16.9,27:17.0,28:17.1,29:17.2,30:17.3,31:17.4,32:17.5,33:17.6,34:17.7,35:17.8,36:17.9,37:18.0,38:18.1,39:18.2,40:18.3,41:18.4,42:18.5,43:18.6,44:18.7,45:18.8,46:18.9,47:19.0,48:19.1,49:19.2,50:19.2,51:19.3,52:19.4,53:19.5,54:19.6,55:19.7,56:19.7,57:19.8,58:19.9,59:20.0,60:20.1,61:20.1,62:20.2,63:20.3,64:20.3,65:20.4,66:20.5,67:20.6,68:20.6,69:20.7,70:20.8,71:20.8,72:20.9,73:20.9,74:21.0,75:21.1,76:21.1,77:21.2,78:21.2,79:21.3,80:21.4,81:21.4,82:21.5,83:21.5,84:21.6,85:21.6,86:21.7,87:21.7,88:21.8,89:21.8,90:21.9,91:21.9,92:22.0,93:22.0,94:22.0,95:22.1,96:22.1};
|
|
const exchangeThresholds37risk = {12:15.7,13:15.9,14:16.0,15:16.1,16:16.2,17:16.4,18:16.5,19:16.6,20:16.7,21:16.8,22:17.0,23:17.1,24:17.2,25:17.3,26:17.4,27:17.5,28:17.7,29:17.8,30:17.9,31:18.0,32:18.1,33:18.2,34:18.3,35:18.4,36:18.5,37:18.6,38:18.7,39:18.8,40:18.9,41:19.0,42:19.1,43:19.2,44:19.3,45:19.4,46:19.5,47:19.6,48:19.7,49:19.8,50:19.9,51:20.0,52:20.1,53:20.1,54:20.2,55:20.3,56:20.4,57:20.5,58:20.6,59:20.7,60:20.7,61:20.8,62:20.9,63:21.0,64:21.1,65:21.1,66:21.2,67:21.3,68:21.4,69:21.4,70:21.5,71:21.6,72:21.7,73:21.7,74:21.8,75:21.9,76:21.9,77:22.0,78:22.1,79:22.1,80:22.2,81:22.3,82:22.3,83:22.4,84:22.5,85:22.5,86:22.6,87:22.6,88:22.7,89:22.8,90:22.8,91:22.9,92:22.9,93:23.0,94:23.0,95:23.1,96:23.1};
|
|
const exchangeThresholds38risk = {12:16.3,13:16.4,14:16.5,15:16.6,16:16.7,17:16.9,18:17.0,19:17.1,20:17.2,21:17.3,22:17.4,23:17.6,24:17.7,25:17.8,26:17.9,27:18.0,28:18.1,29:18.2,30:18.3,31:18.4,32:18.5,33:18.7,34:18.8,35:18.9,36:19.0,37:19.1,38:19.2,39:19.3,40:19.4,41:19.5,42:19.6,43:19.7,44:19.8,45:19.9,46:19.9,47:20.0,48:20.1,49:20.2,50:20.3,51:20.4,52:20.5,53:20.6,54:20.7,55:20.8,56:20.8,57:20.9,58:21.0,59:21.1,60:21.2,61:21.3,62:21.3,63:21.4,64:21.5,65:21.6,66:21.7,67:21.7,68:21.8,69:21.9,70:22.0,71:22.0,72:22.1,73:22.2,74:22.2,75:22.3,76:22.4,77:22.5,78:22.5,79:22.6,80:22.7,81:22.7,82:22.8,83:22.8,84:22.9,85:23.0,86:23.0,87:23.1,88:23.1,89:23.2,90:23.3,91:23.3,92:23.4,93:23.4,94:23.5,95:23.5,96:23.5};
|
|
|
|
// ── Fenton 2013 LMS (verbatim from lines 1168-1183) ─────────
|
|
const fentonLMS = {
|
|
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}
|
|
}
|
|
};
|
|
|
|
// ── Helper math (verbatim from lines 1514-1525 + 2299-2328) ──
|
|
function interpolateThreshold(table, hours) {
|
|
const keys = Object.keys(table).map(Number).sort((a, b) => a - b);
|
|
if (hours <= keys[0]) return table[keys[0]];
|
|
if (hours >= keys[keys.length - 1]) return table[keys[keys.length - 1]];
|
|
for (let i = 0; i < keys.length - 1; i++) {
|
|
if (hours >= keys[i] && hours <= keys[i + 1]) {
|
|
const t = (hours - keys[i]) / (keys[i + 1] - keys[i]);
|
|
return table[keys[i]] + t * (table[keys[i + 1]] - table[keys[i]]);
|
|
}
|
|
}
|
|
return table[keys[0]];
|
|
}
|
|
|
|
function interpolateLMS(table, val) {
|
|
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]], 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]];
|
|
}
|
|
|
|
function calcZ(value, L, M, S) {
|
|
if (L === 0) return Math.log(value / M) / S;
|
|
return (Math.pow(value / M, L) - 1) / (L * S);
|
|
}
|
|
|
|
function zToPercentile(z) {
|
|
// Abramowitz & Stegun normal CDF approximation (from lines 2318-2326).
|
|
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;
|
|
}
|
|
|
|
// ── Bhutani classification (verbatim logic from lines 1662-1666) ──
|
|
function classifyBhutani(hours, tsb) {
|
|
const p95 = interpolateThreshold(bhutaniZones.p95, hours);
|
|
const p75 = interpolateThreshold(bhutaniZones.p75, hours);
|
|
const p40 = interpolateThreshold(bhutaniZones.p40, hours);
|
|
let zone;
|
|
if (tsb >= p95) zone = 'High-Risk';
|
|
else if (tsb >= p75) zone = 'High-Intermediate';
|
|
else if (tsb >= p40) zone = 'Low-Intermediate';
|
|
else zone = 'Low-Risk';
|
|
return { p95, p75, p40, zone };
|
|
}
|
|
|
|
// ── AAP 2022 phototherapy + exchange (verbatim logic from lines 1541-1583) ──
|
|
function aapPhotoTable(gaNum, risk) {
|
|
if (risk === 'medium') {
|
|
if (gaNum === 35) return photoThresholds35risk;
|
|
if (gaNum === 36) return photoThresholds36risk;
|
|
if (gaNum === 37) return photoThresholds37risk;
|
|
return photoThresholds38risk; // 38+
|
|
}
|
|
if (gaNum === 35) return photoThresholds35;
|
|
if (gaNum === 36) return photoThresholds36;
|
|
if (gaNum === 37) return photoThresholds37;
|
|
if (gaNum === 38) return photoThresholds38;
|
|
if (gaNum === 39) return photoThresholds39;
|
|
return photoThresholds40; // 40+
|
|
}
|
|
function aapExchangeTable(gaNum, risk) {
|
|
if (risk === 'medium') {
|
|
if (gaNum === 35) return exchangeThresholds35risk;
|
|
if (gaNum === 36) return exchangeThresholds36risk;
|
|
if (gaNum === 37) return exchangeThresholds37risk;
|
|
return exchangeThresholds38risk; // 38+
|
|
}
|
|
if (gaNum === 35) return exchangeThresholds35;
|
|
if (gaNum === 36) return exchangeThresholds36;
|
|
if (gaNum === 37) return exchangeThresholds37;
|
|
return exchangeThresholds38; // 38+
|
|
}
|
|
function classifyAapBili(gaWeeks, hours, tsb, risk) {
|
|
const photoTable = aapPhotoTable(gaWeeks, risk);
|
|
const exchangeTable = aapExchangeTable(gaWeeks, risk);
|
|
const photoThreshold = interpolateThreshold(photoTable, hours);
|
|
const exchangeThreshold = interpolateThreshold(exchangeTable, hours);
|
|
const abovePhoto = tsb >= photoThreshold;
|
|
const aboveExchange = tsb >= exchangeThreshold;
|
|
let status;
|
|
if (aboveExchange) status = 'Above Exchange';
|
|
else if (abovePhoto) status = 'Above Phototherapy';
|
|
else status = 'Below Phototherapy';
|
|
return { photoThreshold, exchangeThreshold, status };
|
|
}
|
|
|
|
// ── Fenton weight-for-GA Z + percentile (verbatim 1260-1266) ──
|
|
function fentonWeightForAge(gaWeeks, weightGrams, sex) {
|
|
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 };
|
|
}
|
|
|
|
// ── Case picker ────────────────────────────────────────────
|
|
const bhutaniCases = [
|
|
// Edges of the domain
|
|
{ name: 'Bhutani low-end (6h)', inputs: { hours: 6, tsb: 5 } },
|
|
{ name: 'Bhutani high-end (120h)', inputs: { hours: 120, tsb: 18 } },
|
|
// Exactly on each curve boundary at a few time points
|
|
{ name: 'Bhutani exactly p95 at 24h', inputs: { hours: 24, tsb: 9.6 } },
|
|
{ name: 'Bhutani exactly p75 at 24h', inputs: { hours: 24, tsb: 7.8 } },
|
|
{ name: 'Bhutani exactly p40 at 24h', inputs: { hours: 24, tsb: 6.2 } },
|
|
// Interpolated (between table keys)
|
|
{ name: 'Bhutani interpolated 27h', inputs: { hours: 27, tsb: 9 } },
|
|
{ name: 'Bhutani interpolated 45h', inputs: { hours: 45, tsb: 13 } },
|
|
{ name: 'Bhutani interpolated 78h', inputs: { hours: 78, tsb: 16 } },
|
|
// Classifications across all four zones
|
|
{ name: 'Bhutani low-risk 36h', inputs: { hours: 36, tsb: 7 } },
|
|
{ name: 'Bhutani low-intermediate 36h', inputs: { hours: 36, tsb: 9 } },
|
|
{ name: 'Bhutani high-intermediate 36h', inputs: { hours: 36, tsb: 11 } },
|
|
{ name: 'Bhutani high-risk 36h', inputs: { hours: 36, tsb: 15 } },
|
|
];
|
|
|
|
const aapCases = [];
|
|
[35, 36, 37, 38, 39, 40, 41].forEach((ga) => {
|
|
['low', 'medium'].forEach((risk) => {
|
|
// Low-end of table
|
|
aapCases.push({
|
|
name: `AAP ${ga}w ${risk}-risk at 12h`,
|
|
inputs: { gaWeeks: ga, hours: 12, tsb: 10, risk },
|
|
});
|
|
// Interior
|
|
aapCases.push({
|
|
name: `AAP ${ga}w ${risk}-risk at 48h`,
|
|
inputs: { gaWeeks: ga, hours: 48, tsb: 15, risk },
|
|
});
|
|
// Top of table
|
|
aapCases.push({
|
|
name: `AAP ${ga}w ${risk}-risk at 96h`,
|
|
inputs: { gaWeeks: ga, hours: 96, tsb: 18, risk },
|
|
});
|
|
// Interpolated hour
|
|
aapCases.push({
|
|
name: `AAP ${ga}w ${risk}-risk at 31.5h`,
|
|
inputs: { gaWeeks: ga, hours: 31.5, tsb: 13 },
|
|
// risk attached below; keep spread
|
|
});
|
|
aapCases[aapCases.length - 1].inputs.risk = risk;
|
|
});
|
|
});
|
|
// Below / above exchange edge
|
|
aapCases.push({ name: 'AAP 38w low-risk at 72h TSB 19 (above exchange boundary)', inputs: { gaWeeks: 38, hours: 72, tsb: 19, risk: 'low' } });
|
|
aapCases.push({ name: 'AAP 35w medium-risk at 72h TSB 16 (above phototherapy)', inputs: { gaWeeks: 35, hours: 72, tsb: 16, risk: 'medium' } });
|
|
|
|
const fentonCases = [
|
|
// Edges of GA domain
|
|
{ name: 'Fenton male 22w at 500g', inputs: { sex: 'male', gaWeeks: 22, weightGrams: 500 } },
|
|
{ name: 'Fenton female 22w at 500g', inputs: { sex: 'female', gaWeeks: 22, weightGrams: 500 } },
|
|
{ name: 'Fenton male 50w at 5500g', inputs: { sex: 'male', gaWeeks: 50, weightGrams: 5500 } },
|
|
{ name: 'Fenton female 50w at 5500g', inputs: { sex: 'female', gaWeeks: 50, weightGrams: 5500 } },
|
|
// Exactly on table keys
|
|
{ name: 'Fenton male 28w at 1124g (M)', inputs: { sex: 'male', gaWeeks: 28, weightGrams: 1124 } },
|
|
{ name: 'Fenton female 30w at 1330g (M)', inputs: { sex: 'female', gaWeeks: 30, weightGrams: 1330 } },
|
|
// Interpolated GA weeks
|
|
{ name: 'Fenton male 33w at 2000g', inputs: { sex: 'male', gaWeeks: 33, weightGrams: 2000 } },
|
|
{ name: 'Fenton female 37w at 2800g', inputs: { sex: 'female', gaWeeks: 37, weightGrams: 2800 } },
|
|
{ name: 'Fenton male 40.5w at 3600g', inputs: { sex: 'male', gaWeeks: 40.5, weightGrams: 3600 } },
|
|
// Typical term
|
|
{ name: 'Fenton male 40w at 3500g', inputs: { sex: 'male', gaWeeks: 40, weightGrams: 3500 } },
|
|
{ name: 'Fenton female 40w at 3200g', inputs: { sex: 'female', gaWeeks: 40, weightGrams: 3200 } },
|
|
// SGA / LGA
|
|
{ name: 'Fenton male 36w SGA 2000g', inputs: { sex: 'male', gaWeeks: 36, weightGrams: 2000 } },
|
|
{ name: 'Fenton female 36w LGA 3400g', inputs: { sex: 'female', gaWeeks: 36, weightGrams: 3400 } },
|
|
];
|
|
|
|
// ── Run + emit ─────────────────────────────────────────────
|
|
const vectors = {
|
|
generatedAt: new Date().toISOString(),
|
|
source: 'public/js/calculators.js',
|
|
bhutani: bhutaniCases.map((c) => ({ ...c, output: classifyBhutani(c.inputs.hours, c.inputs.tsb) })),
|
|
aap: aapCases.map((c) => ({ ...c, output: classifyAapBili(c.inputs.gaWeeks, c.inputs.hours, c.inputs.tsb, c.inputs.risk) })),
|
|
fenton: fentonCases.map((c) => ({ ...c, output: fentonWeightForAge(c.inputs.gaWeeks, c.inputs.weightGrams, c.inputs.sex) })),
|
|
};
|
|
|
|
const outPath = path.join(__dirname, '..', 'e2e', 'fixtures', 'calc-vectors.json');
|
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
fs.writeFileSync(outPath, JSON.stringify(vectors, null, 2));
|
|
console.log(`Wrote ${vectors.bhutani.length} Bhutani, ${vectors.aap.length} AAP, ${vectors.fenton.length} Fenton cases → ${outPath}`);
|