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