use <- function(pkg, attach = TRUE) { if (attach) { library(pkg, character.only = TRUE) } else { requireNamespace(pkg, quietly = TRUE) } } use("shiny", attach = TRUE) use("bslib", attach = TRUE) use("mrgsolve", attach = TRUE) use("dplyr", attach = TRUE) use("ggplot2", attach = TRUE) model_code <- ' $PARAM @annotated CL : 0.2359 : Clearance (L/day) V : 7.84 : Volume of distribution (L) KA : 1.043 : Absorption rate constant (1/day) F1 : 0.70 : Absolute bioavailability fraction WT : 87 : Body weight (kg) CLWT : 1.11 : Body weight exponent on clearance VWT : 1.00 : Body weight exponent on volume CLMULT: 1.00 : Disease-specific clearance multiplier VMULT : 1.00 : Disease-specific volume multiplier $CMT @annotated DEPOT : Subcutaneous depot (mg) CENT : Central compartment (mg) $MAIN double wt_ratio = WT / 87.0; double CLi = CL * pow(wt_ratio, CLWT) * CLMULT; double Vi = V * pow(wt_ratio, VWT) * VMULT; F_DEPOT = F1; $ODE dxdt_DEPOT = -KA * DEPOT; dxdt_CENT = KA * DEPOT - (CLi / Vi) * CENT; $TABLE double CP = CENT / Vi; double CL_OUT = CLi; double V_OUT = Vi; $CAPTURE @annotated CP : Plasma concentration (mg/L = ug/mL) CL_OUT : Individual clearance (L/day) V_OUT : Individual volume (L) ' mod <- mcode("bimekizumab_pkpd_317", 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; } .small-note { color: #6c757d; font-size: 12px; margin-top: 8px; } .info-grid td { padding: 6px 10px; vertical-align: top; border-bottom: 1px solid #eef2f7; } .info-grid td:first-child { font-weight: 600; width: 220px; } ") regimen_choices <- c( "Single 320 mg SC dose (PK reference)" = "single_320", "Plaque psoriasis label regimen" = "pso_standard", "Plaque psoriasis >=120 kg maintenance option" = "pso_high_weight", "PsA / nr-axSpA / AS label regimen" = "immune_q4w", "Hidradenitis suppurativa label regimen" = "hs_standard", "Custom fixed-interval SC regimen" = "custom" ) build_regimen_schedule <- function(regimen, duration_weeks, custom_dose = 160, custom_interval = 4) { duration_days <- duration_weeks * 7 if (regimen == "single_320") { times <- 0 dose <- 320 current_interval <- 28 label <- "Single 320 mg SC dose" cl_mult <- 1.00 v_mult <- 1.00 } else if (regimen == "pso_standard") { induction <- c(0, 4, 8, 12, 16) * 7 maintenance <- seq(24, ceiling(duration_weeks) + 8, by = 8) * 7 times <- sort(unique(c(induction, maintenance))) times <- times[times <= duration_days] dose <- 320 current_interval <- if (length(times) > 1 && max(times) >= 24 * 7) 56 else 28 label <- "Plaque psoriasis: 320 mg at Weeks 0, 4, 8, 12, 16 then every 8 weeks" cl_mult <- 1.00 v_mult <- 1.00 } else if (regimen == "pso_high_weight") { induction <- c(0, 4, 8, 12, 16) * 7 maintenance <- seq(20, ceiling(duration_weeks) + 4, by = 4) * 7 times <- sort(unique(c(induction, maintenance))) times <- times[times <= duration_days] dose <- 320 current_interval <- 28 label <- "Plaque psoriasis >=120 kg option: 320 mg at Weeks 0, 4, 8, 12, 16 then every 4 weeks" cl_mult <- 1.00 v_mult <- 1.00 } else if (regimen == "immune_q4w") { times <- seq(0, duration_days, by = 28) dose <- 160 current_interval <- 28 label <- "PsA / nr-axSpA / AS: 160 mg every 4 weeks" cl_mult <- 1.00 v_mult <- 1.00 } else if (regimen == "hs_standard") { induction <- seq(0, 16, by = 2) * 7 maintenance <- seq(20, ceiling(duration_weeks) + 4, by = 4) * 7 times <- sort(unique(c(induction, maintenance))) times <- times[times <= duration_days] dose <- 320 current_interval <- if (length(times) > 1 && max(times) >= 20 * 7) 28 else 14 label <- "HS: 320 mg every 2 weeks to Week 16, then every 4 weeks" cl_mult <- 1.31 v_mult <- 1.18 } else { interval_days <- custom_interval * 7 times <- seq(0, duration_days, by = interval_days) dose <- custom_dose current_interval <- interval_days label <- paste0("Custom SC regimen: ", custom_dose, " mg every ", custom_interval, " weeks") cl_mult <- 1.00 v_mult <- 1.00 } if (length(times) == 0) { times <- 0 } list( times = times, dose = dose, current_interval = current_interval, label = label, cl_mult = cl_mult, v_mult = v_mult ) } trap_auc <- function(time, conc) { if (length(time) < 2 || length(conc) < 2) { return(NA_real_) } sum(diff(time) * (head(conc, -1) + tail(conc, -1)) / 2) } ui <- page_sidebar( title = "BIMZELX (Bimekizumab) PK Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, selectInput("regimen", "Label regimen", choices = regimen_choices, selected = "single_320"), conditionalPanel( condition = "input.regimen == 'custom'", sliderInput("custom_dose", "Dose (mg)", min = 80, max = 640, value = 160, step = 20), sliderInput("custom_interval", "Interval (weeks)", min = 1, max = 12, value = 4, step = 1) ), sliderInput("wt", "Body weight (kg)", min = 40, max = 160, value = 87, step = 1), selectInput( "population_type", "Population PK context", choices = c( "Auto from regimen" = "auto", "Standard PSO / PsA / nr-axSpA / AS" = "standard", "HS population" = "hs" ), selected = "auto" ), numericInput("duration_weeks", "Simulation duration (weeks)", value = 32, min = 4, max = 104, step = 4), checkboxInput("log_scale", "Log scale (Y-axis)", value = FALSE), div(class = "small-note", "Weight is modeled as an exposure covariate. Select HS population to apply the label-reported higher apparent CL/F and V/F seen in hidradenitis suppurativa.") ), 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 (ug/mL)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("ctrough")), div(class = "metric-label", "Ctrough (ug/mL)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc")), div(class = "metric-label", "AUCtau (ug*day/mL)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf")), div(class = "metric-label", "t1/2 (days)")) ), card( card_header("Concentration-Time Profile"), full_screen = TRUE, plotOutput("pkPlot", height = "500px"), div(class = "small-note", textOutput("plot_note")) ), navset_card_underline( title = "Drug Information", nav_panel("Model Information", uiOutput("model_info")), nav_panel("References", uiOutput("references")) ), 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) { sim_bundle <- reactive({ schedule <- build_regimen_schedule( regimen = input$regimen, duration_weeks = input$duration_weeks, custom_dose = input$custom_dose, custom_interval = input$custom_interval ) event_df <- data.frame( ID = 1, time = schedule$times, amt = schedule$dose, cmt = 1, evid = 1 ) sim_end <- max(input$duration_weeks * 7, max(schedule$times) + schedule$current_interval) cl_mult <- if (input$population_type == "auto") schedule$cl_mult else if (input$population_type == "hs") 1.31 else 1.00 v_mult <- if (input$population_type == "auto") schedule$v_mult else if (input$population_type == "hs") 1.18 else 1.00 sim <- mod %>% param(WT = input$wt, CLMULT = cl_mult, VMULT = v_mult) %>% data_set(event_df) %>% mrgsim(end = sim_end, delta = 0.25) %>% as.data.frame() |> mutate( time_days = time, time_weeks = time / 7 ) last_dose_time <- max(schedule$times) metric_end <- last_dose_time + schedule$current_interval interval_data <- sim |> dplyr::filter(time_days >= last_dose_time, time_days <= metric_end) list( sim = sim, schedule = schedule, plot_end = input$duration_weeks * 7, interval_data = interval_data, last_dose_time = last_dose_time, metric_end = metric_end, cl_mult = cl_mult, v_mult = v_mult ) }) output$cmax <- renderText({ d <- sim_bundle()$interval_data shiny::req(nrow(d) > 0) sprintf("%.1f", max(d$CP, na.rm = TRUE)) }) output$ctrough <- renderText({ d <- sim_bundle()$interval_data shiny::req(nrow(d) > 0) sprintf("%.1f", d$CP[nrow(d)]) }) output$auc <- renderText({ d <- sim_bundle()$interval_data shiny::req(nrow(d) > 1) auc_val <- trap_auc(d$time_days, d$CP) sprintf("%.1f", auc_val) }) output$thalf <- renderText({ bundle <- sim_bundle() cl_i <- 0.2359 * (input$wt / 87)^1.11 * bundle$cl_mult v_i <- 7.84 * (input$wt / 87)^1.00 * bundle$v_mult sprintf("%.1f", log(2) * v_i / cl_i) }) output$plot_note <- renderText({ paste0( "Metrics are calculated over the final dosing interval after the last simulated dose. Current regimen: ", sim_bundle()$schedule$label ) }) output$pkPlot <- renderPlot({ bundle <- sim_bundle() d <- bundle$sim |> dplyr::filter(time_days <= bundle$plot_end) shiny::req(nrow(d) > 0) d_plot <- if (input$log_scale) { d |> dplyr::filter(CP > 0.001) } else { d } shiny::req(nrow(d_plot) > 0) dose_df <- data.frame(time_weeks = bundle$schedule$times / 7) p <- ggplot(d_plot, aes(x = time_weeks, y = CP)) + geom_vline(data = dose_df, aes(xintercept = time_weeks), color = "#cbd5e1", linetype = "dashed", linewidth = 0.4) + geom_line(color = "#8b5cf6", linewidth = 1) + labs( x = "Time (weeks)", y = "Plasma concentration (ug/mL)", title = "Bimekizumab concentration-time profile" ) + theme_minimal(base_size = 14) if (input$log_scale) { p <- p + scale_y_log10() } if (input$regimen == "single_320") { p <- p + geom_hline(yintercept = 25, color = "#10b981", linetype = "dotted", linewidth = 0.8) + annotate("text", x = 1.5, y = 27, label = "Label median Cmax ≈ 25 ug/mL", color = "#10b981", hjust = 0, size = 3.4) } p }) output$model_info <- renderUI({ bundle <- sim_bundle() info <- bundle$schedule if (input$population_type == "hs" || (input$population_type == "auto" && input$regimen == "hs_standard")) { disease_note <- "HS PK context applies label-based higher apparent clearance (+31%) and volume (+18%) to reflect lower exposure in hidradenitis suppurativa subjects." population_label <- "HS population" } else { disease_note <- "Standard population PK context uses the shared estimates reported across plaque psoriasis, psoriatic arthritis, nr-axSpA, and ankylosing spondylitis." population_label <- "Standard indications" } div( tags$h4("BIMZELX — label-derived 1-compartment SC PopPK model"), tags$p("This simulator uses a simplified first-order absorption, one-compartment model calibrated to the FDA prescribing information for bimekizumab-bkzx."), tags$table( class = "info-grid", tags$tr(tags$td("Model structure"), tags$td("1-compartment SC absorption model")), tags$tr(tags$td("Absolute bioavailability"), tags$td("70%")), tags$tr(tags$td("Apparent clearance (CL/F)"), tags$td("0.337 L/day")), tags$tr(tags$td("Apparent volume (V/F)"), tags$td("11.2 L")), tags$tr(tags$td("Model CL"), tags$td("0.236 L/day after separating F")), tags$tr(tags$td("Model V"), tags$td("7.84 L after separating F")), tags$tr(tags$td("Absorption rate (Ka)"), tags$td("1.043 day^-1, chosen to match Tmax ~3-4 days")), tags$tr(tags$td("Terminal half-life"), tags$td("23 days at 87 kg reference weight")), tags$tr(tags$td("Single-dose PK anchor"), tags$td("320 mg SC: median peak ~25 ug/mL at 3-4 days")), tags$tr(tags$td("Weight effect"), tags$td("Clearance scaled to approximate >=120 kg exposure reduction reported in label")), tags$tr(tags$td("Population context"), tags$td(population_label)), tags$tr(tags$td("Current regimen"), tags$td(info$label)) ), tags$div( class = "ref-box", tags$strong("Clinical interpretation"), tags$ul( tags$li("Plaque psoriasis: 320 mg at Weeks 0, 4, 8, 12, and 16, then every 8 weeks; consider every 4 weeks after Week 16 when body weight is >=120 kg."), tags$li("Psoriatic arthritis, non-radiographic axial spondyloarthritis, and ankylosing spondylitis: 160 mg every 4 weeks."), tags$li("Hidradenitis suppurativa: 320 mg every 2 weeks through Week 16, then every 4 weeks."), tags$li(disease_note) ) ) ) }) output$references <- renderUI({ div( class = "ref-box", tags$h5("Key references"), tags$ol( tags$li( tags$a( href = "https://www.accessdata.fda.gov/drugsatfda_docs/label/2024/761151s005s006s007lbl.pdf", target = "_blank", "BIMZELX (bimekizumab-bkzx) prescribing information" ), " — label-derived PK parameters, dose regimens, and exposure summaries." ), tags$li( tags$a( href = "https://www.ucb-usa.com/bimzelx-prescribing-information.pdf", target = "_blank", "UCB prescribing information mirror" ), " — corroborates label PK values and indication-specific dosing." ) ), tags$h5("What this app covers"), tags$ul( tags$li("Subcutaneous bimekizumab exposure under labeled regimens and custom fixed-interval regimens."), tags$li("Weight-dependent exposure changes and lower HS exposure via disease-specific CL/F and V/F multipliers."), tags$li("Exploratory PK simulation only; no direct efficacy or safety threshold modeling.") ) ) }) } shinyApp(ui = ui, server = server)