library(shiny) library(bslib) library(mrgsolve) library(dplyr) library(ggplot2) model_code <- ' $PARAM @annotated CL : 0.235 : Typical clearance (L/day) VC : 3.11 : Central volume (L) Q : 0.534 : Intercompartmental clearance (L/day) VP : 2.46 : Peripheral volume (L) ALBU : 3.9 : Baseline serum albumin (g/dL) LBW : 49 : Lean body weight (kg) $CMT @annotated CENT : Central compartment (mg) PERIPH : Peripheral compartment (mg) $MAIN double CLi = CL * pow(ALBU / 3.9, -1.06) * pow(LBW / 49.0, 0.516); double VCi = VC * pow(LBW / 49.0, 0.747); double Qi = Q; double VPi = VP * pow(LBW / 49.0, 0.83); $ODE dxdt_CENT = -(CLi / VCi) * CENT - (Qi / VCi) * CENT + (Qi / VPi) * PERIPH; dxdt_PERIPH = (Qi / VCi) * CENT - (Qi / VPi) * PERIPH; $TABLE double CP = CENT / VCi; $CAPTURE @annotated CP : Serum pertuzumab concentration (mg/L) CLi : Individual clearance (L/day) VCi : Individual central volume (L) VPi : Individual peripheral volume (L) ' mod <- mcode("pertuzumab_pkpd_302", 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 = "Pertuzumab (Perjeta) Population PK Simulator", theme = app_theme, sidebar = sidebar( title = "Simulation Settings", width = 340, accordion( open = c("Dosing", "Patient"), accordion_panel( "Dosing", icon = bsicons::bs_icon("capsule"), radioButtons( "dose_strategy", "Dosing strategy", choices = c( "Fixed 840/420 mg q3w" = "fixed", "Weight-based 12/6 mg/kg q3w" = "weight" ), selected = "fixed" ), conditionalPanel( "input.dose_strategy === 'fixed'", numericInput("loading_dose", "Loading dose (mg)", value = 840, min = 420, max = 2000, step = 10), numericInput("maintenance_dose", "Maintenance dose (mg)", value = 420, min = 210, max = 2000, step = 10) ), conditionalPanel( "input.dose_strategy === 'weight'", sliderInput("loading_mgkg", "Loading dose (mg/kg)", min = 6, max = 20, value = 12, step = 0.5), sliderInput("maintenance_mgkg", "Maintenance dose (mg/kg)", min = 3, max = 12, value = 6, step = 0.5) ), sliderInput("interval_days", "Dosing interval (days)", min = 14, max = 28, value = 21, step = 1), numericInput("n_cycles", "Number of cycles", value = 6, min = 1, max = 12), sliderInput("infusion_h", "Infusion duration (hours)", min = 0.5, max = 2.0, value = 1.0, step = 0.25) ), accordion_panel( "Patient", icon = bsicons::bs_icon("person"), sliderInput("wt", "Actual body weight (kg)", min = 36, max = 150, value = 72, step = 1), sliderInput("lbw", "Lean body weight (kg)", min = 25, max = 90, value = 49, step = 1), sliderInput("albu", "Serum albumin (g/dL)", min = 2.0, max = 5.5, value = 3.9, step = 0.1), tags$p( style = "font-size:12px; color:#6c757d; margin-top:8px;", "Lean body weight and albumin drive PK covariate effects in the final model. Actual body weight is used for the weight-based dosing option." ) ), accordion_panel( "Display", icon = bsicons::bs_icon("gear"), checkboxInput("show_target", "Show target trough threshold (20 mg/L)", value = TRUE), checkboxInput("log_scale", "Log scale (Y-axis)", value = FALSE) ) ) ), tags$p( style = "font-size: 11px; color: #6c757d; margin: 4px 0 0 6px;", "Default values reflect the approximate median study population: WT 72 kg, LBW 49 kg, albumin 3.9 g/dL." ), 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 (mg/L)")), div(class = "metric-card metric-warning", div(class = "metric-value", textOutput("ctrough")), div(class = "metric-label", "Ctrough,ss (mg/L)")), div(class = "metric-card metric-primary", div(class = "metric-value", textOutput("auc")), div(class = "metric-label", "AUCtau,ss (mg.day/L)")), div(class = "metric-card metric-info", div(class = "metric-value", textOutput("thalf")), div(class = "metric-label", "t1/2 (days)")) ), card( card_header("Serum Concentration-Time Profile"), full_screen = TRUE, plotOutput("pkPlot", height = "500px") ), navset_card_underline( title = "Drug Information", nav_panel("Model Information", markdown("## Pertuzumab population PK model\n\n**Primary paper:** Garg A et al. *Cancer Chemotherapy and Pharmacology* (2014) 74:819-829 \n**PMID:** 25119184 \n**Drug class:** HER2-directed monoclonal antibody \n**Route:** Intravenous infusion\n\n### Final structural model\n- Two-compartment linear model with first-order elimination from the central compartment\n- Population built from 4,525 serum concentrations in 481 patients with solid tumors\n- Terminal half-life approximately 18.0 days\n\n### Typical PK parameters\n| Parameter | Estimate | Units |\n|-----------|----------|-------|\n| CL | 0.235 | L/day |\n| Vc | 3.11 | L |\n| Q | 0.534 | L/day |\n| Vp | 2.46 | L |\n\n### Covariate effects in the final model\n- Clearance increases with lower albumin and higher lean body weight\n- Central and peripheral volumes increase with lean body weight\n- Covariate effects were statistically significant but not clinically large enough to require dose adjustment\n\n### Dosing conclusion from the paper\n- Fixed dosing: **840 mg loading dose, then 420 mg every 3 weeks**\n- Weight-based comparator: **12 mg/kg loading, then 6 mg/kg every 3 weeks**\n- The fixed regimen maintained most patients above the target **Ctrough >= 20 mg/L** threshold\n\n### Exposure context\nAt steady state with fixed dosing, the paper reported median trough concentrations around **51.6 mg/L** overall, with the 5th percentile at **16.0 mg/L** and about **7.6%** of patients below **20 mg/L**.\n")), nav_panel("References", div(class = "ref-box", tags$h5("Primary reference"), tags$ol( tags$li( tags$strong("Garg A, Quartino A, Li J, et al. (2014)."), " Population pharmacokinetic and covariate analysis of pertuzumab, a HER2-targeted monoclonal antibody, and evaluation of a fixed, non-weight-based dose in patients with a variety of solid tumors. ", tags$em("Cancer Chemother Pharmacol"), " 74(4):819-829. ", tags$a(href = "https://doi.org/10.1007/s00280-014-2560-3", target = "_blank", "doi:10.1007/s00280-014-2560-3") ), tags$li( "PERJETA (pertuzumab) prescribing information. Genentech/Roche." ) ), tags$h5("Therapeutic context"), tags$ul( tags$li(tags$strong("Class:"), " HER2-directed monoclonal antibody"), tags$li(tags$strong("Therapeutic area:"), " Oncology, HER2-positive solid tumors and breast cancer regimens"), tags$li(tags$strong("Typical regimen:"), " 840 mg loading dose followed by 420 mg every 3 weeks"), tags$li(tags$strong("Clinical PK message:"), " Fixed dosing is supported despite statistically significant albumin and lean body weight effects"), tags$li(tags$strong("Target used in the paper:"), " Steady-state trough concentration >= 20 mg/L") ) )) ), 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) { regimen_label <- reactive({ if (input$dose_strategy == "fixed") { paste0(input$loading_dose, " mg load, then ", input$maintenance_dose, " mg q", input$interval_days, "d") } else { paste0(input$loading_mgkg, " mg/kg load, then ", input$maintenance_mgkg, " mg/kg q", input$interval_days, "d") } }) regimen_events <- reactive({ shiny::req(input$infusion_h, input$n_cycles, input$interval_days, input$wt) infusion_days <- input$infusion_h / 24 if (input$dose_strategy == "fixed") { loading_amt <- input$loading_dose maintenance_amt <- input$maintenance_dose } else { loading_amt <- input$loading_mgkg * input$wt maintenance_amt <- input$maintenance_mgkg * input$wt } events <- list( ev(time = 0, amt = loading_amt, cmt = 1, rate = loading_amt / infusion_days) ) if (input$n_cycles > 1) { for (i in seq_len(input$n_cycles - 1)) { events[[length(events) + 1]] <- ev( time = i * input$interval_days, amt = maintenance_amt, cmt = 1, rate = maintenance_amt / infusion_days ) } } do.call(c, events) }) sim_data <- reactive({ shiny::req(input$lbw, input$albu) total_days <- input$interval_days * input$n_cycles mod %>% param(ALBU = input$albu, LBW = input$lbw) %>% ev(regimen_events()) %>% mrgsim(end = total_days, delta = 0.1) %>% as.data.frame() |> dplyr::mutate(time_days = time) }) last_interval_data <- reactive({ d <- sim_data() interval_start <- input$interval_days * (input$n_cycles - 1) d |> dplyr::filter(time_days >= interval_start, time_days <= interval_start + input$interval_days) }) output$cmax <- renderText({ d <- last_interval_data() shiny::req(nrow(d) > 0) sprintf("%.1f", max(d$CP, na.rm = TRUE)) }) output$ctrough <- renderText({ d <- last_interval_data() |> dplyr::filter(CP > 0) shiny::req(nrow(d) > 0) sprintf("%.1f", tail(d$CP, 1)) }) output$auc <- renderText({ d <- last_interval_data() shiny::req(nrow(d) > 1) auc_val <- sum(diff(d$time_days) * (head(d$CP, -1) + tail(d$CP, -1)) / 2) sprintf("%.0f", auc_val) }) output$thalf <- renderText({ cl_i <- 0.235 * (input$albu / 3.9)^(-1.06) * (input$lbw / 49)^0.516 vc_i <- 3.11 * (input$lbw / 49)^0.747 q_i <- 0.534 vp_i <- 2.46 * (input$lbw / 49)^0.83 k10 <- cl_i / vc_i k12 <- q_i / vc_i k21 <- q_i / vp_i beta <- 0.5 * ((k10 + k12 + k21) - sqrt((k10 + k12 + k21)^2 - 4 * k10 * k21)) sprintf("%.1f", log(2) / beta) }) output$pkPlot <- renderPlot({ d <- sim_data() shiny::req(nrow(d) > 0) plot_data <- d if (isTRUE(input$log_scale)) { plot_data <- plot_data |> dplyr::filter(CP > 0) } ymax <- max(plot_data$CP, na.rm = TRUE) p <- ggplot(plot_data, aes(x = time_days, y = CP)) if (isTRUE(input$show_target) && is.finite(ymax) && ymax > 20) { p <- p + annotate("rect", xmin = -Inf, xmax = Inf, ymin = 20, ymax = ymax * 1.02, fill = "#10b981", alpha = 0.08) + geom_hline(yintercept = 20, linetype = "dashed", color = "#10b981", linewidth = 0.7) + annotate( "text", x = max(plot_data$time_days) * 0.02, y = 20 * 1.08, label = "Target Ctrough >= 20 mg/L", hjust = 0, size = 3.5, color = "#10b981" ) } p <- p + geom_line(color = "#8b5cf6", linewidth = 0.9) + labs( x = "Time (days)", y = "Pertuzumab concentration (mg/L)", title = paste("Pertuzumab PK Profile -", regimen_label()), subtitle = paste0( "LBW ", input$lbw, " kg, albumin ", input$albu, " g/dL", if (input$dose_strategy == "weight") paste0(", actual weight ", input$wt, " kg") else "" ) ) + theme_minimal(base_size = 14) if (isTRUE(input$log_scale)) { p <- p + scale_y_log10() } p }) } shinyApp(ui = ui, server = server)