options(rsconnect.http.timeout = 3600) library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ── mrgsolve Model: Minimal PBPK + Full TMDD with Target Shedding ── # Shah DK (2013) CPT Pharmacometrics Syst Pharmacol 2:e73 # Drug: Trastuzumab (MW = 146 kDa), Target: HER2 (membrane + soluble ECD) # # EQUILIBRIUM DERIVATION (no drug): # dRP/dt = LAMBDA*RS - KDEG_P*RP = 0 => RP_ss = LAMBDA*RS_ss/KDEG_P # dRS/dt = KSH_P*RM - LAMBDA*RS = 0 => RS_ss = KSH_P*RM_ss/LAMBDA # dRM/dt = KSYN_M - (KDEG_M+KSH_P)*RM = 0 => RM_ss = RM0*VI # # RP_ss = KSH_P * RM0 * VI / KDEG_P # ECD_HER2 [ng/mL] = RP_ss/VP * 110000 = KSH_P * RM0 * VI / (KDEG_P * VP) * 110000 # => KSH_P = (ECD_HER2 / 110000) * KDEG_P * VP / (RM0 * VI) model_code <- ' $PARAM @annotated VP : 2.8 : Plasma volume (L) VI : 12.0 : Interstitial fluid volume (L) LCONV : 0.006 : Convective transport plasma to ISF (L/h) LLYMPH : 0.096 : Lymph transport ISF to plasma (L/h) K10 : 0.002021 : FcRn-mediated clearance rate (1/h) KON_M : 2520.0 : On-rate membrane-bound HER2 (L/umol/h) KOFF_M : 1.26 : Off-rate membrane-bound HER2 (1/h) KINT_M : 0.1 : Internalization rate drug-membrane complex (1/h) KDEG_M : 0.1 : Degradation rate free membrane HER2 (1/h) RM0 : 0.0035 : Baseline membrane HER2 in ISF (umol/L) LAMBDA : 0.01 : Lymph transport rate ECD HER2 ISF to plasma (1/h) KDEG_P : 0.2 : Degradation free ECD HER2 in plasma (1/h) KELM_S : 0.01 : Elimination drug-ECD complex from ISF (1/h) KELM_P : 0.2 : Elimination drug-ECD complex from plasma (1/h) ECD_HER2 : 14.6 : User-input ECD HER2 plasma level (ng/mL) AFFIN_RATIO : 1.0 : Fold-reduction in affinity to soluble HER2 vs membrane-bound $CMT @annotated DRUG_P : Free trastuzumab in plasma (umol) DRUG_I : Free trastuzumab in ISF (umol) RM : Free membrane-bound HER2 in ISF (umol) RS : Free soluble ECD HER2 in ISF (umol) RP : Free soluble ECD HER2 in plasma (umol) DRM : Drug-membrane complex in ISF (umol) DRS : Drug-soluble complex in ISF (umol) DRP : Drug-soluble complex in plasma (umol) $MAIN // Shedding rate derived from user-specified ECD HER2 plasma level // KSH_P = (ECD_HER2 / MW_ECD) * KDEG_P * VP / (RM0 * VI) double KSH_P = (ECD_HER2 / 110000.0) * KDEG_P * VP / (RM0 * VI); double KSYN_M = (KDEG_M + KSH_P) * RM0 * VI; // Affinity-modified off-rate for soluble HER2 double KOFF_S = KOFF_M * AFFIN_RATIO; double KON_S = KON_M; double KON_P = KON_M; double KOFF_P = KOFF_M * AFFIN_RATIO; $ODE double CP = DRUG_P / VP; double CI = DRUG_I / VI; // Binding rates (umol/h): kon * Drug_amt * Target_amt / V double bind_P = KON_P * DRUG_P * RP / VP; double bind_M = KON_M * DRUG_I * RM / VI; double bind_S = KON_S * DRUG_I * RS / VI; // Free drug in plasma dxdt_DRUG_P = LLYMPH * CI - LCONV * CP - K10 * DRUG_P - bind_P + KOFF_P * DRP; // Free drug in ISF dxdt_DRUG_I = LCONV * CP - LLYMPH * CI - bind_M + KOFF_M * DRM - bind_S + KOFF_S * DRS; // Free membrane-bound HER2 in ISF dxdt_RM = KSYN_M - KDEG_M * RM - KSH_P * RM - bind_M + KOFF_M * DRM; // Drug-membrane complex in ISF dxdt_DRM = bind_M - KOFF_M * DRM - KINT_M * DRM; // Free soluble ECD HER2 in ISF dxdt_RS = KSH_P * RM - LAMBDA * RS - bind_S + KOFF_S * DRS; // Drug-soluble complex in ISF dxdt_DRS = bind_S - KOFF_S * DRS - KELM_S * DRS; // Free soluble ECD HER2 in plasma dxdt_RP = LAMBDA * RS - KDEG_P * RP - bind_P + KOFF_P * DRP; // Drug-soluble complex in plasma dxdt_DRP = bind_P - KOFF_P * DRP - KELM_P * DRP; $TABLE double CP_ugmL = DRUG_P / VP * 146.0; double CI_ugmL = DRUG_I / VI * 146.0; double RP_ngmL = RP / VP * 110000.0; double RS_ngmL = RS / VI * 110000.0; double RO_pct = (RM + DRM > 0) ? 100.0 * DRM / (RM + DRM) : 0.0; $CAPTURE CP_ugmL CI_ugmL RP_ngmL RS_ngmL RO_pct ' mod <- mcode("trastuzumab_pbpk_tmdd", model_code) # ── PKPDBuilder Brand 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; } .metric-danger .metric-value { color: #ef4444; } .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 = "Trastuzumab PBPK-TMDD with HER2 Target Shedding Simulator", theme = app_theme, sidebar = sidebar( title = "Model Parameters", width = 360, h6("ECD HER2 Level"), sliderInput("ecd_her2", "Plasma ECD HER2 (ng/mL)", min = 0, max = 2162, value = 14.6, step = 10), div(style = "font-size:11px; color:#666; margin-top:-10px; margin-bottom:10px;", textOutput("ecd_category", inline = TRUE)), hr(), h6("Dosing Regimen"), sliderInput("load_dose", "Loading Dose (mg/kg)", min = 2, max = 8, value = 4, step = 1), sliderInput("maint_dose", "Maintenance Dose (mg/kg)", min = 1, max = 6, value = 2, step = 1), selectInput("dosing_interval", "Dosing Interval", choices = c("Weekly (Q1W)" = 1, "Every 2 weeks (Q2W)" = 2, "Every 3 weeks (Q3W)" = 3), selected = 1), hr(), h6("Patient & Drug Properties"), sliderInput("bw", "Body Weight (kg)", min = 40, max = 120, value = 70, step = 5), selectInput("affin_ratio", "Drug Affinity to Soluble vs Membrane HER2", choices = c("Equal (1\u00d7)" = 1, "10\u00d7 less" = 10, "100\u00d7 less" = 100, "1000\u00d7 less" = 1000), selected = 1), hr(), h6("Simulation"), sliderInput("n_weeks", "Duration (weeks)", min = 12, max = 52, value = 24, step = 4), checkboxInput("log_scale", "Log Scale Y-axis", value = FALSE) ), # ── Metric Cards ── layout_column_wrap( width = 1/4, fill = FALSE, div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("cmax")), div(class = "metric-label", "Plasma Cmax (\u00b5g/mL)")), div(class = "metric-card metric-success", div(class = "metric-value", textOutput("ctrough")), div(class = "metric-label", "Plasma Ctrough (\u00b5g/mL)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("rec_occ")), div(class = "metric-label", "Receptor Occupancy (%)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("ecd_display")), div(class = "metric-label", "ECD HER2 Input (ng/mL)")) ), # ── Tabbed Plots & Info ── navset_card_underline( full_screen = TRUE, nav_panel("Trastuzumab PK", plotOutput("pkPlot", height = "500px")), nav_panel("Receptor Occupancy", plotOutput("roPlot", height = "500px")), nav_panel("ECD HER2 Dynamics", plotOutput("ecdPlot", height = "500px")), nav_panel("Model Information", div(style = "padding: 20px; max-width: 900px;", tags$h4("Minimal PBPK + Full TMDD with Target Shedding"), tags$p("This simulator implements the model from Shah DK (2013) that incorporates target shedding into a minimal physiologically-based pharmacokinetic (PBPK) model with target-mediated drug disposition (TMDD) for monoclonal antibodies."), tags$h5("Model Structure"), tags$ul( tags$li(tags$strong("Drug distribution:"), " Two-compartment minimal PBPK (plasma + interstitial fluid) with convective transport and lymphatic return."), tags$li(tags$strong("TMDD:"), " Full binding equations (not QSS) for drug binding to both membrane-bound HER2 and soluble ECD HER2."), tags$li(tags$strong("Target shedding:"), " Membrane-bound HER2 is shed to form soluble ECD HER2 in ISF, which then translocates to plasma via lymphatics."), tags$li(tags$strong("State variables:"), " 8 ODEs \u2014 free drug (plasma, ISF), free targets (membrane RM, soluble ISF RS, soluble plasma RP), and drug-target complexes (DRM, DRS, DRP).") ), tags$h5("Parameter Table"), tags$table(class = "table table-sm table-striped", style = "font-size: 13px;", tags$thead(tags$tr(tags$th("Parameter"), tags$th("Value"), tags$th("Description"))), tags$tbody( tags$tr(tags$td("VP"), tags$td("2.8 L"), tags$td("Plasma volume")), tags$tr(tags$td("VI"), tags$td("12.0 L"), tags$td("Interstitial fluid volume")), tags$tr(tags$td("L\u00b7(1\u2212\u03c3V)"), tags$td("0.006 L/h"), tags$td("Convective transport plasma \u2192 ISF")), tags$tr(tags$td("L\u00b7(1\u2212\u03c3I)"), tags$td("0.096 L/h"), tags$td("Lymph transport ISF \u2192 plasma")), tags$tr(tags$td("CL"), tags$td("0.00566 L/h"), tags$td("FcRn-mediated clearance (t\u00bd \u2248 19 days)")), tags$tr(tags$td("kon (membrane)"), tags$td("2520 L/\u00b5mol/h"), tags$td("Association rate for membrane HER2")), tags$tr(tags$td("koff (membrane)"), tags$td("1.26 h\u207b\u00b9"), tags$td("Dissociation rate (KD = 0.5 nM)")), tags$tr(tags$td("kint"), tags$td("0.1 h\u207b\u00b9"), tags$td("Internalization of drug-membrane complex")), tags$tr(tags$td("kdeg,M"), tags$td("0.1 h\u207b\u00b9"), tags$td("Degradation of free membrane HER2")), tags$tr(tags$td("RM0"), tags$td("0.0035 \u00b5mol/L"), tags$td("Baseline membrane HER2 in ISF")), tags$tr(tags$td("\u03bb"), tags$td("0.01 h\u207b\u00b9"), tags$td("ECD HER2 lymphatic transport ISF \u2192 plasma")), tags$tr(tags$td("kdeg,P"), tags$td("0.2 h\u207b\u00b9"), tags$td("Degradation of free ECD HER2 in plasma")), tags$tr(tags$td("kelm,S"), tags$td("0.01 h\u207b\u00b9"), tags$td("Elimination of drug-ECD complex from ISF")), tags$tr(tags$td("kelm,P"), tags$td("0.2 h\u207b\u00b9"), tags$td("Elimination of drug-ECD complex from plasma")) ) ), tags$h5("Key Clinical Insights"), tags$ul( tags$li("High ECD HER2 levels (>500 ng/mL) act as an ", tags$strong("antigen sink"), ", dramatically reducing trastuzumab trough concentrations."), tags$li("At ECD HER2 >1000 ng/mL, trough levels can fall below the therapeutic threshold of 10\u201320 \u00b5g/mL, reducing efficacy."), tags$li("Engineering antibodies with reduced affinity to soluble ECD HER2 (while maintaining membrane affinity) can rescue trough concentrations."), tags$li("Receptor occupancy at the tumor site depends on both drug exposure and competition from soluble ECD HER2 in the interstitial fluid.") ), tags$h5("Typical ECD HER2 Ranges"), tags$ul( tags$li(tags$strong("<15 ng/mL:"), " Healthy subjects / normal"), tags$li(tags$strong("15\u2013500 ng/mL:"), " Standard HER2+ breast cancer"), tags$li(tags$strong(">500 ng/mL:"), " Elevated \u2014 poor prognosis, substantial antigen sink") ) ) ), nav_panel("References", div(class = "ref-box", style = "margin: 20px; max-width: 900px;", tags$h5("Key References"), tags$ol( tags$li("Shah DK. (2013) Incorporating Target Shedding Into a Minimal PBPK\u2013TMDD Model for Monoclonal Antibodies. ", tags$em("CPT: Pharmacometrics & Systems Pharmacology"), ", 2(10):e73. ", tags$a(href = "https://doi.org/10.1038/psp.2013.73", target = "_blank", "doi:10.1038/psp.2013.73")), tags$li("Garg A, Balthasar JP. (2007) Physiologically-based pharmacokinetic (PBPK) model to predict IgG tissue kinetics in wild-type and FcRn-knockout mice. ", tags$em("J Pharmacokinet Pharmacodyn"), ", 34(5):687\u2013745."), tags$li("Mager DE, Jusko WJ. (2001) General pharmacokinetic model for drugs exhibiting target-mediated drug disposition. ", tags$em("J Pharmacokinet Pharmacodyn"), ", 28(6):507\u2013532."), tags$li("Gibiansky L, et al. (2008) Approximations of the target-mediated drug disposition model and identifiability of model parameters. ", tags$em("J Pharmacokinet Pharmacodyn"), ", 35(5):573\u2013591."), tags$li("Baselga J, et al. (1996) Phase II study of weekly intravenous recombinant humanized anti-p185HER2 monoclonal antibody in patients with HER2/neu-overexpressing metastatic breast cancer. ", tags$em("J Clin Oncol"), ", 14(3):737\u2013744.") ), tags$h5("Drug Information"), tags$ul( tags$li(tags$strong("Drug:"), " Trastuzumab (Herceptin\u00ae)"), tags$li(tags$strong("Class:"), " Anti-HER2 humanized monoclonal antibody (IgG1)"), tags$li(tags$strong("MW:"), " 146 kDa"), tags$li(tags$strong("Target:"), " HER2 (ErbB2) \u2014 both membrane-bound and shed soluble ECD"), tags$li(tags$strong("Indication:"), " HER2-positive breast cancer"), tags$li(tags$strong("Route:"), " IV infusion (90 min loading, 30 min maintenance)") ) ) ) ), # ── 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"), " \u2022 Trastuzumab PBPK-TMDD Simulator | For research purposes only" ) ) # ── Server ── server <- function(input, output, session) { # ECD HER2 category label output$ecd_category <- renderText({ ecd <- input$ecd_her2 if (is.null(ecd)) return("") if (ecd <= 100) { "Low / healthy subjects" } else if (ecd <= 500) { "Moderate" } else if (ecd <= 1000) { "High \u2014 steep trough drop zone" } else { "Very high \u2014 elevated cancer burden" } }) # ── Simulation Reactive ── sim_data <- reactive({ shiny::req(input$ecd_her2 >= 0, input$load_dose, input$maint_dose, input$bw, input$n_weeks, input$affin_ratio) ecd_her2 <- input$ecd_her2 affin_ratio <- as.numeric(input$affin_ratio) # Derived shedding rate from equilibrium: # KSH_P = (ECD_HER2 / MW_ECD) * KDEG_P * VP / (RM0 * VI) ksh_p <- (ecd_her2 / 110000.0) * 0.2 * 2.8 / (0.0035 * 12.0) # Initial conditions at steady state without drug RM_0 <- 0.0035 * 12.0 # RM0 * VI [umol] RS_0 <- ksh_p * RM_0 / 0.01 # KSH_P * RM_ss / LAMBDA [umol] RP_0 <- 0.01 * RS_0 / 0.2 # LAMBDA * RS_ss / KDEG_P [umol] # Dosing: dose_umol = dose_mg * 0.006849 (1 mg / 146000 g/mol * 1e6) bw <- input$bw load_mg <- input$load_dose * bw maint_mg <- input$maint_dose * bw load_umol <- load_mg * 0.006849 maint_umol <- maint_mg * 0.006849 # IV infusion over 90 min = 1.5 h load_rate <- load_umol / 1.5 maint_rate <- maint_umol / 1.5 interval_h <- as.numeric(input$dosing_interval) * 7 * 24 total_h <- input$n_weeks * 7 * 24 n_maint <- max(0, floor(total_h / interval_h) - 1) # Build event table e_load <- ev(time = 0, amt = load_umol, rate = load_rate, cmt = 1) if (n_maint > 0) { dose_times <- seq(interval_h, by = interval_h, length.out = n_maint) e_maint <- ev(time = dose_times, amt = maint_umol, rate = maint_rate, cmt = 1) e_all <- c(e_load, e_maint) } else { e_all <- e_load } # Initial conditions init_list <- list( DRUG_P = 0, DRUG_I = 0, RM = RM_0, RS = RS_0, RP = RP_0, DRM = 0, DRS = 0, DRP = 0 ) # Run simulation (mrgsolve uses %>% for S4 chaining) out <- mod %>% param(ECD_HER2 = ecd_her2, AFFIN_RATIO = affin_ratio) %>% init(init_list) %>% ev(e_all) %>% mrgsim(end = total_h, delta = 1, rtol = 1e-6, atol = 1e-9) %>% as.data.frame() out$time_weeks <- out$time / (7 * 24) out }) # ── Metric Outputs ── output$cmax <- renderText({ d <- sim_data() if (is.null(d) || nrow(d) == 0) return("\u2014") early <- dplyr::filter(d, time <= 240) if (nrow(early) == 0) return("\u2014") sprintf("%.1f", max(early$CP_ugmL, na.rm = TRUE)) }) output$ctrough <- renderText({ d <- sim_data() if (is.null(d) || nrow(d) == 0) return("\u2014") late <- dplyr::filter(d, time_weeks >= input$n_weeks * 0.5) if (nrow(late) == 0) return("\u2014") sprintf("%.1f", min(late$CP_ugmL, na.rm = TRUE)) }) output$rec_occ <- renderText({ d <- sim_data() if (is.null(d) || nrow(d) == 0) return("\u2014") late <- dplyr::filter(d, time_weeks >= input$n_weeks * 0.5) if (nrow(late) == 0) return("\u2014") sprintf("%.1f%%", mean(late$RO_pct, na.rm = TRUE)) }) output$ecd_display <- renderText({ sprintf("%.0f", input$ecd_her2) }) # ── Plot: Trastuzumab PK ── output$pkPlot <- renderPlot({ d <- sim_data() shiny::req(nrow(d) > 0) d_plot <- if (input$log_scale) dplyr::filter(d, CP_ugmL > 0.001) else d p <- ggplot(d_plot, aes(x = time_weeks, y = CP_ugmL)) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 10, ymax = 20, fill = "grey70", alpha = 0.2) + annotate("text", x = max(d_plot$time_weeks) * 0.95, y = 15, label = "Therapeutic trough\n(10\u201320 \u00b5g/mL)", hjust = 1, size = 3, color = "grey40") + geom_line(color = "#8b5cf6", linewidth = 0.9) + labs(x = "Time (weeks)", y = "Plasma Trastuzumab (\u00b5g/mL)", title = paste0("Trastuzumab PK \u2014 ", input$load_dose, " mg/kg loading, ", input$maint_dose, " mg/kg Q", input$dosing_interval, "W")) + theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold", size = 14)) if (input$log_scale) { p <- p + scale_y_log10(labels = scales::label_number()) } p }) # ── Plot: Receptor Occupancy ── output$roPlot <- renderPlot({ d <- sim_data() shiny::req(nrow(d) > 0) p <- ggplot(d, aes(x = time_weeks, y = RO_pct)) + geom_hline(yintercept = 80, linetype = "dashed", color = "#ef4444", alpha = 0.7) + annotate("text", x = max(d$time_weeks) * 0.95, y = 82, label = "Target: >80% occupancy", hjust = 1, size = 3, color = "#ef4444") + geom_line(color = "#f59e0b", linewidth = 0.9) + labs(x = "Time (weeks)", y = "Membrane HER2 Receptor Occupancy (%)", title = "HER2 Receptor Occupancy Over Time") + scale_y_continuous(limits = c(0, 100)) + theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold", size = 14)) p }) # ── Plot: ECD HER2 Dynamics ── output$ecdPlot <- renderPlot({ d <- sim_data() shiny::req(nrow(d) > 0) d_plot <- if (input$log_scale) dplyr::filter(d, RP_ngmL > 0.001) else d p <- ggplot(d_plot, aes(x = time_weeks, y = RP_ngmL)) + geom_hline(yintercept = input$ecd_her2, linetype = "dashed", color = "grey50", alpha = 0.7) + annotate("text", x = max(d_plot$time_weeks) * 0.05, y = input$ecd_her2, label = paste0("Baseline: ", round(input$ecd_her2), " ng/mL"), hjust = 0, vjust = -0.5, size = 3, color = "grey40") + geom_line(color = "#0dcaf0", linewidth = 0.9) + labs(x = "Time (weeks)", y = "Free ECD HER2 in Plasma (ng/mL)", title = "Soluble ECD HER2 Dynamics in Plasma") + theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold", size = 14)) if (input$log_scale) { p <- p + scale_y_log10(labels = scales::label_number()) } p }) } shinyApp(ui = ui, server = server)