library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) # ============================================================================= # Inotersen (Tegsedi) Population PK/PD Model # Source: Yu RZ et al. (2020) Nucleic Acid Therapeutics 30(3):153-163 # Two-compartment PK + Indirect Response PD (inhibition of TTR production) # ============================================================================= model_code <- ' $PARAM @annotated CL : 3.4 : Clearance CL/F (L/h) VC : 20.7 : Central volume Vc/F (L) KA : 0.261 : Absorption rate constant (1/h) VP : 230 : Peripheral volume Vp/F (L) QP : 0.266 : Intercompartmental clearance Q/F (L/h) IMAX : 0.913 : Maximum inhibition of TTR production IC50 : 9.07 : IC50 for TTR inhibition (ng/mL) KOUT : 0.00308 : TTR turnover rate constant (1/h) BASE : 20.4 : Baseline serum TTR (mg/dL) LBM : 51.7 : Lean body mass (kg) $CMT @annotated SC : Subcutaneous depot (mg) CENT : Central compartment (mg) PERI : Peripheral compartment (mg) TTR : Serum transthyretin (mg/dL) $MAIN double CLi = CL * (LBM / 51.7); double VCi = VC * (LBM / 51.7); double QPi = QP * (LBM / 51.7); double VPi = VP * (LBM / 51.7); double KIN = KOUT * BASE; TTR_0 = BASE; $ODE double CP_ng = (CENT / VCi) * 1000.0; double INH = IMAX * CP_ng / (IC50 + CP_ng); dxdt_SC = -KA * SC; dxdt_CENT = KA * SC - (CLi / VCi) * CENT - (QPi / VCi) * CENT + (QPi / VPi) * PERI; dxdt_PERI = (QPi / VCi) * CENT - (QPi / VPi) * PERI; dxdt_TTR = KIN * (1.0 - INH) - KOUT * TTR; $TABLE double CP = (CENT / VCi) * 1000.0; double TTRout = TTR; double TTRpct = ((TTR - BASE) / BASE) * 100.0; $CAPTURE CP TTRout TTRpct ' mod <- mcode("inotersen_pkpd", 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; } .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 # ============================================================================= ui <- page_sidebar( title = "Inotersen (Tegsedi\u2122) PK/PD Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, h6("Dosing Regimen"), selectInput("dose", "Dose (mg)", choices = c("150 mg" = 150, "300 mg" = 300), selected = 300), selectInput("schedule", "Schedule", choices = c("Once Weekly (QW)" = "QW", "Every 2 Weeks (Q2W)" = "Q2W"), selected = "QW"), checkboxInput("loading", "Loading Dose (Days 1, 3, 5)", value = TRUE), sliderInput("n_weeks", "Duration (weeks)", min = 13, max = 65, value = 65, step = 1), hr(), h6("Patient Characteristics"), sliderInput("lbm", "Lean Body Mass (kg)", min = 31, max = 80, value = 52, step = 1), sliderInput("base_ttr", "Baseline Serum TTR (mg/dL)", min = 5, max = 40, value = 20, step = 1), hr(), checkboxInput("log_scale", "Log Scale (PK Y-axis)", value = FALSE) ), navset_card_tab( title = "Inotersen PK/PD Simulator", full_screen = TRUE, # --- Simulation Tab --- 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 (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", paste0("AUC0-\u03C4 (\u00B5g\u00B7h/mL)"))), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("ttr_red")), div(class = "metric-label", "TTR Reduction (%)")) ), layout_columns( col_widths = c(6, 6), plotOutput("pkPlot", height = "420px"), plotOutput("pdPlot", height = "420px") ) ), # --- Population Simulation Tab --- nav_panel("Population Simulation", layout_columns( col_widths = c(4, 4, 4), numericInput("n_patients", "Number of Patients", value = 500, min = 50, max = 2000, step = 50), sliderInput("omega_CL", "IIV on CL (CV%)", min = 10, max = 60, value = 35, step = 5), sliderInput("omega_PD", "IIV on PD (CV%)", min = 5, max = 40, value = 20, step = 5) ), layout_column_wrap( width = 1/4, fill = FALSE, div(class = "metric-card metric-success", div(class = "metric-value", textOutput("pop_median_kd")), div(class = "metric-label", "Median TTR KD (%)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("pop_below_target")), div(class = "metric-label", "Below siRNA bar (<80%)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("pop_above_75")), div(class = "metric-label", "≥80% KD (siRNA benchmark)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("pop_p5_p95")), div(class = "metric-label", "P5 - P95 Range")) ), plotOutput("popTTRHist", height = "340px"), plotOutput("popLBMScatter", height = "300px"), div(class = "ref-box", tags$p(tags$strong("Interpretation:"), " The histogram shows the distribution of steady-state TTR knockdown across a virtual patient population. The dashed green line marks the ≥80% siRNA benchmark (patisiran/vutrisiran equivalent). Patients in yellow fall below this bar; patients in dark red reach siRNA-equivalent knockdown. At ~$450,000/year, how many patients actually achieve what competing siRNA therapies deliver routinely?"), tags$p("The scatter plot below reveals how lean body mass drives the spread: smaller patients achieve higher exposures and deeper knockdown at the same dose. This is the core argument for dose individualization.") ) ), # --- Model Information Tab --- nav_panel("Model Information", markdown(" ## Inotersen (Tegsedi\u2122) \u2014 Population PK/PD Model **Drug:** Inotersen (ISIS 420915), a 2'-O-(2-methoxyethyl) modified phosphorothioate antisense oligonucleotide **Brand Name:** Tegsedi\u2122 (Ionis Pharmaceuticals / Akcea Therapeutics) **Route:** Subcutaneous (SC) injection **Indication:** Stage 1 or 2 polyneuropathy in adults with hereditary transthyretin amyloidosis (hATTR-PN) ### PK Model \u2014 Two-Compartment with First-Order SC Absorption | Parameter | Value | Description | |-----------|-------|-------------| | Ka | 0.261 h\u207B\u00B9 | Absorption rate constant | | CL/F | 3.4 L/h | Apparent clearance | | Vc/F | 20.7 L | Apparent central volume | | Q/F | 0.266 L/h | Intercompartmental clearance | | Vp/F | 230 L | Apparent peripheral volume | | \u03B1 t\u00BD | 3.91 h | Distribution half-life | | \u03B2 t\u00BD | 26.9 days | Terminal elimination half-life | ### PD Model \u2014 Indirect Response (Inhibition of TTR Production) | Parameter | Value | Description | |-----------|-------|-------------| | Imax | 0.913 | Maximum inhibition of TTR synthesis | | IC50 | 9.07 ng/mL | Concentration at 50% of max effect | | kout | 0.00308 h\u207B\u00B9 | TTR turnover rate constant | | Baseline TTR | 20.4 mg/dL | Population typical baseline | ### Covariates - **Lean body mass (LBM):** Linear scaling on CL, Q, Vc, Vp (centered on median 51.7 kg) - **Disease status:** ~11% effect on CL, ~28% on Vc (patients vs. healthy; small clinical impact) - Age, race, sex, renal function (eGFR), and hepatic markers were **not** significant covariates ### Therapeutic Target - The original NEURO-TTR trial demonstrated benefit at >50% TTR reduction - However, siRNA therapies (patisiran ~80%, vutrisiran ~85\u201390%) now set the clinical benchmark at **\u226580% TTR knockdown** - Inotersen 300 mg QW achieves median ~72\u201379% TTR reduction \u2014 many patients fall short of the siRNA benchmark - Loading doses (Days 1, 3, 5) accelerate time to target but do not change steady-state response ")), # --- References Tab --- nav_panel("References", div(class = "ref-box", tags$h5("\uD83D\uDCDA Key References"), tags$ol( tags$li("Yu RZ, Collins JW, Hall S, Ackermann EJ, Geary RS, Monia BP, Henry SP, Wang Y. (2020) Population Pharmacokinetic\u2013Pharmacodynamic Modeling of Inotersen. ", tags$em("Nucleic Acid Therapeutics"), " 30(3):153-163. ", tags$a(href = "https://doi.org/10.1089/nat.2019.0822", "DOI: 10.1089/nat.2019.0822", target = "_blank")), tags$li("Benson MD et al. (2018) Inotersen Treatment for Patients with Hereditary Transthyretin Amyloidosis. ", tags$em("N Engl J Med"), " 379:22-31."), tags$li("FDA Full Prescribing Information: TEGSEDI (inotersen) injection, NDA 211172. ", tags$a(href = "https://www.accessdata.fda.gov/drugsatfda_docs/label/2018/211172lbl.pdf", "FDA Label", target = "_blank")), tags$li("Liefaard L et al. (2013) Predicting Levels of Pharmacological Response. PAGE Meeting Abstract III-15.") ), tags$h5("\uD83D\uDC8A Therapeutic Context"), tags$ul( tags$li(tags$strong("Drug class:"), " Antisense oligonucleotide (ASO)"), tags$li(tags$strong("Molecular target:"), " TTR mRNA \u2014 inhibits both mutant and wild-type TTR production via RNase H-mediated degradation"), tags$li(tags$strong("Approved dose:"), " 300 mg SC once weekly"), tags$li(tags$strong("Monitoring:"), " Platelet count (risk of thrombocytopenia); serum TTR levels to assess PD response"), tags$li(tags$strong("Other hATTR therapies:"), " Patisiran (Onpattro, siRNA), tafamidis (Vyndaqel, TTR stabilizer), eplontersen (Wainua, ligand-conjugated ASO)"), tags$li(tags$strong("Therapeutic area:"), " Hereditary transthyretin amyloidosis (hATTR), rare neurological disease") ) )) ), # --- 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) interval_h <- ifelse(input$schedule == "QW", 168, 336) n_hours <- input$n_weeks * 7 * 24 if (input$loading) { # Loading: Days 1, 3, 5 (times 0, 48, 96h), then maintenance from Day 8 load_ev <- ev(amt = dose_mg, cmt = 1, time = 0) + ev(amt = dose_mg, cmt = 1, time = 48) + ev(amt = dose_mg, cmt = 1, time = 96) addl_maint <- max(0, floor((n_hours - 168) / interval_h) - 1) maint_ev <- ev(amt = dose_mg, cmt = 1, time = 168, ii = interval_h, addl = addl_maint) ev_all <- load_ev + maint_ev } else { addl_val <- max(0, floor(n_hours / interval_h) - 1) ev_all <- ev(amt = dose_mg, cmt = 1, ii = interval_h, addl = addl_val) } mod %>% param(LBM = input$lbm, BASE = input$base_ttr) %>% ev(ev_all) %>% mrgsim(end = n_hours, delta = 2) %>% as.data.frame() %>% mutate(time_weeks = time / (24 * 7), time_days = time / 24) }) # --- Metric: Cmax at steady state (last dosing interval) --- output$cmax <- renderText({ d <- sim_data() interval_h <- ifelse(input$schedule == "QW", 168, 336) last_start <- max(d$time) - interval_h last <- d %>% filter(time >= last_start) sprintf("%.0f", max(last$CP, na.rm = TRUE)) }) # --- Metric: Ctrough at steady state --- output$ctrough <- renderText({ d <- sim_data() interval_h <- ifelse(input$schedule == "QW", 168, 336) last_start <- max(d$time) - interval_h last <- d %>% filter(time >= last_start) sprintf("%.1f", min(last$CP, na.rm = TRUE)) }) # --- Metric: AUC over last dosing interval --- output$auc <- renderText({ d <- sim_data() interval_h <- ifelse(input$schedule == "QW", 168, 336) last_start <- max(d$time) - interval_h last <- d %>% filter(time >= last_start, time <= last_start + interval_h) %>% arrange(time) if (nrow(last) < 2) return("\u2014") auc_ng <- sum(diff(last$time) * (head(last$CP, -1) + tail(last$CP, -1)) / 2) sprintf("%.1f", auc_ng / 1000) }) # --- Metric: TTR reduction at end of simulation --- output$ttr_red <- renderText({ d <- sim_data() last_pct <- tail(d$TTRpct, 1) sprintf("%.1f%%", last_pct) }) # --- PK Concentration-Time Plot --- output$pkPlot <- renderPlot({ d <- sim_data() dose_label <- paste0(input$dose, " mg ", input$schedule, if (input$loading) " + Loading" else "") p <- ggplot(d, aes(x = time_weeks, y = CP)) + geom_line(color = "#8b5cf6", linewidth = 0.5) + labs(x = "Time (weeks)", y = "Plasma Concentration (ng/mL)", title = paste0("Inotersen ", dose_label, " \u2014 Plasma PK Profile")) + theme_minimal(base_size = 13) + theme(plot.title = element_text(size = 13, face = "bold")) if (input$log_scale) p <- p + scale_y_log10() p }) # --- PD TTR Response Plot --- output$pdPlot <- renderPlot({ d <- sim_data() ggplot(d, aes(x = time_weeks, y = TTRpct)) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = -100, ymax = -80, fill = "#10b981", alpha = 0.10) + geom_hline(yintercept = -80, linetype = "dashed", color = "#10b981", alpha = 0.7) + annotate("text", x = max(d$time_weeks) * 0.02, y = -83, label = "Target: \u226580% TTR KD (siRNA benchmark)", hjust = 0, vjust = 1, size = 3.5, color = "#10b981") + geom_hline(yintercept = 0, linetype = "dotted", color = "grey60") + geom_line(color = "#e74c3c", linewidth = 0.6) + labs(x = "Time (weeks)", y = "TTR Change from Baseline (%)", title = "Serum Transthyretin (TTR) Response") + theme_minimal(base_size = 13) + theme(plot.title = element_text(size = 13, face = "bold")) + scale_y_continuous(limits = c(-100, 10)) }) # ====== Population Simulation ====== pop_sim <- reactive({ set.seed(42) n_pat <- input$n_patients dose_mg <- as.numeric(input$dose) interval_h <- ifelse(input$schedule == "QW", 168, 336) omega_cl <- input$omega_CL / 100 omega_pd <- input$omega_PD / 100 base_ttr <- input$base_ttr # Generate population with IIV lbm_pop <- rnorm(n_pat, mean = input$lbm, sd = 12) lbm_pop <- pmax(lbm_pop, 25) results <- data.frame( id = 1:n_pat, lbm = lbm_pop, ttr_kd = NA_real_ ) for (i in 1:n_pat) { eta_cl <- rnorm(1, 0, omega_cl) eta_vc <- rnorm(1, 0, 0.30) eta_pd <- rnorm(1, 0, omega_pd) i_cl <- 3.4 * (lbm_pop[i] / 51.7) * exp(eta_cl) i_vc <- 20.7 * (lbm_pop[i] / 51.7) * exp(eta_vc) # Steady-state Cavg (ng/mL) cavg <- (dose_mg / (i_cl * interval_h)) * 1e6 / 1e3 # Individual Imax i_imax <- min(0.913 * exp(eta_pd), 1.0) inh <- i_imax * cavg / (9.07 + cavg) ttr_ss <- base_ttr * (1 - inh) results$ttr_kd[i] <- ((base_ttr - ttr_ss) / base_ttr) * 100 } results$ttr_kd <- pmax(pmin(results$ttr_kd, 100), 0) results }) output$pop_median_kd <- renderText({ d <- pop_sim() sprintf("%.0f%%", median(d$ttr_kd)) }) output$pop_below_target <- renderText({ d <- pop_sim() sprintf("%.0f%%", sum(d$ttr_kd < 80) / nrow(d) * 100) }) output$pop_above_75 <- renderText({ d <- pop_sim() sprintf("%.0f%%", sum(d$ttr_kd >= 80) / nrow(d) * 100) }) output$pop_p5_p95 <- renderText({ d <- pop_sim() q <- quantile(d$ttr_kd, c(0.05, 0.95)) sprintf("%.0f%% - %.0f%%", q[1], q[2]) }) output$popTTRHist <- renderPlot({ d <- pop_sim() d$category <- cut(d$ttr_kd, breaks = c(-Inf, 50, 80, Inf), labels = c("Below siRNA bar (<80%)", "Partial (50-80%)", "At siRNA benchmark (≥80%)")) ggplot(d, aes(x = ttr_kd, fill = category)) + geom_histogram(bins = 35, color = "white", linewidth = 0.3) + geom_vline(xintercept = 80, linetype = "dashed", color = "#10b981", linewidth = 1.2) + annotate("text", x = 81, y = Inf, label = "\u226580% siRNA benchmark", hjust = 0, vjust = 2, color = "#10b981", fontface = "bold", size = 4) + scale_fill_manual(values = c( "Below siRNA bar (<80%)" = "#f59e0b", "Partial (50-80%)" = "#e74c3c", "At siRNA benchmark (≥80%)" = "#991b1b" )) + labs( x = "TTR Knockdown (%)", y = "Number of Patients", title = paste0("Population TTR Knockdown Distribution (n=", nrow(d), ", ", input$dose, " mg ", input$schedule, ")"), fill = NULL ) + theme_minimal(base_size = 13) + theme( plot.title = element_text(size = 13, face = "bold"), legend.position = "bottom" ) + scale_x_continuous(limits = c(0, 100), breaks = seq(0, 100, 10)) }) output$popLBMScatter <- renderPlot({ d <- pop_sim() ggplot(d, aes(x = lbm, y = ttr_kd)) + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 80, ymax = 100, fill = "#10b981", alpha = 0.08) + geom_hline(yintercept = 80, linetype = "dashed", color = "#10b981", linewidth = 0.8) + geom_point(aes(color = ttr_kd >= 80), alpha = 0.5, size = 1.5) + geom_smooth(method = "loess", se = TRUE, color = "#8b5cf6", fill = "#8b5cf6", alpha = 0.15) + scale_color_manual(values = c("TRUE" = "#e74c3c", "FALSE" = "#f59e0b"), labels = c("TRUE" = "At siRNA bar", "FALSE" = "Below siRNA bar")) + labs( x = "Lean Body Mass (kg)", y = "TTR Knockdown (%)", title = "TTR Knockdown vs. Lean Body Mass", color = NULL ) + theme_minimal(base_size = 13) + theme( plot.title = element_text(size = 13, face = "bold"), legend.position = "bottom" ) + scale_y_continuous(limits = c(0, 100)) }) } shinyApp(ui = ui, server = server)