/** * ============================================================================ * KétaPK - Pharmacokinetic Engine (PK/PD) * Models: Domino (1982), Clements (1981), Kamp (2020) * ============================================================================ */ /** * Single application version (centralized) */ export const APP_VERSION = "1.0.0"; export const MODES = { ESKETAMINE: 'ESKETAMINE', RACEMIQUE: 'RACEMIQUE' }; // Multi-exponential models export const PK_MODELS = { DOMINO: { name: "Domino (1982)", refDose: 2.0, A: 14300, alpha: 1.35167, B: 2340, beta: 0.09517, C: 197, gamma: 0.00378, color: '#ff9933' }, CLEMENTS: { name: "Clements (1981)", refDose: 0.25, A: 108, alpha: 0.03940, B: 39, beta: 0.00380, C: 0, gamma: 0, color: '#6ec46c' }, KAMP: { name: "Kamp (2020)", refDose: 0.5, A: 1332, alpha: 0.24658, B: 155, beta: 0.03845, C: 69, gamma: 0.00409, color: '#4fafe3' } }; // Clinical effect zones and thresholds (ng/mL) export const CLINICAL_THRESHOLDS = { [MODES.ESKETAMINE]: { maxScale: 500, defaultConcentration: 5, zones: [ { zoneKey: "inactive", min: 0, max: 10, color: "#a0aa92", textHex: "#2a3110" }, { zoneKey: "anti_hyperalgesic", min: 10, max: 50, color: "#9eb062", textHex: "#243f11" }, { zoneKey: "analgesia", min: 50, max: 150, color: "#ae8b24", textHex: "#442900" }, { zoneKey: "psychedelic", min: 150, max: 350, color: "#9a4697", textHex: "#2e072a" }, { zoneKey: "emergence", min: 350, max: 500, color: "#368ea6", textHex: "#002734" }, { zoneKey: "narcosis", min: 500, max: 1000, color: "#086d9f", textHex: "#ffffff" }, { zoneKey: "excessive", min: 1000, max: Infinity, color: "#bb3737", textHex: "#ffffff" } ] }, [MODES.RACEMIQUE]: { maxScale: 1000, defaultConcentration: 10, zones: [ { zoneKey: "inactive", min: 0, max: 20, color: "#a0aa92", textHex: "#2a3110" }, { zoneKey: "anti_hyperalgesic", min: 20, max: 100, color: "#9eb062", textHex: "#243f11" }, { zoneKey: "analgesia", min: 100, max: 300, color: "#ae8b24", textHex: "#442900" }, { zoneKey: "psychedelic", min: 300, max: 700, color: "#9a4697", textHex: "#2e072a" }, { zoneKey: "emergence", min: 700, max: 1000, color: "#368ea6", textHex: "#002734" }, { zoneKey: "narcosis", min: 1000, max: 2000, color: "#086d9f", textHex: "#ffffff" }, { zoneKey: "excessive", min: 2000, max: Infinity, color: "#bb3737", textHex: "#ffffff" } ] } }; // Supported infusion units export const INFUSION_UNITS = { MG_KG_H: 'mg_kg_h', // mg/kg/h MCG_KG_MIN: 'mcg_kg_min',// µg/kg/min MG_H: 'mg_h' // mg/h }; /** * Calculates the dose in mg administered over a 5-minute interval */ export function calculate5MinInfusionDose(rate, unit, weight) { if (!rate || rate <= 0) return 0; switch (unit) { case INFUSION_UNITS.MG_KG_H: return (rate * weight) / 12; case INFUSION_UNITS.MCG_KG_MIN: return (rate * weight * 5) / 1000; case INFUSION_UNITS.MG_H: return rate / 12; default: return (rate * weight) / 12; } } /** * Calculation of a single bolus at time t, accounting for refDose */ export function calculateBolusCp(model, doseMg, weightKg, timeElapsedMin) { if (timeElapsedMin < 0 || weightKg <= 0 || !model.refDose) return 0; const dosePerKg = doseMg / weightKg; const { A, alpha, B, beta, C, gamma, refDose } = model; const termA = A * Math.exp(-alpha * timeElapsedMin); const termB = B * Math.exp(-beta * timeElapsedMin); const termC = C * (gamma ? Math.exp(-gamma * timeElapsedMin) : 0); // Division by refDose to match the exact Excel formula return (dosePerKg / refDose) * (termA + termB + termC); } /** * Propagates the active infusion across the timeline (5-min steps) */ export function processTimelineEvents(timelineInputs, weightKg, infusionUnit) { let currentRate = 0; return timelineInputs.map((item) => { const isExplicit = item.perfRate !== undefined && item.perfRate !== null && item.perfRate !== ''; if (isExplicit) { const parsed = parseFloat(item.perfRate); currentRate = isNaN(parsed) ? 0 : parsed; } const bolusMg = parseFloat(item.bolusMg) || 0; const infusionMg5Min = calculate5MinInfusionDose(currentRate, infusionUnit, weightKg); return { time: item.time, bolusMg: bolusMg, infusionRate: currentRate, isExplicit: isExplicit, infusionMg5Min: infusionMg5Min }; }); } /** * Converts the current infusion rate to mg/kg/min, regardless of the input unit */ function infusionRateToMgKgPerMin(rate, unit, weightKg) { if (!rate || rate <= 0) return 0; switch (unit) { case INFUSION_UNITS.MG_KG_H: return rate / 60; case INFUSION_UNITS.MCG_KG_MIN: return rate / 1000; case INFUSION_UNITS.MG_H: return weightKg > 0 ? (rate / weightKg) / 60 : 0; default: return rate / 60; } } /** * Exact resolution (discretized recursive method, cf. KétaPK formula) of boluses and * infusions (with variable rate) across the timeline, ng/mL rounded to the nearest integer. */ export function generateSimulationData({ model, weightKg, timelineInputs, infusionUnit = INFUSION_UNITS.MG_KG_H }) { const processedTimeline = processTimelineEvents(timelineInputs, weightKg, infusionUnit); const { A, alpha, B, beta, C, gamma, refDose } = model; const cpResults = []; if (weightKg <= 0 || !refDose) { return processedTimeline.map(event => ({ time: event.time, cp: 0 })); } let X1 = 0, X2 = 0, X3 = 0; processedTimeline.forEach((event, index) => { if (event.bolusMg > 0) { const scaledDose = (event.bolusMg / weightKg) / refDose; X1 += A * scaledDose; X2 += B * scaledDose; X3 += gamma ? C * scaledDose : 0; } cpResults.push({ time: event.time, cp: Math.round(X1 + X2 + X3) }); const nextEvent = processedTimeline[index + 1]; if (!nextEvent) return; const deltaT = nextEvent.time - event.time; // Excel applies the declared rate at the end of the interval (arrival column), not the starting one const Rn = infusionRateToMgKgPerMin(nextEvent.infusionRate, infusionUnit, weightKg) / refDose; const Falpha = Math.exp(-alpha * deltaT); const Fbeta = Math.exp(-beta * deltaT); X1 = Falpha * X1 + (A / alpha) * (1 - Falpha) * Rn; X2 = Fbeta * X2 + (B / beta) * (1 - Fbeta) * Rn; if (gamma) { const Fgamma = Math.exp(-gamma * deltaT); X3 = Fgamma * X3 + (C / gamma) * (1 - Fgamma) * Rn; } }); return cpResults; } // Dose conversion engine export const CalculatorEngine = { mgToMgKg: (mg, weight) => (weight > 0 ? mg / weight : 0), mgKgToMg: (mgKg, weight) => mgKg * weight, mgKgHToMicrogKgMin: (mgKgH) => (mgKgH * 1000) / 60, microgKgMinToMgKgH: (microg) => (microg * 60) / 1000 };