/** * ============================================================================ * KétaPK - Moteur Pharmacocinétique (PK/PD) * Modèles : Domino (1982), Clements (1981), Kamp (2020) * ============================================================================ */ /** * Version unique de l'application (Centralisée) */ export const APP_VERSION = '0.16.0'; export const MODES = { ESKETAMINE: 'ESKETAMINE', RACEMIQUE: 'RACEMIQUE' }; // Modèles Multi-exponentiels 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' } }; // Zones d'effets cliniques et seuils (ng/mL) export const CLINICAL_THRESHOLDS = { [MODES.ESKETAMINE]: { maxScale: 500, defaultConcentration: 5, zones: [ { zoneKey: "inactive", min: 0, max: 10, color: "rgba(249, 252, 126, 0.25)", textHex: "#fef9c3" }, { zoneKey: "anti_hyperalgesic", min: 10, max: 50, color: "rgba(218, 247, 166, 0.25)", textHex: "#d9f99d" }, { zoneKey: "analgesia", min: 50, max: 150, color: "rgba(255, 195, 0, 0.25)", textHex: "#fef08a" }, { zoneKey: "psychedelic", min: 150, max: 350, color: "rgba(242, 160, 223, 0.25)", textHex: "#fbcfe8" }, { zoneKey: "emergence", min: 350, max: 500, color: "rgba(89, 196, 240, 0.25)", textHex: "#bae6fd" }, { zoneKey: "narcosis", min: 500, max: 1000, color: "rgba(43, 122, 244, 0.25)", textHex: "#ffffff" }, { zoneKey: "excessive", min: 1000, max: Infinity, color: "rgba(255, 87, 51, 0.25)", textHex: "#fecaca" } ] }, [MODES.RACEMIQUE]: { maxScale: 1000, defaultConcentration: 10, zones: [ { zoneKey: "inactive", min: 0, max: 20, color: "rgba(249, 252, 126, 0.25)", textHex: "#fef9c3" }, { zoneKey: "anti_hyperalgesic", min: 20, max: 100, color: "rgba(218, 247, 166, 0.25)", textHex: "#d9f99d" }, { zoneKey: "analgesia", min: 100, max: 300, color: "rgba(255, 195, 0, 0.25)", textHex: "#fef08a" }, { zoneKey: "psychedelic", min: 300, max: 700, color: "rgba(242, 160, 223, 0.25)", textHex: "#fbcfe8" }, { zoneKey: "emergence", min: 700, max: 1000, color: "rgba(89, 196, 240, 0.25)", textHex: "#bae6fd" }, { zoneKey: "narcosis", min: 1000, max: 2000, color: "rgba(43, 122, 244, 0.25)", textHex: "#ffffff" }, { zoneKey: "excessive", min: 2000, max: Infinity, color: "rgba(255, 87, 51, 0.25)", textHex: "#fecaca" } ] } }; // Unités de perfusion supportées 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 }; /** * Calcule la dose en mg administrée sur un intervalle de 5 minutes */ 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; } } /** * Calcul d'un bolus unique à l'instant t avec prise en compte de 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 par refDose pour respecter la formule exacte de l'Excel return (dosePerKg / refDose) * (termA + termB + termC); } /** * Propage la perfusion active sur la timeline (pas de 5 min) */ 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 }; }); } /** * Superposition Linéaire des Bolus et Perfusions sur 300 min (ng/mL arrondis à l'entier) */ export function generateSimulationData({ model, weightKg, timelineInputs, infusionUnit = INFUSION_UNITS.MG_KG_H }) { const processedTimeline = processTimelineEvents(timelineInputs, weightKg, infusionUnit); const cpResults = []; processedTimeline.forEach((targetEvent, targetIndex) => { let totalCp = 0; for (let i = 0; i <= targetIndex; i++) { const sourceEvent = processedTimeline[i]; const deltaTime = targetEvent.time - sourceEvent.time; if (sourceEvent.bolusMg > 0) { totalCp += calculateBolusCp(model, sourceEvent.bolusMg, weightKg, deltaTime); } if (sourceEvent.infusionMg5Min > 0) { totalCp += calculateBolusCp(model, sourceEvent.infusionMg5Min, weightKg, deltaTime); } } cpResults.push({ time: targetEvent.time, cp: Math.round(totalCp) }); }); return cpResults; } // Moteur de conversion de doses 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 };