library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) library(tidyr) # ---- mrgsolve model ---- # Unit system: T-DXd amounts in mg, DXd amounts in µg, volumes in L, time in hours # CP_TDXd in ng/mL (mg/L × 1000), CP_DXd in ng/mL (µg/L = ng/mL) model_code <- ' $PARAM @annotated CLintact : 0.01754 : T-DXd clearance base (L/h, = 0.421 L/day) V1 : 2.77 : T-DXd central volume (L) Qintact : 0.2146 : T-DXd distributional CL (L/h, = 5.15 L/day) V2 : 5.16 : T-DXd peripheral volume (L) CLdrug : 19.2 : DXd clearance base (L/h) KREL_C1 : 0.0159 : Release-rate constant cycle 1 (1/h) WT : 57.8 : Body weight (kg) ALB : 40.0 : Albumin (g/L) TSIZE : 57.0 : Baseline tumor size (mm) SEX : 0 : Sex (0=female, 1=male) JAPAN : 0 : Japanese (0=no, 1=yes) BILI : 8.0 : Total bilirubin (umol/L) AST : 30.0 : AST (U/L) $CMT @annotated CENT1 : T-DXd central amount (mg) PERI1 : T-DXd peripheral amount (mg) DXD : Released DXd amount (ug) $MAIN // Covariate-adjusted T-DXd parameters double CLi = CLintact * pow(WT/57.8, 0.370) * pow(ALB/40.0, -0.533) * pow(TSIZE/57.0, 0.071) * (JAPAN == 1 ? 0.903 : 1.0) * (SEX == 1 ? 1.174 : 1.0); double V1i = V1 * pow(WT/57.8, 0.489) * (SEX == 1 ? 1.197 : 1.0); double V2i = V2 * (JAPAN == 1 ? 0.738 : 1.0); // Covariate-adjusted DXd parameters double CLd = CLdrug * pow(WT/57.8, 0.440) * pow(BILI/8.0, -0.207) * pow(AST/30.0, -0.219); // Body surface area (DuBois, HT fixed 163 cm for typical female) double BSA_m2 = 0.007184 * pow(WT, 0.425) * pow(163.0, 0.725); double Vd = 17.0 * BSA_m2; $ODE double C1 = CENT1 / V1i; // mg/L double C2 = PERI1 / V2i; // mg/L // Cycle-dependent release-rate constant int cyc = (int)(SOLVERTIME / 504.0) + 1; if (cyc < 1) cyc = 1; double KREL = KREL_C1 * pow((double)cyc, -0.137); if (cyc > 1) KREL = KREL * 0.830; // T-DXd 2-CMT ODEs (amounts in mg) dxdt_CENT1 = -CLi * C1 - Qintact * C1 + Qintact * C2; dxdt_PERI1 = Qintact * C1 - Qintact * C2; // DXd 1-CMT ODE (amount in µg) // DAR*MW_DXd/MW_TDXd*1000 = 8*600/156000*1000 = 30.77 µg DXd per mg T-DXd dxdt_DXD = KREL * CENT1 * 30.77 - (CLd / Vd) * DXD; $TABLE double CP_TDXd = (CENT1 / V1i) * 1000.0; // ng/mL (mg/L → ng/mL) double CP_DXd = DXD / Vd; // ng/mL (µg/L = ng/mL) $CAPTURE CP_TDXd CP_DXd ' mod <- mcode("tdxd_popPK", model_code, quiet = TRUE) # ---- 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: 22px; font-weight: bold; color: #2c3e50; } .metric-label { font-size: 11px; color: #7f8c8d; margin-top: 3px; } .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; } .section-label { font-size: 11px; font-weight: 600; color: #6c757d; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px; } ") # ---- UI ---- ui <- page_sidebar( title = "Trastuzumab Deruxtecan (T-DXd / ENHERTU) PK Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 330, div(class = "section-label", "Dosing"), sliderInput("dose_mgkg", "Dose (mg/kg)", min = 0.8, max = 8.0, value = 5.4, step = 0.1), tags$small(class = "text-muted", "Approved dose: 5.4 mg/kg Q3W IV. Infusion 90 min."), br(), br(), sliderInput("n_cycles", "Number of Cycles", min = 1, max = 12, value = 6, step = 1), hr(), div(class = "section-label", "Patient Characteristics"), sliderInput("wt", "Body Weight (kg)", min = 35, max = 130, value = 58, step = 1), sliderInput("alb", "Albumin (g/L)", min = 25, max = 50, value = 40, step = 1), sliderInput("tsize", "Baseline Tumor Size (mm)", min = 10, max = 200, value = 57, step = 5), selectInput("sex", "Sex", choices = c("Female", "Male"), selected = "Female"), checkboxInput("japan", "Japanese patient", value = FALSE), hr(), div(class = "section-label", "Hepatic Markers (DXd CL)"), sliderInput("bili", "Total Bilirubin (µmol/L)", min = 2, max = 30, value = 8, step = 1), sliderInput("ast", "AST (U/L)", min = 10, max = 120, value = 30, step = 5), hr(), checkboxInput("log_scale", "Log Scale (Y-axis)", value = TRUE) ), navset_card_underline( title = "Trastuzumab Deruxtecan (T-DXd) 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("tdxd_cmax")), div(class = "metric-label", "T-DXd Cmax (ng/mL)") ), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("tdxd_ctrough")), div(class = "metric-label", "T-DXd Ctrough (ng/mL)") ), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("tdxd_thalf")), div(class = "metric-label", "T-DXd t\u00bd (days)") ), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("dxd_cmax")), div(class = "metric-label", "DXd Cmax (ng/mL)") ) ), card( full_screen = TRUE, height = "500px", plotOutput("pkPlot", height = "100%") ) ), nav_panel("Model Information", div(class = "ref-box", markdown(" ## Trastuzumab Deruxtecan — Sequential Population PK Model **Drug:** Trastuzumab deruxtecan (T-DXd, DS-8201, ENHERTU) — HER2-targeting ADC **Indication:** HER2-positive/HER2-low/HER2-ultralow unresectable or metastatic breast cancer, HER2-mutant NSCLC **Route:** IV infusion Q3W **Approved dose:** 5.4 mg/kg Q3W (breast cancer); 6.4 mg/kg Q3W for some indications --- ### Intact T-DXd (ADC) — 2-Compartment IV Model | Parameter | Typical Value | Units | |-----------|--------------|-------| | CL (CLintact) | 0.421 | L/day | | V1 (central) | 2.77 | L | | Q (distributional CL) | 5.15 | L/day | | V2 (peripheral) | 5.16 | L | | Terminal t½ | ~13.5 | days | | BSV (CL) | 25% | %CV | | BSV (V1) | 16% | %CV | **Covariate effects on CLintact:** `CL = 0.421 × (WT/57.8)^0.370 × (ALB/40)^-0.533 × (TSize/57)^0.071 × [×0.903 if Japanese] × [×1.174 if male]` **Covariate effects on V1:** `V1 = 2.77 × (WT/57.8)^0.489 × [×1.197 if male]` --- ### Released Drug (DXd) — 1-Compartment Formation-Rate Limited | Parameter | Typical Value | Units | |-----------|--------------|-------| | CL (CLdrug) | 19.2 | L/h | | V (Vdrug) | 17 × BSA | L | | Krel (Cycle 1) | 0.0159 | 1/h | | Intrinsic t½ | ~1 h | — | | Apparent t½ | ~13.5 days | formation-rate limited | **Time-varying Krel:** `Krel = 0.0159 × Cycle^(-0.137) × [×0.830 if Cycle > 1]` (39% reduction at Cycle 10; ~45% at Cycle 20 vs Cycle 1) **Covariate effects on CLdrug:** `CL = 19.2 × (WT/57.8)^0.440 × (BILI/8)^-0.207 × (AST/30)^-0.219` --- ### Key Findings - All covariates (WT, albumin, tumor size, sex, race) produced <20% change in AUC at steady state, except extreme body weights (>86 kg: +28% AUC) and very low albumin (<31 g/L: -23% Cmin) - **No dose adjustment required** based on tested covariates, race, or mild/moderate organ impairment - DXd PK is formation-rate limited — apparent t½ driven by T-DXd elimination ### DAR/MW Scaling DXd release modeled with MW-based scaling: 8 × (600 g/mol DXd)/(156,000 g/mol T-DXd) × 1000 = 30.77 µg DXd per mg T-DXd ") ) ), nav_panel("References", div(class = "ref-box", tags$h5("\U0001f4da Key References"), tags$ol( tags$li( "Yin O, et al. ", tags$em("Population Pharmacokinetics of Trastuzumab Deruxtecan in Patients With HER2-Positive Breast Cancer and Other Solid Tumors."), " Clin Pharmacol Ther. 2021;109(5):1314-1325. ", tags$a(href = "https://doi.org/10.1002/cpt.2096", target = "_blank", "doi:10.1002/cpt.2096") ), tags$li( "Modi S, et al. Trastuzumab Deruxtecan in Previously Treated HER2-Positive Breast Cancer. ", tags$em("N Engl J Med."), " 2020;382:610–621." ), tags$li( "FDA Label: ENHERTU (fam-trastuzumab deruxtecan-nxki). ", tags$a(href = "https://www.accessdata.fda.gov/drugsatfda_docs/label/2025/761139s032s035lbl.pdf", target = "_blank", "Prescribing Information (2025)") ) ), tags$h5("\U0001f48a Drug Context"), tags$ul( tags$li(tags$strong("Class:"), " Antibody-Drug Conjugate (ADC), HER2-targeting"), tags$li(tags$strong("Mechanism:"), " Anti-HER2 mAb + cleavable linker + topoisomerase I inhibitor (exatecan derivative, DAR ≈ 8)"), tags$li(tags$strong("Approved indications:"), " HER2+ mBC, HER2-low/ultralow mBC, HER2-mutant NSCLC, HER2+ gastric/GEJ cancer"), tags$li(tags$strong("Approved doses:"), " 5.4 mg/kg Q3W (HER2+ BC, HER2-low/ultralow BC, NSCLC); 6.4 mg/kg Q3W (HER2+ gastric)"), tags$li(tags$strong("Dataset:"), " 639 patients, 5 clinical studies (DESTINY-Breast01 + 4 phase I studies), doses 0.8–8.0 mg/kg Q3W") ) ) ) ), div( style = paste0("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 Yin et al. Clin Pharmacol Ther 2021 \u2022 For research and educational purposes only", br(), tags$span(style = "font-size: 10px;", "Not for clinical decision-making. Population-typical values shown.") ) ) # ---- Server ---- server <- function(input, output, session) { # ---- Simulation ---- sim_data <- reactive({ shiny::req(input$dose_mgkg, input$wt, input$n_cycles) wt <- input$wt dose_mg <- input$dose_mgkg * wt nc <- input$n_cycles sex_val <- ifelse(input$sex == "Male", 1, 0) jp_val <- ifelse(isTRUE(input$japan), 1, 0) # IV infusion: 90 min = 1.5 h rate infusion_h <- 1.5 rate_mgh <- dose_mg / infusion_h # Dosing events: one per cycle (Q3W = 504 h) ev_list <- lapply(seq_len(nc) - 1, function(c_idx) { ev(amt = dose_mg, cmt = 1, time = c_idx * 504, rate = rate_mgh) }) ev_all <- do.call(c, ev_list) # Simulation end: end of last cycle + 3 days buffer end_h <- nc * 504 + 72 mod |> param(WT = wt, ALB = input$alb, TSIZE = input$tsize, SEX = sex_val, JAPAN = jp_val, BILI = input$bili, AST = input$ast) |> ev(ev_all) |> mrgsim(end = end_h, delta = 2) |> as.data.frame() |> dplyr::filter(time >= 0) }) # ---- Helper: last-cycle data ---- last_cycle_data <- reactive({ d <- sim_data() nc <- input$n_cycles t_start <- (nc - 1) * 504 dplyr::filter(d, time >= t_start, time <= t_start + 504) }) # ---- Metric: T-DXd Cmax ---- output$tdxd_cmax <- renderText({ lc <- last_cycle_data() cmax <- max(lc$CP_TDXd, na.rm = TRUE) if (cmax >= 1000) { sprintf("%.1f k", cmax / 1000) } else { sprintf("%.0f", cmax) } }) # ---- Metric: T-DXd Ctrough ---- output$tdxd_ctrough <- renderText({ lc <- last_cycle_data() nc <- input$n_cycles # Trough = last value before next dose (or end of cycle) ct <- dplyr::filter(lc, time >= (nc - 1) * 504 + 480) # last ~24h of cycle val <- min(ct$CP_TDXd, na.rm = TRUE) if (is.finite(val) && val >= 1000) { sprintf("%.1f k", val / 1000) } else if (is.finite(val)) { sprintf("%.0f", val) } else { "—" } }) # ---- Metric: T-DXd t½ ---- output$tdxd_thalf <- renderText({ wt <- input$wt alb <- input$alb tsize <- input$tsize sex_v <- ifelse(input$sex == "Male", 1, 0) jp_v <- ifelse(isTRUE(input$japan), 1, 0) CLi <- 0.01754 * (wt / 57.8)^0.370 * (alb / 40.0)^(-0.533) * (tsize / 57.0)^0.071 * (if (jp_v == 1) 0.903 else 1.0) * (if (sex_v == 1) 1.174 else 1.0) V1i <- 2.77 * (wt / 57.8)^0.489 * (if (sex_v == 1) 1.197 else 1.0) V2i <- 5.16 * (if (jp_v == 1) 0.738 else 1.0) Q <- 0.2146 k10 <- CLi / V1i k12 <- Q / V1i k21 <- Q / V2i a_sum <- k10 + k12 + k21 disc <- sqrt(a_sum^2 - 4 * k10 * k21) beta <- (a_sum - disc) / 2 # h^-1 thalf_h <- log(2) / beta thalf_d <- thalf_h / 24 sprintf("%.1f", thalf_d) }) # ---- Metric: DXd Cmax ---- output$dxd_cmax <- renderText({ lc <- last_cycle_data() val <- max(lc$CP_DXd, na.rm = TRUE) sprintf("%.2f", val) }) # ---- PK Plot ---- output$pkPlot <- renderPlot({ d <- sim_data() nc <- input$n_cycles # Reshape to long format d_long <- d |> dplyr::select(time, CP_TDXd, CP_DXd) |> tidyr::pivot_longer( cols = c(CP_TDXd, CP_DXd), names_to = "analyte", values_to = "conc" ) |> dplyr::mutate( analyte = dplyr::case_when( analyte == "CP_TDXd" ~ "Intact T-DXd (ng/mL)", analyte == "CP_DXd" ~ "Released DXd (ng/mL)" ), time_day = time / 24 ) |> dplyr::filter(conc > 0) # Vertical lines at each dose dose_days <- (seq_len(nc) - 1) * 21 p <- ggplot(d_long, aes(x = time_day, y = conc, color = analyte)) + geom_vline(xintercept = dose_days, linetype = "dashed", color = "#e9ecef", linewidth = 0.4) + geom_line(linewidth = 0.85) + scale_color_manual( values = c( "Intact T-DXd (ng/mL)" = "#8b5cf6", "Released DXd (ng/mL)" = "#f59e0b" ), name = NULL ) + facet_wrap(~ analyte, ncol = 1, scales = "free_y") + labs( x = "Time (days)", y = "Concentration (ng/mL)", title = sprintf( "T-DXd %.1f mg/kg Q3W \u00d7 %d cycles (%.0f kg, ALB %.0f g/L%s%s)", input$dose_mgkg, nc, input$wt, input$alb, if (input$sex == "Male") ", Male" else "", if (isTRUE(input$japan)) ", Japanese" else "" ) ) + theme_minimal(base_size = 13) + theme( legend.position = "none", strip.text = element_text(face = "bold", size = 12), strip.background = element_rect(fill = "#f0f4ff", color = NA), panel.grid.minor = element_blank(), plot.title = element_text(size = 12, color = "#495057") ) if (input$log_scale) { p <- p + scale_y_log10( labels = scales::label_number(scale_cut = scales::cut_short_scale()) ) } else { p <- p + scale_y_continuous( labels = scales::label_number(scale_cut = scales::cut_short_scale()) ) } p }) } shinyApp(ui = ui, server = server)