options(rsconnect.http.timeout = 3600) library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # --------------------------------------------------------------------------- # mrgsolve model: Minimal PBPK-TMDD with HER2 target shedding # Li Z et al. (2014) CPT:PSP 3:e88 # --------------------------------------------------------------------------- model_code <- ' $PARAM @annotated CL : 0.022 : Clearance (L/h) [effective, includes TMDD at therapeutic doses] V1 : 5.00 : Central volume plasma (L) [effective at peak, accounting for rapid tissue distribution] V2 : 2.06 : Peripheral volume tissue (L) Q : 0.10 : Intercompartmental CL (L/h) KON : 2520 : HER2 association rate [(uM)^-1 h^-1] KOFF : 1.26 : Dissociation rate [h^-1] KDEG_M : 0.1 : HER2 membrane degradation [h^-1] KINT_M : 0.1 : Complex internalization [h^-1] RM_SS : 0.0035 : HER2 membrane SS concentration [uM] LAM : 0.01 : Lymphatic transport [h^-1] KELM_S : 0.01 : Drug-RS complex elimination [h^-1] KELM_P : 0.2 : Drug-RP complex elimination [h^-1] MW_DRUG : 148.0 : Trastuzumab MW (g/mmol) ECD_HER2 : 15.0 : Plasma shed HER2 level [ng/mL] $CMT @annotated DRUG_P : Free drug in plasma (umol) DRUG_T : Free drug in tissue (umol) RM : Membrane-bound HER2 in tissue (umol) RS : Soluble HER2 in tissue interstitial (umol) RP : Soluble HER2 in plasma (umol) ARM : Drug-HER2 membrane complex in tissue (umol) ARS : Drug-HER2 soluble complex in tissue (umol) ARP : Drug-HER2 soluble complex in plasma (umol) $MAIN double ecd_uM0 = ECD_HER2 / 100000.0; double ksh_p0 = ecd_uM0 * KELM_P * V1 / (RM_SS * V2); RM_0 = RM_SS * V2; RS_0 = ksh_p0 * RM_SS / LAM * V2; RP_0 = ecd_uM0 * V1; ARM_0 = 0.0; ARS_0 = 0.0; ARP_0 = 0.0; DRUG_P_0 = 0.0; DRUG_T_0 = 0.0; $ODE double ecd_uM = ECD_HER2 / 100000.0; double ksh_p = ecd_uM * KELM_P * V1 / (RM_SS * V2); double ksyn_m = (KDEG_M + ksh_p) * RM_SS; double Cp = DRUG_P / V1; double Ct = DRUG_T / V2; double Rm = RM / V2; double Rs = RS / V2; double Rp = RP / V1; double bind_RM = KON * Ct * Rm * V2; double bind_RS = KON * Ct * Rs * V2; double bind_RP = KON * Cp * Rp * V1; dxdt_DRUG_P = -(CL/V1)*DRUG_P - (Q/V1)*DRUG_P + (Q/V2)*DRUG_T - bind_RP + KOFF*ARP; dxdt_DRUG_T = (Q/V1)*DRUG_P - (Q/V2)*DRUG_T - bind_RM + KOFF*ARM - bind_RS + KOFF*ARS; dxdt_RM = ksyn_m*V2 - KDEG_M*RM - ksh_p*RM - bind_RM + KOFF*ARM - KINT_M*ARM; dxdt_RS = ksh_p*RM - LAM*RS - bind_RS + KOFF*ARS - KELM_S*ARS; dxdt_RP = LAM*RS - KELM_P*RP - bind_RP + KOFF*ARP - KELM_P*ARP; dxdt_ARM = bind_RM - KOFF*ARM - KINT_M*ARM; dxdt_ARS = bind_RS - KOFF*ARS - KELM_S*ARS; dxdt_ARP = bind_RP - KOFF*ARP - KELM_P*ARP; $TABLE double Cp_ugmL = (DRUG_P / V1) * MW_DRUG; double Rp_ngmL = (RP / V1) * 100000.0; double RO_pct = (ARM/V2) / ((RM/V2) + (ARM/V2) + 1e-12) * 100.0; $CAPTURE Cp_ugmL Rp_ngmL RO_pct ' mod <- mcode("pbpk_tmdd_her2", model_code, quiet = TRUE) # --------------------------------------------------------------------------- # Dosing event builder # --------------------------------------------------------------------------- make_events <- function(load_mgkg, maint_mgkg, interval_days, n_cycles, weight_kg) { MW_kDa <- 148.0 # MW_kDa = 148 kDa = 148 mg/µmol => dose(mg) / 148(mg/µmol) = µmol load_umol <- load_mgkg * weight_kg / MW_kDa maint_umol <- maint_mgkg * weight_kg / MW_kDa interval_h <- interval_days * 24 inf_dur <- 1.5 # 90-min IV infusion ev1 <- ev(time = 0, amt = load_umol, rate = load_umol / inf_dur, cmt = 1) if (n_cycles == 1) return(ev1) times_maint <- seq(interval_h, (n_cycles - 1) * interval_h, by = interval_h) ev_m <- ev(time = times_maint, amt = maint_umol, rate = maint_umol / inf_dur, cmt = 1) c(ev1, ev_m) } # --------------------------------------------------------------------------- # Theme + CSS # --------------------------------------------------------------------------- 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; } ") # --------------------------------------------------------------------------- # Shared footer # --------------------------------------------------------------------------- footer_widget <- div( style = "text-align:center; color:#9ca3af; font-size:11px; padding:8px;", "Built with ", tags$a("PKPDBuilder", href = "https://pkpdbuilder.com", target = "_blank", style = "color:#8b5cf6;"), " | Li Z et al. (2014) CPT:PSP 3:e88" ) # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- ui <- page_sidebar( title = "Trastuzumab PBPK-TMDD \u2014 HER2 Target Shedding Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 320, h6("Dosing"), sliderInput("load_dose", "Loading Dose (mg/kg)", min = 2, max = 8, value = 4, step = 0.5), sliderInput("maint_dose", "Maintenance Dose (mg/kg)", min = 0.5, max = 6, value = 2, step = 0.5), sliderInput("interval_d", "Dosing Interval (days)", min = 3, max = 28, value = 7, step = 1), sliderInput("n_cycles", "Number of Cycles", min = 1, max = 12, value = 6, step = 1), hr(), h6("Patient Characteristics"), sliderInput("weight", "Body Weight (kg)", min = 40, max = 120, value = 73, step = 5), hr(), h6("HER2 Target Biology"), sliderInput("ecd_her2", "Plasma ECD HER2 (ng/mL)", min = 0, max = 2200, value = 15, step = 10), helpText("Healthy: <15 ng/mL | Cancer (low): 50\u2013300 | Cancer (high): >500"), checkboxInput("log_scale", "Log Y-axis (PK plot)", value = FALSE) ), navset_card_underline( # ---- Tab 1: Plasma PK ----------------------------------------------- nav_panel("Plasma PK", layout_columns( col_widths = c(3, 3, 3, 3), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("metric_cmax")), div(class = "metric-label", "Cmax (\u00b5g/mL)")), div(class = "metric-card metric-success", div(class = "metric-value", textOutput("metric_trough")), div(class = "metric-label", "C-trough (\u00b5g/mL)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("metric_ro")), div(class = "metric-label", "HER2 Occupancy")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("metric_thalf")), div(class = "metric-label", "Apparent t\u00bd")) ), card( full_screen = TRUE, min_height = "500px", plotOutput("pkPlot", height = "460px") ), footer_widget ), # ---- Tab 2: HER2 Occupancy ------------------------------------------ nav_panel("HER2 Occupancy", card( full_screen = TRUE, min_height = "500px", plotOutput("occupancyPlot", height = "460px") ), footer_widget ), # ---- Tab 3: ECD HER2 Dynamics --------------------------------------- nav_panel("ECD HER2 Dynamics", card( full_screen = TRUE, min_height = "500px", plotOutput("ecdPlot", height = "460px") ), footer_widget ), # ---- Tab 4: Model Information ---------------------------------------- nav_panel("Model Information", card( markdown(" ## Trastuzumab (Herceptin) \u2014 Anti-HER2 Monoclonal Antibody - **Molecular weight:** 148 kDa - **Target:** HER2/ErbB2 (Human Epidermal Growth Factor Receptor 2) - **Approved indications:** HER2+ breast cancer, gastric/gastroesophageal junction cancer - **Approved dosing:** 4 mg/kg IV loading, 2 mg/kg IV q1w (or 8 mg/kg loading, 6 mg/kg q3w) ## Model Description This simulator implements the minimal PBPK-TMDD model with HER2 target shedding from Li Z et al. (2014). **Model components:** 1. Two-compartment PK model for trastuzumab (V\u2081=2.84 L plasma, V\u2082=2.06 L tissue) 2. Membrane-bound HER2 receptor in tissue interstitial space (TMDD) 3. Shed soluble HER2 (ECD-HER2) in tissue and plasma, competing for drug binding 4. Full drug-target complex dynamics (formation, dissociation, internalization) ## Clinical Insight High levels of circulating shed HER2 (ECD-HER2 >500 ng/mL) create a drug \"sink\" effect: - Drug binds to soluble HER2 instead of the membrane-bound target - The drug-ECD complex is rapidly eliminated (t\u00bd ~ 3.5h) - This reduces free trastuzumab levels and effective receptor occupancy - **Clinical consequence:** Higher initial dose or shorter interval may be needed in patients with high ECD-HER2 ## Key Parameters | Parameter | Value | Source | |-----------|-------|--------| | KD (HER2) | 0.5 nM | In vitro (Molina et al.) | | HER2 membrane [SS] | 0.0035 \u00b5M | Estimated (2\u20135\u00d710\u2075 copies/cell) | | Drug-complex t\u00bd (plasma) | 3.5 h | Calculated from kelm,p = 0.2 h\u207b\u00b9 | | HER2 t\u00bd | 6.9 h | In vitro (kdeg = 0.1 h\u207b\u00b9) | | Trastuzumab t½ (effective, low ECD) | ~9.4 days | Includes membrane TMDD; matches clinical 9.1 ± 4.7 days | ") ), footer_widget ), # ---- Tab 5: References ----------------------------------------------- nav_panel("References", card( div(class = "ref-box", p(strong("Li Z, et al. (2014)."), " Incorporating target shedding into a minimal PBPK-TMDD model for monoclonal antibodies. ", em("CPT Pharmacometrics Syst Pharmacol."), " 3(1):e88. doi:10.1038/psp.2013.73"), p(strong("Garg A & Balthasar JP (2007)."), " Physiologically-based pharmacokinetic (PBPK) model to predict IgG tissue kinetics in wild-type and FcRn-knockout mice. ", em("J Pharmacokinet Pharmacodyn."), " 34(5):687\u2013709."), p(strong("Baselga J, et al. (1999)."), " Phase II study of weekly intravenous trastuzumab in patients with HER2/neu-overexpressing metastatic breast cancer. ", em("Semin Oncol."), " 26(4 Suppl 12):78\u201383.") ) ), footer_widget ) ) ) # --------------------------------------------------------------------------- # Server # --------------------------------------------------------------------------- server <- function(input, output, session) { sim_result <- reactive({ shiny::req(input$load_dose, input$maint_dose, input$weight, input$ecd_her2) evs <- make_events(input$load_dose, input$maint_dose, input$interval_d, input$n_cycles, input$weight) last_dose_h <- (input$n_cycles - 1) * input$interval_d * 24 sim_end_h <- last_dose_h + 48 tryCatch({ mod |> param(ECD_HER2 = input$ecd_her2) |> ev(evs) |> mrgsim(end = sim_end_h, delta = 1, hmax = 0.5, atol = 1e-8, rtol = 1e-8) |> as.data.frame() }, error = function(e) NULL) }) metrics <- reactive({ shiny::req(sim_result()) d <- sim_result() cmax <- max(d$Cp_ugmL, na.rm = TRUE) last_dose_h <- (input$n_cycles - 1) * input$interval_d * 24 d_last <- d |> dplyr::filter(time > last_dose_h) ctrough <- min(d_last$Cp_ugmL, na.rm = TRUE) ro_trough <- d_last$RO_pct[which.min(d_last$Cp_ugmL)] tail_d <- d |> dplyr::filter(time >= last_dose_h + 2, Cp_ugmL > 0.001) thalf_days <- tryCatch({ if (nrow(tail_d) < 3) return(NA_real_) fit <- lm(log(Cp_ugmL) ~ time, data = tail_d) slope <- coef(fit)["time"] if (slope >= 0) NA_real_ else -0.693 / slope / 24 }, error = function(e) NA_real_) list(cmax = cmax, ctrough = ctrough, ro = ro_trough, thalf = thalf_days) }) output$metric_cmax <- renderText(sprintf("%.1f", metrics()$cmax)) output$metric_trough <- renderText(sprintf("%.1f", metrics()$ctrough)) output$metric_ro <- renderText(sprintf("%.0f%%", metrics()$ro)) output$metric_thalf <- renderText({ th <- metrics()$thalf if (is.na(th)) "N/A" else sprintf("%.1f d", th) }) output$pkPlot <- renderPlot({ shiny::req(sim_result()) d <- sim_result() if (input$log_scale) d <- d |> dplyr::filter(Cp_ugmL > 0.001) p <- ggplot(d, aes(x = time / 24, y = Cp_ugmL)) + geom_line(color = "#8b5cf6", linewidth = 1) + geom_hline(yintercept = 10, linetype = "dashed", color = "#ef4444", alpha = 0.8) + annotate("text", x = max(d$time / 24) * 0.85, y = 12, label = "Target trough: 10 \u00b5g/mL", color = "#ef4444", size = 3.5) + labs(x = "Time (days)", y = "Trastuzumab (\u00b5g/mL)", title = sprintf("ECD HER2: %.0f ng/mL | Loading: %.1f mg/kg | Maintenance: %.1f mg/kg q%dd", input$ecd_her2, input$load_dose, input$maint_dose, input$interval_d)) + theme_minimal(base_size = 13) + theme(plot.title = element_text(size = 11)) if (input$log_scale) p <- p + scale_y_log10() p }) output$occupancyPlot <- renderPlot({ shiny::req(sim_result()) d <- sim_result() ggplot(d, aes(x = time / 24, y = RO_pct)) + geom_ribbon(aes(ymin = 0, ymax = RO_pct), fill = "#8b5cf6", alpha = 0.2) + geom_line(color = "#8b5cf6", linewidth = 1) + geom_hline(yintercept = 90, linetype = "dashed", color = "#10b981", linewidth = 0.8) + annotate("text", x = max(d$time / 24) * 0.8, y = 92, label = "90% threshold", color = "#10b981", size = 3.5) + ylim(0, 100) + labs(x = "Time (days)", y = "HER2 Membrane Occupancy (%)", title = "Membrane-bound HER2 Receptor Occupancy") + theme_minimal(base_size = 13) }) output$ecdPlot <- renderPlot({ shiny::req(sim_result()) d <- sim_result() p <- ggplot(d, aes(x = time / 24, y = Rp_ngmL)) + geom_line(color = "#f59e0b", linewidth = 1) + labs(x = "Time (days)", y = "ECD HER2 in Plasma (ng/mL)", title = "Shed HER2 Antigen \u2014 Plasma Dynamics") + theme_minimal(base_size = 13) max_conc <- max(d$Rp_ngmL, na.rm = TRUE) if (max_conc > 15) { p <- p + geom_hline(yintercept = 15, linetype = "dashed", color = "#10b981", alpha = 0.8) + annotate("text", x = max(d$time / 24) * 0.1, y = 17, label = "Normal upper limit: 15 ng/mL", color = "#10b981", size = 3.2) } if (max_conc > 500) { p <- p + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 500, ymax = Inf, fill = "#ef4444", alpha = 0.05) + geom_hline(yintercept = 500, linetype = "dashed", color = "#ef4444", alpha = 0.7) + annotate("text", x = max(d$time / 24) * 0.1, y = 520, label = "High-risk zone (>500 ng/mL)", color = "#ef4444", size = 3.2) } p }) } # --------------------------------------------------------------------------- shinyApp(ui, server)