104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
/**
|
|
* Pharmacokinetic Calculation Utilities
|
|
*
|
|
* Combines multiple dose profiles over time to create complete concentration
|
|
* curves. Handles steady-state calculations, dose accumulation, and empty
|
|
* value filtering for robust simulation.
|
|
*
|
|
* @author Andreas Weyer
|
|
* @license MIT
|
|
*/
|
|
|
|
import { timeToMinutes } from './timeUtils';
|
|
import { calculateSingleDoseConcentration } from './pharmacokinetics';
|
|
import type { DayGroup, SteadyStateConfig, PkParams, ConcentrationPoint } from '../constants/defaults';
|
|
|
|
interface ProcessedDose {
|
|
timeMinutes: number;
|
|
ldx: number;
|
|
damph: number;
|
|
isFed?: boolean; // Optional: indicates if dose was taken with food
|
|
}
|
|
|
|
export const calculateCombinedProfile = (
|
|
days: DayGroup[],
|
|
steadyStateConfig: SteadyStateConfig,
|
|
pkParams: PkParams
|
|
): ConcentrationPoint[] => {
|
|
const dataPoints: ConcentrationPoint[] = [];
|
|
const timeStepHours = 0.25;
|
|
const totalDays = days.length;
|
|
const totalHours = totalDays * 24;
|
|
|
|
// Use steadyStateDays from advanced settings (allows 0 for "first day" simulation)
|
|
const daysToSimulate = Math.min(
|
|
parseInt(pkParams.advanced.steadyStateDays, 10) || 0,
|
|
7 // cap at 7 days for performance
|
|
);
|
|
|
|
// Convert days to processed doses with absolute time
|
|
const allDoses: ProcessedDose[] = [];
|
|
|
|
// Add steady-state doses (days before simulation period)
|
|
// Use template day (first day) for steady state
|
|
const templateDay = days[0];
|
|
if (templateDay && daysToSimulate > 0) {
|
|
for (let steadyDay = -daysToSimulate; steadyDay < 0; steadyDay++) {
|
|
const dayOffsetMinutes = steadyDay * 24 * 60;
|
|
templateDay.doses.forEach(dose => {
|
|
const ldxNum = parseFloat(dose.ldx);
|
|
if (dose.time && !isNaN(ldxNum) && ldxNum > 0) {
|
|
allDoses.push({
|
|
timeMinutes: timeToMinutes(dose.time) + dayOffsetMinutes,
|
|
ldx: ldxNum,
|
|
damph: 0, // d-amph is calculated from LDX conversion, not administered directly
|
|
isFed: dose.isFed // Pass through per-dose food effect flag
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add doses from each day in sequence
|
|
days.forEach((day, dayIndex) => {
|
|
const dayOffsetMinutes = dayIndex * 24 * 60;
|
|
day.doses.forEach(dose => {
|
|
const ldxNum = parseFloat(dose.ldx);
|
|
if (dose.time && !isNaN(ldxNum) && ldxNum > 0) {
|
|
allDoses.push({
|
|
timeMinutes: timeToMinutes(dose.time) + dayOffsetMinutes,
|
|
ldx: ldxNum,
|
|
damph: 0, // d-amph is calculated from LDX conversion, not administered directly
|
|
isFed: dose.isFed // Pass through per-dose food effect flag
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Calculate concentrations at each time point
|
|
for (let t = 0; t <= totalHours; t += timeStepHours) {
|
|
let totalLdx = 0;
|
|
let totalDamph = 0;
|
|
|
|
allDoses.forEach(dose => {
|
|
const timeSinceDoseHours = t - dose.timeMinutes / 60;
|
|
|
|
if (timeSinceDoseHours >= 0) {
|
|
// Calculate LDX contribution with per-dose food effect
|
|
const ldxConcentrations = calculateSingleDoseConcentration(
|
|
String(dose.ldx),
|
|
timeSinceDoseHours,
|
|
pkParams,
|
|
dose.isFed // Pass per-dose food flag
|
|
);
|
|
totalLdx += ldxConcentrations.ldx;
|
|
totalDamph += ldxConcentrations.damph;
|
|
}
|
|
});
|
|
|
|
dataPoints.push({ timeHours: t, ldx: totalLdx, damph: totalDamph });
|
|
}
|
|
|
|
return dataPoints;
|
|
};
|