library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ── mrgsolve 2-CMT model ────────────────────────────────────────────────────── # Source: Caldés et al. (2009). Antimicrob Agents Chemother 53(11):4816-4824 # Population: SOT patients with CMV (N=20), NONMEM VI (FOCE-I) model_code <- ' $PARAM @annotated CLCR : 57.0 : Creatinine clearance (mL/min) CLSCALE : 7.49 : Population CL at mean CLCR of 57 mL/min (L/h) V1 : 31.9 : Central volume of distribution (L) Q : 10.2 : Intercompartmental clearance (L/h) V2 : 32.0 : Peripheral volume of distribution (L) KA : 0.895 : First-order absorption rate constant (1/h) F1 : 0.825 : Valganciclovir oral bioavailability (fraction) TLAG : 0.382 : Absorption lag time (h) $CMT @annotated DEPOT : Oral depot - valganciclovir absorption compartment (mg) CENT : Central compartment (mg) PERIPH : Peripheral compartment (mg) $MAIN double CLi = CLSCALE * (CLCR / 57.0); F_DEPOT = F1; ALAG_DEPOT = TLAG; $ODE dxdt_DEPOT = -KA * DEPOT; dxdt_CENT = KA * DEPOT - (CLi / V1) * CENT - (Q / V1) * CENT + (Q / V2) * PERIPH; dxdt_PERIPH = (Q / V1) * CENT - (Q / V2) * PERIPH; $TABLE double CP = CENT / V1; $CAPTURE CP CLi ' mod <- mcode("ganciclovir_2cmt", model_code) # ── 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; } .metric-success .metric-value { color: #10b981; } .metric-warning .metric-value { color: #f59e0b; } .metric-primary .metric-value { color: #8b5cf6; } .metric-info .metric-value { color: #0dcaf0; } .rec-box { background: #e8f4fd; border-left: 4px solid #3498db; padding: 10px 14px; border-radius: 4px; font-size: 12px; } .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 = "Ganciclovir / Valganciclovir PK Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 350, h6("Route of Administration"), radioButtons("route", NULL, choices = c("IV Ganciclovir" = "iv", "Oral Valganciclovir" = "oral"), selected = "iv", inline = TRUE), hr(), h6("Patient Characteristics"), sliderInput("clcr", "Creatinine Clearance (mL/min)", min = 10, max = 120, value = 57, step = 1), sliderInput("wt", "Body Weight (kg)", min = 40, max = 120, value = 66, step = 1), hr(), h6("Dosing"), conditionalPanel("input.route === 'iv'", sliderInput("dose_iv", "IV Dose (mg/kg)", min = 1.25, max = 7.5, value = 5.0, step = 1.25) ), conditionalPanel("input.route === 'oral'", sliderInput("dose_oral", "Valganciclovir Dose (mg)", min = 225, max = 900, value = 900, step = 225) ), selectInput("interval", "Dosing Interval", choices = c("Every 12h (BID)" = "12", "Every 24h (QD)" = "24", "Every 48h (Q48)" = "48"), selected = "12"), numericInput("n_days", "Duration (days)", value = 5, min = 1, max = 21), hr(), uiOutput("rec_box"), hr(), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE) ), # ── Metric cards ──────────────────────────────────────────────────────────── layout_column_wrap( width = 1/4, fill = FALSE, div(class = "metric-card metric-success", div(class = "metric-value", textOutput("out_cmax")), div(class = "metric-label", "Cmax (μg/mL)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("out_ctrough")), div(class = "metric-label", "Ctrough (μg/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("out_auc")), div(class = "metric-label", "AUCτ (μg·h/mL) [target 40–50]")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("out_thalf")), div(class = "metric-label", "t½β (h)")) ), # ── Tabs ──────────────────────────────────────────────────────────────────── navset_card_underline( nav_panel("PK Simulation", card(full_screen = TRUE, plotOutput("pkPlot", height = "500px") ) ), nav_panel("Model Information", markdown(" ## Ganciclovir — 2-Compartment Population PK Model **Publication:** Caldés A et al. (2009). *Antimicrob Agents Chemother* 53(11):4816–4824 **Population:** 20 solid organ transplant (SOT) patients with CMV infection (kidney, liver, heart) **Data:** 382 ganciclovir serum concentrations collected on treatment days 5 (IV) and 15 (oral) **Software:** NONMEM VI — First-order conditional estimation with interaction (FOCE-I) ### Model Structure Two-compartment open model with first-order oral absorption and lag time. Valganciclovir is an oral prodrug rapidly hydrolysed by intestinal and hepatic esterases to ganciclovir. The Ka parameter reflects the combined process of valganciclovir absorption and enzymatic conversion. Ganciclovir doses were derived from valganciclovir using the molecular weight ratio: **0.720**. | Parameter | Estimate | Unit | IIV (%CV) | Description | |-----------|----------|------|-----------|-------------| | CL | 7.49 × (CLCR/57) | L/h | 32.7% | Renal clearance — CLCR-dependent | | V₁ | 31.9 | L | 47.6% | Central volume of distribution | | Q | 10.2 | L/h | — | Intercompartmental clearance | | V₂ | 32.0 | L | — | Peripheral volume of distribution | | Ka | 0.895 | h⁻¹ | 68.1% | Oral absorption rate constant | | F | 0.825 | — | 22.1% | Valganciclovir oral bioavailability | | Tlag | 0.382 | h | — | Absorption lag time | ### Key Covariate **CLCR (Cockcroft-Gault)** is the sole significant covariate on CL, explaining **52%** of IIV. CL ranges from 1.31 L/h (CLCR = 10 mL/min) to 13.14 L/h (CLCR = 100 mL/min). Body weight, gender, and tacrolimus co-administration were not significant independent predictors. ### Therapeutic Target AUC = **40–50 μg·h/mL** per dosing interval (Wiltshire et al. 2005 — viral suppression data) Doses adjusted by CLCR are required to achieve target exposure across the renal function range. ### Manufacturer Recommended Doses (Cockcroft-Gault) **IV Ganciclovir:** - CLCR > 50 mL/min: 5 mg/kg q12h - CLCR 25–50 mL/min: 2.5 mg/kg q12h - CLCR < 25 mL/min: 2.5 mg/kg q24h **Oral Valganciclovir:** - CLCR ≥ 60 mL/min: 900 mg q12h - CLCR 40–59 mL/min: 450 mg q12h - CLCR 25–39 mL/min: 450 mg q24h - CLCR 10–24 mL/min: 450 mg q48h ### Residual Variability Combined error model: additive (σ = 0.465 μg/mL) + proportional (CV = 37.8%) ") ), nav_panel("References", div(class = "ref-box", tags$h5("📚 Primary Reference"), tags$p( "Caldés A, Colom H, Armendariz Y, Garrido MJ, Troconiz IF, Gil-Vernet S, Lloberas N, ", "Pou L, Peraire C, Grinyó JM. (2009). Population Pharmacokinetics of Ganciclovir after ", "Intravenous Ganciclovir and Oral Valganciclovir Administration in Solid Organ Transplant ", "Patients Infected with Cytomegalovirus. ", tags$em("Antimicrob Agents Chemother"), " 53(11):4816–4824. ", tags$a(href = "https://doi.org/10.1128/AAC.00085-09", target = "_blank", "DOI: 10.1128/AAC.00085-09") ), tags$h5("📚 Additional References"), tags$ol( tags$li("Wiltshire H et al. (2005). Pharmacodynamics of oral ganciclovir and valganciclovir ", "in solid organ transplant recipients. Transplantation 79:1477–1483."), tags$li("Wiltshire H et al. (2005). Pharmacokinetic profile of ganciclovir after oral ", "administration from valganciclovir in SOT. Clin Pharmacokinet 44:495–507."), tags$li("Yuen GJ et al. (1995). Population differences in ganciclovir clearance by NLME. ", "Antimicrob Agents Chemother 39:2350–2352."), tags$li("Czock D et al. (2002). Pharmacokinetics of valganciclovir and ganciclovir in ", "renal impairment. Clin Pharmacol Ther 72:142–150."), tags$li("Asberg A et al. (2007). Oral valganciclovir non-inferior to IV ganciclovir for ", "CMV disease in SOT. Am J Transplant 7:2106–2113.") ), tags$h5("💊 Drug Information"), tags$ul( tags$li(tags$strong("Drugs:"), " Ganciclovir (Cymevene®) IV / Valganciclovir (Valcyte®) oral prodrug"), tags$li(tags$strong("Class:"), " Antiviral — synthetic purine nucleoside analogue (guanosine)"), tags$li(tags$strong("Indication:"), " CMV disease treatment and prophylaxis in immunocompromised patients"), tags$li(tags$strong("Mechanism:"), " Inhibits CMV DNA polymerase (UL54); requires viral kinase (UL97) phosphorylation"), tags$li(tags$strong("Elimination:"), " 100% renal excretion as unchanged ganciclovir; dose adjustment essential"), tags$li(tags$strong("MW ratio:"), " Ganciclovir/Valganciclovir = 255.23/354.37 = 0.720") ) ) ) ), # ── Footer ────────────────────────────────────────────────────────────────── 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)", 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) { # Manufacturer-recommended dose guidance output$rec_box <- renderUI({ clcr <- as.numeric(input$clcr) if (input$route == "iv") { rec <- if (clcr > 50) "5 mg/kg q12h" else if (clcr >= 25) "2.5 mg/kg q12h" else "2.5 mg/kg q24h" lbl <- paste0("IV Ganciclovir: ", rec) } else { rec <- if (clcr >= 60) "900 mg q12h" else if (clcr >= 40) "450 mg q12h" else if (clcr >= 25) "450 mg q24h" else "450 mg q48h" lbl <- paste0("Oral Valganciclovir: ", rec) } div(class = "rec-box", tags$strong("📋 Manufacturer Recommendation:"), tags$br(), lbl) }) # Core simulation sim_data <- reactive({ shiny::req(input$clcr, input$wt, input$n_days, input$interval) clcr <- as.numeric(input$clcr) interval <- as.numeric(input$interval) n_days <- as.numeric(input$n_days) n_addl <- max(0L, floor(n_days * 24 / interval) - 1L) if (input$route == "iv") { shiny::req(input$dose_iv) dose_mg <- input$dose_iv * as.numeric(input$wt) evnt <- ev(amt = dose_mg, cmt = 2, rate = dose_mg / 1.0, ii = interval, addl = n_addl) } else { shiny::req(input$dose_oral) # Convert valganciclovir mg → ganciclovir equivalent (MW ratio 0.720) dose_gcv <- input$dose_oral * 0.720 evnt <- ev(amt = dose_gcv, cmt = 1, ii = interval, addl = n_addl) } mod %>% param(CLCR = clcr) %>% ev(evnt) %>% mrgsim(end = n_days * 24, delta = 0.1) %>% as.data.frame() }) # Last dosing interval helper last_cycle <- reactive({ d <- sim_data() interval <- as.numeric(input$interval) n_days <- as.numeric(input$n_days) ls <- max(0, (floor(n_days * 24 / interval) - 1) * interval) list( last = dplyr::filter(d, time >= ls), cycle = dplyr::filter(d, time >= ls, time <= ls + interval) ) }) output$out_cmax <- renderText({ x <- last_cycle() sprintf("%.2f", max(x$last$CP, na.rm = TRUE)) }) output$out_ctrough <- renderText({ x <- last_cycle() sprintf("%.2f", min(x$cycle$CP, na.rm = TRUE)) }) output$out_auc <- renderText({ x <- last_cycle() cyc <- x$cycle if (nrow(cyc) < 2) return("N/A") auc <- sum(diff(cyc$time) * (head(cyc$CP, -1) + tail(cyc$CP, -1)) / 2) sprintf("%.1f", auc) }) output$out_thalf <- renderText({ # Terminal (beta) half-life of 2-CMT system clcr <- as.numeric(input$clcr) CL <- 7.49 * (clcr / 57.0) k10 <- CL / 31.9 k12 <- 10.2 / 31.9 k21 <- 10.2 / 32.0 S <- k10 + k12 + k21 beta <- (S - sqrt(S^2 - 4 * k10 * k21)) / 2 sprintf("%.1f", log(2) / beta) }) output$pkPlot <- renderPlot({ d <- sim_data() shiny::req(nrow(d) > 0) route_label <- if (input$route == "iv") "IV Ganciclovir" else "Oral Valganciclovir" if (input$route == "iv") { dose_label <- paste0(input$dose_iv, " mg/kg (", round(input$dose_iv * input$wt), " mg total)") } else { gcv_eq <- round(input$dose_oral * 0.720) dose_label <- paste0(input$dose_oral, " mg valganciclovir (", gcv_eq, " mg GCV equiv.)") } CL_cur <- round(7.49 * as.numeric(input$clcr) / 57, 2) plot_d <- if (input$log_scale) dplyr::filter(d, CP > 0) else d p <- ggplot(plot_d, aes(x = time, y = CP)) + geom_line(color = "#8b5cf6", linewidth = 0.9) + labs( x = "Time (hours)", y = "Ganciclovir Concentration (μg/mL)", title = paste0(route_label, " — ", dose_label, " | Q", input$interval, "H | CLCR = ", input$clcr, " mL/min | Wt = ", input$wt, " kg"), subtitle = paste0("CL = ", CL_cur, " L/h | Target AUCτ: 40–50 μg·h/mL"), caption = "2-CMT model with first-order absorption and lag time | Caldés et al. (2009) Antimicrob Agents Chemother" ) + theme_minimal(base_size = 14) + theme( plot.title = element_text(size = 12, face = "bold"), plot.subtitle = element_text(size = 11, color = "#555555"), plot.caption = element_text(color = "#6c757d", size = 9) ) if (input$log_scale) p <- p + scale_y_log10() p }) } shinyApp(ui = ui, server = server)