Files
ketapk/frontend/js/pkCore.js
T

182 lines
6.0 KiB
JavaScript

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