library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ─── mrgsolve model ──────────────────────────────────────────────────────────── # Dato-DXd (ADC) + DXd (payload) PopPK model # Source: Hong et al., CPT Pharmacometrics Syst Pharmacol 2025 (psp4.70118) # Dato-DXd: 2-CMT, parallel linear + Michaelis-Menten clearance (IV Q3W) # DXd: 1-CMT, linear CL; release = total Dato-DXd elimination × DAR × MW ratio # Time unit: hours | Dato-DXd: μg/mL | DXd: ng/mL model_code <- ' $PARAM @annotated // ── Dato-DXd structural (reference: 66 kg male, Alb=38, Age=62, non-Japan, TumorSz=66mm) ── CLlin_day : 0.386 : Linear clearance (L/day) Q_day : 0.422 : Inter-compartmental clearance (L/day) Vc_dato : 3.06 : Central volume Dato-DXd (L) Vp_dato : 2.88 : Peripheral volume Dato-DXd (L) Km : 4490.0 : MM constant (μg/L = ng/mL; consistent with C_dato in μg/L) Vmax_day : 350.4 : Max elimination capacity (μg/h, ref 66mm tumor) // ── DXd structural (reference: 66 kg male, Alb=38, AST=22, TBil=0.4, non-Japan) ── CLdxd_ref : 2.66 : DXd linear clearance (L/h) Vcdxd_ref : 25.1 : DXd central volume (L) // ── DAR model ── Factor1 : 0.696 : DAR fraction in cycles >1 vs cycle 1 beta_day : 0.259 : DAR exponential decline rate (1/day) DAR0 : 4.0 : Starting drug-to-antibody ratio // ── MW conversion ratio (MW_DXd/MW_DatoDXd × 1000 μg→ng) ── MW_ratio : 3.290 : (493.5/150000) × 1000 // ── Patient covariates ── WT : 66.0 : Body weight (kg) FEMALE : 0.0 : Sex (0=male, 1=female) AGE : 62.0 : Age (years) ALB : 38.0 : Albumin (g/L) JAPAN : 0.0 : Region Japan (0=no, 1=yes) TUMSZ : 66.0 : Baseline tumor size (mm) AST : 22.0 : AST (U/L) TBIL : 0.4 : Total bilirubin (mg/dL) $CMT @annotated CENT_DATO : Dato-DXd central (μg) PERI_DATO : Dato-DXd peripheral (μg) CENT_DXD : DXd central (ng) $GLOBAL bool past_cycle1 = false; $MAIN // ── Dato-DXd covariate-adjusted parameters ── double sex_cl = (FEMALE > 0.5) ? (1.0 - 0.263) : 1.0; double jap_cl = (JAPAN > 0.5) ? (1.0 - 0.219) : 1.0; double CLlin_h = (CLlin_day / 24.0) * pow(WT/66.0, 0.75) * pow(AGE/62.0, -0.306) * pow(ALB/38.0, -0.788) * sex_cl * jap_cl; double sex_vc = (FEMALE > 0.5) ? (1.0 - 0.160) : 1.0; double Vc = Vc_dato * pow(WT/66.0, 0.415) * sex_vc; double Vp = Vp_dato * pow(WT/66.0, 0.311); double Q_h = Q_day / 24.0; double Vmax_h = Vmax_day * pow(TUMSZ/66.0, 0.125); // ── DXd covariate-adjusted parameters ── double CLdxd = CLdxd_ref * pow(WT/66.0, 0.298) * pow(ALB/38.0, 0.343) * pow(AST/22.0, -0.154) * pow(TBIL/0.4, -0.139); double sex_vcdxd = (FEMALE > 0.5) ? (1.0 - 0.185) : 1.0; double Vcdxd = Vcdxd_ref * pow(WT/66.0, 0.530) * sex_vcdxd; $ODE // Cycle & within-cycle time tracking // Q3W = 504 h per cycle double beta_h = beta_day / 24.0; double t_within = fmod(SOLVERTIME, 504.0); double cycle_factor = (SOLVERTIME < 504.0) ? 1.0 : Factor1; double DAR_t = DAR0 * cycle_factor * exp(-beta_h * t_within); if (DAR_t < 0.0) DAR_t = 0.0; // Dato-DXd concentrations (μg/mL) double C_dato = CENT_DATO / Vc; double C_peri = PERI_DATO / Vp; if (C_dato < 0.0) C_dato = 0.0; // Dato-DXd elimination double elim_lin = CLlin_h * C_dato; // μg/h double elim_mm = Vmax_h * C_dato / (Km + C_dato); // μg/h double elim_tot = elim_lin + elim_mm; // DXd concentration (ng/mL) double C_dxd = CENT_DXD / Vcdxd; if (C_dxd < 0.0) C_dxd = 0.0; // DXd release: Dato-DXd eliminated (μg/h) × DAR × MW_ratio → ng/h double dxd_release = elim_tot * DAR_t * MW_ratio; // ODEs dxdt_CENT_DATO = -elim_lin - elim_mm + Q_h * (C_peri - C_dato); dxdt_PERI_DATO = Q_h * (C_dato - C_peri); dxdt_CENT_DXD = dxd_release - CLdxd * C_dxd; $TABLE // CENT_DATO in μg, Vc in L → C in μg/L; divide by 1000 for μg/mL // CENT_DXD in ng, Vcdxd in L → C in ng/L; divide by 1000 for ng/mL double CP_dato = (CENT_DATO / Vc) / 1000.0; // Dato-DXd μg/mL double CP_dxd = (CENT_DXD / Vcdxd) / 1000.0; // DXd ng/mL if (CP_dato < 0.0) CP_dato = 0.0; if (CP_dxd < 0.0) CP_dxd = 0.0; $CAPTURE CP_dato CP_dxd ' mod <- mcode("dato_dxd_popk", model_code, quiet = TRUE) # ─── UI ──────────────────────────────────────────────────────────────────────── ui <- page_sidebar( title = "Datopotamab Deruxtecan (Dato-DXd) PopPK Simulator", theme = bs_theme( version = 5, bootswatch = "flatly", primary = "#8b5cf6" ), sidebar = sidebar( title = "Patient & Dosing Parameters", width = 310, h6("Dosing", class = "text-muted mt-1 mb-1"), sliderInput("dose_mgkg", "Dose (mg/kg)", min = 2, max = 8, value = 6, step = 0.5), sliderInput("n_cycles", "Cycles to simulate", min = 1, max = 6, value = 3, step = 1), hr(class = "my-2"), h6("Patient Characteristics", class = "text-muted mb-1"), sliderInput("wt", "Body Weight (kg)", min = 37, max = 156, value = 66, step = 1), sliderInput("age", "Age (years)", min = 18, max = 90, value = 62, step = 1), radioButtons("sex", "Sex", choices = c("Male" = 0, "Female" = 1), selected = 0, inline = TRUE), hr(class = "my-2"), h6("Laboratory Values", class = "text-muted mb-1"), sliderInput("alb", "Albumin (g/L)", min = 19, max = 50, value = 38, step = 1), sliderInput("ast", "AST (U/L)", min = 5, max = 239, value = 22, step = 1), sliderInput("tbil", "Total Bilirubin (mg/dL)", min = 0.1, max = 2.5, value = 0.4, step = 0.1), hr(class = "my-2"), h6("Tumor & Region", class = "text-muted mb-1"), sliderInput("tumsz", "Baseline Tumor Size (mm)", min = 10, max = 341, value = 66, step = 5), radioButtons("region", "Region", choices = c("US/Europe/RoW" = 0, "Japan" = 1), selected = 0, inline = TRUE), hr(class = "my-2"), checkboxInput("log_scale", "Log scale (Y-axis)", value = FALSE) ), # ── Metric cards ───────────────────────────────────────────────────────────── layout_column_wrap( width = 1/4, fill = FALSE, card( class = "text-center border-0 shadow-sm", card_body( h6("Dato-DXd Cmax", class = "text-muted mb-1"), h3(textOutput("cmax_dato", inline = TRUE), class = "text-primary fw-bold mb-0"), p("μg/mL (Cycle 3)", class = "small text-muted mb-0") ) ), card( class = "text-center border-0 shadow-sm", card_body( h6("DXd Cmax", class = "text-muted mb-1"), h3(textOutput("cmax_dxd", inline = TRUE), class = "text-primary fw-bold mb-0"), p("ng/mL (Cycle 3)", class = "small text-muted mb-0") ) ), card( class = "text-center border-0 shadow-sm", card_body( h6("Dato-DXd AUC", class = "text-muted mb-1"), h3(textOutput("auc_dato", inline = TRUE), class = "text-primary fw-bold mb-0"), p("μg·h/mL (Cycle 3)", class = "small text-muted mb-0") ) ), card( class = "text-center border-0 shadow-sm", card_body( h6("DXd AUC", class = "text-muted mb-1"), h3(textOutput("auc_dxd", inline = TRUE), class = "text-primary fw-bold mb-0"), p("ng·h/mL (Cycle 3)", class = "small text-muted mb-0") ) ) ), # ── PK plots ───────────────────────────────────────────────────────────────── layout_column_wrap( width = 1/2, fill = FALSE, card( full_screen = TRUE, height = "500px", card_header("Dato-DXd (Intact ADC) Plasma Concentration"), card_body(plotOutput("plot_dato", height = "420px")) ), card( full_screen = TRUE, height = "500px", card_header("DXd (Payload) Plasma Concentration"), card_body(plotOutput("plot_dxd", height = "420px")) ) ), # ── Model info ─────────────────────────────────────────────────────────────── card( navset_card_underline( nav_panel( "Model Information", div( class = "p-3", h5("Dato-DXd PopPK Model Structure"), tags$ul( tags$li(strong("Dato-DXd (intact ADC):"), " 2-compartment model with parallel linear + Michaelis-Menten (non-linear) clearance"), tags$li(strong("DXd (unconjugated payload):"), " 1-compartment model. DXd release = total Dato-DXd elimination rate × time-varying DAR × MW ratio"), tags$li(strong("DAR model:"), " Time-varying drug-to-antibody ratio: DAR₀=4 in cycle 1; Factor₁=0.696 × DAR₀ in later cycles, with exponential decline within each cycle (β=0.259/day)"), tags$li(strong("Route:"), " IV infusion, Q3W (21-day cycle)"), tags$li(strong("Covariates on Dato-DXd CL_lin:"), " Body weight (0.75), age, albumin, sex, region Japan"), tags$li(strong("Covariates on Dato-DXd Vc:"), " Body weight (0.415), sex"), tags$li(strong("Covariates on DXd CL:"), " Body weight (0.298), albumin, AST, total bilirubin"), tags$li(strong("N=729 patients"), " from TROPION-PanTumor01 (Phase 1), TROPION-Lung05 (Phase 2), TROPION-Lung01 (Phase 3)") ), hr(), h5("Reference Parameters (Typical 66 kg Male Patient)"), tags$table( class = "table table-sm table-striped", tags$thead(tags$tr(tags$th("Parameter"), tags$th("Value"), tags$th("Unit"), tags$th("Description"))), tags$tbody( tags$tr(tags$td("CL_lin (Dato-DXd)"), tags$td("0.386"), tags$td("L/day"), tags$td("Linear clearance")), tags$tr(tags$td("Vc (Dato-DXd)"), tags$td("3.06"), tags$td("L"), tags$td("Central volume")), tags$tr(tags$td("Q (Dato-DXd)"), tags$td("0.422"), tags$td("L/day"), tags$td("Inter-compartmental CL")), tags$tr(tags$td("Vp (Dato-DXd)"), tags$td("2.88"), tags$td("L"), tags$td("Peripheral volume")), tags$tr(tags$td("Km"), tags$td("4490"), tags$td("ng/mL"), tags$td("MM constant")), tags$tr(tags$td("Vmax"), tags$td("8410"), tags$td("μg/day"), tags$td("Max elimination capacity")), tags$tr(tags$td("CL (DXd)"), tags$td("2.66"), tags$td("L/h"), tags$td("DXd linear clearance")), tags$tr(tags$td("Vc (DXd)"), tags$td("25.1"), tags$td("L"), tags$td("DXd central volume")) ) ) ) ), nav_panel( "References", div( class = "p-3", tags$ol( tags$li( "Hong M, et al. ", tags$em("Population Pharmacokinetic Analysis of Datopotamab Deruxtecan (Dato-DXd), a TROP2-Directed Antibody-Drug Conjugate, in Patients With Advanced Solid Tumors."), " CPT Pharmacometrics Syst Pharmacol. 2025;14(12). DOI: ", tags$a("10.1002/psp4.70118", href = "https://doi.org/10.1002/psp4.70118", target = "_blank") ), tags$li( "Tanaka K, et al. ", tags$em("Application of the model-informed drug development paradigm to datopotamab deruxtecan dose selection for late-stage development."), " CPT Pharmacometrics Syst Pharmacol. 2023. PMID: 37915242" ), tags$li( "FDA Label. Datopotamab deruxtecan-dlnk (DATROWAY). Approved January 2025. ", tags$a("FDA Prescribing Information", href = "https://www.accessdata.fda.gov/drugsatfda_docs/label/2025/761394s000lbl.pdf", target = "_blank") ) ) ) ) ) ), # ── Footer ─────────────────────────────────────────────────────────────────── div( class = "text-center text-muted small py-3 mt-2 border-top", HTML('Built with PKPDBuilder • Parameters from Hong et al. 2025 • For research and educational use only') ) ) # ─── Server ──────────────────────────────────────────────────────────────────── server <- function(input, output, session) { sim_data <- reactive({ shiny::req(input$dose_mgkg, input$wt, input$age, input$sex, input$alb, input$ast, input$tbil, input$tumsz, input$n_cycles) n_cycles <- as.integer(input$n_cycles) ii_h <- 504 # Q3W = 21 × 24 h total_h <- n_cycles * ii_h dose_mg <- input$dose_mgkg * input$wt # total dose in mg dose_ug <- dose_mg * 1000 # convert to μg (model amounts in μg for Dato-DXd) # Build dose event table (30 min IV infusion, Q3W) # rate = amt / duration (0.5 h); mrgsolve will infuse at constant rate for 0.5 h tinf_h <- 0.5 # 30-minute infusion infuse_rate <- dose_ug / tinf_h dose_times <- seq(0, (n_cycles - 1) * ii_h, by = ii_h) ev_list <- lapply(dose_times, function(t) { ev(amt = dose_ug, cmt = "CENT_DATO", time = t, rate = infuse_rate) }) evnt <- do.call(c, ev_list) # Simulation time grid tgrid <- seq(0, total_h, by = 1) param_update <- list( WT = input$wt, AGE = input$age, FEMALE = as.numeric(input$sex), ALB = input$alb, AST = input$ast, TBIL = input$tbil, TUMSZ = input$tumsz, JAPAN = as.numeric(input$region) ) out <- mod |> param(param_update) |> mrgsim(events = evnt, tgrid = tgrid, carry_out = "evid") |> as.data.frame() |> dplyr::filter(evid == 0 | time == 0) out$time_days <- out$time / 24 out$cycle <- floor(out$time / ii_h) + 1 out }) # ── Metrics from cycle 3 (or last cycle if < 3) ──────────────────────────── metrics <- reactive({ df <- sim_data() target_cycle <- min(3L, max(df$cycle)) cy_data <- dplyr::filter(df, cycle == target_cycle) list( cmax_dato = max(cy_data$CP_dato, na.rm = TRUE), cmax_dxd = max(cy_data$CP_dxd, na.rm = TRUE), auc_dato = tryCatch( {d <- cy_data |> dplyr::arrange(time); sum(diff(d$time) * (head(d$CP_dato,-1) + tail(d$CP_dato,-1)) / 2)}, error = function(e) NA_real_ ), auc_dxd = tryCatch( {d <- cy_data |> dplyr::arrange(time); sum(diff(d$time) * (head(d$CP_dxd,-1) + tail(d$CP_dxd,-1)) / 2)}, error = function(e) NA_real_ ) ) }) output$cmax_dato <- renderText({ m <- metrics(); sprintf("%.1f", m$cmax_dato) }) output$cmax_dxd <- renderText({ m <- metrics(); sprintf("%.2f", m$cmax_dxd) }) output$auc_dato <- renderText({ m <- metrics(); sprintf("%.0f", m$auc_dato) }) output$auc_dxd <- renderText({ m <- metrics(); sprintf("%.0f", m$auc_dxd) }) # ── Plot helpers ───────────────────────────────────────────────────────────── make_plot <- function(df, yvar, ylab, color, log_sc) { # Cycle break lines n_cyc <- max(df$cycle) ii_d <- 21 vlines <- data.frame(x = seq(ii_d, (n_cyc - 1) * ii_d, by = ii_d)) p <- ggplot(df, aes(x = .data[["time_days"]], y = .data[[yvar]])) + geom_line(color = color, linewidth = 1.0) + labs(x = "Time (days)", y = ylab) + theme_bw(base_size = 13) + theme( panel.grid.minor = element_blank(), panel.border = element_rect(color = "#dee2e6"), plot.background = element_blank(), axis.title = element_text(size = 11), axis.text = element_text(size = 10) ) if (nrow(vlines) > 0) { p <- p + geom_vline(data = vlines, aes(xintercept = .data[["x"]]), linetype = "dashed", color = "#adb5bd", linewidth = 0.5) } if (log_sc) { df_pos <- df[df[[yvar]] > 0, ] if (nrow(df_pos) > 0) { p <- p %+% df_pos + scale_y_log10() } } p } output$plot_dato <- renderPlot({ df <- sim_data() make_plot(df, "CP_dato", "Dato-DXd Concentration (μg/mL)", "#8b5cf6", input$log_scale) }) output$plot_dxd <- renderPlot({ df <- sim_data() make_plot(df, "CP_dxd", "DXd Concentration (ng/mL)", "#f59e0b", input$log_scale) }) } shinyApp(ui, server)