use("shiny") use("bslib") use("mrgsolve") use("dplyr") use("ggplot2") model_code <- ' $PARAM @annotated BASECL : 5.57 : Baseline MTX clearance at CLcr 80 mL/min (L/h) CLCR : 80 : Creatinine clearance (mL/min) CLFRAC : 10 : Residual MTX clearance as % of normal after delayed elimination VC : 26.683 : MTX central volume (L) VP : 2.253 : MTX peripheral volume (L) Q : 0.078 : MTX intercompartmental clearance (L/h) CATCLKG : 0.00516 : Glucarpidase clearance per kg (L/h/kg) CATVKG : 0.0679 : Glucarpidase distribution volume per kg (L/kg) WT : 60 : Body weight (kg) KM : 86 : Michaelis-Menten constant for MTX hydrolysis (umol/L) ALPHA : 30 : Reconstructed catalytic conversion constant $CMT @annotated CENT : MTX central compartment (umol) PERIPH : MTX peripheral compartment (umol) CAT : Glucarpidase amount (U) $MAIN // MTX parameters from the manuscript, with delayed elimination captured by CLFRAC // CPG2 disposition from the manuscript phase I data double MTXCLi = BASECL * pow(CLCR / 80.0, 0.112) * (CLFRAC / 100.0); double Kri = MTXCLi / VC; double K12i = Q / VC; double K21i = Q / VP; double CATCLi = CATCLKG * WT; double CATVi = CATVKG * WT; $ODE double CMTX = CENT / VC; double CCAT = CAT / CATVi; double DEGR = ALPHA * CCAT * CMTX / (KM + CMTX); dxdt_CENT = -(Kri + K12i) * CENT + K21i * PERIPH - DEGR; dxdt_PERIPH = K12i * CENT - K21i * PERIPH; dxdt_CAT = -(CATCLi / CATVi) * CAT; $TABLE double MTX = CENT / VC; double CATCONC = CAT / CATVi; double MTX_SAFE = MTX <= 0.1 ? 1 : 0; double MTX_CLINICAL = MTX <= 1.0 ? 1 : 0; $CAPTURE @annotated MTX : Plasma methotrexate concentration (umol/L) CATCONC : Plasma glucarpidase concentration (U/L) MTX_SAFE : Indicator for MTX <= 0.1 umol/L MTX_CLINICAL : Indicator for MTX <= 1.0 umol/L ' mod <- mcode("glucarpidase_pkpd_310", model_code) 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; } .summary-box { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 14px 16px; margin-top: 12px; font-size: 13px; line-height: 1.5; } .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; } ") paper_benchmarks <- tibble::tibble( dose_ukg = c(20, 50), below_01_70 = c("71.8%", "89.6%"), below_01_120 = c("46.4%", "59.0%") ) ui <- page_sidebar( title = "Glucarpidase Rescue PK/PD Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, accordion( open = c("Dosing", "Patient"), accordion_panel( "Dosing", icon = bsicons::bs_icon("capsule"), sliderInput("mtx_dose", "Methotrexate dose (g/m²)", min = 0.5, max = 12, value = 1.0, step = 0.5), sliderInput("mtx_infusion_h", "MTX infusion duration (h)", min = 1, max = 24, value = 4, step = 1), sliderInput("cpg2_dose", "Glucarpidase dose (U/kg)", min = 10, max = 80, value = 50, step = 10), sliderInput("rescue_time", "Glucarpidase rescue start (h after MTX start)", min = 24, max = 72, value = 48, step = 2), sliderInput("cpg2_infusion_min", "Glucarpidase infusion duration (min)", min = 1, max = 30, value = 5, step = 1), sliderInput("sim_end", "Simulation horizon (h)", min = 96, max = 168, value = 144, step = 12) ), accordion_panel( "Patient", icon = bsicons::bs_icon("person"), sliderInput("wt", "Body weight (kg)", min = 10, max = 120, value = 60, step = 1), sliderInput("bsa", "Body surface area (m²)", min = 0.5, max = 2.5, value = 1.73, step = 0.01), sliderInput("clcr", "Creatinine clearance (mL/min)", min = 10, max = 140, value = 80, step = 5), sliderInput("clfrac", "Residual MTX clearance (% of normal)", min = 5, max = 30, value = 10, step = 1), tags$p( style = "font-size:12px; color:#6c757d; margin-top:8px;", "The manuscript simulations assumed a typical 60 kg patient, BSA 1.73 m², creatinine clearance 80 mL/min, and severe delayed MTX elimination approximated here as 10% of normal clearance." ) ), accordion_panel( "Display", icon = bsicons::bs_icon("gear"), checkboxInput("show_clinical_band", "Show 0.1 and 1.0 µmol/L target zones", value = TRUE), checkboxInput("show_cpg2", "Overlay glucarpidase concentration (scaled)", value = FALSE), checkboxInput("log_scale", "Log scale (Y-axis)", value = TRUE) ) ) ), tags$p( style = "font-size: 11px; color: #6c757d; margin: 4px 0 0 6px;", "This app reconstructs the 2023 Anticancer Research dose-finding manuscript using a two-compartment MTX model plus manuscript-derived catalytic glucarpidase rescue dynamics." ), layout_column_wrap( width = 1 / 4, fill = FALSE, div( class = "metric-card metric-success", div(class = "metric-value", textOutput("cmax")), div(class = "metric-label", "MTX Cmax (µmol/L)") ), div( class = "metric-card metric-warning", div(class = "metric-value", textOutput("ctrough")), div(class = "metric-label", "MTX at end of sim (µmol/L)") ), div( class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc")), div(class = "metric-label", "MTX AUC0-end (µmol·h/L)") ), div( class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf")), div(class = "metric-label", "Pre-rescue MTX t½ (h)") ) ), card( card_header("Methotrexate rescue profile"), full_screen = TRUE, plotOutput("pkPlot", height = "500px"), div( class = "summary-box", strong("Model readout"), br(), textOutput("rescueSummary") ) ), navset_card_underline( title = "Drug Information", nav_panel( "Model Information", div( class = "ref-box", tags$h5("Study context"), tags$p( "Kimura et al. developed a modified Michaelis-Menten PK/PD model to support glucarpidase dose selection after high-dose methotrexate therapy. The structural core is a two-compartment MTX model with catalytic glucarpidase-driven MTX hydrolysis added to the central compartment." ), tags$h5("Model elements used here"), tags$ul( tags$li(tags$strong("MTX structure:"), " two-compartment model with manuscript Vc, Vp, and Q values"), tags$li(tags$strong("Renal elimination:"), " BASECL × (CLcr/80)^0.112 with residual clearance fraction to mimic delayed elimination"), tags$li(tags$strong("Glucarpidase PK:"), " one-compartment IV infusion using the manuscript phase I disposition values"), tags$li(tags$strong("Catalytic PD:"), " Michaelis-Menten hydrolysis term with Km = 86 µmol/L and a reconstructed alpha chosen to reproduce the paper's dose-response trend"), tags$li(tags$strong("Default scenario:"), " 1 g/m² MTX over 4 h, rescue glucarpidase at 48 h, 60 kg patient, BSA 1.73 m², severe delayed elimination") ), tags$h5("Published parameters from the manuscript"), tags$table( class = "table table-sm", tags$thead( tags$tr( tags$th("Parameter"), tags$th("Value"), tags$th("Units") ) ), tags$tbody( tags$tr(tags$td("MTX clearance"), tags$td("5.57 × (CLcr/80)^0.112"), tags$td("L/h")), tags$tr(tags$td("MTX Vc"), tags$td("26.9"), tags$td("L")), tags$tr(tags$td("MTX Vp"), tags$td("2.27"), tags$td("L")), tags$tr(tags$td("MTX Q"), tags$td("0.0778"), tags$td("L/h")), tags$tr(tags$td("Glucarpidase CL"), tags$td("0.086"), tags$td("mL/min/kg")), tags$tr(tags$td("Glucarpidase Vdss"), tags$td("67.9"), tags$td("mL/kg")), tags$tr(tags$td("Glucarpidase t½"), tags$td("9.97"), tags$td("h")), tags$tr(tags$td("Km"), tags$td("86"), tags$td("µmol/L")) ) ) ) ), nav_panel( "References", div( class = "ref-box", tags$h5("Primary reference"), tags$ol( tags$li( tags$strong("Kimura T, Fukaya Y, Hamada Y, Yoshimura K, Kawamoto H. (2023). "), "Pharmacokinetics and Pharmacodynamics of Glucarpidase Rescue Treatment After High-dose Methotrexate Therapy Based on Modeling and Simulation. ", tags$em("Anticancer Research"), " 43:1919-1924. ", tags$a(href = "https://doi.org/10.21873/anticanres.16351", target = "_blank", "doi:10.21873/anticanres.16351") ) ), tags$h5("Dose-selection benchmarks reported in the paper"), tags$table( class = "table table-sm", tags$thead( tags$tr( tags$th("Glucarpidase dose"), tags$th("% < 0.1 µmol/L at 70 h"), tags$th("% < 0.1 µmol/L at 120 h") ) ), tags$tbody( lapply(seq_len(nrow(paper_benchmarks)), function(i) { tags$tr( tags$td(paste0(paper_benchmarks$dose_ukg[i], " U/kg")), tags$td(paper_benchmarks$below_01_70[i]), tags$td(paper_benchmarks$below_01_120[i]) ) }) ) ), tags$h5("Therapeutic context"), tags$ul( tags$li(tags$strong("Drug class:"), " Enzyme rescue therapy for methotrexate toxicity"), tags$li(tags$strong("Clinical role:"), " Rapid extracellular hydrolysis of MTX when delayed elimination threatens nephrotoxicity and systemic toxicity"), tags$li(tags$strong("Approved/benchmark dose:"), " 50 U/kg IV"), tags$li(tags$strong("Key clinical message:"), " 50 U/kg improved the probability of achieving MTX < 0.1 µmol/L by 70 h, but rebound remained possible, supporting prolonged monitoring beyond 144 h") ) ) ) ), 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 <- function(input, output, session) { sim_data <- reactive({ shiny::req(input$mtx_dose, input$bsa, input$mtx_infusion_h, input$cpg2_dose, input$rescue_time, input$wt) mtx_dose_umol <- (input$mtx_dose * 1000 * input$bsa) / 0.45444 mtx_rate <- mtx_dose_umol / input$mtx_infusion_h cpg2_dose_total <- input$cpg2_dose * input$wt cpg2_inf_h <- input$cpg2_infusion_min / 60 cpg2_rate <- cpg2_dose_total / cpg2_inf_h ev_mtx <- ev(time = 0, amt = mtx_dose_umol, cmt = 1, rate = mtx_rate) ev_cpg2 <- ev(time = input$rescue_time, amt = cpg2_dose_total, cmt = 3, rate = cpg2_rate) mod %>% param(CLCR = input$clcr, CLFRAC = input$clfrac, WT = input$wt) %>% ev(c(ev_mtx, ev_cpg2)) %>% mrgsim(end = input$sim_end, delta = 0.25) %>% as.data.frame() |> dplyr::arrange(time) |> dplyr::distinct(time, .keep_all = TRUE) |> dplyr::mutate( time_h = time, cat_scaled = ifelse(max(CATCONC, na.rm = TRUE) > 0, CATCONC / max(CATCONC, na.rm = TRUE) * max(MTX, na.rm = TRUE), 0) ) }) rescue_metrics <- reactive({ d <- sim_data() conc_70 <- approx(d$time_h, d$MTX, xout = 70, rule = 2)$y conc_120 <- approx(d$time_h, d$MTX, xout = 120, rule = 2)$y post_rescue <- d |> dplyr::filter(time_h >= input$rescue_time) safe_times <- post_rescue |> dplyr::filter(MTX <= 0.1) time_to_safe <- if (nrow(safe_times) == 0) { NA_real_ } else { min(safe_times$time_h) - input$rescue_time } rebound_ratio <- if (nrow(safe_times) == 0) { NA_real_ } else { nadir_time <- safe_times$time_h[which.min(safe_times$MTX)] nadir_conc <- min(safe_times$MTX) later <- post_rescue |> dplyr::filter(time_h >= nadir_time) if (nrow(later) == 0 || nadir_conc <= 0) { NA_real_ } else { max(later$MTX, na.rm = TRUE) / nadir_conc } } list( conc_70 = conc_70, conc_120 = conc_120, time_to_safe = time_to_safe, rebound_ratio = rebound_ratio ) }) output$cmax <- renderText({ d <- sim_data() sprintf("%.1f", max(d$MTX, na.rm = TRUE)) }) output$ctrough <- renderText({ d <- sim_data() sprintf("%.3f", d$MTX[nrow(d)]) }) output$auc <- renderText({ d <- sim_data() auc <- sum(diff(d$time_h) * (head(d$MTX, -1) + tail(d$MTX, -1)) / 2) sprintf("%.0f", auc) }) output$thalf <- renderText({ mtx_cli <- 5.57 * (input$clcr / 80)^0.112 * (input$clfrac / 100) sprintf("%.1f", log(2) / (mtx_cli / 26.683)) }) output$rescueSummary <- renderText({ m <- rescue_metrics() time_to_safe_txt <- if (is.na(m$time_to_safe)) { "not achieved within the simulation horizon" } else { paste0(sprintf("%.1f", m$time_to_safe), " h after glucarpidase") } rebound_txt <- if (is.na(m$rebound_ratio)) { "not estimable" } else { paste0(sprintf("%.1f", m$rebound_ratio), "× from nadir") } paste0( "Predicted MTX concentration at 70 h: ", sprintf("%.3f", m$conc_70), " µmol/L. ", "Predicted MTX concentration at 120 h: ", sprintf("%.3f", m$conc_120), " µmol/L. ", "Time to MTX ≤ 0.1 µmol/L: ", time_to_safe_txt, ". ", "Post-nadir rebound magnitude: ", rebound_txt, "." ) }) output$pkPlot <- renderPlot({ d <- sim_data() d_plot <- if (input$log_scale) d |> dplyr::filter(MTX > 0.001) else d p <- ggplot(d_plot, aes(x = time_h, y = MTX)) + labs( x = "Time after MTX start (hours)", y = "Plasma methotrexate (µmol/L)", title = paste0("MTX ", input$mtx_dose, " g/m² + glucarpidase ", input$cpg2_dose, " U/kg at ", input$rescue_time, " h") ) + geom_line(color = "#8b5cf6", linewidth = 0.9) + geom_vline(xintercept = input$rescue_time, linetype = "dashed", color = "#ef4444", linewidth = 0.7) + annotate("text", x = input$rescue_time, y = max(d_plot$MTX, na.rm = TRUE), label = "Rescue", vjust = -0.6, color = "#ef4444", size = 3.5) + theme_minimal(base_size = 14) if (input$show_clinical_band) { p <- p + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 0, ymax = 0.1, fill = "#10b981", alpha = 0.12) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 0.1, ymax = 1.0, fill = "#f59e0b", alpha = 0.08) + geom_hline(yintercept = c(0.1, 1.0), linetype = "dashed", color = c("#10b981", "#f59e0b"), alpha = 0.7) } if (input$show_cpg2) { p <- p + geom_line(data = d_plot, aes(x = time_h, y = cat_scaled), color = "#0ea5e9", linewidth = 0.8, alpha = 0.8) + annotate("text", x = max(d_plot$time_h) * 0.78, y = max(d_plot$cat_scaled, na.rm = TRUE), label = "Scaled glucarpidase", color = "#0ea5e9", hjust = 0, size = 3.4) } if (input$log_scale) { p <- p + scale_y_log10() } p }) } shinyApp(ui = ui, server = server)