suppressPackageStartupMessages({ library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) }) paper_reference <- list( title = "Population pharmacokinetics of intravenous linezolid in critically ill pediatric patients", citation = paste( "SANOUFI MR, GUENDOUL K, OUALHA M, ABDALLA S, BERANGER A,", "LOUIS LP, FROELICHER-BOURNAUD L, ROUILLON S, TRELUYER JM,", "FOISSAC F, BOUAZZA N, BENABOUD S. Manuscript PDF uploaded to PKPDBuilder, 2026." ) ) study_constants <- list( tvcl_l_h = 1.76, tvv_l = 5.3, weight_ref_kg = 8, egfr_ref = 111.13, egfr_power = 0.33, transplant_multiplier = 0.51, f_birth = 0.66, tm50_years = 0.44, trough_target_low = 2, trough_target_high = 7, auc_mic_target = 80 ) trap_auc <- function(time, conc) { if (length(time) < 2) { return(NA_real_) } sum(diff(time) * (head(conc, -1) + tail(conc, -1)) / 2) } calc_maturation_factor <- function(age_years) { with( study_constants, f_birth + (1 - f_birth) * (1 - exp((-0.693 * age_years) / tm50_years)) ) } calc_cl_l_h <- function(weight_kg, egfr, age_years, transplant) { with( study_constants, tvcl_l_h * (weight_kg / weight_ref_kg) ^ 0.75 * (egfr / egfr_ref) ^ egfr_power * transplant_multiplier ^ as.numeric(transplant) * calc_maturation_factor(age_years) ) } calc_vd_l <- function(weight_kg) { with(study_constants, tvv_l * (weight_kg / weight_ref_kg)) } calc_half_life_h <- function(weight_kg, egfr, age_years, transplant) { vd_l <- calc_vd_l(weight_kg) cl_l_h <- calc_cl_l_h(weight_kg, egfr, age_years, transplant) log(2) * vd_l / cl_l_h } recommended_regimen <- function(weight_kg) { if (weight_kg <= 40) { list( dose_mg = 10 * weight_kg, interval_h = 8, label = "Published regimen: 10 mg/kg every 8 h" ) } else { list( dose_mg = 600, interval_h = 12, label = "Published regimen: 600 mg every 12 h" ) } } model_code <- " $PARAM @annotated TVCL : 1.76 : Typical clearance at reference covariates (L/h) TVV : 5.3 : Typical volume of distribution at reference weight (L) WT : 8 : Body weight (kg) EGFR : 111.13 : Estimated glomerular filtration rate (mL/min/1.73 m2) AGE : 0.9 : Postnatal age (years) TRANSPLANT : 0 : Liver transplant indicator (0/1) WTREF : 8 : Reference body weight (kg) EGFRREF : 111.13 : Reference eGFR (mL/min/1.73 m2) BEGFR : 0.33 : eGFR effect on clearance TRMULT : 0.51 : Transplant multiplier on clearance FBIRTH : 0.66 : Fraction of mature clearance at birth TM50 : 0.44 : Postnatal age at half maturation (years) $CMT @annotated CENT : Central compartment (mg) $MAIN double CLi = TVCL * pow(WT / WTREF, 0.75) * pow(EGFR / EGFRREF, BEGFR) * pow(TRMULT, TRANSPLANT) * (FBIRTH + (1.0 - FBIRTH) * (1.0 - exp((-0.693 * AGE) / TM50))); double VDi = TVV * (WT / WTREF); $ODE dxdt_CENT = -(CLi / VDi) * CENT; $TABLE double CP = CENT / VDi; $CAPTURE CP CLi VDi " mod <- mcode("linezolid_pediatric_critically_ill", model_code) simulate_profile <- function( weight_kg, egfr, age_years, transplant, dose_mg, interval_h, infusion_h, duration_days, delta = 0.1 ) { total_hours <- duration_days * 24 n_doses <- max(1, floor(total_hours / interval_h)) dose_event <- ev( amt = dose_mg, ii = interval_h, addl = max(n_doses - 1, 0), rate = dose_mg / infusion_h, cmt = 1 ) mod |> param( WT = weight_kg, EGFR = egfr, AGE = age_years, TRANSPLANT = as.numeric(transplant) ) %>% ev(dose_event) %>% mrgsim(end = total_hours, delta = delta) %>% as.data.frame() |> mutate(time_h = time) } metric_frame <- function(sim_df, interval_h, mic) { last_time <- max(sim_df$time_h, na.rm = TRUE) last_interval_start <- max(last_time - interval_h, 0) last_day_start <- max(last_time - 24, 0) interval_df <- sim_df |> dplyr::filter(.data$time_h >= last_interval_start) day_df <- sim_df |> dplyr::filter(.data$time_h >= last_day_start) auc24 <- trap_auc(day_df$time_h, day_df$CP) cmax <- max(interval_df$CP, na.rm = TRUE) ctrough <- min(interval_df$CP, na.rm = TRUE) data.frame( cmax = cmax, ctrough = ctrough, auc24 = auc24, auc_mic = auc24 / mic ) } metric_card <- function(value_output, label, card_class = "metric-primary") { div( class = paste("metric-card", card_class), div(class = "metric-value", textOutput(value_output)), div(class = "metric-label", label) ) } app_theme <- bs_theme( version = 5, bootswatch = "flatly", primary = "#8b5cf6" ) |> bs_add_rules(" .metric-card { background: #f8f9fa; border-radius: 10px; padding: 16px; margin: 6px; text-align: center; border: 1px solid #dee2e6; box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05); } .metric-value { font-size: 24px; font-weight: 700; color: #1f2937; } .metric-label { font-size: 12px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.04em; } .metric-success .metric-value { color: #059669; } .metric-warning .metric-value { color: #d97706; } .metric-primary .metric-value { color: #8b5cf6; } .metric-info .metric-value { color: #0284c7; } .info-box { background: #f8fafc; border: 1px solid #e2e8f0; border-left: 4px solid #8b5cf6; border-radius: 10px; padding: 16px 18px; margin-top: 14px; } .info-box h5 { margin-top: 0; margin-bottom: 10px; color: #111827; } .status-chip { display: inline-block; padding: 6px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; margin-right: 8px; margin-bottom: 8px; } .status-pass { background: rgba(16, 185, 129, 0.12); color: #047857; } .status-warn { background: rgba(245, 158, 11, 0.15); color: #b45309; } .status-fail { background: rgba(239, 68, 68, 0.12); color: #b91c1c; } .ref-box { background: #f0f4ff; border-left: 4px solid #8b5cf6; padding: 14px 18px; border-radius: 6px; margin-top: 12px; font-size: 13px; } .ref-box a { color: #8b5cf6; } ") ui <- page_sidebar( title = "Linezolid PopPK Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Patient Characteristics"), sliderInput("weight_kg", "Weight (kg)", min = 0.8, max = 60, value = 8, step = 0.1), sliderInput("age_years", "Postnatal Age (years)", min = 0.03, max = 16, value = 0.9, step = 0.01), sliderInput("egfr", "eGFR (mL/min/1.73 m²)", min = 15, max = 220, value = 111, step = 1), checkboxInput("transplant", "Liver transplant recipient", value = FALSE), hr(), h6("Dosing"), radioButtons( "dose_mode", "Regimen", choices = c( "Published standard regimen" = "published", "Custom regimen" = "custom" ), selected = "published" ), conditionalPanel( condition = "input.dose_mode === 'custom'", sliderInput("dose_mg", "Dose (mg)", min = 25, max = 1200, value = 80, step = 5), sliderInput("interval_h", "Interval (hours)", min = 6, max = 24, value = 8, step = 1) ), sliderInput("infusion_h", "Infusion duration (hours)", min = 0.5, max = 2, value = 1, step = 0.25), numericInput("duration_days", "Simulation duration (days)", value = 5, min = 2, max = 10), selectInput("mic", "MIC (mg/L)", choices = c(0.5, 1, 2, 4, 8, 16), selected = 1), checkboxInput("log_scale", "Log scale concentration axis", value = FALSE) ), navset_card_underline( title = "Intravenous Linezolid in Critically Ill Pediatric Patients", nav_panel( "Simulation", layout_column_wrap( width = 1 / 4, fill = FALSE, metric_card("cmax", "Cmax at steady state (mg/L)", "metric-success"), metric_card("ctrough", "Ctrough at steady state (mg/L)", "metric-warning"), metric_card("auc24", "AUC0-24 at steady state (mg·h/L)", "metric-primary"), metric_card("thalf", "Half-life (hours)", "metric-info") ), card( full_screen = TRUE, card_header(textOutput("regimen_label")), plotOutput("pk_plot", height = "500px") ), card( card_header("Target Evaluation"), uiOutput("target_status"), div(class = "info-box", textOutput("clinical_note")) ) ), nav_panel( "Model Information", div( class = "info-box", tags$h5("Model structure"), tags$p("One-compartment IV infusion model with first-order elimination."), tags$ul( tags$li("Clearance: 1.76 × (WT/8)^0.75 × (eGFR/111.13)^0.33 × 0.51^transplant × maturation"), tags$li("Maturation: 0.66 + (1 - 0.66) × (1 - exp(-0.693 × age / 0.44))"), tags$li("Volume: 5.3 × (WT/8) L"), tags$li("Population: 47 critically ill pediatric patients, 109 concentrations") ) ), div( class = "info-box", tags$h5("Study interpretation"), tags$ul( tags$li("Published trough target: 2-7 mg/L."), tags$li("Published efficacy target: AUC0-24/MIC ≥ 80."), tags$li("Standard dosing underexposed many non-transplant children, especially at high eGFR and low weight."), tags$li("Liver transplant status reduced clearance by about 49%, shifting some patients toward overexposure.") ) ) ), nav_panel( "References", div( class = "ref-box", tags$h5("Source manuscript"), tags$ol( tags$li(paper_reference$citation) ), tags$h5("Targets used in the manuscript"), tags$ul( tags$li("Trough target: 2-7 mg/L for balancing efficacy and thrombocytopenia risk."), tags$li("AUC0-24/MIC target: ≥ 80 for antimicrobial efficacy."), tags$li("Published regimens evaluated: 10 mg/kg q8h at ≤ 40 kg and 600 mg q12h at > 40 kg.") ) ) ) ), div( style = paste( "text-align:center; padding:20px; margin-top:24px;", "border-top:1px solid #e5e7eb; color:#6b7280; font-size:12px;" ), "Powered by ", tags$a( href = "https://www.pkpdbuilder.com", target = "_blank", style = "color:#8b5cf6; font-weight:600;", "PKPDBuilder.com" ), " • Built by Sunny for Husain Attarwala", br(), tags$span( style = "font-size:10px;", "For research and educational use only. Not for direct clinical dosing decisions." ) ) ) server <- function(input, output, session) { regimen <- reactive({ if (identical(input$dose_mode, "published")) { recommended_regimen(input$weight_kg) } else { list( dose_mg = input$dose_mg, interval_h = input$interval_h, label = paste0("Custom regimen: ", input$dose_mg, " mg every ", input$interval_h, " h") ) } }) sim_data <- reactive({ shiny::req(input$weight_kg, input$age_years, input$egfr, input$duration_days, input$mic) simulate_profile( weight_kg = input$weight_kg, egfr = input$egfr, age_years = input$age_years, transplant = input$transplant, dose_mg = regimen()$dose_mg, interval_h = regimen()$interval_h, infusion_h = input$infusion_h, duration_days = input$duration_days ) }) metrics <- reactive({ metric_frame(sim_data(), interval_h = regimen()$interval_h, mic = as.numeric(input$mic)) }) output$regimen_label <- renderText({ paste0( regimen()$label, " • Infusion ", format(input$infusion_h, trim = TRUE), " h" ) }) output$cmax <- renderText({ sprintf("%.1f", metrics()$cmax[[1]]) }) output$ctrough <- renderText({ sprintf("%.1f", metrics()$ctrough[[1]]) }) output$auc24 <- renderText({ sprintf("%.1f", metrics()$auc24[[1]]) }) output$thalf <- renderText({ sprintf( "%.1f", calc_half_life_h( weight_kg = input$weight_kg, egfr = input$egfr, age_years = input$age_years, transplant = input$transplant ) ) }) output$target_status <- renderUI({ current_metrics <- metrics() current_trough <- current_metrics$ctrough[[1]] current_auc_mic <- current_metrics$auc_mic[[1]] trough_class <- if (current_trough < study_constants$trough_target_low) { "status-fail" } else if (current_trough > study_constants$trough_target_high) { "status-warn" } else { "status-pass" } auc_class <- if (current_auc_mic >= study_constants$auc_mic_target) { "status-pass" } else { "status-fail" } tagList( div( class = paste("status-chip", trough_class), paste0( "Ctrough ", sprintf("%.1f", current_trough), " mg/L" ) ), div( class = paste("status-chip", auc_class), paste0( "AUC0-24/MIC ", sprintf("%.1f", current_auc_mic), " at MIC ", input$mic, " mg/L" ) ) ) }) output$clinical_note <- renderText({ current_auc_mic <- metrics()$auc_mic[[1]] current_trough <- metrics()$ctrough[[1]] if (current_trough < study_constants$trough_target_low && current_auc_mic < study_constants$auc_mic_target) { return("Predicted steady-state trough and AUC/MIC are both below manuscript targets, consistent with likely underexposure.") } if (current_trough > study_constants$trough_target_high) { return("Predicted steady-state trough exceeds the manuscript target range, suggesting higher overexposure risk.") } if (current_auc_mic < study_constants$auc_mic_target) { return("Predicted trough is acceptable, but AUC/MIC remains below the efficacy target for the selected MIC.") } "Predicted steady-state exposure satisfies both manuscript targets for the selected MIC and covariate profile." }) output$pk_plot <- renderPlot({ cutoff_h <- max(sim_data()$time_h) - 48 plot_df <- sim_data() |> dplyr::filter(.data$time_h >= cutoff_h) if (isTRUE(input$log_scale)) { plot_df <- plot_df |> dplyr::filter(.data$CP > 0) } p <- ggplot(plot_df, aes(x = .data$time_h, y = .data$CP)) + annotate( "rect", xmin = -Inf, xmax = Inf, ymin = study_constants$trough_target_low, ymax = study_constants$trough_target_high, fill = "#10b981", alpha = 0.12 ) + geom_hline( yintercept = c(study_constants$trough_target_low, study_constants$trough_target_high), linetype = "dashed", color = "#10b981", alpha = 0.8 ) + geom_line(color = "#8b5cf6", linewidth = 1) + labs( x = "Time (hours)", y = "Linezolid concentration (mg/L)", title = "Predicted concentration-time profile", subtitle = "Last 48 hours of repeated dosing" ) + theme_minimal(base_size = 14) + theme( plot.title = element_text(face = "bold"), panel.grid.minor = element_blank() ) if (isTRUE(input$log_scale)) { p <- p + scale_y_log10() } p }) } app <- shinyApp(ui = ui, server = server) app