library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ─── mrgsolve 3-Compartment IV PK + Emax PD Model ──────────────────────────── # Source: Wang et al. (2015) Clin Pharmacol Drug Dev 4(2):89-98 # CL, V1 from Table 1 (original dataset); V2, Q2, V3, Q3 estimated from # broader rucaparib literature (Clancy et al. 2022, PMC9652254). # PD parameters (IC50, Imax, E0) from Table 2. model_code <- ' $PARAM @annotated CL : 17.5 : Clearance (L/hr) V1 : 15.5 : Central volume (L) V2 : 60.0 : Fast peripheral volume (L) Q2 : 30.0 : Fast intercompartmental clearance (L/hr) V3 : 75.0 : Slow peripheral volume (L) Q3 : 3.0 : Slow intercompartmental clearance (L/hr) IC50 : 0.00105 : PARP IC50 (mg/L; equiv. 1.05 ng/mL) IMAX : 0.909 : Maximum PARP inhibition fraction (90.9%) E0 : 90.8 : Baseline PARP activity (pmol/million PBLs) $CMT @annotated CENT : Central compartment (mg) PERI1 : Fast peripheral compartment (mg) PERI2 : Slow peripheral compartment (mg) $ODE double k10 = CL / V1; double k12 = Q2 / V1; double k21 = Q2 / V2; double k13 = Q3 / V1; double k31 = Q3 / V3; dxdt_CENT = -(k10 + k12 + k13)*CENT + k21*PERI1 + k31*PERI2; dxdt_PERI1 = k12*CENT - k21*PERI1; dxdt_PERI2 = k13*CENT - k31*PERI2; $TABLE double CP = CENT / V1; double CP_ng = CP * 1000.0; double PARP_inh_pct = 100.0 * IMAX * CP / (IC50 + CP); double PARP_act = E0 * (1.0 - IMAX * CP / (IC50 + CP)); $CAPTURE CP CP_ng PARP_inh_pct PARP_act ' mod <- mcode("rucaparib_3cmt", 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; } .dose-note { font-size: 11px; color: #6c757d; margin-top: -8px; } ") # ─── UI ────────────────────────────────────────────────────────────────────── ui <- page_sidebar( title = "Rucaparib (AG-014699) PK/PD Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 330, h6("IV Dosing (30-min Infusion)"), sliderInput("dose_pm2", "Dose (mg/m²)", min = 1, max = 18, value = 12, step = 1), sliderInput("bsa", "Body Surface Area (m²)", min = 1.3, max = 2.3, value = 1.73, step = 0.01), p(class = "dose-note", "Actual dose: ", strong(textOutput("actual_dose_txt", inline = TRUE)), " mg"), numericInput("n_days", "Treatment Duration (days)", value = 5, min = 1, max = 14), p(class = "dose-note", "Study design: daily 30-min IV + temozolomide × 5 days/cycle"), hr(), h6("Display"), checkboxInput("log_scale", "Log Scale (Y-axis, PK plot)", value = FALSE), hr(), div(style = "font-size: 11px; color: #888;", strong("Model:"), " 3-CMT IV | Wang et al. 2015", br(), strong("CL:"), " 17.5 L/hr | ", strong("V1:"), " 15.5 L", br(), strong("IC50:"), " 1.05 ng/mL | ", strong("Imax:"), " 90.9%") ), # Metric cards 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 (ng/mL)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("ctrough")), div(class = "metric-label", "Ctrough at 24h (ng/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc")), div(class = "metric-label", "AUC₀₋₂₄ (ng·h/mL)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf")), div(class = "metric-label", "Terminal t½ (hr)")) ), # Tabs navset_card_underline( nav_panel("PK Profile", card(full_screen = TRUE, height = "500px", plotOutput("pkPlot", height = "460px")) ), nav_panel("PARP Inhibition", card(full_screen = TRUE, height = "500px", plotOutput("pdPlot", height = "460px")) ), nav_panel("Model Information", markdown(" ## Rucaparib (AG-014699) — Population PK/PD Model **Drug:** Rucaparib (formerly AG-014699, PF-01367338) — potent PARP1/2/3 inhibitor **Model:** 3-compartment IV infusion + direct Emax PARP inhibition **Population:** 32 patients with solid tumors (Phase 1 Study A4991002) **Software:** NONMEM 7.1.2 (FOCE method) --- ### PK Parameters (Table 1, Wang et al. 2015) | Parameter | Estimate | %RSE | Bootstrap Mean | Bootstrap %RSE | |-----------|----------|------|----------------|----------------| | CL (L/hr) | 17.5 | 6.57 | 17.3 | 11.5 | | V1 (L) | 15.5 | 11.6 | 15.5 | 13.4 | | IIV CL (%CV) | 51.2 | 32.8 | 51.1 | 32.0 | | IIV V1 (%CV) | 57.3 | 36.3 | 56.4 | 42.2 | | IOV CL (%CV) | 25.5 | 32.6 | 25.5 | 31.2 | | IOV Q2 (%CV) | 32.9 | 60.9 | 33.2 | 61.4 | > **Note:** V2, Q2, V3, Q3 are estimated from the broader rucaparib literature > (Clancy et al. 2022), as Supplementary Table 1 was unavailable. V1 and CL are > directly from Wang et al. (2015) Table 1. --- ### PD Parameters (Table 2, Wang et al. 2015) — PARP Inhibition in PBLs | Parameter | Estimate | %RSE | Description | |-----------|----------|------|-------------| | E₀ (pmol/10⁶ PBL) | 90.8 | 19.7 | Baseline PARP activity | | IC₅₀ (ng/mL) | 1.05 | 24.0 | Conc. for 50% of Imax PARP inhibition | | Imax | 90.9% | — | Maximum achievable PARP inhibition | | IIV E₀ (%CV) | 116 | 20.2 | High inter-individual variability in baseline | | IIV IC₅₀ (%CV) | 61.7 | 103 | — | --- ### Key Clinical Findings - Rucaparib PK is **linear** across the dose range studied (1–18 mg/m²) - **PID (PARP Inhibitory Dose) = 12 mg/m²/day** → ≥50% PARP inhibition at 24h - **No significant covariates** found (weight, age, renal function, sex) - PARP inhibition in PBLs is a **validated surrogate** for tumor PARP inhibition: IC₅₀,PBL = 1.05 ng/mL vs. IC₅₀,tumor = 1.10 ng/mL (not significantly different) - To maintain **>80% PARP inhibition** throughout 24h: requires ≥24 mg/day --- ### Model Equations **PK (3-CMT IV infusion):** dC₁/dt = -(k₁₀+k₁₂+k₁₃)·C₁ + k₂₁·C₂ + k₃₁·C₃ **PD (Emax inhibitory):** PARP inhibition (%) = 100 × Imax × Cp / (IC₅₀ + Cp) ")), nav_panel("References", div(class = "ref-box", tags$h5("📚 Key References"), tags$ol( tags$li( tags$strong("Wang D et al. (2015)"), " Population Pharmacokinetics and Pharmacodynamics of AG-014699, ", "a PARP Inhibitor, in Patients with Solid Tumors. ", tags$em("Clin Pharmacol Drug Dev"), " 4(2):89-98. ", tags$a("DOI: 10.1002/cpdd.154", href = "https://doi.org/10.1002/cpdd.154", target = "_blank")), tags$li( tags$strong("Clancy AJ et al. (2022)"), " Clinical Pharmacokinetics and Pharmacodynamics of Rucaparib. ", tags$em("Clin Pharmacokinet"), " 61(11):1489-1499. ", tags$a("PMC9652254", href = "https://pmc.ncbi.nlm.nih.gov/articles/PMC9652254/", target = "_blank")), tags$li( tags$strong("Plummer R et al. (2008)"), " First-in-Human Phase I Clinical Trial of AG-014699 (PF-01367338), ", "an Inhibitor of Poly(ADP-Ribose) Polymerase. ", tags$em("Clin Cancer Res"), " 14:7476-7482.") ), tags$h5("💊 Drug Profile"), tags$ul( tags$li(tags$strong("Brand name:"), " Rubraca® (voluntary US withdrawal 2022; EU approved)"), tags$li(tags$strong("Class:"), " PARP1/2/3 inhibitor (oncology)"), tags$li(tags$strong("Approved oral dose:"), " 600 mg BID; F = 36%; t½ ≈ 17.7 h"), tags$li(tags$strong("Indications:"), " Recurrent BRCA-mutated ovarian cancer; mCRPC"), tags$li(tags$strong("Mechanism:"), " Synthetic lethality in HR-deficient (BRCA-mutated) tumors"), tags$li(tags$strong("Biomarker:"), " PARP inhibition in PBLs/PBMCs (IC₅₀ ~1 ng/mL)") ) ) ) ), # 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"), " • Built by Sunny ☀️ (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) { output$actual_dose_txt <- renderText({ sprintf("%.1f", input$dose_pm2 * input$bsa) }) sim_data <- reactive({ shiny::req(input$dose_pm2, input$bsa, input$n_days) dose_mg <- input$dose_pm2 * input$bsa inf_rate <- dose_mg / 0.5 # mg/hr for 30-min infusion n_add <- max(0L, as.integer(input$n_days) - 1L) ev_obj <- ev(amt = dose_mg, rate = inf_rate, cmt = 1, ii = 24, addl = n_add) mod |> ev(ev_obj) |> mrgsim(end = as.numeric(input$n_days) * 24, delta = 0.25) |> as.data.frame() |> dplyr::filter(CP_ng >= 0) }) output$cmax <- renderText({ d <- sim_data() first <- d |> dplyr::filter(time <= 24) sprintf("%.0f", max(first$CP_ng, na.rm = TRUE)) }) output$ctrough <- renderText({ d <- sim_data() row24 <- d |> dplyr::filter(abs(time - 24) < 0.4) if (nrow(row24) == 0) return("—") sprintf("%.1f", min(row24$CP_ng, na.rm = TRUE)) }) output$auc <- renderText({ d <- sim_data() day1 <- d |> dplyr::filter(time >= 0, time <= 24) if (nrow(day1) < 2) return("—") auc <- sum(diff(day1$time) * (head(day1$CP_ng, -1) + tail(day1$CP_ng, -1)) / 2) sprintf("%.0f", auc) }) output$thalf <- renderText({ d <- sim_data() t_end <- max(d$time) terminal <- d |> dplyr::filter(time > t_end - 24, CP_ng > 0.01) if (nrow(terminal) < 5) return("—") fit <- lm(log(CP_ng) ~ time, data = terminal) lambda <- -stats::coef(fit)[["time"]] if (!is.finite(lambda) || lambda <= 0) return("—") sprintf("%.1f", log(2) / lambda) }) output$pkPlot <- renderPlot({ d <- sim_data() dose_mg <- input$dose_pm2 * input$bsa ic50_ng <- 1.05 # ng/mL for annotation p <- ggplot(d, aes(x = time, y = CP_ng)) + geom_hline(yintercept = ic50_ng, linetype = "dashed", color = "#e74c3c", linewidth = 0.8) + annotate("text", x = max(d$time) * 0.02, y = ic50_ng * 1.6, label = sprintf("IC\u2085\u2080 = %.2f ng/mL", ic50_ng), hjust = 0, size = 3.5, color = "#e74c3c") + geom_line(color = "#8b5cf6", linewidth = 1.0) + labs( x = "Time (hours)", y = "Rucaparib Plasma Concentration (ng/mL)", title = sprintf("Rucaparib %.1f mg/m\u00b2 \u00d7 %.2f m\u00b2 BSA = %.1f mg IV (30 min), QD", input$dose_pm2, input$bsa, dose_mg), subtitle = "3-Compartment IV Population PK Model \u2014 Wang et al. (2015)" ) + theme_minimal(base_size = 14) + theme(plot.title = element_text(size = 13), plot.subtitle = element_text(size = 11, color = "#666")) if (input$log_scale) { p <- p + scale_y_log10(labels = scales::label_comma()) + annotation_logticks(sides = "l") } p }) output$pdPlot <- renderPlot({ d <- sim_data() # Threshold concentrations: 50% inhibition → C at 50% of Imax # 80% absolute inhibition: 80 = 90.9 * C/(IC50+C)*100 # → C = IC50 * 80 / (90.9 - 80) = 0.00105 * 80 / 10.9 ≈ 0.0077 mg/L ≈ 7.7 ng/mL thresh_80 <- 80 ggplot(d, aes(x = time, y = PARP_inh_pct)) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = thresh_80, ymax = 100, fill = "#10b981", alpha = 0.12) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 45, ymax = thresh_80, fill = "#f59e0b", alpha = 0.08) + geom_hline(yintercept = thresh_80, linetype = "dashed", color = "#10b981", linewidth = 0.8) + geom_hline(yintercept = 45.45, linetype = "dashed", color = "#e74c3c", linewidth = 0.7) + annotate("text", x = max(d$time) * 0.02, y = thresh_80 + 1.5, label = "\u226580% inhibition (target)", hjust = 0, size = 3.5, color = "#10b981") + annotate("text", x = max(d$time) * 0.02, y = 47, label = "50% of Imax (at IC\u2085\u2080 = 1.05 ng/mL)", hjust = 0, size = 3.5, color = "#e74c3c") + geom_line(color = "#8b5cf6", linewidth = 1.0) + scale_y_continuous(limits = c(0, 100), breaks = seq(0, 100, 20), labels = function(x) paste0(x, "%")) + labs( x = "Time (hours)", y = "PARP Inhibition in PBLs (%)", title = "PARP Inhibitory Activity Profile", subtitle = "Emax model: IC\u2085\u2080 = 1.05 ng/mL, Imax = 90.9% \u2014 Wang et al. (2015)" ) + theme_minimal(base_size = 14) + theme(plot.title = element_text(size = 13), plot.subtitle = element_text(size = 11, color = "#666")) }) } shinyApp(ui = ui, server = server)