--- title: "Optimizing a Sparse PK Design in Pediatric Patients" author: "Lucie Fayette, Romain Leroux" classoption: openany output: rmarkdown::html_vignette: toc: true linkcolor: blue urlcolor: green vignette: > %\VignetteIndexEntry{Optimizing a Sparse PK Design in Pediatric Patients} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} %\VignetteAuthor{Lucie Fayette, Romain Leroux} --- ```{r global_options, echo = FALSE, include = FALSE} knitr::opts_knit$set(tangle = FALSE) library(PFIM) set.seed(42) options(width = 200) utils = system.file("vignette-scripts", "pfim-vignette-utils.R", package = "PFIM") if (!nzchar(utils)) stop("pfim-vignette-utils.R not found.", call. = FALSE) source(utils, local = knitr::knit_global()) paths = pfimVignetteSetupPaths() plotOptions = list(unitTime = c("hour"), unitOutcomes = c("mcg/mL")) .pfimVignetteHas = function( name ) { exists( name, inherits = TRUE ) && { val = get( name, inherits = TRUE ) !is.null( val ) && ( !is.character( val ) || any( nzchar( val ) ) ) } } knitr::opts_chunk$set( eval = !nzchar(Sys.getenv("_R_CHECK_PACKAGE_NAME_")), purl = FALSE, collapse = TRUE, comment = "#>", echo = FALSE, warning = FALSE, message = FALSE, cache = FALSE, tidy = FALSE, fig.align = "center", out.width = "100%", dpi = 160, fig.width = 7, fig.height = 4, dev = "png", dev.args = if (isTRUE(capabilities("cairo"))) { list(png = list(type = "cairo", antialias = "default")) } else { list() } ) ``` # Introduction This example is taken from Tim Waterhouse's webinar *A Gentle Introduction to Optimal Design for Pharmacometric Models* ([optimal-design-2023](https://github.com/timwaterhouse/optimal-design-2023)). It shows how PFIM evaluates and optimizes a sparse pharmacokinetic sampling design in pediatric patients. A population pharmacokinetic model for orally administered Mockdrozaline was developed in adults. The objective here is to assess and improve a sparse pediatric design so that population parameters can be estimated with adequate precision. The adult model consists of: - a one-compartment disposition model with first-order absorption, - allometric scaling of clearance and volume with body weight (reference 70 kg), - log-normal inter-individual variability on `Cl`, `V`, and `ka`, - combined proportional and additive residual error. # Population model ## Structural model Clearance and volume enter the oral absorption formula after scaling with `(WT/70)^beta`, where `WT` is body weight in kg and the exponents are fixed at the adult estimates. ```{r model_equations, echo = TRUE} modelEquations = list( RespPK = paste( "dose_RespPK/(V*(WT/70)^(beta_V_WT)) *", "ka/(ka - (Cl*(WT/70)^(beta_Cl_WT))/(V*(WT/70)^(beta_V_WT))) *", "(exp(-(Cl*(WT/70)^(beta_Cl_WT))/(V*(WT/70)^(beta_V_WT)) * t)", "- exp(-ka * t))" ) ) ``` ## Population parameters Typical values `V`, `Cl`, and `ka` refer to a 70 kg adult. Weight exponents `beta_V_WT` and `beta_Cl_WT` are included in the equations but fixed (`fixedMu = TRUE`) because they are not re-estimated in the pediatric study. ```{r model_parameters, echo = TRUE} modelParameters = list( ModelParameter(name = "V", distribution = LogNormal(mu = 100, omega = sqrt(0.1))), ModelParameter(name = "Cl", distribution = LogNormal(mu = 10, omega = sqrt(0.08))), ModelParameter(name = "ka", distribution = LogNormal(mu = 0.25, omega = sqrt(0.2))), ModelParameter( name = "beta_V_WT", distribution = Normal(mu = 1, omega = 0), fixedMu = TRUE, fixedOmega = TRUE ), ModelParameter( name = "beta_Cl_WT", distribution = Normal(mu = 0.75, omega = 0), fixedMu = TRUE, fixedOmega = TRUE ) ) ``` ## Residual error Concentrations are modelled with combined proportional and additive residual error (PopED: `feps.add.prop`). Writing $f$ for the prediction (`RespPK`), both tools use the same variance formula; they differ in what the numbers mean: $$ \mathrm{Var}(y \mid f) = \sigma_{\mathrm{add}}^2 + (\sigma_{\mathrm{prop}}\, f)^2 . $$ | | Proportional term | Additive term | |--|-------------------|---------------| | PopED / NONMEM (`sigma`, `SIGMA`) | variance 0.05 | variance 1 | | PFIM `Combined2` | SD `sigmaSlope = sqrt(0.05)` | SD `sigmaInter = 1` | Tim's tables sometimes write `sigma_prop = 0.05`, which looks like “5% error”. In `optimal-design-2023`, that value is still a variance in `sigma`, not a proportional SD. Copying `0.05` into `sigmaSlope` in PFIM would not reproduce PopED (it would use variance $0.05^2$ instead of $0.05$). Inter-individual variability follows the same variance → SD rule via `omega = sqrt(d)` in the chunk above. Details and a short checklist are in `vignette("Vignette_05_gcse_pkpd", package = "PFIM")`, section *PFIM, PopED, and cross-tool equivalence*. ```{r model_error, echo = TRUE} errorModelRespPK = Combined2( output = "RespPK", sigmaInter = 1, sigmaSlope = sqrt(0.05) ) modelError = list(errorModelRespPK) ``` ## Reference body weight PFIM reads `WT` as a free symbol in the equation (not as a `Covariate()` object). For this homogeneous pediatric cohort, all subjects share the same weight. ```{r weight, echo = TRUE} WT = 32 ``` # Study design Mockdrozaline is administered as 10 mg once daily for 24 weeks (`n = 12` subjects). Doses are expressed in the same units as in the Tim Waterhouse material (`mg × 1000` in the administration record). ```{r administration, echo = TRUE} week_h = 24 * 7 lastDose = week_h * 24 dose_times = seq(0, lastDose, by = 24) administrationRespPK = Administration( outcome = "RespPK", timeDose = dose_times, dose = rep(10 * 1000, times = length(dose_times)) ) ``` The initial sparse design includes five samples: - 5 h after the first dose, - predose at weeks 8, 12, and 24, - 168 h after the last dose. ```{r initial_sampling, echo = TRUE} observation_times = c( 5, week_h * 8 - 1, week_h * c(12, 24), lastDose + 168 ) samplingTimesRespPK = SamplingTimes( outcome = "RespPK", samplings = observation_times ) armInitial = Arm( name = "Initial arm", size = 12, administrations = list(administrationRespPK), samplingTimes = list(samplingTimesRespPK) ) designInitial = Design( name = "designInitial", arms = list(armInitial) ) ``` # Evaluation of the initial design The population Fisher information matrix (FIM) is computed for the initial design. Evaluation and PSO runs are computed by `vignette_04_tim_waterhouse_pediatric_execute.R` (run once, then cached as `.RDS` in `data/`). ```{r evaluation_initial, echo = TRUE, eval = FALSE} evaluationPopFimInitial = Evaluation( name = "FIM design initial", modelParameters = modelParameters, modelEquations = modelEquations, modelError = modelError, designs = list(designInitial), outputs = list("RespPK"), fimType = "population" ) evaluationPopFimInitial = run(evaluationPopFimInitial) ``` ```{r ex04_run, include = FALSE} script = system.file( "vignette-scripts", "vignette_04_tim_waterhouse_pediatric_execute.R", package = "PFIM" ) if (!nzchar(script) || !file.exists(script)) { stop( "vignette_04_tim_waterhouse_pediatric_execute.R not found; ", "reinstall PFIM or rebuild vignettes.", call. = FALSE ) } source(script, local = knitr::knit_global()) ``` ## Predicted concentration-time profile ```{r plot_initial, echo = TRUE, eval = FALSE} plotEvaluation(evaluationPopFimInitial, plotOptions) ``` ```{r ex04_plot_initial, echo = FALSE, fig.width = 7, fig.height = 4, out.width = "100%", eval = .pfimVignetteHas("plotInitialEvaluation")} plotInitialEvaluation ``` ## Design summary ```{r show_initial, echo = TRUE, eval = FALSE} show(evaluationPopFimInitial) ``` ```{r ex04_show_initial, echo = TRUE, eval = .pfimVignetteHas("evaluationPopFimInitial")} show(evaluationPopFimInitial) ``` ## D-criterion ```{r dcrit_initial, echo = TRUE, eval = FALSE} getDcriterion(evaluationPopFimInitial) ``` ```{r ex04_dcrit_initial, echo = TRUE, eval = .pfimVignetteHas("evaluationPopFimInitial")} getDcriterion(evaluationPopFimInitial) ``` Designs can be compared with the D-criterion; relative standard errors (RSE) are often easier to interpret. ## Relative standard errors ```{r rse_initial, echo = TRUE, eval = FALSE} getRSE(evaluationPopFimInitial) ``` ```{r ex04_rse_initial, echo = TRUE, eval = .pfimVignetteHas("evaluationPopFimInitial")} getRSE(evaluationPopFimInitial) ``` # D-optimal sampling-time optimization Sampling times can be optimized within protocol windows. Particle-swarm settings below use a small iteration count so the vignette renders in reasonable time; increase `maxIteration` for production work. ## Five samples One window covers absorption (0–6 h after the first dose), three windows capture steady-state troughs (weeks 8, 12, and 24), and one window covers 96–168 h after the last dose. ```{r constraints_5, echo = TRUE} samplingConstraintsRespPK5 = SamplingTimeConstraints( outcome = "RespPK", initialSamplings = observation_times, samplingsWindows = list( c(0, 6), c(week_h * 8 - 1, week_h * 8), c(week_h * 12 - 1, week_h * 12), c(week_h * 24 - 1, week_h * 24), c(lastDose + 96, lastDose + 168) ), numberOfTimesByWindows = c(1, 1, 1, 1, 1), minSampling = c(1, 1, 1, 1, 1) ) ``` ```{r optimization_5, echo = TRUE, eval = FALSE} armOptim5 = Arm( name = "ArmOptim5", size = 12, administrations = list(administrationRespPK), samplingTimes = list(samplingTimesRespPK), samplingTimesConstraints = list(samplingConstraintsRespPK5) ) designOptim5 = Design( name = "designOptim5", arms = list(armOptim5) ) optimizationPopFIM5 = Optimization( name = "Optimization 5 samples", modelEquations = modelEquations, modelParameters = modelParameters, modelError = modelError, optimizer = "PSOAlgorithm", optimizerParameters = list( maxIteration = 10, populationSize = 50, personalLearningCoefficient = 2.05, globalLearningCoefficient = 2.05, seed = 1234, showProcess = FALSE ), designs = list(designOptim5), fimType = "population", outputs = list("RespPK") ) optimizationPopFIM5 = run(optimizationPopFIM5) ``` ```{r show_optimization_5, echo = TRUE, eval = FALSE} show(optimizationPopFIM5) ``` ```{r ex04_show_optimization_5, echo = TRUE, eval = .pfimVignetteHas("optimizationPopFIM5")} show(optimizationPopFIM5) ``` The optimized five-sample design improves the D-criterion, but precision on the typical parameters remains limited. ## Six samples A sixth occasion is added in the washout phase (72 h and 168 h after the last dose), as in the extended Tim Waterhouse scenario. ```{r constraints_6, echo = TRUE} observation_times_6 = c( 5, week_h * 8 - 1, week_h * c(12, 24), lastDose + 72, lastDose + 168 ) samplingConstraintsRespPK6 = SamplingTimeConstraints( outcome = "RespPK", initialSamplings = observation_times_6, samplingsWindows = list( c(0, 6), c(week_h * 8 - 1, week_h * 8), c(week_h * 12 - 1, week_h * 12), c(week_h * 24 - 1, week_h * 24), c(lastDose, lastDose + 168), c(lastDose + 167.5, lastDose + 168) ), numberOfTimesByWindows = c(1, 1, 1, 1, 1, 1), minSampling = c(1, 1, 1, 1, 1, 1) ) ``` ```{r optimization_6, echo = TRUE, eval = FALSE} armOptim6 = Arm( name = "ArmOptim6", size = 12, administrations = list(administrationRespPK), samplingTimes = list( SamplingTimes(outcome = "RespPK", samplings = observation_times_6) ), samplingTimesConstraints = list(samplingConstraintsRespPK6) ) designOptim6 = Design( name = "designOptim6", arms = list(armOptim6) ) optimizationPopFIM6 = Optimization( name = "Optimization 6 samples", modelEquations = modelEquations, modelParameters = modelParameters, modelError = modelError, optimizer = "PSOAlgorithm", optimizerParameters = list( maxIteration = 10, populationSize = 50, personalLearningCoefficient = 2.05, globalLearningCoefficient = 2.05, seed = 1234, showProcess = FALSE ), designs = list(designOptim6), fimType = "population", outputs = list("RespPK") ) optimizationPopFIM6 = run(optimizationPopFIM6) ``` ```{r show_optimization_6, echo = TRUE, eval = FALSE} show(optimizationPopFIM6) ``` ```{r ex04_show_optimization_6, echo = TRUE, eval = .pfimVignetteHas("optimizationPopFIM6")} show(optimizationPopFIM6) ``` ```{r optimal_times_6, echo = TRUE, eval = FALSE} optArms = prop(optimizationPopFIM6, "optimisationAlgorithmOutputs")$optimalArms optSamplings = prop(prop(optArms[[1]], "samplingTimes")[[1]], "samplings") optSamplings ``` ```{r ex04_optimal_times_6, echo = TRUE, eval = .pfimVignetteHas("optSamplings")} optSamplings ``` With these settings, the typical values `V`, `Cl`, and `ka` are expected to reach RSE below 20% after optimization (variance components are usually less precisely estimated). # Discussion Pediatric studies allow only a few samples per subject. Starting from a feasible sparse design, PFIM supports identifiability checks, FIM-based precision, D-optimal timing under windows, and comparison of alternative schedules. This scenario follows Tim Waterhouse's PopED workshop material (`optimal-design-2023`), re-expressed with PFIM objects. For object mapping, variance versus standard deviation, and cross-checks with PopED, see `vignette("Vignette_05_gcse_pkpd", package = "PFIM")`, section *PFIM, PopED, and cross-tool equivalence*.