library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ============================================================================== # Sertraline Population PK Model — CYP2C19 & Age Covariates # Source: "A Study on Sertraline's Pharmacokinetics and its Interaction with # CYP2C19 Genetic Variants in South Indian Patients: A Pharmacometric # Approach Towards Precision Medicine" # Model: 1-compartment oral (NONMEM ADVAN2 TRANS2), FOCEI # n=104 patients, 258 plasma samples # Final model: CL/F, Vd/F with Age + CYP2C19 genotype covariates on CL # ============================================================================== model_code <- ' $PARAM @annotated CLPOP : 29.8 : Population CL/F at reference (L/h) VD : 2880 : Apparent Vd/F (L) KA : 0.439 : Absorption rate constant (1/h) AGE : 65 : Patient age (years) — reference in model PM : 0 : CYP2C19 Poor Metabolizer (0=EM, 1=PM) THETA4 : 0.0199 : Age covariate on CL (exponential, centered 65y) THETA5 : 0.643 : CYP2C19 PM fractional reduction in CL $CMT @annotated DEPOT : Oral depot compartment (mg) CENT : Central compartment (mg) $MAIN // CL with age (exponential) and CYP2C19 covariate // Age effect: older patients have lower CL (centered at 65y) // CYP2C19 PM: ~64% reduction in CL vs extensive metabolizers double CLi = CLPOP * exp(THETA4 * (65.0 - AGE)) * (1.0 - THETA5 * PM); double Vi = VD; F_DEPOT = 1.0; // apparent parameters (CL/F and Vd/F already account for F) $ODE dxdt_DEPOT = -KA * DEPOT; dxdt_CENT = KA * DEPOT - (CLi / Vi) * CENT; $TABLE // Convert mg/L to ng/mL (× 1000) double CP = (CENT / Vi) * 1000.0; double CL_ind = CLi; $CAPTURE CP CL_ind ' mod <- mcode("sertraline_popmk", model_code, quiet = TRUE) # App theme — PKPDBuilder brand 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; } .metric-danger .metric-value { color: #ef4444; } .genotype-em { background: #e8f5e9; border-color: #4caf50; } .genotype-pm { background: #fff3e0; border-color: #ff9800; } .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; } .param-table th { background: #8b5cf6; color: #fff; } ") ui <- page_sidebar( title = "Sertraline PopPK Simulator — CYP2C19 & Age", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Dosing"), sliderInput("dose", "Dose (mg/day)", min = 25, max = 200, value = 50, step = 25), numericInput("n_days", "Simulation Duration (days)", value = 28, min = 7, max = 60), hr(), h6("Patient Characteristics"), sliderInput("age", "Age (years)", min = 18, max = 85, value = 49, step = 1), selectInput("genotype", "CYP2C19 Genotype", choices = c( "Extensive Metabolizer (EM) — CYP2C19*1/*1" = "EM", "Poor Metabolizer (PM) — CYP2C19*2/*2 or *3/*3" = "PM" ), selected = "EM" ), hr(), checkboxInput("show_comparison", "Overlay EM vs PM comparison", value = FALSE), checkboxInput("log_scale", "Log Scale (Y-axis)", value = FALSE), hr(), div(style = "font-size: 11px; color: #6c757d;", "Study population: n=104 depressive patients (South India)", br(), "Sampling: sparse steady-state", br(), "Model: NONMEM ADVAN2 TRANS2, FOCEI" ) ), uiOutput("pm_ss_warning"), 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,ss (ng/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("css_avg")), div(class = "metric-label", "Css,avg (ng/mL)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf")), div(class = "metric-label", "t½ (h)")) ), card( full_screen = TRUE, height = "500px", plotOutput("pkPlot", height = "460px") ), layout_column_wrap( width = 1/2, fill = FALSE, div(class = "metric-card", div(class = "metric-value", style = "font-size: 18px;", textOutput("cl_ind")), div(class = "metric-label", "Individual CL/F (L/h)")), div(class = "metric-card", div(class = "metric-value", style = "font-size: 18px;", textOutput("ke_ind")), div(class = "metric-label", "Elimination rate ke (h⁻¹)")) ), navset_card_underline( nav_panel("Model Information", markdown(" ## Sertraline — Population PK Model **Drug:** Sertraline hydrochloride (Zoloft®) — selective serotonin reuptake inhibitor (SSRI) **Indication:** Major depressive disorder (MDD), OCD, PTSD, panic disorder **Study population:** 104 South Indian patients, steady-state sparse sampling **Dataset:** 258 plasma concentrations from 104 patients --- ### Structural Model - **Type:** One-compartment, first-order oral absorption (ADVAN2 TRANS2) - **Method:** NONMEM® v7.1, FOCEI - **Parameters reported as apparent** (CL/F and Vd/F; F ≈ 45%) ### Population Parameter Estimates (Final Model) | Parameter | Estimate | IIV (%) | Bootstrap 95% CI | |-----------|----------|---------|-----------------| | CL/F (L/h) | 29.8 | 16% | 28.10–30.50 | | Vd/F (L) | 2880 | 18% | 2599–3045 | | Ka (h⁻¹) | 0.439 | 3% | 0.124–0.730 | ### Covariate Model **Age on CL (exponential, centered at 65 years):** ``` CL = 29.8 × exp(0.0199 × (65 − AGE)) × (1 − 0.643 × I_PM) ``` | Covariate | Effect | Bootstrap 95% CI | |-----------|--------|-----------------| | Age (θ₄) | +1.99%/year below 65y | 0.0186–0.0193 | | CYP2C19 PM (θ₅) | −64.3% CL reduction | 0.383–0.919 | ### Key Pharmacogenetic Finding - **CYP2C19 poor metabolizers** (12.5% of study population) have ~64% lower CL - Results in ~2.8–3.2× higher steady-state concentrations vs extensive metabolizers - **42–48% reduction** in drug:metabolite (Sertraline:DMS) ratio at peak - **Clinical implication:** Dose reduction may be required in elderly and CYP2C19 PMs ### Residual Error Model - Proportional: 2.3% | Additive: 0.67 ng/mL (final model) "), ), nav_panel("References", div(class = "ref-box", tags$h5("📚 Source Publication"), tags$p( "Venkatesan K, et al. ", tags$em("A Study on Sertraline's Pharmacokinetics and its Interaction with CYP2C19 Genetic Variants in South Indian Patients: A Pharmacometric Approach Towards Precision Medicine."), " NONMEM v7.1 PopPK analysis, n=104 patients, 258 plasma samples." ), tags$h5("📚 Supporting References"), tags$ol( tags$li("Rudberg I, et al. (2008) Eur J Clin Pharmacol — Sertraline PK and CYP2C19 genotype at steady state"), tags$li("Sjöqvist F, et al. (2008) Lancet Neurol — Pharmacogenomics of antidepressants"), tags$li("Xu ZH, et al. (2002) Br J Clin Pharmacol — CYP2C19 single-dose PK, Chinese population"), tags$li("FDA Label — Zoloft® (sertraline hydrochloride) prescribing information") ), tags$h5("💊 Clinical Context"), tags$ul( tags$li(tags$strong("Drug class:"), " SSRI antidepressant"), tags$li(tags$strong("Doses studied:"), " 25–200 mg/day (once daily)"), tags$li(tags$strong("Metabolism:"), " CYP2C19, CYP2B6, CYP2C9, CYP2D6, CYP3A4"), tags$li(tags$strong("Major metabolite:"), " Desmethylsertraline (DMS) — less pharmacologically active"), tags$li(tags$strong("Bioavailability:"), " ~45%"), tags$li(tags$strong("PM frequency:"), " 12.5% in South Indian population (vs ~3% in Caucasians)"), tags$li(tags$strong("Therapeutic range:"), " 10–200 ng/mL (literature)") ), tags$h5("⚠️ Disclaimer"), tags$p(style = "font-size:12px; color:#6c757d;", "This simulator is for research and educational purposes only. ", "It is based on population PK estimates and should not be used for individual clinical dosing decisions. ", "Consult current prescribing information and clinical guidelines." ) ) ) ), 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"), " — Interactive PK/PD Simulators from Published Literature", br(), tags$span(style = "font-size: 10px;", "For research and educational purposes only. Not for clinical decision-making.") ) ) # ─── Server ─────────────────────────────────────────────────────────────────── server <- function(input, output, session) { # Helper: build events for a patient make_events <- function(dose, n_days) { ev(amt = dose, cmt = 1, ii = 24, addl = n_days - 1) } # Simulate one profile run_sim <- function(dose, age, pm_flag, n_days) { shiny::req(dose, age, n_days) ev1 <- make_events(dose, n_days) mod |> param(AGE = age, PM = pm_flag) |> ev(ev1) |> mrgsim(end = n_days * 24, delta = 0.5) |> as.data.frame() } pm_val <- reactive({ if (input$genotype == "PM") 1 else 0 }) # PM steady-state warning output$pm_ss_warning <- renderUI({ shiny::req(input$genotype, input$age, input$n_days) if (pm_val() == 1) { cl_pm <- 29.8 * exp(0.0199 * (65 - input$age)) * (1 - 0.643) t_half_pm <- log(2) / (cl_pm / 2880) pct_ss <- (1 - 0.5^(input$n_days * 24 / t_half_pm)) * 100 if (input$n_days < 28) { div( class = "alert alert-warning", role = "alert", style = "margin: 10px 0; font-size: 13px;", tags$strong("\u26a0\ufe0f Steady-State Warning: "), sprintf( "CYP2C19 PM t\u00bd \u2248 %.0fh at age %d. Current %d-day window reaches %.1f%% of true steady state. Increase duration to \u226528 days for accurate Css metrics.", t_half_pm, input$age, input$n_days, pct_ss ) ) } } }) sim_main <- reactive({ run_sim(input$dose, input$age, pm_val(), input$n_days) }) sim_em <- reactive({ run_sim(input$dose, input$age, 0, input$n_days) }) sim_pm <- reactive({ run_sim(input$dose, input$age, 1, input$n_days) }) # Steady-state window: last dosing interval ss_window <- reactive({ d <- sim_main() last_dose_time <- (input$n_days - 1) * 24 dplyr::filter(d, time >= last_dose_time) }) # Metrics output$cmax <- renderText({ ss <- ss_window() sprintf("%.1f", max(ss$CP, na.rm = TRUE)) }) output$ctrough <- renderText({ ss <- ss_window() sprintf("%.1f", min(ss$CP, na.rm = TRUE)) }) output$css_avg <- renderText({ ss <- ss_window() # Trapezoidal AUC over last interval / tau auc <- sum(diff(ss$time) * (head(ss$CP, -1) + tail(ss$CP, -1)) / 2) tau <- tail(ss$time, 1) - head(ss$time, 1) if (tau > 0) sprintf("%.1f", auc / tau) else "—" }) output$thalf <- renderText({ d <- sim_main() if (nrow(d) < 2) return("—") cl_val <- mean(d$CL_ind[d$time > 0], na.rm = TRUE) ke_calc <- cl_val / 2880 sprintf("%.1f", log(2) / ke_calc) }) output$cl_ind <- renderText({ d <- sim_main() cl_val <- mean(d$CL_ind[d$time > 0], na.rm = TRUE) sprintf("%.1f", cl_val) }) output$ke_ind <- renderText({ d <- sim_main() cl_val <- mean(d$CL_ind[d$time > 0], na.rm = TRUE) ke_calc <- cl_val / 2880 sprintf("%.4f", ke_calc) }) # PK plot output$pkPlot <- renderPlot({ shiny::req(sim_main()) # Therapeutic window (literature: 10–200 ng/mL) tw_lo <- 10 tw_hi <- 200 if (input$show_comparison) { # Overlay EM and PM d_em <- sim_em() |> mutate(Genotype = "EM (CYP2C19*1/*1)") d_pm <- sim_pm() |> mutate(Genotype = "PM (CYP2C19*2/*2)") d_plot <- bind_rows(d_em, d_pm) p <- ggplot(d_plot, aes(x = time, y = CP, color = Genotype, linetype = Genotype)) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = tw_lo, ymax = tw_hi, fill = "#10b981", alpha = 0.1) + geom_hline(yintercept = c(tw_lo, tw_hi), linetype = "dashed", color = "#10b981", alpha = 0.5) + annotate("text", x = max(d_plot$time) * 0.01, y = tw_hi * 0.92, label = "Therapeutic: 10–200 ng/mL", hjust = 0, size = 3.5, color = "#10b981") + geom_line(linewidth = 0.9) + scale_color_manual(values = c("EM (CYP2C19*1/*1)" = "#3b82f6", "PM (CYP2C19*2/*2)" = "#f97316")) + scale_linetype_manual(values = c("EM (CYP2C19*1/*1)" = "solid", "PM (CYP2C19*2/*2)" = "longdash")) + labs( x = "Time (hours)", y = "Sertraline Concentration (ng/mL)", title = sprintf("Sertraline %g mg QD — EM vs PM Comparison (Age %d y)", input$dose, input$age), color = "Genotype", linetype = "Genotype" ) + theme_minimal(base_size = 14) + theme(legend.position = "top") } else { d <- sim_main() geno_label <- if (pm_val() == 1) "CYP2C19 PM" else "CYP2C19 EM" line_col <- if (pm_val() == 1) "#f97316" else "#8b5cf6" p <- ggplot(d, aes(x = time, y = CP)) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = tw_lo, ymax = tw_hi, fill = "#10b981", alpha = 0.12) + geom_hline(yintercept = c(tw_lo, tw_hi), linetype = "dashed", color = "#10b981", alpha = 0.6) + annotate("text", x = max(d$time) * 0.01, y = tw_hi * 0.92, label = "Therapeutic: 10–200 ng/mL", hjust = 0, size = 3.5, color = "#10b981") + geom_line(color = line_col, linewidth = 0.9) + labs( x = "Time (hours)", y = "Sertraline Concentration (ng/mL)", title = sprintf("Sertraline %g mg QD — %s, Age %d y", input$dose, geno_label, input$age) ) + theme_minimal(base_size = 14) } if (input$log_scale) { d_check <- if (input$show_comparison) d_plot else sim_main() # Filter zeros before log scale p <- p + scale_y_log10(limits = c(max(0.1, min(d_check$CP[d_check$CP > 0], na.rm = TRUE) * 0.5), NA)) } p }) } shinyApp(ui = ui, server = server)