diff --git a/AMK/AMK_PlasmaODEs.m b/AMK/AMK_PlasmaODEs.m new file mode 100644 index 0000000..0444551 --- /dev/null +++ b/AMK/AMK_PlasmaODEs.m @@ -0,0 +1,104 @@ +%% ======================================================================= +% AMK_PlasmaODEs.m — Two-Compartment Plasma PK ODEs for Amikacin +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for a two-compartment PK model of amikacin +% with zero-order IV infusion input and first-order elimination from +% the central compartment. Intended to be called by an ODE solver +% (e.g. ode45) from AMK_PopPK. +% +% MODEL +% Reimplementation of Tod et al., 1998. +% Michel Tod et al. "Population pharmacokinetic study of amikacin administered +% once or twice daily to febrile, severly neutropenic adults". In: *Antimicrobial +% agents and chemotherapy* 42.4 (1998), pp. 849-856. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver. +% y - State vector of drug amounts (mg): +% y(1) = central compartment amount +% y(2) = peripheral compartment amount +% params - Parameter vector: +% params(1) = Vc/F central volume (L) +% params(2) = Vp/F peripheral volume (L) +% params(3) = CL/F clearance (L/h) +% params(4) = Q/F intercompartmental clearance (L/h) +% params(5) = dose dose for this interval (mg) +% params(6) = infusion duration (h) +% +% OUTPUTS +% dy - Derivative vector (mg/h): +% dy(1) = d(central)/dt +% dy(2) = d(peripheral)/dt +% +% AUTHORS +% Trevor Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: Meghna Nair, 2024-01-28 +% +% VERSION +% Created: 2024-01-28 +% Tested on: MATLAB R2026a +% ======================================================================== + +function dy = AMK_PlasmaODEs(t,y,params) + +% Extracting parameter values from params variable to assign to local variables + +% Departmental constants +V_Fs = [... + params(1) %(L) Central compartment Vc/F + params(2) %(L) Peripheral compartment Vp/F + ]; +rates = [... + params(3) % CL/F Clearance + params(4) % Q/F Intercompartmental Clearance + ]; +dose = params(5); % Dose for this interval (mg) +infusionDuration = params(6); % IV infusion duration (h) + +% Handle dosing duration + +if t < infusionDuration + RateIn = dose / infusionDuration; % (mg/h) zero-order IV input +else + RateIn = 0; % no input after infusion ends +end + +% ODE Y value assignments +A = [... + y(1) %Drug amount in central compartment (mg) + y(2) %Drug amount in peripheral compartment (mg) + ]; + +% Define ODEs + +% ODE around CENTRAL compartment +% ------------------------------------------ +% ODE_1 terms +dA1_term1 = RateIn; % +dA1_term2 = - rates(2) * A(1) / V_Fs(1); % Flow out to Peripheral: -Q * A1 / Vc +dA1_term3 = rates(2) * A(2) / V_Fs(2); % +Q * A2 / Vp +dA1_term4 = - rates(1) * A(1) / V_Fs(1); % Clearance from system: -CL * A1 / Vc + +dA1dt = dA1_term1 + dA1_term2 + dA1_term3 + dA1_term4; %(mg/h) + +% ODE around PERIPHERAL compartment +% ------------------------------------------ +% ODE_2 terms +dA2_term1 = rates(2) * A(1) / V_Fs(1); % +Q * A1 / Vc +dA2_term2 = - rates(2) * A(2) / V_Fs(2); % -Q * A2 / Vp + +dA2dt = dA2_term1 + dA2_term2; %(mg/h) + +% ODE Outputs + +dy = [... + dA1dt + dA2dt + ]; + +end \ No newline at end of file diff --git a/AMK/AMK_PopPK.m b/AMK/AMK_PopPK.m new file mode 100644 index 0000000..25f7f5d --- /dev/null +++ b/AMK/AMK_PopPK.m @@ -0,0 +1,407 @@ +%% ======================================================================= +% AMK_PopPK.m — Population PK Simulation & PTA Analysis for Amikacin +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the amikacin plasma ODEs across a Monte Carlo patient +% population. Models a two-compartment system with zero-order IV infusion +% input, computes steady-state AUC/Cmax, and generates time-course, AUC, +% and probability of target attainment (PTA) plots. +% +% MODEL +% Reimplementation of Tod et al., 1998. +% Michel Tod et al. "Population pharmacokinetic study of amikacin administered +% once or twice daily to febrile, severly neutropenic adults". In: *Antimicrobial +% agents and chemotherapy* 42.4 (1998), pp. 849-856. +% Parameter values are based on a typical individual of 52 kg. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (780) Dose per administration (mg). +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (24) Time between doses (h). +% 'IVDuration' (3) Duration of IV infusion (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: Meghna Nair, 2024-01-17; +% Tyler Dierckman, 2025-01-22. +% +% VERSION +% Created: 2024-01-17 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% AMK_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% ======================================================================== + +function [plotArgs] = AMK_PopPK(varargin) + + % -------------------- Parse options -------------------- + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 780, @(x)isnumeric(x)); + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 24, @(x)isnumeric(x)); + + p.addParameter('IVDuration', 3, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Volume and Bioavailability (F) Parameters + % NOTE: These are based on a typical individual of 50 kg + % ------------------------------------------ + V_F_AMK = [... % Volume Parameters + 8.92 % (L) Vc/F + 11.4 % (L) Vp_F + ]; + + % Rate Constants + % ------------------------------------------ + rates_AMK = [... %This encompasses all rate constants + 3.6 %(L/h) CL/F Overall clearance + 4.43 %(L/h) Q/F apparent intercompartmental clearance + ]; + + % Interindividual variability (exponential error model) + % ------------------------------------------ + IIV_IOV_AMK = [... + 0.21 %CL/F (CV) IIV + 0.30 %Q/F (CV) IIV + 0.15 %Vc (%CV) IIV + 0.25 %Vp (%CV) IIV + ]; + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify how much drug is given in each dose (mg) + dose_size_AMK = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_AMK = opt.NumDoses; + + % Specify how much time (hours) between doses (hours) + dose_time_AMK = opt.DoseInterval; + + % Specify the IV dose duration (hours) + IV_dose_duration_AMK = opt.IVDuration; + + % Specify the interval between time points (hours) + time_step_size_AMK = 0.5; + + % Time vector creation + time_vec_AMK = 0:time_step_size_AMK:dose_time_AMK; + total_timpts_AMK = n_doses_AMK*(length(time_vec_AMK)-1); + total_params_AMK = 6; + + % Initialize matrices + param_store_AMK = zeros([total_params_AMK n_pts]); + AMK_conc_c = zeros([total_timpts_AMK n_pts]); + AMK_conc_p = zeros([total_timpts_AMK n_pts]); + time_vec_local_AMK = 0:time_step_size_AMK:dose_time_AMK*n_doses_AMK-time_step_size_AMK; + + % -------------------- ODE Solving -------------------- + + % Iterate over patients + for ipt = 1:n_pts + + % Sample patient-specific parameters using IIV information + % -------------------------------------------------------- + V_F_pt_AMK = [... + V_F_AMK(1)* exp(random('Normal',0,sqrt(log(IIV_IOV_AMK(3)^2+1)))); + V_F_AMK(2)* exp(random('Normal',0,sqrt(log(IIV_IOV_AMK(4)^2+1)))) + ]; + + % Rate constants for patient + rates_pt_AMK = [... + rates_AMK(1) * exp(random('Normal',0,sqrt(log(IIV_IOV_AMK(1)^2+1)))); + rates_AMK(2) * exp(random('Normal',0,sqrt(log(IIV_IOV_AMK(2)^2+1)))) + ]; + + % Set Initial conditions for this patient + % --------------------------------------- + AMK_c_IC = 0; %Initial concentration in central + AMK_p_IC = 0; %Initial concentration in peripheral + + % Initialize vector to store all doses for this patient + AMK_conc_c_loc = zeros([total_timpts_AMK 1]); + AMK_conc_p_loc = zeros([total_timpts_AMK 1]); + + for idose = 1:n_doses_AMK + + % Compile parameters from patients into vector to pass to ODE solver + % ------------------------------------------------------------------ + params_AMK = [... %This is setup using the same order as the ODE solver + V_F_pt_AMK(1) %Vc_F params(1) + V_F_pt_AMK(2) %Vp_F params(2) + rates_pt_AMK(1) %CL_F params(3) + rates_pt_AMK(2) %Q/F params(4) + dose_size_AMK %dose size params(5) + IV_dose_duration_AMK %IV dose duration params(6) + ]; + + % Call ODE Solver for regular 2 compartment + %--------------- + [~, AMK_sol] = ode45(@(t,y) AMK_PlasmaODEs(t,y,params_AMK), ... + time_vec_AMK, [AMK_c_IC AMK_p_IC]); + + + % Organization of data post ODE + % ----------------------------- + % Concatenate Solutions to end of total time course + % Skip first entry bc it is already included + dloop_inc_start_AMK = (idose-1)*(length(time_vec_AMK)-1)+1; + dloop_inc_end_AMK = (idose*(length(time_vec_AMK)-1)); + + AMK_conc_c_loc(dloop_inc_start_AMK:dloop_inc_end_AMK,1) = AMK_sol(1:end-1,1)/V_F_pt_AMK(1); + AMK_conc_p_loc(dloop_inc_start_AMK:dloop_inc_end_AMK,1) = AMK_sol(1:end-1,2)/V_F_pt_AMK(2); + + % Update initial conditions for the next dose + AMK_c_IC = AMK_sol(end,1); + AMK_p_IC = AMK_sol(end,2); + + end + + % Combine data for all patients + % ----------------------------- + % Parameters + total_params_inc_AMK = 1 : total_params_AMK; + param_store_AMK(total_params_inc_AMK,ipt) = params_AMK; + total_time_inc_AMK = 1 : total_timpts_AMK; + + % Concentrations + AMK_conc_c(total_time_inc_AMK , ipt) = AMK_conc_c_loc; + AMK_conc_p(total_time_inc_AMK , ipt) = AMK_conc_p_loc; + + end + + % -------------------- Plotting Raw TimeCourses -------------------- + if opt.TimeSeriesPlots + + cycles_AMK = n_doses_AMK; + xvalues_AMK = time_vec_local_AMK(1:(dose_time_AMK * cycles_AMK)/time_step_size_AMK); + + % Central Compartment + plotTimeCourses( ... + 'X', xvalues_AMK, ... + 'Y', AMK_conc_c, ... + 'DrugName', 'AMK', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_AMK, ... + 'DoseInterval', dose_time_AMK, ... + 'FigureName', 'AMK Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + % Peripheral Compartment + plotTimeCourses( ... + 'X', xvalues_AMK, ... + 'Y', AMK_conc_p, ... + 'DrugName', 'AMK', ... + 'Compartment', 'Peripheral', ... + 'Cycles', cycles_AMK, ... + 'DoseInterval', dose_time_AMK, ... + 'FigureName', 'AMK Raw Timecourses (Peripheral)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + if opt.StatisticsPlots + + cycles_stats_AMK = n_doses_AMK; + xvalues_cyc_stats_AMK = time_vec_local_AMK(1:(dose_time_AMK * cycles_stats_AMK)/time_step_size_AMK); + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_AMK, ... + 'Y', AMK_conc_c, ... + 'DrugName', 'AMK', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_stats_AMK, ... + 'DoseInterval', dose_time_AMK, ... + 'FigureName', 'AMK Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_AMK, ... + 'Y', AMK_conc_p, ... + 'DrugName', 'AMK', ... + 'Compartment', 'Peripheral', ... + 'Cycles', cycles_stats_AMK, ... + 'DoseInterval', dose_time_AMK, ... + 'FigureName', 'AMK Stats (Peripheral)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_AMK, ... + 'time_step_size', time_step_size_AMK, ... + 'conc_c', AMK_conc_c, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'AMK steady-state attainment', 'SortBy', 'name'); + + statsT = table( ... + mean(last24AUC,'omitnan'), std(last24AUC,'omitnan'), ... + mean(last24Cmax,'omitnan'), std(last24Cmax,'omitnan'), ... + 'VariableNames', {'mean_last24AUC','std_last24AUC','mean_last24Cmax','std_last24Cmax'}); + + fprintf('\n=== AMK last-24h summary stats ===\n'); + disp(statsT); + end + + if opt.plotAUC + plotAUC(last24AUC,'AMK'); + end + + if opt.PTAplot + + % -------------------- MIC dist NTM -------------------- + % This section includes the minimum inhibitory concentration (MIC) + % distributions for the non-tuberculosis mycobacterium strains belonging + % to associated non-tuberculosis mycobacterium species + + MIC_MAB_AMK = { + 'M. abscessus'; + {{'<=1', 3}, {'2', 8}, {'4', 74}, {'8', 366}, {'16', 387}, {'32', 83}, {'64', 32}, {'>=128', 61}}; + 'TimeAboveMIC'; + 1014 + }; + + MIC_MAC_AMK = { + 'M. avium complex'; + {{'<=1', 24}, {'2', 46}, {'4', 259}, {'8', 1166}, {'16', 2144}, {'32', 1029}, {'64', 277}, {'>=128', 61}}; + 'TimeAboveMIC'; + 6817 + }; + + MIC_kan_AMK = { + 'M. kansasii'; + {{'<=1', 10}, {'2', 6}, {'4', 3}, {'8', 1}, {'16', 0}, {'32', 0}, {'64', 1}, {'>=128', 0}}; + 'TimeAboveMIC'; + 21 + }; + + MIC_TB_AMK = { + 'M. tuberculosis'; + {{'<=1', 6}, {'2', 48}, {'4', 2}, {'8', 0}, {'16', 0}, {'32', 0}, {'64', 0}, {'>=128', 1}}; + 'TimeAboveMIC'; + 57 + }; + + speciesAggregation = {MIC_MAB_AMK MIC_MAC_AMK MIC_kan_AMK MIC_TB_AMK}; + normalizedMICSpeciesAggregation = normalizeMICsOfAggregation(speciesAggregation); + uniqueMICs = getUniqueMICs(normalizedMICSpeciesAggregation); + + % -------------------- Setting PTA Targets -------------------- + + target1 = { % https://doi.org/10.3389/fphar.2022.1063453 + 'TB'; + 'Cmax/MIC'; + 10.13; 'tuberculosis' + }; + + target2 = { %rule of thumb Cmax/MIC > 8 or > 12 https://doi.org/10.1007/s10096-004-1109-5 + ''; + 'Cmax/MIC'; + 8; + ''}; + + target3 = { %rule of thumb Cmax/MIC > 8 or > 12 https://doi.org/10.1007/s10096-004-1109-5 + ''; + 'Cmax/MIC'; + 12; + ''}; + + target4 = { % https://doi.org/10.1128/spectrum.03222-23 + 'M. abscessus'; + '%T>MIC'; + 40; + 'abscessus'}; + + uniqueTargets = {target2 target1 target3 target4}; + + % -------------------- PTA Plotting -------------------- + + PTAMatrix = zeros(length(uniqueMICs), length(uniqueTargets)); + + for i = 1:length(uniqueTargets) + % For passing target type and target in, the i argument is for the index of target, 2 is to pass + % in target type, and 3 is to pass in target value + PTAMatrix(:,i) = calculatePTA( ... + last24ConcArray, ... + 'MICDist', uniqueMICs, ... + 'targetType', uniqueTargets{i}{2}, ... + 'target', uniqueTargets{i}{3}, ... + 'time_step_size', time_step_size_AMK, ... + 'last24AUC', last24AUC, ... + 'last24Cmax', last24Cmax); + end + + legendArray = getLegendArray(uniqueTargets, normalizedMICSpeciesAggregation); + + [~, plotArgs] = plotPTAs(uniqueMICs, PTAMatrix, normalizedMICSpeciesAggregation, ... + 'DoseSize', dose_size_AMK, ... + 'DoseFrequency', dose_time_AMK, ... + 'DrugName', 'AMK', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray, ... % species + PTA labels + 'UniqueTargets', uniqueTargets, ... % for footnote + 'NumPatients', n_pts, ... + 'ShowCI', false); + + end + +end \ No newline at end of file diff --git a/BDQ/BDQ_PlasmaODEs.m b/BDQ/BDQ_PlasmaODEs.m new file mode 100644 index 0000000..e58313a --- /dev/null +++ b/BDQ/BDQ_PlasmaODEs.m @@ -0,0 +1,167 @@ +%% ======================================================================= +% BDQ_PlasmaODEs.m — Transit-Absorption / Three-Compartment ODEs for +% Bedaquiline +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the bedaquiline plasma PK model: a depot +% feeding a chain of transit compartments into a central compartment with +% two peripheral compartments and first-order elimination. Intended to be +% called by a stiff ODE solver (ode15s) from BDQ_PopPK. +% +% MODEL +% ODEs as described in Lyons, 2022. +% Michael A. Lyons. "Pharmacodynamics and Bactericidal Activity of +% Bedaquiline in Pulmonary Tuberculosis". In: *Antimicrobial Agents +% and Chemotherapy* 66.2 (2022). DOI: 10.1128/aac.01636-21. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver. +% y - State vector (10 x 1): +% y(1) = depot drug mass (q0) +% y(2) = transit 1 mass (q1) +% y(3) = transit 2 mass (q2) +% y(4) = transit 3 mass (q3) +% y(5) = transit 4 mass (q4) +% y(6) = transit 5 mass (q5) +% y(7) = final transit/absorption mass (q) +% y(8) = central concentration (C) +% y(9) = peripheral 2 concentration (C_2) +% y(10) = peripheral 3 concentration (C_3) +% params - Parameter vector: +% params(1) = V central volume (L) +% params(2) = V2 peripheral 2 volume (L) +% params(3) = V3 peripheral 3 volume (L) +% params(4) = CL clearance (L/h) +% params(5) = Q2 intercompartmental clearance, per 2 (L/h) +% params(6) = Q3 intercompartmental clearance, per 3 (L/h) +% params(7) = ka absorption rate constant (1/h) +% params(8) = ktr transit rate constant (1/h) +% +% OUTPUTS +% dy - Derivative vector (10 x 1) matching the state ordering in y. +% +% NOTES +% params(9:10) (dose sizes) are passed by BDQ_PopPK but are not used +% inside the ODEs; dosing is applied via initial conditions between +% intervals. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2024-02-26 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = BDQ_PlasmaODEs(t,y,params) %#ok + +% Extracting parameter values from params variable to assign to local variables + +% Volumes +V = params(1); %L +V2 = params(2); %L +V3 = params(3); %L + +% Rates +CL = params(4); %L/h +Q2 = params(5); %L/h +Q3 = params(6); %L/h + +ka = params(7); %1/h +ktr = params(8); %1/h + + + +% ODE y value assignmetns +A = [... + y(1) %(q0) drug mass in depot + y(2) %(q1) drug mass in transit 1 compartment + y(3) %(q2) drug mass in transit 2 compartment + y(4) %(q3) drug mass in transit 3 compartment + y(5) %(q4) drug mass in transit 4 compartment + y(6) %(q5) drug mass in transit 5 compartment + y(7) %(q) drug mass in final transit/absorption into central compartment + y(8) %(C) drug concentration in central + y(9) %(C_2) drug concentration in peripheral2 compartment + y(10) %(C_3) drug concentration in peripheral3 compartment + ]; + +% Define ODEs +% ODEs around A_dep (depot) +% --------------------- +dA_depdt = -ktr * A(1); +% dq0/dt = -ktr * q0 + +% ODEs around A_tr1 (transit 1) +% --------------------- +dA_tr1dt = ktr * (A(1) - A(2)); +%dqi/dt = ktr (q_{i-1} - q_i) || dq1/dt = ktr(q_0 - q_1) + +% ODEs around A_tr2 (transit 2) +% --------------------- +dA_tr2dt = ktr * (A(2) - A(3)); +%dqi/dt = ktr (q_{i-1} - q_i) || dq2/dt = ktr(q_1 - q_2) + +% ODEs around A_tr3 (transit 3) +% --------------------- +dA_tr3dt = ktr * (A(3) - A(4)); +%dqi/dt = ktr (q_{i-1} - q_i) || dq3/dt = ktr(q_2 - q_3) + +% ODEs around A_tr4 (transit 4) +% --------------------- +dA_tr4dt = ktr * (A(4) - A(5)); +%dqi/dt = ktr (q_{i-1} - q_i) || dq4/dt = ktr(q_3 - q_4) + +% ODEs around A_tr5 (transit 5) +% --------------------- +dA_tr5dt = ktr * (A(5) - A(6)); +%dqi/dt = ktr (q_{i-1} - q_i) || dq5/dt = ktr(q_4 - q_5) + +% ODEs around A_q (final transit/absorption into central) +% --------------------- +dA_qdt = -ka * A(7) + ktr * A(6); +%dq/dt = -ka * q + ktr * q_5 + +% ODEs around A_cen (central) +% --------------------- +dA_cen_term1 = ka * A(7); +% k_a * q +dA_cen_term2 = - Q2 * (A(8) - A(9)); +% - Q_2 (C - C_2) +dA_cen_term3 = - Q3 * (A(8) - A(10)); +% - Q_3 (C - C_3) +dA_cen_term4 = - CL * A(8); +% - CL * C + +dA_cendt = (1/V) * (dA_cen_term1 + dA_cen_term2 + dA_cen_term3 + dA_cen_term4); + +% ODEs around A_per2 (peripheral 2) +% --------------------- +dA_per2dt = (1/V2) * (Q2 * (A(8) - A(9))); +% Q_2 (C - C2) + +% ODEs around A_per3 (peripheral 3) +% --------------------- +dA_per3dt = (1/V3) * (Q3 * (A(8) - A(10))); +% Q_3 (C - C3) + +% ODE Outputs + +dy = [... + dA_depdt + dA_tr1dt + dA_tr2dt + dA_tr3dt + dA_tr4dt + dA_tr5dt + dA_qdt + dA_cendt + dA_per2dt + dA_per3dt + ]; + +end \ No newline at end of file diff --git a/BDQ/BDQ_PopPK.m b/BDQ/BDQ_PopPK.m new file mode 100644 index 0000000..cd2511b --- /dev/null +++ b/BDQ/BDQ_PopPK.m @@ -0,0 +1,678 @@ +%% ======================================================================= +% BDQ_PopPK.m — Population PK Simulation & PTA Analysis for Bedaquiline +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the bedaquiline plasma ODEs across a Monte Carlo patient +% population. Supports a two-regimen dosing schedule (loading dose +% followed by a maintenance dose after a switch point), computes +% steady-state AUC/Cmax, and generates time-course, AUC, and probability +% of target attainment (PTA) plots. +% +% A serial path and a parfor (parallelized) path are provided; select via +% the 'Parallelize' option. Both paths are functionally equivalent. +% +% MODEL +% Reimplementation of Lyons, 2022. +% Michael A. Lyons. "Pharmacodynamics and Bactericidal Activity of +% Bedaquiline in Pulmonary Tuberculosis". In: *Antimicrobial Agents +% and Chemotherapy* 66.2 (2022). DOI: 10.1128/aac.01636-21. +% +% STRUCTURE +% 10-state model: depot, 5 transit compartments, 1 absorption/transit +% compartment, central, and 2 peripheral compartments. Solved with ode15s +% (stiff) and a non-negativity constraint on all states. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (780) [loading maintenance] dose sizes (mg). +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (24) [reg1 reg2] dose intervals (h). +% 'SwitchDose' (14) Dose number at the loading→maintenance switch. +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% 'Parallelize' (false) Use parfor over patients. +% +% OUTPUTS +% results - Struct of simulation results. +% parameters - 10 x NumPatients matrix of sampled patient parameters. +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2024-02-26 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% BDQ_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, getColor, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% Parallel Computing Toolbox required when 'Parallelize' is true. +% ======================================================================== + +function [results, parameters, plotArgs] = BDQ_PopPK(varargin) + + % Initialize structures + results = struct(); + parameters = struct(); %#ok + + % Set the ODE solver to not allow negative numbers + neq = 10; + options = odeset('NonNegative', 1:neq); + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 780, @(x)isnumeric(x)); + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 24, @(x)isnumeric(x)); + + p.addParameter('SwitchDose', 14, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.addParameter('Parallelize', false, @(b)islogical(b)&&isscalar(b)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Volume parameters + % ------------------------------------------ + V_BDQ = 120; % (L) Central compartment volume + V2_BDQ = 79; % (L) Peripheral compartment 2 volume + V3_BDQ = 750; % (L) Peripheral compartment 3 volume + + % Clearance constants + % ------------------------------------------ + CL_BDQ = 6.7; % (L/h) Clearance from central + Q2_BDQ = 6.8; % (L/h) Clearance between peripheral 2 and central + Q3_BDQ = 7.3; % (L/h) Clearance between peripheral 3 and central + + % Rate constants + % ------------------------------------------ + k_a_BDQ = 0.9; % (1/h) First-order absorption + k_tr_BDQ = 2.6; % (1/h) Transit rate constant + + % Interindividual variability (% CV) + % ------------------------------------------ + CV_BDQ = [... + 0.49 % k_tr (CV(1)) + 0.29 % k_a (CV(2)) + 0.51 % V (CV(3)) + 0.27 % V_2 (CV(4)) + 0.45 % V_3 (CV(5)) + 0.26 % Q_2 (CV(6)) + 0.59 % Q_3 (CV(7)) + 0.30 % CL (CV(8)) + ]; + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify dose sizes (mg) + dose_size_BDQ_daily = opt.DoseSize(1); + dose_size_BDQ = opt.DoseSize(2); + + % Specify number of doses in each regimen + n_doses_BDQ_b4Switch = opt.SwitchDose - 1; + n_doses_BDQ_afterSwitch = opt.NumDoses - n_doses_BDQ_b4Switch; + + % Specify dose intervals (hours) + dose_time_BDQ_daily = opt.DoseInterval(1); + dose_time_BDQ = opt.DoseInterval(2); + + doseSwitch = opt.SwitchDose; + numDoses = opt.NumDoses; + + % Specify time step and build time vectors + time_step_size_BDQ = 0.5; + + time_vec_reg1_BDQ = 0 : time_step_size_BDQ : dose_time_BDQ_daily; + time_vec_reg2_BDQ = 0 : time_step_size_BDQ : dose_time_BDQ; + + total_timpts_BDQ = (n_doses_BDQ_b4Switch) * (length(time_vec_reg1_BDQ) - 1) + ... + n_doses_BDQ_afterSwitch * (length(time_vec_reg2_BDQ) - 1); + + % Local time vectors for plotting + time_vec_reg1_BDQ_local = 0 : time_step_size_BDQ : ... + dose_time_BDQ_daily * n_doses_BDQ_b4Switch - time_step_size_BDQ; + + time_vec_reg2_BDQ_local = time_vec_reg1_BDQ_local(end) + time_step_size_BDQ : ... + time_step_size_BDQ : ... + (dose_time_BDQ * n_doses_BDQ_afterSwitch) + (dose_time_BDQ_daily * n_doses_BDQ_b4Switch) - time_step_size_BDQ; + + time_vec_local_BDQ = [time_vec_reg1_BDQ_local, time_vec_reg2_BDQ_local]; + + + % -------------------- Initialize Matrices -------------------- + + BDQ_conc_dep = zeros([total_timpts_BDQ n_pts]); + BDQ_conc_tr1 = zeros([total_timpts_BDQ n_pts]); + BDQ_conc_tr2 = zeros([total_timpts_BDQ n_pts]); + BDQ_conc_tr3 = zeros([total_timpts_BDQ n_pts]); + BDQ_conc_tr4 = zeros([total_timpts_BDQ n_pts]); + BDQ_conc_tr5 = zeros([total_timpts_BDQ n_pts]); + BDQ_conc_q = zeros([total_timpts_BDQ n_pts]); + BDQ_conc_cen = zeros([total_timpts_BDQ n_pts]); + BDQ_conc_per2 = zeros([total_timpts_BDQ n_pts]); + BDQ_conc_per3 = zeros([total_timpts_BDQ n_pts]); + + params_store_BDQ = zeros([10 n_pts]); + + + % -------------------- ODE Solving -------------------- + + % Serial path (default) + if ~opt.Parallelize + + for ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + V_ipt = V_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(3)^2+1)))); + V2_ipt = V2_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(4)^2+1)))); + V3_ipt = V3_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(5)^2+1)))); + CL_ipt = CL_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(8)^2+1)))); + Q2_ipt = Q2_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(6)^2+1)))); + Q3_ipt = Q3_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(7)^2+1)))); + ka_ipt = k_a_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(2)^2+1)))); + ktr_ipt = k_tr_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(1)^2+1)))); + + p_ipt = [V_ipt; V2_ipt; V3_ipt; CL_ipt; Q2_ipt; Q3_ipt; ... + ka_ipt; ktr_ipt; dose_size_BDQ_daily; dose_size_BDQ]; + + params_store_BDQ(:,ipt) = p_ipt; + + % Set initial conditions + % -------------------------------------------------------- + dep_IC = dose_size_BDQ_daily; + tr1_IC = 0; tr2_IC = 0; tr3_IC = 0; tr4_IC = 0; tr5_IC = 0; + q_IC = 0; cen_IC = 0; per2_IC = 0; per3_IC = 0; + + % Initialize local storage + dep_loc = zeros([total_timpts_BDQ 1]); + tr1_loc = zeros([total_timpts_BDQ 1]); + tr2_loc = zeros([total_timpts_BDQ 1]); + tr3_loc = zeros([total_timpts_BDQ 1]); + tr4_loc = zeros([total_timpts_BDQ 1]); + tr5_loc = zeros([total_timpts_BDQ 1]); + q_loc = zeros([total_timpts_BDQ 1]); + cen_loc = zeros([total_timpts_BDQ 1]); + per2_loc = zeros([total_timpts_BDQ 1]); + per3_loc = zeros([total_timpts_BDQ 1]); + + % Regimen 1: pre-switch doses + for idose = 1:n_doses_BDQ_b4Switch + + [~, sol] = ode15s(@(t,y) BDQ_PlasmaODEs(t,y,p_ipt), ... + time_vec_reg1_BDQ, ... + [dep_IC tr1_IC tr2_IC tr3_IC tr4_IC tr5_IC q_IC cen_IC per2_IC per3_IC], ... + options); + + i0 = (idose-1)*(length(time_vec_reg1_BDQ)-1)+1; + i1 = idose *(length(time_vec_reg1_BDQ)-1); + + dep_loc(i0:i1) = sol(1:end-1,1); + tr1_loc(i0:i1) = sol(1:end-1,2); + tr2_loc(i0:i1) = sol(1:end-1,3); + tr3_loc(i0:i1) = sol(1:end-1,4); + tr4_loc(i0:i1) = sol(1:end-1,5); + tr5_loc(i0:i1) = sol(1:end-1,6); + q_loc(i0:i1) = sol(1:end-1,7); + cen_loc(i0:i1) = sol(1:end-1,8); + per2_loc(i0:i1) = sol(1:end-1,9); + per3_loc(i0:i1) = sol(1:end-1,10); + + nextDose = dose_size_BDQ_daily; + if idose >= doseSwitch + nextDose = dose_size_BDQ; + end + + dep_IC = sol(end,1) + nextDose; + tr1_IC = sol(end,2); tr2_IC = sol(end,3); tr3_IC = sol(end,4); + tr4_IC = sol(end,5); tr5_IC = sol(end,6); q_IC = sol(end,7); + cen_IC = sol(end,8); per2_IC = sol(end,9); per3_IC = sol(end,10); + + end + + % Regimen 2: post-switch doses + for idose = n_doses_BDQ_b4Switch+1:numDoses + + [~, sol] = ode15s(@(t,y) BDQ_PlasmaODEs(t,y,p_ipt), ... + time_vec_reg2_BDQ, ... + [dep_IC tr1_IC tr2_IC tr3_IC tr4_IC tr5_IC q_IC cen_IC per2_IC per3_IC], ... + options); + + i0 = (n_doses_BDQ_b4Switch * (length(time_vec_reg1_BDQ)-1)) + ... + (idose - n_doses_BDQ_b4Switch - 1) * (length(time_vec_reg2_BDQ)-1) + 1; + i1 = (n_doses_BDQ_b4Switch * (length(time_vec_reg1_BDQ)-1)) + ... + (idose - n_doses_BDQ_b4Switch) * (length(time_vec_reg2_BDQ)-1); + + dep_loc(i0:i1) = sol(1:end-1,1); + tr1_loc(i0:i1) = sol(1:end-1,2); + tr2_loc(i0:i1) = sol(1:end-1,3); + tr3_loc(i0:i1) = sol(1:end-1,4); + tr4_loc(i0:i1) = sol(1:end-1,5); + tr5_loc(i0:i1) = sol(1:end-1,6); + q_loc(i0:i1) = sol(1:end-1,7); + cen_loc(i0:i1) = sol(1:end-1,8); + per2_loc(i0:i1) = sol(1:end-1,9); + per3_loc(i0:i1) = sol(1:end-1,10); + + dep_IC = sol(end,1) + dose_size_BDQ; + tr1_IC = sol(end,2); tr2_IC = sol(end,3); tr3_IC = sol(end,4); + tr4_IC = sol(end,5); tr5_IC = sol(end,6); q_IC = sol(end,7); + cen_IC = sol(end,8); per2_IC = sol(end,9); per3_IC = sol(end,10); + + end + + % Store patient results + BDQ_conc_dep( :,ipt) = dep_loc; + BDQ_conc_tr1( :,ipt) = tr1_loc; + BDQ_conc_tr2( :,ipt) = tr2_loc; + BDQ_conc_tr3( :,ipt) = tr3_loc; + BDQ_conc_tr4( :,ipt) = tr4_loc; + BDQ_conc_tr5( :,ipt) = tr5_loc; + BDQ_conc_q( :,ipt) = q_loc; + BDQ_conc_cen( :,ipt) = cen_loc; + BDQ_conc_per2(:,ipt) = per2_loc; + BDQ_conc_per3(:,ipt) = per3_loc; + + end + + else + + parfor ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + V_ipt = V_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(3)^2+1)))); %#ok + V2_ipt = V2_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(4)^2+1)))); %#ok + V3_ipt = V3_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(5)^2+1)))); %#ok + CL_ipt = CL_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(8)^2+1)))); %#ok + Q2_ipt = Q2_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(6)^2+1)))); %#ok + Q3_ipt = Q3_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(7)^2+1)))); %#ok + ka_ipt = k_a_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(2)^2+1)))); %#ok + ktr_ipt = k_tr_BDQ * exp(random('Normal',0,sqrt(log(CV_BDQ(1)^2+1)))); %#ok + + p_ipt = [V_ipt; V2_ipt; V3_ipt; CL_ipt; Q2_ipt; Q3_ipt; ... + ka_ipt; ktr_ipt; dose_size_BDQ_daily; dose_size_BDQ]; + + params_store_BDQ(:,ipt) = p_ipt; %#ok + + % Set initial conditions + % -------------------------------------------------------- + dep_IC = dose_size_BDQ_daily; + tr1_IC = 0; tr2_IC = 0; tr3_IC = 0; tr4_IC = 0; tr5_IC = 0; + q_IC = 0; cen_IC = 0; per2_IC = 0; per3_IC = 0; + + % Initialize local storage + dep_loc = zeros([total_timpts_BDQ 1]); + tr1_loc = zeros([total_timpts_BDQ 1]); + tr2_loc = zeros([total_timpts_BDQ 1]); + tr3_loc = zeros([total_timpts_BDQ 1]); + tr4_loc = zeros([total_timpts_BDQ 1]); + tr5_loc = zeros([total_timpts_BDQ 1]); + q_loc = zeros([total_timpts_BDQ 1]); + cen_loc = zeros([total_timpts_BDQ 1]); + per2_loc = zeros([total_timpts_BDQ 1]); + per3_loc = zeros([total_timpts_BDQ 1]); + + % Regimen 1: pre-switch doses + for idose = 1:n_doses_BDQ_b4Switch + + [~, sol] = ode15s(@(t,y) BDQ_PlasmaODEs(t,y,p_ipt), ... + time_vec_reg1_BDQ, ... + [dep_IC tr1_IC tr2_IC tr3_IC tr4_IC tr5_IC q_IC cen_IC per2_IC per3_IC], ... + options); + + i0 = (idose-1)*(length(time_vec_reg1_BDQ)-1)+1; + i1 = idose *(length(time_vec_reg1_BDQ)-1); + + dep_loc(i0:i1) = sol(1:end-1,1); + tr1_loc(i0:i1) = sol(1:end-1,2); + tr2_loc(i0:i1) = sol(1:end-1,3); + tr3_loc(i0:i1) = sol(1:end-1,4); + tr4_loc(i0:i1) = sol(1:end-1,5); + tr5_loc(i0:i1) = sol(1:end-1,6); + q_loc(i0:i1) = sol(1:end-1,7); + cen_loc(i0:i1) = sol(1:end-1,8); + per2_loc(i0:i1) = sol(1:end-1,9); + per3_loc(i0:i1) = sol(1:end-1,10); + + nextDose = dose_size_BDQ_daily; + if idose >= doseSwitch + nextDose = dose_size_BDQ; + end + + dep_IC = sol(end,1) + nextDose; + tr1_IC = sol(end,2); tr2_IC = sol(end,3); tr3_IC = sol(end,4); + tr4_IC = sol(end,5); tr5_IC = sol(end,6); q_IC = sol(end,7); + cen_IC = sol(end,8); per2_IC = sol(end,9); per3_IC = sol(end,10); + + end + + % Regimen 2: post-switch doses + for idose = n_doses_BDQ_b4Switch+1:numDoses + + [~, sol] = ode15s(@(t,y) BDQ_PlasmaODEs(t,y,p_ipt), ... + time_vec_reg2_BDQ, ... + [dep_IC tr1_IC tr2_IC tr3_IC tr4_IC tr5_IC q_IC cen_IC per2_IC per3_IC], ... + options); + + i0 = (n_doses_BDQ_b4Switch * (length(time_vec_reg1_BDQ)-1)) + ... + (idose - n_doses_BDQ_b4Switch - 1) * (length(time_vec_reg2_BDQ)-1) + 1; + i1 = (n_doses_BDQ_b4Switch * (length(time_vec_reg1_BDQ)-1)) + ... + (idose - n_doses_BDQ_b4Switch) * (length(time_vec_reg2_BDQ)-1); + + dep_loc(i0:i1) = sol(1:end-1,1); + tr1_loc(i0:i1) = sol(1:end-1,2); + tr2_loc(i0:i1) = sol(1:end-1,3); + tr3_loc(i0:i1) = sol(1:end-1,4); + tr4_loc(i0:i1) = sol(1:end-1,5); + tr5_loc(i0:i1) = sol(1:end-1,6); + q_loc(i0:i1) = sol(1:end-1,7); + cen_loc(i0:i1) = sol(1:end-1,8); + per2_loc(i0:i1) = sol(1:end-1,9); + per3_loc(i0:i1) = sol(1:end-1,10); + + dep_IC = sol(end,1) + dose_size_BDQ; + tr1_IC = sol(end,2); tr2_IC = sol(end,3); tr3_IC = sol(end,4); + tr4_IC = sol(end,5); tr5_IC = sol(end,6); q_IC = sol(end,7); + cen_IC = sol(end,8); per2_IC = sol(end,9); per3_IC = sol(end,10); + + end + + % Store patient results + BDQ_conc_dep( :,ipt) = dep_loc; + BDQ_conc_tr1( :,ipt) = tr1_loc; + BDQ_conc_tr2( :,ipt) = tr2_loc; + BDQ_conc_tr3( :,ipt) = tr3_loc; + BDQ_conc_tr4( :,ipt) = tr4_loc; + BDQ_conc_tr5( :,ipt) = tr5_loc; + BDQ_conc_q( :,ipt) = q_loc; + BDQ_conc_cen( :,ipt) = cen_loc; + BDQ_conc_per2(:,ipt) = per2_loc; + BDQ_conc_per3(:,ipt) = per3_loc; + + end + + end + + parameters = params_store_BDQ(:,:); + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + xvalues_BDQ = time_vec_local_BDQ; + + plotTimeCourses( ... + 'X', xvalues_BDQ, ... + 'Y', BDQ_conc_cen, ... + 'DrugName', 'BDQ', ... + 'Compartment', 'Central', ... + 'Cycles', numDoses, ... + 'DoseInterval', dose_time_BDQ, ... + 'FigureName', 'BDQ Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_BDQ, ... + 'Y', BDQ_conc_per2, ... + 'DrugName', 'BDQ', ... + 'Compartment', 'Peripheral 2', ... + 'Cycles', numDoses, ... + 'DoseInterval', dose_time_BDQ, ... + 'FigureName', 'BDQ Raw Timecourses (Peripheral 2)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_BDQ, ... + 'Y', BDQ_conc_per3, ... + 'DrugName', 'BDQ', ... + 'Compartment', 'Peripheral 3', ... + 'Cycles', numDoses, ... + 'DoseInterval', dose_time_BDQ, ... + 'FigureName', 'BDQ Raw Timecourses (Peripheral 3)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + xvalues_cyc_stats_BDQ = time_vec_local_BDQ; + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_BDQ, ... + 'Y', BDQ_conc_cen, ... + 'DrugName', 'BDQ', ... + 'Compartment', 'Central', ... + 'Cycles', numDoses, ... + 'DoseInterval', dose_time_BDQ, ... + 'FigureName', 'BDQ Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_BDQ, ... + 'Y', BDQ_conc_per2, ... + 'DrugName', 'BDQ', ... + 'Compartment', 'Peripheral 2', ... + 'Cycles', numDoses, ... + 'DoseInterval', dose_time_BDQ, ... + 'FigureName', 'BDQ Stats (Peripheral 2)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_BDQ, ... + 'Y', BDQ_conc_per3, ... + 'DrugName', 'BDQ', ... + 'Compartment', 'Peripheral 3', ... + 'Cycles', numDoses, ... + 'DoseInterval', dose_time_BDQ, ... + 'FigureName', 'BDQ Stats (Peripheral 3)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + % --- TRICK: redefine "last 24 h" as the 24 h immediately AFTER the final dose --- + % Each pre-switch dose contributes (length(time_vec_reg1_BDQ)-1) timepoints, + % each post-switch dose contributes (length(time_vec_reg2_BDQ)-1) timepoints. + % The final dose's data therefore begins at total_timpts_BDQ - (pts in last interval) + 1. + % We truncate to the first 24 h after that start, so calculateSteadyState's + % "last 24 h" window lands exactly on the post-final-dose interval. + + if n_doses_BDQ_afterSwitch > 0 + pts_per_lastDose_interval = length(time_vec_reg2_BDQ) - 1; + else + pts_per_lastDose_interval = length(time_vec_reg1_BDQ) - 1; + end + + lastDoseStartIdx = total_timpts_BDQ - pts_per_lastDose_interval + 1; + pts_in_24h = round(24 / time_step_size_BDQ); + truncEndIdx = lastDoseStartIdx + pts_in_24h - 1; + + % Sanity check + assert(truncEndIdx <= total_timpts_BDQ, ... + 'Truncation index exceeds simulation length.'); + + BDQ_conc_cen_trunc = BDQ_conc_cen(1:truncEndIdx, :); + time_vec_local_BDQ_trunc = time_vec_local_BDQ(1:truncEndIdx); + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray_BDQ, last24AUC_BDQ, last24Cmax_BDQ] = calculateSteadyState( ... + time_vec_local_BDQ_trunc, ... + 'time_step_size', time_step_size_BDQ, ... + 'conc_c', BDQ_conc_cen_trunc, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'BDQ steady-state attainment', 'SortBy', 'name'); + + statsT = table( ... + mean(last24AUC_BDQ,'omitnan'), std(last24AUC_BDQ,'omitnan'), ... + mean(last24Cmax_BDQ,'omitnan'), std(last24Cmax_BDQ,'omitnan'), ... + 'VariableNames', {'mean_last24AUC','std_last24AUC','mean_last24Cmax','std_last24Cmax'}); + + fprintf('\n=== BDQ last-24h summary stats ===\n'); + disp(statsT); + end + + if opt.plotAUC + plotAUC(last24AUC_BDQ, 'BDQ', ... + 'Color', getColor('AUC'), ... + 'FontSize', opt.GraphFontSize, ... + 'NumBins', 100, ... + 'AnnotateAUC_MICcutoffs', false, ... + 'UniqueTargets', [], ... + 'MICs', [], ... + 'LegendArray', []); + end + + + % -------------------- MIC Distributions -------------------- + + if opt.PTAplot + + mab_BDQ = { + 'M. abscessus'; + { + {'0.016', 6}, {'0.031', 19}, {'0.062', 51}, {'0.125', 53}, ... + {'0.25', 24}, {'0.5', 15}, {'1', 4}, {'2', 2}, ... + {'4', 3}, {'8', 5}, {'16', 7}, {'32', 29} + } + }; + + mac_BDQ = { + 'M. avium complex'; + { + {'0.016', 91}, {'0.031', 21}, {'0.062', 12}, {'0.125', 9}, ... + {'0.25', 6}, {'0.5', 3}, {'1', 1}, {'2', 2}, ... + {'4', 5}, {'8', 9}, {'16', 15}, {'32', 24} + } + }; + + kan_BDQ = { + 'M. kansasii'; + { + {'0.016', 27}, {'0.031', 8}, {'0.062', 8}, {'0.125', 4}, ... + {'0.25', 2}, {'0.5', 1}, {'1', 1}, {'2', 1}, ... + {'4', 3}, {'8', 5}, {'16', 8}, {'32', 16} + } + }; + + tb_BDQ = { + 'M. tuberculosis'; + { + {'0.008', 544}, {'0.016', 1138}, {'0.031', 4368}, {'0.062', 3270}, ... + {'0.125', 550}, {'0.25', 182}, {'0.5', 64}, {'1', 18}, ... + {'2', 23}, {'4', 0}, {'8', 0}, {'16', 0} + } + }; + + speciesAggregation_BDQ = {mab_BDQ, mac_BDQ, kan_BDQ, tb_BDQ}; + normalizedMICSpeciesAggregation_BDQ = normalizeMICsOfAggregation(speciesAggregation_BDQ); + uniqueMICs_BDQ = getUniqueMICs(normalizedMICSpeciesAggregation_BDQ); + + + % -------------------- Setting PTA Targets -------------------- + + target1_BDQ = { + 'MDR-TB 2 months'; + 'AUC24/MIC'; + 175.5; 'tuberculosis' + }; + + target2_BDQ = { + 'MDR-TB 6 months'; + 'AUC24/MIC'; + 118.2; 'tuberculosis' + }; + + target3_BDQ = { + 'MDR-TB 24 months'; + 'AUC24/MIC'; + 74.6; 'tuberculosis' + }; + + uniqueTargets_BDQ = {target1_BDQ, target2_BDQ, target3_BDQ}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix_BDQ = zeros(length(uniqueMICs_BDQ), length(uniqueTargets_BDQ)); + + for i = 1:length(uniqueTargets_BDQ) + PTAMatrix_BDQ(:,i) = calculatePTA( ... + last24ConcArray_BDQ, ... + 'MICDist', uniqueMICs_BDQ, ... + 'targetType', uniqueTargets_BDQ{i}{2}, ... + 'target', uniqueTargets_BDQ{i}{3}, ... + 'time_step_size', time_step_size_BDQ, ... + 'last24AUC', last24AUC_BDQ); + end + + legendArray_BDQ = getLegendArray(uniqueTargets_BDQ, normalizedMICSpeciesAggregation_BDQ); + + [~, plotArgs] = plotPTAs( ... + uniqueMICs_BDQ, ... + PTAMatrix_BDQ, ... + normalizedMICSpeciesAggregation_BDQ, ... + 'DoseSize', dose_size_BDQ_daily, ... + 'DoseFrequency', dose_time_BDQ, ... + 'DrugName', 'BDQ', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray_BDQ, ... + 'UniqueTargets', uniqueTargets_BDQ, ... + 'NumPatients', n_pts); + + end + +end \ No newline at end of file diff --git a/CFZ/CFZ_PlasmaODEs.m b/CFZ/CFZ_PlasmaODEs.m new file mode 100644 index 0000000..b4617b5 --- /dev/null +++ b/CFZ/CFZ_PlasmaODEs.m @@ -0,0 +1,158 @@ +%% ======================================================================= +% CFZ_PlasmaODEs.m — Three-Compartment Transit-Absorption ODEs for +% Clofazimine +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the clofazimine plasma PK model: oral dose +% enters through a transit-compartment absorption model (approximated by +% the Erlang/transit-rate formulation) into a central compartment with two +% peripheral compartments and first-order elimination. Intended to be +% called by an ODE solver (ode45) from CFZ_PopPK. +% +% MODEL +% ODEs as described in Abdelwahab et al., 2020. +% Mahmoud Tareq Abdelwahab et al. "Clofazimine pharmacokinetics in patients +% with TB: dosing implications." In: *Journal of Antimicrobial Chemotherapy* +% 75.11 (Aug. 2020), pp. 3269-3277. ISSN: 0305-7453. DOI: 10.1093/jac/dkaa310. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver (used as time +% after dose in the transit-absorption term). +% y - State vector of drug amounts (mg): +% y(1) = depot/absorption compartment amount +% y(2) = central compartment amount +% y(3) = peripheral 1 amount +% y(4) = peripheral 2 amount +% params - Parameter vector: +% params(1) = n number of transit compartments +% params(2) = MTT mean transit time (h) +% params(3) = F bioavailability +% params(4) = ka absorption rate constant (1/h) +% params(5) = Vc/F central volume (L) +% params(6) = Vp1/F peripheral 1 volume (L) +% params(7) = Vp2/F peripheral 2 volume (L) +% params(8) = CL/F clearance (L/h) +% params(9) = Q1/F intercompartmental clearance 1 (L/h) +% params(10) = Q2/F intercompartmental clearance 2 (L/h) +% params(11) = dose dose for this interval (mg) +% +% OUTPUTS +% dy - Derivative vector (mg/h) matching the state ordering in y. +% +% NOTES +% The transit rate constant is computed internally as ktr = (n+1)/MTT. +% Gamma(n+1) is approximated via Stirling's formula for the transit- +% absorption input term. +% +% AUTHORS +% Marina Surani +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2023-04-01 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = CFZ_PlasmaODEs(t,y,params) + +%========================================================================== +% Extract parameter values from the param variable and assign to local +% variables + +% Number of transit compartments n (number) +n = params(1); + +% Mean transit time MTT in hr +MTT = params(2); + +% Bioavailability F +F = params(3); + +% Absorption rate ka in 1/hr +ka = params(4); + +% Departmental constants +V_Fs = [... + params(5) %(L) Central compartment Vc/F + params(6) %(L) Peripheral compartment Vp1/F + params(7) %(L) Peripheral compartment Vp2/F + ]; +rates = [... + params(8) % (L/h) CL/F Clearance + params(9) % (L/h) Q1/F Intercompartmental Clearance + params(10) % (L/h) Q2/F Intercompartmental Clearance + ]; + +% Amount of drug administered dose in mg +dose = params(11); + +% Transit rate constant between transit compartments ktr in 1/h +ktr = (n+1) / MTT; + +% Current time - after last dose tad in hours +tad = t; + + +% ODE Y value assignmetns +A = [... + y(1) %Drug amount in depot compartment + y(2) %Drug amount in central compartment + y(3) %Drug amount in peripheral compartment 1 + y(4) %Drug amount in peripheral compartment 2 + ]; + + +% Define ODEs + +% Change in mass of drug in absorption compartment A0 (mg/hr) +Gamma_n = sqrt(2*pi) * n^(n+0.5) * exp(-n); +A0_frac = ((ktr*tad)^n * exp(-ktr*tad)) / Gamma_n; +dA0_term1 = F * dose * ktr * A0_frac; %Flow in system by taking dose +dA0_term2 = - ka * A(1); %Out term + +dA0dt = (dA0_term1 + dA0_term2); + + +% ODE around CENTRAL compartment (an IV drug so the central comp. is also +% depot) +% ------------------------------------------ +% ODE_1 terms +dA1_term1 = - rates(2) * A(2) / V_Fs(1); %Flow out to rapid Peripheral compartment +dA1_term2 = rates(2) * A(3) / V_Fs(2); %Flow in from rapid peripheral to central +dA1_term3 = - rates(3) * A(2) / V_Fs(1); %Flow out to slow Peripheral compartment +dA1_term4 = rates(3) * A(4) / V_Fs(3); %Flow in from slow peripheral to central +dA1_term5 = - rates(1) * A(2) / V_Fs(1); %Clearance of drug from system +dA1_term6 = ka * A(1); + +dA1dt = (dA1_term1 + dA1_term2 + dA1_term3 + dA1_term4 + dA1_term5 + dA1_term6); %(mg/h) + +% ODE around PERIPHERAL compartment 1 +% ------------------------------------------ +% ODE_2 terms +dA2_term1 = rates(2) * A(2) / V_Fs(1); %In term +dA2_term2 = - rates(2) * A(3) / V_Fs(2); %Out term + +dA2dt = (dA2_term1 + dA2_term2); %(mg/h) + +% ODE around PERIPHERAL compartment 2 +% ------------------------------------------ +% ODE_3 terms +dA3_term1 = rates(3) * A(2) / V_Fs(1); %In term +dA3_term2 = - rates(3) * A(4) / V_Fs(3); %Out term + +dA3dt = (dA3_term1 + dA3_term2); %(mg/h) + + +% ODE Outputs + +dy = [... + dA0dt + dA1dt + dA2dt + dA3dt + ]; + +end \ No newline at end of file diff --git a/CFZ/CFZ_PopPK.m b/CFZ/CFZ_PopPK.m new file mode 100644 index 0000000..d9538a7 --- /dev/null +++ b/CFZ/CFZ_PopPK.m @@ -0,0 +1,439 @@ +%% ======================================================================= +% CFZ_PopPK.m — Population PK Simulation & PTA Analysis for Clofazimine +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and inter-occasion variability (IOV) and solves the clofazimine plasma +% ODEs across a Monte Carlo patient population. Models a three-compartment +% system with transit-compartment oral absorption, computes steady-state +% AUC/Cmax, and generates time-course, AUC, and probability of target +% attainment (PTA) plots. +% +% MODEL +% Reimplementation of Abdelwahab et al., 2020. +% Mahmoud Tareq Abdelwahab et al. "Clofazimine pharmacokinetics in patients +% with TB: dosing implications." In: *Journal of Antimicrobial Chemotherapy* +% 75.11 (Aug. 2020), pp. 3269-3277. ISSN: 0305-7453. DOI: 10.1093/jac/dkaa310. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (100) Dose per administration (mg). +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (24) Time between doses (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: Marina Surani, 2023-04-01; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2023-04-01 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% CFZ_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% ======================================================================== + +function [plotArgs] = CFZ_PopPK(varargin) + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 100, @(x)isnumeric(x)); + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 24, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Volume parameters + % ------------------------------------------ + V_F_CFZ = [... + 262 % (L) Vc/F + 10500 % (L) Vp1/F + 889 % (L) Vp2/F + ]; + + % Clearance constants + % ------------------------------------------ + rates_CFZ = [... + 11.5 % (L/h) CL/F Overall clearance + 56.3 % (L/h) Q1/F Intercompartmental clearance + 86 % (L/h) Q2/F Intercompartmental clearance + ]; + + % Absorption and transit parameters + % ------------------------------------------ + ka_mean_CFZ = 0.209; % (1/h) Absorption rate + MTT_mean_CFZ = 1.41; % (h) Mean transit time + n_mean_CFZ = 4.75; % Number of transit compartments + + % Bioavailability + % ------------------------------------------ + F_CFZ = 1; + + % Interindividual variability (% CV) + % ------------------------------------------ + IIV_CFZ = [... + 0.256 % CL (CV) IIV + 0.235 % Vc (CV) IIV + 0.296 % Vp1 (CV) IIV + 0.546 % Vp2 (CV) IIV + ]; + + % Inter-occasion variability (% CV) + % ------------------------------------------ + IOV_CFZ = [... + 0.466 % MTT (CV) IOV + 0.326 % ka (CV) IOV + ]; + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify dose size (mg) + dose_size_CFZ = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_CFZ = opt.NumDoses; + + % Specify dose interval (hours) + dose_time_CFZ = opt.DoseInterval; + + % Specify time step and build time vectors + time_step_size_CFZ = 0.5; + + time_vec_CFZ = 0 : time_step_size_CFZ : dose_time_CFZ; + time_vec_local_CFZ = 0 : time_step_size_CFZ : dose_time_CFZ * n_doses_CFZ - time_step_size_CFZ; + total_timpts_CFZ = n_doses_CFZ * (length(time_vec_CFZ) - 1); + + + % -------------------- Initialize Matrices -------------------- + + total_params_CFZ = 11; + param_store_CFZ = zeros([total_params_CFZ n_pts]); + CFZ_conc_depot = zeros([total_timpts_CFZ n_pts]); + CFZ_conc_plasma = zeros([total_timpts_CFZ n_pts]); + CFZ_conc_p1 = zeros([total_timpts_CFZ n_pts]); + CFZ_conc_p2 = zeros([total_timpts_CFZ n_pts]); + + + % -------------------- ODE Solving -------------------- + + for ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + V_F_pt_CFZ = [... + V_F_CFZ(1) * exp(random('Normal',0,sqrt(log(IIV_CFZ(2)^2+1)))); % Vc/F + V_F_CFZ(2) * exp(random('Normal',0,sqrt(log(IIV_CFZ(3)^2+1)))); % Vp1/F + V_F_CFZ(3) * exp(random('Normal',0,sqrt(log(IIV_CFZ(4)^2+1)))); % Vp2/F + ]; + + rates_pt_CFZ = [... + rates_CFZ(1) * exp(random('Normal',0,sqrt(log(IIV_CFZ(1)^2+1)))); % CL/F + rates_CFZ(2); % Q1/F + rates_CFZ(3); % Q2/F + ]; + + % Set initial conditions + % -------------------------------------------------------- + CFZ_depot_ic = dose_size_CFZ; + CFZ_plasma_ic = 0; + CFZ_p1_ic = 0; + CFZ_p2_ic = 0; + + % Initialize local storage + CFZ_conc_depot_loc = zeros([total_timpts_CFZ 1]); + CFZ_conc_plasma_loc = zeros([total_timpts_CFZ 1]); + CFZ_conc_p1_loc = zeros([total_timpts_CFZ 1]); + CFZ_conc_p2_loc = zeros([total_timpts_CFZ 1]); + + for idose = 1:n_doses_CFZ + + % Sample occasion-specific parameters using IOV + % -------------------------------------------------------- + MTT_oc_CFZ = MTT_mean_CFZ * exp(random('Normal',0,sqrt(log(IOV_CFZ(1)^2+1)))); + ka_oc_CFZ = ka_mean_CFZ * exp(random('Normal',0,sqrt(log(IOV_CFZ(2)^2+1)))); + F_oc_CFZ = F_CFZ * exp(random('Normal',0,sqrt(log(IOV_CFZ(2)^2+1)))); + + % Compile parameters + params_CFZ = [... + n_mean_CFZ; % n params(1) + MTT_oc_CFZ; % MTT params(2) + F_oc_CFZ; % F params(3) + ka_oc_CFZ; % ka params(4) + V_F_pt_CFZ(1); % Vc/F params(5) + V_F_pt_CFZ(2); % Vp1/F params(6) + V_F_pt_CFZ(3); % Vp2/F params(7) + rates_pt_CFZ(1); % CL/F params(8) + rates_pt_CFZ(2); % Q1/F params(9) + rates_pt_CFZ(3); % Q2/F params(10) + dose_size_CFZ; % dose params(11) + ]; + + % Call ODE solver + [~, CFZ_sol] = ode45(@(t,y) CFZ_PlasmaODEs(t,y,params_CFZ), ... + time_vec_CFZ, [CFZ_depot_ic, CFZ_plasma_ic, CFZ_p1_ic, CFZ_p2_ic]); + + % Organization of data post ODE + i0 = (idose-1)*(length(time_vec_CFZ)-1)+1; + i1 = idose *(length(time_vec_CFZ)-1); + + CFZ_conc_depot_loc( i0:i1,1) = CFZ_sol(1:end-1,1); + CFZ_conc_plasma_loc(i0:i1,1) = CFZ_sol(1:end-1,2) / V_F_pt_CFZ(1); + CFZ_conc_p1_loc( i0:i1,1) = CFZ_sol(1:end-1,3) / V_F_pt_CFZ(2); + CFZ_conc_p2_loc( i0:i1,1) = CFZ_sol(1:end-1,4) / V_F_pt_CFZ(3); + + % Update initial conditions for the next dose + % Dose is added inside the ODE depot equation, so not added here + CFZ_depot_ic = CFZ_sol(end,1); + CFZ_plasma_ic = CFZ_sol(end,2); + CFZ_p1_ic = CFZ_sol(end,3); + CFZ_p2_ic = CFZ_sol(end,4); + + end + + % Combine data for all patients + param_store_CFZ(:,ipt) = params_CFZ; + CFZ_conc_depot( :,ipt) = CFZ_conc_depot_loc; + CFZ_conc_plasma(:,ipt) = CFZ_conc_plasma_loc; + CFZ_conc_p1( :,ipt) = CFZ_conc_p1_loc; + CFZ_conc_p2( :,ipt) = CFZ_conc_p2_loc; + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + xvalues_CFZ = time_vec_local_CFZ; % Convert hours to weeks for CFZ + + plotTimeCourses( ... + 'X', xvalues_CFZ, ... + 'Y', CFZ_conc_plasma, ... + 'DrugName', 'CFZ', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_CFZ, ... + 'DoseInterval', dose_time_CFZ, ... + 'FigureName', 'CFZ Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'XLabel', 'Time', ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_CFZ, ... + 'Y', CFZ_conc_p1, ... + 'DrugName', 'CFZ', ... + 'Compartment', 'Peripheral 1', ... + 'Cycles', n_doses_CFZ, ... + 'DoseInterval', dose_time_CFZ / 168, ... + 'FigureName', 'CFZ Raw Timecourses (Peripheral 1)', ... + 'DoStats', false, ... + 'XLabel', 'Time (weeks)', ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_CFZ, ... + 'Y', CFZ_conc_p2, ... + 'DrugName', 'CFZ', ... + 'Compartment', 'Peripheral 2', ... + 'Cycles', n_doses_CFZ, ... + 'DoseInterval', dose_time_CFZ / 168, ... + 'FigureName', 'CFZ Raw Timecourses (Peripheral 2)', ... + 'DoStats', false, ... + 'XLabel', 'Time (weeks)', ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + xvalues_cyc_stats_CFZ = time_vec_local_CFZ; % Convert hours to weeks for CFZ + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_CFZ, ... + 'Y', CFZ_conc_plasma, ... + 'DrugName', 'CFZ', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_CFZ, ... + 'DoseInterval', dose_time_CFZ, ... + 'FigureName', 'CFZ Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'XLabel', 'Time', ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_CFZ, ... + 'Y', CFZ_conc_p1, ... + 'DrugName', 'CFZ', ... + 'Compartment', 'Peripheral 1', ... + 'Cycles', n_doses_CFZ, ... + 'DoseInterval', dose_time_CFZ / 168, ... + 'FigureName', 'CFZ Stats (Peripheral 1)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'XLabel', 'Time (weeks)', ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_CFZ, ... + 'Y', CFZ_conc_p2, ... + 'DrugName', 'CFZ', ... + 'Compartment', 'Peripheral 2', ... + 'Cycles', n_doses_CFZ, ... + 'DoseInterval', dose_time_CFZ / 168, ... + 'FigureName', 'CFZ Stats (Peripheral 2)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'XLabel', 'Time (weeks)', ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.3; + CmaxTolerance = 0.3; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_CFZ, ... + 'time_step_size', time_step_size_CFZ, ... + 'conc_c', CFZ_conc_plasma, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'CFZ steady-state attainment', 'SortBy', 'name'); + + if numBothReached ~= n_pts + warning('CFZ: steady state not reached for all patients'); + end + + statsT = table( ... + mean(last24AUC,'omitnan'), std(last24AUC,'omitnan'), ... + mean(last24Cmax,'omitnan'), std(last24Cmax,'omitnan'), ... + 'VariableNames', {'mean_last24AUC','std_last24AUC','mean_last24Cmax','std_last24Cmax'}); + + fprintf('\n=== CFZ last-24h summary stats ===\n'); + disp(statsT); + end + + if opt.plotAUC + plotAUC(last24AUC, 'CFZ'); + end + + % -------------------- MIC Distributions -------------------- + + if opt.PTAplot + + avium = { + 'M. avium complex'; + {{'0.06', 92}, {'0.125', 74}, {'0.25', 8}, {'0.5', 7}, {'1', 6}, {'>=2', 16}} + }; + + kanamycin = { + 'M. kansasii'; + {{'0.06', 63}, {'0.125', 0}, {'0.25', 0}, {'0.5', 1}, {'1', 1}, {'>=2', 1}} + }; + + mab = { + 'M. abscessus'; + {{'0.06', 2}, {'0.125', 18}, {'0.25', 42}, {'0.5', 50}, {'1', 26}, {'>=2', 35}} + }; + + tuberculosis = { + 'M. tuberculosis'; + {{'0.06', 160}, {'0.125', 87}, {'0.25', 18}, {'0.5', 2}, {'1', 0}, {'>=2', 0}} + }; + + speciesAggregation = {avium, kanamycin, mab, tuberculosis}; + normalizedMICSpeciesAggregation = normalizeMICsOfAggregation(speciesAggregation); + uniqueMICs = getUniqueMICs(normalizedMICSpeciesAggregation); + + + % -------------------- Setting PTA Targets -------------------- + + target1 = { + 'MDR-TB'; + 'AUC24/MIC'; + 50; 'tuberculosis' + }; + + uniqueTargets = {target1}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix = zeros(length(uniqueMICs), length(uniqueTargets)); + + for i = 1:length(uniqueTargets) + PTAMatrix(:,i) = calculatePTA( ... + last24ConcArray, ... + 'MICDist', uniqueMICs, ... + 'targetType', uniqueTargets{i}{2}, ... + 'target', uniqueTargets{i}{3}, ... + 'time_step_size', time_step_size_CFZ, ... + 'last24AUC', last24AUC); + end + + legendArray = getLegendArray(uniqueTargets, normalizedMICSpeciesAggregation); + + [~, plotArgs] = plotPTAs( ... + uniqueMICs, ... + PTAMatrix, ... + normalizedMICSpeciesAggregation, ... + 'DoseSize', dose_size_CFZ, ... + 'DoseFrequency', dose_time_CFZ, ... + 'DrugName', 'CFZ', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray, ... + 'UniqueTargets', uniqueTargets, ... + 'NumPatients', n_pts, ... + 'ShowCI', false); + + end + +end \ No newline at end of file diff --git a/CLR/CLR_PlasmaODEs.m b/CLR/CLR_PlasmaODEs.m new file mode 100644 index 0000000..02526ea --- /dev/null +++ b/CLR/CLR_PlasmaODEs.m @@ -0,0 +1,126 @@ +%% ======================================================================= +% CLR_PlasmaODEs.m — Three-Compartment ODEs for Clarithromycin +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the clarithromycin plasma PK model: a +% central compartment exchanging with a peripheral compartment and an +% epithelial lining fluid (ELF) compartment, with first-order oral +% absorption and first-order elimination from the central compartment. +% Intended to be called by an ODE solver (ode45) from CLR_PopPK. +% +% MODEL +% ODEs as described in Ikawa et al., 2014. +% K. Ikawa et al. "Pharmacokinetic modelling of serum and bronchial +% concentrations for clarithromycin and telithromycin, and site-secific +% pharmacodynamic simulation for their dosages". In: *Journal of Clinical +% Pharmacy and Therapeutics* 39.4 (Mar. 2014), pp. 411-417. +% DOI: 10.1111/jcpt.12157. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver (used as time +% after dose in the absorption input term). +% y - State vector of drug amounts (mg): +% y(1) = central compartment amount +% y(2) = peripheral compartment amount +% y(3) = ELF compartment amount +% params - Parameter vector: +% params(1) = V1/F central volume (L) +% params(2) = V2/F peripheral volume (L) +% params(3) = V3/F ELF volume (L) +% params(4) = CL/F clearance (L/h) +% params(5) = Ka absorption rate constant (1/h) +% params(6) = K12 central -> peripheral rate (1/h) +% params(7) = K21 peripheral -> central rate (1/h) +% params(8) = K13 central -> ELF rate (1/h) +% params(9) = K31 ELF -> central rate (1/h) +% params(10) = dose dose for this interval (mg) +% +% OUTPUTS +% dy - Derivative vector (mg/h) matching the state ordering in y. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2023-06-22 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = CLR_PlasmaODEs(t,y,params) + +% Extracting parameter values from params variable to assign to local variables + +% Departmental constants +V_Fs = [params(1) %(L) Central compartment V1/F + params(2) %(L) Peripheral compartment V2/F + params(3) %(L) ELF Compartment V3/F + ]; +CL_F = params(4); %(L/h) Clearance Constant CL/F +Ks = [params(5) %(1/h) K_a + params(6) %(1/h) K_1_2 + params(7) %(1/h) K_2_1 + params(8) %(1/h) K_1_3 + params(9) %(1/h) K_3_1 + ]; + +% Dose related variables +dose = params(10); %(mg) dose size + +% Current time - after last dose tad in hours +tad = t; + +% ODE Y value assignmetns +A = [y(1) %drug conc in central compartment + y(2) %drug conc in peripheral compartment + y(3) %drug conc in epithelial compartment + ]; + +% Define ODEs + +% ODE around central compartment +% ------------------------------------------ +% ODE_1 terms +dA1_term1 = ((Ks(1) * dose * exp(-Ks(1)*tad))); %Flow in system by taking dose +dA1_term2 = - (Ks(2) * A(1)); %Flow out to Peripheral compartment +dA1_term3 = (Ks(3) * A(2)); %Flow in from peripheral to central + % compartment +dA1_term4 = - (Ks(4) * A(1)); %Flow out to Epithelial to central +dA1_term5 = (Ks(5) * A(3)); %Flow in from epithelial to central +dA1_term6 = - (A(1) * (CL_F / V_Fs(1))) ; %Clearance of drug from system + % through central compartment + +% Adding terms together +dA1dt = (dA1_term1 + dA1_term2 + dA1_term3 + dA1_term4 + dA1_term5 + dA1_term6); + +% ODE around peripheral compartment +% ------------------------------------------ +% ODE_2 terms +dA2_term1 = (Ks(2)* A(1)); %In term +dA2_term2 = - (Ks(3) * A(2)); %Out term + +%Adding terms together +dA2dt = (dA2_term1 + dA2_term2); + +% ODE around epithelial compartment +% ------------------------------------------ +% ODE_3 terms +dA3_term1 = (Ks(4)* A(1)) ; %In term +dA3_term2 = - (Ks(5) * A(3)); %Out term + +%Adding terms together +dA3dt = (dA3_term1 + dA3_term2); + + +% ODE Outputs + +dy = [dA1dt + dA2dt + dA3dt + ]; + + +end \ No newline at end of file diff --git a/CLR/CLR_PopPK.m b/CLR/CLR_PopPK.m new file mode 100644 index 0000000..aa57223 --- /dev/null +++ b/CLR/CLR_PopPK.m @@ -0,0 +1,440 @@ +%% ======================================================================= +% CLR_PopPK.m — Population PK Simulation & PTA Analysis for +% Clarithromycin +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the clarithromycin plasma ODEs across a Monte Carlo patient +% population. Models a three-compartment system (central, peripheral, and +% epithelial lining fluid [ELF]) with first-order oral absorption, computes +% steady-state AUC/Cmax in both ELF and plasma, and generates time-course, +% AUC, and probability of target attainment (PTA) plots. +% +% MODEL +% Reimplementation of Ikawa et al., 2014. +% K. Ikawa et al. "Pharmacokinetic modelling of serum and bronchial +% concentrations for clarithromycin and telithromycin, and site-secific +% pharmacodynamic simulation for their dosages". In: *Journal of Clinical +% Pharmacy and Therapeutics* 39.4 (Mar. 2014), pp. 411-417. +% DOI: 10.1111/jcpt.12157. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (500) Dose per administration (mg). +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (12) Time between doses (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distributions (ELF + plasma). +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: T. J. Shoaf, 2023-06-22; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2023-06-22 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% CLR_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% ======================================================================== + +function [plotArgs] = CLR_PopPK(varargin) + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 500, @(x)isnumeric(x)); + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 12, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Volume parameters + % ------------------------------------------ + V1_F_CLR = 204.7; % (L) Central compartment V/F + V2_F_CLR = 168.9; % (L) Peripheral compartment V/F + V3_F_CLR = 67.1; % (L) ELF compartment V/F + + % Clearance constants + % ------------------------------------------ + CL_F_CLR = 34.4; % (L/h) Clearance/F + + % Rate constants + % ------------------------------------------ + K_a_CLR = 0.680; % (1/h) First-order absorption depot to central + K_12_CLR = 0.0193; % (1/h) Central to peripheral + K_21_CLR = 0.434; % (1/h) Peripheral to central + K_13_CLR = 0.667; % (1/h) Central to ELF + K_31_CLR = 0.260; % (1/h) ELF to central + + % Interindividual variability (exponential error model) + % ------------------------------------------ + eta_values_CLR = [... + 0.120 % V1/F (1) + 0 % V2/F (2) + 0.340 % V3/F (3) + 0.132 % CL/F (4) + 0.339 % Ka (5) + 0.0 % K12 (6) + 0.0 % K21 (7) + 0.133 % K13 (8) + 0.128 % K31 (9) + ]; + + % Residual variability (additive error model) + % ------------------------------------------ + eps = 0.160; %#ok + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify dose size (mg) + dose_size_CLR = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_CLR = opt.NumDoses; + + % Specify dose interval (hours) + dose_time_CLR = opt.DoseInterval; + + % Specify time step and build time vectors + time_step_size_CLR = 0.5; + + time_vec_CLR = 0 : time_step_size_CLR : dose_time_CLR; + time_vec_local_CLR = 0 : time_step_size_CLR : dose_time_CLR * n_doses_CLR - time_step_size_CLR; + total_timpts_CLR = n_doses_CLR * (length(time_vec_CLR) - 1); + total_params_CLR = 10; + + + % -------------------- Initialize Matrices -------------------- + + param_store_CLR = zeros([total_params_CLR n_pts]); + CLR_conc_central = zeros([total_timpts_CLR n_pts]); + CLR_conc_per = zeros([total_timpts_CLR n_pts]); + CLR_conc_elf = zeros([total_timpts_CLR n_pts]); + + + % -------------------- ODE Solving -------------------- + + for ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + V1_ipt = V1_F_CLR * exp(random('Normal',0, eta_values_CLR(1))); + V2_ipt = V2_F_CLR; % IIV = 0 (fixed) + V3_ipt = V3_F_CLR * exp(random('Normal',0, eta_values_CLR(3))); + CL_ipt = CL_F_CLR * exp(random('Normal',0, eta_values_CLR(4))); + Ka_ipt = K_a_CLR * exp(random('Normal',0, eta_values_CLR(5))); + K12_ipt = K_12_CLR; % IIV = 0 (fixed) + K21_ipt = K_21_CLR; % IIV = 0 (fixed) + K13_ipt = K_13_CLR * exp(random('Normal',0, eta_values_CLR(8))); + K31_ipt = K_31_CLR * exp(random('Normal',0, eta_values_CLR(9))); + + p_ipt = [... + V1_ipt; % params(1) + V2_ipt; % params(2) + V3_ipt; % params(3) + CL_ipt; % params(4) + Ka_ipt; % params(5) + K12_ipt; % params(6) + K21_ipt; % params(7) + K13_ipt; % params(8) + K31_ipt; % params(9) + dose_size_CLR; % params(10) + ]; + + param_store_CLR(:,ipt) = p_ipt; + + % Set initial conditions + % -------------------------------------------------------- + cen_IC = 0; + per_IC = 0; + elf_IC = 0; + + % Initialize local storage + cen_loc = zeros([total_timpts_CLR 1]); + per_loc = zeros([total_timpts_CLR 1]); + elf_loc = zeros([total_timpts_CLR 1]); + + for idose = 1:n_doses_CLR + + % Call ODE solver + [~, CLR_sol] = ode45(@(t,y) CLR_PlasmaODEs(t,y,p_ipt), ... + time_vec_CLR, [cen_IC per_IC elf_IC]); + + % Organization of data post ODE + i0 = (idose-1)*(length(time_vec_CLR)-1)+1; + i1 = idose *(length(time_vec_CLR)-1); + + cen_loc(i0:i1) = CLR_sol(1:end-1,1) / V1_ipt; + per_loc(i0:i1) = CLR_sol(1:end-1,2) / V2_ipt; + elf_loc(i0:i1) = CLR_sol(1:end-1,3) / V3_ipt; + + % Update initial conditions for the next dose + cen_IC = CLR_sol(end,1); + per_IC = CLR_sol(end,2); + elf_IC = CLR_sol(end,3); + + end + + % Store patient results + CLR_conc_central(:,ipt) = cen_loc; + CLR_conc_per( :,ipt) = per_loc; + CLR_conc_elf( :,ipt) = elf_loc; + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + xvalues_CLR = time_vec_local_CLR; + + plotTimeCourses( ... + 'X', xvalues_CLR, ... + 'Y', CLR_conc_central, ... + 'DrugName', 'CLR', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_CLR, ... + 'DoseInterval', dose_time_CLR, ... + 'FigureName', 'CLR Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_CLR, ... + 'Y', CLR_conc_per, ... + 'DrugName', 'CLR', ... + 'Compartment', 'Peripheral', ... + 'Cycles', n_doses_CLR, ... + 'DoseInterval', dose_time_CLR, ... + 'FigureName', 'CLR Raw Timecourses (Peripheral)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_CLR, ... + 'Y', CLR_conc_elf, ... + 'DrugName', 'CLR', ... + 'Compartment', 'ELF', ... + 'Cycles', n_doses_CLR, ... + 'DoseInterval', dose_time_CLR, ... + 'FigureName', 'CLR Raw Timecourses (ELF)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + xvalues_cyc_stats_CLR = time_vec_local_CLR; + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_CLR, ... + 'Y', CLR_conc_central, ... + 'DrugName', 'CLR', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_CLR, ... + 'DoseInterval', dose_time_CLR, ... + 'FigureName', 'CLR Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_CLR, ... + 'Y', CLR_conc_elf, ... + 'DrugName', 'CLR', ... + 'Compartment', 'ELF', ... + 'Cycles', n_doses_CLR, ... + 'DoseInterval', dose_time_CLR, ... + 'FigureName', 'CLR Stats (ELF)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + % --- ELF compartment --- + [nAUC_ELF, nCmax_ELF, nBoth_ELF, ... + last24ConcArray_ELF, last24AUC_ELF, last24Cmax_ELF] = calculateSteadyState( ... + time_vec_local_CLR, ... + 'time_step_size', time_step_size_CLR, ... + 'conc_c', CLR_conc_elf, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + % --- Plasma (central) compartment --- + [nAUC_Pl, nCmax_Pl, nBoth_Pl, ... + last24ConcArray_Pl, last24AUC_Pl, last24Cmax_Pl] = calculateSteadyState( ... + time_vec_local_CLR, ... + 'time_step_size', time_step_size_CLR, ... + 'conc_c', CLR_conc_central, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(nAUC_ELF, nCmax_ELF, nBoth_ELF, n_pts, ... + 'Title', 'CLR ELF steady-state attainment', 'SortBy', 'name'); + summarize_ss_counts(nAUC_Pl, nCmax_Pl, nBoth_Pl, n_pts, ... + 'Title', 'CLR Plasma steady-state attainment', 'SortBy', 'name'); + + if nBoth_ELF ~= n_pts, warning('CLR (ELF): steady state not reached for all patients'); end + if nBoth_Pl ~= n_pts, warning('CLR (Plasma): steady state not reached for all patients'); end + + statsT = table( ... + mean(last24AUC_ELF,'omitnan'), std(last24AUC_ELF,'omitnan'), ... + mean(last24Cmax_ELF,'omitnan'), std(last24Cmax_ELF,'omitnan'), ... + mean(last24AUC_Pl, 'omitnan'), std(last24AUC_Pl, 'omitnan'), ... + mean(last24Cmax_Pl,'omitnan'), std(last24Cmax_Pl,'omitnan'), ... + 'VariableNames', {'mean_AUC_ELF','std_AUC_ELF','mean_Cmax_ELF','std_Cmax_ELF', ... + 'mean_AUC_Pl', 'std_AUC_Pl', 'mean_Cmax_Pl', 'std_Cmax_Pl'}); + + fprintf('\n=== CLR last-24h summary stats ===\n'); + disp(statsT); + end + + if opt.plotAUC + plotAUC(last24AUC_ELF, 'CLR (ELF)'); + plotAUC(last24AUC_Pl, 'CLR (Plasma)'); + end + + + % -------------------- MIC Distributions -------------------- + + if opt.PTAplot + + mab = { + 'M. abscessus'; + {{'0.06', 112}, {'0.12', 156}, {'0.25', 155}, {'0.5', 108}, {'1', 90}, ... + {'2', 49}, {'4', 26}, {'8', 39}, {'16', 92}, {'32', 235}, {'64', 0}, {'128', 0}} + }; + + mac = { + 'M. avium complex'; + {{'0.031', 7}, {'0.06', 71}, {'0.12', 90}, {'0.25', 165}, {'0.5', 631}, ... + {'1', 1315}, {'2', 1987}, {'4', 1014}, {'8', 421}, {'16', 124}, ... + {'32', 31}, {'64', 67}, {'128', 260}} + }; + + kan = { + 'M. kansasii'; + {{'0.031', 0}, {'0.06', 0}, {'0.12', 1}, {'0.25', 2}, {'0.5', 11}, ... + {'1', 0}, {'2', 0}, {'4', 0}, {'8', 0}, {'16', 0}, {'32', 0}, {'64', 0}, {'128', 0}} + }; + + tb = { + 'M. tuberculosis'; + {{'0.031', 0}, {'0.06', 0}, {'0.12', 0}, {'0.25', 0}, {'0.5', 0}, ... + {'1', 0}, {'2', 0}, {'4', 22.22}, {'8', 0}, {'16', 33.33}, ... + {'>16', 44.44}, {'64', 0}, {'128', 0}} + }; + + speciesAggregation = {mab, mac, kan, tb}; + normalizedMICSpeciesAggregation = normalizeMICsOfAggregation(speciesAggregation); + uniqueMICs = getUniqueMICs(normalizedMICSpeciesAggregation); + + + % -------------------- Setting PTA Targets -------------------- + + target_ELF = { % https://doi.org/10.1111/jcpt.12157 + 'ELF'; + 'AUC24/MIC'; + 100; 'ELF' + }; + + target_Plasma = { + 'Plasma'; + 'AUC24/MIC'; + 10; 'Plasma' + }; + + uniqueTargets = {target_ELF, target_Plasma}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix = zeros(length(uniqueMICs), length(uniqueTargets)); + + % Curve 1: ELF target (AUC24/MIC >= 100) using ELF concentrations + PTAMatrix(:,1) = calculatePTA( ... + last24ConcArray_ELF, ... + 'MICDist', uniqueMICs, ... + 'targetType', target_ELF{2}, ... + 'target', target_ELF{3}, ... + 'time_step_size', time_step_size_CLR, ... + 'last24AUC', last24AUC_ELF); + + % Curve 2: Plasma target (AUC24/MIC >= 10) using central concentrations + PTAMatrix(:,2) = calculatePTA( ... + last24ConcArray_Pl, ... + 'MICDist', uniqueMICs, ... + 'targetType', target_Plasma{2}, ... + 'target', target_Plasma{3}, ... + 'time_step_size', time_step_size_CLR, ... + 'last24AUC', last24AUC_Pl); + + legendArray = getLegendArray(uniqueTargets, normalizedMICSpeciesAggregation); + + [~, plotArgs] = plotPTAs( ... + uniqueMICs, ... + PTAMatrix, ... + normalizedMICSpeciesAggregation, ... + 'DoseSize', dose_size_CLR, ... + 'DoseFrequency', dose_time_CLR, ... + 'DrugName', 'CLR', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray, ... + 'UniqueTargets', uniqueTargets, ... + 'NumPatients', n_pts, ... + 'ShowCI', false... + ); + + end + +end \ No newline at end of file diff --git a/EMB/EMB_PlasmaODEs.m b/EMB/EMB_PlasmaODEs.m new file mode 100644 index 0000000..74e0780 --- /dev/null +++ b/EMB/EMB_PlasmaODEs.m @@ -0,0 +1,134 @@ +%% ======================================================================= +% EMB_PlasmaODEs.m — Two-Compartment Transit-Absorption ODEs for +% Ethambutol +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the ethambutol plasma PK model: oral dose +% enters through a transit-compartment absorption term (single transit +% compartment, n = 1) into a central compartment with one peripheral +% compartment and first-order elimination. Intended to be called by an +% ODE solver (ode45) from EMB_PopPK. +% +% MODEL +% ODEs as described in Jonsson et al., 2011. +% Siv Jönsson et al. "Population Pharmacokinetics of Ethambutol in South +% African Tuberculosis Patients." In: *Antimicrobial Agents and Chemotherapy* +% 55.9 (2011), pp. 4230-4237. DOI: 10.1128/aac.00274-11. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver (used as time +% after dose in the transit-absorption term). +% y - State vector of drug amounts (mg): +% y(1) = depot/absorption compartment amount +% y(2) = central compartment amount +% y(3) = peripheral compartment amount +% params - Parameter vector: +% params(1) = Vc/F central volume (L) +% params(2) = Vp/F peripheral volume (L) +% params(3) = CL/F clearance (L/h) +% params(4) = Q/F intercompartmental clearance (L/h) +% params(5) = Ka absorption rate constant (1/h) +% params(6) = MTT mean transit time (h) +% params(7) = F bioavailability +% params(8) = dose dose for this interval (mg) +% +% OUTPUTS +% dy - Derivative vector (mg/h) matching the state ordering in y. +% +% NOTES +% The transit rate constant is computed internally as ktr = (n+1)/MTT +% with n = 1; Gamma(n+1) is approximated via Stirling's formula for the +% transit-absorption input term. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2023-07-17 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = EMB_PlasmaODEs(t,y,params) + +% Extracting parameter values from params variable to assign to local variables + +% Departmental constants +V_Fs = [... + params(1) %(L) Central compartment Vc/F + params(2) %(L) Peripheral compartment Vp/F + ]; +rates = [... + params(3) % CL/F Oral Clearance + params(4) % Q/F Itercompartmental Clearance + params(5) % Ka Absorption rate + ]; +params_other = [... + params(6) % MTT Mean transit time + params(7) % F Bioavailability + ]; + +% Dose related variables +dose = params(8); %(mg) dose size + +% Current time - after last dose tad in hours +tad = t; + +n = 1; + +% Transit rate between transit compartment +ktr = (n + 1) / params_other(1); + +% ODE Y value assignmetns +A = [... + y(1) %Drug amount in depot compartment + y(2) %Drug amount in central compartment + y(3) %Drug amount in peripheral compartment + ]; + +% Define ODEs + +% ODE around Depot compartment +% ------------------------------------------ +% change this to 1 +% ODE_0 terms +Gamma_n = sqrt(2*pi) * n^(n+0.5) * exp(-n); +A0_frac = ((ktr*tad)^n * exp(-ktr*tad)) / Gamma_n; +dA0_term1 = exp(log(params_other(2) * dose * ktr * A0_frac)); %Flow in system by taking dose +dA0_term2 = - rates(3) * A(1); %Out term + +dA0dt = dA0_term1 + dA0_term2; %(mg/h) + + +% ODE around CENTRAL compartment +% ------------------------------------------ +% ODE_1 terms +dA1_term1 = rates(3) * A(1); +dA1_term2 = - rates(2) * A(2)/V_Fs(1); %Flow out to Peripheral compartment +dA1_term3 = rates(2) * A(3)/V_Fs(2); %Flow in from peripheral to central +dA1_term4 = - rates(1) * A(2)/V_Fs(1); %Clearance of drug from system + +dA1dt = (dA1_term1 + dA1_term2 + dA1_term3 + dA1_term4); %(mg/h) + +% ODE around PERIPHERAL compartment +% ------------------------------------------ +% ODE_2 terms +dA2_term1 = rates(2) * A(2)/V_Fs(1); %In term +dA2_term2 = - rates(2) * A(3)/V_Fs(2); %Out term + +dA2dt = dA2_term1 + dA2_term2; %(mg/h) + + +% ODE Outputs + +dy = [... + dA0dt + dA1dt + dA2dt + ]; + + +end \ No newline at end of file diff --git a/EMB/EMB_PopPK.m b/EMB/EMB_PopPK.m new file mode 100644 index 0000000..5bceca2 --- /dev/null +++ b/EMB/EMB_PopPK.m @@ -0,0 +1,412 @@ +%% ======================================================================= +% EMB_PopPK.m — Population PK Simulation & PTA Analysis for Ethambutol +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the ethambutol plasma ODEs across a Monte Carlo patient +% population. Models a two-compartment system with transit-compartment +% oral absorption, per-patient weight-based dosing, and an HIV covariate +% effect on bioavailability. Computes steady-state AUC/Cmax and generates +% time-course, AUC, and probability of target attainment (PTA) plots. +% +% MODEL +% Reimplementation of Jonsson et al., 2011. +% Siv Jönsson et al. "Population Pharmacokinetics of Ethambutol in South +% African Tuberculosis Patients." In: *Antimicrobial Agents and Chemotherapy* +% 55.9 (2011), pp. 4230-4237. DOI: 10.1128/aac.00274-11. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (15) Dose (mg/kg); scaled by per-patient weight +% (sampled uniformly 36-67 kg) inside the loop. +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (24) Time between doses (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: T. J. Shoaf, 2023-07-17; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2023-07-17 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% EMB_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% ======================================================================== + +function [plotArgs] = EMB_PopPK(varargin) + + options = odeset('NonNegative', 1); + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 15, @(x)isnumeric(x)); % mg/kg; scaled by per-patient weight inside loop + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 24, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Volume parameters + % ------------------------------------------ + V_F_EMB = [... + 82.4 % (L) Vc/F + 623 % (L) Vp/F + ]; + + % Clearance and rate constants + % ------------------------------------------ + rates_EMB = [... + 39.9 % (L/h) CL/F Overall clearance + 34.3 % (L/h) Q/F Apparent intercompartmental clearance + 0.474 % (1/h) Ka Absorption rate constant + ]; + + % Other constants + % ------------------------------------------ + const_other_EMB = [... + 0.789 % (h) Mean transit time (MTT) + -0.154 % Effect of HIV on F (fractional change) + 12/60 % Fraction of cohort with HIV + 0.52 % (%) Fraction with 800 mg dose + 0.25 % (%) Fraction with 1000 mg dose + 0.23 % (%) Fraction with 1200 mg dose + ]; + + % Interindividual variability (% CV) + % ------------------------------------------ + IIV_IOV_EMB = [... + 0.39 % Ka (CV) IIV + 0.93 % MTT (CV) IIV + 0.20 % CL/F (CV) IIV + 0.36 % CL/F (CV) IOV + ]; + + % Residual errors + % ------------------------------------------ + res_err_EMB = [... + 0.107 % (mg/L) Additive residual error + 0.318 % Proportional residual error (% CV) + ]; %#ok + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify dose size (mg/kg; multiplied by per-patient weight in loop) + dose_size_EMB = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_EMB = opt.NumDoses; + + % Specify dose interval (hours) + dose_time_EMB = opt.DoseInterval; + + % Specify time step and build time vectors + time_step_size_EMB = 0.5; + + time_vec_EMB = 0 : time_step_size_EMB : dose_time_EMB; + time_vec_local_EMB = 0 : time_step_size_EMB : dose_time_EMB * n_doses_EMB - time_step_size_EMB; + total_timpts_EMB = n_doses_EMB * (length(time_vec_EMB) - 1); + total_params_EMB = 8; + + + % -------------------- Initialize Matrices -------------------- + + param_store_EMB = zeros([total_params_EMB n_pts]); + EMB_conc_dep = zeros([total_timpts_EMB n_pts]); + EMB_conc_c = zeros([total_timpts_EMB n_pts]); + EMB_conc_p = zeros([total_timpts_EMB n_pts]); + + + % -------------------- ODE Solving -------------------- + + for ipt = 1:n_pts + + % Sample patient HIV status + if rand() < const_other_EMB(3) + HIV_pt = 1; + else + HIV_pt = 0; + end + + % Sample patient weight (uniform between 36 and 67 kg) and scale dose + weight_pt_EMB = 36 + (67 - 36) * rand(); + dose_size_pt_EMB = dose_size_EMB * weight_pt_EMB; + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + V_F_pt_EMB = [... + V_F_EMB(1); % Vc/F (no IIV) + V_F_EMB(2); % Vp/F (no IIV) + ]; + + rates_pt_EMB = [... + rates_EMB(1) * exp(random('Normal',0,sqrt(log(IIV_IOV_EMB(3)^2+1)))); % CL/F + rates_EMB(2); % Q/F (no IIV) + rates_EMB(3) * exp(random('Normal',0,sqrt(log(IIV_IOV_EMB(1)^2+1)))); % Ka + ]; + + const_other_pt_EMB = [... + const_other_EMB(1) * exp(random('Normal',0,sqrt(log(IIV_IOV_EMB(2)^2+1)))); % MTT + 1 * (1 - const_other_EMB(2) * HIV_pt); % F + ]; + + % Set initial conditions + % -------------------------------------------------------- + dep_IC = 0; + cen_IC = 0; + per_IC = 0; + + % Initialize local storage + dep_loc = zeros([total_timpts_EMB 1]); + cen_loc = zeros([total_timpts_EMB 1]); + per_loc = zeros([total_timpts_EMB 1]); + + for idose = 1:n_doses_EMB + + % Compile parameters + params_EMB = [... + V_F_pt_EMB(1); % Vc/F params(1) + V_F_pt_EMB(2); % Vp/F params(2) + rates_pt_EMB(1); % CL/F params(3) + rates_pt_EMB(2); % Q/F params(4) + rates_pt_EMB(3); % Ka params(5) + const_other_pt_EMB(1); % MTT params(6) + const_other_pt_EMB(2); % F params(7) + dose_size_pt_EMB; % dose params(8) + ]; + + % Call ODE solver + [~, EMB_sol] = ode45(@(t,y) EMB_PlasmaODEs(t,y,params_EMB), ... + time_vec_EMB, [dep_IC cen_IC per_IC], options); + + % Organization of data post ODE + i0 = (idose-1)*(length(time_vec_EMB)-1)+1; + i1 = idose *(length(time_vec_EMB)-1); + + dep_loc(i0:i1) = EMB_sol(1:end-1,1); + cen_loc(i0:i1) = EMB_sol(1:end-1,2) / V_F_pt_EMB(1); + per_loc(i0:i1) = EMB_sol(1:end-1,3) / V_F_pt_EMB(2); + + % Update initial conditions for the next dose + dep_IC = EMB_sol(end,1); + cen_IC = EMB_sol(end,2); + per_IC = EMB_sol(end,3); + + end + + % Store patient results + param_store_EMB(:,ipt) = params_EMB; + EMB_conc_dep( :,ipt) = dep_loc; + EMB_conc_c( :,ipt) = cen_loc; + EMB_conc_p( :,ipt) = per_loc; + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + xvalues_EMB = time_vec_local_EMB; + + plotTimeCourses( ... + 'X', xvalues_EMB, ... + 'Y', EMB_conc_c, ... + 'DrugName', 'EMB', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_EMB, ... + 'DoseInterval', dose_time_EMB, ... + 'FigureName', 'EMB Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_EMB, ... + 'Y', EMB_conc_p, ... + 'DrugName', 'EMB', ... + 'Compartment', 'Peripheral', ... + 'Cycles', n_doses_EMB, ... + 'DoseInterval', dose_time_EMB, ... + 'FigureName', 'EMB Raw Timecourses (Peripheral)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + xvalues_cyc_stats_EMB = time_vec_local_EMB; + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_EMB, ... + 'Y', EMB_conc_c, ... + 'DrugName', 'EMB', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_EMB, ... + 'DoseInterval', dose_time_EMB, ... + 'FigureName', 'EMB Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_EMB, ... + 'Y', EMB_conc_p, ... + 'DrugName', 'EMB', ... + 'Compartment', 'Peripheral', ... + 'Cycles', n_doses_EMB, ... + 'DoseInterval', dose_time_EMB, ... + 'FigureName', 'EMB Stats (Peripheral)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_EMB, ... + 'time_step_size', time_step_size_EMB, ... + 'conc_c', EMB_conc_c, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'EMB steady-state attainment', 'SortBy', 'name'); + + if numBothReached ~= n_pts + warning('EMB: steady state not reached for all patients'); + end + + statsT = table( ... + mean(last24AUC,'omitnan'), std(last24AUC,'omitnan'), ... + mean(last24Cmax,'omitnan'), std(last24Cmax,'omitnan'), ... + 'VariableNames', {'mean_last24AUC','std_last24AUC','mean_last24Cmax','std_last24Cmax'}); + + fprintf('\n=== EMB last-24h summary stats ===\n'); + disp(statsT); + end + + % -------------------- MIC Distributions -------------------- + + if opt.PTAplot + + mac = { + 'M. avium complex'; + {{'0.06', 0}, {'0.125', 0}, {'0.25', 4}, {'0.5', 15}, {'1', 94}, ... + {'2', 253}, {'4', 832}, {'8', 1157}, {'16', 829}, {'32', 374}, ... + {'64', 604}, {'128', 59}} + }; + + kan = { + 'M. kansasii'; + {{'0.06', 0}, {'0.125', 1}, {'0.25', 7}, {'0.5', 2}, {'1', 3}, ... + {'2', 48}, {'4', 54}, {'8', 3}, {'16', 0}, {'32', 0}, {'64', 0}, {'128', 0}} + }; + + tb = { + 'M. tuberculosis'; + {{'0.06', 0}, {'0.125', 0}, {'0.25', 1}, {'0.5', 42}, {'1', 180}, ... + {'2', 43}, {'4', 3}, {'8', 0}, {'16', 0}, {'32', 0}, {'64', 0}, {'128', 0}} + }; + + speciesAggregation = {mac, kan, tb}; + normalizedMICSpeciesAggregation = normalizeMICsOfAggregation(speciesAggregation); + uniqueMICs = getUniqueMICs(normalizedMICSpeciesAggregation); + + + % -------------------- Setting PTA Targets -------------------- + + target1 = { % https://doi.org/10.1128/aac.01355-09 + 'MAC'; + 'Cmax/MIC'; + 1.23; 'avium' + }; + + uniqueTargets = {target1}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix = zeros(length(uniqueMICs), length(uniqueTargets)); + + for i = 1:length(uniqueTargets) + PTAMatrix(:,i) = calculatePTA( ... + last24ConcArray, ... + 'MICDist', uniqueMICs, ... + 'targetType', uniqueTargets{i}{2}, ... + 'target', uniqueTargets{i}{3}, ... + 'time_step_size', time_step_size_EMB, ... + 'last24AUC', last24AUC, ... + 'last24Cmax', last24Cmax); + end + + legendArray = getLegendArray(uniqueTargets, normalizedMICSpeciesAggregation); + + if opt.plotAUC + plotAUC(last24AUC, 'EMB'); + end + + [~, plotArgs] = plotPTAs( ... + uniqueMICs, ... + PTAMatrix, ... + normalizedMICSpeciesAggregation, ... + 'DoseSize', dose_size_EMB, ... % mg/kg; per-patient actual dose varies by sampled weight + 'DoseFrequency', dose_time_EMB, ... + 'DrugName', 'EMB', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray, ... + 'UniqueTargets', uniqueTargets, ... + 'NumPatients', n_pts, ... + 'ShowCI', false); + + end + +end \ No newline at end of file diff --git a/FOX/FOX_PlasmaODEs.m b/FOX/FOX_PlasmaODEs.m new file mode 100644 index 0000000..57d24dd --- /dev/null +++ b/FOX/FOX_PlasmaODEs.m @@ -0,0 +1,81 @@ +%% ======================================================================= +% FOX_PlasmaODEs.m — One-Compartment IV Infusion ODE for Cefoxitin +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the cefoxitin plasma PK model: a single +% central compartment with zero-order IV infusion input and first-order +% elimination. Intended to be called by an ODE solver (ode45) from +% FOX_PopPK. +% +% MODEL +% Reimplementation of Novy et al., 2024. +% Emmanuel Novy et al. "Population pharmacokinetics of prophylactic cefoxitin +% in elective bariatric surgery patients: a prospective monocentric study". +% In: *Anaesthesia Critical Care & Pain Medicine* 43.3 (2024), p. 101376.% +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver. +% y - State vector of drug amounts (mg): +% y(1) = central compartment amount +% params - Parameter vector: +% params(1) = Vc central volume (L) +% params(2) = CL clearance (L/h) +% params(3) = dose for this interval (mg) +% params(4) = infusion duration (h) +% +% OUTPUTS +% dy - Derivative vector (mg/h): +% dy(1) = d(central)/dt +% +% NOTES +% Commented-out three-compartment scaffolding is retained from an earlier +% model variant; the active model is one-compartment. +% +% AUTHORS +% Marina de L. M. Surani +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2024-02-26 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = FOX_PlasmaODEs(t,y,params) + +%%Extracting parameter values from params variable to assign to local variables + +V_c = params(1); +CL = params(2); + +dose = params(3); +infusion_duration = params(4); + +% Infusion rate +if t < infusion_duration + infusion_rate = dose / infusion_duration; +else + infusion_rate = 0; +end + +% d ODE Y value assignmetns +A = [... + y(1) %Drug amount in central compartment + ]; + + +% Define ODEs + +dA1dt = infusion_rate - CL * A(1) / V_c; + + +% ODE Outputs + +dy = [... + dA1dt + ]; + + +end \ No newline at end of file diff --git a/FOX/FOX_PopPK.m b/FOX/FOX_PopPK.m new file mode 100644 index 0000000..ec28796 --- /dev/null +++ b/FOX/FOX_PopPK.m @@ -0,0 +1,362 @@ +%% ======================================================================= +% FOX_PopPK.m — Population PK Simulation & PTA Analysis for Cefoxitin +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the cefoxitin plasma ODEs across a Monte Carlo patient +% population. Models a one-compartment system with zero-order IV infusion +% input and first-order elimination, computes steady-state AUC/Cmax and +% %T>MIC, and generates time-course, AUC, and probability of target +% attainment (PTA) plots. +% +% MODEL +% Reimplementation of Novy et al., 2024. +% Emmanuel Novy et al. "Population pharmacokinetics of prophylactic cefoxitin +% in elective bariatric surgery patients: a prospective monocentric study". +% In: *Anaesthesia Critical Care & Pain Medicine* 43.3 (2024), p. 101376. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (2000) Dose per administration (mg). +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (4) Time between doses (h). +% 'IVDuration' (0.167) Duration of IV infusion (h; ~10 min default). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% +% OUTPUTS +% results - Struct of simulation results (time grid, concentration +% timecourses, last-24h exposure metrics, steady-state +% counts, and PTA/MIC outputs when PTAplot is enabled). +% parameters - Patient-specific parameter samples (one column per patient). +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: Marina de L. M. Surani, 2024-02-26; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2024-02-26 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% FOX_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% ======================================================================== + +function [results, parameters, plotArgs] = FOX_PopPK(varargin) + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 2000, @(x)isnumeric(x)); % mg; 2 g IV q4h or 3 g IV q6h + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 4, @(x)isnumeric(x)); % hours; 2 g q4h default + p.addParameter('IVDuration', 0.167, @(x)isnumeric(x)); % hours; 10 min default + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.parse(varargin{:}); + opt = p.Results; + + neq = 1; + options = odeset('NonNegative', 1:neq); + + + + % % -------------------- Parameter Values -------------------- + V_mean = 23.4; %L + CL_mean = 10.9; %L/h + CL_IIV_CV = 0.563; + V_IIV_CV = 0.448; + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify dose size (mg) + dose_size_FOX = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_FOX = opt.NumDoses; + + % Specify dose interval (hours) + dose_time_FOX = opt.DoseInterval; + + % Specify IV infusion duration (hours) + IV_dose_duration_FOX = opt.IVDuration; + + % Specify time step and build time vectors + time_step_size_FOX = 0.5; + + time_vec_FOX = 0 : time_step_size_FOX : dose_time_FOX; + time_vec_local_FOX = 0 : time_step_size_FOX : dose_time_FOX * n_doses_FOX - time_step_size_FOX; + total_timpts_FOX = n_doses_FOX * (length(time_vec_FOX) - 1); + + % -------------------- Initialize Matrices -------------------- + + % param_store_FOX = zeros([total_params_FOX n_pts]); + FOX_conc_c = zeros([total_timpts_FOX n_pts]); + + + % -------------------- ODE Solving -------------------- + + for ipt = 1:n_pts + + V_pt = V_mean * exp(random('Normal',0,sqrt(log(V_IIV_CV(1)^2+1)))); + CL_pt = CL_mean * exp(random('Normal',0,sqrt(log(CL_IIV_CV(1)^2+1)))); + + % Set initial conditions + % -------------------------------------------------------- + cen_IC = 0; + + % Initialize local storage + cen_loc = zeros([total_timpts_FOX 1]); + + for idose = 1:n_doses_FOX + + % Compile parameters + params_FOX = [V_pt, CL_pt, dose_size_FOX, IV_dose_duration_FOX]; + + % Call ODE solver + [~, FOX_sol] = ode45(@(t,y) FOX_PlasmaODEs(t,y,params_FOX), ... + time_vec_FOX, [cen_IC], options); + + % Organization of data post ODE + i0 = (idose-1)*(length(time_vec_FOX)-1)+1; + i1 = idose *(length(time_vec_FOX)-1); + + cen_loc(i0:i1) = FOX_sol(1:end-1,1) / V_pt; + + % Update initial conditions for the next dose + cen_IC = FOX_sol(end,1); + + end + + % Store patient results + param_store_FOX(:,ipt) = params_FOX; + FOX_conc_c( :,ipt) = cen_loc; + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + xvalues_FOX = time_vec_local_FOX; + + plotTimeCourses( ... + 'X', xvalues_FOX, ... + 'Y', FOX_conc_c, ... + 'DrugName', 'FOX', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_FOX, ... + 'DoseInterval', dose_time_FOX, ... + 'FigureName', 'FOX Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'YLabel', 'Free plasma drug concentration [mg/L]', ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + xvalues_cyc_stats_FOX = time_vec_local_FOX; + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_FOX, ... + 'Y', FOX_conc_c, ... + 'DrugName', 'FOX', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_FOX, ... + 'DoseInterval', dose_time_FOX, ... + 'FigureName', 'FOX Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'YLabel', 'Free plasma drug concentration [mg/L]', ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_FOX, ... + 'time_step_size', time_step_size_FOX, ... + 'conc_c', FOX_conc_c, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'FOX steady-state attainment', 'SortBy', 'name'); + + if numBothReached ~= n_pts + warning('FOX: steady state not reached for all patients'); + end + + statsT = table( ... + mean(last24AUC,'omitnan'), std(last24AUC,'omitnan'), ... + median(last24AUC,'omitnan'), ... + mean(last24Cmax,'omitnan'), std(last24Cmax,'omitnan'), ... + 'VariableNames', {'mean_last24AUC','std_last24AUC','median_last24AUC','mean_last24Cmax','std_last24Cmax'}); + + fprintf('\n=== FOX last-24h summary stats ===\n'); + disp(statsT); + end + + if opt.plotAUC + plotAUC(last24AUC, 'FOX'); + end + + + % -------------------- MIC Distributions -------------------- + + if opt.PTAplot + + % MIC distributions updated to start at MIC = 1 per Vero request 01Dec2025 + MAB = { + 'M. abscessus'; + {{'1', 0}, {'2', 0}, {'4', 0}, {'8', 2.99}, {'16', 21.26}, ... + {'32', 68.42}, {'64', 100.75}, {'128', 80.59}, {'>128', 89.98}} + }; + + MAC = { + 'M. avium complex'; + {{'1', 0}, {'2', 0}, {'4', 0}, {'8', 0}, {'16', 0}, ... + {'32', 0}, {'64', 1}, {'128', 7}, {'>128', 138}} + }; + + speciesAggregation = {MAB, MAC}; + normalizedMICSpeciesAggregation = normalizeMICsOfAggregation(speciesAggregation); + uniqueMICs = getUniqueMICs(normalizedMICSpeciesAggregation); + + + % -------------------- Setting PTA Targets -------------------- + + target1 = { % https://doi.org/10.1111/bcp.14883 + ''; + '%T>MIC'; + 40; 'unk' + }; + + target2 = { + ''; + '%T>MIC'; + 50; 'unk' + }; + + target3 = { + ''; + '%T>MIC'; + 70; 'unk' + }; + + uniqueTargets = {target1, target2, target3}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix = zeros(length(uniqueMICs), length(uniqueTargets)); + + for i = 1:length(uniqueTargets) + PTAMatrix(:,i) = calculatePTA( ... + last24ConcArray, ... + 'MICDist', uniqueMICs, ... + 'targetType', uniqueTargets{i}{2}, ... + 'target', uniqueTargets{i}{3}, ... + 'time_step_size', time_step_size_FOX, ... + 'last24AUC', last24AUC, ... + 'last24Cmax', last24Cmax); + end + + legendArray = getLegendArray(uniqueTargets, normalizedMICSpeciesAggregation); + + if opt.plotAUC + plotAUC(last24AUC, 'FOX'); + end + + [~, plotArgs] = plotPTAs( ... + uniqueMICs, ... + PTAMatrix, ... + normalizedMICSpeciesAggregation, ... + 'DoseSize', dose_size_FOX, ... + 'DoseFrequency', dose_time_FOX, ... + 'DrugName', 'FOX', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray, ... + 'UniqueTargets', uniqueTargets, ... + 'NumPatients', n_pts, ... + 'ShowCI', false); + + end + + % -------------------- Package Results and Parameters -------------------- + + % Patient-specific parameter samples (one column per patient), + % mirroring how BDQ_PopPK returns params_store_BDQ. + parameters = param_store_FOX(:,:); + + % Bundle simulated concentrations, time grid, and exposure / steady-state + % metrics into a results struct for the caller. + results = struct(); + + % Time grid + results.time_vec_local = time_vec_local_FOX; + results.time_step_size = time_step_size_FOX; + + % Concentration timecourses (rows = timepoints, cols = patients) + results.conc_central = FOX_conc_c; + + % Last-24h exposure metrics + results.last24ConcArray = last24ConcArray; + results.last24AUC = last24AUC; + results.last24Cmax = last24Cmax; + + % Steady-state attainment counts + results.numAUCSSReached = numAUCSSReached; + results.numCmaxSSReached = numCmaxSSReached; + results.numBothReached = numBothReached; + + % PTA / MIC outputs only exist if PTAplot ran — guard accordingly + if opt.PTAplot + results.uniqueMICs = uniqueMICs; + results.uniqueTargets = uniqueTargets; + results.normalizedMICSpeciesAggregation = normalizedMICSpeciesAggregation; + results.PTAMatrix = PTAMatrix; + results.legendArray = legendArray; + end + +end \ No newline at end of file diff --git a/IMI/IMI_PlasmaODEs.m b/IMI/IMI_PlasmaODEs.m new file mode 100644 index 0000000..865c024 --- /dev/null +++ b/IMI/IMI_PlasmaODEs.m @@ -0,0 +1,109 @@ +%% ======================================================================= +% IMI_PlasmaODEs.m — Two-Compartment IV Infusion ODEs for Imipenem +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the imipenem plasma PK model: a central +% compartment with zero-order IV infusion input, exchange with one +% peripheral compartment, and first-order elimination from the central +% compartment. Intended to be called by an ODE solver (ode45) from +% IMI_PopPK. +% +% MODEL +% ODEs as described in Chen et al., 2020. +% Wenqian Chen et al. "Imipenem population pharmacokinetics: therapeutic +% drug monitoring data cellected in critically ill patients with or without +% extracorporeal membrane oxygenation". InL *Antimicrobial agents and +% chemotherapy* 64.5 (2020), pp. 10-1128. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver. +% y - State vector of drug amounts (mg): +% y(1) = central compartment amount +% y(2) = peripheral compartment amount +% params - Parameter vector: +% params(1) = Vc/F central volume (L) +% params(2) = Vp/F peripheral volume (L) +% params(3) = CL/F clearance (L/h) +% params(4) = Q/F intercompartmental clearance (L/h) +% params(5) = dose dose for this interval (mg) +% params(6) = infusion duration (h) +% +% OUTPUTS +% dy - Derivative vector (mg/h): +% dy(1) = d(central)/dt +% dy(2) = d(peripheral)/dt +% +% AUTHORS +% Marina de L. M. Surani +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2024-01-24 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = IMI_PlasmaODEs(t,y,params) + +% Extracting parameter values from params variable to assign to local variables + +% Departmental constants +V_Fs = [... + params(1) %(L) Central compartment Vc/F + params(2) %(L) Peripheral compartment Vp/F + ]; +rates = [... + params(3) % (L/h) CL/F Oral Clearance + params(4) % (L/h) Q/F Intercompartmental Clearance + ]; + +% Dose related variables +dose = params(5); %(mg) dose size +doseDuration = params(6); % (h) + +% Infusion rate +if t < doseDuration + infusion_rate = dose / doseDuration; %mg/h +else + infusion_rate = 0; +end + +% ODE Y value assignmetns +A = [... + y(1) %Drug amount in central compartment + y(2) %Drug amount in peripheral compartment + ]; + %y(1) %Drug amount in depot compartment + +% Define ODEs + +% ODE around CENTRAL compartment (an IV drug so the central comp. is also +% depot) +% ------------------------------------------ +% ODE_1 terms +dA1_term1 = - rates(2) * A(1) / V_Fs(1); %Flow out to Peripheral compartment +dA1_term2 = rates(2) * A(2) / V_Fs(2); %Flow in from peripheral to central +dA1_term3 = - rates(1) * A(1) / V_Fs(1); %Clearance of drug from system + +dA1dt = infusion_rate + (dA1_term1 + dA1_term2 + dA1_term3); %(mg/h) + +% ODE around PERIPHERAL compartment +% ------------------------------------------ +% ODE_2 terms +dA2_term1 = rates(2) * A(1) / V_Fs(1); %In term +dA2_term2 = - rates(2) * A(2) / V_Fs(2); %Out term + +dA2dt = (dA2_term1 + dA2_term2); %(mg/h) + + +% ODE Outputs + +dy = [... + dA1dt + dA2dt + ]; + + +end \ No newline at end of file diff --git a/IMI/IMI_PopPK.m b/IMI/IMI_PopPK.m new file mode 100644 index 0000000..c8af482 --- /dev/null +++ b/IMI/IMI_PopPK.m @@ -0,0 +1,415 @@ +%% ======================================================================= +% IMI_PopPK.m — Population PK Simulation & PTA Analysis for Imipenem +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the imipenem plasma ODEs across a Monte Carlo patient +% population. Models a two-compartment system with zero-order IV infusion +% input and first-order elimination, computes steady-state AUC/Cmax and +% %T>MIC, and generates time-course, AUC, prediction-corrected visual +% predictive check (pcVPC), and probability of target attainment (PTA) +% plots. +% +% MODEL +% Reimplementation of Chen et al., 2020. +% Wenqian Chen et al. "Imipenem population pharmacokinetics: therapeutic +% drug monitoring data cellected in critically ill patients with or without +% extracorporeal membrane oxygenation". InL *Antimicrobial agents and +% chemotherapy* 64.5 (2020), pp. 10-1128. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (1000) Dose per administration (mg). +% 'NumDoses' (40) Total number of doses. +% 'DoseInterval' (12) Time between doses (h). +% 'IVDuration' (3) Duration of IV infusion (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile time courses and pcVPC. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: Marina de L. M. Surani, 2024-01-24; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2024-01-24 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% IMI_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% ======================================================================== + + +function [plotArgs] = IMI_PopPK(varargin) + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 1000, @(x)isnumeric(x)); % mg + p.addParameter('NumDoses', 40, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 12, @(x)isnumeric(x)); % hours + p.addParameter('IVDuration', 3, @(x)isnumeric(x)); % hours + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Volume parameters + % ------------------------------------------ + V_F_IMI = [... + 20.5 % (L) Vc/F + 8.86 % (L) Vp/F + ]; + + % Clearance constants + % ------------------------------------------ + rates_IMI = [... + 8.88 % (L/h) CL/F Overall clearance + 1.74 % (L/h) Q/F Apparent intercompartmental clearance + ]; + + % Covariates + % ------------------------------------------ + covariates_IMI = [... + 0.295 % theta_CLCR_CL + 0.306 % theta_Weight + ]; %#ok + + % Interindividual variability (% CV) + % ------------------------------------------ + IIV_IMI = [... + 0.177 % CL (CV) IIV + 0.148 % Vc (CV) IIV + 0 % Q (CV) IIV (fixed) + 0 % Vp (CV) IIV (fixed) + ]; + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify dose size (mg) + dose_size_IMI = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_IMI = opt.NumDoses; + + % Specify dose interval (hours) + dose_time_IMI = opt.DoseInterval; + + % Specify IV infusion duration (hours) + IV_dose_duration_IMI = opt.IVDuration; + + % Specify time step and build time vectors + time_step_size_IMI = 0.5; + + time_vec_IMI = 0 : time_step_size_IMI : dose_time_IMI; + time_vec_local_IMI = 0 : time_step_size_IMI : dose_time_IMI * n_doses_IMI - time_step_size_IMI; + total_timpts_IMI = n_doses_IMI * (length(time_vec_IMI) - 1); + total_params_IMI = 6; + + + % -------------------- Initialize Matrices -------------------- + + param_store_IMI = zeros([total_params_IMI n_pts]); + IMI_conc_c = zeros([total_timpts_IMI n_pts]); + IMI_conc_p = zeros([total_timpts_IMI n_pts]); + + + % -------------------- ODE Solving -------------------- + + for ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + V_F_pt_IMI = [... + V_F_IMI(1) * exp(random('Normal',0,sqrt(log(IIV_IMI(2)^2+1)))); % Vc/F + V_F_IMI(2); % Vp/F (no IIV) + ]; + + rates_pt_IMI = [... + rates_IMI(1) * exp(random('Normal',0,sqrt(log(IIV_IMI(1)^2+1)))); % CL/F + rates_IMI(2); % Q/F (no IIV) + ]; + + % Set initial conditions + % -------------------------------------------------------- + cen_IC = 0; + per_IC = 0; + + % Initialize local storage + cen_loc = zeros([total_timpts_IMI 1]); + per_loc = zeros([total_timpts_IMI 1]); + + for idose = 1:n_doses_IMI + + % Compile parameters + params_IMI = [... + V_F_pt_IMI(1); % Vc/F params(1) + V_F_pt_IMI(2); % Vp/F params(2) + rates_pt_IMI(1); % CL/F params(3) + rates_pt_IMI(2); % Q/F params(4) + dose_size_IMI; % dose size params(5) + IV_dose_duration_IMI; % IV duration params(6) + ]; + + % Call ODE solver + [~, IMI_sol] = ode45(@(t,y) IMI_PlasmaODEs(t,y,params_IMI), ... + time_vec_IMI, [cen_IC per_IC]); + + % Organization of data post ODE + i0 = (idose-1)*(length(time_vec_IMI)-1)+1; + i1 = idose *(length(time_vec_IMI)-1); + + cen_loc(i0:i1) = IMI_sol(1:end-1,1) / V_F_pt_IMI(1); + per_loc(i0:i1) = IMI_sol(1:end-1,2) / V_F_pt_IMI(2); + + % Update initial conditions for the next dose + cen_IC = IMI_sol(end,1); + per_IC = IMI_sol(end,2); + + end + + % Store patient results + param_store_IMI(:,ipt) = params_IMI; + IMI_conc_c( :,ipt) = cen_loc; + IMI_conc_p( :,ipt) = per_loc; + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + xvalues_IMI = time_vec_local_IMI; + + plotTimeCourses( ... + 'X', xvalues_IMI, ... + 'Y', IMI_conc_c, ... + 'DrugName', 'IMI', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_IMI, ... + 'DoseInterval', dose_time_IMI, ... + 'FigureName', 'IMI Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_IMI, ... + 'Y', IMI_conc_p, ... + 'DrugName', 'IMI', ... + 'Compartment', 'Peripheral', ... + 'Cycles', n_doses_IMI, ... + 'DoseInterval', dose_time_IMI, ... + 'FigureName', 'IMI Raw Timecourses (Peripheral)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + xvalues_cyc_stats_IMI = time_vec_local_IMI; + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_IMI, ... + 'Y', IMI_conc_c, ... + 'DrugName', 'IMI', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_IMI, ... + 'DoseInterval', dose_time_IMI, ... + 'FigureName', 'IMI Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_cyc_stats_IMI, ... + 'Y', IMI_conc_p, ... + 'DrugName', 'IMI', ... + 'Compartment', 'Peripheral', ... + 'Cycles', n_doses_IMI, ... + 'DoseInterval', dose_time_IMI, ... + 'FigureName', 'IMI Stats (Peripheral)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + % pcVPC diagnostic plot (IMI-specific) + bin_edges = 0 : 1 : max(time_vec_local_IMI); + bin_centers = bin_edges(1:end-1) + 0.5; + nBins = length(bin_centers); + + p2_5 = zeros(nBins, 1); + p10 = zeros(nBins, 1); + p50 = zeros(nBins, 1); + p90 = zeros(nBins, 1); + p97_5 = zeros(nBins, 1); + + for i = 1:nBins + bin_mask = time_vec_local_IMI >= bin_edges(i) & time_vec_local_IMI < bin_edges(i+1); + flat_data = IMI_conc_c(bin_mask, :); + flat_data = flat_data(:); + if ~isempty(flat_data) + p2_5(i) = prctile(flat_data, 2.5); + p10(i) = prctile(flat_data, 10); + p50(i) = prctile(flat_data, 50); + p90(i) = prctile(flat_data, 90); + p97_5(i) = prctile(flat_data, 97.5); + else + p2_5(i) = NaN; p10(i) = NaN; p50(i) = NaN; + p90(i) = NaN; p97_5(i) = NaN; + end + end + + pcVPC_fig = figure; + set(pcVPC_fig, 'WindowStyle', 'docked'); + hold on; + fill([bin_centers fliplr(bin_centers)], [p2_5' fliplr(p97_5')], ... + [0.9 0.9 0.9], 'EdgeColor', 'none'); + plot(bin_centers, p50, 'k-', 'LineWidth', 2); + plot(bin_centers, p10, 'k--', 'LineWidth', 1.5); + plot(bin_centers, p90, 'k--', 'LineWidth', 1.5); + set(gca, 'YScale', 'log'); + xlabel('Time (h)'); + ylabel('Concentration (mg/L)'); + title('pcVPC of Simulated Imipenem (Last 24h, Log Scale)'); + legend('95% Prediction Interval', 'Median', '10th / 90th Percentiles'); + x_end = max(time_vec_local_IMI); + xlim([x_end - 24, x_end]); + ylim([0.01, max(p97_5) * 1.2]); + grid on; + box on; + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_IMI, ... + 'time_step_size', time_step_size_IMI, ... + 'conc_c', IMI_conc_c, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'IMI steady-state attainment', 'SortBy', 'name'); + + if numBothReached ~= n_pts + warning('IMI: steady state not reached for all patients'); + end + + statsT = table( ... + mean(last24AUC,'omitnan'), std(last24AUC,'omitnan'), ... + mean(last24Cmax,'omitnan'), std(last24Cmax,'omitnan'), ... + 'VariableNames', {'mean_last24AUC','std_last24AUC','mean_last24Cmax','std_last24Cmax'}); + + fprintf('\n=== IMI last-24h summary stats ===\n'); + disp(statsT); + end + + if opt.plotAUC + plotAUC(last24AUC, 'IMI'); + end + + + % -------------------- MIC Distributions -------------------- + + if opt.PTAplot + + MAB = { + 'M. abscessus'; + {{'0.06', 0}, {'0.125', 0}, {'0.25', 0}, {'0.5', 0}, {'1', 1.23}, ... + {'2', 5.98}, {'4', 27.06}, {'8', 55.47}, {'16', 44.76}, ... + {'32', 35.83}, {'64', 35.96}, {'>64', 151.72}} + }; + + speciesAggregation = {MAB}; + normalizedMICSpeciesAggregation = normalizeMICsOfAggregation(speciesAggregation); + uniqueMICs = getUniqueMICs(normalizedMICSpeciesAggregation); + + + % -------------------- Setting PTA Targets -------------------- + + target1 = { % https://doi.org/10.1128/aac.00385-20 + ''; + '%T>MIC'; + 50; 'unk' + }; + + uniqueTargets = {target1}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix = zeros(length(uniqueMICs), length(uniqueTargets)); + + for i = 1:length(uniqueTargets) + PTAMatrix(:,i) = calculatePTA( ... + last24ConcArray, ... + 'MICDist', uniqueMICs, ... + 'targetType', uniqueTargets{i}{2}, ... + 'target', uniqueTargets{i}{3}, ... + 'time_step_size', time_step_size_IMI, ... + 'last24AUC', last24AUC, ... + 'last24Cmax', last24Cmax); + end + + legendArray = getLegendArray(uniqueTargets, normalizedMICSpeciesAggregation); + + [~, plotArgs] = plotPTAs( ... + uniqueMICs, ... + PTAMatrix, ... + normalizedMICSpeciesAggregation, ... + 'DoseSize', dose_size_IMI, ... + 'DoseFrequency', dose_time_IMI, ... + 'DrugName', 'IMI', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray, ... + 'UniqueTargets', uniqueTargets, ... + 'NumPatients', n_pts, ... + 'ShowCI', false); + + end + +end \ No newline at end of file diff --git a/LZD/LZD_PlasmaODEs.m b/LZD/LZD_PlasmaODEs.m new file mode 100644 index 0000000..6d69303 --- /dev/null +++ b/LZD/LZD_PlasmaODEs.m @@ -0,0 +1,117 @@ +%% ======================================================================= +% LZD_PlasmaODEs.m — Transit-Absorption / One-Compartment ODEs for +% Linezolid +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the linezolid plasma PK model: a depot +% feeding a five-compartment transit chain into a gut compartment, which +% is absorbed first-order (with bioavailability F) into a one-compartment +% central disposition with first-order elimination. Intended to be called +% by a stiff ODE solver (ode15s) from LZD_PopPK. +% +% MODEL +% ODEs as described in Abdelwahab et al., 2021. +% Mahmoud Tareq Abdelwahab et al. "Linezolid population pharmacokinetics +% in South African adults with drug-resistant tuberculosis". +% In: *Antimicrobial agents and chemotherapy* 65.12 (2021), pp. 10-1128. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver. +% y - State vector (8 x 1): +% y(1) = depot drug mass (q0) +% y(2) = transit 1 mass (q1) +% y(3) = transit 2 mass (q2) +% y(4) = transit 3 mass (q3) +% y(5) = transit 4 mass (q4) +% y(6) = transit 5 mass (q5) +% y(7) = gut compartment mass +% y(8) = central compartment amount (C) +% params - Parameter vector: +% params(1) = V central volume (L) +% params(2) = CL/F clearance (L/h) +% params(3) = ka absorption rate constant (1/h) +% params(4) = MTT mean transit time (h) +% params(5) = F bioavailability +% params(6) = dose dose for this interval (mg) [unused here; +% dosing applied via initial conditions] +% +% OUTPUTS +% dy - Derivative vector (8 x 1) matching the state ordering in y. +% +% NOTES +% The transit rate constant is computed internally as ktr = (n+1)/MTT +% with n = 5 transit compartments. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2025-09-12 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = LZD_PlasmaODEs(t,y,params) + + % Extracting parameter values from params variable to assign to local variables + + % Departmental constants + V_Fs = params(1); %(L) Central compartment Vc/F + + CL = params (2); % CL/F L/h Oral Clearance + ka = params(3); % Ka Absorption rate + + + params_other = [... + params(4) % MTT Mean transit time + params(5) % F Bioavailability + ]; + + n = 5; + + % Transit rate between transit compartment + ktr = (n + 1) / params_other(1); + + % ODE Y value assignmetns + A = [... + y(1) %(q0) drug mass in depot + y(2) %(q1) drug mass in transit 1 compartment + y(3) %(q2) drug mass in transit 2 compartment + y(4) %(q3) drug mass in transit 3 compartment + y(5) %(q4) drug mass in transit 4 compartment + y(6) %(q5) drug mass in transit 5 compartment + y(7) %(gut) + y(8) %(C) drug mass in central + ]; + + % Define ODEs + % Depot and transit chain + dA_depdt = -ktr * A(1); + dA_tr1dt = ktr * (A(1) - A(2)); + dA_tr2dt = ktr * (A(2) - A(3)); + dA_tr3dt = ktr * (A(3) - A(4)); + dA_tr4dt = ktr * (A(4) - A(5)); + dA_tr5dt = ktr * (A(5) - A(6)); + + % Gut absorption compartment (gain from last transit, loss to central) + dA_gutdt = ktr * A(6) - ka * A(7); + + % Central (gain via first-order absorption with F; loss via linear elim) + dA_cendt = params_other(2) * ka * A(7) - (CL / V_Fs) * A(8); + + % ODE Outputs (8 states; order matches A) + dy = [... + dA_depdt + dA_tr1dt + dA_tr2dt + dA_tr3dt + dA_tr4dt + dA_tr5dt + dA_gutdt + dA_cendt + ]; + +end \ No newline at end of file diff --git a/LZD/LZD_PopPK.m b/LZD/LZD_PopPK.m new file mode 100644 index 0000000..4b2e592 --- /dev/null +++ b/LZD/LZD_PopPK.m @@ -0,0 +1,557 @@ +%% ======================================================================= +% LZD_PopPK.m — Population PK Simulation & PTA Analysis for Linezolid +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and inter-occasion variability (IOV) and solves the linezolid plasma +% ODEs across a Monte Carlo patient population. Models a one-compartment +% disposition system fed by a five-compartment transit-absorption chain +% plus a gut compartment, computes steady-state AUC/Cmax, and generates +% time-course, AUC, and probability of target attainment (PTA) plots. +% +% A parfor (parallelized) path and a serial path are provided; select via +% the 'Parallelize' option. Both paths are functionally equivalent. +% +% MODEL +% Reimplementation of Abdelwahab et al., 2021. +% Mahmoud Tareq Abdelwahab et al. "Linezolid population pharmacokinetics +% in South African adults with drug-resistant tuberculosis". +% In: *Antimicrobial agents and chemotherapy* 65.12 (2021), pp. 10-1128. +% +% STRUCTURE +% 8-state model: depot, 5 transit compartments, gut, and central. Solved +% with ode15s (stiff) under a non-negativity constraint and MaxStep 0.05. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (600) Dose per administration (mg). +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (12) Time between doses (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% 'Parallelize' (true) Use parfor over patients. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: EB and T. J. Shoaf, 2024; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2024 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% LZD_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% Parallel Computing Toolbox required when 'Parallelize' is true. +% ======================================================================== + + +function [plotArgs] = LZD_PopPK(varargin) + + % Initialize output + plotArgs = []; + + % Set the ODE solver to not allow negative numbers + neq = 8; + options = odeset('NonNegative', 1:neq, 'MaxStep', 0.05); + + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 600, @(x)isnumeric(x)); + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 12, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.addParameter('Parallelize', true, @(b)islogical(b)&&isscalar(b)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Volume parameters + % ------------------------------------------ + V_LZD = 40.2; % (L) Central compartment volume + + % Clearance constants + % ------------------------------------------ + CL_LZD = 3.57; % (L/h) Clearance from central compartment + + % Rate constants + % ------------------------------------------ + k_a_LZD = 1.22; % (1/h) First-order absorption rate constant + MTT_LZD = 0.528; % (h) Absorption mean transit time + F_LZD = 1; % (-) Bioavailability + + % Interindividual variability (% CV) + % ------------------------------------------ + CV_IIV_LZD = 0.371; % CL (% CV) IIV + + % Interoccasion variability (% CV) + % ------------------------------------------ + CV_IOV_LZD = [... + 0.568 % MTT (CV(1)) + 0.785 % ka (CV(2)) + 0.220 % F (CV(3)) + ]; + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify how much drug is given in each dose (mg) + dose_size_LZD = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_LZD = opt.NumDoses; + + % Specify time (hours) between doses + dose_time_LZD = opt.DoseInterval; + + % Specify time step and build time vectors + time_step_size_LZD = 0.5; + + time_vec_LZD = 0 : time_step_size_LZD : dose_time_LZD; + total_timpts_LZD = n_doses_LZD * (length(time_vec_LZD) - 1); + time_vec_local_LZD = 0 : time_step_size_LZD : dose_time_LZD * n_doses_LZD - time_step_size_LZD; + + + % -------------------- Initialize Matrices -------------------- + + LZD_conc_dep = zeros([total_timpts_LZD n_pts]); + LZD_conc_tr1 = zeros([total_timpts_LZD n_pts]); + LZD_conc_tr2 = zeros([total_timpts_LZD n_pts]); + LZD_conc_tr3 = zeros([total_timpts_LZD n_pts]); + LZD_conc_tr4 = zeros([total_timpts_LZD n_pts]); + LZD_conc_tr5 = zeros([total_timpts_LZD n_pts]); + LZD_conc_gut = zeros([total_timpts_LZD n_pts]); + LZD_conc_cen = zeros([total_timpts_LZD n_pts]); + + + % -------------------- ODE Solving -------------------- + + % Parallel path (default) + if opt.Parallelize + + parfor ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + CL_ipt = CL_LZD * exp(random('Normal', 0, sqrt(log(CV_IIV_LZD^2 + 1)))); + + % Set initial conditions + % -------------------------------------------------------- + dep_IC = dose_size_LZD; + tr1_IC = 0; tr2_IC = 0; tr3_IC = 0; + tr4_IC = 0; tr5_IC = 0; gut_IC = 0; + cen_IC = 0; + + % Initialize local storage + dep_loc = zeros([total_timpts_LZD 1]); + tr1_loc = zeros([total_timpts_LZD 1]); + tr2_loc = zeros([total_timpts_LZD 1]); + tr3_loc = zeros([total_timpts_LZD 1]); + tr4_loc = zeros([total_timpts_LZD 1]); + tr5_loc = zeros([total_timpts_LZD 1]); + gut_loc = zeros([total_timpts_LZD 1]); + cen_loc = zeros([total_timpts_LZD 1]); + + for idose = 1:n_doses_LZD + + % Apply IOV per dose to ka, MTT, and F + params_LZD = [... + V_LZD % params(1) V + CL_ipt % params(2) CL/F + k_a_LZD * exp(random('Normal', 0, sqrt(log(CV_IOV_LZD(2)^2 + 1)))) % params(3) ka + MTT_LZD * exp(random('Normal', 0, sqrt(log(CV_IOV_LZD(1)^2 + 1)))) % params(4) MTT + F_LZD * exp(random('Normal', 0, sqrt(log(CV_IOV_LZD(3)^2 + 1)))) % params(5) F + dose_size_LZD % params(6) dose size + ]; + + % Call ODE solver + % --------------- + [~, sol] = ode15s(@(t,y) LZD_PlasmaODEs(t, y, params_LZD), ... + time_vec_LZD, ... + [dep_IC tr1_IC tr2_IC tr3_IC tr4_IC tr5_IC gut_IC cen_IC], ... + options); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_LZD) - 1) + 1; + i1 = idose * (length(time_vec_LZD) - 1); + + dep_loc(i0:i1) = sol(1:end-1, 1); + tr1_loc(i0:i1) = sol(1:end-1, 2); + tr2_loc(i0:i1) = sol(1:end-1, 3); + tr3_loc(i0:i1) = sol(1:end-1, 4); + tr4_loc(i0:i1) = sol(1:end-1, 5); + tr5_loc(i0:i1) = sol(1:end-1, 6); + gut_loc(i0:i1) = sol(1:end-1, 7); + cen_loc(i0:i1) = sol(1:end-1, 8) * (1 / V_LZD); + + % Update initial conditions for next dose + dep_IC = sol(end, 1) + dose_size_LZD; + tr1_IC = sol(end, 2); tr2_IC = sol(end, 3); tr3_IC = sol(end, 4); + tr4_IC = sol(end, 5); tr5_IC = sol(end, 6); gut_IC = sol(end, 7); + cen_IC = sol(end, 8); + + end + + % Store patient results + LZD_conc_dep(:, ipt) = dep_loc; + LZD_conc_tr1(:, ipt) = tr1_loc; + LZD_conc_tr2(:, ipt) = tr2_loc; + LZD_conc_tr3(:, ipt) = tr3_loc; + LZD_conc_tr4(:, ipt) = tr4_loc; + LZD_conc_tr5(:, ipt) = tr5_loc; + LZD_conc_gut(:, ipt) = gut_loc; + LZD_conc_cen(:, ipt) = cen_loc; + + end + + % Serial path + else + + for ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + CL_ipt = CL_LZD * exp(random('Normal', 0, sqrt(log(CV_IIV_LZD^2 + 1)))); + + % Set initial conditions + % -------------------------------------------------------- + dep_IC = dose_size_LZD; + tr1_IC = 0; tr2_IC = 0; tr3_IC = 0; + tr4_IC = 0; tr5_IC = 0; gut_IC = 0; + cen_IC = 0; + + % Initialize local storage + dep_loc = zeros([total_timpts_LZD 1]); + tr1_loc = zeros([total_timpts_LZD 1]); + tr2_loc = zeros([total_timpts_LZD 1]); + tr3_loc = zeros([total_timpts_LZD 1]); + tr4_loc = zeros([total_timpts_LZD 1]); + tr5_loc = zeros([total_timpts_LZD 1]); + gut_loc = zeros([total_timpts_LZD 1]); + cen_loc = zeros([total_timpts_LZD 1]); + + for idose = 1:n_doses_LZD + + % Apply IOV per dose to ka, MTT, and F + params_LZD = [... + V_LZD % params(1) V + CL_ipt % params(2) CL/F + k_a_LZD * exp(random('Normal', 0, sqrt(log(CV_IOV_LZD(2)^2 + 1)))) % params(3) ka + MTT_LZD * exp(random('Normal', 0, sqrt(log(CV_IOV_LZD(1)^2 + 1)))) % params(4) MTT + F_LZD * exp(random('Normal', 0, sqrt(log(CV_IOV_LZD(3)^2 + 1)))) % params(5) F + dose_size_LZD % params(6) dose size + ]; + + % Call ODE solver + % --------------- + [~, sol] = ode15s(@(t,y) LZD_PlasmaODEs(t, y, params_LZD), ... + time_vec_LZD, ... + [dep_IC tr1_IC tr2_IC tr3_IC tr4_IC tr5_IC gut_IC cen_IC], ... + options); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_LZD) - 1) + 1; + i1 = idose * (length(time_vec_LZD) - 1); + + dep_loc(i0:i1) = sol(1:end-1, 1); + tr1_loc(i0:i1) = sol(1:end-1, 2); + tr2_loc(i0:i1) = sol(1:end-1, 3); + tr3_loc(i0:i1) = sol(1:end-1, 4); + tr4_loc(i0:i1) = sol(1:end-1, 5); + tr5_loc(i0:i1) = sol(1:end-1, 6); + gut_loc(i0:i1) = sol(1:end-1, 7); + cen_loc(i0:i1) = sol(1:end-1, 8) * (1 / V_LZD); + + % Update initial conditions for next dose + dep_IC = sol(end, 1) + dose_size_LZD; + tr1_IC = sol(end, 2); tr2_IC = sol(end, 3); tr3_IC = sol(end, 4); + tr4_IC = sol(end, 5); tr5_IC = sol(end, 6); gut_IC = sol(end, 7); + cen_IC = sol(end, 8); + + end + + % Store patient results + LZD_conc_dep(:, ipt) = dep_loc; + LZD_conc_tr1(:, ipt) = tr1_loc; + LZD_conc_tr2(:, ipt) = tr2_loc; + LZD_conc_tr3(:, ipt) = tr3_loc; + LZD_conc_tr4(:, ipt) = tr4_loc; + LZD_conc_tr5(:, ipt) = tr5_loc; + LZD_conc_gut(:, ipt) = gut_loc; + LZD_conc_cen(:, ipt) = cen_loc; + + end + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + cycles_LZD = n_doses_LZD; + xvalues_LZD = time_vec_local_LZD; + + plotTimeCourses( ... + 'X', xvalues_LZD, ... + 'Y', LZD_conc_dep, ... + 'DrugName', 'LZD', ... + 'Compartment', 'Depot', ... + 'Cycles', cycles_LZD, ... + 'DoseInterval', dose_time_LZD, ... + 'FigureName', 'LZD Raw Timecourses (Depot)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_LZD, ... + 'Y', LZD_conc_tr1, ... + 'DrugName', 'LZD', ... + 'Compartment', 'Transit 1', ... + 'Cycles', cycles_LZD, ... + 'DoseInterval', dose_time_LZD, ... + 'FigureName', 'LZD Raw Timecourses (Transit 1)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_LZD, ... + 'Y', LZD_conc_tr2, ... + 'DrugName', 'LZD', ... + 'Compartment', 'Transit 2', ... + 'Cycles', cycles_LZD, ... + 'DoseInterval', dose_time_LZD, ... + 'FigureName', 'LZD Raw Timecourses (Transit 2)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_LZD, ... + 'Y', LZD_conc_tr3, ... + 'DrugName', 'LZD', ... + 'Compartment', 'Transit 3', ... + 'Cycles', cycles_LZD, ... + 'DoseInterval', dose_time_LZD, ... + 'FigureName', 'LZD Raw Timecourses (Transit 3)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_LZD, ... + 'Y', LZD_conc_tr4, ... + 'DrugName', 'LZD', ... + 'Compartment', 'Transit 4', ... + 'Cycles', cycles_LZD, ... + 'DoseInterval', dose_time_LZD, ... + 'FigureName', 'LZD Raw Timecourses (Transit 4)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_LZD, ... + 'Y', LZD_conc_tr5, ... + 'DrugName', 'LZD', ... + 'Compartment', 'Transit 5', ... + 'Cycles', cycles_LZD, ... + 'DoseInterval', dose_time_LZD, ... + 'FigureName', 'LZD Raw Timecourses (Transit 5)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_LZD, ... + 'Y', LZD_conc_gut, ... + 'DrugName', 'LZD', ... + 'Compartment', 'Gut', ... + 'Cycles', cycles_LZD, ... + 'DoseInterval', dose_time_LZD, ... + 'FigureName', 'LZD Raw Timecourses (Gut)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_LZD, ... + 'Y', LZD_conc_cen, ... + 'DrugName', 'LZD', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_LZD, ... + 'DoseInterval', dose_time_LZD, ... + 'FigureName', 'LZD Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + cycles_stats_LZD = n_doses_LZD; + xvalues_stats_LZD = time_vec_local_LZD; + + plotTimeCourses( ... + 'X', xvalues_stats_LZD, ... + 'Y', LZD_conc_cen, ... + 'DrugName', 'LZD', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_stats_LZD, ... + 'DoseInterval', dose_time_LZD, ... + 'FigureName', 'LZD Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_LZD, ... + 'time_step_size', time_step_size_LZD, ... + 'conc_c', LZD_conc_cen, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'LZD steady-state attainment', 'SortBy', 'name'); + + statsT = table( ... + mean(last24AUC, 'omitnan'), std(last24AUC, 'omitnan'), ... + mean(last24Cmax, 'omitnan'), std(last24Cmax, 'omitnan'), ... + 'VariableNames', {'mean_last24AUC', 'std_last24AUC', 'mean_last24Cmax', 'std_last24Cmax'}); + + fprintf('\n=== LZD last-24h summary stats ===\n'); + disp(statsT); + end + + if opt.plotAUC + plotAUC(last24AUC, 'LZD'); + end + + + % -------------------- PTA Analysis -------------------- + + if opt.PTAplot + + % -------------------- MIC Distributions NTM -------------------- + % This section includes the minimum inhibitory concentration (MIC) + % distributions for the non-tuberculosis mycobacterium strains belonging + % to associated non-tuberculosis mycobacterium species + + MIC_MAB_LZD = { + 'M. abscessus'; + {{'0.5', 30}, {'1', 45}, {'2', 71}, {'4', 138}, {'8', 260}, {'16', 320}, {'32', 177}, {'64', 54}, {'128', 0}}; + }; + + MIC_MAC_LZD = { + 'M. avium complex'; + {{'0.12', 0}, {'0.25', 1}, {'0.5', 1}, {'1', 40}, {'2', 160}, {'4', 269}, {'8', 740}, {'16', 1847}, {'32', 2562}... + {'64', 928}, {'128', 111}}; + }; + + MIC_kan_LZD = { + 'M. kansasii'; + {{'0.12', 6}, {'0.25', 21}, {'0.5', 39}, {'1', 28}, {'2', 9}, {'4', 1}, {'8', 0}, {'16', 0}, {'32', 0}}; + }; + + MIC_TB_LZD = { + 'M. tuberculosis'; + {{'0.008', 7}, {'0.016', 10}, {'0.032', 74}, {'0.062', 247}, {'0.12', 590}, {'0.25', 1477}, {'0.5', 2313}, {'1', 404}, ... + {'2', 24}, {'4', 58}, {'8',0}, {'16',0}, {'32', 4}, {'64', 18}, {'128', 0}, {'256', 0}}; + 'AUC24/MIC'; + 58 + }; + + speciesAggregation_LZD = {MIC_MAB_LZD MIC_MAC_LZD MIC_kan_LZD MIC_TB_LZD}; + normalizedMICSpeciesAggregation_LZD = normalizeMICsOfAggregation(speciesAggregation_LZD); + uniqueMICs_LZD = getUniqueMICs(normalizedMICSpeciesAggregation_LZD); + + + % -------------------- Setting PTA Targets -------------------- + + target1_LZD = { % https://doi.org/10.3389/fphar.2022.1063453 + 'TB'; + 'AUC24/MIC'; + 119; + 'tuberculosis' + }; + + uniqueTargets_LZD = {target1_LZD}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix_LZD = zeros(length(uniqueMICs_LZD), length(uniqueTargets_LZD)); + + for i = 1:length(uniqueTargets_LZD) + PTAMatrix_LZD(:, i) = calculatePTA( ... + last24ConcArray, ... + 'MICDist', uniqueMICs_LZD, ... + 'targetType', uniqueTargets_LZD{i}{2}, ... + 'target', uniqueTargets_LZD{i}{3}, ... + 'time_step_size', time_step_size_LZD, ... + 'last24AUC', last24AUC, ... + 'last24Cmax', last24Cmax); + end + + legendArray_LZD = getLegendArray(uniqueTargets_LZD, normalizedMICSpeciesAggregation_LZD); + + [~, plotArgs] = plotPTAs( ... + uniqueMICs_LZD, ... + PTAMatrix_LZD, ... + normalizedMICSpeciesAggregation_LZD, ... + 'DoseSize', dose_size_LZD, ... + 'DoseFrequency', dose_time_LZD, ... + 'DrugName', 'LZD', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray_LZD, ... + 'UniqueTargets', uniqueTargets_LZD, ... + 'NumPatients', n_pts, ... + 'ShowCI', false); + + end + +end \ No newline at end of file diff --git a/MXF/MXF_PlasmaODEs.m b/MXF/MXF_PlasmaODEs.m new file mode 100644 index 0000000..dff4bac --- /dev/null +++ b/MXF/MXF_PlasmaODEs.m @@ -0,0 +1,91 @@ +%% ======================================================================= +% MXF_PlasmaODEs.m — One-Compartment Oral-Absorption ODEs for +% Moxifloxacin +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the moxifloxacin plasma PK model: a depot +% compartment feeding a single central compartment by first-order +% absorption, with first-order elimination from the central compartment. +% Intended to be called by an ODE solver (ode45) from MXF_PopPK. +% +% MODEL +% ODEs as described in Al-Shaer et al., 2019. +% Mohammad H. Al-Shaer et al. "Fluoroquinolones in Drug-Resistant Tuberculosis: +% Culture Conversino and Pharmacokinetic/Pharmacodynamic Target Attainment To +% Guide Dose Selection". In: *Antimicrobial Agents and Chemotherapy* 63.7 +% (2019). DOI: 10.1128/aac.00279-19. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver. +% y - State vector of drug amounts (mg): +% y(1) = depot compartment amount +% y(2) = central compartment amount +% params - Parameter vector: +% params(1) = V/F central volume (L) +% params(2) = CL/F clearance (L/h) +% params(3) = Ka absorption rate constant (1/h) +% params(4) = dose dose for this interval (mg) [unused here; +% dosing applied via initial conditions] +% +% OUTPUTS +% dy - Derivative vector (mg/h): +% dy(1) = d(depot)/dt +% dy(2) = d(central)/dt +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2023-09-12 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = MXF_PlasmaODEs(t,y,params) + +% Extracting parameter values from params variable to assign to local variables + +% Departmental constants +V_Fs = [... + params(1) %(L) V/F + ]; +rates = [... + params(2) % CL/F Oral Clearance + params(3) % Ka Absorption rate + ]; + +% ODE Y value assignmetns +A = [... + y(1) + y(2) %Drug amount in central compartment + ]; + +% Define ODEs + +% ODE around Depot compartment +% ------------------------------------------ +dA1_term1 = - rates(2) * A(1); %Out term + +dA1dt = dA1_term1; %(mg/hL) + + +% ODE around CENTRAL compartment +% ------------------------------------------ +% ODE_2 terms +dA2_term1 = rates(2) * A(1); % Flow into the central compartment +dA2_term2 = - rates(1) * A(2) / V_Fs(1); %Clearance of drug from system + +dA2dt = (dA2_term1 + dA2_term2); %(mg/hL) + +% ODE Outputs + +dy = [... + dA1dt + dA2dt + ]; + + +end \ No newline at end of file diff --git a/MXF/MXF_PopPK.m b/MXF/MXF_PopPK.m new file mode 100644 index 0000000..437fc32 --- /dev/null +++ b/MXF/MXF_PopPK.m @@ -0,0 +1,538 @@ +%% ======================================================================= +% MXF_PopPK.m — Population PK Simulation & PTA Analysis for Moxifloxacin +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the moxifloxacin plasma ODEs across a Monte Carlo patient +% population. Models a one-compartment system with first-order oral +% absorption from a depot, computes steady-state AUC/Cmax, applies an +% unbound fraction to derive free-drug exposure, and generates time-course, +% AUC, and probability of target attainment (PTA) plots. +% +% Supports a two-arm dose comparison: when 'DoseSize' is given as +% [low high], the first half of patients receive the low dose and the +% second half the high dose, and PTA is computed and plotted per arm. +% A serial path and a parfor (parallelized) path are provided; the serial +% path is the default given the dose-arm splitting logic. +% +% MODEL +% Reimplementation of Al-Shaer et al., 2019. +% Mohammad H. Al-Shaer et al. "Fluoroquinolones in Drug-Resistant Tuberculosis: +% Culture Conversino and Pharmacokinetic/Pharmacodynamic Target Attainment To +% Guide Dose Selection". In: *Antimicrobial Agents and Chemotherapy* 63.7 +% (2019). DOI: 10.1128/aac.00279-19. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (400) Dose per administration (mg); scalar for a +% single arm or [low high] for a two-arm run. +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (24) Time between doses (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC (per dose arm). +% 'GraphFontSize' (16) Font size for generated figures. +% 'Parallelize' (false) Use parfor over patients. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: 2023-09-11; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2023-09-11 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% MXF_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% Parallel Computing Toolbox required when 'Parallelize' is true. +% ======================================================================== + + +function [plotArgs] = MXF_PopPK(varargin) + + % Initialize output + plotArgs = []; + + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 400, @(x)isnumeric(x)); + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 24, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.addParameter('Parallelize', false, @(b)islogical(b)&&isscalar(b)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Volume parameters + % ------------------------------------------ + V_F_MXF = 110; % (L) V/F central compartment volume + + % Rate constants + % ------------------------------------------ + rates_MXF = [... + 9.59 % (L/h) CL/F overall clearance + 2.69 % (1/h) Ka absorption rate constant + ]; + + % Interindividual variability (exponential error model) + % ------------------------------------------ + IIV_IOV_MXF = [... + 0.60 % Ka (CV) IIV + 0.30 % CL/F (CV) IIV + 0.38 % V/F (CV) IIV + ]; + + % Unbound fraction + % ------------------------------------------ + f_MXF = 0.5; % (-) MOX free fraction + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify how much drug is given in each dose (mg) + % Allow scalar (single arm) or [low high] (two-arm comparison) + if numel(opt.DoseSize) == 1 + dose_low_MXF = opt.DoseSize; + dose_high_MXF = opt.DoseSize; + else + dose_low_MXF = opt.DoseSize(1); % e.g. 400 mg + dose_high_MXF = opt.DoseSize(2); % e.g. 800 mg + end + nominalDose_MXF = dose_high_MXF; % used only for labeling + + % Specify number of doses to simulate + n_doses_MXF = opt.NumDoses; + + % Specify time (hours) between doses + dose_time_MXF = opt.DoseInterval; + + % Specify time step and build time vectors + time_step_size_MXF = 0.5; + + time_vec_MXF = 0 : time_step_size_MXF : dose_time_MXF; + total_timpts_MXF = n_doses_MXF * (length(time_vec_MXF) - 1); + total_params_MXF = 4; + time_vec_local_MXF = 0 : time_step_size_MXF : dose_time_MXF * n_doses_MXF - time_step_size_MXF; + + + % -------------------- Initialize Matrices -------------------- + + param_store_MXF = zeros([total_params_MXF n_pts]); + MXF_conc_dep = zeros([total_timpts_MXF n_pts]); + MXF_conc_c = zeros([total_timpts_MXF n_pts]); + + + % -------------------- ODE Solving -------------------- + + % Serial path (default for MXF due to dose-arm splitting logic) + if ~opt.Parallelize + + for ipt = 1:n_pts + + % Assign dose arm: first half low, second half high + % -------------------------------------------------------- + if ipt <= floor(n_pts / 2) + patient_dose_MXF = dose_low_MXF; + else + patient_dose_MXF = dose_high_MXF; + end + + % Sample patient-specific parameters using IIV + % NOTE: IIV uses raw CV directly (not lognormal transform) + % -------------------------------------------------------- + V_F_pt_MXF = V_F_MXF * exp(random('Normal', 0, IIV_IOV_MXF(3))); + + rates_pt_MXF = [... + rates_MXF(1) * exp(random('Normal', 0, IIV_IOV_MXF(2))); % CL/F + rates_MXF(2) * exp(random('Normal', 0, IIV_IOV_MXF(1))) % Ka + ]; + + % Set initial conditions + % -------------------------------------------------------- + MXF_dep_IC = patient_dose_MXF; + MXF_c_IC = 0; + + % Initialize local storage + MXF_conc_dep_loc = zeros([total_timpts_MXF 1]); + MXF_conc_c_loc = zeros([total_timpts_MXF 1]); + + for idose = 1:n_doses_MXF + + % Compile parameters into vector to pass to ODE solver + params_MXF = [... + V_F_pt_MXF % V_F params(1) + rates_pt_MXF(1) % CL_F params(2) + rates_pt_MXF(2) % Ka params(3) + patient_dose_MXF % dose size params(4) + ]; + + % Call ODE solver + % --------------- + [~, MXF_sol] = ode45(@(t,y) MXF_PlasmaODEs(t, y, params_MXF), ... + time_vec_MXF, [MXF_dep_IC MXF_c_IC]); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_MXF) - 1) + 1; + i1 = idose * (length(time_vec_MXF) - 1); + + MXF_conc_dep_loc(i0:i1) = MXF_sol(1:end-1, 1); + MXF_conc_c_loc(i0:i1) = MXF_sol(1:end-1, 2) / V_F_pt_MXF; + + % Update initial conditions for next dose + MXF_dep_IC = MXF_sol(end, 1) + patient_dose_MXF; + MXF_c_IC = MXF_sol(end, 2); + + end + + % Combine data for all patients + param_store_MXF(1:total_params_MXF, ipt) = params_MXF; + MXF_conc_dep(:, ipt) = MXF_conc_dep_loc; + MXF_conc_c(:, ipt) = MXF_conc_c_loc; + + end + + % Parallel path + else + + parfor ipt = 1:n_pts + + % Assign dose arm: first half low, second half high + % -------------------------------------------------------- + if ipt <= floor(n_pts / 2) + patient_dose_MXF = dose_low_MXF; + else + patient_dose_MXF = dose_high_MXF; + end + + % Sample patient-specific parameters using IIV + % NOTE: IIV uses raw CV directly (not lognormal transform) + % -------------------------------------------------------- + V_F_pt_MXF = V_F_MXF * exp(random('Normal', 0, IIV_IOV_MXF(3))); + + rates_pt_MXF = [... + rates_MXF(1) * exp(random('Normal', 0, IIV_IOV_MXF(2))); % CL/F + rates_MXF(2) * exp(random('Normal', 0, IIV_IOV_MXF(1))) % Ka + ]; + + % Set initial conditions + % -------------------------------------------------------- + MXF_dep_IC = patient_dose_MXF; + MXF_c_IC = 0; + + % Initialize local storage + MXF_conc_dep_loc = zeros([total_timpts_MXF 1]); + MXF_conc_c_loc = zeros([total_timpts_MXF 1]); + + params_MXF_loc = zeros([total_params_MXF 1]); + + for idose = 1:n_doses_MXF + + % Compile parameters into vector to pass to ODE solver + params_MXF_loc = [... + V_F_pt_MXF % V_F params(1) + rates_pt_MXF(1) % CL_F params(2) + rates_pt_MXF(2) % Ka params(3) + patient_dose_MXF % dose size params(4) + ]; + + % Call ODE solver + % --------------- + [~, MXF_sol] = ode45(@(t,y) MXF_PlasmaODEs(t, y, params_MXF_loc), ... + time_vec_MXF, [MXF_dep_IC MXF_c_IC]); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_MXF) - 1) + 1; + i1 = idose * (length(time_vec_MXF) - 1); + + MXF_conc_dep_loc(i0:i1) = MXF_sol(1:end-1, 1); + MXF_conc_c_loc(i0:i1) = MXF_sol(1:end-1, 2) / V_F_pt_MXF; + + % Update initial conditions for next dose + MXF_dep_IC = MXF_sol(end, 1) + patient_dose_MXF; + MXF_c_IC = MXF_sol(end, 2); + + end + + % Store patient results + param_store_MXF(:, ipt) = params_MXF_loc; + MXF_conc_dep(:, ipt) = MXF_conc_dep_loc; + MXF_conc_c(:, ipt) = MXF_conc_c_loc; + + end + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + cycles_MXF = n_doses_MXF; + xvalues_MXF = time_vec_local_MXF; + + plotTimeCourses( ... + 'X', xvalues_MXF, ... + 'Y', MXF_conc_c, ... + 'DrugName', 'MXF', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_MXF, ... + 'DoseInterval', dose_time_MXF, ... + 'FigureName', 'MXF Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + cycles_stats_MXF = n_doses_MXF; + xvalues_stats_MXF = time_vec_local_MXF; + + plotTimeCourses( ... + 'X', xvalues_stats_MXF, ... + 'Y', MXF_conc_c, ... + 'DrugName', 'MXF', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_stats_MXF, ... + 'DoseInterval', dose_time_MXF, ... + 'FigureName', 'MXF Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_MXF, ... + 'time_step_size', time_step_size_MXF, ... + 'conc_c', MXF_conc_c, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'MXF steady-state attainment', 'SortBy', 'name'); + + statsT = table( ... + mean(last24AUC, 'omitnan'), std(last24AUC, 'omitnan'), ... + mean(last24Cmax, 'omitnan'), std(last24Cmax, 'omitnan'), ... + 'VariableNames', {'mean_last24AUC', 'std_last24AUC', 'mean_last24Cmax', 'std_last24Cmax'}); + + fprintf('\n=== MXF last-24h summary stats ===\n'); + disp(statsT); + + fprintf('Mean CL/F: %.3f [5th 50th 95th: %.3f %.3f %.3f]\n', ... + mean(param_store_MXF(2,:)), prctile(param_store_MXF(2,:), [5 50 95])); + fprintf('Mean V/F: %.3f\n', mean(param_store_MXF(1,:))); + + fLast24AUC_quiet = last24AUC * f_MXF; + fprintf('Mean fAUC: %.3f\n', mean(fLast24AUC_quiet, 'omitnan')); + end + + if opt.plotAUC + plotAUC(last24AUC, 'MXF'); + end + + + % -------------------- PTA Analysis -------------------- + + if opt.PTAplot + + % -------------------- MIC Distributions NTM -------------------- + % This section includes the minimum inhibitory concentration (MIC) + % distributions for the non-tuberculosis mycobacterium strains belonging + % to associated non-tuberculosis mycobacterium species + + MIC_MAB_MXF = { + 'M. abscessus'; + {{'0.06', 0}, {'0.125', 0}, {'0.25', 3}, {'0.5', 5}, {'1', 52}, {'2', 98}, {'4', 191}, {'8', 290}, {'16', 507}, {'32', 0}}; + 'fAUC24/MIC'; + 1146 + }; + + MIC_MAC_MXF = { + 'M. avium complex'; + {{'0.06', 0}, {'0.125', 20}, {'0.25', 82}, {'0.5', 248}, {'1', 650}, {'2', 1607}, {'4', 1703}, {'8', 533}, {'16', 143}, {'32', 114}}; + 'fAUC24/MIC'; + 5100 + }; + + MIC_kan_MXF = { + 'M. kansasii'; + {{'0.06', 63}, {'0.125', 57}, {'0.25', 10}, {'0.5', 4}, {'1', 3}, {'2', 0}, {'4', 0}, {'8', 0}, {'16', 0}, {'32', 0}}; + 'fAUC24/MIC'; + 137 + }; + + MIC_TB_MXF = { + 'M. tuberculosis'; + {{'0.06', 1}, {'0.125', 30}, {'0.25', 193}, {'0.5', 43}, {'1', 0}, {'2', 0}, {'4', 0}, {'8', 0}, {'16', 0}, {'32', 0}}; + 'fAUC24/MIC'; + 267 + }; + + speciesAggregation_MXF = {MIC_MAB_MXF MIC_MAC_MXF MIC_kan_MXF MIC_TB_MXF}; + normalizedMICSpeciesAggregation_MXF = normalizeMICsOfAggregation(speciesAggregation_MXF); + uniqueMICs_MXF = getUniqueMICs(normalizedMICSpeciesAggregation_MXF); + + + % -------------------- Setting PTA Targets -------------------- + + target1_MXF = { % https://pubmed.ncbi.nlm.nih.gov/35146045/ + 'DR-TB'; + 'fAUC24/MIC'; + 53; + 'tuberculosis' + }; + + uniqueTargets_MXF = {target1_MXF}; + + + % -------------------- Free Concentration Arrays -------------------- + % Apply unbound fraction to concentration array and PK metrics + % before passing to PTA calculation + + fLast24ConcArray_MXF = last24ConcArray * f_MXF; + fLast24AUC_MXF = last24AUC * f_MXF; + fLast24Cmax_MXF = last24Cmax * f_MXF; + + + % -------------------- Split Patients by Dose Arm -------------------- + % First half: low dose; second half: high dose (matches assignment above) + + half_n = floor(n_pts / 2); + idxLowDose = 1 : half_n; + idxHighDose = (half_n + 1) : n_pts; + + fLast24Conc_low = fLast24ConcArray_MXF(:, idxLowDose); + fLast24Conc_high = fLast24ConcArray_MXF(:, idxHighDose); + fLast24AUC_low = fLast24AUC_MXF(idxLowDose); + fLast24AUC_high = fLast24AUC_MXF(idxHighDose); + fLast24Cmax_low = fLast24Cmax_MXF(idxLowDose); + fLast24Cmax_high = fLast24Cmax_MXF(idxHighDose); + + + % -------------------- PTA Calculation -------------------- + % Compute PTA separately per dose arm, then interleave columns: + % PTAMatrix = [low_t1 high_t1 low_t2 high_t2 ...] + + nMIC_MXF = length(uniqueMICs_MXF); + nTarg_MXF = length(uniqueTargets_MXF); + + PTA_low_MXF = zeros(nMIC_MXF, nTarg_MXF); + PTA_high_MXF = zeros(nMIC_MXF, nTarg_MXF); + + for i = 1:nTarg_MXF + PTA_low_MXF(:, i) = calculatePTA( ... + fLast24Conc_low, ... + 'MICDist', uniqueMICs_MXF, ... + 'targetType', uniqueTargets_MXF{i}{2}, ... + 'target', uniqueTargets_MXF{i}{3}, ... + 'time_step_size', time_step_size_MXF, ... + 'last24AUC', fLast24AUC_low, ... + 'last24Cmax', fLast24Cmax_low); + + PTA_high_MXF(:, i) = calculatePTA( ... + fLast24Conc_high, ... + 'MICDist', uniqueMICs_MXF, ... + 'targetType', uniqueTargets_MXF{i}{2}, ... + 'target', uniqueTargets_MXF{i}{3}, ... + 'time_step_size', time_step_size_MXF, ... + 'last24AUC', fLast24AUC_high, ... + 'last24Cmax', fLast24Cmax_high); + end + + PTAMatrix_MXF = zeros(nMIC_MXF, 2 * nTarg_MXF); + col = 1; + for i = 1:nTarg_MXF + PTAMatrix_MXF(:, col) = PTA_low_MXF(:, i); + PTAMatrix_MXF(:, col + 1) = PTA_high_MXF(:, i); + col = col + 2; + end + + + % -------------------- PTA Plotting -------------------- + % Build legend: species entries stay dose-agnostic; each PTA target + % is duplicated with low- and high-dose labels + + baseLegend_MXF = getLegendArray(uniqueTargets_MXF, normalizedMICSpeciesAggregation_MXF); + nSpecies_MXF = numel(normalizedMICSpeciesAggregation_MXF); + speciesLegend_MXF = baseLegend_MXF(1 : nSpecies_MXF); + targetLegend_MXF = baseLegend_MXF(nSpecies_MXF + 1 : end); + + legendArray_MXF = cell(1, nSpecies_MXF + 2 * nTarg_MXF); + legendArray_MXF(1 : nSpecies_MXF) = speciesLegend_MXF; + + idx = nSpecies_MXF + 1; + for i = 1:nTarg_MXF + legendArray_MXF{idx} = sprintf('%s (%d mg q%dh)', ... + targetLegend_MXF{i}, dose_low_MXF, dose_time_MXF); + legendArray_MXF{idx + 1} = sprintf('%s (%d mg q%dh)', ... + targetLegend_MXF{i}, dose_high_MXF, dose_time_MXF); + idx = idx + 2; + end + + [~, plotArgs] = plotPTAs( ... + uniqueMICs_MXF, ... + PTAMatrix_MXF, ... + normalizedMICSpeciesAggregation_MXF, ... + 'DoseSize', nominalDose_MXF, ... + 'DoseFrequency', dose_time_MXF, ... + 'DrugName', 'MXF', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray_MXF, ... + 'UniqueTargets', uniqueTargets_MXF, ... + 'NumPatients', n_pts, ... + 'ShowCI', false, ... + 'DoseLabel', sprintf('Doses: %d vs %d mg, q%dh', ... + dose_low_MXF, dose_high_MXF, dose_time_MXF)); + + end + +end \ No newline at end of file diff --git a/Methods/README.md b/Methods/README.md new file mode 100644 index 0000000..534fa50 --- /dev/null +++ b/Methods/README.md @@ -0,0 +1,125 @@ +# Methods + +Shared helper functions used by all per-drug population-PK (PopPK) simulations +in this repository. Each `_PopPK.m` driver samples a virtual population, +solves its ODEs, and then calls into these functions to compute exposure +metrics, evaluate probability of target attainment (PTA), and render figures. + +All functions are tested on **MATLAB R2026a**. Some functions use `parfor` +internally (selected via a `'Parallelize'` option in the drug drivers) and +therefore require the **Parallel Computing Toolbox**; random sampling uses the +**Statistics and Machine Learning Toolbox**. + +## Typical call sequence + +For a single drug, the helpers are invoked roughly in this order: + +1. `normalizeMICsOfAggregation` — convert string MIC labels to numeric values. +2. `getUniqueMICs` — collect the sorted unique MIC axis across all species. +3. `calculateSteadyState` — extract the last-24h window and its AUC/Cmax, and + test steady-state attainment. +4. `calculatePTA` — compute PTA at each MIC for each target. +5. `getLegendArray` — build legend entries for species and targets. +6. `plotPTAs` — render the per-drug PTA figure (and return a `plotArgs` bundle). +7. `summarize_ss_counts`, `plotAUC`, `plotTimeCourses` — console summary and + diagnostic plots. + +The `plotArgs` bundles returned by `plotPTAs` are later consumed by +`plotFullPTAFig` and `plotSuppPTAFig` to assemble the multi-panel figures. + +--- + +## Exposure and target-attainment + +### `calculateSteadyState.m` +Extracts the last-24h concentration window and computes its per-patient AUC +(trapezoidal) and Cmax. Tests steady-state attainment by comparing AUC and +Cmax between the last two non-overlapping 24h windows within caller-supplied +tolerances. Optional first-dose modes (`'isFirstDose'`, `'useFirstDose'`) +return first-interval metrics and skip the steady-state check. + +Returns steady-state attainment counts (AUC, Cmax, both) plus the last-24h +concentration array, AUC, and Cmax. Assumes a uniform time grid at +`time_step_size`. "Steady state" here means inter-day stability within +tolerance, not asymptotic accumulation — relevant for slowly accumulating +drugs (e.g. clofazimine, rifabutin). + +### `calculatePTA.m` +Computes PTA across a MIC range for one pharmacodynamic target. Supports three +target types: + +* `%T>MIC` — fraction of time concentration exceeds the MIC (uses the + concentration matrix directly). +* `AUC24/MIC` — uses the precomputed per-patient `last24AUC`. +* `Cmax/MIC` — uses the precomputed per-patient `last24Cmax`. + +Does not recompute AUC or Cmax. Target-type matching is substring-based +(`contains`), which lets `fAUC24/MIC` (free-drug, used by moxifloxacin and +omadacycline) route through the `AUC24/MIC` branch. + +### `summarize_ss_counts.m` +Builds and prints a table of steady-state attainment counts (AUC, Cmax, both), +each as a percentage of the total population. Optionally sorts by count or +metric name. NaN-safe when the total N is unknown. + +--- + +## MIC distribution handling + +### `normalizeMICsOfAggregation.m` +Converts the string MIC labels in a species/MIC aggregation to numeric values, +resolving censored-bin operators: `<=`, `<`, and `>=` map to the value itself, +while `>` maps to twice the value. Counts pass through unchanged — this +function normalizes the **MIC axis only**, not the counts. Per-species count +normalization (to percentages) happens later, inside `plotPTAs`. + +### `getUniqueMICs.m` +Collects every (already numericized) MIC value across all species and returns +the sorted unique set, which becomes the shared MIC axis for the PTA curves and +distribution bars. Run *after* `normalizeMICsOfAggregation`. + +--- + +## Figure construction + +### `plotPTAs.m` +Core per-drug plotting routine. Plots one or more PTA curves against a shared +MIC axis, with per-species MIC distribution bars overlaid. Each species' +distribution is normalized to its own total and shown as a within-species +percentage, so heterogeneous count scales across species are reconciled at +plot time. PTA curves are black by default and distinguished by line style and +marker; per-curve colors can be opted in via `'RegimenColors'`. Optional CI +ribbons are available (off by default). Returns the figure handle plus a +`plotArgs` struct that bundles the inputs (including the original `varargin`) +for later regeneration by the panel assemblers. + +### `plotAUC.m` +Plots a histogram of per-patient 24h AUC for a drug, with an optional +(off by default) overlay of AUC/MIC cutoff lines annotated by MIC and target. + +### `plotTimeCourses.m` +Plots per-patient concentration time courses with optional mean and percentile +overlays (stats-only mode hides the raw lines). Supports dose-aware x-axis +ticks. Used for diagnostic/QC time-course figures. + +### `plotFullPTAFig.m` +Assembles the main-text multi-panel PTA figure from the per-drug `plotArgs` +bundles in `PTAplotInfo`. Renders each drug via `plotPTAs`, copies it into a +tile of a shared layout, applies per-drug legend overrides, and adds a grouped +species (MIC-distribution) legend panel. Drugs without a valid PTA result are +skipped. + +### `plotSuppPTAFig.m` +Assembles the supplementary 3x2 PTA figure comparing alternative dosing +scenarios (cefoxitin 2000/4000 mg TID, imipenem 1000 mg TID, tigecycline +50/100 mg BID), with a shared species legend tile. + +--- + +## Color + +### `getColor.m` +Returns a fixed RGB triplet for a species or label string (case-insensitive, +substring match), keeping colors consistent across all figures. +*M. tuberculosis* is drawn grey/black; other NTM species map to a shared +colormap plus a Purdue-branded palette. Unrecognized labels fall back to blue. diff --git a/Methods/calculatePTA.m b/Methods/calculatePTA.m new file mode 100644 index 0000000..f2251fc --- /dev/null +++ b/Methods/calculatePTA.m @@ -0,0 +1,190 @@ +%% ======================================================================= +% calculatePTA.m — Probability of Target Attainment (PTA) Calculation +% ======================================================================== +% +% DESCRIPTION +% Computes the Probability of Target Attainment (PTA) across a range of +% MIC values for a given pharmacodynamic target. Three target types are +% supported: +% - %T>MIC : percentage of time drug concentration exceeds the MIC +% - AUC24/MIC : 24-hour AUC divided by MIC +% - Cmax/MIC : peak concentration divided by MIC +% +% PTA is returned as the fraction of the simulated population meeting the +% target at each MIC. +% +% INPUTS +% concMatrix - Matrix of drug concentrations (time points x patients). +% Used directly for the %T>MIC branch; for AUC24/MIC and +% Cmax/MIC the precomputed scalar arrays (last24AUC / +% last24Cmax) are used instead. The window length of +% concMatrix is NOT validated — the caller is responsible +% for supplying a window meaningful for the chosen target. +% +% Name-value pairs: +% 'MICDist' : (numMICs x 1) vector of MIC values to evaluate. +% 'targetType' : Target type ('%T>MIC', 'AUC24/MIC', 'Cmax/MIC'). +% 'target' : Numeric threshold for the PTA comparison. +% 'time_step_size' : Kept for backward compatibility; not used here. +% 'last24AUC' : Precomputed per-patient AUC (1 x numPatients). +% Required for the 'AUC24/MIC' branch. +% 'last24Cmax' : Precomputed per-patient Cmax (1 x numPatients). +% Required for the 'Cmax/MIC' branch. +% +% OUTPUTS +% PTAArray - (numMICs x 1) vector of PTA values (fraction of +% population meeting the target at each MIC). +% +% NOTES +% This function does NOT recompute AUC or Cmax. It uses the supplied +% last24AUC / last24Cmax for those branches and only inspects concMatrix +% directly for the %T>MIC branch. +% +% When using 'AUC24/MIC' with an AUC computed over a window other than +% 24 hours (e.g., a first-dose interval), the comparison against an +% "AUC24" target may not be clinically meaningful unless the AUC has been +% scaled or the target reinterpreted. Decide deliberately before relying +% on the result. +% +% AUTHORS +% Tyler Dierckman, Trevor Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function PTAArray = calculatePTA(concMatrix, varargin) + + % ----- Parse inputs ----- + p = inputParser; + p.addRequired('concMatrix', @(x) isnumeric(x) && ~isempty(x)); + p.addParameter('MICDist', []); + p.addParameter('targetType', ''); + p.addParameter('target', []); + p.addParameter('time_step_size', []); % kept for back-compat; unused + p.addParameter('last24AUC', []); + p.addParameter('last24Cmax', []); + p.parse(concMatrix, varargin{:}); + opt = p.Results; + + concMatrix = opt.concMatrix; + MICDist = opt.MICDist; + targetType = opt.targetType; + target = opt.target; + last24AUC = opt.last24AUC; + last24Cmax = opt.last24Cmax; + + % Basic sanity checks on key inputs + if isempty(MICDist) + error('calculatePTA:MissingMICDist', ... + 'MICDist must be provided.'); + end + if isempty(targetType) + error('calculatePTA:MissingTargetType', ... + 'targetType must be provided (e.g., ''%%T>MIC'', ''AUC24/MIC'', ''Cmax/MIC'').'); + end + if isempty(target) + error('calculatePTA:MissingTarget', ... + 'target threshold value must be provided.'); + end + + % --------------------------------------------------------------------- + % Branch 1: %T>MIC (uses concMatrix only, no AUC/Cmax recompute) + % --------------------------------------------------------------------- + if contains(targetType, '%T>MIC') + + numPatients = size(concMatrix, 2); % columns = patients + numMICs = length(MICDist); + PTAArray = zeros(numMICs, 1); + + for i = 1:numMICs + currentMIC = MICDist(i); + patientsMeetingTarget = 0; + + for iPatient = 1:numPatients + % time x patients orientation + patientConc = concMatrix(:, iPatient); % [time x 1] + + % Determine how many time points the patient concentration is above the MIC value + countExceeding = sum(patientConc > currentMIC); + numTimePoints = length(patientConc); + + % Convert the count of time points to percentage + percentageExceeding = (countExceeding / numTimePoints) * 100; + + if percentageExceeding > target + patientsMeetingTarget = patientsMeetingTarget + 1; + end + end + + PTAArray(i) = patientsMeetingTarget / numPatients; + end + + % --------------------------------------------------------------------- + % Branch 2: AUC24/MIC (uses precomputed last24AUC) + % --------------------------------------------------------------------- + elseif contains(targetType, 'AUC24/MIC') + + if isempty(last24AUC) + error('calculatePTA:MissingLast24AUC', ... + 'last24AUC must be provided for AUC24/MIC calculations.'); + end + + numPatients = numel(last24AUC); + numMICs = length(MICDist); + PTAArray = zeros(numMICs, 1); + + % Loop over each MIC value + for i = 1:numMICs + currentMIC = MICDist(i); + patientsMeetingTarget = 0; + + % Loop over each patient + for iPatient = 1:numPatients + patientAUC = last24AUC(iPatient); + + % Check if AUC/MIC exceeds the target + if (patientAUC / currentMIC) >= target + patientsMeetingTarget = patientsMeetingTarget + 1; + end + end + + PTAArray(i) = patientsMeetingTarget / numPatients; + end + + % --------------------------------------------------------------------- + % Branch 3: Cmax/MIC (uses ONLY last24Cmax; no Cmax recompute) + % --------------------------------------------------------------------- + elseif contains(targetType, 'Cmax/MIC') + + if isempty(last24Cmax) + error('calculatePTA:MissingLast24Cmax', ... + 'last24Cmax must be provided for Cmax/MIC calculations.'); + end + + numPatients = numel(last24Cmax); + numMICs = length(MICDist); + PTAArray = zeros(numMICs, 1); + + for i = 1:numMICs + currentMIC = MICDist(i); + patientsMeetingTarget = 0; + + for iPatient = 1:numPatients + patientCmax = last24Cmax(iPatient); + + if (patientCmax / currentMIC) >= target + patientsMeetingTarget = patientsMeetingTarget + 1; + end + end + + PTAArray(i) = patientsMeetingTarget / numPatients; + end + + else + error('Target type not yet implemented'); + end +end \ No newline at end of file diff --git a/Methods/calculateSteadyState.m b/Methods/calculateSteadyState.m new file mode 100644 index 0000000..dc6160b --- /dev/null +++ b/Methods/calculateSteadyState.m @@ -0,0 +1,258 @@ +%% ======================================================================= +% calculateSteadyState.m — Steady-State Detection and Last-24h +% Exposure Metrics +% ======================================================================== +% +% DESCRIPTION +% Evaluates whether steady-state conditions have been reached for a PK +% simulation by comparing AUC and Cmax between the last two non-overlapping +% 24-hour windows, within per-patient tolerances. Always returns the +% last-24h concentration window and its AUC/Cmax; optionally returns +% first-dose-interval metrics instead (skipping the SS check) when one of +% the first-dose flags is set. +% +% INPUTS +% time_vec_local - Simulation time vector (1D). Currently unused. +% +% Name-value pairs: +% 'time_step_size' : Simulation time step (scalar; default 0.5 h). +% 'conc_c' : Concentration matrix (time points x subjects). +% 'AUCTolerance' : Relative tolerance for AUC steady-state (scalar). +% 'CmaxTolerance' : Relative tolerance for Cmax steady-state (scalar). +% 'n_pts' : Number of individuals (optional; inferred from +% size(conc_c,2) if empty). +% 'last24AUC' : Placeholder; recomputed inside (optional). +% 'last24Cmax' : Placeholder; recomputed inside (optional). +% 'isFirstDose' : If true, compute 0-24h AUC/Cmax only and skip the +% SS check. +% 'useFirstDose' : If true, compute AUC/Cmax over the first dose +% interval (0 to doseFrequency h) only and skip the +% SS check. Requires 'doseFrequency'. Cannot be +% combined with 'isFirstDose'. +% 'doseFrequency' : Dose frequency (h, positive scalar). Required when +% 'useFirstDose' is true; ignored otherwise. +% 'Quiet' : Suppress the warning when there is not enough data +% for two 24h windows (logical; default false). +% +% OUTPUTS +% numAUCSSReached - Count of individuals reaching AUC steady state. +% 0 if the SS check could not be performed; NaN when +% 'isFirstDose' or 'useFirstDose' is set. +% numCmaxSSReached - Count reaching Cmax steady state (same 0/NaN rules). +% numBothReached - Count reaching both AUC and Cmax SS (same 0/NaN rules). +% last24ConcArray - Concentrations over the last 24h (time points x +% individuals); the first-dose interval instead when +% 'useFirstDose' is true. +% last24AUC - Per-patient AUC over the last 24h (1D); first-dose +% AUC when 'useFirstDose' is true. +% last24Cmax - Per-patient Cmax over the last 24h (1D); first-dose +% Cmax when 'useFirstDose' is true. +% +% AUTHORS +% Tyler Dierckman, Trevor Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + +function [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = ... + calculateSteadyState(time_vec_local, varargin) %#ok + + % ----- Parse inputs ----- + p = inputParser; + p.addParameter('time_step_size', 0.5); + p.addParameter('conc_c', []); + p.addParameter('AUCTolerance', []); + p.addParameter('CmaxTolerance', []); + p.addParameter('last24AUC', []); + p.addParameter('last24Cmax', []); + p.addParameter('isFirstDose', false); + p.addParameter('useFirstDose', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('doseFrequency', [], ... + @(x)isempty(x) || (isnumeric(x) && isscalar(x) && isfinite(x) && x > 0)); + p.addParameter('n_pts', []); % optional; infer from conc_c if empty + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.parse(varargin{:}); + opt = p.Results; + + % Local convenience variables + time_step_size = opt.time_step_size; + conc_c = opt.conc_c; + AUCTolerance = opt.AUCTolerance; + CmaxTolerance = opt.CmaxTolerance; + isFirstDose = opt.isFirstDose; + useFirstDose = opt.useFirstDose; + doseFrequency = opt.doseFrequency; + n_pts = opt.n_pts; + quietFlag = opt.Quiet; + + if isempty(conc_c) + error('calculateSteadyState:MissingConc', ... + 'The ''conc_c'' matrix must be provided.'); + end + + % Mutually exclusive flags + if isFirstDose && useFirstDose + error('calculateSteadyState:ConflictingFlags', ... + ['''isFirstDose'' and ''useFirstDose'' cannot both be true. ' ... + 'Choose one.']); + end + + % Infer n_pts if not supplied + if isempty(n_pts) + n_pts = size(conc_c, 2); + end + + % Number of samples in a 24h window INCLUDING both endpoints (0..24) + nWin = round(24 / time_step_size) + 1; % dt=0.5 -> 49 + + % --------------------------------------------------------------------- + % useFirstDose branch: compute AUC/Cmax over the first dose interval + % (0 to doseFrequency hours) only, no SS check. + % --------------------------------------------------------------------- + if useFirstDose + if isempty(doseFrequency) + error('calculateSteadyState:MissingDoseFrequency', ... + ['When ''useFirstDose'' is true, ''doseFrequency'' ' ... + '(in hours) must be provided.']); + end + + % Number of samples spanning [0, doseFrequency] inclusive + nFirstDoseWin = round(doseFrequency / time_step_size) + 1; + + indexStart = 1; + indexEnd = nFirstDoseWin; + + if size(conc_c, 1) < indexEnd + error('calculateSteadyState:NotEnoughDataUseFirstDose', ... + ['Need at least %d rows in conc_c for first-dose window ' ... + 'of %g h at dt=%g (got %d).'], ... + indexEnd, doseFrequency, time_step_size, size(conc_c, 1)); + end + + last24ConcArray = conc_c(indexStart:indexEnd, :); + + last24AUC = zeros(1, n_pts); + last24Cmax = zeros(1, n_pts); + for ipt = 1:n_pts + last24AUC(ipt) = trapz(time_step_size, last24ConcArray(:, ipt)); + last24Cmax(ipt) = max(last24ConcArray(:, ipt)); + end + + numAUCSSReached = NaN; + numCmaxSSReached = NaN; + numBothReached = NaN; + return; + end + + % --------------------------------------------------------------------- + % First-dose branch: only compute 0–24h AUC and Cmax, no SS check + % --------------------------------------------------------------------- + if isFirstDose + indexStart = 1; + indexEnd = nWin; + + if size(conc_c, 1) < indexEnd + error('calculateSteadyState:NotEnoughDataFirstDose', ... + ['Need at least %d rows in conc_c for first-dose 0-24h ' ... + 'window (got %d).'], indexEnd, size(conc_c, 1)); + end + + last24ConcArray = conc_c(indexStart:indexEnd, :); + + last24AUC = zeros(1, n_pts); + last24Cmax = zeros(1, n_pts); + for ipt = 1:n_pts + last24AUC(ipt) = trapz(time_step_size, last24ConcArray(:, ipt)); + last24Cmax(ipt) = max(last24ConcArray(:, ipt)); + end + + numAUCSSReached = NaN; + numCmaxSSReached = NaN; + numBothReached = NaN; + return; + end + + % --------------------------------------------------------------------- + % Steady-state branch + % --------------------------------------------------------------------- + + indexEnd = size(conc_c, 1); + + % Indices for the LAST 24h window + i2_end = indexEnd; + i2_start = indexEnd - nWin + 1; + + % We need at least one full 24h window to compute anything. + if i2_start < 1 + error('calculateSteadyState:NotEnoughDataForOneWindow', ... + ['Not enough data to compute even a single 24h window. ' ... + 'Need at least %d rows, got %d.'], nWin, indexEnd); + end + + % Always compute the last-24h window stats. + last24ConcArray = conc_c(i2_start:i2_end, :); + last24AUC = zeros(1, n_pts); + last24Cmax = zeros(1, n_pts); + for ipt = 1:n_pts + last24AUC(ipt) = trapz(time_step_size, last24ConcArray(:, ipt)); + last24Cmax(ipt) = max(last24ConcArray(:, ipt)); + end + + % Indices for the PREVIOUS 24h window (non-overlapping, immediately before). + i1_end = i2_start - 1; + i1_start = i1_end - nWin + 1; + + % If we don't have a full prior window, gracefully skip the SS check. + if i1_start < 1 + if ~quietFlag + warning('calculateSteadyState:CannotPerformSSCheck', ... + ['Not enough data to perform steady-state comparison ' ... + '(need %d rows for two non-overlapping 24h windows, ' ... + 'got %d). Returning last-24h AUC/Cmax only; ' ... + 'SS counts set to 0.'], 2*nWin - 1, indexEnd); + end + numAUCSSReached = 0; + numCmaxSSReached = 0; + numBothReached = 0; + return; + end + + % We have two full windows — proceed with SS comparison. + secondToLast24ConcArray = conc_c(i1_start:i1_end, :); + + % Hard assert on shapes + if size(last24ConcArray,1) ~= nWin || size(secondToLast24ConcArray,1) ~= nWin + error('calculateSteadyState:WindowSizingFailed', ... + 'Window sizing failed: last=%d prev=%d expected=%d', ... + size(last24ConcArray,1), size(secondToLast24ConcArray,1), nWin); + end + + % Preallocate + isCmaxSSReachedArray = zeros(1, n_pts); + isAUCSSReachedArray = zeros(1, n_pts); + areBothReachedArray = zeros(1, n_pts); + secondToLast24AUC = zeros(1, n_pts); + secondToLast24Cmax = zeros(1, n_pts); + + for ipt = 1:n_pts + secondToLast24AUC(ipt) = trapz(time_step_size, secondToLast24ConcArray(:, ipt)); + secondToLast24Cmax(ipt) = max(secondToLast24ConcArray(:, ipt)); + + AUCDiff = abs((last24AUC(ipt) - secondToLast24AUC(ipt)) ./ secondToLast24AUC(ipt)); + CmaxDiff = abs((last24Cmax(ipt) - secondToLast24Cmax(ipt)) ./ secondToLast24Cmax(ipt)); + + if AUCDiff < AUCTolerance, isAUCSSReachedArray(ipt) = 1; end + if CmaxDiff < CmaxTolerance, isCmaxSSReachedArray(ipt) = 1; end + if isAUCSSReachedArray(ipt) && isCmaxSSReachedArray(ipt) + areBothReachedArray(ipt) = 1; + end + end + + numAUCSSReached = sum(isAUCSSReachedArray); + numCmaxSSReached = sum(isCmaxSSReachedArray); + numBothReached = sum(areBothReachedArray); +end \ No newline at end of file diff --git a/Methods/getColor.m b/Methods/getColor.m new file mode 100644 index 0000000..58883d1 --- /dev/null +++ b/Methods/getColor.m @@ -0,0 +1,55 @@ +%% ======================================================================= +% getColor.m — Fixed RGB Color Lookup for Species / Label Strings +% ======================================================================== +% +% DESCRIPTION +% Returns a fixed RGB triplet for a given species or label string, used to +% keep colors consistent across all drug figures. Matching is +% case-insensitive and substring-based. Mycobacterial species map to a +% shared NTM colormap (cool(3)) plus a Purdue-branded palette; M. +% tuberculosis is always black. Unrecognized labels fall back to blue. +% +% INPUTS +% speciesName - Species or label string (char or string). +% +% OUTPUTS +% c - 1x3 RGB triplet in [0,1]. +% +% AUTHORS +% Tyler Dierckman (updated by Trevor Shoaf) +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function c = getColor(speciesName) + + name = lower(char(speciesName)); + + % Purdue colors + purdueGold = [0.8078, 0.7216, 0.5333]; + purdueCampusGold= [0.7608, 0.5569, 0.0471]; + purdueBrown = [0.4196, 0.2706, 0.2118]; + purdueAgedGold = [0.5569, 0.4353, 0.2431]; + purdueDarkGray = [0.3333, 0.3490, 0.3765]; + purdueRushGold = [0.8549, 0.6667, 0.0000]; + + % NTM colormap (cool(3) — same palette for all drugs) + NTMColors = cool(3); + + if contains(name, 'tuberculosis'), c = [0, 0, 0]; % black + elseif contains(name, 'avium'), c = NTMColors(1,:); + elseif contains(name, 'kansasii'), c = NTMColors(2,:); + elseif contains(name, 'abscessus'), c = NTMColors(3,:); + elseif contains(name, 'massiliense'), c = purdueCampusGold; + elseif contains(name, 'fortuitum'), c = [0.40, 0.00, 0.80]; % purple + elseif contains(name, 'chelonae'), c = purdueBrown; + elseif contains(name, 'xenopi'), c = purdueRushGold; + elseif contains(name, 'auc'), c = purdueGold; + elseif contains(name, 'unk'), c = [0, 0, 1]; + else, c = [0, 0, 1]; % blue fallback + end +end \ No newline at end of file diff --git a/Methods/getLegendArray.m b/Methods/getLegendArray.m new file mode 100644 index 0000000..99d8398 --- /dev/null +++ b/Methods/getLegendArray.m @@ -0,0 +1,65 @@ +%% ======================================================================= +% getLegendArray.m — Build Figure Legend Entries for Species and +% PTA Targets +% ======================================================================== +% +% DESCRIPTION +% Builds a combined legend array for PTA figures. Species labels are added +% first (with M. tuberculosis moved to the front when present), followed +% by one formatted entry per PTA target of the form +% 'EC80 PTA: $[Cmax/MIC] \geq 3.2$'. Percent signs are escaped for LaTeX +% interpretation, and target values are formatted with %.3g. +% +% INPUTS +% uniqueTargets - Cell array; each entry is a cell whose +% first three elements are species/label, +% target type, and numeric target value. +% normalizedMICSpeciesAggregation - Cell array; each entry's first element +% is the species name. +% +% OUTPUTS +% legendArray - Cell array of legend entry strings +% (species entries followed by target +% entries). +% +% AUTHORS +% Tyler Dierckman +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function legendArray = getLegendArray(uniqueTargets, normalizedMICSpeciesAggregation) + + % ---------- Species labels ---------- + legendArray = {}; + + for i = 1:length(normalizedMICSpeciesAggregation) + speciesName = normalizedMICSpeciesAggregation{i}{1}; % e.g. 'M. tuberculosis' + + % Prioritize tuberculosis first if present + if contains(speciesName, 'tuberculosis') + legendArray = [{speciesName}, legendArray]; + else + legendArray{end + 1} = speciesName; + end + end + + % ---------- PTA target labels (short) ---------- + % e.g. 'EC80 PTA: [Cmax/MIC] ≥ 3.2' + for i = 1:length(uniqueTargets) + desc = uniqueTargets{i}{1}; % 'EC80 for hollow-fiber ...' + targetType = uniqueTargets{i}{2}; % 'Cmax/MIC' + targetVal = uniqueTargets{i}{3}; % numeric 3.2, 10.13 + + % Escape % for LaTeX + targetType = strrep(targetType, '%', '\%'); + ecLabel = strrep(desc, '%', '\%'); + + legendArray{end + 1} = sprintf('%s PTA: $[%s] \\geq %.3g$', ... + ecLabel, targetType, targetVal); + end +end \ No newline at end of file diff --git a/Methods/getUniqueMICs.m b/Methods/getUniqueMICs.m new file mode 100644 index 0000000..c1a90b2 --- /dev/null +++ b/Methods/getUniqueMICs.m @@ -0,0 +1,50 @@ +%% ======================================================================= +% getUniqueMICs.m — Extract Sorted Unique MIC Values Across Species +% ======================================================================== +% +% DESCRIPTION +% Collects every MIC value from a species/MIC-distribution aggregation, +% flattens them across all species, and returns the sorted unique set. +% Each species entry's second element is a cell array of {MIC, count} +% pairs; this function reads the MIC (first) element of each pair. +% +% INPUTS +% MICDistributionAggregation - Cell array; each entry's second element is +% a cell array of MIC entries, where each +% entry's first element is the MIC value. +% +% OUTPUTS +% uniqueMICs - Sorted vector of unique MIC values across +% all species. +% +% AUTHORS +% Tyler Dierckman +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function uniqueMICs = getUniqueMICs(MICDistributionAggregation) + + % Initialize an empty array to store all MIC values + micValues = []; + + % Loop through each species in the MICDistributionAggregation + for i = 1:length(MICDistributionAggregation) + + currentDistribution = MICDistributionAggregation{i}{2}; % Get the MIC distribution + + % Loop through the current distribution and extract the MIC values + for j = 1:length(currentDistribution) + micValues = [micValues; currentDistribution{j}{1}]; % Add each MIC value to the list + end + + end + + % Extract the unique MIC values from the list + uniqueMICs = unique(micValues); + +end diff --git a/Methods/normalizeMICsOfAggregation.m b/Methods/normalizeMICsOfAggregation.m new file mode 100644 index 0000000..bea7122 --- /dev/null +++ b/Methods/normalizeMICsOfAggregation.m @@ -0,0 +1,80 @@ +%% ======================================================================= +% normalizeMICsOfAggregation.m — Convert String MIC Labels to Numeric +% Values +% ======================================================================== +% +% DESCRIPTION +% Converts the string MIC labels in a species/MIC-distribution aggregation +% into numeric values, resolving censored-bin operators as follows: +% '<=' -> the numeric value itself +% '<' -> the numeric value itself +% '>=' -> the numeric value itself +% '>' -> twice the numeric value +% (none)-> str2double of the label +% The output preserves the input structure, replacing each {MIClabel, count} +% pair with {numericMIC, count}. +% +% INPUTS +% speciesAggregation - Cell array; each entry's second element +% is a cell array of {MIClabel, count} +% pairs (MIClabel is a string, possibly +% prefixed with <=, <, >=, or >). +% +% OUTPUTS +% normalizedMICSpeciesAggregation - Same structure, with each MIC label +% replaced by its numeric value. +% +% AUTHORS +% Tyler Dierckman +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function normalizedMICSpeciesAggregation = normalizeMICsOfAggregation(speciesAggregation) + % Preallocate the normalizedMICSpeciesAggregation array to hold normalized data + normalizedMICSpeciesAggregation = speciesAggregation; + + % Loop through each species in the speciesAggregation + for i = 1:length(speciesAggregation) + + currentSpecies = speciesAggregation{i}; % Get current species data + + % Loop through each tuple in the species' data + for j = 1:length(currentSpecies{2}) + + xValue = currentSpecies{2}{j}{1}; % Get the x-axis value (as a string) + count = currentSpecies{2}{j}{2}; % Get the associated count + + % Normalize the x-axis value based on the specified rules + if contains(xValue, '<=') + % For '<=', use the numeric value directly as the max value in the range + numValue = str2double(strrep(xValue, '<=', '')); + normalizedValue = numValue; % Treat as the maximum of that range + elseif contains(xValue, '<') + % For '<', treat as a small value (e.g., 0.125) + numValue = str2double(strrep(xValue, '<', '')); + normalizedValue = numValue; % Treat as the maximum of that range + elseif contains(xValue, '>=') + % For '>=', treat as the value directly + numValue = str2double(strrep(xValue, '>=', '')); + normalizedValue = numValue; % Keep the value as is + elseif contains(xValue, '>') + % For '>', double the numeric value + numValue = str2double(strrep(xValue, '>', '')); + normalizedValue = numValue * 2; % Double the value + else + % For other cases, simply convert the string to a numeric value + normalizedValue = str2double(xValue); + end + + % Store the normalized x-axis value and the count in the output array + normalizedMICSpeciesAggregation{i}{2}{j} = {normalizedValue, count}; + end + + end + +end diff --git a/Methods/plotAUC.m b/Methods/plotAUC.m new file mode 100644 index 0000000..7fd1038 --- /dev/null +++ b/Methods/plotAUC.m @@ -0,0 +1,207 @@ +%% ======================================================================= +% plotAUC.m — Plot Population AUC Distribution with Optional AUC/MIC +% Cutoff Annotation +% ======================================================================== +% +% DESCRIPTION +% Plots a histogram of per-patient 24-hour AUC values for a drug. When +% annotation is enabled, overlays vertical AUC/MIC cutoff lines (one per +% MIC per target) labeled with their MIC values, color-coded by target, +% with a legend. The bar color defaults to getColor('AUC') when available. +% +% INPUTS +% AUC24 - Vector of per-patient AUC values (mg*h/L). +% drugName - Drug name string (used as the figure name). +% +% Name-value pairs: +% 'Color' : Histogram bar color (RGB; default getColor +% ('AUC') or [0.9 0.8 0.6]). +% 'FontSize' : Base font size (default 20). +% 'NumBins' : Histogram bin count (default 80). +% 'AnnotateAUC_MICcutoffs' : Draw AUC/MIC cutoff lines (default false). +% 'UniqueTargets' : Target cells; required when annotating. +% 'MICs' : MIC vector; required when annotating. +% 'LegendArray' : Legend entries; required when annotating. +% +% OUTPUTS +% (none) — creates a docked figure. +% +% AUTHORS +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function plotAUC(AUC24, drugName, varargin) + + % ---------------- Input parsing ---------------- + p = inputParser; + p.FunctionName = 'plotAUC'; + + % Default bar color + defaultColor = [0.9 0.8 0.6]; + if exist('getColor','file') == 2 + try + defaultColor = getColor('AUC'); + catch + end + end + + addParameter(p, 'Color', defaultColor); + addParameter(p, 'FontSize', 20, @(x)isnumeric(x)&&isscalar(x)); + addParameter(p, 'NumBins', 80, @(x)isnumeric(x)&&isscalar(x)&&x>0); + addParameter(p, 'AnnotateAUC_MICcutoffs', false); + addParameter(p, 'UniqueTargets', {}); + addParameter(p, 'MICs', []); + addParameter(p, 'LegendArray', {}); % used for annotation legend + + parse(p, varargin{:}); + + color = p.Results.Color; + fontSize = p.Results.FontSize; + numBins = p.Results.NumBins; + annotateFlag = logical(p.Results.AnnotateAUC_MICcutoffs); + uniqueTargets = p.Results.UniqueTargets; + micVals = p.Results.MICs; + legendArray = p.Results.LegendArray; + + % Validate annotation inputs + if annotateFlag + if isempty(uniqueTargets) + error('plotAUC:MissingTargets', ... + 'UniqueTargets must be provided when annotation is ON.'); + end + if isempty(micVals) + error('plotAUC:MissingMICs', ... + 'MICs must be provided when annotation is ON.'); + end + if isempty(legendArray) + error('plotAUC:MissingLegendArray', ... + 'LegendArray must be provided when annotation is ON (same as in plotPTAs).'); + end + end + + % ---------------- Histogram ---------------- + AUCfig = figure("Name", drugName, 'Color','white'); + set(AUCfig,'WindowStyle','docked'); + + % if strlength(string(opts.FigureName)) > 0 + % set(fig,'Name',char(opts.FigureName)); + % elseif strlength(drugName) > 0 + % set(fig,'Name',char(drugName)); + % end + + hold on; + + histogram(AUC24, 'FaceColor', color, 'NumBins', numBins); + + xlabel('\bf{AUC (mg*h/L)}','Interpreter','latex'); + ylabel('\bf{Frequency}','Interpreter','latex'); + % title('\bf{Frequency of AUC in Population}','Interpreter','latex'); + + ax = gca; + ax.TickLabelInterpreter = 'latex'; + fontsize(AUCfig, fontSize, "points"); + + % Tight x-limits around data with padding + dataMin = min(AUC24(:)); + dataMax = max(AUC24(:)); + if dataMax == dataMin + dataMax = dataMin + 1; + end + xPad = 0.05*(dataMax - dataMin); + xL = [max(0, dataMin - xPad), dataMax + xPad]; + xlim(xL); + + % ---------------- AUC/MIC cutoff annotation ---------------- + if annotateFlag + + micVals = micVals(:)'; % row vector + targetColors = lines(numel(uniqueTargets)); + yL = ylim; + yTop = 0.95 * yL(2); + yBot = 0.15 * yL(2); + + % For legend: one dummy line handle per target + hLegLines = gobjects(numel(uniqueTargets),1); + + for iT = 1:numel(uniqueTargets) + tgt = uniqueTargets{iT}; + metric = tgt{2}; + thr = tgt{3}; + legendText = legendArray{iT}; + + % Compute AUC cutoffs + if contains(metric,'AUC','IgnoreCase',true) + allCutoffs = thr .* micVals; % one per MIC + micLabel = micVals; + else + allCutoffs = thr; + micLabel = nan(size(allCutoffs)); + end + + % Keep only those within current x-axis + inRange = (allCutoffs >= xL(1)) & (allCutoffs <= xL(2)); + aucCut = allCutoffs(inRange); + micUse = micLabel(inRange); + + if isempty(aucCut) + continue; + end + + % Dummy line for legend (NaN so nothing drawn, but style captured) + hLegLines(iT) = plot(nan, nan, '--', ... + 'Color', targetColors(iT,:), 'LineWidth', 1.3, ... + 'HandleVisibility','on'); + + % Compute vertical positions for labels for this target + nCut = numel(aucCut); + if nCut == 1 + yPos = (yTop + yBot) / 2; + yPositions = yPos; + else + yPositions = linspace(yTop, yBot, nCut); + end + + % Draw lines + labels + for k = 1:nCut + c = aucCut(k); + + % Actual line (hidden from legend to avoid duplicates) + xline(c,'--','Color',targetColors(iT,:), ... + 'LineWidth',1.3,'HandleVisibility','off'); + + % New simplified label text: ONLY the MIC + if ~isnan(micUse(k)) + txt = sprintf('MIC = %.3g', micUse(k)); + else + txt = sprintf('AUC = %.3g', c); % fallback if metric isn't AUC/MIC + end + + text(c, yPositions(k), txt, ... + 'Rotation', 90, ... + 'HorizontalAlignment','right', ... + 'VerticalAlignment','middle', ... + 'Interpreter','none', ... + 'FontSize', max(fontSize-6,8), ... + 'Color', targetColors(iT,:)); + end + end + + % Build legend for cutoff colors (only for those targets that had lines) + valid = isgraphics(hLegLines); + if any(valid) + lgd = legend(hLegLines(valid), legendArray(valid), ... + 'Interpreter','latex', ... + 'FontSize', max(fontSize-6, 8), ... + 'Location','northoutside'); + lgd.Color = 'none'; + lgd.EdgeColor = 'none'; + end + end + + hold off; +end \ No newline at end of file diff --git a/Methods/plotFullPTAFig.m b/Methods/plotFullPTAFig.m new file mode 100644 index 0000000..574ecd8 --- /dev/null +++ b/Methods/plotFullPTAFig.m @@ -0,0 +1,293 @@ +%% ======================================================================= +% plotFullPTAFig.m — Assemble the Combined Main-Text PTA Figure +% ======================================================================== +% +% DESCRIPTION +% Builds the multi-panel main-text PTA figure from the per-drug plotArgs +% payloads collected in PTAplotInfo. Drugs lacking a valid PTAMatrix are +% skipped. Each drug is rendered by calling plotPTAs into a temporary +% figure, then its contents are copied into a tile of a shared +% tiledlayout; per-drug legend strings can be replaced with hand-formatted +% overrides, and a grouped species (MIC-distribution) legend panel is +% placed in a trailing empty tile. +% +% INPUTS +% PTAplotInfo - Struct with one field per drug (AMK, BDQ, ... OMC); each +% field is either empty or a plotArgs struct containing +% uniqueMICs, PTAMatrix, normalizedMICSpeciesAggregation, +% and varargin (the name-value args originally passed to +% plotPTAs). +% +% OUTPUTS +% megaFig - Handle to the assembled figure (empty if no valid drugs). +% t - Handle to the tiledlayout (empty if no valid drugs). +% +% AUTHORS +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function [megaFig, t] = plotFullPTAFig(PTAplotInfo) + +% ---- Collect all PTAplot structs --------------------------------------- +allPTAplots = { ... + PTAplotInfo.AMK, 'Amikacin'; ... + PTAplotInfo.BDQ, 'Bedaquiline'; ... + PTAplotInfo.CFZ, 'Clofazimine'; ... + PTAplotInfo.CLR, 'Clarithromycin'; ... + PTAplotInfo.EMB, 'Ethambutol'; ... + PTAplotInfo.FOX, 'Cefoxitin'; ... + PTAplotInfo.IMI, 'Imipenem'; ... + PTAplotInfo.LZD, 'Linezolid'; ... + PTAplotInfo.MXF, 'Moxifloxacin'; ... + PTAplotInfo.RBT, 'Rifabutin'; ... + PTAplotInfo.RIF, 'Rifampicin'; ... + PTAplotInfo.TIG, 'Tigecycline'; ... + PTAplotInfo.OMC, 'Omadacycline'; ... +}; + +validMask = cellfun(@(s) isstruct(s) && isfield(s, 'PTAMatrix'), allPTAplots(:,1)); +validPTAs = allPTAplots(validMask, :); +nDrugs = size(validPTAs, 1); + +megaFig = []; +t = []; + +if nDrugs == 0 + warning('plotFullPTAFig: no valid PTAplot structs found.'); + return; +end + +% ---- Layout ------------------------------------------------------------- +nCols = ceil(sqrt(nDrugs * 1.6)); +nRows = ceil(nDrugs / nCols); + +megaFig = figure('Name', 'All Drugs — PTA Summary', ... + 'Color', 'white', ... + 'DefaultAxesFontName', 'Arial', ... + 'DefaultTextFontName', 'Arial', ... + 'DefaultLegendFontName', 'Arial'); + +t = tiledlayout(megaFig, nRows, nCols, ... + 'TileSpacing', 'compact', 'Padding', 'compact'); + +xlabel(t, 'MIC (mg/L)', 'FontSize', 20, 'FontWeight', 'bold'); +ylabel(t, 'PTA (%)', 'FontSize', 20, 'FontWeight', 'bold'); + +% title(t, 'Probability of Target Attainment — All Drugs', ... +% 'FontSize', 16, 'FontWeight', 'bold'); + +% Per-drug legend overrides — replace with custom (often line-wrapped) strings +nl = char(10); % literal newline + +legendOverrides = struct(); + +legendOverrides.Bedaquiline = { ... + ['MDR-TB 2 months PTA:' nl '$[AUC24/MIC] \geq 176$'], ... + ['MDR-TB 6 months PTA:' nl '$[AUC24/MIC] \geq 118$'], ... + ['MDR-TB 24 months PTA:' nl '$[AUC24/MIC] \geq 74.6$'] ... +}; + + +legendOverrides.Rifabutin = { ... + ['PTA: $[AUC24/MIC] \geq 9$'], ... + ['PTA: $[AUC24/MIC] \geq 18$'], ... + ['PTA: $[AUC24/MIC] \geq 36$'] ... + }; + +legendOverrides.Clarithromycin = {... + ['ELF PTA:' nl '$[AUC24/MIC] \geq 100$'], ... + ['Plasma PTA:' nl '$[AUC24/MIC] \geq 10$'] ... + }; + +legendOverrides.Moxifloxacin = { ... + ['PTA: $[fAUC24/MIC] \geq 53$' nl '(400 mg q24h)'], ... + ['PTA: $[fAUC24/MIC] \geq 53$' nl '(800 mg q24h)'] ... +}; +legendOverrides.Amikacin = {... + ['PTA: $[Cmax/MIC] \geq 8$'], ... + ['PTA: $[Cmax/MIC] \geq 10.13$'], ... + ['PTA: $[Cmax/MIC] \geq 12$'], ... + ['PTA: $[\%T > MIC] \geq 40$'] +}; +legendOverrides.Linezolid = {... + ['PTA:' nl '$[AUC24/MIC] \geq 119$']... +}; +legendOverrides.Omadacycline = {... + ['PTA: $[fAUC24/MIC] \geq 17$']... +}; +legendOverrides.Tigecycline = {... + ['PTA: $[AUC24/MIC] \geq 36.6$'],... + ['PTA: $[AUC24/MIC] \geq 44.6$'], ... + ['PTA: $[AUC24/MIC] \geq 42.3$'] +}; + + + +% ---- Draw each tile ----------------------------------------------------- +for d = 1:nDrugs + + plotArgs = validPTAs{d, 1}; + ax_tile = nexttile(t, d); + + tmpFig = plotPTAs(plotArgs.uniqueMICs, plotArgs.PTAMatrix, ... + plotArgs.normalizedMICSpeciesAggregation, ... + plotArgs.varargin{:}); + + tmpAx = findobj(tmpFig, 'Type', 'axes'); + tmpAx = tmpAx(end); + + copyobj(get(tmpAx,'Children'), ax_tile); + delete(findobj(ax_tile, 'Type', 'Text')); + + % Legend rebuild (curves only) + tmpLeg = findobj(tmpFig, 'Type', 'Legend'); + lineHandles = flipud(findobj(ax_tile, 'Type', 'line')); + if ~isempty(tmpLeg) && ~isempty(lineHandles) + legStrings = tmpLeg(1).String; + curveStrings = legStrings(end-numel(lineHandles)+1:end); + + % Apply override if defined for this drug + drugName = validPTAs{d,2}; + fieldKey = matlab.lang.makeValidName(drugName); + if isfield(legendOverrides, fieldKey) ... + && numel(legendOverrides.(fieldKey)) == numel(curveStrings) + curveStrings = legendOverrides.(fieldKey); + end + + legend(ax_tile, lineHandles, curveStrings, ... + 'FontSize', 12, ... + 'Location', 'northeast', ... + 'Interpreter', 'latex', ... + 'Box', 'on'); + end + + % Axis styling + ax_tile.XLim = tmpAx.XLim; + ax_tile.YLim = tmpAx.YLim; + ax_tile.XTick = tmpAx.XTick; + ax_tile.XTickLabel = tmpAx.XTickLabel; + ax_tile.YGrid = 'on'; + ax_tile.Box = 'off'; + ax_tile.Layer = 'top'; + ax_tile.FontSize = 14; + + close(tmpFig); + + % Title (drug name + dose regimen) + doseLabel = ''; + + vIdx = find(strcmpi(plotArgs.varargin, 'DoseLabel'), 1); + if ~isempty(vIdx) && vIdx < numel(plotArgs.varargin) ... + && ~isempty(plotArgs.varargin{vIdx + 1}) + doseLabel = plotArgs.varargin{vIdx + 1}; + else + % Fallback: build from DoseSize + DoseFrequency + sIdx = find(strcmpi(plotArgs.varargin, 'DoseSize'), 1); + fIdx = find(strcmpi(plotArgs.varargin, 'DoseFrequency'), 1); + if ~isempty(sIdx) && ~isempty(fIdx) + doseSize = plotArgs.varargin{sIdx + 1}; + doseFreq = plotArgs.varargin{fIdx + 1}; + doseLabel = sprintf('%g mg q%gh', doseSize, doseFreq); + end + end + + if ~isempty(doseLabel) + title(ax_tile, sprintf('%s (%s)', validPTAs{d,2}, doseLabel), ... + 'FontWeight','bold','FontSize',18); + else + title(ax_tile, validPTAs{d,2}, 'FontWeight','bold',... + 'FontSize',18); + end + % xlabel(ax_tile, 'MIC (mg/L)'); + % ylabel(ax_tile, 'PTA (%)'); + + % % Panel label + % text(ax_tile, -0.04, 1.06, sprintf('(%s)', char('a'+d-1)), ... + % 'Units','normalized','FontWeight','bold','FontSize',18); +end + +% ---- Legend + Caption Panels ------------------------------------------- +nEmpty = nRows * nCols - nDrugs; + +if nEmpty >= 2 + + % Blank tiles + for d = nDrugs+1 : nRows*nCols - 2 + axis(nexttile(t,d),'off'); + end + + % ===== GROUPED LEGEND PANEL ===== + ax_note = nexttile(t, nRows*nCols - 1); + axis(ax_note, 'off'); + hold(ax_note, 'on'); + + speciesNames = { ... + 'M. tuberculosis', ... + 'M. avium', ... + 'M. kansasii', ... + 'M. abscessus', ... + 'M. fortuitum' ... + }; + + speciesKeys = { ... + 'tuberculosis', ... + 'avium', ... + 'kansasii', ... + 'abscessus', ... + 'fortuitum'... + }; + + nSpecies = numel(speciesNames); + + hLegend = gobjects(nSpecies,1); + + for si = 1:nSpecies + + % MIC-bar fill color: TB grey, unknown blue, others from getColor + if strcmpi(speciesKeys{si}, 'tuberculosis') + markerColor = [0.7 0.7 0.7]; + else + markerColor = getColor(speciesKeys{si}); + end + + % Square only — represents the MIC distribution bars. + % PTA curves are all black, so species are identified by bar color alone. + hLegend(si) = plot(ax_note, NaN, NaN, 's', ... + 'LineStyle', 'none', ... + 'MarkerFaceColor', markerColor, ... + 'MarkerEdgeColor', 'k', ... + 'MarkerSize', 22); + end + + lh = legend(ax_note, hLegend, speciesNames, ... + 'Location', 'best', ... + 'FontSize', 24, ... + 'Box', 'on'); + + lh.Title.String = 'Species (MIC distribution)'; + lh.Title.FontWeight = 'bold'; + lh.ItemTokenSize = [40, 18]; + + % %% ===== CAPTION PANEL ===== + % ax_cap = nexttile(t, nRows*nCols); + % axis(ax_cap, 'off'); + % + % text(ax_cap, 0.5, 0.5, sprintf([ ... + % 'Monte Carlo PopPK Simulation\n' ... + % 'N = 10,000 patients\n' ... + % 'PTA at steady state']), ... + % 'HorizontalAlignment','center', ... + % 'FontSize',24); + +else + for d = nDrugs+1 : nRows*nCols + axis(nexttile(t,d),'off'); + end +end +set(findall(megaFig, '-property', 'FontName'), 'FontName', 'Arial'); +end \ No newline at end of file diff --git a/Methods/plotPTAs.m b/Methods/plotPTAs.m new file mode 100644 index 0000000..fc0309b --- /dev/null +++ b/Methods/plotPTAs.m @@ -0,0 +1,316 @@ +%% ======================================================================= +% plotPTAs.m — Plot PTA Curves with Overlaid MIC Distribution Bars +% ======================================================================== +% +% DESCRIPTION +% Plots one or more probability-of-target-attainment (PTA) curves against +% a shared MIC axis, with per-species MIC distribution bars overlaid as a +% grouped/stacked backdrop. PTA curves are black by default and +% distinguished by line style + marker (cycling every four curves); +% per-curve colors can be opted in via 'RegimenColors'. M. tuberculosis +% bars are drawn grey/translucent; other species are colored via getColor. +% Optional 95% Wilson-style CI ribbons can be drawn from NumPatients. +% Returns the figure handle and a plotArgs struct that bundles the inputs +% (including the original varargin) so the panel can be regenerated by +% plotFullPTAFig / plotSuppPTAFig. +% +% REQUIRED INPUTS +% uniqueMICs [1 x nMIC] numeric MIC breakpoints. +% PTAMatrix [nMIC x nCurves] PTA values (0-1). +% normalizedMICSpeciesAggregation Cell array of normalized MIC +% distributions (from +% normalizeMICsOfAggregation). +% +% OPTIONAL NAME-VALUE PAIRS +% 'DrugName' Title/figure label (default 'Drug'). +% 'FontSize' Global figure font size (default 12). +% 'DoseSize' Dose size (mg) for the auto title line. +% 'DoseFrequency' Dose interval (h) for the auto title line. +% 'DoseLabel' Custom middle title line; overrides DoseSize/Frequency. +% 'RegimenColors' [nCurves x 3] per-curve RGB; [] uses default black + +% style cycling. +% 'ShowBars' Show MIC distribution bars (default true). +% 'LegendArray' Legend entries: species first, then PTA curves in +% column order; {} uses the MATLAB default legend. +% 'NumPatients' N patients (scalar or [nMIC x 1]) for CI ribbons. +% 'ShowCI' Draw CI ribbons (default false). +% 'UniqueTargets' Target metadata {description, type, value, group}; used +% for group-based coloring fallback and footnote. +% +% OUTPUTS +% fig - Handle to the created figure. +% plotArgs - Struct bundling uniqueMICs, PTAMatrix, +% normalizedMICSpeciesAggregation, and the original varargin. +% +% AUTHORS +% Tyler Dierckman, Trevor Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function [fig, plotArgs] = plotPTAs(uniqueMICs, PTAMatrix, normalizedMICSpeciesAggregation, varargin) + +% ---- Input parsing ------------------------------------------------------- +p = inputParser; +p.FunctionName = 'plotPTAs'; + +addParameter(p, 'DoseSize', [], @(x) isnumeric(x) && isscalar(x)); +addParameter(p, 'DoseFrequency', "", @(x) ischar(x) || isstring(x) || isnumeric(x)); +addParameter(p, 'DrugName', "Drug", @(x) ischar(x) || isstring(x)); +addParameter(p, 'FontSize', 12, @(x) isnumeric(x) && isscalar(x)); +addParameter(p, 'ShowBars', true, @(x) islogical(x) || ismember(x,[0 1])); +addParameter(p, 'LegendArray', {}, @(x) iscell(x) || isempty(x)); +addParameter(p, 'UniqueTargets', {}, @(x) iscell(x) || isempty(x)); +addParameter(p, 'NumPatients', [], @(x) isnumeric(x)); +addParameter(p, 'ShowCI', false, @(x) islogical(x) || ismember(x,[0 1])); +addParameter(p, 'DoseLabel', "", @(x) ischar(x) || isstring(x)); +addParameter(p, 'RegimenColors', [], @(x) isempty(x) || (isnumeric(x) && size(x,2) == 3)); + +parse(p, varargin{:}); +opts = p.Results; + +drugName = char(opts.DrugName); +doseSize = opts.DoseSize; +doseFrequency = opts.DoseFrequency; +fontSize = opts.FontSize; +showBars = logical(opts.ShowBars); +legendArray = opts.LegendArray; +uniqueTargets = opts.UniqueTargets; +Npatients = opts.NumPatients; +showCI = logical(opts.ShowCI); +doseLabel = string(opts.DoseLabel); +regimenColors = opts.RegimenColors; + +nMIC = numel(uniqueMICs); +nCurve = size(PTAMatrix, 2); + +% ---- Target group / dose index assignment (legacy coloring support) ------ +% These are only used when regimenColors is empty (i.e. the caller has not +% supplied per-curve colors and wants the original group-based style logic). + +targetGroup = repmat("unknown", 1, nCurve); +targetIndexPerCurve = 1:nCurve; +doseIndexPerCurve = ones(1, nCurve); + +if ~isempty(uniqueTargets) + nTarg = numel(uniqueTargets); + if mod(nCurve, nTarg) == 0 + curvesPerTarget = nCurve / nTarg; + + baseGroups = strings(1, nTarg); + for t = 1:nTarg + ti = uniqueTargets{t}; + if numel(ti) >= 4 && ~isempty(ti{4}) + baseGroups(t) = lower(string(ti{4})); + else + baseGroups(t) = "unknown"; + end + end + + targetGroup = repelem(baseGroups, curvesPerTarget); + targetIndexPerCurve = repelem(1:nTarg, curvesPerTarget); + doseIndexPerCurve = repmat(1:curvesPerTarget, 1, nTarg); + else + warning('plotPTAs:GroupMismatch', ... + 'nCurve (%d) not divisible by numel(uniqueTargets) (%d); using "unknown" groups.', ... + nCurve, nTarg); + end +end + +% ---- CI vector ----------------------------------------------------------- +if isempty(Npatients) + Nvec = []; +elseif isscalar(Npatients) + Nvec = repmat(Npatients, nMIC, 1); +elseif numel(Npatients) == nMIC + Nvec = Npatients(:); +else + warning('plotPTAs:NpatientsMismatch', ... + 'NumPatients length (%d) does not match nMIC (%d); disabling CI.', ... + numel(Npatients), nMIC); + Nvec = []; +end + +% ---- Figure / axes setup ------------------------------------------------ +fig = figure('Name', drugName,'Color','white'); +set(fig, 'WindowStyle', 'docked'); +hold on; +ax = gca; +ax.Box = 'off'; +ax.YGrid = 'on'; +ax.GridAlpha = 0.15; +ax.Layer = 'top'; + +% ---- MIC distribution bar charts ---------------------------------------- +hSpecies = gobjects(0); + +if showBars + MICMatrix = []; + TBMatrix = []; + + for i = 1:numel(normalizedMICSpeciesAggregation) + speciesName = normalizedMICSpeciesAggregation{i}{1}; + speciesData = normalizedMICSpeciesAggregation{i}{2}; + + micDist = zeros(1, nMIC); + totalCount = sum(cellfun(@(x) x{2}, speciesData)); + + for j = 1:numel(speciesData) + micVal = speciesData{j}{1}; + micCount = speciesData{j}{2}; + idx = find(uniqueMICs == micVal); + if ~isempty(idx) + micDist(idx) = (micCount / totalCount) * 100; + end + end + + if contains(lower(speciesName), 'tuberculosis') + TBMatrix = [TBMatrix; micDist]; %#ok + else + MICMatrix = [MICMatrix; micDist]; %#ok + end + end + + % M. tuberculosis bars — grey, slightly transparent + if ~isempty(TBMatrix) + hTB = bar(TBMatrix, 'FaceAlpha', 0.4, ... + 'FaceColor', [0.4 0.4 0.4], 'EdgeColor', [0 0 0]); + hSpecies(end+1) = hTB(1); + end + + % All other NTM species bars — colored, grouped + if ~isempty(MICMatrix) + hBars = bar(MICMatrix.', 'grouped'); + for k = 1:numel(hBars) + hBars(k).FaceColor = 'flat'; + hBars(k).FaceAlpha = 0.8; + hBars(k).EdgeColor = [0 0 0]; + hBars(k).LineWidth = 0.5; + + % getColor maps species name -> fixed RGB (defined at bottom) + spName = normalizedMICSpeciesAggregation{k}{1}; + hBars(k).CData = repmat(getColor(spName), size(hBars(k).CData, 1), 1); + end + hSpecies = [hSpecies, hBars(:).']; %#ok + end +end + +% ---- PTA curves --------------------------------------------------------- + +hPTA = gobjects(nCurve, 1); +xVals = 1:nMIC; + +% Style cycles used when RegimenColors is supplied +markerCycle = {'o','x','+','s','d','^'}; +styleCycle = {'-','--',':','-.'}; + +% Group counter only matters in the legacy (no RegimenColors) path +groupCounts = containers.Map(); + +for i = 1:nCurve + pta_vals = PTAMatrix(:, i); + + % All PTA lines black; markers + line styles distinguish curves. + % Drugs have at most 4 targets, so each curve gets a unique marker. + if ~isempty(regimenColors) + lineColor = regimenColors(i, :); % opt-in override preserved + else + lineColor = [0 0 0]; + end + + switch mod(i-1, 4) + 1 + case 1, lineStyle='-'; markerStyle='o'; markerSize=4; + case 2, lineStyle='--'; markerStyle='x'; markerSize=12; + case 3, lineStyle=':'; markerStyle='+'; markerSize=12; + case 4, lineStyle='-.'; markerStyle='s'; markerSize=8; + end + + lineWidth = 2.5; + + % --- Optional 95% CI ribbon --- + if ~isempty(Nvec) && showCI + se = sqrt(pta_vals .* (1 - pta_vals) ./ Nvec); + y = 100 * pta_vals; + yL = max(0, y - 1.96 * 100 * se); + yU = min(100, y + 1.96 * 100 * se); + + xx = [xVals, fliplr(xVals)]; + yy = [yL', fliplr(yU')]; + + hFill = fill(xx, yy, lineColor, ... + 'FaceAlpha', 0.12, 'EdgeColor', 'none', 'Parent', ax); + set(hFill, 'HandleVisibility', 'off'); + end + + % --- Main PTA line --- + hPTA(i) = plot(xVals, 100 * pta_vals, ... + 'LineStyle', lineStyle, ... + 'LineWidth', lineWidth, ... + 'Color', lineColor, ... + 'Marker', markerStyle, ... + 'MarkerFaceColor', lineColor, ... + 'MarkerSize', markerSize); +end + +% ---- Title -------------------------------------------------------------- +% Middle title line: prefer DoseLabel if set, else build from DoseSize/Freq. +if strlength(doseLabel) > 0 + doseLine = sprintf('\\bf{%s}', char(doseLabel)); +elseif ~isempty(doseSize) + if isempty(doseFrequency) || (isstring(doseFrequency) && strlength(doseFrequency) == 0) + doseLine = sprintf('\\bf{Dose: %g mg}', doseSize); + elseif isnumeric(doseFrequency) + doseLine = sprintf('\\bf{Dose: %g mg, q%gh}', doseSize, doseFrequency); + else + doseLine = sprintf('\\bf{Dose: %g mg, %s}', doseSize, char(doseFrequency)); + end +else + doseLine = '\bf{Dose regimen not specified}'; +end + +if ~isempty(Nvec) + Nline = sprintf('\\bf{Monte Carlo PopPK simulation, N = %d virtual patients}', max(Nvec)); + ttl = {sprintf('\\bf{Probability of Target Attainment for %s}', drugName), doseLine, Nline}; +else + ttl = {sprintf('\\bf{Probability of Target Attainment for %s}', drugName), doseLine}; +end + +title(ttl, 'Interpreter', 'latex'); + +% ---- Axes --------------------------------------------------------------- +ax.XTick = xVals; +ax.XTickLabel = uniqueMICs; +xlim([0.35, nMIC + 0.85]); +ylim([0, 101]); +xlabel('\bf{MIC mg/L}', 'Interpreter', 'latex'); +ylabel('\bf{Percentage (\%)}','Interpreter', 'latex'); + +% ---- Legend ------------------------------------------------------------- +if ~isempty(legendArray) + handlesForLegend = [hSpecies(:).', hPTA(:).']; + lgd = legend(handlesForLegend, legendArray, ... + 'Interpreter', 'latex', 'FontSize', 8); +else + lgd = legend('show'); +end +lgd.Color = 'none'; +lgd.EdgeColor = 'none'; + +% ---- Global font size --------------------------------------------------- +try + fontsize(fig, fontSize, 'points'); +catch + set(ax, 'FontSize', fontSize); +end + +% ---- Bundle inputs for figure regeneration ------------------------------ +plotArgs.uniqueMICs = uniqueMICs; +plotArgs.PTAMatrix = PTAMatrix; +plotArgs.normalizedMICSpeciesAggregation = normalizedMICSpeciesAggregation; +plotArgs.varargin = varargin; + +end % plotPTAs diff --git a/Methods/plotSuppPTAFig.m b/Methods/plotSuppPTAFig.m new file mode 100644 index 0000000..9f365bf --- /dev/null +++ b/Methods/plotSuppPTAFig.m @@ -0,0 +1,209 @@ +%% ======================================================================= +% plotSuppPTAFig.m — Assemble the Supplementary 3x2 PTA Figure +% ======================================================================== +% +% DESCRIPTION +% Builds the supplementary multi-panel PTA figure comparing alternative +% dosing scenarios (cefoxitin 2000/4000 mg TID, imipenem 1000 mg TID, +% tigecycline 50/100 mg BID). Each scenario is rendered by calling +% plotPTAs into a temporary figure and copied into a tile of a 3x2 +% tiledlayout; per-drug legend strings may be replaced with hand-formatted +% overrides, and tile 4 holds a shared species (MIC-distribution) legend. +% Scenarios lacking a valid PTAMatrix are skipped. +% +% INPUTS +% PTAplotInfoSupp - Struct with fields FOX_2000mgTID, FOX_4000mgTID, +% IMI_1000mgTID, TIG_50mgBID, TIG_100mgBID; each is a +% plotArgs struct (uniqueMICs, PTAMatrix, +% normalizedMICSpeciesAggregation, varargin) or empty. +% +% OUTPUTS +% suppFig - Handle to the assembled figure (empty if no valid scenarios). +% t - Handle to the tiledlayout (empty if no valid scenarios). +% +% AUTHORS +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function [suppFig, t] = plotSuppPTAFig(PTAplotInfoSupp) + +% ---- Tile specification ------------------------------------------------- +% { tileNumber, PTAplotStruct, drugName, doseLabel } +plotSpec = { ... + 1, PTAplotInfoSupp.FOX_2000mgTID, 'Cefoxitin', '2000 mg TID'; ... + 2, PTAplotInfoSupp.FOX_4000mgTID, 'Cefoxitin', '4000 mg TID'; ... + 3, PTAplotInfoSupp.IMI_1000mgTID, 'Imipenem', '1000 mg TID'; ... + 5, PTAplotInfoSupp.TIG_50mgBID, 'Tigecycline', '50 mg BID'; ... + 6, PTAplotInfoSupp.TIG_100mgBID, 'Tigecycline', '100 mg BID'; ... +}; + +legendTile = 4; % reserved for species bar legend + +% ---- Validate ----------------------------------------------------------- +validMask = cellfun(@(s) isstruct(s) && isfield(s,'PTAMatrix'), plotSpec(:,2)); +plotSpec = plotSpec(validMask, :); +nDrugs = size(plotSpec, 1); + +suppFig = []; +t = []; + +if nDrugs == 0 + warning('plotSuppPTAFig: no valid PTAplot structs found.'); + return; +end + +% ---- Figure / layout ---------------------------------------------------- +nRows = 3; +nCols = 2; + +suppFig = figure('Name', 'Supplementary Figure -- PTA Summary', ... + 'Color', 'white', ... + 'DefaultAxesFontName', 'Arial', ... + 'DefaultTextFontName', 'Arial', ... + 'DefaultLegendFontName', 'Arial'); + +t = tiledlayout(suppFig, nRows, nCols, ... + 'TileSpacing', 'compact', 'Padding', 'compact'); + +xlabel(t, 'MIC (mg/L)', 'FontSize', 20, 'FontWeight', 'bold'); +ylabel(t, 'PTA (%)', 'FontSize', 20, 'FontWeight', 'bold'); + +% ---- Per-drug legend overrides ----------------------------------------- +nl = char(10); %#ok + +legendOverrides = struct(); + +legendOverrides.Cefoxitin = { ... + 'PTA: $[\%T > MIC] \geq 40$', ... + 'PTA: $[\%T > MIC] \geq 50$', ... + 'PTA: $[\%T > MIC] \geq 70$' ... +}; + +legendOverrides.Imipenem = { ... + 'PTA: $[\%T > MIC] \geq 50$' ... +}; + +legendOverrides.Tigecycline = { ... + ['EC80 MAB PTA:' nl '$[AUC24/MIC] \geq 36.6$'], ... + ['1-log kill MAB PTA:' nl '$[AUC24/MIC] \geq 44.6$'], ... + 'TB PTA: $[AUC24/MIC] \geq 42.3$' ... +}; + +% ---- Draw each drug tile ----------------------------------------------- +for d = 1:nDrugs + + tileNum = plotSpec{d, 1}; + plotArgs = plotSpec{d, 2}; + drugName = plotSpec{d, 3}; + doseLabel = plotSpec{d, 4}; + + ax_tile = nexttile(t, tileNum); + + tmpFig = plotPTAs(plotArgs.uniqueMICs, plotArgs.PTAMatrix, ... + plotArgs.normalizedMICSpeciesAggregation, ... + plotArgs.varargin{:}); + + tmpAx = findobj(tmpFig, 'Type', 'axes'); + tmpAx = tmpAx(end); + + copyobj(get(tmpAx, 'Children'), ax_tile); + delete(findobj(ax_tile, 'Type', 'Text')); + + % --- Legend rebuild (curves only, with override if defined) --- + tmpLeg = findobj(tmpFig, 'Type', 'Legend'); + lineHandles = flipud(findobj(ax_tile, 'Type', 'line')); + if ~isempty(tmpLeg) && ~isempty(lineHandles) + legStrings = tmpLeg(1).String; + curveStrings = legStrings(end - numel(lineHandles) + 1 : end); + + fieldKey = matlab.lang.makeValidName(drugName); + if isfield(legendOverrides, fieldKey) ... + && numel(legendOverrides.(fieldKey)) == numel(curveStrings) + curveStrings = legendOverrides.(fieldKey); + end + + legend(ax_tile, lineHandles, curveStrings, ... + 'FontSize', 12, ... + 'Location', 'northeast', ... + 'Interpreter', 'latex', ... + 'Box', 'on'); + end + + % --- Axis styling --- + ax_tile.XLim = tmpAx.XLim; + ax_tile.YLim = tmpAx.YLim; + ax_tile.XTick = tmpAx.XTick; + ax_tile.XTickLabel = tmpAx.XTickLabel; + ax_tile.YGrid = 'on'; + ax_tile.Box = 'off'; + ax_tile.Layer = 'top'; + ax_tile.FontSize = 14; + + close(tmpFig); + + % --- Title --- + title(ax_tile, sprintf('%s (%s)', drugName, doseLabel), ... + 'FontWeight', 'bold', 'FontSize', 18); + + % --- Panel label: uses loop index d, so labels go a,b,c,d,e in + % reading order (legend tile is skipped, gets no label). + text(ax_tile, -0.04, 1.06, sprintf('(%s)', char('a' + d - 1)), ... + 'Units', 'normalized', 'FontWeight', 'bold', 'FontSize', 18); +end + +% ---- Species bar legend tile ------------------------------------------- +ax_note = nexttile(t, legendTile); +axis(ax_note, 'off'); +hold(ax_note, 'on'); + +speciesNames = { ... + 'M. tuberculosis', ... + 'M. avium', ... + 'M. kansasii', ... + 'M. abscessus', ... + 'M. fortuitum'... +}; + +speciesKeys = { ... + 'tuberculosis', ... + 'avium', ... + 'kansasii', ... + 'abscessus', ... + 'fortuitum'... +}; + +nSpecies = numel(speciesNames); +hLegend = gobjects(nSpecies, 1); + +for si = 1:nSpecies + if strcmpi(speciesKeys{si}, 'tuberculosis') + markerColor = [0.7 0.7 0.7]; + else + markerColor = getColor(speciesKeys{si}); + end + + % Square only — PTA curves are all black, so species are identified + % solely by MIC-bar color. + hLegend(si) = plot(ax_note, NaN, NaN, 's', ... + 'LineStyle', 'none', ... + 'MarkerFaceColor', markerColor, ... + 'MarkerEdgeColor', 'k', ... + 'MarkerSize', 22); +end + +lh = legend(ax_note, hLegend, speciesNames, ... + 'Location', 'best', ... + 'FontSize', 20, ... + 'Box', 'on'); +lh.Title.String = 'Species (MIC distribution)'; +lh.Title.FontWeight = 'bold'; +lh.ItemTokenSize = [40, 18]; + +set(findall(suppFig, '-property', 'FontName'), 'FontName', 'Arial'); + +end \ No newline at end of file diff --git a/Methods/plotTimeCourses.m b/Methods/plotTimeCourses.m new file mode 100644 index 0000000..c6fcf09 --- /dev/null +++ b/Methods/plotTimeCourses.m @@ -0,0 +1,253 @@ +%% ======================================================================= +% plotTimeCourses.m — Plot Concentration Time Courses with Optional +% Mean / Percentile Overlays +% ======================================================================== +% +% DESCRIPTION +% Plots per-patient concentration time courses for a population, with +% optional mean and percentile overlays. All inputs are name-value pairs. +% Supports a stats-only mode (raw lines hidden), dose-aware x-axis ticks +% formatted as "Xd Yh", and configurable line widths/colors. Returns the +% figure handle. +% +% REQUIRED NAME-VALUE PAIRS +% 'X' - [nTime x 1] time vector (h). +% 'Y' - [nTime x nPts] concentration matrix (e.g. mg/L). +% +% RECOMMENDED NAME-VALUE PAIRS +% 'DrugName', 'Compartment', 'Cycles', 'DoseInterval'. +% +% OPTIONAL NAME-VALUE PAIRS +% 'FigureNumber', 'FigureName', 'WindowStyle', 'XLabel', 'YLabel', +% 'FontSize', 'DoStats', 'Percentiles' (default [10 50 90]), +% 'MeanLineWidth', 'PctLineWidth', 'HideRawWhenStats', 'RawLineWidth', +% 'RawColor', 'StatsOnTop', 'Subtitle'. +% +% OUTPUTS +% fig - Handle to the created figure. +% +% AUTHORS +% Trevor Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function fig = plotTimeCourses(varargin) + +% ------------------------------------------------------------ +% Parse inputs +% ------------------------------------------------------------ +p = inputParser; +p.FunctionName = 'plotTimeCourses'; + +addParameter(p,'X',[],@(x)isnumeric(x)&&isvector(x)&&~isempty(x)); +addParameter(p,'Y',[],@(x)isnumeric(x)&&~isempty(x)); + +addParameter(p,'DrugName',"",@(x)ischar(x)||isstring(x)); +addParameter(p,'Compartment',"",@(x)ischar(x)||isstring(x)); +addParameter(p,'Cycles',[],@(x)isnumeric(x)&&isscalar(x)); +addParameter(p,'DoseInterval',[],@(x)isnumeric(x)&&isscalar(x)); + +addParameter(p,'FigureNumber',[],@(x)(isnumeric(x)&&isscalar(x)) || isempty(x)); +addParameter(p,'FigureName',"",@(x)ischar(x)||isstring(x)); +addParameter(p,'WindowStyle','docked',@(s)ischar(s)||isstring(s)); + +addParameter(p,'XLabel','Time (h)',@(x)ischar(x)||isstring(x)); +addParameter(p,'YLabel','Drug concentration [mg/L]',@(x)ischar(x)||isstring(x)); + +addParameter(p,'FontSize',14,@(x)isnumeric(x)&&isscalar(x)); + +addParameter(p,'DoStats',false,@(x)islogical(x)&&isscalar(x)); +addParameter(p,'Percentiles',[10 50 90],@(x)isnumeric(x)&&isvector(x)&&~isempty(x)); + +addParameter(p,'MeanLineWidth',4,@(x)isnumeric(x)&&isscalar(x)&&x>0); +addParameter(p,'PctLineWidth',4,@(x)isnumeric(x)&&isscalar(x)&&x>0); + +addParameter(p,'HideRawWhenStats',true,@(x)islogical(x)&&isscalar(x)); +addParameter(p,'RawLineWidth',0.75,@(x)isnumeric(x)&&isscalar(x)&&x>0); +addParameter(p,'RawColor',[],@(c)isempty(c) || (isnumeric(c)&&isequal(size(c),[1 3]))); +addParameter(p,'StatsOnTop',true,@(x)islogical(x)&&isscalar(x)); + +addParameter(p,'Subtitle',"",@(x)ischar(x)||isstring(x)); + +parse(p,varargin{:}); +opts = p.Results; + +x = opts.X(:); +Y = opts.Y; + +if isempty(x) || isempty(Y) + error('plotTimeCourses:MissingXY', ... + 'You must pass both ''X'' and ''Y'' as name–value pairs.'); +end + +if size(Y,1) ~= numel(x) + error('plotTimeCourses:SizeMismatch', ... + 'X must have length nTime and Y must be [nTime x nPts]. Got numel(X)=%d, size(Y,1)=%d', ... + numel(x), size(Y,1)); +end + +drugName = string(opts.DrugName); +compartment = string(opts.Compartment); +cycles = opts.Cycles; +doseInt = opts.DoseInterval; + +% ------------------------------------------------------------ +% Create figure / axes +% ------------------------------------------------------------ +if isempty(opts.FigureNumber) + fig = figure("Name", char(opts.FigureName),'Color','white'); +else + fig = figure(opts.FigureNumber); +end + +if strlength(string(opts.FigureName)) > 0 + set(fig,'Name',char(opts.FigureName)); +elseif strlength(drugName) > 0 + set(fig,'Name',char(drugName)); +end + +set(fig,'WindowStyle',char(opts.WindowStyle)); + +ax = gca; +hold(ax,'on'); +ax.Box = 'off'; +ax.YGrid = 'on'; +ax.XGrid = 'off'; +ax.GridAlpha = 0.15; +ax.Layer = 'top'; + +% ------------------------------------------------------------ +% Plot raw timecourses +% ------------------------------------------------------------ +hRaw = gobjects(0); + +plotRaw = true; +if opts.DoStats && opts.HideRawWhenStats + plotRaw = false; % stats-only mode +end + +if plotRaw + if isempty(opts.RawColor) + hRaw = plot(ax, x, Y, 'LineWidth', opts.RawLineWidth); + else + % Force all raw lines to the same faint-ish color if you want + hRaw = plot(ax, x, Y, 'LineWidth', opts.RawLineWidth, 'Color', opts.RawColor); + end +end + +% ------------------------------------------------------------ +% Optional stats overlays (mean + percentiles) +% ------------------------------------------------------------ +hMean = gobjects(0); +hPct = gobjects(0); + +if opts.DoStats + yMean = mean(Y, 2, 'omitnan'); + + % Compute percentiles safely (Y is [nTime x nPts]) + pct = opts.Percentiles(:)'; % row + yPct = prctile(Y', pct, 1)'; % -> [nTime x numPct] + + % Make stats clearly visible + meanLW = max(opts.MeanLineWidth, 6); + pctLW = max(opts.PctLineWidth, 5); + + hMean = plot(ax, x, yMean, 'k', 'LineWidth', meanLW); + hPct = plot(ax, x, yPct, 'k:', 'LineWidth', pctLW); + + if opts.StatsOnTop + try + uistack([hMean; hPct(:)], 'top'); + catch + % older MATLAB: plotting last usually already puts them on top + end + end +end + +% ------------------------------------------------------------ +% Labels + title +% ------------------------------------------------------------ +xlabel(ax, opts.XLabel); +ylabel(ax, opts.YLabel); + +titleLines = strings(0,1); + +if strlength(drugName) > 0 + titleLines(end+1) = sprintf('\\bf{Drug Concentration Timecourses for %s}', char(drugName)); +end + +if ~isempty(cycles) && isfinite(cycles) && strlength(compartment) > 0 + titleLines(end+1) = sprintf('\\bf{First %d doses in %s compartment}', cycles, char(compartment)); +elseif ~isempty(cycles) && isfinite(cycles) + titleLines(end+1) = sprintf('\\bf{First %d doses}', cycles); +elseif strlength(compartment) > 0 + titleLines(end+1) = sprintf('\\bf{%s compartment}', char(compartment)); +end + +if isempty(titleLines) + titleLines = "\\bf{Drug Concentration Timecourses}"; +end + +if strlength(string(opts.Subtitle)) > 0 + % Replace the auto-generated second line with the custom subtitle + if numel(titleLines) > 1 + titleLines(2) = sprintf('\\bf{%s}', char(opts.Subtitle)); + else + titleLines(end+1) = sprintf('\\bf{%s}', char(opts.Subtitle)); + end +end + +% title(ax, cellstr(titleLines), 'Interpreter','latex'); + +% ------------------------------------------------------------ +% X limits + ticks +% ------------------------------------------------------------ +if ~isempty(doseInt) && isfinite(doseInt) && ~isempty(cycles) && isfinite(cycles) + xlim(ax, [doseInt*(cycles-2) doseInt*(cycles)]); + % xlim(ax, [0 doseInt*(cycles)]); + try + tickVals = 0:(doseInt/4):(doseInt*cycles); + xticks(ax, tickVals); + + % Format as "Xd Yh" (e.g., 206 hours -> "8d14h") + tickLabels = strings(size(tickVals)); + for k = 1:numel(tickVals) + d = floor(tickVals(k)/24); + h = tickVals(k) - d*24; + tickLabels(k) = sprintf('%dd%gh', d, h); + end + xticklabels(ax, tickLabels); + catch + end +else + xlim(ax, [min(x) max(x)]); +end + +% ------------------------------------------------------------ +% Legend +% ------------------------------------------------------------ +if opts.DoStats + pctStr = sprintf('%dth/%dth/%dth percentiles', ... + opts.Percentiles(1), opts.Percentiles(2), opts.Percentiles(3)); + + lgd = legend(ax, [hMean, hPct(1)], {'Mean', pctStr}, ... + 'Interpreter','latex','FontSize',8); + lgd.Color = 'none'; + lgd.EdgeColor = 'none'; +end + +% ------------------------------------------------------------ +% Font size +% ------------------------------------------------------------ +try + fontsize(fig, opts.FontSize, "points"); +catch + set(ax,'FontSize',opts.FontSize); +end + +end \ No newline at end of file diff --git a/Methods/summarize_ss_counts.m b/Methods/summarize_ss_counts.m new file mode 100644 index 0000000..371360a --- /dev/null +++ b/Methods/summarize_ss_counts.m @@ -0,0 +1,72 @@ +%% ======================================================================= +% summarize_ss_counts.m — Tabulate and Print Steady-State Attainment +% Counts +% ======================================================================== +% +% DESCRIPTION +% Builds and prints a table of steady-state attainment counts (AUC, Cmax, +% and both) with each count expressed as a percentage of the total +% population. Optionally sorts rows by count or metric name. +% +% INPUTS +% numAUCSSReached - Count of patients reaching AUC steady state. +% numCmaxSSReached - Count reaching Cmax steady state. +% numBothReached - Count reaching both. +% N - Total patient count (NaN-safe; percentages omitted +% if N is missing/non-positive). +% +% Name-value pairs: +% 'Title' - Header printed above the table (default 'Steady-State +% Summary'). +% 'SortBy' - 'count' | 'name' | 'none' (default 'none'). +% +% OUTPUTS +% T - Table with columns Metric, Count, PercentOfTotal. +% +% AUTHORS +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function T = summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, N, varargin) + + p = inputParser; + p.addParameter('Title', 'Steady-State Summary'); + p.addParameter('SortBy','none'); % 'count' | 'name' | 'none' + p.parse(varargin{:}); + + if isempty(N) || ~isfinite(N) || N <= 0 + N = NaN; + end + + names = ["AUC_SS_Reached"; "Cmax_SS_Reached"; "Both_SS_Reached"]; + counts = [numAUCSSReached; numCmaxSSReached; numBothReached]; + + pctStr = strings(numel(counts),1); + for i = 1:numel(counts) + if ~isnan(N) + pct = 100 * counts(i) / max(N,1); + pctStr(i) = sprintf('%.2f%% (%d/%d)', pct, counts(i), N); + else + pctStr(i) = sprintf('%d/(unknown N)', counts(i)); + end + end + + T = table(names, counts, pctStr, ... + 'VariableNames', {'Metric','Count','PercentOfTotal'}); + + switch lower(p.Results.SortBy) + case 'count' + T = sortrows(T, 'Count', 'descend'); + case 'name' + T = sortrows(T, 'Metric', 'ascend'); + end + + fprintf('\n=== %s ===\n', p.Results.Title); + if ~isnan(N), fprintf('Total patients: %d\n', N); end + disp(T); +end \ No newline at end of file diff --git a/OMC/OMC_PlasmaODEs.m b/OMC/OMC_PlasmaODEs.m new file mode 100644 index 0000000..835b4ba --- /dev/null +++ b/OMC/OMC_PlasmaODEs.m @@ -0,0 +1,95 @@ +%% ======================================================================= +% OMC_PlasmaODEs.m — Dual-Absorption / Two-Compartment ODEs for +% Omadacycline +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the omadacycline oral PK model: two parallel +% first-order absorption pathways (fast depot A1, and slow depot A2 gated +% by a lag time) feeding a central compartment with one peripheral +% compartment and first-order elimination. Intended to be called by an +% ODE solver (ode45) from OMC_PopPK. +% +% MODEL +% ODEs as described in Yang et al., 2022. +% Haijing Yang et al. "Pharmacokinetics, Safety and +% Pharmacokinetics/Pharmacodynamics Analysis of Omadacycline in Chinese Healthy +% Subjects". In: *Frontiers in Pharmacology* Volume 13 (2022). +% ISSN: 1663-9812. DOI: 10.3389/fphar.2022.869237. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver (used to gate the +% slow absorption pathway against Tlag). +% y - State vector of drug amounts (mg): +% y(1) = fast absorption depot (A1) +% y(2) = slow absorption depot (A2) +% y(3) = central compartment (Acent) +% y(4) = peripheral compartment (Aper2) +% params - Parameter vector: +% params(1) = V1 central volume Vc/F (L) +% params(2) = V2 peripheral volume Vp2/F (L) +% params(3) = CL1 clearance CL/F (L/h) +% params(4) = CL2 intercompartmental clearance Q2/F (L/h) +% params(5) = ka1 absorption rate constant 1 (1/h) +% params(6) = ka2 absorption rate constant 2 (1/h) +% params(7) = Tlag lag time before pathway 2 activates (h) +% +% OUTPUTS +% dy - Derivative vector (mg/h) matching the state ordering in y. +% +% NOTES +% The dose split (fraction P1 to the fast depot) is applied outside this +% function at each dose event; A1 and A2 already hold their fractions when +% this function is called. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2026 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = OMC_PlasmaODEs(t, y, params) + + % -------- Extract params -------- + V1 = params(1); + V2 = params(2); + CL1 = params(3); + Q2 = params(4); + ka1 = params(5); + ka2 = params(6); + Tlag = params(7); + + % -------- States -------- + A1 = y(1); + A2 = y(2); + Acent = y(3); + Aper2 = y(4); + + % -------- Lag gate for pathway 2 -------- + if t < Tlag + gate2 = 0; + else + gate2 = 1; + end + + % -------- Absorption outflows -------- + dA1_abs = ka1 * A1; + dA2_abs = gate2 * ka2 * A2; + + RateIn = dA1_abs + dA2_abs; + + % -------- Disposition -------- + dAcentdt = RateIn - CL1 * (Acent/V1) - Q2 * (Acent/V1) + Q2 * (Aper2/V2); + dAper2dt = Q2 * (Acent/V1) - Q2 * (Aper2/V2); + + dA1dt = -dA1_abs; + dA2dt = -dA2_abs; + + dy = [dA1dt; dA2dt; dAcentdt; dAper2dt]; + +end \ No newline at end of file diff --git a/OMC/OMC_PopPK.m b/OMC/OMC_PopPK.m new file mode 100644 index 0000000..026b5a8 --- /dev/null +++ b/OMC/OMC_PopPK.m @@ -0,0 +1,498 @@ +%% ======================================================================= +% OMC_PopPK.m — Population PK Simulation & PTA Analysis for Omadacycline +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the omadacycline plasma ODEs across a Monte Carlo patient +% population. Models a two-compartment disposition system with a dual +% (fast/slow) oral absorption pathway and a lag time on the slow depot, +% computes steady-state AUC/Cmax, applies an unbound fraction to derive +% free-drug exposure, and generates time-course, AUC, and probability of +% target attainment (PTA) plots. +% +% A serial path and a parfor (parallelized) path are provided; select via +% the 'Parallelize' option. Both paths are functionally equivalent. +% +% MODEL +% Reimplementation of Yang et al., 2022. +% Haijing Yang et al. "Pharmacokinetics, Safety and +% Pharmacokinetics/Pharmacodynamics Analysis of Omadacycline in Chinese Healthy +% Subjects". In: *Frontiers in Pharmacology* Volume 13 (2022). +% ISSN: 1663-9812. DOI: 10.3389/fphar.2022.869237. +% Dosing fixed at 300 mg oral q24h. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (24) Time between doses (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% 'Parallelize' (false) Use parfor over patients. +% +% NOTE: Dose size is fixed internally at 300 mg (no 'DoseSize' option). +% +% OUTPUTS +% results - Struct of simulation results (time grid, concentration +% timecourses, steady-state counts, and last-24h metrics). +% parameters - Empty struct (no per-patient parameter export for OMC). +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2026 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% OMC_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% Parallel Computing Toolbox required when 'Parallelize' is true. +% ======================================================================== + + +function [results, parameters, plotArgs] = OMC_PopPK(varargin) + + % Initialize output + plotArgs = []; + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 24, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.addParameter('Parallelize', false, @(b)islogical(b)&&isscalar(b)); + + p.parse(varargin{:}); + opt = p.Results; + + neq = 4; + options = odeset('NonNegative', 1:neq); + + + % -------------------- Parameter Values -------------------- + + % Fixed dose + % ------------------------------------------ + dose_size_OMC = 300; % (mg) oral dose + + % Volume parameters (300 mg oral, Yang et al. 2022) + % ------------------------------------------ + V1_OMC = 103; % (L) Vc/F central volume + V2_OMC = 244; % (L) Vp/F peripheral volume + + % Rate/clearance parameters + % ------------------------------------------ + CL1_OMC = 15.2; % (L/h) CL/F clearance + CL2_OMC = 34.2; % (L/h) Q/F intercompartmental clearance + ka1_OMC = 0.342; % (1/h) absorption rate constant 1 + ka2_OMC = 4.58; % (1/h) absorption rate constant 2 + P1_OMC = 0.786; % (-) fraction of dose to fast depot + Tlag_OMC = 1.85; % (h) lag time for slow depot + + % Interindividual variability (omega, log-scale SD) + % ------------------------------------------ + IIV_OMC = [... + 0.170 % V1 IIV + 0.301 % V2 IIV + 0.207 % CL1 IIV + 0.238 % CL2 IIV + 0.572 % ka1 IIV + 0.721 % ka2 IIV + 0.108 % P1 IIV + 0.157 % Tlag IIV + ]; + + % Unbound fraction + % ------------------------------------------ + f_OMC = 0.79; % (-) OMC free fraction (PPB 21%) + + + % -------------------- Variables for ODEs -------------------- + + n_pts = opt.NumPatients; + n_doses_OMC = opt.NumDoses; + dose_time_OMC = opt.DoseInterval; + + time_step_size_OMC = 0.5; + + time_vec_OMC = 0 : time_step_size_OMC : dose_time_OMC; + total_timpts_OMC = n_doses_OMC * (length(time_vec_OMC) - 1); + time_vec_local_OMC = 0 : time_step_size_OMC : dose_time_OMC * n_doses_OMC - time_step_size_OMC; + + + % -------------------- Initialize Matrices -------------------- + + OMC_conc_dep1 = zeros([total_timpts_OMC n_pts]); + OMC_conc_dep2 = zeros([total_timpts_OMC n_pts]); + OMC_conc_c = zeros([total_timpts_OMC n_pts]); + OMC_conc_p2 = zeros([total_timpts_OMC n_pts]); + + + % -------------------- ODE Solving -------------------- + + if ~opt.Parallelize + + for ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + V1_pt = V1_OMC * exp(random('Normal', 0, IIV_OMC(1))); + V2_pt = V2_OMC * exp(random('Normal', 0, IIV_OMC(2))); + CL1_pt = CL1_OMC * exp(random('Normal', 0, IIV_OMC(3))); + CL2_pt = CL2_OMC * exp(random('Normal', 0, IIV_OMC(4))); + ka1_pt = ka1_OMC * exp(random('Normal', 0, IIV_OMC(5))); + ka2_pt = ka2_OMC * exp(random('Normal', 0, IIV_OMC(6))); + P1_pt = min(P1_OMC * exp(random('Normal', 0, IIV_OMC(7))), 1); + Tlag_pt= Tlag_OMC* exp(random('Normal', 0, IIV_OMC(8))); + + % Set initial conditions + % -------------------------------------------------------- + A1_IC = P1_pt * dose_size_OMC; + A2_IC = (1 - P1_pt) * dose_size_OMC; + CEN_IC = 0; + PER2_IC = 0; + + % Initialize local storage + OMC_conc_dep1_loc = zeros([total_timpts_OMC 1]); + OMC_conc_dep2_loc = zeros([total_timpts_OMC 1]); + OMC_conc_c_loc = zeros([total_timpts_OMC 1]); + OMC_conc_p2_loc = zeros([total_timpts_OMC 1]); + + for idose = 1:n_doses_OMC + + params_OMC = [... + V1_pt % params(1) + V2_pt % params(2) + CL1_pt % params(3) + CL2_pt % params(4) + ka1_pt % params(5) + ka2_pt % params(6) + Tlag_pt % params(7) + ]; + + [~, OMC_sol] = ode45(@(t,y) OMC_PlasmaODEs(t, y, params_OMC), ... + time_vec_OMC, [A1_IC A2_IC CEN_IC PER2_IC], options); + + i0 = (idose - 1) * (length(time_vec_OMC) - 1) + 1; + i1 = idose * (length(time_vec_OMC) - 1); + + OMC_conc_dep1_loc(i0:i1) = OMC_sol(1:end-1, 1); + OMC_conc_dep2_loc(i0:i1) = OMC_sol(1:end-1, 2); + OMC_conc_c_loc(i0:i1) = OMC_sol(1:end-1, 3) / V1_pt; + OMC_conc_p2_loc(i0:i1) = OMC_sol(1:end-1, 4) / V2_pt; + + % Update ICs: add new dose split to depots + A1_IC = OMC_sol(end, 1) + P1_pt * dose_size_OMC; + A2_IC = OMC_sol(end, 2) + (1 - P1_pt) * dose_size_OMC; + CEN_IC = OMC_sol(end, 3); + PER2_IC = OMC_sol(end, 4); + + end + + OMC_conc_dep1(:, ipt) = OMC_conc_dep1_loc; + OMC_conc_dep2(:, ipt) = OMC_conc_dep2_loc; + OMC_conc_c(:, ipt) = OMC_conc_c_loc; + OMC_conc_p2(:, ipt) = OMC_conc_p2_loc; + + end + + else + + parfor ipt = 1:n_pts + + V1_pt = V1_OMC * exp(random('Normal', 0, IIV_OMC(1))); + V2_pt = V2_OMC * exp(random('Normal', 0, IIV_OMC(2))); + CL1_pt = CL1_OMC * exp(random('Normal', 0, IIV_OMC(3))); + CL2_pt = CL2_OMC * exp(random('Normal', 0, IIV_OMC(4))); + ka1_pt = ka1_OMC * exp(random('Normal', 0, IIV_OMC(5))); + ka2_pt = ka2_OMC * exp(random('Normal', 0, IIV_OMC(6))); + P1_pt = min(P1_OMC * exp(random('Normal', 0, IIV_OMC(7))), 1); + Tlag_pt= Tlag_OMC* exp(random('Normal', 0, IIV_OMC(8))); + + A1_IC = P1_pt * dose_size_OMC; + A2_IC = (1 - P1_pt) * dose_size_OMC; + CEN_IC = 0; + PER2_IC = 0; + + OMC_conc_dep1_loc = zeros([total_timpts_OMC 1]); + OMC_conc_dep2_loc = zeros([total_timpts_OMC 1]); + OMC_conc_c_loc = zeros([total_timpts_OMC 1]); + OMC_conc_p2_loc = zeros([total_timpts_OMC 1]); + + params_OMC_loc = zeros([7 1]); + + for idose = 1:n_doses_OMC + + params_OMC_loc = [... + V1_pt; V2_pt; CL1_pt; CL2_pt; ka1_pt; ka2_pt; Tlag_pt]; + + [~, OMC_sol] = ode45(@(t,y) OMC_PlasmaODEs(t, y, params_OMC_loc), ... + time_vec_OMC, [A1_IC A2_IC CEN_IC PER2_IC]); + + i0 = (idose - 1) * (length(time_vec_OMC) - 1) + 1; + i1 = idose * (length(time_vec_OMC) - 1); + + OMC_conc_dep1_loc(i0:i1) = OMC_sol(1:end-1, 1); + OMC_conc_dep2_loc(i0:i1) = OMC_sol(1:end-1, 2); + OMC_conc_c_loc(i0:i1) = OMC_sol(1:end-1, 3) / V1_pt; + OMC_conc_p2_loc(i0:i1) = OMC_sol(1:end-1, 4) / V2_pt; + + A1_IC = OMC_sol(end, 1) + P1_pt * dose_size_OMC; + A2_IC = OMC_sol(end, 2) + (1 - P1_pt) * dose_size_OMC; + CEN_IC = OMC_sol(end, 3); + PER2_IC = OMC_sol(end, 4); + + end + + OMC_conc_dep1(:, ipt) = OMC_conc_dep1_loc; + OMC_conc_dep2(:, ipt) = OMC_conc_dep2_loc; + OMC_conc_c(:, ipt) = OMC_conc_c_loc; + OMC_conc_p2(:, ipt) = OMC_conc_p2_loc; + + end + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + plotTimeCourses( ... + 'X', time_vec_local_OMC, ... + 'Y', OMC_conc_c, ... + 'DrugName', 'OMC', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_OMC, ... + 'DoseInterval', dose_time_OMC, ... + 'FigureName', 'OMC Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', time_vec_local_OMC, ... + 'Y', OMC_conc_p2, ... + 'DrugName', 'OMC', ... + 'Compartment', 'Peripheral', ... + 'Cycles', n_doses_OMC, ... + 'DoseInterval', dose_time_OMC, ... + 'FigureName', 'OMC Raw Timecourses (Peripheral)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', time_vec_local_OMC, ... + 'Y', OMC_conc_dep1, ... + 'DrugName', 'OMC', ... + 'Compartment', 'Absorption Depot 1 (Fast)', ... + 'Cycles', n_doses_OMC, ... + 'DoseInterval', dose_time_OMC, ... + 'FigureName', 'OMC Raw Timecourses (Depot 1)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', time_vec_local_OMC, ... + 'Y', OMC_conc_dep2, ... + 'DrugName', 'OMC', ... + 'Compartment', 'Absorption Depot 2 (Slow)', ... + 'Cycles', n_doses_OMC, ... + 'DoseInterval', dose_time_OMC, ... + 'FigureName', 'OMC Raw Timecourses (Depot 2)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + plotTimeCourses( ... + 'X', time_vec_local_OMC, ... + 'Y', OMC_conc_c, ... + 'DrugName', 'OMC', ... + 'Compartment', 'Central', ... + 'Cycles', n_doses_OMC, ... + 'DoseInterval', dose_time_OMC, ... + 'FigureName', 'OMC Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_OMC, ... + 'time_step_size', time_step_size_OMC, ... + 'conc_c', OMC_conc_c, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'OMC steady-state attainment', 'SortBy', 'name'); + + statsT = table( ... + mean(last24AUC, 'omitnan'), std(last24AUC, 'omitnan'), ... + mean(last24Cmax, 'omitnan'), std(last24Cmax, 'omitnan'), ... + 'VariableNames', {'mean_last24AUC', 'std_last24AUC', 'mean_last24Cmax', 'std_last24Cmax'}); + + fprintf('\n=== OMC last-24h summary stats ===\n'); + disp(statsT); + + fLast24AUC_quiet = last24AUC * f_OMC; + fprintf('Mean fAUC: %.3f\n', mean(fLast24AUC_quiet, 'omitnan')); + end + + if opt.plotAUC + plotAUC(last24AUC, 'OMC'); + end + + + % -------------------- PTA Analysis -------------------- + + if opt.PTAplot + + MIC_MAB_OMC = { + 'M. abscessus'; + {{'0.008',2},{'0.016',2},{'0.03',10},{'0.06',53},{'0.12',81},{'0.25',154},{'0.5',122}, ... + {'1',87},{'2',73},{'4',30},{'8',3},{'16',0},{'32',0}}; + }; + + MIC_FORT_OMC = { + 'M. fortuitum'; + {{'0.015',0},{'0.03',0},{'0.06',0},{'0.12',3},{'0.25',31},{'0.5',20}, ... + {'1',5},{'2',0},{'4',0},{'8',1},{'16',0},{'32',0}}; + }; + + speciesAggregation_OMC = {MIC_MAB_OMC, MIC_FORT_OMC}; + normalizedMICSpeciesAggregation_OMC = normalizeMICsOfAggregation(speciesAggregation_OMC); + uniqueMICs_OMC = getUniqueMICs(normalizedMICSpeciesAggregation_OMC); + + + % -------------------- Setting PTA Targets -------------------- + + target1_OMC = { + 'NTM'; + 'fAUC24/MIC'; + 17; + 'NTM' + }; + + uniqueTargets_OMC = {target1_OMC}; + + + % -------------------- Free Concentration Arrays -------------------- + + fLast24ConcArray_OMC = last24ConcArray * f_OMC; + fLast24AUC_OMC = last24AUC * f_OMC; + fLast24Cmax_OMC = last24Cmax * f_OMC; + + + % -------------------- PTA Calculation -------------------- + + nMIC_OMC = length(uniqueMICs_OMC); + nTarg_OMC = length(uniqueTargets_OMC); + + PTAMatrix_OMC = zeros(nMIC_OMC, nTarg_OMC); + + for i = 1:nTarg_OMC + PTAMatrix_OMC(:, i) = calculatePTA( ... + fLast24ConcArray_OMC, ... + 'MICDist', uniqueMICs_OMC, ... + 'targetType', uniqueTargets_OMC{i}{2}, ... + 'target', uniqueTargets_OMC{i}{3}, ... + 'time_step_size', time_step_size_OMC, ... + 'last24AUC', fLast24AUC_OMC, ... + 'last24Cmax', fLast24Cmax_OMC); + end + + + % -------------------- PTA Plotting -------------------- + + baseLegend_OMC = getLegendArray(uniqueTargets_OMC, normalizedMICSpeciesAggregation_OMC); + nSpecies_OMC = numel(normalizedMICSpeciesAggregation_OMC); + speciesLegend_OMC = baseLegend_OMC(1 : nSpecies_OMC); + targetLegend_OMC = baseLegend_OMC(nSpecies_OMC + 1 : end); + + legendArray_OMC = cell(1, nSpecies_OMC + nTarg_OMC); + legendArray_OMC(1 : nSpecies_OMC) = speciesLegend_OMC; + + idx = nSpecies_OMC + 1; + for i = 1:nTarg_OMC + legendArray_OMC{idx} = sprintf('%s (%d mg q%dh)', ... + targetLegend_OMC{i}, dose_size_OMC, dose_time_OMC); + idx = idx + 1; + end + + [~, plotArgs] = plotPTAs( ... + uniqueMICs_OMC, ... + PTAMatrix_OMC, ... + normalizedMICSpeciesAggregation_OMC, ... + 'DoseSize', dose_size_OMC, ... + 'DoseFrequency', dose_time_OMC, ... + 'DrugName', 'OMC', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray_OMC, ... + 'UniqueTargets', uniqueTargets_OMC, ... + 'NumPatients', n_pts, ... + 'ShowCI', false, ... + 'DoseLabel', sprintf('%d mg q%dh', dose_size_OMC, dose_time_OMC)); + + end + + + % -------------------- Package results -------------------- + + results.time_vec_local = time_vec_local_OMC; + results.dt_hr = time_step_size_OMC; + + results.conc.central = OMC_conc_c; + results.conc.per2 = OMC_conc_p2; + results.conc.dep1 = OMC_conc_dep1; + results.conc.dep2 = OMC_conc_dep2; + + results.ss.numAUCSSReached = numAUCSSReached; + results.ss.numCmaxSSReached = numCmaxSSReached; + results.ss.numBothReached = numBothReached; + + results.last24.conc = last24ConcArray; + results.last24.AUC = last24AUC; + results.last24.Cmax = last24Cmax; + + % No parameters output needed (no ptParams struct without setOMCParams) + parameters = struct(); + +end \ No newline at end of file diff --git a/RBT/RBT_PlasmaODEs.m b/RBT/RBT_PlasmaODEs.m new file mode 100644 index 0000000..7128a8c --- /dev/null +++ b/RBT/RBT_PlasmaODEs.m @@ -0,0 +1,121 @@ +%% ======================================================================= +% RBT_PlasmaODEs.m — Parent/Metabolite Transit-Absorption ODEs for +% Rifabutin +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the rifabutin plasma PK model: an oral dose +% enters through a transit-compartment absorption term and splits into +% rifabutin (RBT) and its des-rifabutin (dRBT) metabolite via first-pass +% conversion (fraction Fm). Each species has a central and a peripheral +% compartment with intercompartmental and elimination clearances. Intended +% to be called by a stiff ODE solver (ode15s) from RBT_PopPK. +% +% MODEL +% ODEs based on Hennig et al., 2016. +% Stefanie Hennig et al. "Population pharmacokinetic drug-drug interaction +% pooled analysis of existing data for rifabutin and HIV PIs". In: *Jounral +% of Antimicrobial Chemotherapy* 71.5 (2016), pp. 1330-1340. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver (used as time +% after dose in the transit-absorption term). +% y - State vector (amounts/concentrations as modeled): +% y(1) = absorption compartment mass (m_abs) +% y(2) = RBT central (C_c_RBT) +% y(3) = dRBT central (C_c_dRBT) +% y(4) = RBT peripheral (C_p_RBT) +% y(5) = dRBT peripheral (C_p_dRBT) +% params - Parameter vector: +% params(1) = k_tr transit rate constant (1/h) +% params(2) = n_trans number of transit compartments +% params(3) = ka absorption rate constant (1/h) +% params(4) = Fm first-pass fraction RBT -> dRBT +% params(5) = Q_RBT RBT intercompartmental CL (L/h) +% params(6) = CL_RBT RBT central clearance (L/h) +% params(7) = CL_to_dRBT RBT -> dRBT conversion CL (L/h) +% params(8) = Q_dRBT dRBT intercompartmental CL (L/h) +% params(9) = CL_dRBT dRBT central clearance (L/h) +% params(10) = Vd_c_RBT RBT central volume (L) +% params(11) = Vd_c_dRBT dRBT central volume (L) +% params(12) = Vd_p_RBT RBT peripheral volume (L) +% params(13) = Vd_p_dRBT dRBT peripheral volume (L) +% params(14) = dose dose for this interval (mg) +% +% OUTPUTS +% dy - Derivative vector (5 x 1) matching the state ordering in y. +% +% NOTES +% Gamma(n_trans+1) is approximated via Stirling's formula for the +% transit-absorption input term. k_tr is derived from the sampled MTT in +% RBT_PopPK as (n_trans+1)/MTT. +% +% AUTHORS +% Noah Strawhacker +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2024-11 +% Tested on: MATLAB R2026a +% ======================================================================== + +function dy = RBT_PlasmaODEs(t, y, params) + + + % Extracting constants from params + k_tr = params(1); + n_trans_compts = params(2); + ka = params(3); + Fm = params(4); + Q_RBT = params(5); + CL_RBT = params(6); + CL_to_dRBT = params(7); + Q_dRBT = params(8); + CL_dRBT = params(9); + Vd_c_RBT = params(10); + Vd_c_dRBT = params(11); + Vd_p_RBT = params(12); + Vd_p_dRBT = params(13); + dose_size_RBT = params(14); + + + % Setting up solution equations + sols = [... + y(1) % m_abs + y(2) % C_c_RBT + y(3) % C_c_dRBT + y(4) % C_p_RBT + y(5) % C_p_dRBT + ]; + + + % ODE Section + % Setting up ODE + gamma = sqrt(2 * pi) * n_trans_compts^(n_trans_compts + 0.5) * exp(-n_trans_compts); + + % Absorption compartment + m_abs_diff = dose_size_RBT * k_tr * (((k_tr * t)^n_trans_compts * exp(-k_tr * t)) / gamma) - ka * sols(1); + + % Central compartment rifabutin + C_c_RBT_diff = (ka * (1 - Fm) / Vd_c_RBT) * sols(1) + ((-Q_RBT - CL_RBT - CL_to_dRBT) / Vd_c_RBT) * sols(2) + ((Q_RBT) / Vd_c_RBT) * sols(4); + + % Central compartment desRBT + C_c_dRBT_diff = (ka * Fm / Vd_c_dRBT) * sols(3) + ((CL_to_dRBT) / Vd_c_dRBT) * sols(2) + ((-Q_dRBT - CL_dRBT) / Vd_c_dRBT) * sols(3) + ((Q_dRBT) / Vd_c_dRBT) * sols(5); + + % Peripheral compartment RBT + C_p_RBT_diff = ((Q_RBT) / Vd_p_RBT) * sols(2) + ((-Q_RBT) / Vd_p_RBT) * sols(4); + + % Peripheral compartment desRBT + C_p_dRBT_diff = ((Q_dRBT) / Vd_p_dRBT) * sols(3) + ((-Q_dRBT) / Vd_p_dRBT) * sols(5); + + + % Output + dy = [... + m_abs_diff + C_c_RBT_diff + C_c_dRBT_diff + C_p_RBT_diff + C_p_dRBT_diff + ]; +end \ No newline at end of file diff --git a/RBT/RBT_PopPK.m b/RBT/RBT_PopPK.m new file mode 100644 index 0000000..ed39433 --- /dev/null +++ b/RBT/RBT_PopPK.m @@ -0,0 +1,599 @@ +%% ======================================================================= +% RBT_PopPK.m — Population PK Simulation & PTA Analysis for Rifabutin +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the rifabutin plasma ODEs across a Monte Carlo patient +% population. Models a parent/metabolite system (rifabutin [RBT] and its +% des-rifabutin [dRBT] metabolite), each with central and peripheral +% compartments, fed by a transit-compartment oral absorption model with +% first-pass conversion. Computes steady-state AUC/Cmax and generates +% time-course, log-scale last-24h, AUC, and probability of target +% attainment (PTA) plots. +% +% A serial path and a parfor (parallelized) path are provided; select via +% the 'Parallelize' option. Both paths are functionally equivalent. +% +% MODEL +% Reimplementation of Hennig et al., 2016. +% Stefanie Hennig et al. "Population pharmacokinetic drug-drug interaction +% pooled analysis of existing data for rifabutin and HIV PIs". In: *Jounral +% of Antimicrobial Chemotherapy* 71.5 (2016), pp. 1330-1340. +% All structural parameters scaled to a 70 kg body weight. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (300) Dose per administration (mg; 5 mg/kg capped +% at 300 mg). +% 'NumDoses' (21) Total number of doses. +% 'DoseInterval' (24) Time between doses (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile (5/50/95) time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% 'Parallelize' (false) Use parfor over patients. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: Noah Strawhacker, 2024; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2024 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% RBT_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% Parallel Computing Toolbox required when 'Parallelize' is true. +% ======================================================================== + + +function [plotArgs] = RBT_PopPK(varargin) + + % Initialize output + plotArgs = []; + + % Set the ODE solver to not allow negative numbers + neq = 1; + options = odeset('NonNegative', 1:neq); + + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 300, @(x)isnumeric(x)); + p.addParameter('NumDoses', 21, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 24, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.addParameter('Parallelize', false, @(b)islogical(b)&&isscalar(b)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + % All parameters scaled to fit patient with 70 kg body weight + + % Overall parameters + % ------------------------------------------ + MTT = 2.16; % (h) Mean transit time + n_trans_compts = 7.15; % (-) Number of transit compartments + ka = 0.25; % (1/h) Absorption rate constant + Fm = 0.09; % (-) Fraction of RBT converted to dRBT in first-pass + + % Rifabutin (RBT) parameters + % ------------------------------------------ + Q_RBT = 62.21; % (L/h) Intercompartmental clearance of RBT + CL_RBT = 58.80; % (L/h) Clearance from central compartment of RBT + CL_to_dRBT = 5.76; % (L/h) Clearance/conversion of RBT to dRBT + Vd_c_RBT = 6.55; % (L) Central volume of distribution of RBT + Vd_p_RBT = 1580; % (L) Peripheral volume of distribution of RBT + + % Des-rifabutin (dRBT) parameters + % ------------------------------------------ + Q_dRBT = 71.80; % (L/h) Intercompartmental clearance of dRBT + CL_dRBT = 122.00; % (L/h) Clearance from central compartment of dRBT + Vd_c_dRBT = 37.30; % (L) Central volume of distribution of dRBT + Vd_p_dRBT = 1220; % (L) Peripheral volume of distribution of dRBT + + % Interindividual variability (% CV) + % ------------------------------------------ + MTT_IIV = 0.518; + ka_IIV = 0.310; + Fm_IIV = 0.893; + Q_RBT_IIV = 0.602; + CL_RBT_IIV = 0.540; + CL_to_dRBT_IIV = 0.515; + Vd_c_RBT_IIV = 1.691; + Vd_p_RBT_IIV = 0.656; + Q_dRBT_IIV = 0.608; + CL_dRBT_IIV = 0.783; + Vd_c_dRBT_IIV = 1.114; + Vd_p_dRBT_IIV = 0.268; + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify how much drug is given in each dose (mg) + % Dose is given as 5 mg/kg, maximum of 300 mg + dose_size_RBT = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_RBT = opt.NumDoses; + + % Specify time (hours) between doses + dose_time_RBT = opt.DoseInterval; + + % Specify time step and build time vectors + time_step_size_RBT = 0.5; + + time_vec_RBT = 0 : time_step_size_RBT : dose_time_RBT; + total_timpts_RBT = n_doses_RBT * (length(time_vec_RBT) - 1); + total_params_RBT = 14; + time_vec_local_RBT = 0 : time_step_size_RBT : dose_time_RBT * n_doses_RBT - time_step_size_RBT; + + + % -------------------- Initialize Matrices -------------------- + + param_store_RBT = zeros([total_params_RBT n_pts]); + RBT_mass_abs = zeros([total_timpts_RBT n_pts]); + RBT_conc_cen = zeros([total_timpts_RBT n_pts]); + dRBT_conc_cen = zeros([total_timpts_RBT n_pts]); + RBT_conc_per = zeros([total_timpts_RBT n_pts]); + dRBT_conc_per = zeros([total_timpts_RBT n_pts]); + + + % -------------------- ODE Solving -------------------- + + % Serial path (default) + if ~opt.Parallelize + + for ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + MTT_pt = MTT * exp(random('Normal', 0, sqrt(log(MTT_IIV^2 + 1)))); + k_tr_pt = (n_trans_compts + 1) / MTT_pt; % derived from sampled MTT + ka_pt = ka * exp(random('Normal', 0, sqrt(log(ka_IIV^2 + 1)))); + Fm_pt = Fm * exp(random('Normal', 0, sqrt(log(Fm_IIV^2 + 1)))); + + % Resample Fm until bioavailability fraction is physically valid + while Fm_pt > 1 + Fm_pt = Fm * exp(random('Normal', 0, sqrt(log(Fm_IIV^2 + 1)))); + end + + Q_RBT_pt = Q_RBT * exp(random('Normal', 0, sqrt(log(Q_RBT_IIV^2 + 1)))); + CL_RBT_pt = CL_RBT * exp(random('Normal', 0, sqrt(log(CL_RBT_IIV^2 + 1)))); + CL_to_dRBT_pt = CL_to_dRBT * exp(random('Normal', 0, sqrt(log(CL_to_dRBT_IIV^2 + 1)))); + Vd_c_RBT_pt = Vd_c_RBT * exp(random('Normal', 0, sqrt(log(Vd_c_RBT_IIV^2 + 1)))); + Vd_p_RBT_pt = Vd_p_RBT * exp(random('Normal', 0, sqrt(log(Vd_p_RBT_IIV^2 + 1)))); + Q_dRBT_pt = Q_dRBT * exp(random('Normal', 0, sqrt(log(Q_dRBT_IIV^2 + 1)))); + CL_dRBT_pt = CL_dRBT * exp(random('Normal', 0, sqrt(log(CL_dRBT_IIV^2 + 1)))); + Vd_c_dRBT_pt = Vd_c_dRBT * exp(random('Normal', 0, sqrt(log(Vd_c_dRBT_IIV^2 + 1)))); + Vd_p_dRBT_pt = Vd_p_dRBT * exp(random('Normal', 0, sqrt(log(Vd_p_dRBT_IIV^2 + 1)))); + + % Compile parameters into vector to pass to ODE solver + params_RBT = [... + k_tr_pt % params(1) + n_trans_compts % params(2) + ka_pt % params(3) + Fm_pt % params(4) + Q_RBT_pt % params(5) + CL_RBT_pt % params(6) + CL_to_dRBT_pt % params(7) + Q_dRBT_pt % params(8) + CL_dRBT_pt % params(9) + Vd_c_RBT_pt % params(10) + Vd_c_dRBT_pt % params(11) + Vd_p_RBT_pt % params(12) + Vd_p_dRBT_pt % params(13) + dose_size_RBT % params(14) + ]; + + % Set initial conditions + % -------------------------------------------------------- + initial_conds_RBT = [0; 0; 0; 0; 0]; + + % Initialize local storage + abs_loc = zeros([total_timpts_RBT 1]); + cen_loc = zeros([total_timpts_RBT 1]); + dcen_loc = zeros([total_timpts_RBT 1]); + per_loc = zeros([total_timpts_RBT 1]); + dper_loc = zeros([total_timpts_RBT 1]); + + for idose = 1:n_doses_RBT + + % Call ODE solver + % --------------- + [~, RBT_sol] = ode15s(@(t,y) RBT_PlasmaODEs(t, y, params_RBT), ... + time_vec_RBT, initial_conds_RBT, options); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_RBT) - 1) + 1; + i1 = idose * (length(time_vec_RBT) - 1); + + abs_loc(i0:i1) = RBT_sol(1:end-1, 1); + cen_loc(i0:i1) = RBT_sol(1:end-1, 2); + dcen_loc(i0:i1) = RBT_sol(1:end-1, 3); + per_loc(i0:i1) = RBT_sol(1:end-1, 4); + dper_loc(i0:i1) = RBT_sol(1:end-1, 5); + + % Update initial conditions for next dose + initial_conds_RBT = [RBT_sol(end,1); RBT_sol(end,2); ... + RBT_sol(end,3); RBT_sol(end,4); RBT_sol(end,5)]; + + end + + % Store patient results + param_store_RBT(:, ipt) = params_RBT; + RBT_mass_abs(:, ipt) = abs_loc; + RBT_conc_cen(:, ipt) = cen_loc; + dRBT_conc_cen(:, ipt) = dcen_loc; + RBT_conc_per(:, ipt) = per_loc; + dRBT_conc_per(:, ipt) = dper_loc; + + end + + % Parallel path + else + + parfor ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + MTT_pt = MTT * exp(random('Normal', 0, sqrt(log(MTT_IIV^2 + 1)))); + k_tr_pt = (n_trans_compts + 1) / MTT_pt; + ka_pt = ka * exp(random('Normal', 0, sqrt(log(ka_IIV^2 + 1)))); + Fm_pt = Fm * exp(random('Normal', 0, sqrt(log(Fm_IIV^2 + 1)))); + + % Resample Fm until bioavailability fraction is physically valid + while Fm_pt > 1 + Fm_pt = Fm * exp(random('Normal', 0, sqrt(log(Fm_IIV^2 + 1)))); + end + + Q_RBT_pt = Q_RBT * exp(random('Normal', 0, sqrt(log(Q_RBT_IIV^2 + 1)))); + CL_RBT_pt = CL_RBT * exp(random('Normal', 0, sqrt(log(CL_RBT_IIV^2 + 1)))); + CL_to_dRBT_pt = CL_to_dRBT * exp(random('Normal', 0, sqrt(log(CL_to_dRBT_IIV^2 + 1)))); + Vd_c_RBT_pt = Vd_c_RBT * exp(random('Normal', 0, sqrt(log(Vd_c_RBT_IIV^2 + 1)))); + Vd_p_RBT_pt = Vd_p_RBT * exp(random('Normal', 0, sqrt(log(Vd_p_RBT_IIV^2 + 1)))); + Q_dRBT_pt = Q_dRBT * exp(random('Normal', 0, sqrt(log(Q_dRBT_IIV^2 + 1)))); + CL_dRBT_pt = CL_dRBT * exp(random('Normal', 0, sqrt(log(CL_dRBT_IIV^2 + 1)))); + Vd_c_dRBT_pt = Vd_c_dRBT * exp(random('Normal', 0, sqrt(log(Vd_c_dRBT_IIV^2 + 1)))); + Vd_p_dRBT_pt = Vd_p_dRBT * exp(random('Normal', 0, sqrt(log(Vd_p_dRBT_IIV^2 + 1)))); + + % Compile parameters into vector to pass to ODE solver + params_RBT_loc = [... + k_tr_pt % params(1) + n_trans_compts % params(2) + ka_pt % params(3) + Fm_pt % params(4) + Q_RBT_pt % params(5) + CL_RBT_pt % params(6) + CL_to_dRBT_pt % params(7) + Q_dRBT_pt % params(8) + CL_dRBT_pt % params(9) + Vd_c_RBT_pt % params(10) + Vd_c_dRBT_pt % params(11) + Vd_p_RBT_pt % params(12) + Vd_p_dRBT_pt % params(13) + dose_size_RBT % params(14) + ]; + + % Set initial conditions + % -------------------------------------------------------- + initial_conds_RBT = [0; 0; 0; 0; 0]; + + % Initialize local storage + abs_loc = zeros([total_timpts_RBT 1]); + cen_loc = zeros([total_timpts_RBT 1]); + dcen_loc = zeros([total_timpts_RBT 1]); + per_loc = zeros([total_timpts_RBT 1]); + dper_loc = zeros([total_timpts_RBT 1]); + + for idose = 1:n_doses_RBT + + % Call ODE solver + % --------------- + [~, RBT_sol] = ode15s(@(t,y) RBT_PlasmaODEs(t, y, params_RBT_loc), ... + time_vec_RBT, initial_conds_RBT, options); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_RBT) - 1) + 1; + i1 = idose * (length(time_vec_RBT) - 1); + + abs_loc(i0:i1) = RBT_sol(1:end-1, 1); + cen_loc(i0:i1) = RBT_sol(1:end-1, 2); + dcen_loc(i0:i1) = RBT_sol(1:end-1, 3); + per_loc(i0:i1) = RBT_sol(1:end-1, 4); + dper_loc(i0:i1) = RBT_sol(1:end-1, 5); + + % Update initial conditions for next dose + initial_conds_RBT = [RBT_sol(end,1); RBT_sol(end,2); ... + RBT_sol(end,3); RBT_sol(end,4); RBT_sol(end,5)]; + + end + + % Store patient results + param_store_RBT(:, ipt) = params_RBT_loc; + RBT_mass_abs(:, ipt) = abs_loc; + RBT_conc_cen(:, ipt) = cen_loc; + dRBT_conc_cen(:, ipt) = dcen_loc; + RBT_conc_per(:, ipt) = per_loc; + dRBT_conc_per(:, ipt) = dper_loc; + + end + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + + if opt.TimeSeriesPlots + + cycles_RBT = n_doses_RBT; + xvalues_RBT = time_vec_local_RBT; + + plotTimeCourses( ... + 'X', xvalues_RBT, ... + 'Y', RBT_conc_cen, ... + 'DrugName', 'RBT', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_RBT, ... + 'DoseInterval', dose_time_RBT, ... + 'FigureName', 'RBT Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_RBT, ... + 'Y', dRBT_conc_cen, ... + 'DrugName', 'dRBT', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_RBT, ... + 'DoseInterval', dose_time_RBT, ... + 'FigureName', 'dRBT Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + cycles_stats_RBT = n_doses_RBT; + xvalues_stats_RBT = time_vec_local_RBT; + + plotTimeCourses( ... + 'X', xvalues_stats_RBT, ... + 'Y', RBT_conc_cen, ... + 'DrugName', 'RBT', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_stats_RBT, ... + 'DoseInterval', dose_time_RBT, ... + 'FigureName', 'RBT Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_stats_RBT, ... + 'Y', dRBT_conc_cen, ... + 'DrugName', 'dRBT', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_stats_RBT, ... + 'DoseInterval', dose_time_RBT, ... + 'FigureName', 'dRBT Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + % NOTE: SS tolerances are intentionally wider (0.30) for RBT due to + % high interindividual variability in this model + AUCTolerance = 0.30; + CmaxTolerance = 0.30; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_RBT, ... + 'time_step_size', time_step_size_RBT, ... + 'conc_c', RBT_conc_cen, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'RBT steady-state attainment', 'SortBy', 'name'); + + statsT = table( ... + mean(last24AUC, 'omitnan'), std(last24AUC, 'omitnan'), ... + mean(last24Cmax, 'omitnan'), std(last24Cmax, 'omitnan'), ... + 'VariableNames', {'mean_last24AUC', 'std_last24AUC', 'mean_last24Cmax', 'std_last24Cmax'}); + + fprintf('\n=== RBT last-24h summary stats ===\n'); + disp(statsT); + fprintf('Last24 ConcArray mean value: %.4f\n', mean(mean(last24ConcArray))); + end + + % SS is required for PTA; abort early if not all patients reached it + if numBothReached ~= n_pts + error('Cannot calculate PTA; steady state not reached for all patients.'); + end + + + % -------------------- Log-Scale Last-24h Visualization -------------------- + % Reimplementation of Figure 2 from Hennig et al.: log(ng/mL) scale, + % shaded 90% interval with median overlay, on the final 24-hour window + + last24WindowEnd = length(time_vec_local_RBT); + last24WindowStart = last24WindowEnd - round(24 / time_step_size_RBT) + 1; + time_vec_last24 = time_vec_local_RBT(last24WindowStart : last24WindowEnd); + + logConc_RBT_cen = log(RBT_conc_cen(last24WindowStart:last24WindowEnd, :) * 1000); + pct_log_RBT = prctile(logConc_RBT_cen', [5 50 95]); + + fig_log_RBT = figure; + set(fig_log_RBT, 'WindowStyle', 'docked'); + hold on; + + fill([time_vec_last24, fliplr(time_vec_last24)], ... + [pct_log_RBT(1,:), fliplr(pct_log_RBT(3,:))], ... + [0.7 0.7 0.7], 'EdgeColor', 'none', 'FaceAlpha', 0.5); + + plot(time_vec_last24, pct_log_RBT(2,:), 'k-', 'LineWidth', 3); + + title('RBT Central: log(ng/mL), last 24 h'); + xlabel('Time (h)'); + ylabel('log(Concentration [ng/mL])'); + xlim([time_vec_last24(1) - time_step_size_RBT, ... + time_vec_last24(end) - time_step_size_RBT]); + legend({'90% Interval', 'Median'}, 'Location', 'best'); + set(gca, 'FontSize', opt.GraphFontSize); + hold off; + + + if opt.plotAUC + plotAUC(last24AUC, 'RBT'); + end + + + % -------------------- PTA Analysis -------------------- + + if opt.PTAplot + + % -------------------- MIC Distributions NTM -------------------- + % This section includes the minimum inhibitory concentration (MIC) + % distributions for the non-tuberculosis mycobacterium strains belonging + % to associated non-tuberculosis mycobacterium species + + MIC_MAB_RBT = { + 'M. abscessus'; + {{'0.06', 0}, {'0.125', 2}, {'0.25', 1}, {'0.5', 15}, ... + {'1', 43}, {'2', 64}, {'4', 58}, {'8', 10}, {'16', 1}, {'>16', 0}}; + 'AUC24/MIC'; + 194 + }; + + MIC_MAC_RBT = { + 'M. avium complex'; + {{'0.06', 0}, {'0.125', 1}, {'0.25', 801}, {'0.5', 375}, ... + {'1', 248}, {'2', 130}, {'4', 67}, {'8', 68}, {'16', 10}, {'>16', 0}}; + 'AUC24/MIC'; + 1700 + }; + + MIC_kan_RBT = { + 'M. kansasii'; + {{'0.06', 0}, {'0.125', 0}, {'0.25', 35}, {'0.5', 0}, ... + {'1', 0}, {'2', 0}, {'4', 0}, {'8', 0}, {'16', 0}, {'>16', 0}}; + 'AUC24/MIC'; + 35 + }; + + MIC_TB_RBT = { + 'M. tuberculosis'; + {{'0.06', 233}, {'0.125', 4}, {'0.25', 1}, {'0.5', 1}, ... + {'1', 0}, {'2', 0}, {'4', 0}, {'8', 0}, {'16', 0}, {'>16', 0}}; + 'AUC24/MIC'; + 239 + }; + + speciesAggregation_RBT = {MIC_MAB_RBT MIC_MAC_RBT MIC_kan_RBT MIC_TB_RBT}; + normalizedMICSpeciesAggregation_RBT = normalizeMICsOfAggregation(speciesAggregation_RBT); + uniqueMICs_RBT = getUniqueMICs(normalizedMICSpeciesAggregation_RBT); + + + % -------------------- Setting PTA Targets -------------------- + + target1_RBT = { % + 'NTM *'; + 'AUC24/MIC'; + 9; + 'unk' + }; + + target2_RBT = { % + 'NTM *'; + 'AUC24/MIC'; + 18; + 'unk' + }; + + target3_RBT = { % + 'NTM *'; + 'AUC24/MIC'; + 36; + 'unk' + }; + + uniqueTargets_RBT = {target1_RBT target2_RBT target3_RBT}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix_RBT = zeros(length(uniqueMICs_RBT), length(uniqueTargets_RBT)); + + for i = 1:length(uniqueTargets_RBT) + PTAMatrix_RBT(:, i) = calculatePTA( ... + last24ConcArray, ... + 'MICDist', uniqueMICs_RBT, ... + 'targetType', uniqueTargets_RBT{i}{2}, ... + 'target', uniqueTargets_RBT{i}{3}, ... + 'time_step_size', time_step_size_RBT, ... + 'last24AUC', last24AUC, ... + 'last24Cmax', last24Cmax); + end + + legendArray_RBT = getLegendArray(uniqueTargets_RBT, normalizedMICSpeciesAggregation_RBT); + + [~, plotArgs] = plotPTAs( ... + uniqueMICs_RBT, ... + PTAMatrix_RBT, ... + normalizedMICSpeciesAggregation_RBT, ... + 'DoseSize', dose_size_RBT, ... + 'DoseFrequency', dose_time_RBT, ... + 'DrugName', 'RBT', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray_RBT, ... + 'UniqueTargets', uniqueTargets_RBT, ... + 'NumPatients', n_pts, ... + 'ShowCI', false); + + end + +end \ No newline at end of file diff --git a/README.md b/README.md index f2110c7..2785879 100644 --- a/README.md +++ b/README.md @@ -1 +1,68 @@ -# NTM_PTAs_Public \ No newline at end of file +# NTM_PKmodels Repository + +## Overview + +This repository contains population pharmacokinetic (PopPK) Monte Carlo models +for the antibiotics most frequently used to treat *Non-Tuberculous +Mycobacterium* (NTM) infections. Each model samples a virtual patient +population, solves the drug's plasma PK ODEs, and computes probability of +target attainment (PTA) against species-specific MIC distributions. + +These models are associated with the following manuscript: + +* *Falling Short: Inadequate Target Attainment in Guideline-Based Therapy of Nontuberculous Mycobacterial Disease* + +### Source PopPK models + +Each drug's model reimplements a published population-PK analysis. Citation +placeholders below should be completed before publication. + +| Code | Drug | Source model | +|------|------|--------------| +| AMK | Amikacin | Michel Tod et al. "Population pharmacokinetic study of amikacin administered once or twice daily to febrile, severly neutropenic adults". In: *Antimicrobial agents and chemotherapy* 42.4 (1998), pp. 849-856. | +| BDQ | Bedaquiline | Michael A. Lyons. "Pharmacodynamics and Bactericidal Activity of Bedaquiline in Pulmonary Tuberculosis". In: *Antimicrobial Agents and Chemotherapy* 66.2 (2022). DOI: 10.1128/aac.01636-21. | +| CFZ | Clofazimine | Mahmoud Tareq Abdelwahab et al. "Clofazimine pharmacokinetics in patients with TB: dosing implications." In: *Journal of Antimicrobial Chemotherapy* 75.11 (Aug. 2020), pp. 3269-3277. ISSN: 0305-7453. DOI: 10.1093/jac/dkaa310. | +| CLR | Clarithromycin | K. Ikawa et al. "Pharmacokinetic modelling of serum and bronchial concentrations for clarithromycin and telithromycin, and site-secific pharmacodynamic simulation for their dosages". In: *Journal of Clinical Pharmacy and Therapeutics* 39.4 (Mar. 2014), pp. 411-417. DOI: 10.1111/jcpt.12157 | +| EMB | Ethambutol | Siv Jönsson et al. "Population Pharmacokinetics of Ethambutol in South African Tuberculosis Patients." In: *Antimicrobial Agents and Chemotherapy* 55.9 (2011), pp. 4230-4237. DOI: 10.1128/aac.00274-11. | +| FOX | Cefoxitin | Emmanuel Novy et al. "Population pharmacokinetics of prophylactic cefoxitin in elective bariatric surgery patients: a prospective monocentric study". In: *Anaesthesia Critical Care & Pain Medicine* 43.3 (2024), p. 101376. | +| IMI | Imipenem | Wenqian Chen et al. "Imipenem population pharmacokinetics: therapeutic drug monitoring data cellected in critically ill patients with or without extracorporeal membrane oxygenation". InL *Antimicrobial agents and chemotherapy* 64.5 (2020), pp. 10-1128. | +| LZD | Linezolid | Mahmoud Tareq Abdelwahab et al. "Linezolid population pharmacokinetics in South African adults with drug-resistant tuberculosis". In: *Antimicrobial agents and chemotherapy* 65.12 (2021), pp. 10-1128. | +| MXF | Moxifloxacin | Mohammad H. Al-Shaer et al. "Fluoroquinolones in Drug-Resistant Tuberculosis: Culture Conversino and Pharmacokinetic/Pharmacodynamic Target Attainment To Guide Dose Selection". In: *Antimicrobial Agents and Chemotherapy* 63.7 (2019). DOI: 10.1128/aac.00279-19. | +| OMC | Omadacycline | Haijing Yang et al. "Pharmacokinetics, Safety and Pharmacokinetics/Pharmacodynamics Analysis of Omadacycline in Chinese Healthy Subjects". In: *Frontiers in Pharmacology* Volume 13 (2022). ISSN: 1663-9812. DOI: 10.3389/fphar.2022.869237. | +| RBT | Rifabutin | Stefanie Hennig et al. "Population pharmacokinetic drug-drug interaction pooled analysis of existing data for rifabutin and HIV PIs". In: *Jounral of Antimicrobial Chemotherapy* 71.5 (2016), pp. 1330-1340. | +| RIF | Rifampicin | Justin J. Wilkins et al. "Population Pharmacokinetics of Rifampin in Pulmonary Tuberculosis Patients, Including a Semimechanistic Model To Describe Variable Absorption". In: *Antimicrobial Agents and Chemotherapy* 52.6 (2008), pp. 2138-2148. DOI: 10.1128/aac.0461-07. | +| TIG | Tigecycline | Agnieszka Borsuk-De Moor et al. "Population pharmacokinetics of high-dose tigecycline in patients with sepsis or septic shock". In: *Antimicrobial agents and chemotherapy* 62.4 (2018), pp. 10-1128. | + +## Running the models + +Run `main.m` from the repository root. It simulates each drug in turn, assembles +the main-text PTA figure (`plotFullPTAFig`), runs the supplementary dosing +scenarios, and assembles the supplementary figure (`plotSuppPTAFig`). The +relative `addpath` calls in `main.m` require the repository root as the working +directory. + +Tested on **MATLAB R2026a**. The Statistics and Machine Learning Toolbox is +required; the Parallel Computing Toolbox is required for any drug run with +`'Parallelize'` set to true. + +## Repository structure + +Each drug has its own folder containing a `_PlasmaODEs.m` (the ODE +right-hand side) and a `_PopPK.m` (the population driver). + +| Folder | Drug | Contents | +|--------|------|----------| +| `AMK/` | Amikacin | `AMK_PlasmaODEs.m`, `AMK_PopPK.m` | +| `BDQ/` | Bedaquiline | `BDQ_PlasmaODEs.m`, `BDQ_PopPK.m` | +| `CFZ/` | Clofazimine | `CFZ_PlasmaODEs.m`, `CFZ_PopPK.m` | +| `CLR/` | Clarithromycin | `CLR_PlasmaODEs.m`, `CLR_PopPK.m` | +| `EMB/` | Ethambutol | `EMB_PlasmaODEs.m`, `EMB_PopPK.m` | +| `FOX/` | Cefoxitin | `FOX_PlasmaODEs.m`, `FOX_PopPK.m` | +| `IMI/` | Imipenem | `IMI_PlasmaODEs.m`, `IMI_PopPK.m` | +| `LZD/` | Linezolid | `LZD_PlasmaODEs.m`, `LZD_PopPK.m` | +| `MXF/` | Moxifloxacin | `MXF_PlasmaODEs.m`, `MXF_PopPK.m` | +| `OMC/` | Omadacycline | `OMC_PlasmaODEs.m`, `OMC_PopPK.m` | +| `RBT/` | Rifabutin | `RBT_PlasmaODEs.m`, `RBT_PopPK.m` | +| `RIF/` | Rifampicin | `RIF_PlasmaODEs.m`, `RIF_PopPK.m` | +| `TIG/` | Tigecycline | `TIG_PlasmaODEs.m`, `TIG_PopPK.m` | +| `Methods/` | — | Shared helper functions used by all simulations (see `Methods/README.md`) | diff --git a/RIF/RIF_PlasmaODEs.m b/RIF/RIF_PlasmaODEs.m new file mode 100644 index 0000000..4579283 --- /dev/null +++ b/RIF/RIF_PlasmaODEs.m @@ -0,0 +1,123 @@ +%% ======================================================================= +% RIF_PlasmaODEs.m — Transit-Absorption / One-Compartment ODEs for +% Rifampicin +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the rifampicin plasma PK model: an oral +% dose enters through a transit-compartment absorption term into a single +% central compartment with first-order elimination. The transit-absorption +% input is evaluated in log space (gammaln) for numerical stability. +% Intended to be called by a stiff ODE solver (ode15s) from RIF_PopPK. +% +% MODEL +% ODEs as described in Wilkins et al., AAC, 2008. +% Justin J. Wilkins et al. "Population Pharmacokinetics of Rifampin in +% Pulmonary Tuberculosis Patients, Including a Semimechanistic Model To +% Describe Variable Absorption". In: *Antimicrobial Agents and Chemotherapy* +% 52.6 (2008), pp. 2138-2148. DOI: 10.1128/aac.0461-07. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver (used as time +% after dose in the transit-absorption term). +% y - State vector of drug amounts (mg): +% y(1) = absorption compartment amount (A1) +% y(2) = central compartment amount (A2) +% params - Parameter vector: +% params(1) = n number of transit compartments +% params(2) = MTT mean transit time (h) +% params(3) = dose dose for this interval (mg) +% params(4) = F bioavailability +% params(5) = ka absorption rate constant (1/h) +% params(6) = CL/F clearance (L/h) +% params(7) = V/F central volume (L) +% params(8) = SDF formulation flag (0 = FDC, 1 = SDF) +% +% OUTPUTS +% dy - Derivative vector (mg/h): +% dy(1) = d(absorption)/dt +% dy(2) = d(central)/dt +% +% NOTES +% The transit rate constant is computed internally as ktr = (n+1)/MTT. +% The transit-absorption input uses log_input = log(dose)+log(F)+log(ktr) +% + n*log(ktr*t) - ktr*t - gammaln(n+1), then exp(), which is the +% numerically stable form of the Erlang/transit density. SDF is passed in +% but the formulation effect is applied to CL/F and MTT in RIF_PopPK, not +% inside this function. +% +% AUTHORS +% E. Pienaar +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: 2023-04-18 +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = RIF_PlasmaODEs(t,y,params) + + %========================================================================== + % Extract parameter values from the param variable and assign to local + % variables + + % Number of transit compartments n (number) + n = params(1); + + % Mean transit time MTT in hr + MTT = params(2); + + % Amount of drug administered dose in mg + dose = params(3); + + % Bioavailability F + F = params(4); + + % Absorption rate ka in 1/hr + ka = params(5); + + % Clearance CL/F in liters/hr + CL_F = params(6); + + % Volume of distribution V/F in liters + V_F = params(7); + + % Formulation type: 0 for FDC Fixed dose combo; 1 for SDF single dose + % formulation + SDF = params(8); + + % Transit rate constant between transit compartments ktr in 1/h + ktr = (n+1) / MTT; + + % Current time - after last dose tad in hours + tad = t; + + + %========================================================================== + % Get ODE variables from the y to rename them + + % Drug amount in absorption compartment A1 in mg + A1 = y(1); + + % Drug amount in Central compartment A2 in mg + A2 = y(2); + + %========================================================================== + % Define ODEs + + % Change in mass of drug in absorption compartment A1 (mg/hr) + x = max(ktr*tad, realmin); % protect small t + log_input = log(dose) + log(F) + log(ktr) + n*log(x) - x - gammaln(n+1); + inp = exp(log_input); % mg/h + dA1dt = inp - ka*A1; % mg/h + + % Change in mass of drug in central compartment A2 (mg/hr) + dA2dt = ka * A1 - (CL_F / V_F)*A2; + + % Assign differentials to output arguments + dy(1,1) = dA1dt; + dy(2,1) = dA2dt; + +end \ No newline at end of file diff --git a/RIF/RIF_PopPK.m b/RIF/RIF_PopPK.m new file mode 100644 index 0000000..92af877 --- /dev/null +++ b/RIF/RIF_PopPK.m @@ -0,0 +1,545 @@ +%% ======================================================================= +% RIF_PopPK.m — Population PK Simulation & PTA Analysis for Rifampicin +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the rifampicin plasma ODEs across a Monte Carlo patient +% population. Models a one-compartment disposition system with a +% transit-compartment oral absorption model and a formulation covariate +% (single-drug formulation [SDF] vs fixed-dose combination [FDC]) acting +% on CL/F and MTT. Computes steady-state AUC/Cmax and generates +% time-course, AUC, and probability of target attainment (PTA) plots. +% +% A parfor (parallelized) path and a serial path are provided; select via +% the 'Parallelize' option. Both paths are functionally equivalent. +% +% MODEL +% Reimplementation of Wilkins et al., AAC, 2008. +% Justin J. Wilkins et al. "Population Pharmacokinetics of Rifampin in +% Pulmonary Tuberculosis Patients, Including a Semimechanistic Model To +% Describe Variable Absorption". In: *Antimicrobial Agents and Chemotherapy* +% 52.6 (2008), pp. 2138-2148. DOI: 10.1128/aac.0461-07. +% Formulation mix (fraction receiving SDF) derived from the source Table 1. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (600) Dose per administration (mg). +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (24) Time between doses (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% 'Parallelize' (true) Use parfor over patients. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: E. Pienaar; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2023-04-18 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% RIF_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% Parallel Computing Toolbox required when 'Parallelize' is true. +% ======================================================================== + + +function [plotArgs] = RIF_PopPK(varargin) + + % Initialize output + plotArgs = []; + + % Set the ODE solver tolerances and constraints + % NOTE: RIF uses explicit RelTol/AbsTol and NonNegative on 2 equations + options = odeset('RelTol', 1e-8, 'AbsTol', 1e-10, ... + 'NonNegative', [1 2], 'MaxStep', 0.05); + + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 600, @(x)isnumeric(x)); + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 24, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.addParameter('Parallelize', true, @(b)islogical(b)&&isscalar(b)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Clearance (CL/F, L/h) + % ------------------------------------------ + CL_F_mean_RIF = 19.2; + + % Volume of distribution (V/F, L) + % ------------------------------------------ + V_F_mean_RIF = 53.2; + + % Absorption rate constant (ka, 1/h) + % ------------------------------------------ + ka_mean_RIF = 1.15; + + % Mean transit time (MTT, h) + % ------------------------------------------ + MTT_mean_RIF = 0.424; + + % Number of transit compartments (n, unitless) + % ------------------------------------------ + n_mean_RIF = 7.13; + + % Effect of SDF formulation on CL/F + % ------------------------------------------ + theta_SDF_CLF_mean_RIF = 0.236; + + % Effect of SDF formulation on MTT + % ------------------------------------------ + theta_SDF_MTT_mean_RIF = 1.04; + + % Bioavailability (F, unitless; assumed 1 per Wilkins et al.) + % ------------------------------------------ + F_RIF = 1; + + % Fraction of patients that received SDF formulation (from Table 1) + % ------------------------------------------ + frac_sdf_RIF = (4 + 105 + 0) / (4 + 105 + 0 + 87 + 34 + 31); + + % Interindividual variability (IIV) — stored as variance, used as sqrt(var) + % NOTE: RIF IIV is parameterized on the log-scale variance directly, + % not as % CV; sigma = sqrt(variance) is passed to Normal(0, sigma) + % ------------------------------------------ + eta_CL_F_var_RIF = 0.279; % IIV variance for CL/F + eta_V_F_var_RIF = 0.188; % IIV variance for V/F + eta_ka_var_RIF = 0.439; % IIV variance for ka + eta_MTT_var_RIF = 0.361; % IIV variance for MTT + eta_n_var_RIF = 2.44; % IIV variance for n + + % Interoccasion variability (IOV) — defined per Wilkins et al. + % ------------------------------------------ + kappa_CL_F_var_RIF = 0.0508; % IOV variance for CL/F %#ok + kappa_MTT_var_RIF = 0.461; % IOV variance for MTT %#ok + + % Residual variability + % ------------------------------------------ + epsilon_add_RIF = 0.0923; % Additive error (mg/L) %#ok + epsilon_ccv_RIF = 0.222; % Proportional error %#ok + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify how much drug is given in each dose (mg) + dose_size_RIF = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_RIF = opt.NumDoses; + + % Specify time (hours) between doses + dose_time_RIF = opt.DoseInterval; + + % Specify time step and build time vectors + time_step_size_RIF = 0.5; + + time_vec_RIF = 0 : time_step_size_RIF : dose_time_RIF; + total_timpts_RIF = n_doses_RIF * (length(time_vec_RIF) - 1); + total_params_RIF = 8; + time_vec_local_RIF = 0 : time_step_size_RIF : dose_time_RIF * n_doses_RIF - time_step_size_RIF; + + + % -------------------- Initialize Matrices -------------------- + + param_store_RIF = zeros([total_params_RIF n_pts]); + RIF_conc_depot = zeros([total_timpts_RIF n_pts]); + RIF_conc_plasma = zeros([total_timpts_RIF n_pts]); + + % Diagnostic arrays for sampled patient parameters + ka_pt_RIF = zeros([1 n_pts]); + n_pt_RIF = zeros([1 n_pts]); + V_F_pt_RIF = zeros([1 n_pts]); + MTT_pt_RIF = zeros([1 n_pts]); + CL_F_pt_RIF = zeros([1 n_pts]); + + + % -------------------- ODE Solving -------------------- + + % Parallel path (default) + if opt.Parallelize + + parfor ipt = 1:n_pts + + % Formulation assignment: Bernoulli draw for SDF vs FDC + % -------------------------------------------------------- + if rand < frac_sdf_RIF + SDF_i = 1; + else + SDF_i = 0; + end + + % Sample patient-specific parameters using IIV + % NOTE: sigma = sqrt(variance) per Wilkins et al. log-scale parameterization + % -------------------------------------------------------- + ka_i = ka_mean_RIF * exp(random('Normal', 0, sqrt(eta_ka_var_RIF))); + n_i = n_mean_RIF * exp(random('Normal', 0, sqrt(eta_n_var_RIF))); + V_i = V_F_mean_RIF * exp(random('Normal', 0, sqrt(eta_V_F_var_RIF))); + MTT_i = MTT_mean_RIF * (1 + theta_SDF_MTT_mean_RIF * SDF_i) * ... + exp(random('Normal', 0, sqrt(eta_MTT_var_RIF))); + CL_i = CL_F_mean_RIF * (1 + theta_SDF_CLF_mean_RIF * SDF_i) * ... + exp(random('Normal', 0, sqrt(eta_CL_F_var_RIF))); + + % Save to diagnostic arrays + ka_pt_RIF(ipt) = ka_i; + n_pt_RIF(ipt) = n_i; + V_F_pt_RIF(ipt) = V_i; + MTT_pt_RIF(ipt) = MTT_i; + CL_F_pt_RIF(ipt) = CL_i; + + % Compile parameters into vector to pass to ODE solver + % params = [n; MTT; dose; F; ka; CL/F; V/F; SDF flag] + params_i = [n_i; MTT_i; dose_size_RIF; F_RIF; ka_i; CL_i; V_i; SDF_i]; + + % Set initial conditions + % -------------------------------------------------------- + depot_ic = 0; + plasma_ic = 0; + + % Initialize local storage + depot_loc = zeros([total_timpts_RIF 1]); + plasma_loc = zeros([total_timpts_RIF 1]); + + for idose = 1:n_doses_RIF + + % Call ODE solver + % --------------- + [~, y] = ode15s(@(t,y) RIF_PlasmaODEs(t, y, params_i), ... + time_vec_RIF, [depot_ic, plasma_ic], options); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_RIF) - 1) + 1; + i1 = idose * (length(time_vec_RIF) - 1); + + depot_loc(i0:i1) = y(1:end-1, 1); % depot amount (mg) + plasma_loc(i0:i1) = y(1:end-1, 2) / V_i; % plasma conc (mg/L) + + % Update initial conditions for next dose + depot_ic = y(end, 1); + plasma_ic = y(end, 2); + + end + + % Store patient results + RIF_conc_depot(:, ipt) = depot_loc; + RIF_conc_plasma(:, ipt) = plasma_loc; + param_store_RIF(:, ipt) = params_i; + + end + + % Serial path + else + + for ipt = 1:n_pts + + % Formulation assignment: Bernoulli draw for SDF vs FDC + % -------------------------------------------------------- + if rand < frac_sdf_RIF + SDF_i = 1; + else + SDF_i = 0; + end + + % Sample patient-specific parameters using IIV + % NOTE: sigma = sqrt(variance) per Wilkins et al. log-scale parameterization + % -------------------------------------------------------- + ka_i = ka_mean_RIF * exp(random('Normal', 0, sqrt(eta_ka_var_RIF))); + n_i = n_mean_RIF * exp(random('Normal', 0, sqrt(eta_n_var_RIF))); + V_i = V_F_mean_RIF * exp(random('Normal', 0, sqrt(eta_V_F_var_RIF))); + MTT_i = MTT_mean_RIF * (1 + theta_SDF_MTT_mean_RIF * SDF_i) * ... + exp(random('Normal', 0, sqrt(eta_MTT_var_RIF))); + CL_i = CL_F_mean_RIF * (1 + theta_SDF_CLF_mean_RIF * SDF_i) * ... + exp(random('Normal', 0, sqrt(eta_CL_F_var_RIF))); + + % Save to diagnostic arrays + ka_pt_RIF(ipt) = ka_i; + n_pt_RIF(ipt) = n_i; + V_F_pt_RIF(ipt) = V_i; + MTT_pt_RIF(ipt) = MTT_i; + CL_F_pt_RIF(ipt) = CL_i; + + % Compile parameters into vector to pass to ODE solver + % params = [n; MTT; dose; F; ka; CL/F; V/F; SDF flag] + params_i = [n_i; MTT_i; dose_size_RIF; F_RIF; ka_i; CL_i; V_i; SDF_i]; + + % Set initial conditions + % -------------------------------------------------------- + depot_ic = 0; + plasma_ic = 0; + + % Initialize local storage + depot_loc = zeros([total_timpts_RIF 1]); + plasma_loc = zeros([total_timpts_RIF 1]); + + for idose = 1:n_doses_RIF + + % Call ODE solver + % --------------- + [~, y] = ode15s(@(t,y) RIF_PlasmaODEs(t, y, params_i), ... + time_vec_RIF, [depot_ic, plasma_ic], options); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_RIF) - 1) + 1; + i1 = idose * (length(time_vec_RIF) - 1); + + depot_loc(i0:i1) = y(1:end-1, 1); % depot amount (mg) + plasma_loc(i0:i1) = y(1:end-1, 2) / V_i; % plasma conc (mg/L) + + % Update initial conditions for next dose + depot_ic = y(end, 1); + plasma_ic = y(end, 2); + + end + + % Store patient results + RIF_conc_depot(:, ipt) = depot_loc; + RIF_conc_plasma(:, ipt) = plasma_loc; + param_store_RIF(:, ipt) = params_i; + + end + + end + + % Expose parameter matrix for downstream analysis + parameters = param_store_RIF; %#ok + + + % -------------------- Plotting Raw Timecourses -------------------- + % NOTE: Plasma raw plot shows last-dose window only; depot shows full timecourse + + if opt.TimeSeriesPlots + + cycles_RIF = n_doses_RIF; + xvalues_RIF = time_vec_local_RIF; + + % Plasma (central) — last dose window only + plotTimeCourses( ... + 'X', xvalues_RIF, ... + 'Y', RIF_conc_plasma, ... + 'DrugName', 'RIF', ... + 'Compartment', 'Plasma (Central)', ... + 'Cycles', cycles_RIF, ... + 'DoseInterval', dose_time_RIF, ... + 'FigureName', 'RIF Raw Timecourses (Plasma)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + % Depot — full timecourse + plotTimeCourses( ... + 'X', xvalues_RIF, ... + 'Y', RIF_conc_depot, ... + 'DrugName', 'RIF', ... + 'Compartment', 'Depot', ... + 'Cycles', cycles_RIF, ... + 'DoseInterval', dose_time_RIF, ... + 'FigureName', 'RIF Raw Timecourses (Depot)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + cycles_stats_RIF = n_doses_RIF; + xvalues_stats_RIF = time_vec_local_RIF; + + plotTimeCourses( ... + 'X', xvalues_stats_RIF, ... + 'Y', RIF_conc_plasma, ... + 'DrugName', 'RIF', ... + 'Compartment', 'Plasma (Central)', ... + 'Cycles', cycles_stats_RIF, ... + 'DoseInterval', dose_time_RIF, ... + 'FigureName', 'RIF Stats (Plasma)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_stats_RIF, ... + 'Y', RIF_conc_depot, ... + 'DrugName', 'RIF', ... + 'Compartment', 'Depot', ... + 'Cycles', cycles_stats_RIF, ... + 'DoseInterval', dose_time_RIF, ... + 'FigureName', 'RIF Stats (Depot)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_RIF, ... + 'time_step_size', time_step_size_RIF, ... + 'conc_c', RIF_conc_plasma, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'RIF steady-state attainment', 'SortBy', 'name'); + + statsT = table( ... + mean(last24AUC, 'omitnan'), std(last24AUC, 'omitnan'), ... + mean(last24Cmax, 'omitnan'), std(last24Cmax, 'omitnan'), ... + 'VariableNames', {'mean_last24AUC', 'std_last24AUC', 'mean_last24Cmax', 'std_last24Cmax'}); + + fprintf('\n=== RIF last-24h summary stats ===\n'); + disp(statsT); + end + + if numBothReached ~= n_pts + warning('Steady state not reached for all patients.'); + end + + if opt.plotAUC + plotAUC(last24AUC, 'RIF'); + end + + + % -------------------- PTA Analysis -------------------- + + if opt.PTAplot + + % -------------------- MIC Distributions NTM -------------------- + % This section includes the minimum inhibitory concentration (MIC) + % distributions for the non-tuberculosis mycobacterium strains belonging + % to associated non-tuberculosis mycobacterium species + + MIC_MAB_RIF = { + 'M. abscessus (RIF)'; + {{'0.031', 0}, {'0.062', 0}, {'0.125', 0}, {'0.25', 0}, {'0.5', 0}, ... + {'1', 1}, {'2', 1}, {'4', 0}, {'8', 1}, {'16', 1}, ... + {'32', 13}, {'64', 120}, {'128', 55}, {'256', 2}}; + 'AUC24/MIC'; + 194 + }; + + MIC_MAC_RIF = { + 'M. avium complex (RIF)'; + {{'0.008', 0}, {'0.016', 0}, ... + {'0.031', 0}, {'0.062', 0}, {'0.125', 24}, {'0.25', 42}, {'0.5', 109}, ... + {'1', 407}, {'2', 771}, {'4', 1103}, {'8', 1131}, {'16', 967}, ... + {'32', 134}, {'64', 0}, {'128', 1}, {'256', 0}}; + 'AUC24/MIC'; + 4689 + }; + + MIC_kan_RIF = { + 'M. kansasii (RIF)'; + {{'0.031', 0}, {'0.062', 40}, {'0.125', 40}, {'0.25', 33}, {'0.5', 3}, ... + {'1', 1}, {'2', 1}, {'4', 0}, {'8', 0}, {'16', 0}, ... + {'32', 0}, {'64', 0}, {'128', 0}, {'256', 0}}; + 'AUC24/MIC'; + 118 + }; + + MIC_TB_RIF = { + 'M. tuberculosis (RIF)'; + {{'0.031', 1213}, {'0.062', 2087}, {'0.125', 1296}, {'0.25', 510}, {'0.5', 130}, ... + {'1', 68}, {'2', 0}, {'4', 6}, {'8', 0}, {'16', 2289}, ... + {'32', 134}, {'64', 0}, {'128', 1}, {'256', 0}}; + 'AUC24/MIC'; + 356 + }; + + speciesAggregation_RIF = {MIC_MAB_RIF MIC_MAC_RIF MIC_kan_RIF MIC_TB_RIF}; + normalizedMICSpeciesAggregation_RIF = normalizeMICsOfAggregation(speciesAggregation_RIF); + uniqueMICs_RIF = getUniqueMICs(normalizedMICSpeciesAggregation_RIF); + + + % -------------------- Setting PTA Targets -------------------- + + target1_RIF = { % https://doi.org/10.5588/ijtld.22.0188 + 'TB'; + 'AUC24/MIC'; + 271; + 'tuberculosis' + }; + + uniqueTargets_RIF = {target1_RIF}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix_RIF = zeros(length(uniqueMICs_RIF), length(uniqueTargets_RIF)); + + for i = 1:length(uniqueTargets_RIF) + PTAMatrix_RIF(:, i) = calculatePTA( ... + last24ConcArray, ... + 'MICDist', uniqueMICs_RIF, ... + 'targetType', uniqueTargets_RIF{i}{2}, ... + 'target', uniqueTargets_RIF{i}{3}, ... + 'time_step_size', time_step_size_RIF, ... + 'last24AUC', last24AUC, ... + 'last24Cmax', last24Cmax); + end + + legendArray_RIF = getLegendArray(uniqueTargets_RIF, normalizedMICSpeciesAggregation_RIF); + + [~, plotArgs] = plotPTAs( ... + uniqueMICs_RIF, ... + PTAMatrix_RIF, ... + normalizedMICSpeciesAggregation_RIF, ... + 'DoseSize', dose_size_RIF, ... + 'DoseFrequency', dose_time_RIF, ... + 'DrugName', 'RIF', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray_RIF, ... + 'UniqueTargets', uniqueTargets_RIF, ... + 'NumPatients', n_pts, ... + 'ShowCI', false); + + end + +end \ No newline at end of file diff --git a/TIG/TIG_PlasmaODEs.m b/TIG/TIG_PlasmaODEs.m new file mode 100644 index 0000000..8273a4a --- /dev/null +++ b/TIG/TIG_PlasmaODEs.m @@ -0,0 +1,108 @@ +%% ======================================================================= +% TIG_PlasmaODEs.m — Two-Compartment IV Infusion ODEs for Tigecycline +% ======================================================================== +% +% DESCRIPTION +% Right-hand side function for the tigecycline plasma PK model: a central +% compartment with zero-order IV infusion input, exchange with one +% peripheral compartment, and first-order elimination from the central +% compartment. Intended to be called by an ODE solver (ode45) from +% TIG_PopPK. +% +% MODEL +% ODEs as described in Bastida et al., 2022. +% Agnieszka Borsuk-De Moor et al. "Population pharmacokinetics of high-dose +% tigecycline in patients with sepsis or septic shock". In: *Antimicrobial +% agents and chemotherapy* 62.4 (2018), pp. 10-1128. +% +% INPUTS +% t - Time (h); scalar supplied by the ODE solver. +% y - State vector of drug amounts (mg): +% y(1) = central compartment amount +% y(2) = peripheral compartment amount +% params - Parameter vector: +% params(1) = Vc central volume (L) +% params(2) = Vp peripheral volume (L) +% params(3) = CL clearance (L/h) +% params(4) = Q intercompartmental clearance (L/h) +% params(5) = dose dose for this interval (mg) +% params(6) = infusion duration (h) +% +% OUTPUTS +% dy - Derivative vector (mg/h): +% dy(1) = d(central)/dt +% dy(2) = d(peripheral)/dt +% +% AUTHORS +% Tyler Dierckman +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Tested on: MATLAB R2026a +% ======================================================================== + + +function dy = TIG_PlasmaODEs(t,y,params) + + % Extracting parameter values from params variable to assign to local variables + + % Departmental constants + + V_Fs = [... + params(1) % (L) Central compartment Vc/F + params(2) % (L) Peripheral compartment Vp/F + ]; + rates = [... + params(3) % (L/h) CL/F Oral Clearance + params(4) % (L/h) Q/F Intercompartmental Clearance + ]; + + % Dose related variables + dose = params(5); %(mg) dose size + infusion_duration = params(6); + + % Infusion rate + if t < infusion_duration + infusion_rate = dose / infusion_duration; + else + infusion_rate = 0; + end + + % ODE Y value assignmetns + A = [... + y(1) % Drug amount in central compartment + y(2) % Drug amount in peripheral compartment + ]; + %y(1) % Drug amount in depot compartment + + % Define ODEs + + % ODE around CENTRAL compartment (an IV drug so the central comp. is also + % depot) + % ------------------------------------------ + % ODE_1 terms + dA1_term1 = - rates(2) * A(1) / V_Fs(1); %Flow out to Peripheral compartment + dA1_term2 = rates(2) * A(2) / V_Fs(2); %Flow in from peripheral to central + dA1_term3 = - rates(1) * A(1) / V_Fs(1); %Clearance of drug from system + + dA1dt = (infusion_rate)+(dA1_term1 + dA1_term2 + dA1_term3); %(mg/h) + + % ODE around PERIPHERAL compartment + % ------------------------------------------ + % ODE_2 terms + dA2_term1 = rates(2) * A(1) / V_Fs(1); %In term + dA2_term2 = - rates(2) * A(2) / V_Fs(2); %Out term + + dA2dt = (dA2_term1 + dA2_term2); %(mg/h) + + + % ODE Outputs + + dy = [... + dA1dt + dA2dt + ]; + + +end \ No newline at end of file diff --git a/TIG/TIG_PopPK.m b/TIG/TIG_PopPK.m new file mode 100644 index 0000000..045d75c --- /dev/null +++ b/TIG/TIG_PopPK.m @@ -0,0 +1,494 @@ +%% ======================================================================= +% TIG_PopPK.m — Population PK Simulation & PTA Analysis for Tigecycline +% ======================================================================== +% +% DESCRIPTION +% Samples population PK parameters with interindividual variability (IIV) +% and solves the tigecycline plasma ODEs across a Monte Carlo patient +% population. Models a two-compartment system with zero-order IV infusion +% input and first-order elimination, with a loading dose (first dose given +% at 2x the maintenance dose). Computes steady-state AUC/Cmax and generates +% time-course, AUC, and probability of target attainment (PTA) plots. +% +% A serial path and a parfor (parallelized) path are provided; select via +% the 'Parallelize' option. Both paths are functionally equivalent. +% +% MODEL +% Reimplementation of Bastida et al., 2022. +% Agnieszka Borsuk-De Moor et al. "Population pharmacokinetics of high-dose +% tigecycline in patients with sepsis or septic shock". In: *Antimicrobial +% agents and chemotherapy* 62.4 (2018), pp. 10-1128. +% +% INPUTS (name-value pairs; defaults in parentheses) +% 'NumPatients' (1000) Number of virtual patients to simulate. +% 'DoseSize' (50) Maintenance dose per administration (mg); +% first dose is given at 2x this value. +% 'NumDoses' (30) Total number of doses. +% 'DoseInterval' (12) Time between doses (h). +% 'IVDuration' (0.5) Duration of IV infusion (h). +% 'Quiet' (false) Suppress console summary output. +% 'TimeSeriesPlots' (true) Plot raw concentration time courses. +% 'StatisticsPlots' (true) Plot percentile time courses. +% 'plotAUC' (true) Plot last-24h AUC distribution. +% 'PTAplot' (true) Compute and plot PTA vs MIC. +% 'GraphFontSize' (16) Font size for generated figures. +% 'Parallelize' (false) Use parfor over patients. +% +% OUTPUTS +% plotArgs - PTA plotting payload consumed by plotFullPTAFig. +% +% AUTHORS +% T. J. Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, +% Weldon School of Biomedical Engineering, Purdue University +% +% Original implementation: Tyler Dierckman; +% refactored by T. J. Shoaf, 2025. +% +% VERSION +% Created: 2025 +% Tested on: MATLAB R2026a +% +% DEPENDENCIES +% TIG_PlasmaODEs, plotTimeCourses, calculateSteadyState, +% summarize_ss_counts, plotAUC, normalizeMICsOfAggregation, +% getUniqueMICs, calculatePTA, getLegendArray, plotPTAs. +% Parallel Computing Toolbox required when 'Parallelize' is true. +% ======================================================================== + + +function [plotArgs] = TIG_PopPK(varargin) + + % Initialize output + plotArgs = []; + + + % -------------------- Parse Options -------------------- + + p = inputParser; + + p.addParameter('NumPatients', 1000, @(x)isnumeric(x)); + p.addParameter('DoseSize', 50, @(x)isnumeric(x)); + p.addParameter('NumDoses', 30, @(x)isnumeric(x)); + p.addParameter('DoseInterval', 12, @(x)isnumeric(x)); + + p.addParameter('IVDuration', 0.5, @(x)isnumeric(x)); + + p.addParameter('Quiet', false, @(b)islogical(b)&&isscalar(b)); + p.addParameter('TimeSeriesPlots', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('StatisticsPlots', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('plotAUC', true, @(b)islogical(b)&&isscalar(b)); + p.addParameter('PTAplot', true, @(b)islogical(b)&&isscalar(b)); + + p.addParameter('GraphFontSize', 16, @(x)isnumeric(x)); + + p.addParameter('Parallelize', false, @(b)islogical(b)&&isscalar(b)); + + p.parse(varargin{:}); + opt = p.Results; + + + % -------------------- Parameter Values -------------------- + + % Volume parameters + % ------------------------------------------ + V_1 = 63.7; % (L) Central compartment volume of distribution + V_2 = 233; % (L) Peripheral compartment volume of distribution + + % Clearance constants + % ------------------------------------------ + CL = 14.8; % (L/h) Clearance from central compartment + Q = 38.4; % (L/h) Intercompartmental clearance + + % Interindividual variability (% CV) + % ------------------------------------------ + V_1_IIV = 0.502; % V_1 (% CV) IIV + CL_IIV = 0.466; % CL (% CV) IIV + + + % -------------------- Variables for ODEs -------------------- + + % Specify number of patients to simulate + n_pts = opt.NumPatients; + + % Specify how much drug is given in each dose (mg) + dose_size_TIG = opt.DoseSize; + + % Specify number of doses to simulate + n_doses_TIG = opt.NumDoses; + + % Specify time (hours) between doses + dose_time_TIG = opt.DoseInterval; + + % Specify IV infusion duration (hours) + IV_dose_duration_TIG = opt.IVDuration; + + % Specify time step and build time vectors + time_step_size_TIG = 0.5; + + time_vec_TIG = 0 : time_step_size_TIG : dose_time_TIG; + total_timpts_TIG = n_doses_TIG * (length(time_vec_TIG) - 1); + total_params_TIG = 6; + time_vec_local_TIG = 0 : time_step_size_TIG : dose_time_TIG * n_doses_TIG - time_step_size_TIG; + + + % -------------------- Initialize Matrices -------------------- + + TIG_conc_c = zeros([total_timpts_TIG n_pts]); + TIG_conc_p = zeros([total_timpts_TIG n_pts]); + param_store_TIG = zeros([total_params_TIG n_pts]); + + + % -------------------- ODE Solving -------------------- + + % Serial path (default) + if ~opt.Parallelize + + for ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + V_1_pt = V_1 * exp(random('Normal', 0, sqrt(log(V_1_IIV^2 + 1)))); + CL_pt = CL * exp(random('Normal', 0, sqrt(log(CL_IIV^2 + 1)))); + + % Set initial conditions + % -------------------------------------------------------- + TIG_c_IC = 0; + TIG_p_IC = 0; + + % Initialize local storage + TIG_conc_c_loc = zeros([total_timpts_TIG 1]); + TIG_conc_p_loc = zeros([total_timpts_TIG 1]); + + for idose = 1:n_doses_TIG + + % Loading dose: first dose is 2x the maintenance dose + if idose == 1 + dose_size_loc = dose_size_TIG * 2; + else + dose_size_loc = dose_size_TIG; + end + + % Compile parameters into vector to pass to ODE solver + params_TIG = [... + V_1_pt % params(1) Vc + V_2 % params(2) Vp + CL_pt % params(3) CL + Q % params(4) Q + dose_size_loc % params(5) dose size + IV_dose_duration_TIG % params(6) IV infusion duration + ]; + + % Call ODE solver + % --------------- + [~, TIG_sol] = ode45(@(t,y) TIG_PlasmaODEs(t, y, params_TIG), ... + time_vec_TIG, [TIG_c_IC TIG_p_IC]); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_TIG) - 1) + 1; + i1 = idose * (length(time_vec_TIG) - 1); + + TIG_conc_c_loc(i0:i1) = TIG_sol(1:end-1, 1) / V_1_pt; + TIG_conc_p_loc(i0:i1) = TIG_sol(1:end-1, 2) / V_2; + + % Update initial conditions for next dose + TIG_c_IC = TIG_sol(end, 1); + TIG_p_IC = TIG_sol(end, 2); + + end + + % Store patient results + param_store_TIG(:, ipt) = params_TIG; + TIG_conc_c(:, ipt) = TIG_conc_c_loc; + TIG_conc_p(:, ipt) = TIG_conc_p_loc; + + end + + % Parallel path + else + + parfor ipt = 1:n_pts + + % Sample patient-specific parameters using IIV + % -------------------------------------------------------- + V_1_pt = V_1 * exp(random('Normal', 0, sqrt(log(V_1_IIV^2 + 1)))); + CL_pt = CL * exp(random('Normal', 0, sqrt(log(CL_IIV^2 + 1)))); + + % Set initial conditions + % -------------------------------------------------------- + TIG_c_IC = 0; + TIG_p_IC = 0; + + % Initialize local storage + TIG_conc_c_loc = zeros([total_timpts_TIG 1]); + TIG_conc_p_loc = zeros([total_timpts_TIG 1]); + + params_TIG_loc = zeros([total_params_TIG 1]); + + for idose = 1:n_doses_TIG + + % Loading dose: first dose is 2x the maintenance dose + if idose == 1 + dose_size_loc = dose_size_TIG * 2; + else + dose_size_loc = dose_size_TIG; + end + + % Compile parameters into vector to pass to ODE solver + params_TIG_loc = [... + V_1_pt % params(1) Vc + V_2 % params(2) Vp + CL_pt % params(3) CL + Q % params(4) Q + dose_size_loc % params(5) dose size + IV_dose_duration_TIG % params(6) IV infusion duration + ]; + + % Call ODE solver + % --------------- + [~, TIG_sol] = ode45(@(t,y) TIG_PlasmaODEs(t, y, params_TIG_loc), ... + time_vec_TIG, [TIG_c_IC TIG_p_IC]); + + % Organization of data post ODE + % ----------------------------- + i0 = (idose - 1) * (length(time_vec_TIG) - 1) + 1; + i1 = idose * (length(time_vec_TIG) - 1); + + TIG_conc_c_loc(i0:i1) = TIG_sol(1:end-1, 1) / V_1_pt; + TIG_conc_p_loc(i0:i1) = TIG_sol(1:end-1, 2) / V_2; + + % Update initial conditions for next dose + TIG_c_IC = TIG_sol(end, 1); + TIG_p_IC = TIG_sol(end, 2); + + end + + % Store patient results + param_store_TIG(:, ipt) = params_TIG_loc; + TIG_conc_c(:, ipt) = TIG_conc_c_loc; + TIG_conc_p(:, ipt) = TIG_conc_p_loc; + + end + + end + + + % -------------------- Plotting Raw Timecourses -------------------- + % NOTE: Raw plot window is trimmed by 15 doses from the end to show + % early dynamics; this offset is preserved from the original + + if opt.TimeSeriesPlots + + cycles_TIG = n_doses_TIG - 15; + xvalues_TIG = time_vec_local_TIG; + + plotTimeCourses( ... + 'X', xvalues_TIG, ... + 'Y', TIG_conc_c, ... + 'DrugName', 'TIG', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_TIG, ... + 'DoseInterval', dose_time_TIG, ... + 'FigureName', 'TIG Raw Timecourses (Central)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_TIG, ... + 'Y', TIG_conc_p, ... + 'DrugName', 'TIG', ... + 'Compartment', 'Peripheral', ... + 'Cycles', cycles_TIG, ... + 'DoseInterval', dose_time_TIG, ... + 'FigureName', 'TIG Raw Timecourses (Peripheral)', ... + 'DoStats', false, ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Mean Value Graphs -------------------- + + if opt.StatisticsPlots + + cycles_stats_TIG = n_doses_TIG - 15; + xvalues_stats_TIG = time_vec_local_TIG; + + plotTimeCourses( ... + 'X', xvalues_stats_TIG, ... + 'Y', TIG_conc_c, ... + 'DrugName', 'TIG', ... + 'Compartment', 'Central', ... + 'Cycles', cycles_stats_TIG, ... + 'DoseInterval', dose_time_TIG, ... + 'FigureName', 'TIG Stats (Central)', ... + 'DoStats', true, ... + 'Percentiles', [5 50 95], ... + 'FontSize', opt.GraphFontSize); + + plotTimeCourses( ... + 'X', xvalues_stats_TIG, ... + 'Y', TIG_conc_p, ... + 'DrugName', 'TIG', ... + 'Compartment', 'Peripheral', ... + 'Cycles', cycles_stats_TIG, ... + 'DoseInterval', dose_time_TIG, ... + 'FigureName', 'TIG Stats (Peripheral)', ... + 'DoStats', true, ... + 'Percentiles', [10 50 90], ... + 'FontSize', opt.GraphFontSize); + + end + + + % -------------------- Calculating AUC and Cmax -------------------- + + AUCTolerance = 0.05; + CmaxTolerance = 0.05; + + [numAUCSSReached, numCmaxSSReached, numBothReached, ... + last24ConcArray, last24AUC, last24Cmax] = calculateSteadyState( ... + time_vec_local_TIG, ... + 'time_step_size', time_step_size_TIG, ... + 'conc_c', TIG_conc_c, ... + 'AUCTolerance', AUCTolerance, ... + 'CmaxTolerance', CmaxTolerance, ... + 'n_pts', n_pts); + + if ~opt.Quiet + summarize_ss_counts(numAUCSSReached, numCmaxSSReached, numBothReached, ... + n_pts, 'Title', 'TIG steady-state attainment', 'SortBy', 'name'); + + statsT = table( ... + mean(last24AUC, 'omitnan'), std(last24AUC, 'omitnan'), ... + mean(last24Cmax, 'omitnan'), std(last24Cmax, 'omitnan'), ... + 'VariableNames', {'mean_last24AUC', 'std_last24AUC', 'mean_last24Cmax', 'std_last24Cmax'}); + + fprintf('\n=== TIG last-24h summary stats ===\n'); + disp(statsT); + end + + if numBothReached ~= n_pts + warning('Steady state not reached for all patients.'); + end + + if opt.plotAUC + plotAUC(last24AUC, 'TIG'); + end + + + % -------------------- PTA Analysis -------------------- + + if opt.PTAplot + + % -------------------- MIC Distributions NTM -------------------- + % This section includes the minimum inhibitory concentration (MIC) + % distributions for the non-tuberculosis mycobacterium strains belonging + % to associated non-tuberculosis mycobacterium species + % + % NOTE: M. tuberculosis MIC data uses fractional counts (proportions) + % rather than integer isolate counts; this is preserved from the source + + MIC_MAB_TIG = { + 'M. abscessus'; + {{'0.008', 0}, {'0.016', 0}, {'0.031', 1}, {'0.062', 12}, {'0.125', 22}, ... + {'0.25', 77}, {'0.5', 98}, {'1', 75}, {'2', 40}, {'4', 7}, ... + {'8', 4}, {'16', 0}, {'32', 0}, {'64', 0}, {'128', 0}}; + 'AUC24/MIC'; + 336 + }; + + MIC_MAC_TIG = { + 'M. avium complex'; + {{'0.008', 0}, {'0.016', 0}, {'0.031', 0}, {'0.062', 1}, {'0.125', 0}, ... + {'0.25', 1}, {'0.5', 0}, {'1', 0}, {'2', 1}, {'4', 0}, ... + {'8', 1}, {'16', 8}, {'32', 8}, {'64', 10}, {'128', 16}}; + 'AUC24/MIC'; + 46 + }; + + MIC_kan_TIG = { + 'M. kansasii'; + {{'0.008', 0}, {'0.016', 0}, {'0.031', 0}, {'0.062', 0}, {'0.125', 0}, ... + {'0.25', 0}, {'0.5', 0}, {'1', 1}, {'2', 0}, {'4', 0}, ... + {'8', 1}, {'16', 5}, {'32', 0}, {'64', 1}, {'128', 3}}; + 'AUC24/MIC'; + 11 + }; + + MIC_TB_TIG = { + 'M. tuberculosis'; + {{'0.008', 0}, {'0.016', 0}, {'0.031', 0}, {'0.062', 0}, {'0.125', 0}, ... + {'0.25', 0.07}, {'0.5', 0.21}, {'1', 0.25}, {'2', 0.14}, {'4', 0.11}, ... + {'8', 0.07}, {'16', 0.11}, {'32', 0}, {'64', 0}, {'128', 0}}; + 'AUC24/MIC'; + 0.96 + }; + + speciesAggregation_TIG = {MIC_MAB_TIG MIC_MAC_TIG MIC_kan_TIG MIC_TB_TIG}; + normalizedMICSpeciesAggregation_TIG = normalizeMICsOfAggregation(speciesAggregation_TIG); + uniqueMICs_TIG = getUniqueMICs(normalizedMICSpeciesAggregation_TIG); + + + % -------------------- Setting PTA Targets -------------------- + + target1_TIG = { % https://doi.org/10.1128%2FAAC.03112-15 + 'EC80 for MAB'; + 'AUC24/MIC'; + 36.65; + 'abscessus' + }; + + target2_TIG = { % https://doi.org/10.1128%2FAAC.03112-15 + '1-log kill for MAB'; + 'AUC24/MIC'; + 44.6; + 'abscessus' + }; + + target3_TIG = { % https://doi.org/10.3389/fphar.2022.1063453 + 'TB'; + 'AUC24/MIC'; + 42.3; + 'tuberculosis' + }; + + uniqueTargets_TIG = {target1_TIG target2_TIG target3_TIG}; + + + % -------------------- PTA Plotting -------------------- + + PTAMatrix_TIG = zeros(length(uniqueMICs_TIG), length(uniqueTargets_TIG)); + + for i = 1:length(uniqueTargets_TIG) + PTAMatrix_TIG(:, i) = calculatePTA( ... + last24ConcArray, ... + 'MICDist', uniqueMICs_TIG, ... + 'targetType', uniqueTargets_TIG{i}{2}, ... + 'target', uniqueTargets_TIG{i}{3}, ... + 'time_step_size', time_step_size_TIG, ... + 'last24AUC', last24AUC, ... + 'last24Cmax', last24Cmax); + end + + legendArray_TIG = getLegendArray(uniqueTargets_TIG, normalizedMICSpeciesAggregation_TIG); + + [~, plotArgs] = plotPTAs( ... + uniqueMICs_TIG, ... + PTAMatrix_TIG, ... + normalizedMICSpeciesAggregation_TIG, ... + 'DoseSize', dose_size_TIG, ... + 'DoseFrequency', dose_time_TIG, ... + 'DrugName', 'TIG', ... + 'FontSize', opt.GraphFontSize, ... + 'ShowBars', true, ... + 'LegendArray', legendArray_TIG, ... + 'UniqueTargets', uniqueTargets_TIG, ... + 'NumPatients', n_pts, ... + 'ShowCI', false); + + end + +end \ No newline at end of file diff --git a/main.m b/main.m new file mode 100644 index 0000000..c4225a0 --- /dev/null +++ b/main.m @@ -0,0 +1,469 @@ +%% ======================================================================= +% main.m — Population PK Simulation & PTA Analysis for NTM Drug Panel +% ======================================================================== +% +% DESCRIPTION +% Driver script that runs Monte Carlo population-PK (PopPK) simulations +% for a panel of antimycobacterial drugs, computes probability of target +% attainment (PTA), and assembles the main-text and supplementary PTA +% figures. +% +% Workflow: +% (1) Initialize the PTAplotInfo results structure. +% (2) Simulate each drug in turn (one section per drug). +% (3) Assemble and render the main-text PTA figure (plotFullPTAFig). +% (4) Run supplementary dosing scenarios. +% (5) Assemble and render Supplementary Figure 1 (plotSuppPTAFig). +% +% +% AUTHORS +% Trevor Shoaf +% Copyright: Elsje Pienaar's Computational Systems Pharmacology Lab, Weldon School of Biomedical Engineering, Purdue University +% +% VERSION +% Created: +% Last modified: +% Tested on: MATLAB R20XXx +% +% DEPENDENCIES +% - Methods/ ............... shared helper functions (PTA, plotting) +% - / ................ per-drug PopPK model functions +% - Statistics and Machine Learning Toolbox (random sampling / distributions) +% - Parallel Computing Toolbox (required when 'Parallelize' is true) +% +% USAGE +% Run from the repository root so the relative addpath() calls resolve. +% +% ======================================================================== + +% Shared helper functions (PTA computation, figure assembly, etc.) +addpath('Methods/') + + +%% ======================================================================== +% INITIALIZE RESULTS STRUCTURE +% ------------------------------------------------------------------------ +% PTAplotInfo collects the PTA plotting payload returned by each drug's +% PopPK function. Fields are populated as each drug section runs below. +% ======================================================================== +PTAplotInfo = struct; +PTAplotInfo.AMK = []; % Amikacin +PTAplotInfo.BDQ = []; % Bedaquiline +PTAplotInfo.CFZ = []; % Clofazimine +PTAplotInfo.CLR = []; % Clarithromycin +PTAplotInfo.EMB = []; % Ethambutol +PTAplotInfo.FOX = []; % Cefoxitin +PTAplotInfo.IMI = []; % Imipenem +PTAplotInfo.LZD = []; % Linezolid +PTAplotInfo.MXF = []; % Moxifloxacin +PTAplotInfo.RBT = []; % Rifabutin +PTAplotInfo.RIF = []; % Rifampicin +PTAplotInfo.TIG = []; % Tigecycline +PTAplotInfo.OMC = []; % Omadacycline + + +%% ======================================================================== +% AMIKACIN (AMK) — Aminoglycoside +% ------------------------------------------------------------------------ +% PopPK model: Michel Tod et al. "Population pharmacokinetic study of amikacin administered +% once or twice daily to febrile, severly neutropenic adults". In: *Antimicrobial +% agents and chemotherapy* 42.4 (1998), pp. 849-856. +% Route: IV infusion +% Dose: 15 mg/kg (mean weight 52 kg) +% ======================================================================== +addpath('AMK/') + +dose_size_AMK = 15 * 52; % 15 mg/kg × 52 kg mean weight (mg) +num_patients_AMK = 10000; % Number of patients to simulate +num_doses_AMK = 45; % Number of doses to simulate +dose_interval_AMK = 24; % Time between doses (h) +IV_dose_duration_AMK = 1; % Duration of IV infusion (h) + +[PTAplotInfo.AMK] = AMK_PopPK(... + 'NumPatients', num_patients_AMK, ... + 'NumDoses', num_doses_AMK, ... + 'DoseSize', dose_size_AMK, ... + 'DoseInterval', dose_interval_AMK, ... + 'IVDuration', IV_dose_duration_AMK, ... + ... + 'Quiet', false, ... + 'TimeSeriesPlots', true, ... + 'StatisticsPlots', true, ... + ... + 'plotAUC', true, ... + 'PTAplot', true, ... + ... + 'GraphFontSize', 20 ... + ); + + +%% ======================================================================== +% BEDAQUILINE (BDQ) — Diarylquinoline +% ------------------------------------------------------------------------ +% PopPK model: % Michael A. Lyons. "Pharmacodynamics and Bactericidal Activity of +% Bedaquiline in Pulmonary Tuberculosis". In: *Antimicrobial Agents +% and Chemotherapy* 66.2 (2022). DOI: 10.1128/aac.01636-21. +% Route: Oral +% Dose: 400 mg q24h × 14 days, then 200 mg three times weekly (q56h) +% Notes: SwitchDose marks the loading-to-maintenance transition. +% ======================================================================== +addpath('BDQ/') + +numPatients_BDQ = 10000; +numDoses_BDQ = 30; +doseSize_BDQ = [400 200]; +timeBtwnDoses_BDQ = [24 56]; % hours +doseSwitch_BDQ = 14; + +[resultsBDQ, ~, PTAplotInfo.BDQ] = BDQ_PopPK(... + 'NumPatients', numPatients_BDQ, ... + 'NumDoses', numDoses_BDQ, ... + 'DoseSize', doseSize_BDQ, ... + 'DoseInterval', timeBtwnDoses_BDQ, ... + 'SwitchDose', doseSwitch_BDQ, ... + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + ... + 'Parallelize', true ... + ); + + +%% ======================================================================== +% CLOFAZIMINE (CFZ) — Riminophenazine +% ------------------------------------------------------------------------ +% PopPK model: Mahmoud Tareq Abdelwahab et al. "Clofazimine pharmacokinetics in patients +% with TB: dosing implications." In: *Journal of Antimicrobial Chemotherapy* +% 75.11 (Aug. 2020), pp. 3269-3277. ISSN: 0305-7453. DOI: 10.1093/jac/dkaa310. +% Route: Oral +% Dose: 100 mg q24h +% Notes: 60 doses simulated to reach a 2-month (steady-state) AUC. +% ======================================================================== +addpath('CFZ/') + +[PTAplotInfo.CFZ] = CFZ_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 60, ... % 60 doses to get 2-month AUC + 'DoseSize', 100, ... + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'DoseInterval', 24); + + +%% ======================================================================== +% CLARITHROMYCIN (CLR) — Macrolide +% ------------------------------------------------------------------------ +% PopPK model: K. Ikawa et al. "Pharmacokinetic modelling of serum and bronchial +% concentrations for clarithromycin and telithromycin, and site-secific +% pharmacodynamic simulation for their dosages". In: *Journal of Clinical +% Pharmacy and Therapeutics* 39.4 (Mar. 2014), pp. 411-417. +% DOI: 10.1111/jcpt.12157. +% Route: Oral +% Dose: 500 mg q12h +% ======================================================================== +addpath('CLR/') + +[PTAplotInfo.CLR] = CLR_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 30, ... + 'DoseSize', 500, ... + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'DoseInterval', 12); + + +%% ======================================================================== +% ETHAMBUTOL (EMB) — Ethylenediamine +% ------------------------------------------------------------------------ +% PopPK model: Siv Jönsson et al. "Population Pharmacokinetics of Ethambutol in South +% African Tuberculosis Patients." In: *Antimicrobial Agents and Chemotherapy* +% 55.9 (2011), pp. 4230-4237. DOI: 10.1128/aac.00274-11. +% Route: Oral +% Dose: 15 mg/kg q24h (scaled by per-patient weight in EMB_PopPK) +% Notes: Weight scaling handled internally by EMB_PopPK. +% ======================================================================== +addpath('EMB/') + +[PTAplotInfo.EMB] = EMB_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 30, ... + 'DoseSize', 15, ... % mg/kg; scaled by per-patient weight inside EMB_PopPK + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'DoseInterval', 24); + + +%% ======================================================================== +% CEFOXITIN (FOX) — Cephamycin (beta-lactam) +% ------------------------------------------------------------------------ +% PopPK model: Emmanuel Novy et al. "Population pharmacokinetics of prophylactic cefoxitin +% in elective bariatric surgery patients: a prospective monocentric study". +% In: *Anaesthesia Critical Care & Pain Medicine* 43.3 (2024), p. 101376. +% Route: IV infusion +% Dose: 4000 mg q24h +% ======================================================================== +addpath('FOX/') + +[results_FOX, parameters_FOX, PTAplotInfo.FOX] = FOX_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 20, ... + 'DoseSize', 4000, ... % mg + 'DoseInterval', 24, ... % hours + 'TimeSeriesPlots', true, ... + 'StatisticsPlots', true, ... + 'IVDuration', 3); % hours + + +%% ======================================================================== +% IMIPENEM (IMI) — Carbapenem (beta-lactam) +% ------------------------------------------------------------------------ +% PopPK model: Wenqian Chen et al. "Imipenem population pharmacokinetics: therapeutic +% drug monitoring data cellected in critically ill patients with or without +% extracorporeal membrane oxygenation". InL *Antimicrobial agents and +% chemotherapy* 64.5 (2020), pp. 10-1128. +% Route: IV infusion +% Dose: 1000 mg q12h +% ======================================================================== +addpath('IMI/') + +[PTAplotInfo.IMI] = IMI_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 40, ... + 'DoseSize', 1000, ... % mg + 'DoseInterval', 12, ... % hours + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'IVDuration', 3); % hours + + +%% ======================================================================== +% LINEZOLID (LZD) — Oxazolidinone +% ------------------------------------------------------------------------ +% PopPK model: Mahmoud Tareq Abdelwahab et al. "Linezolid population pharmacokinetics +% in South African adults with drug-resistant tuberculosis". +% In: *Antimicrobial agents and chemotherapy* 65.12 (2021), pp. 10-1128. +% Route: Oral +% Dose: 600 mg q24h +% ======================================================================== +addpath('LZD/') + +[PTAplotInfo.LZD] = LZD_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 45, ... + 'DoseSize', 600, ... + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'DoseInterval', 24); + + +%% ======================================================================== +% MOXIFLOXACIN (MXF) — Fluoroquinolone +% ------------------------------------------------------------------------ +% PopPK model: Mohammad H. Al-Shaer et al. "Fluoroquinolones in Drug-Resistant Tuberculosis: +% Culture Conversino and Pharmacokinetic/Pharmacodynamic Target Attainment To +% Guide Dose Selection". In: *Antimicrobial Agents and Chemotherapy* 63.7 +% (2019). DOI: 10.1128/aac.00279-19. +% Route: Oral +% Dose: 400 mg (reference) and 800 mg (NTM treatment) q24h +% ======================================================================== +addpath('MXF/') + +[PTAplotInfo.MXF] = MXF_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 30, ... + 'DoseSize', [400 800], ... % 400 mg in paper; 800 mg in NTM treatment + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'DoseInterval', 24); + + +%% ======================================================================== +% RIFABUTIN (RBT) — Rifamycin +% ------------------------------------------------------------------------ +% PopPK model: Stefanie Hennig et al. "Population pharmacokinetic drug-drug interaction +% pooled analysis of existing data for rifabutin and HIV PIs". In: *Jounral +% of Antimicrobial Chemotherapy* 71.5 (2016), pp. 1330-1340. +% Route: Oral +% Dose: 300 mg q24h +% ======================================================================== +addpath('RBT/') + +[PTAplotInfo.RBT] = RBT_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 30, ... + 'DoseSize', 300, ... + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'DoseInterval', 24); + + +%% ======================================================================== +% RIFAMPICIN (RIF) — Rifamycin +% ------------------------------------------------------------------------ +% PopPK model: Justin J. Wilkins et al. "Population Pharmacokinetics of Rifampin in +% Pulmonary Tuberculosis Patients, Including a Semimechanistic Model To +% Describe Variable Absorption". In: *Antimicrobial Agents and Chemotherapy* +% 52.6 (2008), pp. 2138-2148. DOI: 10.1128/aac.0461-07. +% Route: Oral +% Dose: 600 mg q24h +% ======================================================================== +addpath('RIF/') + +[PTAplotInfo.RIF] = RIF_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 30, ... + 'DoseSize', 600, ... + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'DoseInterval', 24); + + +%% ======================================================================== +% TIGECYCLINE (TIG) — Glycylcycline +% ------------------------------------------------------------------------ +% PopPK model: Agnieszka Borsuk-De Moor et al. "Population pharmacokinetics of high-dose +% tigecycline in patients with sepsis or septic shock". In: *Antimicrobial +% agents and chemotherapy* 62.4 (2018), pp. 10-1128. +% Route: IV infusion +% Dose: 25 mg q12h +% ======================================================================== +addpath('TIG/') + +[PTAplotInfo.TIG] = TIG_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 40, ... + 'DoseSize', 25, ... + 'DoseInterval', 12, ... + 'TimeSeriesPlots', true, ... + 'StatisticsPlots', true, ... + 'IVDuration', 1); + + +%% ======================================================================== +% OMADACYCLINE (OMC) — Aminomethylcycline (tetracycline class) +% ------------------------------------------------------------------------ +% PopPK model: Haijing Yang et al. "Pharmacokinetics, Safety and +% Pharmacokinetics/Pharmacodynamics Analysis of Omadacycline in Chinese Healthy +% Subjects". In: *Frontiers in Pharmacology* Volume 13 (2022). +% ISSN: 1663-9812. DOI: 10.3389/fphar.2022.869237. +% Route: Oral +% Dose: 300 mg q24h +% ======================================================================== +addpath('OMC/') + +[~, ~, PTAplotInfo.OMC] = OMC_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 30, ... + 'DoseInterval', 24, ... + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'GraphFontSize', 12, ... + 'Quiet', false, ... + 'plotAUC', true, ... + 'PTAplot', true, ... + 'Parallelize', false); + + +%% ======================================================================== +% MAIN-TEXT FIGURE — Combined PTA Panel +% ------------------------------------------------------------------------ +% Assemble all per-drug PTA payloads into the main-text figure. Drugs that +% did not return a payload are skipped automatically. +% ======================================================================== +% close all +plotFullPTAFig(PTAplotInfo); + + +%% ======================================================================== +% SUPPLEMENTARY FIGURE 1 — Alternative Dosing Scenarios +% ------------------------------------------------------------------------ +% Dose-escalation / frequency variants for selected drugs (tigecycline, +% imipenem, cefoxitin) to support the supplementary PTA comparison. +% ======================================================================== +PTAplotInfoSupp = struct; +PTAplotInfoSupp.TIG_50mgBID = []; +PTAplotInfoSupp.TIG_100mgBID = []; +PTAplotInfoSupp.IMI_1000mgTID = []; +PTAplotInfoSupp.FOX_2000mgTID = []; +PTAplotInfoSupp.FOX_4000mgTID = []; + + +%% ------------------------------------------------------------------------ +% Tigecycline — 50 mg q12h (BID) +% ------------------------------------------------------------------------ +addpath('TIG/') + +[PTAplotInfoSupp.TIG_50mgBID] = TIG_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 40, ... + 'DoseSize', 50, ... + 'DoseInterval', 12, ... + 'TimeSeriesPlots', true, ... + 'StatisticsPlots', true, ... + 'IVDuration', 1); + + +%% ------------------------------------------------------------------------ +% Tigecycline — 100 mg q12h (BID) +% ------------------------------------------------------------------------ +addpath('TIG/') + +[PTAplotInfoSupp.TIG_100mgBID] = TIG_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 40, ... + 'DoseSize', 100, ... + 'DoseInterval', 12, ... + 'TimeSeriesPlots', true, ... + 'StatisticsPlots', true, ... + 'IVDuration', 1); + + +%% ------------------------------------------------------------------------ +% Imipenem — 1000 mg q8h (TID) +% ------------------------------------------------------------------------ +addpath('IMI/') + +[PTAplotInfoSupp.IMI_1000mgTID] = IMI_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 40, ... + 'DoseSize', 1000, ... % mg + 'DoseInterval', 8, ... % hours + 'TimeSeriesPlots', false, ... + 'StatisticsPlots', true, ... + 'IVDuration', 3); % hours + + +%% ------------------------------------------------------------------------ +% Cefoxitin — 2000 mg q8h (TID) +% ------------------------------------------------------------------------ +addpath('FOX/') + +[~, ~, PTAplotInfoSupp.FOX_2000mgTID] = FOX_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 20, ... + 'DoseSize', 2000, ... % mg; 2 g IV + 'DoseInterval', 8, ... % hours + 'TimeSeriesPlots', true, ... + 'StatisticsPlots', true, ... + 'IVDuration', 3); % hours + + +%% ------------------------------------------------------------------------ +% Cefoxitin — 4000 mg q8h (TID) +% ------------------------------------------------------------------------ +addpath('FOX/') + +[~, ~, PTAplotInfoSupp.FOX_4000mgTID] = FOX_PopPK( ... + 'NumPatients', 10000, ... + 'NumDoses', 20, ... + 'DoseSize', 4000, ... % mg; 4 g IV + 'DoseInterval', 8, ... % hours + 'TimeSeriesPlots', true, ... + 'StatisticsPlots', true, ... + 'IVDuration', 3); % hours + + +%% ------------------------------------------------------------------------ +% Render Supplementary Figure 1 +% ------------------------------------------------------------------------ +% close all; +plotSuppPTAFig(PTAplotInfoSupp); \ No newline at end of file