library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ── Rucaparib Clinical PK Model (2-compartment oral) ─────────────────────── pk_code <- ' $PARAM @annotated CL : 37.6 : Apparent oral clearance CL/F (L/h) V1 : 310 : Central volume V1/F (L) V2 : 230 : Peripheral volume V2/F (L) Q : 16 : Inter-compartmental clearance Q/F (L/h) KA : 1.15 : Absorption rate constant (1/h) WT : 70 : Body weight (kg) HEPF : 1.0 : Hepatic function (fraction of normal) $CMT @annotated DEPOT : Oral depot (mg) CENT : Central compartment (mg) PERI : Peripheral compartment (mg) $MAIN double CLi = CL * pow(WT/70.0, 0.75) * HEPF; double V1i = V1 * (WT/70.0); double V2i = V2 * (WT/70.0); double Qi = Q * pow(WT/70.0, 0.75); $ODE dxdt_DEPOT = -KA * DEPOT; dxdt_CENT = KA * DEPOT - (CLi/V1i)*CENT - (Qi/V1i)*CENT + (Qi/V2i)*PERI; dxdt_PERI = (Qi/V1i)*CENT - (Qi/V2i)*PERI; $TABLE double CP = CENT / V1i; // mg/L double CPng = CP * 1000.0; // ng/mL $CAPTURE CP CPng ' # ── DDR-based Tumor Growth Inhibition Model (simplified from paper) ──────── # Uses Gompertz growth + PK-driven kill with HR deficiency modulating sensitivity # Based on the DDR framework from Villette et al. (2025) Br J Cancer tgi_code <- ' $PARAM @annotated // Mouse PK (1-comp oral, rucaparib in mouse) CLm : 3.0 : Mouse apparent clearance (L/h/kg) Vm : 2.5 : Mouse apparent volume (L/kg) KAm : 2.0 : Mouse absorption rate (1/h) // Drug effect params (calibrated to paper behavior) kmax : 0.015 : Maximum kill rate constant (1/h) KC50 : 5.0 : Concentration for half-max kill (mg/L) sens : 4.0 : HR deficiency sensitivity amplifier (fold) ip_fork : 0.5 : PARPi fork trapping potency (/(mg/L)/h) clpf : 0.45 : Clearance of fork trapping effect (1/h) // Tumor biology lambda : 0.004 : Gompertz growth rate (1/h) TVmax : 3.0 : Carrying capacity (cm3) def4 : 0.2 : HR repair deficiency pathway 4 (0-1) TV0 : 0.1 : Initial tumor volume (cm3) $CMT @annotated GDEPOT : Drug depot (mg/kg) GCENT : Drug central (mg/kg) EFF_FORK : PARPi fork trapping effect TV : Tumor volume (cm3) $MAIN if(NEWIND <= 1) { TV_0 = TV0; } $ODE // Mouse PK (1-comp oral) dxdt_GDEPOT = -KAm * GDEPOT; dxdt_GCENT = KAm * GDEPOT - (CLm/Vm) * GCENT; double Cplasma = GCENT / Vm; if(Cplasma < 0) Cplasma = 0; // Fork trapping effect (indirect response, Eq 8 from paper) dxdt_EFF_FORK = ip_fork * Cplasma - clpf * EFF_FORK; // PK-driven kill rate with HR deficiency sensitization // Synthetic lethality: def4 amplifies drug kill (BRCA-mut tumors are more sensitive) // Fork trapping adds to effective kill double drug_kill = kmax * Cplasma / (KC50 + Cplasma) * (1.0 + sens * def4) * (1.0 + 0.5 * EFF_FORK); // Gompertz tumor growth - drug kill double growth = 0.0; if(TV > 1e-6 && TV < TVmax) { growth = lambda * TV * log(TVmax / TV); } else if(TV >= TVmax) { growth = 0.0; } dxdt_TV = growth - drug_kill * TV; // Floor at tiny value if(TV < 1e-6 && dxdt_TV < 0) dxdt_TV = 0; $TABLE double TVout = TV; double Cpl = GCENT / Vm; $CAPTURE TVout Cpl ' mod_pk <- mcode("rucaparib_pk", pk_code) mod_tgi <- mcode("rucaparib_tgi", tgi_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 = "Rucaparib PK/PD Simulator — PARP Inhibitor", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 360, conditionalPanel( condition = "input.tabs == 'Clinical PK'", h6("Dosing Regimen"), sliderInput("dose_pk", "Dose (mg)", min = 200, max = 800, value = 600, step = 100), radioButtons("regimen", "Regimen", choices = c("BID (twice daily)" = 12, "QD (once daily)" = 24), selected = 12, inline = TRUE), numericInput("n_days_pk", "Duration (days)", value = 14, min = 1, max = 28), hr(), h6("Patient Characteristics"), sliderInput("wt", "Weight (kg)", min = 40, max = 120, value = 70, step = 5), sliderInput("hepf", "Hepatic Function", min = 0.3, max = 1.0, value = 1.0, step = 0.1), hr(), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE), checkboxInput("show_target", "Show Target Ctrough (1500 ng/mL)", value = TRUE) ), conditionalPanel( condition = "input.tabs == 'Tumor Growth Inhibition'", h6("Rucaparib Dosing (Preclinical)"), sliderInput("dose_tgi", "Rucaparib Dose (mg/kg)", min = 0, max = 150, value = 50, step = 10), radioButtons("schedule_tgi", "Schedule", choices = c("QD (daily)" = 24, "BID (twice daily)" = 12), selected = 24, inline = TRUE), numericInput("n_days_tgi", "Treatment Duration (days)", value = 35, min = 7, max = 56), hr(), h6("Tumor Characteristics"), sliderInput("def4_tgi", "HR Deficiency (def4)", min = 0, max = 1.0, value = 0.2, step = 0.05, post = ""), helpText(style = "font-size:11px; margin-top:-8px;", "0% = HR proficient (HRD−) | 20-30% = HRD+ | 80-100% = BRCA mutant"), sliderInput("tv0_tgi", "Initial Tumor Volume (cm³)", min = 0.05, max = 0.3, value = 0.1, step = 0.01), hr(), checkboxInput("show_vehicle", "Show Vehicle (Untreated)", value = TRUE) ) ), navset_card_tab( id = "tabs", title = "Rucaparib PK/PD", full_screen = TRUE, # ── Tab 1: Clinical PK ────────────────────────────────────────────── nav_panel("Clinical PK", layout_column_wrap( width = 1/4, fill = FALSE, div(class = "metric-card metric-success", div(class = "metric-value", textOutput("cmax")), div(class = "metric-label", "Cmax,ss (ng/mL)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("ctrough")), div(class = "metric-label", "Ctrough,ss (ng/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc")), div(class = "metric-label", HTML("AUC0-τ (μg·h/mL)"))), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf")), div(class = "metric-label", "t½ (h)")) ), plotOutput("pkPlot", height = "500px") ), # ── Tab 2: Tumor Growth Inhibition ────────────────────────────────── nav_panel("Tumor Growth Inhibition", layout_column_wrap( width = 1/4, fill = FALSE, div(class = "metric-card metric-success", div(class = "metric-value", textOutput("tgi_percent")), div(class = "metric-label", "TGI (%)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("tv_final")), div(class = "metric-label", "Tumor Vol at EOT (cm³)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("tv_vehicle")), div(class = "metric-label", "Vehicle Vol at EOT (cm³)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("hrd_status")), div(class = "metric-label", "HRD Status")) ), plotOutput("tgiPlot", height = "500px") ), # ── Tab 3: Model Information ──────────────────────────────────────── nav_panel("Model Information", markdown(" ## Rucaparib — Semi-Mechanistic PARP Inhibitor PK/PD Model ### Clinical Pharmacokinetics **Structure:** Two-compartment model with first-order oral absorption and first-order elimination **CL/F:** 37.6 L/h (apparent oral clearance, allometric scaling on weight) **V1/F:** 310 L (central volume) **V2/F:** 230 L (peripheral volume) **Q/F:** 16 L/h (inter-compartmental clearance) **Ka:** 1.15 h⁻¹ **Bioavailability:** 36% (absolute, range 30–45%) **t½:** ~17 h (terminal half-life) **Tmax:** ~1.9 h (median) **Steady state:** ~7 days with BID dosing ### Mechanism of Action: DNA Damage Response (DDR) Model Rucaparib inhibits PARP enzymes, which play a key role in: - **SSB repair** — blocking single-strand break repair pathway 1 - **DSB repair** — impairing double-strand break repair pathway 3 - **PARP trapping** — accelerating conversion of SSBs to DSBs at replication forks #### Synthetic Lethality In tumors with **HR deficiency** (e.g., BRCA mutation), the alternative DSB repair pathway (pathway 4) is already impaired. When PARP inhibitor blocks pathway 3, both DSB repair pathways are compromised → DSB accumulation → cell death. #### Key DDR Model Equations - SSB dynamics: dSSB/dt = r_ssb − R_conv·SSB − R_ssb·SSB - DSB dynamics: dDSB/dt = r_dsb + R_conv·SSB − R_dsb·DSB - Drug effect: dX/dt = u·C_drug − Cl_x·X (indirect response) - Tumor growth: dTV/dt = k_g·TV − k_kill·DSB·TV ### Therapeutic Context - **Approved dose:** 600 mg BID (oral tablets) - **Indications:** Recurrent ovarian cancer (maintenance & treatment), mCRPC with BRCA mutation - **Target Ctrough:** ≥1500 ng/mL associated with clinical efficacy - **Metabolism:** CYP2D6 (primary), CYP1A2 and CYP3A4 (minor) - **Key toxicities:** Fatigue, nausea, anemia, thrombocytopenia ")), # ── Tab 4: References ─────────────────────────────────────────────── nav_panel("References", div(class = "ref-box", tags$h5(HTML("📚 Key References")), tags$ol( tags$li(HTML("Villette CC et al. (2025) Semi-mechanistic efficacy model for PARP + ATR inhibitors — application to rucaparib and talazoparib in combination with gartisertib in breast cancer PDXs. Br J Cancer 132:481–491. ", tags$a(href = "https://doi.org/10.1038/s41416-024-02935-w", target = "_blank", "doi:10.1038/s41416-024-02935-w"))), tags$li(HTML("Liao M et al. (2022) Clinical Pharmacokinetics and Pharmacodynamics of Rucaparib. Clin Pharmacokinet 61:1477–1493. ", tags$a(href = "https://doi.org/10.1007/s40262-022-01157-8", target = "_blank", "doi:10.1007/s40262-022-01157-8"))), tags$li(HTML("Xiao JJ et al. (2022) Population pharmacokinetics of rucaparib in patients with advanced ovarian cancer or other solid tumors. Cancer Chemother Pharmacol 90:55–65. ", tags$a(href = "https://doi.org/10.1007/s00280-022-04413-7", target = "_blank", "doi:10.1007/s00280-022-04413-7"))), tags$li(HTML("Shapiro GI et al. (2019) Pharmacokinetic study of rucaparib in patients with advanced solid tumors. Clin Pharmacol Drug Dev 8(1):107–118. ", tags$a(href = "https://doi.org/10.1002/cpdd.575", target = "_blank", "doi:10.1002/cpdd.575"))) ), tags$h5(HTML("💊 Therapeutic Context")), tags$ul( tags$li(tags$strong("Class:"), " PARP1/2/3 inhibitor (DDR-targeted agent)"), tags$li(tags$strong("Brand name:"), " Rubraca"), tags$li(tags$strong("Approved dose:"), " 600 mg BID, continuous dosing"), tags$li(tags$strong("Route:"), " Oral (tablet)"), tags$li(tags$strong("Indications:"), " Recurrent ovarian cancer (maintenance/treatment), mCRPC (BRCA-mutant)"), tags$li(tags$strong("Biomarkers:"), " BRCA1/2 mutation, HRD status, PAR levels"), tags$li(tags$strong("Metabolism:"), " CYP2D6 (primary), with CYP1A2/3A4 (minor); inhibits CYP1A2, MATE1/2-K, OCT1/2") ) )) ), # ── 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 Built by Sunny \u2600\uFE0F (Husain Attarwala's AI Assistant)", tags$br(), tags$span(style = "font-size: 10px;", "For research and educational purposes only. Not for clinical decision-making.")) ) # ── Server ───────────────────────────────────────────────────────────────── server <- function(input, output, session) { # ── Clinical PK reactive ─────────────────────────────────────────────── pk_data <- reactive({ tau <- as.numeric(input$regimen) n_doses <- ceiling(input$n_days_pk * 24 / tau) ev1 <- ev(amt = input$dose_pk, cmt = 1, ii = tau, addl = n_doses - 1) mod_pk %>% param(WT = input$wt, HEPF = input$hepf) %>% ev(ev1) %>% mrgsim(end = input$n_days_pk * 24, delta = 0.25) %>% as.data.frame() %>% mutate(time_h = time, time_d = time / 24) }) # ── PK metrics ───────────────────────────────────────────────────────── output$cmax <- renderText({ d <- pk_data() tau <- as.numeric(input$regimen) last_start <- (input$n_days_pk - 1) * 24 last <- d %>% filter(time >= last_start) sprintf("%.0f", max(last$CPng, na.rm = TRUE)) }) output$ctrough <- renderText({ d <- pk_data() tau <- as.numeric(input$regimen) last_start <- (input$n_days_pk - 1) * 24 last <- d %>% filter(time >= last_start) sprintf("%.0f", min(last$CPng[last$CPng > 0], na.rm = TRUE)) }) output$auc <- renderText({ d <- pk_data() tau <- as.numeric(input$regimen) last_start <- (input$n_days_pk - 1) * 24 last <- d %>% filter(time >= last_start, time <= last_start + tau) if (nrow(last) < 2) return("—") auc <- sum(diff(last$time) * (head(last$CP, -1) + tail(last$CP, -1)) / 2) sprintf("%.1f", auc) }) output$thalf <- renderText({ CLi <- 37.6 * (input$wt / 70)^0.75 * input$hepf V1i <- 130 * (input$wt / 70) V2i <- 250 * (input$wt / 70) Qi <- 15 * (input$wt / 70)^0.75 ab_sum <- CLi/V1i + Qi/V1i + Qi/V2i ab_prod <- CLi * Qi / (V1i * V2i) beta_val <- (ab_sum - sqrt(ab_sum^2 - 4 * ab_prod)) / 2 sprintf("%.1f", log(2) / beta_val) }) # ── PK Plot ──────────────────────────────────────────────────────────── output$pkPlot <- renderPlot({ d <- pk_data() tau <- as.numeric(input$regimen) reg_label <- if (tau == 12) "BID" else "QD" p <- ggplot(d, aes(x = time_d, y = CPng)) + geom_line(color = "#8b5cf6", linewidth = 0.9) if (input$show_target) { p <- p + geom_hline(yintercept = 1500, linetype = "dashed", color = "#10b981", alpha = 0.7) + annotate("text", x = max(d$time_d) * 0.02, y = 1600, label = "Target Ctrough: 1500 ng/mL", hjust = 0, size = 3.5, color = "#10b981") } p <- p + labs( x = "Time (days)", y = "Plasma Concentration (ng/mL)", title = paste0("Rucaparib ", input$dose_pk, " mg ", reg_label, " | Wt: ", input$wt, " kg | Hepatic fn: ", input$hepf * 100, "%") ) + theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold", size = 13)) if (input$log_scale) p <- p + scale_y_log10() p }) # ── TGI reactive ────────────────────────────────────────────────────── tgi_data <- reactive({ tau <- as.numeric(input$schedule_tgi) n_doses <- ceiling(input$n_days_tgi * 24 / tau) total_h <- (input$n_days_tgi + 14) * 24 # simulate 2 weeks beyond treatment # Treated ev_treat <- ev(amt = input$dose_tgi, cmt = 1, ii = tau, addl = n_doses - 1) sim_treat <- mod_tgi %>% param(def4 = input$def4_tgi, TV0 = input$tv0_tgi) %>% ev(ev_treat) %>% mrgsim(end = total_h, delta = 2) %>% as.data.frame() %>% mutate(time_d = time / 24, group = "Rucaparib") # Vehicle (no treatment) ev_veh <- ev(amt = 0, cmt = 1) sim_veh <- mod_tgi %>% param(def4 = input$def4_tgi, TV0 = input$tv0_tgi) %>% ev(ev_veh) %>% mrgsim(end = total_h, delta = 2) %>% as.data.frame() %>% mutate(time_d = time / 24, group = "Vehicle") list(treat = sim_treat, vehicle = sim_veh) }) # ── TGI metrics ──────────────────────────────────────────────────────── output$tgi_percent <- renderText({ d <- tgi_data() eot <- input$n_days_tgi tv_treat <- d$treat %>% filter(abs(time_d - eot) < 0.5) %>% pull(TVout) %>% tail(1) tv_veh <- d$vehicle %>% filter(abs(time_d - eot) < 0.5) %>% pull(TVout) %>% tail(1) if (length(tv_veh) == 0 || tv_veh <= 0) return("\u2014") tgi <- (1 - tv_treat / tv_veh) * 100 if (tgi < 0) tgi <- 0 sprintf("%.0f%%", tgi) }) output$tv_final <- renderText({ d <- tgi_data() eot <- input$n_days_tgi tv <- d$treat %>% filter(abs(time_d - eot) < 0.5) %>% pull(TVout) %>% tail(1) if (length(tv) == 0) return("\u2014") sprintf("%.2f", tv) }) output$tv_vehicle <- renderText({ d <- tgi_data() eot <- input$n_days_tgi tv <- d$vehicle %>% filter(abs(time_d - eot) < 0.5) %>% pull(TVout) %>% tail(1) if (length(tv) == 0) return("\u2014") sprintf("%.2f", tv) }) output$hrd_status <- renderText({ d4 <- input$def4_tgi if (d4 >= 0.7) return("BRCA-mutant") if (d4 >= 0.2) return("HRD+") return("HRD\u2212") }) # ── TGI Plot ─────────────────────────────────────────────────────────── output$tgiPlot <- renderPlot({ d <- tgi_data() eot <- input$n_days_tgi tau <- as.numeric(input$schedule_tgi) sched_label <- if (tau == 12) "BID" else "QD" def_label <- paste0("def4=", round(input$def4_tgi * 100), "%") plot_data <- d$treat if (input$show_vehicle) { plot_data <- bind_rows(d$treat, d$vehicle) } colors <- c("Rucaparib" = "#8b5cf6", "Vehicle" = "#95a5a6") p <- ggplot(plot_data, aes(x = time_d, y = TVout, color = group)) + geom_vline(xintercept = eot, linetype = "dotted", color = "gray50") + annotate("text", x = eot + 0.5, y = max(plot_data$TVout, na.rm = TRUE) * 0.95, label = "End of\ntreatment", size = 3, hjust = 0, color = "gray50") + geom_line(linewidth = 1) + scale_color_manual(values = colors) + labs( x = "Time (days)", y = expression(Tumor~Volume~(cm^3)), title = paste0("Rucaparib ", input$dose_tgi, " mg/kg ", sched_label, " | HRD: ", def_label, " | Tdoub: ", input$tdoub_tgi, "h"), color = NULL ) + theme_minimal(base_size = 14) + theme( plot.title = element_text(face = "bold", size = 13), legend.position = "top" ) p }) } shinyApp(ui = ui, server = server)