library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # Tramadol PK/PD model: 2-compartment parent + 1-compartment active M1 metabolite + Imax PD # Based on Chun et al. 2025, CPT: Pharmacometrics & Systems Pharmacology # Racemic tramadol PK with (1R,2R)-M1 active metabolite linked to pain response model_code <- ' $PARAM @annotated CL : 35 : Apparent clearance CL/F (L/h) VC : 200 : Central volume Vc/F (L) VP : 80 : Peripheral volume Vp/F (L) Q : 30 : Intercompartmental clearance Q/F (L/h) KA : 1.2 : Absorption rate constant (1/h) TLAG : 0.30 : Absorption lag time (h) FM1 : 0.269 : Fraction of racemic dose to active (1R,2R)-M1 CLM1 : 10 : M1 apparent clearance (L/h) VM1 : 31 : M1 apparent volume (L) KE0 : 0.0398 : Effect compartment rate constant (1/h) IMAX : 0.908 : Maximum fractional pain reduction PD0 : 46.4 : Baseline VAS pain score (mm) IC50U : 0.588 : IC50 unbound (1R,2R)-M1 (ng/mL) FUM1 : 0.246 : Fraction unbound of M1 WT : 72 : Body weight (kg) BMI : 26 : Body mass index (kg/m2) CYP2D6 : 1.0 : CYP2D6 activity multiplier $CMT @annotated DEPOT : Oral depot (mg) CENT : Central tramadol (mg) PERIPH : Peripheral tramadol (mg) M1CENT : Central M1 (mg) EFFECT : Effect compartment (conc scale) $MAIN ALAG_DEPOT = TLAG; double CLi = CL * pow(WT / 72.0, 0.75); double VCi = VC * pow(BMI / 26.0, 1.5); double FM1i = FM1 * pow(CYP2D6, 0.5); $ODE double CM1_mgL = M1CENT / VM1; dxdt_DEPOT = -KA * DEPOT; dxdt_CENT = KA * DEPOT - (CLi / VCi) * CENT - (Q / VCi) * CENT + (Q / VP) * PERIPH; dxdt_PERIPH = (Q / VCi) * CENT - (Q / VP) * PERIPH; dxdt_M1CENT = FM1i * (CLi / VCi) * CENT - (CLM1 / VM1) * M1CENT; dxdt_EFFECT = KE0 * (CM1_mgL - EFFECT); $TABLE double CP = CENT / VCi * 1000.0; double CM1 = M1CENT / VM1 * 1000.0; double CE = EFFECT * 1000.0; double CUE = FUM1 * CE; double PAIN = PD0 * (1.0 - IMAX * CUE / (IC50U + CUE)); $CAPTURE CP CM1 PAIN ' mod <- mcode("tramadol_pkpd", 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 = "Tramadol PK/PD Simulator \u2014 Neuropathic Pain", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Dosing"), sliderInput("dose", "Dose (mg)", min = 50, max = 400, value = 100, step = 50), selectInput("frequency", "Dosing Frequency", choices = c("Once daily (q24h)" = 24, "Twice daily (q12h)" = 12, "Three times daily (q8h)" = 8, "Four times daily (q6h)" = 6), selected = 12), numericInput("n_days", "Duration (days)", value = 3, min = 1, max = 7), hr(), h6("Patient Characteristics"), sliderInput("wt", "Weight (kg)", min = 40, max = 120, value = 72, step = 1), sliderInput("bmi", "BMI (kg/m\u00B2)", min = 17, max = 40, value = 26, step = 1), selectInput("diabetes", "Diabetes Status", choices = c("None (nerve compression)" = "none", "Type 1 Diabetes (T1DM)" = "t1dm", "Type 2 Diabetes (T2DM)" = "t2dm"), selected = "none"), selectInput("cyp2d6", "CYP2D6 Phenotype", choices = c("Poor Metabolizer (PM)" = 0.2, "Intermediate Metabolizer (IM)" = 0.5, "Normal Metabolizer (NM/EM)" = 1.0, "Ultra-rapid Metabolizer (UM)" = 2.0), selected = 1.0), hr(), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE), checkboxInput("show_therapeutic", "Show Therapeutic Range", value = TRUE) ), navset_card_tab( title = "Tramadol PK/PD 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,ss (ng/mL)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("ctrough")), div(class = "metric-label", "Ctrough (ng/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc")), div(class = "metric-label", "AUC\u2080-\u03C4 (ng\u00B7h/mL)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf")), div(class = "metric-label", "t\u00BD (h)")) ), plotOutput("pkPlot", height = "500px") ), nav_panel("Pain Response", layout_columns( col_widths = c(4, 4, 4), div(class = "metric-card metric-success", div(class = "metric-value", textOutput("pain_baseline")), div(class = "metric-label", "Baseline VAS (mm)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("pain_min")), div(class = "metric-label", "Min Pain Score (mm)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("pain_reduction")), div(class = "metric-label", "Max Pain Reduction (%)")) ), plotOutput("pdPlot", height = "500px") ), nav_panel("Model Information", markdown(" ## Tramadol PK/PD Model \u2014 Neuropathic Pain with Diabetes Covariates **Drug:** Tramadol (racemic mixture of (1R,2R)- and (1S,2S)-tramadol) **Class:** Centrally acting opioid analgesic (\u00B5-opioid agonist + SNRI) **Indication:** Neuropathic pain (diabetic neuropathy, nerve compression) **Route:** Oral (IR tablets) ### PK Model Structure - **Tramadol:** Two-compartment model with first-order absorption, lag time, and linear elimination - **Active metabolite (1R,2R)-M1 (O-desmethyltramadol):** One-compartment with linear formation and elimination - **(1R,2R)-M1** has 700\u00D7 higher \u00B5-opioid receptor affinity (Ki = 3.4 nM) than racemic tramadol ### PD Model Structure - **Inhibitory Emax model** with effect compartment (ke0 = 0.0398 h\u207B\u00B9) - PD linked to **unbound (1R,2R)-M1** concentration at effect site - VAS pain score (0\u2013100 mm) as continuous endpoint ### Key Findings: Diabetes Effect | Parameter | Non-Diabetic | T1DM | T2DM | |-----------|-------------|------|------| | fu (M1) | 0.246 | 0.756 | 0.284 | | IC50 unbound (nmol/L) | 2.36 | 0.54 | 1.02 | | Sensitivity | Reference | \u2191\u2191\u2191 | \u2191\u2191 | - **Diabetic patients show enhanced tramadol sensitivity** (lower IC50) - Higher fraction unbound in T1DM due to albumin glycation - **Dosing guidance:** QD dosing sufficient for diabetic neuropathy; BID recommended for nerve compression neuropathy ### Covariates - **BMI:** Power function on central volume of distribution - **CYP2D6 activity:** Affects formation clearance of active M1 metabolite - **Diabetes status:** Affects protein binding (fu) and pharmacodynamic sensitivity (IC50) ")), nav_panel("References", div(class = "ref-box", tags$h5("\U0001F4DA Key References"), tags$ol( tags$li("Chun D, Mehta P, Guzy S, et al. Enhanced Sensitivity to Tramadol in Diabetic Neuropathic Pain Compared to Nerve Compression Neuropathies: A Population PK/PD Model Analysis. CPT Pharmacometrics Syst Pharmacol. 2025;14(4):781-795. PMID: 39961992."), tags$li("Grond S, Sablotzki A. Clinical pharmacology of tramadol. Clin Pharmacokinet. 2004;43(13):879-923."), tags$li(tags$a(href = "https://www.accessdata.fda.gov/drugsatfda_docs/label/2009/020281s032s033lbl.pdf", target = "_blank", "ULTRAM (tramadol) FDA Prescribing Information")), tags$li("De Moraes NV, Lauretti GR, Lanchote VL. Effects of Type 1 and Type 2 Diabetes on the Pharmacokinetics of Tramadol Enantiomers. J Pharm Pharmacol. 2014;66(9):1222-1230.") ), tags$h5("\U0001F48A Therapeutic Context"), tags$ul( tags$li(tags$strong("Class:"), " Centrally acting opioid analgesic (\u00B5-opioid agonist + weak SNRI)"), tags$li(tags$strong("Active metabolite:"), " (1R,2R)-O-desmethyltramadol (M1) via CYP2D6"), tags$li(tags$strong("Dosing:"), " IR: 50\u2013100 mg q4\u20136h (max 400 mg/day)"), tags$li(tags$strong("Therapeutic range:"), " 100\u2013300 ng/mL (analgesic effect)"), tags$li(tags$strong("Half-life:"), " ~6 h (tramadol), ~7\u20139 h apparent (M1, formation-rate limited)"), tags$li(tags$strong("Metabolism:"), " CYP2D6 \u2192 M1 (active), CYP3A4/CYP2B6 \u2192 M2 (inactive)") ), tags$h5("\U0001F52C Clinical Implications"), tags$ul( tags$li("Diabetic neuropathic pain patients: \u226580% respond to 100 mg QD"), tags$li("Nerve compression neuropathy: requires BID dosing for sustained relief"), tags$li("CYP2D6 poor metabolizers have reduced M1 formation and diminished opioid effect"), tags$li("Tramadol has hypoglycemic effects in T2DM via \u00B5-opioid receptor activation") ) )) ), 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)", br(), tags$span(style = "font-size: 10px;", "For research and educational purposes only. Not for clinical decision-making.")) ) server <- function(input, output, session) { diabetes_params <- reactive({ switch(input$diabetes, "none" = list(ic50u = 0.588, fum1 = 0.246, label = "Non-Diabetic"), "t1dm" = list(ic50u = 0.135, fum1 = 0.756, label = "T1DM"), "t2dm" = list(ic50u = 0.254, fum1 = 0.284, label = "T2DM") ) }) sim_data <- reactive({ tau <- as.numeric(input$frequency) total_hours <- input$n_days * 24 n_doses <- floor(total_hours / tau) dp <- diabetes_params() ev1 <- ev(amt = input$dose, cmt = 1, ii = tau, addl = max(n_doses - 1, 0)) mod %>% param(WT = input$wt, BMI = input$bmi, CYP2D6 = as.numeric(input$cyp2d6), IC50U = dp$ic50u, FUM1 = dp$fum1) %>% ev(ev1) %>% mrgsim(end = total_hours, delta = 0.25) %>% as.data.frame() %>% mutate( time_h = time, time_days = time / 24 ) }) last_interval_data <- reactive({ d <- sim_data() tau <- as.numeric(input$frequency) total_hours <- input$n_days * 24 last_dose_time <- total_hours - tau d %>% filter(time_h >= last_dose_time, time_h <= total_hours) }) output$cmax <- renderText({ li <- last_interval_data() if (nrow(li) < 2) return("--") sprintf("%.0f", max(li$CP, na.rm = TRUE)) }) output$ctrough <- renderText({ li <- last_interval_data() if (nrow(li) < 2) return("--") sprintf("%.1f", min(li$CP, na.rm = TRUE)) }) output$auc <- renderText({ li <- last_interval_data() if (nrow(li) < 2) return("--") auc_val <- sum(diff(li$time_h) * (head(li$CP, -1) + tail(li$CP, -1)) / 2) sprintf("%.0f", auc_val) }) output$thalf <- renderText({ CLi <- 35 * (input$wt / 72)^0.75 VCi <- 200 * (input$bmi / 26)^1.5 k10 <- CLi / VCi; k12 <- 30 / VCi; k21 <- 30 / 80 s <- k10 + k12 + k21; p <- k10 * k21 beta_val <- 0.5 * (s - sqrt(s^2 - 4 * p)) sprintf("%.1f", log(2) / beta_val) }) # Pain response metrics output$pain_baseline <- renderText({ sprintf("%.0f", 46.4) }) output$pain_min <- renderText({ d <- sim_data() sprintf("%.1f", min(d$PAIN, na.rm = TRUE)) }) output$pain_reduction <- renderText({ d <- sim_data() min_pain <- min(d$PAIN, na.rm = TRUE) pct <- (46.4 - min_pain) / 46.4 * 100 sprintf("%.0f%%", pct) }) output$pkPlot <- renderPlot({ d <- sim_data() dp <- diabetes_params() tau <- as.numeric(input$frequency) freq_label <- switch(as.character(tau), "24" = "QD", "12" = "BID", "8" = "TID", "6" = "QID") d_long <- d %>% tidyr::pivot_longer(cols = c(CP, CM1), names_to = "Analyte", values_to = "Conc") %>% mutate(Analyte = ifelse(Analyte == "CP", "Tramadol", "(1R,2R)-M1")) p <- ggplot(d_long, aes(x = time_h, y = Conc, color = Analyte)) + geom_line(linewidth = 0.8) if (input$show_therapeutic) { p <- p + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 100, ymax = 300, fill = "#10b981", alpha = 0.10) + geom_hline(yintercept = c(100, 300), linetype = "dashed", color = "#10b981", alpha = 0.5) + annotate("text", x = max(d$time_h) * 0.02, y = 310, label = "Therapeutic: 100-300 ng/mL", hjust = 0, size = 3.5, color = "#10b981") } p <- p + scale_color_manual(values = c("Tramadol" = "#8b5cf6", "(1R,2R)-M1" = "#f59e0b")) + labs( x = "Time (hours)", y = "Concentration (ng/mL)", title = paste0("Tramadol ", input$dose, " mg ", freq_label, " \u2014 ", dp$label, " (CYP2D6: ", names(which(c("0.2" = "PM", "0.5" = "IM", "1" = "NM", "2" = "UM") == input$cyp2d6)[1]), ")"), color = "Analyte" ) + theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold"), legend.position = "top") if (input$log_scale) p <- p + scale_y_log10() p }) output$pdPlot <- renderPlot({ d <- sim_data() dp <- diabetes_params() tau <- as.numeric(input$frequency) freq_label <- switch(as.character(tau), "24" = "QD", "12" = "BID", "8" = "TID", "6" = "QID") p <- ggplot(d, aes(x = time_h, y = PAIN)) + geom_hline(yintercept = 46.4, linetype = "dashed", color = "#6c757d", alpha = 0.6) + annotate("text", x = max(d$time_h) * 0.02, y = 48, label = "Baseline (no drug)", hjust = 0, size = 3.5, color = "#6c757d") + geom_hline(yintercept = 46.4 * 0.5, linetype = "dotted", color = "#10b981", alpha = 0.6) + annotate("text", x = max(d$time_h) * 0.02, y = 46.4 * 0.5 + 1.5, label = "50% reduction target", hjust = 0, size = 3.5, color = "#10b981") + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 0, ymax = 46.4 * 0.5, fill = "#10b981", alpha = 0.06) + geom_line(color = "#e74c3c", linewidth = 1.0) + scale_y_continuous(limits = c(0, 55)) + labs( x = "Time (hours)", y = "VAS Pain Score (mm)", title = paste0("Pain Response \u2014 Tramadol ", input$dose, " mg ", freq_label, " (", dp$label, ")"), subtitle = paste0("IC50 (unbound M1) = ", dp$ic50u, " ng/mL | ", "fu(M1) = ", dp$fum1, " | Imax = 0.908") ) + theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold")) p }) } shinyApp(ui = ui, server = server)