library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # --------------------------------------------------------------------------- # mrgsolve 2-CMT Model — Valganciclovir / Ganciclovir # Vezina et al. (2014) Br J Clin Pharmacol 78(2):343-352 # Parameters: 2-CMT oral with lag time, allometric scaling, CrCL covariate # --------------------------------------------------------------------------- model_code <- ' $PARAM @annotated CLF : 14.5 : Apparent oral clearance (L/h) at WT=70kg CrCL=60mL/min V2F : 87.5 : Apparent central volume of distribution (L) at WT=70kg QF : 4.80 : Apparent intercompartmental clearance (L/h) at WT=70kg V3F : 42.6 : Apparent peripheral volume of distribution (L) at WT=70kg KA : 3.0 : Absorption rate constant (1/h) [fixed] TLAG : 0.5 : Absorption lag time (h) [fixed] WT : 70 : Body weight (kg) CRCL : 60 : Creatinine clearance (mL/min) CRCL_EXP : 0.492 : CrCL power exponent on CL/F $CMT @annotated DEPOT : Oral absorption depot (mg) CENT : Central compartment (mg) PERIPH : Peripheral compartment (mg) $MAIN double CLFi = CLF * pow(CRCL / 60.0, CRCL_EXP) * pow(WT / 70.0, 0.75); double V2Fi = V2F * (WT / 70.0); double QFi = QF * pow(WT / 70.0, 0.75); double V3Fi = V3F * (WT / 70.0); ALAG_DEPOT = TLAG; $ODE dxdt_DEPOT = -KA * DEPOT; dxdt_CENT = KA * DEPOT - (CLFi / V2Fi) * CENT - (QFi / V2Fi) * CENT + (QFi / V3Fi) * PERIPH; dxdt_PERIPH = (QFi / V2Fi) * CENT - (QFi / V3Fi) * PERIPH; $TABLE double CP = CENT / V2Fi; $CAPTURE CP CLFi V2Fi ' mod <- mcode("valganciclovir_popPK", model_code, quiet = TRUE) # --------------------------------------------------------------------------- # Theme # --------------------------------------------------------------------------- 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; margin-top:4px; } .metric-success .metric-value { color:#10b981; } .metric-warning .metric-value { color:#f59e0b; } .metric-primary .metric-value { color:#8b5cf6; } .metric-info .metric-value { color:#0dcaf0; } .auc-target-ok { color:#10b981 !important; } .auc-target-low { color:#f59e0b !important; } .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 # --------------------------------------------------------------------------- ui <- page_sidebar( title = "Valganciclovir / Ganciclovir PK Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Dosing"), sliderInput("dose", "Valganciclovir Dose (mg)", min = 75, max = 900, value = 900, step = 75), selectInput("interval", "Dosing Interval", choices = c("Every 12 hours" = 12, "Every 24 hours" = 24, "Every 48 hours" = 48), selected = 24), numericInput("n_days", "Duration (days)", value = 7, min = 1, max = 14), hr(), h6("Patient Characteristics"), sliderInput("wt", "Body Weight (kg)", min = 10, max = 150, value = 70, step = 5), sliderInput("crcl", "Creatinine Clearance (mL/min)", min = 10, max = 150, value = 60, step = 5), hr(), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE), checkboxInput("show_ic50", "Show CMV/EBV IC\u2085\u2080 lines", value = TRUE) ), 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 (μg/mL)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("ctrough_out")), div(class = "metric-label", "Ctrough (μg/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc_out")), div(class = "metric-label", "AUC\u2080\u208b\u03c4 (μg·h/mL)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf_out")), div(class = "metric-label", "t\u00bd (h)")) ), navset_card_underline( full_screen = TRUE, nav_panel("PK Profile", plotOutput("pkPlot", height = "500px") ), nav_panel("Model Information", markdown(" ## Ganciclovir Population PK — 2-Compartment Oral Model **Source:** Vezina HE, Brundage RC, Balfour HH Jr. (2014) *Br J Clin Pharmacol* 78(2):343–352. doi: 10.1111/bcp.12343 --- ### Model Structure A **two-compartment model with first-order absorption and lag time** best described ganciclovir plasma concentrations following oral valganciclovir in 95 kidney, liver, and lung transplant recipients (100 adults, 15 children). | Parameter | Estimate | 95% CI | Bootstrap | |-----------|----------|--------|-----------| | CL/F (L/h) at 70 kg | 14.5 | 10.9–18.1 | 14.7 (11.9–18.0) | | V₂/F (L) at 70 kg | 87.5 | 21.3–154 | 84.3 (53.1–139) | | Q/F (L/h) at 70 kg | 4.80 | 0.41–9.19 | 4.64 (1.88–21.3) | | V₃/F (L) at 70 kg | 42.6 | 11.2–74.0 | 47.1 (24.9–177) | | Ka (h⁻¹) | 3.0 **fixed** | — | — | | Lag time (h) | 0.5 **fixed** | — | — | | IIV CL/F (CV%) | 33.5 | — | 33.2 | | Residual (CV%) | 32.7 | — | 31.9 | ### Covariate Model $$CL/F = 14.5 \\times \\left(\\frac{CrCL}{60}\\right)^{0.492} \\times \\left(\\frac{WT}{70}\\right)^{0.75}$$ $$V_2/F, V_3/F = \\theta \\times \\left(\\frac{WT}{70}\\right)^{1.0}$$ $$Q/F = 4.80 \\times \\left(\\frac{WT}{70}\\right)^{0.75}$$ - **CrCL exponent:** 0.492 (95% CI: 0.340–0.644) — reflecting renal elimination (GFR + tubular secretion) - **Standard allometric exponents:** 0.75 on clearance terms, 1.0 on volume terms - Reference: WT = 70 kg, CrCL = 60 mL/min ### Study Population - n = 95 (82 adults + 13 children), solid organ transplant recipients - Transplant types: kidney, liver, lung - CrCL range: pediatric to adult - Doses: 75–900 mg (tablets and oral solution) ### Therapeutic Targets (CMV Prophylaxis) - **AUC target (900 mg q24h):** ~57.4 μg·h/mL (median observed) - **Proposed efficacy threshold:** AUC ≥ 40–50 μg·h/mL for CMV suppression - **CMV IC₅₀:** ~0.65 μM ≈ 0.17 μg/mL - **EBV IC₅₀:** ~0.05 μM ≈ 0.013 μg/mL - **Max daily dose (adult):** 900 mg (dose-adjusted for renal function in children by BSA×CrCL algorithm) ")), nav_panel("References", div(class = "ref-box", tags$h5("📚 Primary Reference"), tags$p( tags$strong("Vezina HE, Brundage RC, Balfour HH Jr."), " Population pharmacokinetics of valganciclovir prophylaxis in paediatric and adult ", "solid organ transplant recipients. ", tags$em("Br J Clin Pharmacol."), " 2014;78(2):343-352.", tags$br(), tags$a(href = "https://doi.org/10.1111/bcp.12343", target = "_blank", "doi: 10.1111/bcp.12343"), " | PMC: ", tags$a(href = "https://pmc.ncbi.nlm.nih.gov/articles/PMC4137826/", target = "_blank", "PMC4137826") ), tags$hr(), tags$h5("💊 Drug Information"), tags$ul( tags$li(tags$strong("Drug:"), " Valganciclovir (oral prodrug → Ganciclovir)"), tags$li(tags$strong("Class:"), " Nucleoside analogue antiviral (DNA polymerase inhibitor)"), tags$li(tags$strong("Indication:"), " CMV & EBV prophylaxis in solid organ transplant recipients"), tags$li(tags$strong("Bioavailability:"), " ~60% (10× higher than oral ganciclovir capsules)"), tags$li(tags$strong("Elimination:"), " Renal (GFR + tubular secretion); dose-adjust for CrCL"), tags$li(tags$strong("Brand:"), " Valcyte® (Roche/Genentech)") ), tags$hr(), tags$h5("📖 Related References"), tags$ol( tags$li("Asberg A et al. (2009) Am J Transplant 9:1519–1525 — Ganciclovir AUC and CMV outcomes"), tags$li("Perrottet N et al. (2009) Clin Pharmacokinet 48(1):39–49 — Adult transplant PopPK"), tags$li("Kimberlin DW et al. (2008) J Infect Dis 197:836–845 — Neonatal ganciclovir PK") ) ) ) ), 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"), " • Vezina et al. (2014) Br J Clin Pharmacol", tags$br(), tags$span(style = "font-size:10px;", "For research and educational purposes only. Not for clinical decision-making.") ) ) # --------------------------------------------------------------------------- # Server # --------------------------------------------------------------------------- server <- function(input, output, session) { interval_h <- reactive({ as.numeric(input$interval) }) sim_data <- reactive({ shiny::req(input$dose, input$wt, input$crcl, input$n_days) tau <- interval_h() n_doses <- ceiling(input$n_days * 24 / tau) ev1 <- ev(amt = input$dose, cmt = 1, ii = tau, addl = n_doses - 1) mod |> param(WT = input$wt, CRCL = input$crcl) |> ev(ev1) |> mrgsim(end = input$n_days * 24, delta = 0.1) |> as.data.frame() }) pk_params <- reactive({ shiny::req(input$wt, input$crcl) CLFi <- 14.5 * (input$crcl / 60)^0.492 * (input$wt / 70)^0.75 V2Fi <- 87.5 * (input$wt / 70) QFi <- 4.80 * (input$wt / 70)^0.75 V3Fi <- 42.6 * (input$wt / 70) list(CLF = CLFi, V2F = V2Fi, QF = QFi, V3F = V3Fi) }) last_dose_window <- reactive({ d <- sim_data() tau <- interval_h() n_days <- input$n_days t_last <- floor(n_days * 24 / tau) * tau dplyr::filter(d, time >= t_last & time <= t_last + tau) }) output$cmax_out <- renderText({ d <- last_dose_window() shiny::req(nrow(d) > 0) sprintf("%.2f", max(d$CP, na.rm = TRUE)) }) output$ctrough_out <- renderText({ d <- last_dose_window() shiny::req(nrow(d) > 0) vals <- dplyr::filter(d, time == max(time)) sprintf("%.2f", min(vals$CP, na.rm = TRUE)) }) output$auc_out <- renderText({ d <- last_dose_window() shiny::req(nrow(d) > 1) auc <- sum(diff(d$time) * (head(d$CP, -1) + tail(d$CP, -1)) / 2) sprintf("%.1f", auc) }) output$thalf_out <- renderText({ p <- pk_params() # Terminal half-life for 2-CMT (approximate from macro rate constants) A <- p$CLF / p$V2F B <- p$QF / p$V2F C <- p$QF / p$V3F sum_BC <- A + B + C diff_BC <- sqrt(sum_BC^2 - 4 * A * C) lambda_z <- (sum_BC - diff_BC) / 2 # terminal rate constant sprintf("%.1f", log(2) / lambda_z) }) output$pkPlot <- renderPlot({ d <- sim_data() tau <- interval_h() auc_target_lo <- 40 # compute last-interval AUC for annotation d_last <- last_dose_window() auc_val <- if (nrow(d_last) > 1) sum(diff(d_last$time) * (head(d_last$CP, -1) + tail(d_last$CP, -1)) / 2) else NA_real_ auc_label <- if (!is.na(auc_val)) { status <- if (auc_val >= auc_target_lo) "✓ On target" else "⚠ Below target" sprintf("AUC₀₋τ = %.1f μg·h/mL\n(%s, target ≥40)", auc_val, status) } else "" cmv_ic50 <- 0.17 # μg/mL (~0.65 μM) ebv_ic50 <- 0.013 # μg/mL (~0.05 μM) p <- ggplot(d, aes(x = time, y = CP)) + geom_line(color = "#8b5cf6", linewidth = 0.9) if (input$show_ic50) { p <- p + geom_hline(yintercept = cmv_ic50, linetype = "dashed", color = "#ef4444", linewidth = 0.6, alpha = 0.8) + geom_hline(yintercept = ebv_ic50, linetype = "dotted", color = "#f97316", linewidth = 0.6, alpha = 0.8) + annotate("text", x = max(d$time) * 0.75, y = cmv_ic50 * 1.3, label = paste0("CMV IC\u2085\u2080 = ", cmv_ic50, " μg/mL"), color = "#ef4444", size = 3.5, hjust = 0) + annotate("text", x = max(d$time) * 0.75, y = ebv_ic50 * 1.5, label = paste0("EBV IC\u2085\u2080 = ", ebv_ic50, " μg/mL"), color = "#f97316", size = 3.5, hjust = 0) } if (!is.na(auc_val) && nchar(auc_label) > 0) { p <- p + annotate("text", x = max(d$time) * 0.02, y = max(d$CP, na.rm = TRUE) * 0.95, label = auc_label, hjust = 0, vjust = 1, size = 3.8, color = if (auc_val >= auc_target_lo) "#10b981" else "#f59e0b") } p + labs( title = sprintf( "Ganciclovir PK — Valganciclovir %g mg Q%gh | WT=%g kg | CrCL=%g mL/min", input$dose, tau, input$wt, input$crcl ), x = "Time (hours)", y = "Ganciclovir Concentration (μg/mL)", caption = "Vezina et al. (2014) Br J Clin Pharmacol 78(2):343-352 | 2-CMT oral model" ) + theme_minimal(base_size = 13) + theme( plot.title = element_text(face = "bold", size = 12), panel.grid.minor = element_blank() ) + scale_y_continuous(trans = if (input$log_scale) "log10" else "identity") }) } shinyApp(ui = ui, server = server)