library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ============================================================================= # High-Dose Methotrexate 3-Compartment PopPK Model # Based on: Taylor et al. (2020) Clin Pharmacol Ther 108(3):635-643 # MTXPK.org — Clinical Decision Support Tool for HD-MTX # Population: Pediatric ALL (772 patients, 31,672 concentrations) # ============================================================================= model_code <- ' $PARAM @annotated CL_pop : 11 : Population clearance (L/h/1.73m2) V1_pop : 16.5 : Central volume (L/1.73m2) Q2_pop : 0.602 : Inter-compartmental CL2 (L/h/1.73m2) V2_pop : 4.55 : Peripheral volume 2 (L/1.73m2) Q3_pop : 0.111 : Inter-compartmental CL3 (L/h/1.73m2) V3_pop : 13.1 : Peripheral volume 3 (L/1.73m2) SCR_PWR : -0.247 : Serum creatinine power covariate BSA : 1.73 : Body surface area (m2) SCR : 29 : Serum creatinine (umol/L) $CMT @annotated CENT : Central compartment (mg) PERI2 : Vascular peripheral (mg) PERI3 : Non-vascular peripheral (mg) $MAIN // BSA-based allometric scaling (normalized to 1.73 m2) double BSA_ratio = BSA / 1.73; double SCR_ratio = SCR / 29.0; double CLi = CL_pop * BSA_ratio * pow(SCR_ratio, SCR_PWR); double V1i = V1_pop * BSA_ratio; double Q2i = Q2_pop * BSA_ratio; double V2i = V2_pop * BSA_ratio; double Q3i = Q3_pop * BSA_ratio; double V3i = V3_pop * BSA_ratio; $ODE double CP1 = CENT / V1i; double CP2 = PERI2 / V2i; double CP3 = PERI3 / V3i; dxdt_CENT = -CLi * CP1 - Q2i * (CP1 - CP2) - Q3i * (CP1 - CP3); dxdt_PERI2 = Q2i * (CP1 - CP2); dxdt_PERI3 = Q3i * (CP1 - CP3); $TABLE double CP_mgL = CENT / V1i; double CP_umol = CP_mgL / 0.45444; // MW of MTX = 454.44 g/mol $CAPTURE CP_umol CP_mgL ' mod <- mcode("hdmtx_taylor2020", 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; } .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 <- page_sidebar( title = "High-Dose Methotrexate PK Simulator — MTXPK.org Model", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Dosing"), sliderInput("dose_gm2", "Dose (g/m²)", min = 1, max = 12, value = 5, step = 0.5), selectInput("infusion_dur", "Infusion Duration (hours)", choices = c("3" = 3, "4" = 4, "6" = 6, "24" = 24), selected = 24), numericInput("n_courses", "Number of Courses", value = 1, min = 1, max = 3), sliderInput("course_interval", "Course Interval (days)", min = 7, max = 28, value = 14, step = 1), hr(), h6("Patient Characteristics"), sliderInput("bsa", "Body Surface Area (m²)", min = 0.4, max = 2.3, value = 0.745, step = 0.05), sliderInput("scr", "Serum Creatinine (µmol/L)", min = 5, max = 150, value = 29, step = 1), hr(), checkboxInput("log_scale", "Log Scale (Y-axis)", value = TRUE), checkboxInput("show_thresholds", "Show Clinical Thresholds", value = TRUE) ), navset_card_tab( title = "High-Dose Methotrexate 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("cmax")), div(class="metric-label", "Cmax (µmol/L)")), div(class="metric-card metric-warning", div(class="metric-value", textOutput("c48h")), div(class="metric-label", "C at 48h (µmol/L)")), div(class="metric-card metric-primary", div(class="metric-value", textOutput("auc")), div(class="metric-label", "AUC₀₋ₜ (µmol·h/L)")), div(class="metric-card metric-info", div(class="metric-value", textOutput("t_discharge")), div(class="metric-label", "Time to ≤0.2 µmol/L (h)")) ), plotOutput("pkPlot", height = "500px"), div(style = "margin-top: 10px; padding: 10px; background: #fff3cd; border-radius: 6px; font-size: 12px;", tags$strong("⚠️ Clinical Thresholds:"), " Glucarpidase indication: MTX > 2 SD above mean excretion curve. ", "Guideline thresholds: ~1 µmol/L at 42h, ~0.4 µmol/L at 48h post-infusion start. ", "Discharge: ≤ 0.2 µmol/L. See consensus guideline (Ramsey et al. 2018)." ) ), nav_panel("Model Information", markdown(" ## High-Dose Methotrexate — 3-Compartment IV Population PK Model **Source:** Taylor ZL et al. (2020) *MTXPK.org: A Clinical Decision Support Tool Evaluating High-Dose Methotrexate Pharmacokinetics to Inform Post-Infusion Care and Use of Glucarpidase.* Clinical Pharmacology & Therapeutics 108(3):635-643. **Population:** 772 pediatric ALL patients (ages 1–18.8 years), NOPHO ALL2000/ALL2008 protocols. **Data:** 31,672 plasma MTX concentrations from 4,986 courses. ### Structural Model - **3-compartment linear elimination**, IV infusion - Central (V1) + vascular peripheral (V2) + non-vascular peripheral (V3) - Third compartment may reflect intracellular MTX redistribution ### Population Parameters (normalized to BSA = 1.73 m²) | Parameter | Estimate | Description | |-----------|----------|-------------| | CL | 11 L/h | Clearance from central compartment | | V1 | 16.5 L | Central volume of distribution | | Q2 | 0.602 L/h | Inter-compartmental clearance (vascular) | | V2 | 4.55 L | Vascular peripheral volume | | Q3 | 0.111 L/h | Inter-compartmental clearance (non-vascular) | | V3 | 13.1 L | Non-vascular peripheral volume | ### Covariate Model - **BSA-based scaling:** All parameters normalized to 1.73 m² - **Serum creatinine:** Power model on CL - CLᵢ = CL_pop × (BSA / 1.73) × (SCr / 29)^(−0.247) - Higher SCr → lower clearance (renal impairment reduces MTX elimination) ### Clinical Decision Support - **Glucarpidase indication:** MTX > 2 SD above mean excretion curve with SCr increase >50% - **Consensus guideline thresholds (Ramsey et al. 2018):** - 42h post-infusion: >1 µmol/L - 48h post-infusion: >0.4 µmol/L - **Discharge threshold:** ≤ 0.2 µmol/L - **MW of MTX:** 454.44 g/mol (1 µmol/L ≈ 0.454 mg/L) ")), nav_panel("References", div(class = "ref-box", tags$h5("📚 Key References"), tags$ol( tags$li("Taylor ZL, Mizuno T, Punt NC, et al. (2020) MTXPK.org: A Clinical Decision Support Tool Evaluating High-Dose Methotrexate Pharmacokinetics. ", tags$em("Clin Pharmacol Ther"), " 108(3):635-643. ", tags$a(href="https://doi.org/10.1002/cpt.1957", target="_blank", "doi:10.1002/cpt.1957")), tags$li("Ramsey LB, Balis FM, O'Brien MM, et al. (2018) Consensus Guideline for Use of Glucarpidase in Patients with HD-MTX Induced Acute Kidney Injury. ", tags$em("Oncologist"), " 23:52-61."), tags$li("Kawakatsu S, Nikanjam M, Lin T, et al. (2019) Population Pharmacokinetic Analysis of HD-MTX in Pediatric and Adult Oncology Patients. ", tags$em("Cancer Chemother Pharmacol"), " 84:1339-1348."), tags$li("VORAXAZE® (glucarpidase) Package Insert. US FDA (2012). ", tags$a(href="https://www.accessdata.fda.gov/drugsatfda_docs/label/2012/125327lbl.pdf", target="_blank", "FDA Label")) ), tags$h5("💊 Drug Information"), tags$ul( tags$li(tags$strong("Drug:"), " Methotrexate (MTX) — antifolate antineoplastic"), tags$li(tags$strong("High-dose range:"), " 1–12 g/m² IV over 3–24 hours"), tags$li(tags$strong("Indications:"), " Acute lymphoblastic leukemia (ALL), osteosarcoma, lymphoma"), tags$li(tags$strong("Mechanism:"), " Inhibits dihydrofolate reductase (DHFR), blocks DNA synthesis"), tags$li(tags$strong("Rescue:"), " Leucovorin (folinic acid) at 36–42h; glucarpidase for severe toxicity"), tags$li(tags$strong("Elimination:"), " Primarily renal; t½ ≈ 8–15h (HD-MTX); delayed clearance in 0.5–12% of patients"), tags$li(tags$strong("Tool:"), " ", tags$a(href="https://mtxpk.org", target="_blank", "MTXPK.org"), " — free clinical decision support for HD-MTX") ) )) ), # ── PKPDBuilder Branding 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 <- function(input, output, session) { sim_data <- reactive({ dose_mg <- input$dose_gm2 * 1000 * input$bsa # g/m2 × 1000 mg/g × BSA m2 inf_dur <- as.numeric(input$infusion_dur) inf_rate <- dose_mg / inf_dur # mg/h # Build event objects for each course ev_list <- list() for(i in 1:input$n_courses) { course_start <- (i - 1) * input$course_interval * 24 # hours ev_list[[i]] <- ev(amt = dose_mg, cmt = 1, rate = inf_rate, time = course_start) } ev_all <- do.call(c, ev_list) total_time <- (input$n_courses - 1) * input$course_interval * 24 + 300 # 300h post last dose mod %>% param(BSA = input$bsa, SCR = input$scr) %>% ev(ev_all) %>% mrgsim(end = total_time, delta = 0.5) %>% as.data.frame() %>% mutate(time_h = time) }) output$cmax <- renderText({ d <- sim_data() cmax <- max(d$CP_umol, na.rm = TRUE) sprintf("%.1f", cmax) }) output$c48h <- renderText({ d <- sim_data() # 48h after infusion start target_t <- 48 closest <- d[which.min(abs(d$time_h - target_t)), ] sprintf("%.2f", closest$CP_umol) }) output$auc <- renderText({ d <- sim_data() # AUC from 0 to end using trapezoidal rule total_time <- max(d$time_h) auc <- sum(diff(d$time_h) * (head(d$CP_umol, -1) + tail(d$CP_umol, -1)) / 2) sprintf("%.0f", auc) }) output$t_discharge <- renderText({ d <- sim_data() inf_dur <- as.numeric(input$infusion_dur) # Find when concentration drops below 0.2 µmol/L after end of infusion post_inf <- d %>% filter(time_h > inf_dur, CP_umol > 0) below <- post_inf %>% filter(CP_umol <= 0.2) if(nrow(below) > 0) { sprintf("%.0f", min(below$time_h)) } else { ">300" } }) output$pkPlot <- renderPlot({ d <- sim_data() inf_dur <- as.numeric(input$infusion_dur) p <- ggplot(d, aes(x = time_h, y = CP_umol)) + geom_line(color = "#8b5cf6", linewidth = 1) if(input$show_thresholds) { # Discharge threshold p <- p + geom_hline(yintercept = 0.2, linetype = "dashed", color = "#10b981", linewidth = 0.6) + annotate("text", x = max(d$time_h) * 0.75, y = 0.2, label = "Discharge: 0.2 µmol/L", vjust = -0.5, size = 3.5, color = "#10b981") + # Glucarpidase threshold zone geom_hline(yintercept = 1.0, linetype = "dashed", color = "#ef4444", linewidth = 0.6) + annotate("text", x = max(d$time_h) * 0.75, y = 1.0, label = "Guideline: 1 µmol/L at 42h", vjust = -0.5, size = 3.5, color = "#ef4444") + # Infusion end marker geom_vline(xintercept = inf_dur, linetype = "dotted", color = "#6b7280", linewidth = 0.5) + annotate("text", x = inf_dur, y = max(d$CP_umol) * 0.95, label = paste0("End infusion (", inf_dur, "h)"), hjust = -0.05, size = 3, color = "#6b7280") } p <- p + labs( x = "Time After Infusion Start (hours)", y = "Plasma MTX Concentration (µmol/L)", title = paste0("HD-MTX ", input$dose_gm2, " g/m² over ", inf_dur, "h | BSA=", input$bsa, " m² | SCr=", input$scr, " µmol/L") ) + theme_minimal(base_size = 14) + theme( plot.title = element_text(face = "bold", size = 14), panel.grid.minor = element_blank() ) if(input$log_scale) { p <- p + scale_y_log10( labels = scales::label_number(drop0trailing = TRUE), breaks = c(0.01, 0.1, 0.2, 1, 10, 100, 1000) ) + annotation_logticks(sides = "l") } p }) } shinyApp(ui = ui, server = server)