use("shiny") use("bslib") use("mrgsolve") use("dplyr") use("ggplot2") model_code <- ' $PARAM @annotated CLTH : 6.5 : Typical clearance coefficient (L/h) BSA_EXP : 0.62 : Body surface area exponent on clearance VC_KG : 0.36 : Central volume coefficient (L/kg) Q : 0.42 : Intercompartmental clearance (L/h) VP : 3.2 : Peripheral volume (L) BSA : 0.80 : Body surface area (m2) WT : 20 : Body weight (kg) $CMT @annotated CENT : Central compartment (mg) PERIPH : Peripheral compartment (mg) $MAIN double CLi = CLTH * pow(BSA, BSA_EXP); double VCi = VC_KG * WT; double Qi = Q; double VPi = VP; $ODE dxdt_CENT = -(CLi / VCi + Qi / VCi) * CENT + (Qi / VPi) * PERIPH; dxdt_PERIPH = (Qi / VCi) * CENT - (Qi / VPi) * PERIPH; $TABLE double CP_MG_L = CENT / VCi; double CP_uM = CP_MG_L / 0.45444; $CAPTURE @annotated CP_MG_L : Plasma methotrexate concentration (mg/L) CP_uM : Plasma methotrexate concentration (uM) CLi : Individual clearance (L/h) VCi : Individual central volume (L) Qi : Intercompartmental clearance (L/h) VPi : Peripheral volume (L) ' mod <- mrgsolve::mcode("methotrexate_medellin_garibay_2019", model_code) metric_card <- function(output_id, label, class = "metric-primary") { div( class = paste("metric-card", class), div(class = "metric-value", textOutput(output_id)), div(class = "metric-label", label) ) } micro_rates <- function(bsa, wt) { cl_i <- 6.5 * bsa^0.62 vc_i <- 0.36 * wt q_i <- 0.42 vp_i <- 3.2 k10 <- cl_i / vc_i k12 <- q_i / vc_i k21 <- q_i / vp_i list(cl = cl_i, vc = vc_i, q = q_i, vp = vp_i, k10 = k10, k12 = k12, k21 = k21) } terminal_half_life_h <- function(bsa, wt) { rates <- micro_rates(bsa, wt) a_term <- rates$k10 + rates$k12 + rates$k21 disc <- a_term^2 - 4 * rates$k10 * rates$k21 lambda_beta <- 0.5 * (a_term - sqrt(max(disc, 0))) if (!is.finite(lambda_beta) || lambda_beta <= 0) { return(NA_real_) } log(2) / lambda_beta } simulate_mtx <- function(dose_g_m2 = 3.5, bsa = 0.8, wt = 20, infusion_h = 24, end_h = 72, delta_h = 0.1) { total_mg <- dose_g_m2 * 1000 * bsa first_mg <- total_mg * 0.20 second_mg <- total_mg * 0.80 dose_events <- data.frame( time = c(0, 1), amt = c(first_mg, second_mg), cmt = c(1, 1), evid = c(1, 1), rate = c(first_mg / 1, second_mg / 23) ) mod |> mrgsolve::param(BSA = bsa, WT = wt) |> mrgsolve::ev(dose_events) %>% mrgsolve::mrgsim(end = end_h, delta = delta_h) |> as.data.frame() |> dplyr::mutate( time_h = time, CP_MG_L = pmax(CP_MG_L, 0), CP_uM = pmax(CP_uM, 0) ) } value_at_time <- function(data, target_h, column = "CP_uM") { data_unique <- data[!duplicated(data$time_h, fromLast = TRUE), c("time_h", column)] stats::approx(data_unique$time_h, data_unique[[column]], xout = target_h, rule = 2)$y } paper_target_note <- function(risk_group) { if (identical(risk_group, "high")) { "Paper target: Cpss about 65 uM with C42h <= 1 uM and C60h < 0.2 uM." } else { "Paper target: Cpss about 33 uM with C42h <= 1 uM and C54h <= 0.2 uM." } } 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; } .small-note { color: #6c757d; font-size: 12px; line-height: 1.5; } ") ui <- page_sidebar( title = "Methotrexate Pediatric PopPK Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, accordion( open = c("Scenario", "Patient", "Display"), accordion_panel( "Scenario", selectInput( "risk_group", "Paper scenario", choices = c( "Standard / low-risk target" = "standard", "High-risk target" = "high", "Custom dose" = "custom" ), selected = "standard" ), numericInput("dose_g_m2", "Methotrexate dose (g/m^2)", value = 3.5, min = 1, max = 10, step = 0.1), sliderInput("infusion_h", "Infusion duration (h)", min = 24, max = 24, value = 24, step = 1), div( class = "small-note", "The manuscript model assumes 20% of the total dose in the first hour and 80% over the next 23 hours." ) ), accordion_panel( "Patient", numericInput("bsa", "Body surface area (m^2)", value = 0.80, min = 0.40, max = 1.80, step = 0.01), sliderInput("wt", "Body weight (kg)", min = 8, max = 80, value = 20, step = 1), div( class = "small-note", "Paper-average child used in the dosing simulations: BSA 0.8 m^2 and weight 20 kg." ) ), accordion_panel( "Display", sliderInput("end_h", "Monitoring duration (h)", min = 48, max = 96, value = 72, step = 6), checkboxInput("log_scale", "Log scale (Y-axis)", value = TRUE) ) ) ), layout_column_wrap( width = 1/4, fill = FALSE, metric_card("c24", "Cpss at 24 h (uM)", "metric-success"), metric_card("c42", "Concentration at 42 h (uM)", "metric-warning"), metric_card("auc", "AUC0-end (uM*h/L)", "metric-primary"), metric_card("thalf", "Terminal t1/2 (h)", "metric-info") ), card( card_header("Methotrexate Concentration-Time Profile"), full_screen = TRUE, plotOutput("pkPlot", height = "500px"), div(class = "small-note", textOutput("paperSummary")) ), navset_card_underline( title = "Supporting Details", nav_panel( "Model Information", div( class = "ref-box", tags$h5("Model Summary"), tags$p("Population pharmacokinetic model for high-dose methotrexate in Mexican pediatric patients with acute lymphoblastic leukemia."), tags$ul( tags$li(tags$strong("Structure:"), " Two-compartment IV infusion model with first-order elimination."), tags$li(tags$strong("Clearance:"), " CL (L/h) = 6.5 x BSA^0.62."), tags$li(tags$strong("Central volume:"), " Vc (L) = 0.36 x body weight."), tags$li(tags$strong("Distribution:"), " Q = 0.42 L/h and Vp = 3.2 L."), tags$li(tags$strong("Sampling:"), " Plasma concentrations measured at 24, 36, 42, and 48 h after infusion start."), tags$li(tags$strong("Clinical targets:"), " Paper target Cpss about 33 uM for standard-risk and 65 uM for high-risk children.") ) ) ), nav_panel( "References", div( class = "ref-box", tags$h5("Key References"), tags$ol( tags$li( "Medellin-Garibay SE, Hernandez-Villa N, Correa-Gonzalez LC, et al. Population pharmacokinetics of methotrexate in Mexican pediatric patients with acute lymphoblastic leukemia. Cancer Chemother Pharmacol. 2019. ", tags$a( href = "https://doi.org/10.1007/s00280-019-03977-1", target = "_blank", "doi:10.1007/s00280-019-03977-1" ) ), tags$li( "Methotrexate Injection prescribing information. ", tags$a( href = "https://www.accessdata.fda.gov/drugsatfda_docs/label/2022/214121s001lbl.pdf", target = "_blank", "FDA label" ) ) ), tags$h5("Therapeutic Context"), tags$ul( tags$li(tags$strong("Drug class:"), " Antifolate antineoplastic"), tags$li(tags$strong("Population:"), " Children with acute lymphoblastic leukemia receiving 24-hour high-dose methotrexate infusion"), tags$li(tags$strong("Paper recommendations:"), " 3.5 g/m^2 for standard-risk and 7 g/m^2 for high-risk children with the paper-average BSA and body weight"), tags$li(tags$strong("Rescue threshold:"), " Leucovorin starts at 42 h when methotrexate concentration is <= 1 uM, then continues until concentration is <= 0.2 uM") ) ) ) ), 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) { observeEvent(input$risk_group, { if (identical(input$risk_group, "standard")) { updateNumericInput(session, "dose_g_m2", value = 3.5) } else if (identical(input$risk_group, "high")) { updateNumericInput(session, "dose_g_m2", value = 7.0) } }, ignoreInit = TRUE) sim_data <- reactive({ shiny::req(input$dose_g_m2, input$bsa, input$wt, input$infusion_h, input$end_h) simulate_mtx( dose_g_m2 = input$dose_g_m2, bsa = input$bsa, wt = input$wt, infusion_h = input$infusion_h, end_h = input$end_h ) }) output$c24 <- renderText({ d <- sim_data() sprintf("%.1f", value_at_time(d, 24)) }) output$c42 <- renderText({ d <- sim_data() sprintf("%.2f", value_at_time(d, 42)) }) output$auc <- renderText({ d <- sim_data() auc_value <- sum(diff(d$time_h) * (head(d$CP_uM, -1) + tail(d$CP_uM, -1)) / 2) sprintf("%.0f", auc_value) }) output$thalf <- renderText({ half_h <- terminal_half_life_h(input$bsa, input$wt) if (!is.finite(half_h)) { return("NA") } sprintf("%.1f", half_h) }) output$paperSummary <- renderText({ d <- sim_data() c24 <- value_at_time(d, 24) c42 <- value_at_time(d, 42) c54 <- value_at_time(d, 54) c60 <- value_at_time(d, 60) paste0( paper_target_note(input$risk_group), " Simulated values with the current inputs: C24h ", sprintf("%.1f", c24), " uM, C42h ", sprintf("%.2f", c42), " uM, C54h ", sprintf("%.2f", c54), " uM, C60h ", sprintf("%.2f", c60), " uM." ) }) output$pkPlot <- renderPlot({ d <- sim_data() d_plot <- if (input$log_scale) { d |> dplyr::filter(CP_uM > 0.001) } else { d } p <- ggplot(d_plot, aes(x = time_h, y = CP_uM)) + annotate("rect", xmin = 23.5, xmax = 24.5, ymin = 33, ymax = 65, fill = "#10b981", alpha = 0.15) + geom_hline(yintercept = c(1, 0.2), linetype = "dashed", color = c("#f59e0b", "#ef4444"), alpha = 0.8) + geom_vline(xintercept = c(24, 42, 54, 60), linetype = "dotted", color = "#adb5bd") + geom_line(color = "#8b5cf6", linewidth = 1) + annotate("text", x = 25.2, y = 63, label = "Target Cpss band", hjust = 0, size = 3.5, color = "#10b981") + annotate("text", x = max(d_plot$time_h, na.rm = TRUE) * 0.75, y = 1.1, label = "42 h rescue threshold: 1 uM", hjust = 0, size = 3.3, color = "#f59e0b") + annotate("text", x = max(d_plot$time_h, na.rm = TRUE) * 0.75, y = 0.24, label = "Stop rescue threshold: 0.2 uM", hjust = 0, size = 3.3, color = "#ef4444") + labs( x = "Time after infusion start (hours)", y = "Plasma methotrexate (uM)", title = paste0("Methotrexate ", sprintf("%.1f", input$dose_g_m2), " g/m^2 over 24 h") ) + theme_minimal(base_size = 14) if (input$log_scale) { p <- p + scale_y_log10() } p }) } shinyApp(ui = ui, server = server)