library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) model_code <- ' $PARAM @annotated TVCL : 23 : Typical Apparent Clearance (L/h) [ref: 70 kg, CYP3A5 PM, MAC] TVV1 : 150 : Typical Central Volume (L) TVV2 : 1153 : Typical Peripheral Volume (L) TVQ : 43 : Typical Intercompartmental Clearance (L/h) TVKA : 0.50 : Absorption Rate Constant (1/h) WT : 70 : Body Weight (kg) CYP3A5 : 0 : CYP3A5 Phenotype (0=PM, 1=IM/NM) REGIMEN : 0 : Conditioning Regimen (0=MAC, 1=RIC) $CMT @annotated DEPOT : Oral depot (mg) CENT : Central compartment (mg) PERIPH : Peripheral compartment (mg) $MAIN double CYP_EFF = 1.0; if(CYP3A5 == 1) CYP_EFF = 2.14; double RIC_EFF = 1.0; if(REGIMEN == 1) RIC_EFF = 0.63; double CLi = TVCL * pow(WT/70.0, 0.75) * CYP_EFF * RIC_EFF; double V1i = TVV1 * (WT/70.0); double V2i = TVV2 * (WT/70.0); double Qi = TVQ * pow(WT/70.0, 0.75); double KAi = TVKA; $ODE dxdt_DEPOT = -KAi * DEPOT; dxdt_CENT = KAi * DEPOT - (CLi/V1i)*CENT - (Qi/V1i)*CENT + (Qi/V2i)*PERIPH; dxdt_PERIPH = (Qi/V1i)*CENT - (Qi/V2i)*PERIPH; $TABLE double CP = (CENT / V1i) * 1000.0; $CAPTURE CP ' mod <- mcode("tacrolimus_hct", model_code) 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; } .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; } .dose-info { background: #fffbeb; border-left: 4px solid #f59e0b; padding: 10px 14px; border-radius: 4px; margin-bottom: 15px; font-size: 12px; } ") ui <- page_sidebar( title = "Tacrolimus PopPK Simulator — Allo-HCT Recipients", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 360, h6("Dosing Protocol"), radioButtons("dose_mode", "Dosing Strategy", choices = c( "UNCMC Standard" = "uncmc", "Model-Derived (Recommended)" = "model", "Custom" = "custom" ), selected = "uncmc" ), conditionalPanel( condition = "input.dose_mode == 'custom'", sliderInput("dose_mg", "Dose (mg)", min = 0.5, max = 10, value = 2, step = 0.5), radioButtons("freq", "Frequency", choices = c("BID (every 12h)" = "12", "TID (every 8h)" = "8"), selected = "12" ) ), numericInput("n_days", "Duration (days)", value = 7, min = 3, max = 14), hr(), h6("Patient Characteristics"), sliderInput("wt", "Body Weight (kg)", min = 50, max = 125, value = 70, step = 5), radioButtons("cyp3a5", "CYP3A5 Metabolizer Phenotype", choices = c("Poor Metabolizer (PM)" = "0", "Intermediate/Normal (IM/NM)" = "1"), selected = "0" ), radioButtons("conditioning", "Conditioning Regimen", choices = c("Myeloablative (MAC)" = "0", "Reduced Intensity (RIC)" = "1"), selected = "0" ), hr(), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE), div(class = "dose-info", tags$strong("Calculated Dose: "), textOutput("dose_display", inline = TRUE) ) ), navset_card_tab( title = "Tacrolimus PK in Allo-HCT", full_screen = TRUE, nav_panel("Simulation", layout_column_wrap( width = 1/4, fill = FALSE, div(class="metric-card metric-success", div(class="metric-value", textOutput("cmax")), div(class="metric-label","Cmax (ng/mL)")), div(class="metric-card metric-warning", div(class="metric-value", textOutput("ctrough")), div(class="metric-label","Ctrough (ng/mL)")), div(class="metric-card metric-primary", div(class="metric-value", textOutput("auc")), div(class="metric-label","AUC0-τ (ng·h/mL)")), div(class="metric-card metric-info", div(class="metric-value", textOutput("thalf")), div(class="metric-label","Terminal t½ (h)")) ), plotOutput("pkPlot", height = "500px") ), nav_panel("Model Information", markdown(" ## Tacrolimus Population Pharmacokinetics in Adult Allo-HCT Recipients **Model Structure:** Two-compartment with first-order absorption **Estimation Method:** NONMEM 7.4.3 (FOCE-I) **Population:** 290 adult allo-HCT recipients, 906 PK samples ### Population Typical Values (Reference: 70 kg, CYP3A5 PM, MAC) | Parameter | Estimate | Unit | |-----------|----------|------| | KA (absorption rate) | 0.50 | h⁻¹ | | CL/F (apparent clearance) | 23 | L/h | | V1/F (central volume) | 150 | L | | V2/F (peripheral volume) | 1,153 | L | | Q/F (intercompartmental CL) | 43 | L/h | ### Covariate Effects on CL/F | Covariate | Multiplier | Interpretation | |-----------|-----------|----------------| | CYP3A5 IM or NM (vs PM) | 2.14× | ~100% increase in clearance | | RIC (vs MAC) conditioning | 0.63× | ~37% decrease in clearance | | Body weight | Allometric (0.75 on CL/Q, 1.0 on V) | Standard scaling to 70 kg | ### Interindividual Variability (CV%) - CL/F: 55% - V1/F: 95% - V2/F: 66% ### Therapeutic Drug Monitoring - **Institutional Target Range (ITR):** 5–10 ng/mL (trough, whole blood) - **Assay:** LC-MS/MS, measurement range 1–40 ng/mL - **Goal:** Achieve ITR by day of transplant (D0) to reduce aGVHD risk ### Model-Derived Dosing Recommendations | Subgroup | Recommendation | |----------|----------------| | PM + RIC | No change from standard (0.045 mg/kg BID × 2d, then 0.03 mg/kg BID) | | PM + MAC | 45% dose increase | | IM/NM + RIC | 100% dose increase | | IM/NM + MAC | 0.06 mg/kg TID (no acceptable BID regimen) | ")), nav_panel("References", div(class = "ref-box", tags$h5(HTML("📚 Key References")), tags$ol( tags$li("Dunlap TC, Zhu J, Weiner DL, et al. (2025) A Tacrolimus Population Pharmacokinetic Model for Adult Allogeneic Hematopoietic Cell Transplant Recipients Provides Clinical Opportunities for Precision Dosing. Clin Pharmacokinet 64:1621–1637. doi:10.1007/s40262-025-01529-w"), tags$li("Birdwell KA, Decker B, Engbakken KM, et al. (2015) Clinical Pharmacogenetics Implementation Consortium (CPIC) Guidelines for CYP3A5 Genotype and Tacrolimus Dosing. Clin Pharmacol Ther 98(1):19-24."), tags$li("Zhu J, Crona DJ, Engbakken KM, et al. (2020) CYP3A5 Genotype Impacts Tacrolimus Pharmacokinetics in Adult HCT Recipients. Blood 136(Suppl 1):42-43.") ), tags$h5(HTML("💊 Therapeutic Context")), tags$ul( tags$li(tags$strong("Drug:"), " Tacrolimus (FK506, Prograf®)"), tags$li(tags$strong("Class:"), " Calcineurin inhibitor (immunosuppressant)"), tags$li(tags$strong("Indication:"), " aGVHD prophylaxis in allogeneic hematopoietic cell transplant"), tags$li(tags$strong("Route:"), " Oral (immediate-release capsules)"), tags$li(tags$strong("Standard Dosing:"), " 0.045 mg/kg BID × 2 days, then 0.03 mg/kg BID"), tags$li(tags$strong("Target Trough:"), " 5–10 ng/mL (whole blood, LC-MS/MS)"), tags$li(tags$strong("Metabolism:"), " CYP3A4/CYP3A5 (hepatic and intestinal); CYP3A5 genotype is a major source of PK variability"), tags$li(tags$strong("Key Covariates:"), " CYP3A5 metabolizer phenotype, conditioning regimen intensity, body weight") ), tags$h5(HTML("⚠️ Clinical Notes")), tags$ul( tags$li("Subtherapeutic trough concentrations are associated with increased risk of grade 2–4 aGVHD"), tags$li("Supratherapeutic exposure may increase risk of acute kidney injury"), tags$li("CYP3A5 IM/NM patients require approximately double the dose of PM patients"), tags$li("MAC conditioning is associated with higher clearance (lower troughs) compared to RIC"), tags$li("ClinicalTrials.gov: NCT04645667") ) )) ), 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) { # Compute doses based on dosing strategy get_dosing <- reactive({ wt <- input$wt cyp <- as.numeric(input$cyp3a5) cond <- as.numeric(input$conditioning) if (input$dose_mode == "uncmc") { # UNCMC standard: 0.045 mg/kg BID x2 days, then 0.03 mg/kg BID load_dose <- round(0.045 * wt * 2) / 2 # round to nearest 0.5 mg maint_dose <- round(0.03 * wt * 2) / 2 list(load_dose = load_dose, maint_dose = maint_dose, interval = 12, label = paste0(load_dose, " mg BID × 2d → ", maint_dose, " mg BID")) } else if (input$dose_mode == "model") { # Model-derived recommendations by subgroup if (cyp == 0 && cond == 1) { # PM + RIC: no change load_dose <- round(0.045 * wt * 2) / 2 maint_dose <- round(0.03 * wt * 2) / 2 list(load_dose = load_dose, maint_dose = maint_dose, interval = 12, label = paste0(load_dose, " mg BID × 2d → ", maint_dose, " mg BID (no change)")) } else if (cyp == 0 && cond == 0) { # PM + MAC: 45% increase load_dose <- round(0.045 * 1.45 * wt * 2) / 2 maint_dose <- round(0.03 * 1.45 * wt * 2) / 2 list(load_dose = load_dose, maint_dose = maint_dose, interval = 12, label = paste0(load_dose, " mg BID × 2d → ", maint_dose, " mg BID (+45%)")) } else if (cyp == 1 && cond == 1) { # IM/NM + RIC: 100% increase load_dose <- round(0.045 * 2.0 * wt * 2) / 2 maint_dose <- round(0.03 * 2.0 * wt * 2) / 2 list(load_dose = load_dose, maint_dose = maint_dose, interval = 12, label = paste0(load_dose, " mg BID × 2d → ", maint_dose, " mg BID (+100%)")) } else { # IM/NM + MAC: 0.06 mg/kg TID dose_tid <- round(0.06 * wt * 2) / 2 list(load_dose = dose_tid, maint_dose = dose_tid, interval = 8, label = paste0(dose_tid, " mg TID (Q8H)")) } } else { # Custom list(load_dose = input$dose_mg, maint_dose = input$dose_mg, interval = as.numeric(input$freq), label = paste0(input$dose_mg, " mg Q", input$freq, "H")) } }) output$dose_display <- renderText({ get_dosing()$label }) sim_data <- reactive({ dosing <- get_dosing() wt <- input$wt cyp <- as.numeric(input$cyp3a5) cond <- as.numeric(input$conditioning) total_h <- input$n_days * 24 ii <- dosing$interval if (input$dose_mode %in% c("uncmc", "model") && dosing$load_dose != dosing$maint_dose) { # Loading + maintenance: 4 loading doses (2 days BID), then maintenance n_load <- 4 n_maint <- max(0, floor((total_h - 2*24) / ii)) ev_load <- ev(amt = dosing$load_dose, cmt = 1, ii = 12, addl = n_load - 1, time = 0) ev_maint <- ev(amt = dosing$maint_dose, cmt = 1, ii = ii, addl = n_maint - 1, time = 48) ev_all <- ev_load + ev_maint } else { # Flat dosing n_doses <- floor(total_h / ii) ev_all <- ev(amt = dosing$load_dose, cmt = 1, ii = ii, addl = n_doses - 1) } mod %>% param(WT = wt, CYP3A5 = cyp, REGIMEN = cond) %>% ev(ev_all) %>% mrgsim(end = total_h, delta = 0.25) %>% as.data.frame() %>% mutate(time_h = time) }) output$cmax <- renderText({ d <- sim_data() ii <- get_dosing()$interval last_start <- (input$n_days - 1) * 24 last <- d %>% filter(time >= last_start) sprintf("%.1f", max(last$CP, na.rm = TRUE)) }) output$ctrough <- renderText({ d <- sim_data() ii <- get_dosing()$interval # Find trough at last dosing interval last_start <- (input$n_days - 1) * 24 last <- d %>% filter(time >= last_start) # Trough is min in last interval sprintf("%.1f", min(last$CP[last$CP > 0], na.rm = TRUE)) }) output$auc <- renderText({ d <- sim_data() ii <- get_dosing()$interval last_start <- (input$n_days - 1) * 24 last <- d %>% filter(time >= last_start, time <= last_start + ii) if (nrow(last) < 2) return("--") auc_val <- sum(diff(last$time) * (head(last$CP, -1) + tail(last$CP, -1)) / 2) sprintf("%.0f", auc_val) }) output$thalf <- renderText({ wt <- input$wt cyp <- as.numeric(input$cyp3a5) cond <- as.numeric(input$conditioning) cyp_eff <- ifelse(cyp == 1, 2.14, 1.0) ric_eff <- ifelse(cond == 1, 0.63, 1.0) CLi <- 23 * (wt/70)^0.75 * cyp_eff * ric_eff V1i <- 150 * (wt/70) V2i <- 1153 * (wt/70) Qi <- 43 * (wt/70)^0.75 k10 <- CLi / V1i k12 <- Qi / V1i k21 <- Qi / V2i sum_k <- k10 + k12 + k21 prod_k <- k10 * k21 beta <- (sum_k - sqrt(sum_k^2 - 4 * prod_k)) / 2 thalf <- log(2) / beta sprintf("%.1f", thalf) }) output$pkPlot <- renderPlot({ d <- sim_data() dosing <- get_dosing() # Determine trough window label window_label <- "Target Trough: 5–10 ng/mL" p <- ggplot(d, aes(x = time_h, y = CP)) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 5, ymax = 10, fill = "#10b981", alpha = 0.12) + geom_hline(yintercept = c(5, 10), linetype = "dashed", color = "#10b981", alpha = 0.6) + annotate("text", x = max(d$time_h) * 0.02, y = 10.5, label = window_label, hjust = 0, size = 3.5, color = "#10b981") + geom_line(color = "#8b5cf6", linewidth = 0.9) + labs( x = "Time (hours)", y = "Tacrolimus Concentration (ng/mL)", title = paste0("Tacrolimus PK — ", dosing$label), subtitle = paste0( input$wt, " kg | CYP3A5 ", ifelse(input$cyp3a5 == "0", "PM", "IM/NM"), " | ", ifelse(input$conditioning == "0", "MAC", "RIC"), " conditioning" ) ) + theme_minimal(base_size = 14) + theme( plot.title = element_text(face = "bold"), plot.subtitle = element_text(color = "#6c757d") ) if (input$log_scale) { p <- p + scale_y_log10() + annotation_logticks(sides = "l") } p }) } shinyApp(ui = ui, server = server)