mirror of
https://github.com/gauvainboiche/ketapk.git
synced 2026-09-02 11:13:11 +02:00
218 lines
6.8 KiB
JavaScript
218 lines
6.8 KiB
JavaScript
/**
|
|
* ============================================================================
|
|
* KétaPK - Pharmacokinetic Engine (PK/PD)
|
|
* Models: Domino (1982), Clements (1981), Kamp (2020)
|
|
* ============================================================================
|
|
*/
|
|
|
|
/**
|
|
* Single application version (centralized)
|
|
*/
|
|
export const APP_VERSION = '0.19.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
|
|
}; |