library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ── Rifapentine PopPK Model with Autoinduction ── # Based on: Yang et al. (2026) Int J Infect Dis 162:108229 # PopPK model from: Hibma et al. (2020) Am J Respir Crit Care Med 202(6):866-877 # One-compartment model with transit absorption and enzyme autoinduction model_code <- ' $PARAM @annotated CL : 1.11 : Baseline apparent clearance (L/h) V : 36.7 : Apparent volume of distribution (L) KA : 1.62 : Absorption rate constant (1/h) EMAX : 0.73 : Max autoinduction effect (fractional increase in CL) EC50 : 4.27 : Conc for half-max induction (mg/L) GAMMA : 10 : Hill coefficient for induction KENZ : 0.00587 : Enzyme production rate constant (1/h) FHIV : 1.0 : HIV effect on bioavailability (0.729 if HIV+) FMEAL : 1.0 : Meal effect (1.0=low-fat, 1.49=high-fat, 0.731=fasting) FDOSE : 1.0 : Dose-dependent F reduction factor $CMT @annotated DEPOT : Oral depot (mg) CENT : Central compartment (mg) ENZ : Enzyme pool (relative) $MAIN double CLi = CL; double Vi = V; double Fi = FHIV * FMEAL * FDOSE; F_DEPOT = Fi; ENZ_0 = 1.0; $ODE double CP = CENT / Vi; double INDUCTION = (EMAX * pow(CP, GAMMA)) / (pow(EC50, GAMMA) + pow(CP, GAMMA)); double KDEG = KENZ; dxdt_DEPOT = -KA * DEPOT; dxdt_CENT = KA * DEPOT - (CLi * ENZ / Vi) * CENT; dxdt_ENZ = KENZ * (1.0 + INDUCTION) - KDEG * ENZ; $TABLE double DV = CENT / Vi; DV = DV > 1e-6 ? DV : 1e-6; double CL_effective = CLi * ENZ; $CAPTURE @annotated DV : Plasma concentration (mg/L) CL_effective : Effective clearance with induction (L/h) ' mod <- mcode("rifapentine", 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; } ") # ── UI ── ui <- page_sidebar( title = "Rifapentine PK Simulator — Ethnic Differences & Drug Interactions", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Patient Characteristics"), radioButtons("hiv_status", "HIV Status", choices = c("HIV-negative" = "neg", "HIV-positive" = "pos"), selected = "neg", inline = TRUE ), hr(), h6("Dosing"), selectInput("dose", "Rifapentine Dose (mg)", choices = c(300, 450, 600, 750, 900), selected = 600 ), selectInput("regimen", "Regimen", choices = c( "1HP (daily x 28 days)" = "daily", "3HP (weekly x 12 weeks)" = "weekly" ), selected = "daily" ), selectInput("meal", "Meal Condition", choices = c( "Low-fat meal (reference)" = "lowfat", "High-fat meal (+49% F)" = "highfat", "Fasting (-28% F)" = "fasting" ), selected = "lowfat" ), hr(), h6("Display"), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE), checkboxInput("show_induction", "Show CL Induction Profile", value = TRUE), hr(), card( card_header("Model Parameters", class = "bg-light"), p(strong("CL/F:"), "1.11 L/h (baseline)"), p(strong("V/F:"), "36.7 L"), p(strong("t\u00bd:"), "~15 h (pre-induction)"), p(strong("Autoinduction:"), "Up to 73% CL increase"), p(strong("MIC:"), "0.06 mg/L"), p(em("Source: Hibma et al. 2020")) ) ), # Metric cards layout_column_wrap( width = 1/4, fill = FALSE, div(class = "metric-card metric-success", div(class = "metric-value", textOutput("cmax_ss")), div(class = "metric-label", "Cmax,ss (mg/L)") ), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("cmin_ss")), div(class = "metric-label", "Cmin,ss (mg/L)") ), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc_ss")), div(class = "metric-label", "AUCss (h\u00b7mg/L)") ), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("cl_induced")), div(class = "metric-label", "CL at Steady State (L/h)") ) ), # Main PK plot card( card_header("Concentration-Time Profile"), full_screen = TRUE, plotOutput("pkPlot", height = "450px") ), # Tabbed content navset_card_tab( title = "Analysis", nav_panel("Autoinduction Profile", plotOutput("inductionPlot", height = "400px") ), nav_panel("HIV vs Non-HIV Comparison", plotOutput("hivComparePlot", height = "400px") ), nav_panel("Ethnic Comparison", card_body( markdown(" ### No Clinically Significant Ethnic Differences **Key Finding (Yang et al. 2026):** Integrated 'bottom-up' (PBPK) and 'top-down' (PopPK) modeling demonstrated **no clinically significant ethnic differences** in rifapentine PK between Chinese and non-Chinese populations. #### Cross-Validation Results (600 mg daily, 1HP regimen) | Parameter | PBPK Model | PopPK Model | |-----------|-----------|-------------| | Cmin,ss (mg/L) | 7.89 | 6.54 | | Cmax,ss (mg/L) | 21.07 | 18.17 | | AUCss (h\u00b7mg/L) | 337.95 | 285.54 | #### Drug Interaction with Ritonavir Rifapentine at steady state had **minimal impact** on ritonavir exposure: - AUCss reduction: ~9% - Cmax,ss reduction: ~7% - **No dose adjustment needed** for ritonavir when co-administered *Ritonavir's mechanism-based inhibition of CYP3A4 counteracts rifapentine-mediated enzyme induction.* ") ) ), nav_panel("References", card_body( markdown(" ### References 1. **Yang H, Liu W, Xu N, et al.** (2026) Model-informed evidence for rifapentine use in Chinese HIV-positive adults with latent tuberculosis: Assessing ethnic PK differences and interactions with ritonavir. *Int J Infect Dis* 162:108229. [DOI](https://doi.org/10.1016/j.ijid.2025.108229) 2. **Hibma JE, Radtke KK, Dorman SE, et al.** (2020) Rifapentine Population Pharmacokinetics and Dosing Recommendations for Latent Tuberculosis Infection. *Am J Respir Crit Care Med* 202(6):866-877. [PMC](https://pmc.ncbi.nlm.nih.gov/articles/PMC7491398/) 3. **FDA Label:** [Priftin (rifapentine) tablets](https://www.accessdata.fda.gov/drugsatfda_docs/label/2010/021024s009lbl.pdf) 4. **Swindells S, et al.** (2019) One month of rifapentine plus isoniazid to prevent HIV-related tuberculosis. *N Engl J Med* 380(11):1001-1011. ### Therapeutic Context - **Indication:** Latent tuberculosis infection (LTBI) prevention - **Regimens:** 1HP (daily x 28 days) or 3HP (weekly x 12 weeks) with isoniazid - **Key covariates:** HIV status (\u221227% F), meal type, dose-dependent F - **MIC target:** 0.06 mg/L (M. tuberculosis) - **Autoinduction:** CL increases up to 73%, stabilizes by Day 21 ") ) ), nav_panel("Model Information", card_body( markdown(" ### Model Structure **One-compartment oral PK with enzyme autoinduction** - Transit absorption simplified to first-order (Ka = 1.62 h\u207b\u00b9) - Semimechanistic enzyme turnover model for autoinduction - Rifapentine concentration drives enzyme synthesis via sigmoid Emax ### Key Parameters (Hibma et al. 2020) | Parameter | Value | Description | |-----------|-------|-------------| | CL/F | 1.11 L/h | Baseline apparent clearance | | V/F | 36.7 L | Apparent volume | | Ka | 1.62 h\u207b\u00b9 | Absorption rate | | Emax | 0.73 (73%) | Max CL increase from induction | | EC50 | 4.27 mg/L | Half-max induction concentration | | \u03b3 | 10 | Hill coefficient | | kENZ | 0.00587 h\u207b\u00b9 | Enzyme turnover rate (t\u00bd = 118 h) | ### Covariate Effects on Bioavailability | Factor | Effect | Relative F | |--------|--------|-----------| | HIV infection | \u221227% | 0.729 | | High-fat meal | +49% | 1.49 | | Fasting | \u221228% | 0.731 | | Dose (per 100 mg > 300 mg) | \u22121.67% | Variable | ### Population (N = 863 from 9 studies) - Median age: 34 years - Median weight: 59 kg - 9% HIV-positive - Doses: 300-1200 mg; daily to weekly ") ) ) ), # 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"), " \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 ── server <- function(input, output, session) { sim_data <- reactive({ dose_mg <- as.numeric(input$dose) hiv <- ifelse(input$hiv_status == "pos", 0.729, 1.0) meal <- switch(input$meal, "lowfat" = 1.0, "highfat" = 1.49, "fasting" = 0.731 ) # Dose-dependent F reduction: 1 - 0.0167*(dose/100 - 3) for dose > 300 fdose <- max(1.0 - 0.0167 * (dose_mg / 100 - 3), 0.5) if (input$regimen == "daily") { ii <- 24 addl <- 27 end_time <- 28 * 24 } else { ii <- 168 addl <- 11 end_time <- 12 * 168 } out <- mod %>% param(FHIV = hiv, FMEAL = meal, FDOSE = fdose) %>% ev(amt = dose_mg, cmt = 1, ii = ii, addl = addl) %>% mrgsim(end = end_time, delta = 0.5) %>% as.data.frame() out }) # Metrics at steady state ss_metrics <- reactive({ d <- sim_data() if (input$regimen == "daily") { # Last dosing interval (day 27-28) ss <- d %>% dplyr::filter(time >= 27 * 24) } else { # Last dosing interval (week 11-12) ss <- d %>% dplyr::filter(time >= 11 * 168) } list( cmax = max(ss$DV, na.rm = TRUE), cmin = min(ss$DV[ss$DV > 0.001], na.rm = TRUE), auc = sum(diff(ss$time) * (head(ss$DV, -1) + tail(ss$DV, -1)) / 2), cl_ss = tail(ss$CL_effective, 1) ) }) output$cmax_ss <- renderText({ sprintf("%.1f", ss_metrics()$cmax) }) output$cmin_ss <- renderText({ sprintf("%.2f", ss_metrics()$cmin) }) output$auc_ss <- renderText({ sprintf("%.0f", ss_metrics()$auc) }) output$cl_induced <- renderText({ sprintf("%.2f", ss_metrics()$cl_ss) }) # Main PK plot output$pkPlot <- renderPlot({ d <- sim_data() d$time_days <- d$time / 24 d_plot <- if (input$log_scale) d %>% dplyr::filter(DV > 0.001) else d p <- ggplot(d_plot, aes(x = time_days, y = DV)) + geom_line(color = "#8b5cf6", linewidth = 1) + geom_hline(yintercept = 0.06, linetype = "dashed", color = "#e74c3c", linewidth = 0.7) + annotate("text", x = max(d_plot$time_days) * 0.95, y = 0.08, label = "MIC = 0.06 mg/L", color = "#e74c3c", hjust = 1, size = 3.5) + labs( x = "Time (days)", y = "Rifapentine Concentration (mg/L)", title = paste0("Rifapentine ", input$dose, " mg ", ifelse(input$regimen == "daily", "Daily (1HP)", "Weekly (3HP)"), " — ", ifelse(input$hiv_status == "pos", "HIV+", "HIV\u2212")) ) + theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold")) if (input$log_scale) { p <- p + scale_y_log10() } p }) # Autoinduction plot output$inductionPlot <- renderPlot({ d <- sim_data() d$time_days <- d$time / 24 ggplot(d, aes(x = time_days, y = CL_effective)) + geom_line(color = "#e67e22", linewidth = 1.2) + geom_hline(yintercept = 1.11, linetype = "dashed", color = "#95a5a6") + annotate("text", x = 2, y = 1.15, label = "Baseline CL = 1.11 L/h", color = "#95a5a6", hjust = 0, size = 3.5) + labs( x = "Time (days)", y = "Effective Clearance (L/h)", title = "Autoinduction of Rifapentine Clearance Over Time", subtitle = "Enzyme turnover model: CL increases as rifapentine induces its own metabolism" ) + theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold")) }) # HIV comparison plot output$hivComparePlot <- renderPlot({ dose_mg <- as.numeric(input$dose) meal <- switch(input$meal, "lowfat" = 1.0, "highfat" = 1.49, "fasting" = 0.731) fdose <- max(1.0 - 0.0167 * (dose_mg / 100 - 3), 0.5) if (input$regimen == "daily") { ii <- 24; addl <- 27; end_time <- 28 * 24 } else { ii <- 168; addl <- 11; end_time <- 12 * 168 } sim_neg <- mod %>% param(FHIV = 1.0, FMEAL = meal, FDOSE = fdose) %>% ev(amt = dose_mg, cmt = 1, ii = ii, addl = addl) %>% mrgsim(end = end_time, delta = 0.5) %>% as.data.frame() %>% mutate(Group = "HIV\u2212") sim_pos <- mod %>% param(FHIV = 0.729, FMEAL = meal, FDOSE = fdose) %>% ev(amt = dose_mg, cmt = 1, ii = ii, addl = addl) %>% mrgsim(end = end_time, delta = 0.5) %>% as.data.frame() %>% mutate(Group = "HIV+") # Also simulate HIV+ with 30% higher dose dose_hiv <- round(dose_mg * 1.3 / 150) * 150 # round to nearest 150 fdose_hiv <- max(1.0 - 0.0167 * (dose_hiv / 100 - 3), 0.5) sim_pos_high <- mod %>% param(FHIV = 0.729, FMEAL = meal, FDOSE = fdose_hiv) %>% ev(amt = dose_hiv, cmt = 1, ii = ii, addl = addl) %>% mrgsim(end = end_time, delta = 0.5) %>% as.data.frame() %>% mutate(Group = paste0("HIV+ (", dose_hiv, " mg)")) combined <- bind_rows(sim_neg, sim_pos, sim_pos_high) %>% mutate(time_days = time / 24) ggplot(combined, aes(x = time_days, y = DV, color = Group)) + geom_line(linewidth = 0.9) + geom_hline(yintercept = 0.06, linetype = "dashed", color = "#e74c3c", alpha = 0.5) + scale_color_manual(values = c("HIV\u2212" = "#2ecc71", "HIV+" = "#e74c3c", setNames("#3498db", paste0("HIV+ (", dose_hiv, " mg)")))) + labs( x = "Time (days)", y = "Concentration (mg/L)", title = "Impact of HIV Status on Rifapentine Exposure", subtitle = "HIV+ patients need ~30% higher doses to match HIV\u2212 exposure", color = "Group" ) + theme_minimal(base_size = 14) + theme(plot.title = element_text(face = "bold"), legend.position = "top") }) } shinyApp(ui = ui, server = server)