library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) paper_auc_grid <- data.frame( daily_dose_mg = c(1250, 1500, 1750, 2000, 2250, 2500, 2750, 3000, 3500, 3750, 4000, 4500, 5000), clcr_77 = c(308.1, 375.1, 436.1, 496.6, 560.8, 621.6, 681.7, 740.3, 869.0, 930.7, 996.9, 1118.9, 1239.4), clcr_87 = c(274.0, 328.8, 385.9, 439.7, 493.6, 548.0, 600.4, 659.8, 769.4, 825.8, 877.6, 986.5, 1091.6), clcr_97 = c(248.0, 295.7, 345.5, 394.6, 446.7, 489.9, 541.3, 596.2, 690.6, 739.4, 794.2, 881.9, 984.1), clcr_107 = c(222.6, 267.3, 311.8, 358.1, 400.6, 445.4, 489.6, 536.2, 626.7, 673.9, 712.2, 801.7, 890.1), clcr_117 = c(204.7, 243.7, 287.7, 326.5, 366.1, 408.3, 452.4, 488.4, 571.4, 615.3, 650.8, 735.0, 822.5), clcr_127 = c(187.8, 226.0, 264.4, 299.4, 337.6, 377.4, 414.5, 452.2, 523.9, 561.8, 600.2, 679.3, 753.6), clcr_137 = c(174.7, 210.0, 243.3, 279.6, 314.5, 350.8, 385.3, 420.7, 489.3, 521.6, 559.2, 631.4, 700.5), clcr_147 = c(162.3, 195.9, 228.2, 258.5, 291.3, 324.6, 356.9, 389.1, 452.3, 489.4, 517.8, 584.2, 649.0) ) paper_clcr_values <- c(77, 87, 97, 107, 117, 127, 137, 147) predict_paper_auc <- function(daily_dose_mg, clcr) { dose_idx <- match(daily_dose_mg, paper_auc_grid$daily_dose_mg) if (is.na(dose_idx)) { return(NA_real_) } auc_vals <- as.numeric(paper_auc_grid[dose_idx, -1]) stats::approx( x = paper_clcr_values, y = auc_vals, xout = min(max(clcr, min(paper_clcr_values)), max(paper_clcr_values)), rule = 2 )$y } recommended_daily_dose <- function(clcr, target_auc = 400) { aucs <- vapply( paper_auc_grid$daily_dose_mg, function(dose) predict_paper_auc(dose, clcr), numeric(1) ) keep <- which(aucs >= target_auc) if (length(keep) == 0) { return(NA_real_) } paper_auc_grid$daily_dose_mg[min(keep)] } trap_auc <- function(time, conc) { if (length(time) < 2 || length(conc) < 2) { return(NA_real_) } sum(diff(time) * (head(conc, -1) + tail(conc, -1)) / 2) } build_events <- function(daily_dose_mg, interval_h, infusion_h, n_days) { dose_amt <- daily_dose_mg * interval_h / 24 addl <- max(ceiling(n_days * 24 / interval_h) - 1, 0) ev( amt = dose_amt, cmt = 1, rate = dose_amt / infusion_h, ii = interval_h, addl = addl ) } model_code <- ' $PARAM @annotated CLREF : 6.54 : Clearance at reference Clcr 125 mL/min/1.73 m2 (L/h) VWT : 0.51 : Volume term per kg total body weight (L/kg) WT : 147.9 : Total body weight (kg) CRCL : 125 : Cockcroft-Gault creatinine clearance normalized to 1.73 m2 (mL/min/1.73 m2) $CMT @annotated CENT : Central compartment (mg) $MAIN double V = VWT * WT; double CL = CLREF * (CRCL / 125.0); if (V < 1.0) V = 1.0; if (CL < 0.05) CL = 0.05; $ODE dxdt_CENT = -(CL / V) * CENT; $TABLE double CP = CENT / V; $CAPTURE @annotated CP : Plasma vancomycin concentration (mg/L) CL : Individual clearance (L/h) V : Individual volume of distribution (L) ' mod <- mcode("vancomycin_adane_2015", model_code, quiet = TRUE) app_theme <- bs_theme( version = 5, bootswatch = "flatly", primary = "#8b5cf6" ) |> bs_add_rules(" .metric-card { background: #f8f9fa; border-radius: 8px; padding: 15px; margin: 5px; text-align: center; border: 1px solid #dee2e6; } .metric-value { font-size: 24px; font-weight: bold; color: #2c3e50; } .metric-label { font-size: 12px; color: #7f8c8d; } .metric-success .metric-value { color: #10b981; } .metric-warning .metric-value { color: #f59e0b; } .metric-primary .metric-value { color: #8b5cf6; } .metric-info .metric-value { color: #0dcaf0; } .callout-box { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px 14px; margin-top: 10px; font-size: 13px; } .ref-box { background: #f0f4ff; border-left: 4px solid #8b5cf6; padding: 12px 16px; border-radius: 4px; margin-top: 10px; font-size: 13px; } .ref-box a { color: #8b5cf6; } ") ui <- page_sidebar( title = "Vancomycin in Extreme Obesity PK Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Patient Characteristics"), sliderInput("weight", "Total Body Weight (kg)", min = 110, max = 280, value = 147.9, step = 1), sliderInput("crcl", "Creatinine Clearance (mL/min/1.73 m²)", min = 77, max = 147, value = 125, step = 1), numericInput("mic", "MIC (mg/L)", value = 1, min = 0.25, max = 4, step = 0.25), hr(), h6("Dosing Regimen"), sliderInput("daily_dose", "Total Daily Dose (mg/day)", min = 1250, max = 5000, value = 4000, step = 250), sliderInput("interval", "Dosing Interval (h)", min = 8, max = 24, value = 12, step = 4), sliderInput("infusion_h", "Infusion Duration (h)", min = 1, max = 4, value = 2, step = 0.5), numericInput("n_days", "Simulation Duration (days)", value = 4, min = 2, max = 14), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE), hr(), actionButton("apply_reco", "Apply Paper AUC Recommendation", class = "btn-primary"), uiOutput("paper_recommendation"), div( class = "callout-box", strong("Paper anchor"), tags$br(), "Final model: V = 0.51 x TBW and CL = 6.54 x (Clcr / 125).", tags$br(), "Therapeutic window shown on the plot reflects the 10-20 mg/L trough range discussed in the manuscript." ) ), layout_column_wrap( width = 1 / 4, fill = FALSE, div( class = "metric-card metric-success", div(class = "metric-value", textOutput("cmax_out")), div(class = "metric-label", "Cmax (mg/L)") ), div( class = "metric-card metric-warning", div(class = "metric-value", textOutput("ctrough_out")), div(class = "metric-label", "Ctrough (mg/L)") ), div( class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc24_out")), div(class = "metric-label", "AUC24 (mg·h/L)") ), div( class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf_out")), div(class = "metric-label", "t½ (h)") ) ), navset_card_underline( title = "Vancomycin PopPK Explorer", nav_panel( "Simulation", card( full_screen = TRUE, plotOutput("pk_plot", height = "500px") ) ), nav_panel( "Exposure Summary", card( full_screen = TRUE, card_header("Paper-based exposure check"), tableOutput("dose_table") ), div( class = "callout-box", textOutput("auc_mic_out"), tags$br(), textOutput("dose_note_out") ) ), nav_panel( "Model Information", markdown( paste0( "## Adane et al. 2015 Vancomycin PopPK Simulator\n\n", "**Source:** Adane ED, Herald M, Koura F. *Pharmacokinetics of Vancomycin in Extremely Obese Patients with Suspected or Confirmed Staphylococcus aureus Infections.* Pharmacotherapy. 2015;35:127-139. doi:10.1002/phar.1531.\n\n", "### Model structure\n", "- **Structural model:** one-compartment intravenous infusion model\n", "- **Estimation:** NONMEM 7.3, ADVAN1 TRANS2\n", "- **Population:** 29 analyzed adults with BMI >= 40 kg/m2\n", "- **Sampling:** peak, midpoint, and trough at steady state\n\n", "### Final covariate model (Table 3)\n", "- **V = 0.51 x TBW** (L)\n", "- **CL = 6.54 x (Clcr / 125)** (L/h)\n", "- **Reference patient:** TBW 147.9 kg and Clcr 125 mL/min/1.73 m2\n", "- **Between-subject variability:** 23.9% on V and 26.7% on CL\n\n", "### Published cohort summary\n", "- Median weight: **147.9 kg**\n", "- Median BMI: **49.5 kg/m2**\n", "- Median Clcr: **124.8 mL/min/1.73 m2**\n", "- Median daily dose: **4000 mg/day**\n", "- Median AUC24: **582.9 mg·h/L**\n\n", "### Clinical interpretation\n", "- The manuscript supports dosing in extreme obesity using **total body weight** for V and **Cockcroft-Gault Clcr** for CL.\n", "- Simulations in the paper found that **4000-5000 mg/day** gave >=93% probability of **AUC24/MIC >= 400** for MIC = 1 mg/L.\n", "- Trough monitoring remains important because AUC-only targets can still produce troughs >20 mg/L in some patients.\n" ) ) ), nav_panel( "References", div( class = "ref-box", tags$h5("Key references"), tags$ol( tags$li("Adane ED, Herald M, Koura F. Pharmacotherapy. 2015;35:127-139."), tags$li("Rybak MJ et al. Therapeutic monitoring of vancomycin in adult patients. Am J Health Syst Pharm. 2009;66:82-98."), tags$li("Brown DL, Lalla CD, Masselink AJ. AUC versus peak-trough dosing of vancomycin in the morbidly obese. Ann Pharmacother. 2013;47:1264-1273.") ), tags$h5("Drug context"), tags$ul( tags$li(tags$strong("Drug class:"), " Glycopeptide antibiotic"), tags$li(tags$strong("Route modeled:"), " Intravenous infusion"), tags$li(tags$strong("Primary elimination:"), " Renal"), tags$li(tags$strong("Population:"), " Extremely obese adults with suspected or confirmed S. aureus infection") ) ) ) ), div( style = "text-align: center; padding: 20px; margin-top: 30px; border-top: 1px solid #e9ecef; color: #6c757d; font-size: 12px;", "Powered by ", tags$a( href = "https://www.pkpdbuilder.com", target = "_blank", style = "color: #8b5cf6; font-weight: 500;", "PKPDBuilder.com" ), " • Built by Sunny ☀️ (Husain Attarwala's AI Assistant)", br(), tags$span( style = "font-size: 10px;", "For research and educational purposes only. Not for clinical decision-making." ) ) ) server <- function(input, output, session) { observeEvent(input$apply_reco, { reco <- recommended_daily_dose(input$crcl, target_auc = 400) if (is.finite(reco)) { updateSliderInput(session, "daily_dose", value = reco) } }) output$paper_recommendation <- renderUI({ reco <- recommended_daily_dose(input$crcl, target_auc = 400) per_dose <- input$daily_dose * input$interval / 24 paper_auc <- predict_paper_auc(input$daily_dose, input$crcl) div( class = "callout-box", strong("Paper-based minimum daily dose for AUC24 >= 400"), tags$br(), if (is.finite(reco)) { paste0(sprintf("%.0f", reco), " mg/day at current renal function") } else { "No dose in the published grid reached the selected target." }, tags$br(), paste0("Current regimen: ", sprintf("%.0f", per_dose), " mg every ", input$interval, " h"), tags$br(), paste0("Published table AUC24 estimate for current daily dose: ", sprintf("%.1f", paper_auc), " mg·h/L") ) }) sim_data <- reactive({ shiny::req(input$weight, input$crcl, input$daily_dose, input$interval, input$infusion_h, input$n_days) reg <- build_events( daily_dose_mg = input$daily_dose, interval_h = input$interval, infusion_h = input$infusion_h, n_days = input$n_days ) mod %>% param(WT = input$weight, CRCL = input$crcl) %>% ev(reg) %>% mrgsim(end = input$n_days * 24, delta = 0.1) %>% as.data.frame() |> mutate(time_h = time) }) last_interval <- reactive({ d <- sim_data() end_time <- max(d$time_h) start_time <- max(end_time - input$interval, 0) d |> dplyr::filter(time_h >= start_time, time_h <= end_time) }) last_24h <- reactive({ d <- sim_data() end_time <- max(d$time_h) start_time <- max(end_time - 24, 0) d |> dplyr::filter(time_h >= start_time, time_h <= end_time) }) output$cmax_out <- renderText({ d <- last_interval() sprintf("%.1f", max(d$CP, na.rm = TRUE)) }) output$ctrough_out <- renderText({ d <- last_interval() sprintf("%.1f", d$CP[nrow(d)]) }) output$auc24_out <- renderText({ d <- last_24h() sprintf("%.1f", trap_auc(d$time_h, d$CP)) }) output$thalf_out <- renderText({ d <- sim_data() sprintf("%.1f", log(2) * d$V[1] / d$CL[1]) }) output$pk_plot <- renderPlot({ d <- sim_data() d_plot <- if (input$log_scale) { d |> dplyr::filter(CP > 0) } else { d } p <- ggplot(d_plot, aes(x = time_h, y = CP)) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 10, ymax = 20, fill = "#10b981", alpha = 0.10) + geom_hline(yintercept = c(10, 20), linetype = "dashed", color = "#10b981", alpha = 0.7) + geom_line(color = "#8b5cf6", linewidth = 0.9) + labs( x = "Time (hours)", y = "Vancomycin Concentration (mg/L)", title = paste0( "Vancomycin ", sprintf("%.0f", input$daily_dose * input$interval / 24), " mg every ", input$interval, " h" ), subtitle = "Green band shows the 10-20 mg/L trough range discussed in the manuscript" ) + theme_minimal(base_size = 14) if (input$log_scale) { p <- p + scale_y_log10() } p }) output$dose_table <- renderTable({ dose_choices <- c(2000, 2500, 2750, 3000, 3500, 4000, 4500, 5000) auc_vals <- vapply( dose_choices, function(dose) predict_paper_auc(dose, input$crcl), numeric(1) ) data.frame( `Daily dose (mg/day)` = dose_choices, `Paper AUC24 (mg·h/L)` = round(auc_vals, 1), `AUC24/MIC` = round(auc_vals / input$mic, 1), `Target >= 400?` = ifelse(auc_vals / input$mic >= 400, "Yes", "No"), check.names = FALSE ) }, striped = TRUE, bordered = TRUE, digits = 1) output$auc_mic_out <- renderText({ d <- last_24h() auc24 <- trap_auc(d$time_h, d$CP) paste0( "Simulated steady-state AUC24/MIC at MIC ", sprintf("%.2f", input$mic), " mg/L: ", sprintf("%.1f", auc24 / input$mic) ) }) output$dose_note_out <- renderText({ reco <- recommended_daily_dose(input$crcl, target_auc = 400 * input$mic) if (is.finite(reco)) { paste0( "Published Table 4 suggests a minimum of ", sprintf("%.0f", reco), " mg/day to reach AUC24/MIC >= 400 at the selected MIC." ) } else { "The selected MIC is higher than the published dose grid can reliably support." } }) } shinyApp(ui = ui, server = server)