library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ── PK Model: 2-Compartment IV Infusion ────────────────────────────────────── # Parameters: Asiimwe et al. CPT:PSP 2025. doi:10.1002/psp4.70013 # MBMA of T-DM1 and T-DXd using summary-level data from 103 clinical trials model_code <- ' $PARAM @annotated CL : 0.809 : Clearance (L/day) V1 : 3.283 : Central volume of distribution (L) V2 : 0.748 : Peripheral volume of distribution (L) Q : 1.120 : Intercompartment clearance (L/day) WT : 70.0 : Body weight (kg) $CMT @annotated CENT : Central compartment (mg) PERI : Peripheral compartment (mg) $MAIN double CLi = CL * pow(WT / 70.0, 0.75); double V1i = V1 * (WT / 70.0); double V2i = V2 * (WT / 70.0); double Qi = Q * pow(WT / 70.0, 0.75); $ODE double k10 = CLi / V1i; double k12 = Qi / V1i; double k21 = Qi / V2i; dxdt_CENT = -k10 * CENT - k12 * CENT + k21 * PERI; dxdt_PERI = k12 * CENT - k21 * PERI; $TABLE double CP = CENT / V1i; $CAPTURE CP ' mod <- mcode("adc_2cmt", model_code) # ── Drug parameter library ──────────────────────────────────────────────────── DRUGS <- list( tdm1 = list( label = "T-DM1 (Trastuzumab emtansine / Kadcyla\u00ae)", short = "T-DM1", CL = 0.809, V1 = 3.283, V2 = 0.748, Q = 1.120, approved_dose = 3.6, color = "#8b5cf6" ), tdxd = list( label = "T-DXd (Trastuzumab deruxtecan / Enhertu\u00ae)", short = "T-DXd", CL = 0.585, V1 = 2.785, V2 = 1.243, Q = 0.652, approved_dose = 5.4, color = "#0ea5e9" ) ) # ── Helpers ─────────────────────────────────────────────────────────────────── simulate_adc <- function(mod, CL, V1, V2, Q, dose_mgkg, wt, n_cycles) { dose_mg <- dose_mgkg * wt inf_dur <- 0.0625 # 90 min = 1.5 h = 0.0625 days (Cycle 1 standard infusion) ev1 <- ev( amt = dose_mg, cmt = 1, rate = dose_mg / inf_dur, ii = 21, addl = n_cycles - 1 ) mod %>% param(CL = CL, V1 = V1, V2 = V2, Q = Q, WT = wt) %>% ev(ev1) %>% mrgsim(end = n_cycles * 21 + 7, delta = 0.1) %>% as.data.frame() } calc_thalf <- function(CL, V1, V2, Q, wt = 70) { f <- (wt / 70)^0.75 g <- wt / 70 k10 <- (CL * f) / (V1 * g) k12 <- (Q * f) / (V1 * g) k21 <- (Q * f) / (V2 * g) s <- k10 + k12 + k21 beta <- (s - sqrt(s^2 - 4 * k10 * k21)) / 2 round(log(2) / beta, 1) } get_metrics <- function(df, n_cycles, dose_interval = 21) { last_start <- (n_cycles - 1) * dose_interval last_end <- last_start + dose_interval cyc <- dplyr::filter(df, time >= last_start, time <= last_end) cmax <- round(max(cyc$CP, na.rm = TRUE), 1) ctrough <- round(min(cyc$CP, na.rm = TRUE), 3) dt <- diff(cyc$time) cm <- (head(cyc$CP, -1) + tail(cyc$CP, -1)) / 2 auc <- round(sum(dt * cm, na.rm = TRUE), 1) list(cmax = cmax, ctrough = ctrough, auc = auc) } # ── Theme ───────────────────────────────────────────────────────────────────── app_theme <- bs_theme(version = 5, bootswatch = "flatly", primary = "#8b5cf6") |> bs_add_rules(" .metric-card { background:#f8f9fa; border-radius:8px; padding:14px 10px; margin:4px; text-align:center; border:1px solid #dee2e6; } .metric-value { font-size:21px; font-weight:bold; color:#2c3e50; } .metric-label { font-size:11px; color:#7f8c8d; margin-top:4px; } .metric-primary .metric-value { color:#8b5cf6; } .metric-info .metric-value { color:#0ea5e9; } .metric-success .metric-value { color:#10b981; } .metric-warning .metric-value { color:#f59e0b; } .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; } .drug-tdm1 { color:#8b5cf6; font-weight:600; } .drug-tdxd { color:#0ea5e9; font-weight:600; } ") # ── UI ──────────────────────────────────────────────────────────────────────── ui <- page_sidebar( title = "Trastuzumab ADC PK Simulator: T-DM1 & T-DXd", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Drug Selection"), radioButtons("drug_sel", NULL, choices = list( "T-DM1 (Trastuzumab emtansine)" = "tdm1", "T-DXd (Trastuzumab deruxtecan)" = "tdxd", "Compare Both" = "both" ), selected = "both" ), hr(), conditionalPanel("input.drug_sel !== 'tdxd'", h6(tags$span(class = "drug-tdm1", "\u25a0 T-DM1 Dose (mg/kg)")), sliderInput("dose_tdm1", NULL, min = 0.3, max = 4.8, value = 3.6, step = 0.3), tags$small(class = "text-muted", "Approved: 3.6 mg/kg Q3W") ), conditionalPanel("input.drug_sel !== 'tdm1'", h6(tags$span(class = "drug-tdxd", "\u25a0 T-DXd Dose (mg/kg)")), sliderInput("dose_tdxd", NULL, min = 0.8, max = 8.0, value = 5.4, step = 0.2), tags$small(class = "text-muted", "Approved: 5.4 mg/kg Q3W (breast/NSCLC); 6.4 mg/kg (gastric)") ), hr(), h6("Patient"), sliderInput("wt", "Weight (kg)", min = 40, max = 120, value = 70, step = 5), sliderInput("n_cycles", "Cycles (Q3W, 21-day)", min = 1, max = 8, value = 4, step = 1), hr(), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE) ), navset_card_underline( title = "HER2-Targeted ADC Population PK Simulator", full_screen = TRUE, # ── Simulation Panel ────────────────────────────────────────────────────── nav_panel("Simulation", # Single-drug metrics conditionalPanel("input.drug_sel !== 'both'", layout_columns( col_widths = c(3, 3, 3, 3), div(class = "metric-card metric-success", div(class = "metric-value", textOutput("cmax_s")), div(class = "metric-label", "Cmax (\u03bcg/mL)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("ctrough_s")), div(class = "metric-label", "Ctrough (\u03bcg/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc_s")), div(class = "metric-label", "AUC\u2080\u208b\u2082\u2081\u1d48 (\u03bcg\u00b7day/mL)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf_s")), div(class = "metric-label", "t\u00bd (days)")) ) ), # Comparison metrics (2 rows of 4) conditionalPanel("input.drug_sel === 'both'", layout_columns( col_widths = c(3, 3, 3, 3, 3, 3, 3, 3), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("cmax_tdm1_c")), div(class = "metric-label", "T-DM1 Cmax (\u03bcg/mL)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("cmax_tdxd_c")), div(class = "metric-label", "T-DXd Cmax (\u03bcg/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("ctrough_tdm1_c")), div(class = "metric-label", "T-DM1 Ctrough (\u03bcg/mL)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("ctrough_tdxd_c")), div(class = "metric-label", "T-DXd Ctrough (\u03bcg/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc_tdm1_c")), div(class = "metric-label", "T-DM1 AUC\u2080\u208b\u2082\u2081\u1d48")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("auc_tdxd_c")), div(class = "metric-label", "T-DXd AUC\u2080\u208b\u2082\u2081\u1d48")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("thalf_tdm1_c")), div(class = "metric-label", "T-DM1 t\u00bd (days)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf_tdxd_c")), div(class = "metric-label", "T-DXd t\u00bd (days)")) ) ), # PK Plot card( full_screen = TRUE, height = "500px", plotOutput("pkPlot", height = "460px") ) ), # ── Model Information ───────────────────────────────────────────────────── nav_panel("Model Information", markdown(" ## Trastuzumab ADC Population PK Models **Structure:** Two-compartment with linear elimination (IV infusion, ~90 min standard infusion) **Modeling approach:** Model-Based Meta-Analysis (MBMA) using summary-level PK data from 103 clinical trials **Software used:** Monolix 2024R1 via R (lixoftConnectors) **Reference:** Asiimwe et al., *CPT: Pharmacometrics & Systems Pharmacology*, 2025. doi:10.1002/psp4.70013 --- ### T-DM1 (Trastuzumab emtansine — Kadcyla®) | Parameter | Estimate | RSE (%) | |-----------|----------|---------| | CL (L/day) | 0.809 | 7.3 | | V₁ (L) | 3.283 | 4.8 | | V₂ (L) | 0.748 | 8.3 | | Q (L/day) | 1.120 | 26.3 | | BSV CL | 0.334 | 15.5 | | BSV V₁ | 0.221 | 14.8 | **Approved dose:** 3.6 mg/kg IV Q3W (HER2+ breast cancer) **Terminal t½:** ~3.5 days (this app) / ~4 days (reported) **FDA approval:** 2013 | Median study weight: 69.4 kg --- ### T-DXd (Trastuzumab deruxtecan — Enhertu®) | Parameter | Estimate | RSE (%) | |-----------|----------|---------| | CL (L/day) | 0.585 | 3.7 | | V₁ (L) | 2.785 | 1.5 | | V₂ (L) | 1.243 | 9.2 | | Q (L/day) | 0.652 | 9.7 | | BSV CL | 0.084 | 48.3 | | BSV V₁ | 0.038 | 38.8 | **Approved doses:** 5.4 mg/kg Q3W (breast cancer, NSCLC) | 6.4 mg/kg Q3W (gastric cancer) **Terminal t½:** ~5.3 days (this app) / ~6 days (reported) **FDA approval:** 2019 | Median study weight: 59.0 kg --- ### Covariate Scaling (app implementation) - **CL, Q:** Allometric scaling — (WT/70 kg)^0.75 - **V₁, V₂:** Linear scaling — (WT/70 kg) ### Exposure Metrics Used in CUI Analysis This paper combined Cmax (PK), ORR (efficacy), and DLT (safety) into a Clinical Utility Index. The T-DM1 approved dose (3.6 mg/kg) aligns with an **ORR:DLT weight of 81:19** at all trial phases. These weights successfully predicted the approved T-DXd dose of 5.4 mg/kg for breast cancer. ") ), # ── References ──────────────────────────────────────────────────────────── nav_panel("References", div(class = "ref-box", tags$h5("\U0001f4da Key References"), tags$ol( tags$li( "Asiimwe I, et al. Postmarketing Assessment of Antibody Drug Conjugates: Proof-of-Concept Using Model-Based Meta-Analysis and a Clinical Utility Index Approach. ", tags$em("CPT Pharmacometrics Syst Pharmacol."), " 2025;11. ", tags$a(href = "https://doi.org/10.1002/psp4.70013", target = "_blank", "doi:10.1002/psp4.70013") ), tags$li("Lu D, et al. Population pharmacokinetics of trastuzumab emtansine (T-DM1) in HER2-positive metastatic breast cancer. Cancer Chemother Pharmacol. 2014;74(2):399-409."), tags$li("Yin O, et al. Population pharmacokinetics of trastuzumab deruxtecan. Clin Pharmacokinet. 2020;59(11):1461-1473."), tags$li("Verma S, et al. Trastuzumab emtansine for HER2-positive advanced breast cancer (EMILIA). N Engl J Med. 2012;367(19):1783-1791."), tags$li("Cortés J, et al. Trastuzumab deruxtecan versus trastuzumab emtansine (DESTINY-Breast03). N Engl J Med. 2022;386(12):1143-1154.") ), tags$h5("\U0001f48a Drug Information"), tags$ul( tags$li(tags$strong("T-DM1 (Kadcyla\u00ae):"), " HER2-targeting mAb (trastuzumab) + DM1 (maytansinoid). DAR ~3.5. FDA approved 2013. 3.6 mg/kg Q3W."), tags$li(tags$strong("T-DXd (Enhertu\u00ae):"), " HER2-targeting mAb (trastuzumab) + DXd (topoisomerase I inhibitor). DAR ~8. FDA approved 2019. 5.4 or 6.4 mg/kg Q3W."), tags$li(tags$strong("Drug class:"), " Antibody-Drug Conjugates (ADC), HER2-targeted"), tags$li(tags$strong("Route:"), " IV infusion — 90 min (Cycle 1), 30 min (subsequent cycles if tolerated)"), tags$li(tags$strong("Indication:"), " HER2-positive breast cancer, NSCLC (T-DXd), gastric/GEJ cancer (T-DXd)") ) ) ) ), 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"), " \u2022 For research and educational purposes only. Not for clinical decision-making.", tags$br(), tags$span(style = "font-size:10px;", "Model: Asiimwe et al. CPT:PSP 2025 \u2022 T-DM1: CL=0.809 L/d, V\u2081=3.28 L \u2022 T-DXd: CL=0.585 L/d, V\u2081=2.79 L") ) ) # ── Server ──────────────────────────────────────────────────────────────────── server <- function(input, output, session) { # thematic removed — not available on shinyapps.io # Reactive simulations sim_tdm1 <- reactive({ shiny::req(input$dose_tdm1, input$wt, input$n_cycles) p <- DRUGS$tdm1 simulate_adc(mod, p$CL, p$V1, p$V2, p$Q, input$dose_tdm1, input$wt, input$n_cycles) }) sim_tdxd <- reactive({ shiny::req(input$dose_tdxd, input$wt, input$n_cycles) p <- DRUGS$tdxd simulate_adc(mod, p$CL, p$V1, p$V2, p$Q, input$dose_tdxd, input$wt, input$n_cycles) }) sim_active <- reactive({ if (input$drug_sel == "tdm1") sim_tdm1() else sim_tdxd() }) # ── Single-drug metrics ──────────────────────────────────────────────────── output$cmax_s <- renderText({ m <- get_metrics(sim_active(), input$n_cycles) sprintf("%.1f", m$cmax) }) output$ctrough_s <- renderText({ m <- get_metrics(sim_active(), input$n_cycles) sprintf("%.3f", m$ctrough) }) output$auc_s <- renderText({ m <- get_metrics(sim_active(), input$n_cycles) sprintf("%.1f", m$auc) }) output$thalf_s <- renderText({ shiny::req(input$wt) p <- if (input$drug_sel == "tdm1") DRUGS$tdm1 else DRUGS$tdxd sprintf("%.1f", calc_thalf(p$CL, p$V1, p$V2, p$Q, input$wt)) }) # ── Comparison metrics ───────────────────────────────────────────────────── output$cmax_tdm1_c <- renderText({ m <- get_metrics(sim_tdm1(), input$n_cycles); sprintf("%.1f", m$cmax) }) output$cmax_tdxd_c <- renderText({ m <- get_metrics(sim_tdxd(), input$n_cycles); sprintf("%.1f", m$cmax) }) output$ctrough_tdm1_c <- renderText({ m <- get_metrics(sim_tdm1(), input$n_cycles); sprintf("%.3f", m$ctrough) }) output$ctrough_tdxd_c <- renderText({ m <- get_metrics(sim_tdxd(), input$n_cycles); sprintf("%.3f", m$ctrough) }) output$auc_tdm1_c <- renderText({ m <- get_metrics(sim_tdm1(), input$n_cycles); sprintf("%.1f", m$auc) }) output$auc_tdxd_c <- renderText({ m <- get_metrics(sim_tdxd(), input$n_cycles); sprintf("%.1f", m$auc) }) output$thalf_tdm1_c <- renderText({ sprintf("%.1f", calc_thalf(DRUGS$tdm1$CL, DRUGS$tdm1$V1, DRUGS$tdm1$V2, DRUGS$tdm1$Q, input$wt)) }) output$thalf_tdxd_c <- renderText({ sprintf("%.1f", calc_thalf(DRUGS$tdxd$CL, DRUGS$tdxd$V1, DRUGS$tdxd$V2, DRUGS$tdxd$Q, input$wt)) }) # ── PK Plot ──────────────────────────────────────────────────────────────── output$pkPlot <- renderPlot({ shiny::req(input$drug_sel, input$wt, input$n_cycles) if (input$drug_sel == "both") { shiny::req(input$dose_tdm1, input$dose_tdxd) d1 <- sim_tdm1() |> mutate(drug = paste0("T-DM1 (", input$dose_tdm1, " mg/kg)")) d2 <- sim_tdxd() |> mutate(drug = paste0("T-DXd (", input$dose_tdxd, " mg/kg)")) d <- bind_rows(d1, d2) p <- ggplot(d, aes(x = time, y = CP, color = drug)) + geom_line(linewidth = 0.9) + scale_color_manual( values = setNames( c(DRUGS$tdm1$color, DRUGS$tdxd$color), c(paste0("T-DM1 (", input$dose_tdm1, " mg/kg)"), paste0("T-DXd (", input$dose_tdxd, " mg/kg)")) ) ) + labs( x = "Time (days)", y = "Concentration (\u03bcg/mL)", title = paste0("ADC PK Comparison \u2014 ", input$n_cycles, " Cycles Q3W \u2014 ", input$wt, " kg"), color = NULL ) + theme_minimal(base_size = 14) + theme(legend.position = "top") } else { dose_v <- if (input$drug_sel == "tdm1") input$dose_tdm1 else input$dose_tdxd p_drug <- DRUGS[[input$drug_sel]] d <- sim_active() p <- ggplot(d, aes(x = time, y = CP)) + geom_line(color = p_drug$color, linewidth = 0.9) + labs( x = "Time (days)", y = "Concentration (\u03bcg/mL)", title = paste0(p_drug$short, " \u2014 ", dose_v, " mg/kg Q3W \u00d7 ", input$n_cycles, " cycles \u2014 ", input$wt, " kg") ) + theme_minimal(base_size = 14) } if (input$log_scale) { p <- p + scale_y_log10() + annotation_logticks(sides = "l", alpha = 0.4) } p }) } shinyApp(ui = ui, server = server)