library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) library(tidyr) # ============================================================================== # Intrathecal Trastuzumab PK Model # Based on: Le Tilly et al. (2021) Clin Pharmacol Ther 110:210-219 # Two-compartment (Serum + CSF) with target-mediated drug disposition # ============================================================================== model_code <- ' $PARAM @annotated V1 : 3.25 : Serum volume of distribution (L) CL : 0.139 : Serum clearance (L/day) V2 : 0.644 : CSF volume of distribution (L) K21 : 0.311 : CSF-to-serum first-order transfer (1/day) K12 : 0.264 : Serum-to-CSF zero-order transfer (mg/day) KIN : 11.88 : Latent target production rate (nmol/day) KTR : 0.325 : Transit rate constant (1/day) KDEG : 0.0116 : Target-drug degradation rate (1/(nmol*day)) $CMT @annotated SERUM : Serum compartment (mg) CSF : CSF compartment (mg) L0 : Transit compartment 0 (nmol) L1 : Transit compartment 1 (nmol) L2 : Transit compartment 2 (nmol) L3 : Transit compartment 3 (nmol) LTARG : Final target compartment (nmol) $MAIN double K10 = CL / V1; double KOUT = KTR; double LSS = KIN / KOUT; L0_0 = LSS; L1_0 = LSS; L2_0 = LSS; L3_0 = LSS; LTARG_0 = LSS; $ODE // Negative feedback on latent target production double KIN_FB = KIN * LSS / (LTARG + 1e-10); // Transit compartments for latent HER2 target dxdt_L0 = KIN_FB - KTR * L0; dxdt_L1 = KTR * L0 - KTR * L1; dxdt_L2 = KTR * L1 - KTR * L2; dxdt_L3 = KTR * L2 - KTR * L3; dxdt_LTARG = KTR * L3 - KOUT * LTARG - KDEG * LTARG * CSF; // Guard zero-order transfer when serum is near zero double k12_adj = K12; if(SERUM < 0.01 && SERUM >= 0) k12_adj = K12 * (SERUM / 0.01); if(SERUM < 0) k12_adj = 0; // Serum and CSF drug amounts dxdt_SERUM = -k12_adj + K21 * CSF - K10 * SERUM; dxdt_CSF = k12_adj - K21 * CSF - KDEG * CSF * LTARG; $TABLE double CP_SERUM = SERUM / V1; double CP_CSF = CSF / V2; $CAPTURE CP_SERUM CP_CSF ' mod <- mcode("trastuzumab_it", 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; } .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 = "Intrathecal Trastuzumab PK Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Intrathecal Dosing"), sliderInput("dose_it", "IT Dose (mg)", min = 10, max = 200, value = 150, step = 10), sliderInput("interval_it", "IT Dosing Interval (days)", min = 3, max = 14, value = 7, step = 1), numericInput("n_weeks", "Treatment Duration (weeks)", value = 8, min = 1, max = 16), hr(), h6("Concomitant IV Trastuzumab"), checkboxInput("add_iv", "Add IV trastuzumab (6 mg/kg Q3W)", value = FALSE), conditionalPanel( condition = "input.add_iv", sliderInput("wt", "Patient Weight (kg)", min = 40, max = 120, value = 65, step = 5), sliderInput("iv_dose_mgkg", "IV Dose (mg/kg)", min = 2, max = 8, value = 6, step = 1) ), hr(), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE), checkboxInput("show_combined", "Overlay CSF & Serum", value = FALSE) ), navset_card_tab( title = "Intrathecal Trastuzumab PK Simulator", 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("csf_cmax")), div(class = "metric-label", "CSF Cmax (mg/L)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("csf_ctrough")), div(class = "metric-label", "CSF Ctrough (mg/L)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("csf_auc")), div(class = "metric-label", "CSF AUC/week (mg·day/L)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf_transfer")), div(class = "metric-label", "t½ CSF→Serum (days)")) ), plotOutput("pkPlot", height = "500px") ), nav_panel("Model Information", markdown(" ## Intrathecal Trastuzumab — Two-Compartment PK Model with TMDD **Source:** Le Tilly O et al. (2021) *Clin Pharmacol Ther* 110:210-219 **Study:** Phase I/II trial (NCT01373710) — 21 patients with HER2+ breast cancer leptomeningeal carcinomatosis ### Model Structure - **Two compartments:** Serum (V₁ = 3.25 L) and CSF (V₂ = 0.644 L) - **Zero-order transfer** from serum to CSF: k₁₂ = 0.264 mg/day (suggests saturable FcRn-mediated transport) - **First-order transfer** from CSF to serum: k₂₁ = 0.311 day⁻¹ (t½ = 2.2 days) - **Serum clearance:** CL = 0.139 L/day - **Target-mediated drug disposition (TMDD):** Irreversible binding to latent HER2 in CSF - **Transit compartments** (L₀–L₃) with negative feedback for delayed HER2 dynamics ### Key Findings - **CSF exposure 142× higher** after IT vs. IV administration - **Rapid CSF-to-serum transfer:** MRT = 3.8 days in CSF, 15.6 days in serum - **Weekly IT dosing** maintains therapeutically relevant CSF concentrations - **Mean transit time** of latent target: 12.3 days ### PK Parameters (Table 2) | Parameter | Value | Description | |-----------|-------|-------------| | V₁ | 3.25 L | Serum volume | | V₂ | 0.644 L | CSF volume | | CL | 0.139 L/day | Serum clearance | | k₂₁ | 0.311 day⁻¹ | CSF→Serum transfer | | k₁₂ | 0.264 mg/day | Serum→CSF transfer (zero-order) | | k_in | 11.88 nmol/day | Target production | | k_tr | 0.325 day⁻¹ | Transit rate | | k_deg | 0.0116 nmol⁻¹·day⁻¹ | Target-drug degradation | ### Therapeutic Context - **Target CSF concentrations:** 10–60 mg/L (based on serum targets from IV efficacy studies) - **Standard IT dose:** 150 mg weekly × 8 weeks - **Route:** Lumbar puncture, Ommaya reservoir, or indwelling IT device - **Concomitant IV:** 6 mg/kg every 3 weeks (when applicable) ")), nav_panel("References", div(class = "ref-box", tags$h5("\U0001F4DA Key References"), tags$ol( tags$li("Le Tilly O et al. (2021) Clin Pharmacol Ther 110:210-219 — Intrathecal trastuzumab population PK with TMDD"), tags$li("Bonneau C et al. (2018) Eur J Cancer — Phase I feasibility study for IT trastuzumab (NCT01373710)"), tags$li("Bruno R et al. (2005) Cancer Chemother Pharmacol 56:361-369 — IV trastuzumab population PK"), tags$li("Kadoch C et al. (2014) Clin Cancer Res 20:1029-1041 — Intraventricular rituximab PK"), tags$li("Tout M et al. (2017) Clin Pharmacokinet 56:635-647 — TMDD model for rituximab"), tags$li("Zagouri F et al. (2013) Breast Cancer Res Treat 139:13-22 — IT trastuzumab systematic review") ), tags$h5("\U0001F48A Therapeutic Context"), tags$ul( tags$li(tags$strong("Drug:"), " Trastuzumab (Herceptin\u00AE)"), tags$li(tags$strong("Class:"), " Humanized monoclonal antibody (IgG1, anti-HER2)"), tags$li(tags$strong("Indication:"), " Leptomeningeal carcinomatosis of HER2+ breast cancer"), tags$li(tags$strong("Routes:"), " Intrathecal (study focus) ± Intravenous"), tags$li(tags$strong("IT Doses tested:"), " 30, 60, 100, 150 mg weekly"), tags$li(tags$strong("Mechanism:"), " Targets HER2 receptor; IT route bypasses blood-meningeal barrier"), tags$li(tags$strong("Approval:"), " FDA 1998 (IV); IT use is investigational") ), tags$h5("\U0001F52C Clinical Notes"), tags$ul( tags$li("Leptomeningeal carcinomatosis median survival ~12 weeks with standard therapy"), tags$li("CSF:serum IgG ratio ~1:429 after IV dosing; IT route achieves ~142× higher CSF exposure"), tags$li("No serious adverse events in 88% of IT trastuzumab cases (literature review)"), tags$li("Clinical improvement observed in 69% of reported cases"), tags$li("Maximum tolerated dose not reached at 150 mg in phase I") ) )) ), 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 Built by Sunny \u2600\uFE0F (Husain Attarwala's AI Assistant)", br(), tags$span(style = "font-size: 10px;", "For research and educational purposes only. Not for clinical decision-making.")) ) # --- Server --- server <- function(input, output, session) { sim_data <- reactive({ # Total simulation time in days total_days <- input$n_weeks * 7 # Intrathecal dosing events (CMT 2 = CSF) n_it_doses <- floor(total_days / input$interval_it) ev_it <- ev(amt = input$dose_it, cmt = 2, ii = input$interval_it, addl = n_it_doses - 1) # Combine events all_ev <- ev_it # Optional IV dosing (CMT 1 = Serum) if (input$add_iv) { dose_iv <- input$iv_dose_mgkg * input$wt n_iv_doses <- floor(total_days / 21) if (n_iv_doses < 1) n_iv_doses <- 1 ev_iv <- ev(amt = dose_iv, cmt = 1, ii = 21, addl = n_iv_doses - 1) all_ev <- ev_it + ev_iv } # Simulate out <- mod %>% ev(all_ev) %>% mrgsim(end = total_days, delta = 0.1) %>% as.data.frame() out }) # --- Metrics --- output$csf_cmax <- renderText({ d <- sim_data() total_days <- input$n_weeks * 7 last_interval_start <- total_days - input$interval_it last <- d %>% filter(time >= last_interval_start) sprintf("%.1f", max(last$CP_CSF, na.rm = TRUE)) }) output$csf_ctrough <- renderText({ d <- sim_data() total_days <- input$n_weeks * 7 # Ctrough just before the last dose last_dose_time <- total_days - input$interval_it trough_window <- d %>% filter(time >= last_dose_time - 0.5, time <= last_dose_time + 0.5) if (nrow(trough_window) == 0) { sprintf("%.1f", 0) } else { sprintf("%.1f", min(trough_window$CP_CSF, na.rm = TRUE)) } }) output$csf_auc <- renderText({ d <- sim_data() total_days <- input$n_weeks * 7 last_start <- total_days - input$interval_it last <- d %>% filter(time >= last_start, time <= total_days) if (nrow(last) < 2) { sprintf("%.0f", 0) } else { auc <- sum(diff(last$time) * (head(last$CP_CSF, -1) + tail(last$CP_CSF, -1)) / 2) sprintf("%.0f", auc) } }) output$thalf_transfer <- renderText({ sprintf("%.1f", log(2) / 0.311) }) # --- Plot --- output$pkPlot <- renderPlot({ d <- sim_data() if (input$show_combined) { # Overlay plot d_long <- d %>% select(time, CP_CSF, CP_SERUM) %>% pivot_longer(cols = c(CP_CSF, CP_SERUM), names_to = "Compartment", values_to = "Concentration") %>% mutate(Compartment = ifelse(Compartment == "CP_CSF", "CSF", "Serum")) p <- ggplot(d_long, aes(x = time, y = Concentration, color = Compartment)) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 10, ymax = 60, fill = "#10b981", alpha = 0.10) + geom_hline(yintercept = c(10, 60), linetype = "dashed", color = "#10b981", alpha = 0.5) + annotate("text", x = max(d$time) * 0.02, y = 65, label = "CSF Target: 10-60 mg/L", hjust = 0, size = 3.5, color = "#10b981") + geom_line(linewidth = 0.9) + scale_color_manual(values = c("CSF" = "#8b5cf6", "Serum" = "#f59e0b")) + labs(x = "Time (days)", y = "Concentration (mg/L)", title = paste0("Trastuzumab IT ", input$dose_it, " mg Q", input$interval_it, "D", ifelse(input$add_iv, paste0(" + IV ", input$iv_dose_mgkg, " mg/kg Q3W"), "")), color = "Compartment") + theme_minimal(base_size = 14) + theme(legend.position = "top") if (input$log_scale) p <- p + scale_y_log10(labels = scales::label_number()) p } else { # Faceted plot d_long <- d %>% select(time, CP_CSF, CP_SERUM) %>% pivot_longer(cols = c(CP_CSF, CP_SERUM), names_to = "Compartment", values_to = "Concentration") %>% mutate(Compartment = factor( ifelse(Compartment == "CP_CSF", "CSF", "Serum"), levels = c("CSF", "Serum"))) p <- ggplot(d_long, aes(x = time, y = Concentration, color = Compartment)) + geom_line(linewidth = 0.9) + scale_color_manual(values = c("CSF" = "#8b5cf6", "Serum" = "#f59e0b")) + facet_wrap(~Compartment, ncol = 1, scales = "free_y") + # Therapeutic window annotation on CSF panel geom_hline(data = data.frame(Compartment = factor("CSF", levels = c("CSF", "Serum")), yint = c(10, 60)), aes(yintercept = yint), linetype = "dashed", color = "#10b981", alpha = 0.6) + labs(x = "Time (days)", y = "Concentration (mg/L)", title = paste0("Trastuzumab IT ", input$dose_it, " mg Q", input$interval_it, "D", ifelse(input$add_iv, paste0(" + IV ", input$iv_dose_mgkg, " mg/kg Q3W"), ""))) + theme_minimal(base_size = 14) + theme(legend.position = "none", strip.text = element_text(face = "bold", size = 13)) if (input$log_scale) p <- p + scale_y_log10(labels = scales::label_number()) p } }) } shinyApp(ui = ui, server = server)