From affd61134f1907a4b28bf0d40ddf3c5678186eb8 Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Thu, 18 Dec 2025 00:13:06 -0600 Subject: [PATCH 01/14] Adding option to handle big spec file, via MSstatsBig --- DESCRIPTION | 4 +-- R/module-loadpage-server.R | 67 ++++++++++++++++++++++++++++++++++++-- R/module-loadpage-ui.R | 18 +++++++++- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index abdfb0d..c085cbf 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -24,8 +24,8 @@ Authors@R: c( person("Olga", "Vitek", email = "o.vitek@northeastern.edu", role = "aut")) License: Artistic-2.0 Depends: R (>= 4.2) -Imports: shiny, shinyBS, shinyjs, shinybusy, dplyr, ggplot2, plotly, data.table, Hmisc, - MSstats, MSstatsTMT, MSstatsPTM, MSstatsConvert, gplots, marray, DT, readxl, +Imports: shiny, shinyBS, shinyjs, shinybusy, dplyr, ggplot2, plotly, data.table, Hmisc, shinyFiles, + MSstats,MSstatsBig, MSstatsTMT, MSstatsPTM, MSstatsConvert, gplots, marray, DT, readxl, ggrepel, uuid, utils, stats, htmltools, methods, tidyr, grDevices, graphics, mockery, MSstatsBioNet, shinydashboard, arrow, tools, MSstatsResponse Suggests: diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index f6e1659..60f5f68 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -1,3 +1,4 @@ + #' Loadpage Server module for data selection and upload server. #' #' This function sets up the loadpage server where it consists of several, @@ -14,6 +15,33 @@ #' loadpageServer <- function(id, parent_session) { moduleServer(id, function(input, output, session) { + + # == shinyFiles LOGIC FOR LOCAL FILE BROWSER ================================= + # Define volumes for the file selection. + volumes <- shinyFiles::getVolumes()() + + # Server-side logic for the shinyFiles button + shinyFiles::shinyFileChoose(input, "specdata_big_browse", roots = volumes, session = session) + + # Reactive to parse and store the full file information (path, name, etc.) + # This is efficient because parseFilePaths is only called once. + local_spec_file_info <- reactive({ + req(is.list(input$specdata_big_browse)) + shinyFiles::parseFilePaths(volumes, input$specdata_big_browse) + }) + + # Reactive to get just the full datapath, for use in backend processing. + local_path_spec <- reactive({ + path_info <- local_spec_file_info() + if (nrow(path_info) > 0) path_info$datapath else NULL + }) + + # Render just the filename for user feedback in the UI. + output$specdata_big_path <- renderPrint({ + req(nrow(local_spec_file_info()) > 0) + cat(local_spec_file_info()$name) + }) + # toggle ui (DDA DIA SRM) observe({ print("bio") @@ -103,7 +131,9 @@ loadpageServer <- function(id, parent_session) { enable("proceed1") } } else if (input$filetype == "spec") { - if(!is.null(input$specdata) && !is.null(input$sep_specdata)) { # && !is.null(input$annot) + spec_regular_file_ok <- !isTRUE(input$big_file_spec) && !is.null(input$specdata) + spec_big_file_ok <- isTRUE(input$big_file_spec) && length(local_path_spec()) > 0 + if((spec_regular_file_ok || spec_big_file_ok) && !is.null(input$sep_specdata)) { enable("proceed1") } } else if (input$filetype == "ump") { @@ -221,7 +251,40 @@ loadpageServer <- function(id, parent_session) { get_data = eventReactive(input$proceed1, { - getData(input) + # Check if the filetype is Spectronaut AND the big file checkbox is checked + if (input$filetype == "spec" && isTRUE(input$big_file_spec)) { + # Ensure a file has been selected via the local browser + req(length(local_path_spec()) > 0) + + # Show a busy indicator as this might take time + shinybusy::show_modal_spinner( + spin = "fading-circle", + text = "Processing large Spectronaut file..." + ) + + # Define the output path using a temporary file. + #currently can't use the timeporary file because bigSpectronauttoMSstatsFormat modifies the filepath internally + #temp_output_path <- tempfile(fileext = ".csv") + + + + # Call the big file conversion function from MSstatsConvert + converted_data <- MSstatsBig::bigSpectronauttoMSstatsFormat( + input = local_path_spec(), + output = "output_file.csv", + backend = "arrow" + ) + + converted_data <- dplyr::collect(converted_data) + # Remove the busy indicator + shinybusy::remove_modal_spinner() + + return(converted_data) + } else { + # For all other cases (non-spec files, or standard spec uploads), + # use the existing getData function. + getData(input) + } }) diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index 9020459..a95931d 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -285,7 +285,23 @@ create_spectronaut_uploads <- function(ns) { conditionalPanel( condition = "input['loadpage-filetype'] == 'spec' && input['loadpage-BIO'] != 'PTM'", h4("4. Upload MSstats scheme output from Spectronaut"), - fileInput(ns('specdata'), "", multiple = FALSE, accept = NULL), + + # Checkbox to switch between upload methods + checkboxInput(ns("big_file_spec"), "Use local file browser (for large files)"), + + # Standard fileInput, shown when checkbox is NOT checked + conditionalPanel( + condition = "!input['loadpage-big_file_spec']", + fileInput(ns('specdata'), "", multiple = FALSE, accept = NULL) + ), + + # Local browser button, shown when checkbox IS checked + conditionalPanel( + condition = "input['loadpage-big_file_spec']", + shinyFiles::shinyFilesButton(ns("specdata_big_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), + verbatimTextOutput(ns("specdata_big_path")) + ), + create_separator_buttons(ns, "sep_specdata") ) } From 7ec393843fe496a0581b8a431c6eb4cde1e2b28c Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Thu, 25 Dec 2025 18:53:30 -0500 Subject: [PATCH 02/14] Updating the method for loadpageserver() to indicate if we are running on a server or locally Updated UI for big spec file --- R/module-loadpage-server.R | 147 +++++++++++++++++++++++++++++-------- R/module-loadpage-ui.R | 21 +++++- R/server.R | 2 +- man/loadpageServer.Rd | 4 +- 4 files changed, 139 insertions(+), 35 deletions(-) diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 60f5f68..0a47f3a 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -1,3 +1,16 @@ +#' Decide which data loading path to use +#' +#' @param filetype The selected file type from the UI +#' @param is_big_file Boolean indicating if the 'big file' checkbox is checked +#' @return string "big_spectronaut" or "standard" +#' @noRd +.get_data_source_type <- function(filetype, is_big_file) { + if (filetype == "spec" && isTRUE(is_big_file)) { + "big_spectronaut" + } else { + "standard" + } +} #' Loadpage Server module for data selection and upload server. #' @@ -6,6 +19,7 @@ #' #' @param id namespace prefix for the module #' @param parent_session session of the main calling module +#' @param is_web_server boolean indicating if the app is running on a web server #' #' @return input object with user selected options #' @@ -13,33 +27,85 @@ #' @examples #' NA #' -loadpageServer <- function(id, parent_session) { +loadpageServer <- function(id, parent_session, is_web_server = FALSE) { moduleServer(id, function(input, output, session) { # == shinyFiles LOGIC FOR LOCAL FILE BROWSER ================================= # Define volumes for the file selection. - volumes <- shinyFiles::getVolumes()() - - # Server-side logic for the shinyFiles button - shinyFiles::shinyFileChoose(input, "specdata_big_browse", roots = volumes, session = session) - - # Reactive to parse and store the full file information (path, name, etc.) - # This is efficient because parseFilePaths is only called once. - local_spec_file_info <- reactive({ - req(is.list(input$specdata_big_browse)) - shinyFiles::parseFilePaths(volumes, input$specdata_big_browse) - }) - - # Reactive to get just the full datapath, for use in backend processing. - local_path_spec <- reactive({ - path_info <- local_spec_file_info() - if (nrow(path_info) > 0) path_info$datapath else NULL - }) + if (!is_web_server) { + volumes <- shinyFiles::getVolumes()() + + # Server-side logic for the shinyFiles button + shinyFiles::shinyFileChoose(input, "specdata_big_browse", roots = volumes, session = session) + + # Reactive to parse and store the full file information (path, name, etc.) + # This is efficient because parseFilePaths is only called once. + local_spec_file_info <- reactive({ + req(is.list(input$specdata_big_browse)) + shinyFiles::parseFilePaths(volumes, input$specdata_big_browse) + }) + + # Reactive to get just the full datapath, for use in backend processing. + local_path_spec <- reactive({ + path_info <- local_spec_file_info() + if (nrow(path_info) > 0) path_info$datapath else NULL + }) + + # Render just the filename for user feedback in the UI. + output$specdata_big_path <- renderPrint({ + req(nrow(local_spec_file_info()) > 0) + cat(local_spec_file_info()$name) + }) + } else { + local_path_spec <- reactive({ NULL }) + } - # Render just the filename for user feedback in the UI. - output$specdata_big_path <- renderPrint({ - req(nrow(local_spec_file_info()) > 0) - cat(local_spec_file_info()$name) + output$spectronaut_upload_ui <- renderUI({ + req(input$filetype == 'spec', input$BIO != 'PTM') + + if (!is_web_server) { + tagList( + h4("4. Upload MSstats scheme output from Spectronaut"), + + # Checkbox to switch between upload methods + checkboxInput(session$ns("big_file_spec"), "Use local file browser (for large files)"), + + # Standard fileInput, shown when checkbox is NOT checked + conditionalPanel( + condition = paste0("!input['", session$ns("big_file_spec"), "']"), + fileInput(session$ns('specdata'), "", multiple = FALSE, accept = NULL) + ), + + # Local browser button, shown when checkbox IS checked + conditionalPanel( + condition = paste0("input['", session$ns("big_file_spec"), "']"), + shinyFiles::shinyFilesButton(session$ns("specdata_big_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), + verbatimTextOutput(session$ns("specdata_big_path")) + ), + + radioButtons(session$ns("sep_specdata"), + label = h5("Column separator in uploaded file", class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("Choose how columns are separated in the uploaded file", class = "icon-tooltip")), + c(Comma=",", Semicolon=";", Tab="\t", Pipe="|"), + inline = TRUE + ) + ) + } else { + tagList( + h4("4. Upload MSstats scheme output from Spectronaut"), + + fileInput(session$ns('specdata'), "", multiple = FALSE, accept = NULL), + + radioButtons(session$ns("sep_specdata"), + label = h5("Column separator in uploaded file", class = "icon-wrapper", + icon("question-circle", lib = "font-awesome"), + div("Choose how columns are separated in the uploaded file", class = "icon-tooltip")), + c(Comma=",", Semicolon=";", Tab="\t", Pipe="|"), + inline = TRUE + ) + ) + } }) # toggle ui (DDA DIA SRM) @@ -251,8 +317,9 @@ loadpageServer <- function(id, parent_session) { get_data = eventReactive(input$proceed1, { - # Check if the filetype is Spectronaut AND the big file checkbox is checked - if (input$filetype == "spec" && isTRUE(input$big_file_spec)) { + data_source <- .get_data_source_type(input$filetype, input$big_file_spec) + + if (data_source == "big_spectronaut") { # Ensure a file has been selected via the local browser req(length(local_path_spec()) > 0) @@ -266,16 +333,38 @@ loadpageServer <- function(id, parent_session) { #currently can't use the timeporary file because bigSpectronauttoMSstatsFormat modifies the filepath internally #temp_output_path <- tempfile(fileext = ".csv") - # Call the big file conversion function from MSstatsConvert converted_data <- MSstatsBig::bigSpectronauttoMSstatsFormat( - input = local_path_spec(), - output = "output_file.csv", - backend = "arrow" + input_file = local_path_spec(), + output_file_name = "output_file.csv", + backend = "arrow", + filter_by_excluded = input$filter_by_excluded, + filter_by_identified = input$filter_by_identified, + filter_by_qvalue = input$filter_by_qvalue, + qvalue_cutoff = input$qvalue_cutoff, + max_feature_count = input$max_feature_count, + filter_unique_peptides = input$filter_unique_peptides, + aggregate_psms = input$aggregate_psms, + filter_few_obs = input$filter_few_obs ) - converted_data <- dplyr::collect(converted_data) + # Attempt to load the data into memory. + # If the data is too large for RAM, this will catch the allocation error. + converted_data <- tryCatch({ + dplyr::collect(converted_data) + }, error = function(e) { + shinybusy::remove_modal_spinner() + showNotification( + paste("Memory Error: The dataset is too large to process in-memory.", e$message), + type = "error", + duration = NULL + ) + return(NULL) + }) + + if (is.null(converted_data)) return(NULL) + # Remove the busy indicator shinybusy::remove_modal_spinner() diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index a95931d..74dfd57 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -223,7 +223,7 @@ create_standard_uploads <- function(ns) { #' @noRd create_standard_annotation_uploads <- function(ns) { conditionalPanel( - condition = "(input['loadpage-filetype'] == 'sky' || input['loadpage-filetype'] == 'prog' || input['loadpage-filetype'] == 'PD' || input['loadpage-filetype'] == 'spec' || input['loadpage-filetype'] == 'open'|| input['loadpage-filetype'] =='spmin' || input['loadpage-filetype'] == 'phil' || input['loadpage-filetype'] == 'diann') && input['loadpage-BIO'] != 'PTM'", + condition = "(input['loadpage-filetype'] == 'sky' || input['loadpage-filetype'] == 'prog' || input['loadpage-filetype'] == 'PD' || (input['loadpage-filetype'] == 'spec' && !input['loadpage-big_file_spec']) || input['loadpage-filetype'] == 'open'|| input['loadpage-filetype'] =='spmin' || input['loadpage-filetype'] == 'phil' || input['loadpage-filetype'] == 'diann') && input['loadpage-BIO'] != 'PTM'", h4("5. Upload annotation File", class = "icon-wrapper", icon("question-circle", lib = "font-awesome"), div("Upload manually created annotation file. This file maps MS runs to experiment metadata (i.e. conditions, bioreplicates). Please see Help tab for information on creating this file.", class = "icon-tooltip")), @@ -299,7 +299,20 @@ create_spectronaut_uploads <- function(ns) { conditionalPanel( condition = "input['loadpage-big_file_spec']", shinyFiles::shinyFilesButton(ns("specdata_big_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), - verbatimTextOutput(ns("specdata_big_path")) + verbatimTextOutput(ns("specdata_big_path")), + tags$hr(), + h4("Options for large file processing"), + checkboxInput(ns("filter_by_excluded"), "Filter by excluded from quantification", value = FALSE), + checkboxInput(ns("filter_by_identified"), "Filter by identified", value = FALSE), + checkboxInput(ns("filter_by_qvalue"), "Filter by q-value", value = TRUE), + conditionalPanel( + condition = "input['loadpage-filter_by_qvalue']", + numericInput(ns("qvalue_cutoff"), "Q-value cutoff", value = 0.01, min = 0, max = 1, step = 0.01) + ), + numericInput(ns("max_feature_count"), "Max feature count", value = 20, min = 1), + checkboxInput(ns("filter_unique_peptides"), "Use unique peptides", value = FALSE), + checkboxInput(ns("aggregate_psms"), "Aggregate PSMs to peptides", value = FALSE), + checkboxInput(ns("filter_few_obs"), "Filter features with few observations", value = FALSE) ), create_separator_buttons(ns, "sep_specdata") @@ -502,14 +515,14 @@ create_tmt_options <- function(ns) { create_label_free_options <- function(ns) { tagList( conditionalPanel( - condition = "input['loadpage-filetype'] && input['loadpage-DDA_DIA'] == 'LType' && input['loadpage-filetype'] != 'sample' && input['loadpage-filetype'] != 'MRF'", + condition = "input['loadpage-filetype'] && input['loadpage-DDA_DIA'] == 'LType' && input['loadpage-filetype'] != 'sample' && input['loadpage-filetype'] != 'MRF' && (input['loadpage-filetype'] != 'spec' || !input['loadpage-big_file_spec'])", h4("Select the options for pre-processing"), checkboxInput(ns("unique_peptides"), "Use unique peptides", value = TRUE), checkboxInput(ns("remove"), "Remove proteins with 1 peptide and charge", value = FALSE) ), conditionalPanel( - condition = "input['loadpage-filetype'] && input['loadpage-DDA_DIA'] == 'LType' && input['loadpage-filetype'] != 'sample'", + condition = "input['loadpage-filetype'] && input['loadpage-DDA_DIA'] == 'LType' && input['loadpage-filetype'] != 'sample' && (input['loadpage-filetype'] != 'spec' || !input['loadpage-big_file_spec'])", checkboxInput(ns("remove"), "Remove proteins with 1 feature", value = FALSE), # Quality filtering options diff --git a/R/server.R b/R/server.R index 75d02cd..87d3b98 100644 --- a/R/server.R +++ b/R/server.R @@ -32,7 +32,7 @@ server = function(input, output, session) { selected = "Uploaddata") }) - loadpage_values = loadpageServer("loadpage", parent_session = session) + loadpage_values = loadpageServer("loadpage", parent_session = session, is_web_server = isWebServer) loadpage_input = loadpage_values$input get_data = loadpage_values$getData diff --git a/man/loadpageServer.Rd b/man/loadpageServer.Rd index 47aae4f..19d6f4e 100644 --- a/man/loadpageServer.Rd +++ b/man/loadpageServer.Rd @@ -4,12 +4,14 @@ \alias{loadpageServer} \title{Loadpage Server module for data selection and upload server.} \usage{ -loadpageServer(id, parent_session) +loadpageServer(id, parent_session, is_web_server = FALSE) } \arguments{ \item{id}{namespace prefix for the module} \item{parent_session}{session of the main calling module} + +\item{is_web_server}{boolean indicating if the app is running on a web server} } \value{ input object with user selected options From e07c645d467c1af794671431c0560aa72924d693 Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Sun, 28 Dec 2025 12:58:31 -0500 Subject: [PATCH 03/14] Adding Test --- tests/testthat/test-loadpage-server.R | 21 ++++++++++++ tests/testthat/test-module-loadpage-ui.R | 41 ++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 tests/testthat/test-loadpage-server.R diff --git a/tests/testthat/test-loadpage-server.R b/tests/testthat/test-loadpage-server.R new file mode 100644 index 0000000..12a7231 --- /dev/null +++ b/tests/testthat/test-loadpage-server.R @@ -0,0 +1,21 @@ +test_that(".get_data_source_type correctly identifies the data loading path", { + # This test checks the helper function that determines whether to use the + # standard data loader or the special loader for large Spectronaut files. + # This approach is simpler and more robust than a complex shiny::testServer test. + + # Case 1: Spectronaut file with the 'big file' checkbox checked + expect_equal(.get_data_source_type("spec", TRUE), "big_spectronaut") + + # Case 2: Spectronaut file without the 'big file' checkbox + expect_equal(.get_data_source_type("spec", FALSE), "standard") + + # Case 3: A non-Spectronaut file (should always be standard) + expect_equal(.get_data_source_type("maxq", FALSE), "standard") + + # Case 4: A non-Spectronaut file where the Spectronaut checkbox might be TRUE + # (though the UI should prevent this, the logic should be robust) + expect_equal(.get_data_source_type("maxq", TRUE), "standard") + + # Case 5: Input is NULL (initial state) + expect_equal(.get_data_source_type("spec", NULL), "standard") +}) diff --git a/tests/testthat/test-module-loadpage-ui.R b/tests/testthat/test-module-loadpage-ui.R index ee4b47a..c5fd723 100644 --- a/tests/testthat/test-module-loadpage-ui.R +++ b/tests/testthat/test-module-loadpage-ui.R @@ -392,4 +392,45 @@ test_that("file inputs have proper accept attributes", { annot_input <- create_standard_annotation_uploads(NS("test")) annot_html <- as.character(annot_input) expect_true(grepl('accept=.*csv', annot_html)) +}) + +test_that("Spectronaut big file UI is configured correctly", { + # This test checks for the existence and conditional logic of UI elements + # related to the Spectronaut big file workflow. + result <- loadpageUI("test") + html_output <- as.character(result) + + # 1. Check for the presence of the big file checkbox itself + expect_true(grepl("test-big_file_spec", html_output), + info = "Spectronaut 'big file' checkbox is missing.") + + # 2. Check that the new processing options are present in the HTML. + new_options_labels <- c( + "Filter by excluded from quantification", + "Filter by q-value", + "Max feature count", + "Aggregate PSMs to peptides" + ) + for(label in new_options_labels) { + expect_true(grepl(label, html_output), info = paste("Missing Spectronaut big file option:", label)) + } + + # 3. Verify the conditional logic for hiding/showing related panels. + # Note: HTML entities encode ' as ', && as &&, and || as || + + # Annotation upload should be hidden for big spec files. + # Condition contains: (input['loadpage-filetype'] == 'spec' && !input['loadpage-big_file_spec']) + expected_annot_condition <- "input['loadpage-filetype'] == 'spec' && !input['loadpage-big_file_spec']" + expect_true( + grepl(expected_annot_condition, html_output, fixed = TRUE), + info = "Conditional logic to hide annotation upload for big Spectronaut files is incorrect." + ) + + # Standard pre-processing should be hidden for big spec files. + # Condition contains: (input['loadpage-filetype'] != 'spec' || !input['loadpage-big_file_spec']) + expected_preprocess_condition <- "input['loadpage-filetype'] != 'spec' || !input['loadpage-big_file_spec']" + expect_true( + grepl(expected_preprocess_condition, html_output, fixed = TRUE), + info = "Conditional logic to hide pre-processing options for big Spectronaut files is incorrect." + ) }) \ No newline at end of file From 983265eee71417bd8718cb129986ea10f7e523ad Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Fri, 9 Jan 2026 16:38:35 -0600 Subject: [PATCH 04/14] Updating based on the feedback --- R/module-loadpage-server.R | 71 +++++++-------------------------- R/module-loadpage-ui.R | 81 +++++++++++++++++++++++--------------- 2 files changed, 64 insertions(+), 88 deletions(-) diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 0a47f3a..5f022bc 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -36,76 +36,35 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { volumes <- shinyFiles::getVolumes()() # Server-side logic for the shinyFiles button - shinyFiles::shinyFileChoose(input, "specdata_big_browse", roots = volumes, session = session) + shinyFiles::shinyFileChoose(input, "big_file_browse", roots = volumes, session = session) # Reactive to parse and store the full file information (path, name, etc.) # This is efficient because parseFilePaths is only called once. - local_spec_file_info <- reactive({ - req(is.list(input$specdata_big_browse)) - shinyFiles::parseFilePaths(volumes, input$specdata_big_browse) + local_file_info <- reactive({ + req(is.list(input$big_file_browse)) + shinyFiles::parseFilePaths(volumes, input$big_file_browse) }) # Reactive to get just the full datapath, for use in backend processing. - local_path_spec <- reactive({ - path_info <- local_spec_file_info() + local_big_file_path <- reactive({ + path_info <- local_file_info() if (nrow(path_info) > 0) path_info$datapath else NULL }) # Render just the filename for user feedback in the UI. output$specdata_big_path <- renderPrint({ - req(nrow(local_spec_file_info()) > 0) - cat(local_spec_file_info()$name) + req(nrow(local_file_info()) > 0) + cat(local_file_info()$name) }) - } else { - local_path_spec <- reactive({ NULL }) + } + else { + local_big_file_path <- reactive({ NULL }) } output$spectronaut_upload_ui <- renderUI({ req(input$filetype == 'spec', input$BIO != 'PTM') - if (!is_web_server) { - tagList( - h4("4. Upload MSstats scheme output from Spectronaut"), - - # Checkbox to switch between upload methods - checkboxInput(session$ns("big_file_spec"), "Use local file browser (for large files)"), - - # Standard fileInput, shown when checkbox is NOT checked - conditionalPanel( - condition = paste0("!input['", session$ns("big_file_spec"), "']"), - fileInput(session$ns('specdata'), "", multiple = FALSE, accept = NULL) - ), - - # Local browser button, shown when checkbox IS checked - conditionalPanel( - condition = paste0("input['", session$ns("big_file_spec"), "']"), - shinyFiles::shinyFilesButton(session$ns("specdata_big_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), - verbatimTextOutput(session$ns("specdata_big_path")) - ), - - radioButtons(session$ns("sep_specdata"), - label = h5("Column separator in uploaded file", class = "icon-wrapper", - icon("question-circle", lib = "font-awesome"), - div("Choose how columns are separated in the uploaded file", class = "icon-tooltip")), - c(Comma=",", Semicolon=";", Tab="\t", Pipe="|"), - inline = TRUE - ) - ) - } else { - tagList( - h4("4. Upload MSstats scheme output from Spectronaut"), - - fileInput(session$ns('specdata'), "", multiple = FALSE, accept = NULL), - - radioButtons(session$ns("sep_specdata"), - label = h5("Column separator in uploaded file", class = "icon-wrapper", - icon("question-circle", lib = "font-awesome"), - div("Choose how columns are separated in the uploaded file", class = "icon-tooltip")), - c(Comma=",", Semicolon=";", Tab="\t", Pipe="|"), - inline = TRUE - ) - ) - } + create_spectronaut_ui_content(session$ns, is_web_server) }) # toggle ui (DDA DIA SRM) @@ -198,7 +157,7 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { } } else if (input$filetype == "spec") { spec_regular_file_ok <- !isTRUE(input$big_file_spec) && !is.null(input$specdata) - spec_big_file_ok <- isTRUE(input$big_file_spec) && length(local_path_spec()) > 0 + spec_big_file_ok <- isTRUE(input$big_file_spec) && length(local_big_file_path()) > 0 if((spec_regular_file_ok || spec_big_file_ok) && !is.null(input$sep_specdata)) { enable("proceed1") } @@ -321,7 +280,7 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { if (data_source == "big_spectronaut") { # Ensure a file has been selected via the local browser - req(length(local_path_spec()) > 0) + req(length(local_big_file_path()) > 0) # Show a busy indicator as this might take time shinybusy::show_modal_spinner( @@ -336,7 +295,7 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { # Call the big file conversion function from MSstatsConvert converted_data <- MSstatsBig::bigSpectronauttoMSstatsFormat( - input_file = local_path_spec(), + input_file = local_big_file_path(), output_file_name = "output_file.csv", backend = "arrow", filter_by_excluded = input$filter_by_excluded, diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index 74dfd57..8ac1cf8 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -282,41 +282,58 @@ create_diann_uploads <- function(ns) { #' Create Spectronaut file uploads #' @noRd create_spectronaut_uploads <- function(ns) { - conditionalPanel( - condition = "input['loadpage-filetype'] == 'spec' && input['loadpage-BIO'] != 'PTM'", - h4("4. Upload MSstats scheme output from Spectronaut"), - - # Checkbox to switch between upload methods - checkboxInput(ns("big_file_spec"), "Use local file browser (for large files)"), - - # Standard fileInput, shown when checkbox is NOT checked - conditionalPanel( - condition = "!input['loadpage-big_file_spec']", - fileInput(ns('specdata'), "", multiple = FALSE, accept = NULL) - ), + uiOutput(ns("spectronaut_upload_ui")) +} - # Local browser button, shown when checkbox IS checked - conditionalPanel( - condition = "input['loadpage-big_file_spec']", - shinyFiles::shinyFilesButton(ns("specdata_big_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), - verbatimTextOutput(ns("specdata_big_path")), - tags$hr(), - h4("Options for large file processing"), - checkboxInput(ns("filter_by_excluded"), "Filter by excluded from quantification", value = FALSE), - checkboxInput(ns("filter_by_identified"), "Filter by identified", value = FALSE), - checkboxInput(ns("filter_by_qvalue"), "Filter by q-value", value = TRUE), +#' Create content for Spectronaut upload UI +#' @param ns Namespace function +#' @param is_web_server Boolean indicating if running on web server +#' @noRd +create_spectronaut_ui_content <- function(ns, is_web_server) { + if (!is_web_server) { + tagList( + h4("4. Upload MSstats scheme output from Spectronaut"), + + # Checkbox to switch between upload methods + checkboxInput(ns("big_file_spec"), "Large file mode"), + + # Standard fileInput, shown when checkbox is NOT checked conditionalPanel( - condition = "input['loadpage-filter_by_qvalue']", - numericInput(ns("qvalue_cutoff"), "Q-value cutoff", value = 0.01, min = 0, max = 1, step = 0.01) + condition = paste0("!input['", ns("big_file_spec"), "']"), + fileInput(ns('specdata'), "", multiple = FALSE, accept = NULL) ), - numericInput(ns("max_feature_count"), "Max feature count", value = 20, min = 1), - checkboxInput(ns("filter_unique_peptides"), "Use unique peptides", value = FALSE), - checkboxInput(ns("aggregate_psms"), "Aggregate PSMs to peptides", value = FALSE), - checkboxInput(ns("filter_few_obs"), "Filter features with few observations", value = FALSE) - ), - - create_separator_buttons(ns, "sep_specdata") - ) + + # Local browser button, shown when checkbox IS checked + conditionalPanel( + condition = paste0("input['", ns("big_file_spec"), "']"), + shinyFiles::shinyFilesButton(ns("big_file_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), + verbatimTextOutput(ns("specdata_big_path")), + tags$hr(), + h4("Options for large file processing"), + checkboxInput(ns("filter_by_excluded"), "Filter by excluded from quantification", value = FALSE), + checkboxInput(ns("filter_by_identified"), "Filter by identified", value = FALSE), + checkboxInput(ns("filter_by_qvalue"), "Filter by q-value", value = TRUE), + conditionalPanel( + condition = paste0("input['", ns("filter_by_qvalue"), "']"), + numericInput(ns("qvalue_cutoff"), "Q-value cutoff", value = 0.01, min = 0, max = 1, step = 0.01) + ), + numericInput(ns("max_feature_count"), "Max feature count", value = 20, min = 1), + checkboxInput(ns("filter_unique_peptides"), "Use unique peptides", value = FALSE), + checkboxInput(ns("aggregate_psms"), "Aggregate PSMs to peptides", value = FALSE), + checkboxInput(ns("filter_few_obs"), "Filter features with few observations", value = FALSE) + ), + + create_separator_buttons(ns, "sep_specdata") + ) + } else { + tagList( + h4("4. Upload MSstats scheme output from Spectronaut"), + + fileInput(ns('specdata'), "", multiple = FALSE, accept = NULL), + + create_separator_buttons(ns, "sep_specdata") + ) + } } #' Create PTM FragPipe uploads From 2feabd146e322b719560f86a42e7285fe8909316 Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Sat, 17 Jan 2026 14:11:16 -0600 Subject: [PATCH 05/14] Respoding to the comments in the PR --- R/module-loadpage-server.R | 119 +++++++++-------------- R/module-loadpage-ui.R | 107 ++++++++++---------- R/utils.R | 114 +++++++++++++++++----- tests/testthat/test-module-loadpage-ui.R | 87 +++++++++-------- tests/testthat/test-utils.R | 90 +++++++++++++++++ 5 files changed, 331 insertions(+), 186 deletions(-) diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 5f022bc..975b6a6 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -1,17 +1,3 @@ -#' Decide which data loading path to use -#' -#' @param filetype The selected file type from the UI -#' @param is_big_file Boolean indicating if the 'big file' checkbox is checked -#' @return string "big_spectronaut" or "standard" -#' @noRd -.get_data_source_type <- function(filetype, is_big_file) { - if (filetype == "spec" && isTRUE(is_big_file)) { - "big_spectronaut" - } else { - "standard" - } -} - #' Loadpage Server module for data selection and upload server. #' #' This function sets up the loadpage server where it consists of several, @@ -61,10 +47,53 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { local_big_file_path <- reactive({ NULL }) } - output$spectronaut_upload_ui <- renderUI({ + output$spectronaut_header_ui <- renderUI({ + req(input$filetype == 'spec', input$BIO != 'PTM') + create_spectronaut_header() + }) + + output$spectronaut_file_selection_ui <- renderUI({ + req(input$filetype == 'spec', input$BIO != 'PTM') + + ui_elements <- tagList() + + if (!is_web_server) { + ui_elements <- tagList(ui_elements, create_spectronaut_mode_selector(session$ns, isTRUE(input$big_file_spec))) + + if (isTRUE(input$big_file_spec)) { + ui_elements <- tagList(ui_elements, create_spectronaut_large_file_ui(session$ns)) + } else { + ui_elements <- tagList(ui_elements, create_spectronaut_standard_ui(session$ns)) + } + } else { + ui_elements <- tagList(ui_elements, create_spectronaut_standard_ui(session$ns)) + } + + tagList(ui_elements, create_separator_buttons(session$ns, "sep_specdata")) + }) + + output$spectronaut_options_ui <- renderUI({ req(input$filetype == 'spec', input$BIO != 'PTM') - create_spectronaut_ui_content(session$ns, is_web_server) + if (!is_web_server && isTRUE(input$big_file_spec)) { + qval_def <- if (is.null(input$filter_by_qvalue)) TRUE else input$filter_by_qvalue + excluded_def <- if (is.null(input$filter_by_excluded)) FALSE else input$filter_by_excluded + identified_def <- if (is.null(input$filter_by_identified)) FALSE else input$filter_by_identified + cutoff_def <- if (is.null(input$qvalue_cutoff)) 0.01 else input$qvalue_cutoff + + max_feature_def <- if (is.null(input$max_feature_count)) 20 else input$max_feature_count + unique_peps_def <- if (is.null(input$filter_unique_peptides)) FALSE else input$filter_unique_peptides + agg_psms_def <- if (is.null(input$aggregate_psms)) FALSE else input$aggregate_psms + few_obs_def <- if (is.null(input$filter_few_obs)) FALSE else input$filter_few_obs + + tagList( + create_spectronaut_large_filter_options(session$ns, excluded_def, identified_def, qval_def), + if (qval_def) create_spectronaut_qvalue_cutoff_ui(session$ns, cutoff_def), + create_spectronaut_large_bottom_ui(session$ns, max_feature_def, unique_peps_def, agg_psms_def, few_obs_def) + ) + } else { + NULL + } }) # toggle ui (DDA DIA SRM) @@ -276,63 +305,7 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { get_data = eventReactive(input$proceed1, { - data_source <- .get_data_source_type(input$filetype, input$big_file_spec) - - if (data_source == "big_spectronaut") { - # Ensure a file has been selected via the local browser - req(length(local_big_file_path()) > 0) - - # Show a busy indicator as this might take time - shinybusy::show_modal_spinner( - spin = "fading-circle", - text = "Processing large Spectronaut file..." - ) - - # Define the output path using a temporary file. - #currently can't use the timeporary file because bigSpectronauttoMSstatsFormat modifies the filepath internally - #temp_output_path <- tempfile(fileext = ".csv") - - - # Call the big file conversion function from MSstatsConvert - converted_data <- MSstatsBig::bigSpectronauttoMSstatsFormat( - input_file = local_big_file_path(), - output_file_name = "output_file.csv", - backend = "arrow", - filter_by_excluded = input$filter_by_excluded, - filter_by_identified = input$filter_by_identified, - filter_by_qvalue = input$filter_by_qvalue, - qvalue_cutoff = input$qvalue_cutoff, - max_feature_count = input$max_feature_count, - filter_unique_peptides = input$filter_unique_peptides, - aggregate_psms = input$aggregate_psms, - filter_few_obs = input$filter_few_obs - ) - - # Attempt to load the data into memory. - # If the data is too large for RAM, this will catch the allocation error. - converted_data <- tryCatch({ - dplyr::collect(converted_data) - }, error = function(e) { - shinybusy::remove_modal_spinner() - showNotification( - paste("Memory Error: The dataset is too large to process in-memory.", e$message), - type = "error", - duration = NULL - ) - return(NULL) - }) - - if (is.null(converted_data)) return(NULL) - - # Remove the busy indicator - shinybusy::remove_modal_spinner() - - return(converted_data) - } else { - # For all other cases (non-spec files, or standard spec uploads), - # use the existing getData function. - getData(input) - } + getData(input) }) diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index 8ac1cf8..0d06d74 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -282,58 +282,67 @@ create_diann_uploads <- function(ns) { #' Create Spectronaut file uploads #' @noRd create_spectronaut_uploads <- function(ns) { - uiOutput(ns("spectronaut_upload_ui")) + tagList( + uiOutput(ns("spectronaut_header_ui")), + uiOutput(ns("spectronaut_file_selection_ui")), + uiOutput(ns("spectronaut_options_ui")) + ) } -#' Create content for Spectronaut upload UI -#' @param ns Namespace function -#' @param is_web_server Boolean indicating if running on web server +#' Create Spectronaut header #' @noRd -create_spectronaut_ui_content <- function(ns, is_web_server) { - if (!is_web_server) { - tagList( - h4("4. Upload MSstats scheme output from Spectronaut"), - - # Checkbox to switch between upload methods - checkboxInput(ns("big_file_spec"), "Large file mode"), - - # Standard fileInput, shown when checkbox is NOT checked - conditionalPanel( - condition = paste0("!input['", ns("big_file_spec"), "']"), - fileInput(ns('specdata'), "", multiple = FALSE, accept = NULL) - ), - - # Local browser button, shown when checkbox IS checked - conditionalPanel( - condition = paste0("input['", ns("big_file_spec"), "']"), - shinyFiles::shinyFilesButton(ns("big_file_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), - verbatimTextOutput(ns("specdata_big_path")), - tags$hr(), - h4("Options for large file processing"), - checkboxInput(ns("filter_by_excluded"), "Filter by excluded from quantification", value = FALSE), - checkboxInput(ns("filter_by_identified"), "Filter by identified", value = FALSE), - checkboxInput(ns("filter_by_qvalue"), "Filter by q-value", value = TRUE), - conditionalPanel( - condition = paste0("input['", ns("filter_by_qvalue"), "']"), - numericInput(ns("qvalue_cutoff"), "Q-value cutoff", value = 0.01, min = 0, max = 1, step = 0.01) - ), - numericInput(ns("max_feature_count"), "Max feature count", value = 20, min = 1), - checkboxInput(ns("filter_unique_peptides"), "Use unique peptides", value = FALSE), - checkboxInput(ns("aggregate_psms"), "Aggregate PSMs to peptides", value = FALSE), - checkboxInput(ns("filter_few_obs"), "Filter features with few observations", value = FALSE) - ), - - create_separator_buttons(ns, "sep_specdata") - ) - } else { - tagList( - h4("4. Upload MSstats scheme output from Spectronaut"), - - fileInput(ns('specdata'), "", multiple = FALSE, accept = NULL), - - create_separator_buttons(ns, "sep_specdata") - ) - } +create_spectronaut_header <- function() { + h4("4. Upload MSstats scheme output from Spectronaut") +} + +#' Create Spectronaut mode selector (Local only) +#' @noRd +create_spectronaut_mode_selector <- function(ns, selected = FALSE) { + checkboxInput(ns("big_file_spec"), "Large file mode", value = selected) +} + +#' Create Spectronaut standard file input +#' @noRd +create_spectronaut_standard_ui <- function(ns) { + fileInput(ns('specdata'), "", multiple = FALSE, accept = NULL) +} + +#' Create Spectronaut large file selection UI +#' @noRd +create_spectronaut_large_file_ui <- function(ns) { + tagList( + shinyFiles::shinyFilesButton(ns("big_file_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), + verbatimTextOutput(ns("specdata_big_path")) + ) +} + +#' Create Spectronaut large file filter options +#' @noRd +create_spectronaut_large_filter_options <- function(ns, excluded_def = FALSE, identified_def = FALSE, qval_def = TRUE) { + tagList( + tags$hr(), + h4("Options for large file processing"), + checkboxInput(ns("filter_by_excluded"), "Filter by excluded from quantification", value = excluded_def), + checkboxInput(ns("filter_by_identified"), "Filter by identified", value = identified_def), + checkboxInput(ns("filter_by_qvalue"), "Filter by q-value", value = qval_def) + ) +} + +#' Create Spectronaut Q-value cutoff input +#' @noRd +create_spectronaut_qvalue_cutoff_ui <- function(ns, cutoff_def = 0.01) { + numericInput(ns("qvalue_cutoff"), "Q-value cutoff", value = cutoff_def, min = 0, max = 1, step = 0.01) +} + +#' Create Spectronaut large file options (Bottom part) +#' @noRd +create_spectronaut_large_bottom_ui <- function(ns, max_feature_def = 20, unique_peps_def = FALSE, agg_psms_def = FALSE, few_obs_def = FALSE) { + tagList( + numericInput(ns("max_feature_count"), "Max feature count", value = max_feature_def, min = 1), + checkboxInput(ns("filter_unique_peptides"), "Use unique peptides", value = unique_peps_def), + checkboxInput(ns("aggregate_psms"), "Aggregate PSMs to peptides", value = agg_psms_def), + checkboxInput(ns("filter_few_obs"), "Filter features with few observations", value = few_obs_def) + ) } #' Create PTM FragPipe uploads diff --git a/R/utils.R b/R/utils.R index b09bf89..e2dee6b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -501,33 +501,95 @@ getData <- function(input) { } else if(input$filetype == 'spec') { - # if (input$subset){ - # data = read.csv.sql(infile$datapath, sep="\t", - # sql = "select * from file order by random() limit 100000") - # } else { - data = read.csv(input$specdata$datapath, sep=input$sep_specdata, check.names = FALSE) - # } - # Base arguments for the Spectronaut converter - converter_args = list( - input = data, - annotation = getAnnot(input), - filter_with_Qvalue = input$q_val, - qvalue_cutoff = input$q_cutoff, - removeProtein_with1Feature = input$remove, - use_log_file = FALSE - ) - - if (isTRUE(input$calculate_anomaly_scores) && !is.null(input$run_order_file)) { - # Add anomaly score parameters only if the checkbox is checked - converter_args$calculateAnomalyScores = TRUE - converter_args$runOrder = read.csv(input$run_order_file$datapath) - converter_args$anomalyModelFeatures = c("FG.ShapeQualityScore (MS2)", "FG.ShapeQualityScore (MS1)", "EGDeltaRT") - converter_args$anomalyModelFeatureTemporal = c("mean_decrease", "mean_decrease", "dispersion_increase") - converter_args$n_trees = 100 - converter_args$max_depth = "auto" - converter_args$numberOfCores = 1 + if (isTRUE(input$big_file_spec)) { + # Logic for big Spectronaut files + # Parse the file path from shinyFiles input + volumes <- shinyFiles::getVolumes()() + path_info <- shinyFiles::parseFilePaths(volumes, input$big_file_browse) + local_big_file_path <- if (nrow(path_info) > 0) path_info$datapath else NULL + + # Validate inputs + if (!is.numeric(input$qvalue_cutoff) || is.na(input$qvalue_cutoff) || input$qvalue_cutoff < 0 || input$qvalue_cutoff > 1) { + showNotification("Error: qvalue_cutoff must be between 0 and 1.", type = "error") + shinybusy::remove_modal_spinner() + return(NULL) + } + + if (!is.numeric(input$max_feature_count) || is.na(input$max_feature_count) || input$max_feature_count <= 0) { + showNotification("Error: max_feature_count must be a positive number.", type = "error") + shinybusy::remove_modal_spinner() + return(NULL) + } + + if (is.null(local_big_file_path) || !file.exists(local_big_file_path)) { + showNotification("Error: The selected file does not exist or is not readable.", type = "error") + shinybusy::remove_modal_spinner() + return(NULL) + } + + shinybusy::update_modal_spinner(text = "Processing large Spectronaut file...") + + # Call the big file conversion function from MSstatsConvert + converted_data <- MSstatsBig::bigSpectronauttoMSstatsFormat( + input_file = local_big_file_path, + output_file_name = "output_file.csv", + backend = "arrow", + filter_by_excluded = input$filter_by_excluded, + filter_by_identified = input$filter_by_identified, + filter_by_qvalue = input$filter_by_qvalue, + qvalue_cutoff = input$qvalue_cutoff, + max_feature_count = input$max_feature_count, + filter_unique_peptides = input$filter_unique_peptides, + aggregate_psms = input$aggregate_psms, + filter_few_obs = input$filter_few_obs + ) + + # Attempt to load the data into memory. + mydata <- tryCatch({ + dplyr::collect(converted_data) + }, error = function(e) { + showNotification( + paste("Memory Error: The dataset is too large to process in-memory.", e$message), + type = "error", + duration = NULL + ) + return(NULL) + }) + + if (is.null(mydata)) { + shinybusy::remove_modal_spinner() + return(NULL) + } + + } else { + # if (input$subset){ + # data = read.csv.sql(infile$datapath, sep="\t", + # sql = "select * from file order by random() limit 100000") + # } else { + data = read.csv(input$specdata$datapath, sep=input$sep_specdata, check.names = FALSE) + # } + # Base arguments for the Spectronaut converter + converter_args = list( + input = data, + annotation = getAnnot(input), + filter_with_Qvalue = input$q_val, + qvalue_cutoff = input$q_cutoff, + removeProtein_with1Feature = input$remove, + use_log_file = FALSE + ) + + if (isTRUE(input$calculate_anomaly_scores) && !is.null(input$run_order_file)) { + # Add anomaly score parameters only if the checkbox is checked + converter_args$calculateAnomalyScores = TRUE + converter_args$runOrder = read.csv(input$run_order_file$datapath) + converter_args$anomalyModelFeatures = c("FG.ShapeQualityScore (MS2)", "FG.ShapeQualityScore (MS1)", "EGDeltaRT") + converter_args$anomalyModelFeatureTemporal = c("mean_decrease", "mean_decrease", "dispersion_increase") + converter_args$n_trees = 100 + converter_args$max_depth = "auto" + converter_args$numberOfCores = 1 + } + mydata = do.call(SpectronauttoMSstatsFormat, converter_args) } - mydata = do.call(SpectronauttoMSstatsFormat, converter_args) } else if(input$filetype == 'diann') { if (getFileExtension(input$dianndata$name) %in% c("parquet", "pq")) { diff --git a/tests/testthat/test-module-loadpage-ui.R b/tests/testthat/test-module-loadpage-ui.R index c5fd723..7bc3062 100644 --- a/tests/testthat/test-module-loadpage-ui.R +++ b/tests/testthat/test-module-loadpage-ui.R @@ -394,43 +394,54 @@ test_that("file inputs have proper accept attributes", { expect_true(grepl('accept=.*csv', annot_html)) }) -test_that("Spectronaut big file UI is configured correctly", { - # This test checks for the existence and conditional logic of UI elements - # related to the Spectronaut big file workflow. - result <- loadpageUI("test") - html_output <- as.character(result) - - # 1. Check for the presence of the big file checkbox itself - expect_true(grepl("test-big_file_spec", html_output), - info = "Spectronaut 'big file' checkbox is missing.") - - # 2. Check that the new processing options are present in the HTML. - new_options_labels <- c( - "Filter by excluded from quantification", - "Filter by q-value", - "Max feature count", - "Aggregate PSMs to peptides" - ) - for(label in new_options_labels) { - expect_true(grepl(label, html_output), info = paste("Missing Spectronaut big file option:", label)) - } - - # 3. Verify the conditional logic for hiding/showing related panels. - # Note: HTML entities encode ' as ', && as &&, and || as || - - # Annotation upload should be hidden for big spec files. - # Condition contains: (input['loadpage-filetype'] == 'spec' && !input['loadpage-big_file_spec']) - expected_annot_condition <- "input['loadpage-filetype'] == 'spec' && !input['loadpage-big_file_spec']" - expect_true( - grepl(expected_annot_condition, html_output, fixed = TRUE), - info = "Conditional logic to hide annotation upload for big Spectronaut files is incorrect." - ) +# Tests for Spectronaut specific UI components +test_that("create_spectronaut_uploads creates UI outputs", { + uploads <- create_spectronaut_uploads(NS("test")) + uploads_html <- as.character(uploads) - # Standard pre-processing should be hidden for big spec files. - # Condition contains: (input['loadpage-filetype'] != 'spec' || !input['loadpage-big_file_spec']) - expected_preprocess_condition <- "input['loadpage-filetype'] != 'spec' || !input['loadpage-big_file_spec']" - expect_true( - grepl(expected_preprocess_condition, html_output, fixed = TRUE), - info = "Conditional logic to hide pre-processing options for big Spectronaut files is incorrect." - ) + expect_true(grepl("spectronaut_header_ui", uploads_html)) + expect_true(grepl("spectronaut_file_selection_ui", uploads_html)) + expect_true(grepl("spectronaut_options_ui", uploads_html)) +}) + +test_that("Spectronaut helper functions create correct UI elements", { + # Header + header <- create_spectronaut_header() + expect_true(grepl("Upload MSstats scheme output from Spectronaut", as.character(header))) + + # Mode selector + mode_sel <- create_spectronaut_mode_selector(NS("test")) + expect_true(grepl("Large file mode", as.character(mode_sel))) + expect_true(grepl("checkbox", as.character(mode_sel))) + + # Standard UI + std_ui <- create_spectronaut_standard_ui(NS("test")) + expect_true(grepl("file", as.character(std_ui))) + expect_true(grepl("specdata", as.character(std_ui))) + + # Large file UI + large_ui <- create_spectronaut_large_file_ui(NS("test")) + large_ui_html <- as.character(large_ui) + expect_true(grepl("Browse for local file", large_ui_html)) + expect_true(grepl("specdata_big_path", large_ui_html)) + + # Filter options + filter_opts <- create_spectronaut_large_filter_options(NS("test")) + opts_html <- as.character(filter_opts) + expect_true(grepl("Filter by excluded", opts_html)) + expect_true(grepl("Filter by identified", opts_html)) + expect_true(grepl("Filter by q-value", opts_html)) + + # Q-value cutoff + qval_ui <- create_spectronaut_qvalue_cutoff_ui(NS("test")) + expect_true(grepl("Q-value cutoff", as.character(qval_ui))) + expect_true(grepl("0.01", as.character(qval_ui))) + + # Bottom UI + bottom_ui <- create_spectronaut_large_bottom_ui(NS("test")) + bottom_html <- as.character(bottom_ui) + expect_true(grepl("Max feature count", bottom_html)) + expect_true(grepl("Use unique peptides", bottom_html)) + expect_true(grepl("Aggregate PSMs", bottom_html)) + expect_true(grepl("Filter features with few observations", bottom_html)) }) \ No newline at end of file diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index ef8cfcc..8d3d6ce 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -1437,3 +1437,93 @@ describe("getData for Spectronaut input with anomaly scores", { expect_null(result_args$anomalyModelFeatures) }) }) + +describe("getData for Big Spectronaut", { + + # Common mock input for big spec + mock_input_big <- list( + filetype = "spec", + big_file_spec = TRUE, + big_file_browse = list(files = list("file.csv")), + qvalue_cutoff = 0.01, + max_feature_count = 20, + filter_by_excluded = FALSE, + filter_by_identified = FALSE, + filter_by_qvalue = TRUE, + filter_unique_peptides = TRUE, + aggregate_psms = TRUE, + filter_few_obs = TRUE, + BIO = "Protein", + DDA_DIA = "DIA" + ) + + # Mock data to return + mock_arrow_obj <- list(dummy = "arrow") + mock_df <- data.frame(ProteinName = "P1", Intensity = 100) + + test_that("Valid input returns data", { + # Mocks + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "MSstatsBig::bigSpectronauttoMSstatsFormat", mock_arrow_obj) + stub(getData, "dplyr::collect", mock_df) + stub(getData, "showNotification", function(...) NULL) + stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + + res <- getData(mock_input_big) + expect_equal(res, mock_df) + }) + + test_that("Invalid qvalue_cutoff returns NULL", { + bad_input <- mock_input_big + bad_input$qvalue_cutoff <- 1.5 + + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "showNotification", function(msg, ...) expect_match(msg, "qvalue_cutoff")) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + + res <- getData(bad_input) + expect_null(res) + }) + + test_that("Invalid max_feature_count returns NULL", { + bad_input <- mock_input_big + bad_input$max_feature_count <- 0 + + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "showNotification", function(msg, ...) expect_match(msg, "max_feature_count")) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + + res <- getData(bad_input) + expect_null(res) + }) + + test_that("File not found returns NULL", { + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "nonexistent.csv")) + stub(getData, "file.exists", FALSE) + stub(getData, "showNotification", function(msg, ...) expect_match(msg, "does not exist")) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + + res <- getData(mock_input_big) + expect_null(res) + }) + + test_that("Memory error returns NULL", { + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "MSstatsBig::bigSpectronauttoMSstatsFormat", mock_arrow_obj) + stub(getData, "dplyr::collect", function(...) stop("Memory allocation failed")) + stub(getData, "showNotification", function(msg, ...) expect_match(msg, "Memory Error")) + stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + + res <- getData(mock_input_big) + expect_null(res) + }) +}) From ee99a002c0f26d498fa40e3ca15a2c501c299af3 Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Sun, 25 Jan 2026 14:54:21 -0600 Subject: [PATCH 06/14] Adding Big File option for DIANN --- R/module-loadpage-server.R | 52 +++++++++++++++++++++++++++++-- R/module-loadpage-ui.R | 63 ++++++++++++++++++++++++++++++++++---- R/utils.R | 52 +++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 8 deletions(-) diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 975b6a6..ac0a2ce 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -38,7 +38,7 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { }) # Render just the filename for user feedback in the UI. - output$specdata_big_path <- renderPrint({ + output$big_file_path <- renderPrint({ req(nrow(local_file_info()) > 0) cat(local_file_info()$name) }) @@ -72,6 +72,52 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { tagList(ui_elements, create_separator_buttons(session$ns, "sep_specdata")) }) + output$diann_header_ui <- renderUI({ + req(input$filetype == 'diann', input$BIO != 'PTM') + create_diann_header() + }) + + output$diann_file_selection_ui <- renderUI({ + req(input$filetype == 'diann', input$BIO != 'PTM') + + ui_elements <- tagList() + + if (!is_web_server) { + ui_elements <- tagList(ui_elements, create_diann_mode_selector(session$ns, isTRUE(input$big_file_diann))) + + if (isTRUE(input$big_file_diann)) { + ui_elements <- tagList(ui_elements, create_diann_large_file_ui(session$ns)) + } else { + ui_elements <- tagList(ui_elements, create_diann_standard_ui(session$ns)) + } + } else { + ui_elements <- tagList(ui_elements, create_diann_standard_ui(session$ns)) + } + + ui_elements + }) + + output$diann_options_ui <- renderUI({ + req(input$filetype == 'diann', input$BIO != 'PTM') + + if (!is_web_server && isTRUE(input$big_file_diann)) { + mbr_def <- if (is.null(input$diann_MBR)) TRUE else input$diann_MBR + quant_col_def <- if (is.null(input$diann_quantificationColumn)) "Fragment.Quant.Corrected" else input$diann_quantificationColumn + + max_feature_def <- if (is.null(input$max_feature_count)) 100 else input$max_feature_count + unique_peps_def <- if (is.null(input$filter_unique_peptides)) FALSE else input$filter_unique_peptides + agg_psms_def <- if (is.null(input$aggregate_psms)) FALSE else input$aggregate_psms + few_obs_def <- if (is.null(input$filter_few_obs)) FALSE else input$filter_few_obs + + tagList( + create_diann_large_filter_options(session$ns, mbr_def, quant_col_def), + create_diann_large_bottom_ui(session$ns, max_feature_def, unique_peps_def, agg_psms_def, few_obs_def) + ) + } else { + NULL + } + }) + output$spectronaut_options_ui <- renderUI({ req(input$filetype == 'spec', input$BIO != 'PTM') @@ -195,7 +241,9 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { enable("proceed1") } } else if (input$filetype == "diann") { - if(!is.null(input$dianndata) && !is.null(input$sep_dianndata)) { # && !is.null(input$annot) + diann_regular_file_ok <- !isTRUE(input$big_file_diann) && !is.null(input$dianndata) && !is.null(input$sep_dianndata) + diann_big_file_ok <- isTRUE(input$big_file_diann) && length(local_big_file_path()) > 0 + if(diann_regular_file_ok || diann_big_file_ok) { enable("proceed1") } } diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index 0d06d74..2969f8f 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -271,11 +271,10 @@ create_skyline_uploads <- function(ns) { #' Create DIANN file uploads #' @noRd create_diann_uploads <- function(ns) { - conditionalPanel( - condition = "input['loadpage-filetype'] == 'diann' && input['loadpage-BIO'] != 'PTM'", - h4("4. Upload MSstats report from DIANN"), - fileInput(ns('dianndata'), "", multiple = FALSE, accept = NULL), - create_separator_buttons(ns, "sep_dianndata") + tagList( + uiOutput(ns("diann_header_ui")), + uiOutput(ns("diann_file_selection_ui")), + uiOutput(ns("diann_options_ui")) ) } @@ -295,24 +294,54 @@ create_spectronaut_header <- function() { h4("4. Upload MSstats scheme output from Spectronaut") } +#' Create DIANN header +#' @noRd +create_diann_header <- function() { + h4("4. Upload MSstats report from DIANN") +} + #' Create Spectronaut mode selector (Local only) #' @noRd create_spectronaut_mode_selector <- function(ns, selected = FALSE) { checkboxInput(ns("big_file_spec"), "Large file mode", value = selected) } +#' Create DIANN mode selector (Local only) +#' @noRd +create_diann_mode_selector <- function(ns, selected = FALSE) { + checkboxInput(ns("big_file_diann"), "Large file mode", value = selected) +} + #' Create Spectronaut standard file input #' @noRd create_spectronaut_standard_ui <- function(ns) { fileInput(ns('specdata'), "", multiple = FALSE, accept = NULL) } +#' Create DIANN standard file input +#' @noRd +create_diann_standard_ui <- function(ns) { + tagList( + fileInput(ns('dianndata'), "", multiple = FALSE, accept = NULL), + create_separator_buttons(ns, "sep_dianndata") + ) +} + #' Create Spectronaut large file selection UI #' @noRd create_spectronaut_large_file_ui <- function(ns) { tagList( shinyFiles::shinyFilesButton(ns("big_file_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), - verbatimTextOutput(ns("specdata_big_path")) + verbatimTextOutput(ns("big_file_path")) + ) +} + +#' Create DIANN large file selection UI +#' @noRd +create_diann_large_file_ui <- function(ns) { + tagList( + shinyFiles::shinyFilesButton(ns("big_file_browse"), "Browse for local file...", "Please select a file", multiple = FALSE), + verbatimTextOutput(ns("big_file_path")) ) } @@ -328,6 +357,17 @@ create_spectronaut_large_filter_options <- function(ns, excluded_def = FALSE, id ) } +#' Create DIANN large file filter options +#' @noRd +create_diann_large_filter_options <- function(ns, mbr_def = TRUE, quant_col_def = "Fragment.Quant.Corrected") { + tagList( + tags$hr(), + h4("Options for large file processing"), + checkboxInput(ns("diann_MBR"), "MBR Enabled", value = mbr_def), + textInput(ns("diann_quantificationColumn"), "Quantification Column", value = quant_col_def) + ) +} + #' Create Spectronaut Q-value cutoff input #' @noRd create_spectronaut_qvalue_cutoff_ui <- function(ns, cutoff_def = 0.01) { @@ -345,6 +385,17 @@ create_spectronaut_large_bottom_ui <- function(ns, max_feature_def = 20, unique_ ) } +#' Create DIANN large file options (Bottom part) +#' @noRd +create_diann_large_bottom_ui <- function(ns, max_feature_def = 100, unique_peps_def = FALSE, agg_psms_def = FALSE, few_obs_def = FALSE) { + tagList( + numericInput(ns("max_feature_count"), "Max feature count", value = max_feature_def, min = 1), + checkboxInput(ns("filter_unique_peptides"), "Use unique peptides", value = unique_peps_def), + checkboxInput(ns("aggregate_psms"), "Aggregate PSMs to peptides", value = agg_psms_def), + checkboxInput(ns("filter_few_obs"), "Filter features with few observations", value = few_obs_def) + ) +} + #' Create PTM FragPipe uploads #' @noRd create_ptm_fragpipe_uploads <- function(ns) { diff --git a/R/utils.R b/R/utils.R index e2dee6b..eed3052 100644 --- a/R/utils.R +++ b/R/utils.R @@ -592,6 +592,57 @@ getData <- function(input) { } } else if(input$filetype == 'diann') { + if (isTRUE(input$big_file_diann)) { + # Logic for big DIANN files + # Parse the file path from shinyFiles input + volumes <- shinyFiles::getVolumes()() + path_info <- shinyFiles::parseFilePaths(volumes, input$big_file_browse) + local_big_file_path <- if (nrow(path_info) > 0) path_info$datapath else NULL + + if (!is.numeric(input$max_feature_count) || is.na(input$max_feature_count) || input$max_feature_count <= 0) { + showNotification("Error: max_feature_count must be a positive number.", type = "error") + shinybusy::remove_modal_spinner() + return(NULL) + } + + if (is.null(local_big_file_path) || !file.exists(local_big_file_path)) { + showNotification("Error: The selected file does not exist or is not readable.", type = "error") + shinybusy::remove_modal_spinner() + return(NULL) + } + + shinybusy::update_modal_spinner(text = "Processing large DIANN file...") + + # Call the big file conversion function from MSstatsConvert + converted_data <- MSstatsBig::bigDIANNtoMSstatsFormat( + input_file = local_big_file_path, + output_file_name = "output_file.csv", + backend = "arrow", + MBR = isTRUE(input$diann_MBR), + quantificationColumn = input$diann_quantificationColumn, + max_feature_count = input$max_feature_count, + filter_unique_peptides = input$filter_unique_peptides, + aggregate_psms = input$aggregate_psms, + filter_few_obs = input$filter_few_obs + ) + + # Attempt to load the data into memory. + mydata <- tryCatch({ + dplyr::collect(converted_data) + }, error = function(e) { + showNotification( + paste("Memory Error: The dataset is too large to process in-memory.", e$message), + type = "error", + duration = NULL + ) + return(NULL) + }) + + if (is.null(mydata)) { + shinybusy::remove_modal_spinner() + return(NULL) + } + } else { if (getFileExtension(input$dianndata$name) %in% c("parquet", "pq")) { data = read_parquet(input$dianndata$datapath) } else { @@ -620,6 +671,7 @@ getData <- function(input) { use_log_file = FALSE, quantificationColumn = quantificationColumn ) + } print("Mydata from mstats") print(mydata) } From 8ced47e6fd836076f218cd40e817b3b1cb75a72c Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Mon, 2 Feb 2026 18:34:19 -0600 Subject: [PATCH 07/14] Updating GetDataCode() --- R/utils.R | 72 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/R/utils.R b/R/utils.R index eed3052..25f9ec8 100644 --- a/R/utils.R +++ b/R/utils.R @@ -773,7 +773,8 @@ library(MSstatsTMT) library(MSstatsPTM)\n", sep = "") codes = paste(codes, "\n# Package versions\n# MSstats version ", packageVersion("MSstats"), "\n# MSstatsTMT version ", packageVersion("MSstatsTMT"), - "\n# MSstatsPTM version ", packageVersion("MSstatsPTM"), sep = "") + "\n# MSstatsPTM version ", packageVersion("MSstatsPTM"), + "\n# MSstatsBig version ", tryCatch(packageVersion("MSstatsBig"), error = function(e) "Not Installed"), sep = "") codes = paste(codes, "\n\n# Read data\n", sep = "") if(input$filetype == 'sample') { if(input$BIO != "PTM" && input$DDA_DIA =='LType' && input$LabelFreeType == "SRM_PRM") { @@ -895,27 +896,60 @@ library(MSstatsPTM)\n", sep = "") } else if(input$filetype == 'spec') { - codes = paste(codes, "data = read.csv(\"insert your MSstats scheme output from Spectronaut filepath\", header = TRUE, sep = ",input$sep_specdata,")\nannot_file = read.csv(\"insert your annotation filepath\", sep='\t')#Optional\n" - , sep = "") - - codes = paste(codes, "data = SpectronauttoMSstatsFormat(data, - annotation = annot_file #Optional, - filter_with_Qvalue = TRUE, ## same as default - qvalue_cutoff = 0.01, ## same as default - fewMeasurements=\"remove\", - removeProtein_with1Feature = TRUE, - use_log_file = FALSE)\n", sep = "") + if (isTRUE(input$big_file_spec)) { + codes = paste(codes, "library(MSstatsBig)\n", sep = "") + codes = paste(codes, "data = MSstatsBig::bigSpectronauttoMSstatsFormat(\n", sep = "") + codes = paste(codes, " input_file = \"insert your large Spectronaut file path\",\n", sep = "") + codes = paste(codes, " output_file_name = \"output_file.csv\",\n", sep = "") + codes = paste(codes, " backend = \"arrow\",\n", sep = "") + codes = paste(codes, " filter_by_excluded = ", input$filter_by_excluded, ",\n", sep = "") + codes = paste(codes, " filter_by_identified = ", input$filter_by_identified, ",\n", sep = "") + codes = paste(codes, " filter_by_qvalue = ", input$filter_by_qvalue, ",\n", sep = "") + codes = paste(codes, " qvalue_cutoff = ", input$qvalue_cutoff, ",\n", sep = "") + codes = paste(codes, " max_feature_count = ", input$max_feature_count, ",\n", sep = "") + codes = paste(codes, " filter_unique_peptides = ", input$filter_unique_peptides, ",\n", sep = "") + codes = paste(codes, " aggregate_psms = ", input$aggregate_psms, ",\n", sep = "") + codes = paste(codes, " filter_few_obs = ", input$filter_few_obs, "\n", sep = "") + codes = paste(codes, ")\n", sep = "") + codes = paste(codes, "data = dplyr::collect(data)\n", sep = "") + } else { + codes = paste(codes, "data = read.csv(\"insert your MSstats scheme output from Spectronaut filepath\", header = TRUE, sep = ",input$sep_specdata,")\nannot_file = read.csv(\"insert your annotation filepath\", sep='\t')#Optional\n" + , sep = "") + codes = paste(codes, "data = SpectronauttoMSstatsFormat(data, + annotation = annot_file #Optional, + filter_with_Qvalue = TRUE, ## same as default + qvalue_cutoff = 0.01, ## same as default + fewMeasurements=\"remove\", + removeProtein_with1Feature = TRUE, + use_log_file = FALSE)\n", sep = "") + } } else if(input$filetype == 'diann') { - codes = paste(codes, "data = read.csv(\"insert your MSstats scheme output from DIANN filepath\", header = TRUE, sep = '\\t')\nannot_file = read.csv(\"insert your annotation filepath\")#Optional\n" - , sep = "") - - codes = paste(codes, "data = DIANNtoMSstatsFormat(data, - annotation = annot_file, #Optional - qvalue_cutoff = 0.01, ## same as default - removeProtein_with1Feature = TRUE, - use_log_file = FALSE)\n", sep = "") + if (isTRUE(input$big_file_diann)) { + codes = paste(codes, "library(MSstatsBig)\n", sep = "") + codes = paste(codes, "data = MSstatsBig::bigDIANNtoMSstatsFormat(\n", sep = "") + codes = paste(codes, " input_file = \"insert your large DIANN file path\",\n", sep = "") + codes = paste(codes, " output_file_name = \"output_file.csv\",\n", sep = "") + codes = paste(codes, " backend = \"arrow\",\n", sep = "") + codes = paste(codes, " MBR = ", isTRUE(input$diann_MBR), ",\n", sep = "") + codes = paste(codes, " quantificationColumn = \"", input$diann_quantificationColumn, "\",\n", sep = "") + codes = paste(codes, " max_feature_count = ", input$max_feature_count, ",\n", sep = "") + codes = paste(codes, " filter_unique_peptides = ", input$filter_unique_peptides, ",\n", sep = "") + codes = paste(codes, " aggregate_psms = ", input$aggregate_psms, ",\n", sep = "") + codes = paste(codes, " filter_few_obs = ", input$filter_few_obs, "\n", sep = "") + codes = paste(codes, ")\n", sep = "") + codes = paste(codes, "data = dplyr::collect(data)\n", sep = "") + } else { + codes = paste(codes, "data = read.csv(\"insert your MSstats scheme output from DIANN filepath\", header = TRUE, sep = '\\t')\nannot_file = read.csv(\"insert your annotation filepath\")#Optional\n" + , sep = "") + + codes = paste(codes, "data = DIANNtoMSstatsFormat(data, + annotation = annot_file, #Optional + qvalue_cutoff = 0.01, ## same as default + removeProtein_with1Feature = TRUE, + use_log_file = FALSE)\n", sep = "") + } } else if(input$filetype == 'open') { From 1fc981cce2a687cc2eb8488996277c4a7fff9bbc Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Tue, 3 Feb 2026 00:41:07 -0600 Subject: [PATCH 08/14] Adding test --- tests/testthat/test-utils.R | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 8d3d6ce..e72f98b 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -1527,3 +1527,78 @@ describe("getData for Big Spectronaut", { expect_null(res) }) }) + +describe("getData for Big DIANN", { + + # Common mock input for big diann + mock_input_big_diann <- list( + filetype = "diann", + big_file_diann = TRUE, + big_file_browse = list(files = list("file.csv")), + max_feature_count = 20, + diann_MBR = TRUE, + diann_quantificationColumn = "Fragment.Quant.Corrected", + filter_unique_peptides = TRUE, + aggregate_psms = TRUE, + filter_few_obs = TRUE, + BIO = "Protein", + DDA_DIA = "DIA" + ) + + # Mock data to return + mock_arrow_obj <- list(dummy = "arrow") + mock_df <- data.frame(ProteinName = "P1", Intensity = 100) + + test_that("Valid input returns data", { + # Mocks + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "MSstatsBig::bigDIANNtoMSstatsFormat", mock_arrow_obj) + stub(getData, "dplyr::collect", mock_df) + stub(getData, "showNotification", function(...) NULL) + stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + + res <- getData(mock_input_big_diann) + expect_equal(res, mock_df) + }) + + test_that("Invalid max_feature_count returns NULL", { + bad_input <- mock_input_big_diann + bad_input$max_feature_count <- 0 + + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "showNotification", function(msg, ...) expect_match(msg, "max_feature_count")) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + + res <- getData(bad_input) + expect_null(res) + }) + + test_that("File not found returns NULL", { + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "nonexistent.csv")) + stub(getData, "file.exists", FALSE) + stub(getData, "showNotification", function(msg, ...) expect_match(msg, "does not exist")) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + + res <- getData(mock_input_big_diann) + expect_null(res) + }) + + test_that("Memory error returns NULL", { + stub(getData, "shinyFiles::getVolumes", function() function() c(root = "/")) + stub(getData, "shinyFiles::parseFilePaths", function(...) data.frame(datapath = "test.csv")) + stub(getData, "file.exists", TRUE) + stub(getData, "MSstatsBig::bigDIANNtoMSstatsFormat", mock_arrow_obj) + stub(getData, "dplyr::collect", function(...) stop("Memory allocation failed")) + stub(getData, "showNotification", function(msg, ...) expect_match(msg, "Memory Error")) + stub(getData, "shinybusy::update_modal_spinner", function(...) NULL) + stub(getData, "shinybusy::remove_modal_spinner", function(...) NULL) + + res <- getData(mock_input_big_diann) + expect_null(res) + }) +}) From bd4d39a1ecbd5eac0dc0ba117330130c1d84d1af Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Wed, 4 Feb 2026 23:38:13 -0600 Subject: [PATCH 09/14] Fixing TSV file issue --- DESCRIPTION | 2 +- R/utils.R | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c085cbf..e9a357a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: MSstatsShiny Type: Package Title: MSstats GUI for Statistical Anaylsis of Proteomics Experiments -Version: 1.13.0 +Version: 1.13.1 Description: MSstatsShiny is an R-Shiny graphical user interface (GUI) integrated with the R packages MSstats, MSstatsTMT, and MSstatsPTM. It provides a point and click end-to-end analysis pipeline applicable to a wide diff --git a/R/utils.R b/R/utils.R index 25f9ec8..a50c0f7 100644 --- a/R/utils.R +++ b/R/utils.R @@ -646,7 +646,15 @@ getData <- function(input) { if (getFileExtension(input$dianndata$name) %in% c("parquet", "pq")) { data = read_parquet(input$dianndata$datapath) } else { - data = read.csv(input$dianndata$datapath, sep=input$sep_dianndata) + sep = input$sep_dianndata + if(is.null(sep)) { + sep = "\t" + } + if (sep == "\t") { + data = read.delim(input$dianndata$datapath) + } else { + data = read.csv(input$dianndata$datapath, sep = sep) + } } qvalue_cutoff = 0.01 @@ -941,8 +949,16 @@ library(MSstatsPTM)\n", sep = "") codes = paste(codes, ")\n", sep = "") codes = paste(codes, "data = dplyr::collect(data)\n", sep = "") } else { - codes = paste(codes, "data = read.csv(\"insert your MSstats scheme output from DIANN filepath\", header = TRUE, sep = '\\t')\nannot_file = read.csv(\"insert your annotation filepath\")#Optional\n" - , sep = "") + sep = input$sep_dianndata + if(is.null(sep)) { + sep = "\t" + } + + if (sep == "\t") { + codes = paste(codes, "data = read.delim(\"insert your MSstats scheme output from DIANN filepath\")\nannot_file = read.csv(\"insert your annotation filepath\")#Optional\n", sep = "") + } else { + codes = paste(codes, "data = read.csv(\"insert your MSstats scheme output from DIANN filepath\", header = TRUE, sep = '", sep, "')\nannot_file = read.csv(\"insert your annotation filepath\")#Optional\n", sep = "") + } codes = paste(codes, "data = DIANNtoMSstatsFormat(data, annotation = annot_file, #Optional From cf54d435ca2cda607529142ac51c65df789e3cd6 Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Wed, 4 Feb 2026 23:39:29 -0600 Subject: [PATCH 10/14] removing this file --- tests/testthat/test-loadpage-server.R | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 tests/testthat/test-loadpage-server.R diff --git a/tests/testthat/test-loadpage-server.R b/tests/testthat/test-loadpage-server.R deleted file mode 100644 index 12a7231..0000000 --- a/tests/testthat/test-loadpage-server.R +++ /dev/null @@ -1,21 +0,0 @@ -test_that(".get_data_source_type correctly identifies the data loading path", { - # This test checks the helper function that determines whether to use the - # standard data loader or the special loader for large Spectronaut files. - # This approach is simpler and more robust than a complex shiny::testServer test. - - # Case 1: Spectronaut file with the 'big file' checkbox checked - expect_equal(.get_data_source_type("spec", TRUE), "big_spectronaut") - - # Case 2: Spectronaut file without the 'big file' checkbox - expect_equal(.get_data_source_type("spec", FALSE), "standard") - - # Case 3: A non-Spectronaut file (should always be standard) - expect_equal(.get_data_source_type("maxq", FALSE), "standard") - - # Case 4: A non-Spectronaut file where the Spectronaut checkbox might be TRUE - # (though the UI should prevent this, the logic should be robust) - expect_equal(.get_data_source_type("maxq", TRUE), "standard") - - # Case 5: Input is NULL (initial state) - expect_equal(.get_data_source_type("spec", NULL), "standard") -}) From 5c7c74b1e79a4b509ef350d4c897f69e66e415e0 Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Fri, 6 Feb 2026 21:43:53 -0600 Subject: [PATCH 11/14] Add separator buttons to DIANN big file mode UI --- R/module-loadpage-server.R | 4 ++-- R/module-loadpage-ui.R | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index ac0a2ce..7d8bf38 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -94,7 +94,7 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { ui_elements <- tagList(ui_elements, create_diann_standard_ui(session$ns)) } - ui_elements + tagList(ui_elements, create_separator_buttons(session$ns, "sep_dianndata")) }) output$diann_options_ui <- renderUI({ @@ -117,7 +117,7 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { NULL } }) - + output$spectronaut_options_ui <- renderUI({ req(input$filetype == 'spec', input$BIO != 'PTM') diff --git a/R/module-loadpage-ui.R b/R/module-loadpage-ui.R index 2969f8f..efae2bf 100644 --- a/R/module-loadpage-ui.R +++ b/R/module-loadpage-ui.R @@ -321,10 +321,7 @@ create_spectronaut_standard_ui <- function(ns) { #' Create DIANN standard file input #' @noRd create_diann_standard_ui <- function(ns) { - tagList( - fileInput(ns('dianndata'), "", multiple = FALSE, accept = NULL), - create_separator_buttons(ns, "sep_dianndata") - ) + fileInput(ns('dianndata'), "", multiple = FALSE, accept = NULL) } #' Create Spectronaut large file selection UI From f03f00316ba0d5a411b33c08c5b6db2a9d93d72f Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Fri, 6 Feb 2026 23:24:38 -0600 Subject: [PATCH 12/14] consistent separator requirement between Spectronaut and DIANN big-file modes. --- R/module-loadpage-server.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/module-loadpage-server.R b/R/module-loadpage-server.R index 7d8bf38..ba8b707 100644 --- a/R/module-loadpage-server.R +++ b/R/module-loadpage-server.R @@ -241,9 +241,9 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE) { enable("proceed1") } } else if (input$filetype == "diann") { - diann_regular_file_ok <- !isTRUE(input$big_file_diann) && !is.null(input$dianndata) && !is.null(input$sep_dianndata) + diann_regular_file_ok <- !isTRUE(input$big_file_diann) && !is.null(input$dianndata) diann_big_file_ok <- isTRUE(input$big_file_diann) && length(local_big_file_path()) > 0 - if(diann_regular_file_ok || diann_big_file_ok) { + if((diann_regular_file_ok || diann_big_file_ok) && !is.null(input$sep_dianndata)) { enable("proceed1") } } From 6d06c88b36ecaadb57f14895f94752ac41c7f845 Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Fri, 13 Feb 2026 13:04:42 -0600 Subject: [PATCH 13/14] Updating BigDiann function to accept annotation file. Updating MSstatsShiny.R with dependancies --- R/MSstatsShiny.R | 1 + R/utils.R | 5 +- tests/testthat/test_parallel_simulation.R | 159 ++++++++++++++++++++++ 3 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 tests/testthat/test_parallel_simulation.R diff --git a/R/MSstatsShiny.R b/R/MSstatsShiny.R index c378098..1861d5c 100644 --- a/R/MSstatsShiny.R +++ b/R/MSstatsShiny.R @@ -35,6 +35,7 @@ #' @importFrom dplyr `%>%` filter summarise n_distinct group_by ungroup select n mutate #' @importFrom tidyr unite #' @importFrom MSstatsConvert MSstatsLogsSettings +#' @importFrom MSstatsBig bigDIANNtoMSstatsFormat bigSpectronauttoMSstatsFormat #' @importFrom MSstatsPTM dataProcessPlotsPTM groupComparisonPlotsPTM MaxQtoMSstatsPTMFormat PDtoMSstatsPTMFormat FragPipetoMSstatsPTMFormat SkylinetoMSstatsPTMFormat SpectronauttoMSstatsPTMFormat #' @importFrom utils capture.output head packageVersion read.csv read.delim write.csv #' @importFrom stats aggregate diff --git a/R/utils.R b/R/utils.R index a50c0f7..4aa0094 100644 --- a/R/utils.R +++ b/R/utils.R @@ -530,7 +530,7 @@ getData <- function(input) { shinybusy::update_modal_spinner(text = "Processing large Spectronaut file...") # Call the big file conversion function from MSstatsConvert - converted_data <- MSstatsBig::bigSpectronauttoMSstatsFormat( + converted_data <- bigSpectronauttoMSstatsFormat( input_file = local_big_file_path, output_file_name = "output_file.csv", backend = "arrow", @@ -614,8 +614,9 @@ getData <- function(input) { shinybusy::update_modal_spinner(text = "Processing large DIANN file...") # Call the big file conversion function from MSstatsConvert - converted_data <- MSstatsBig::bigDIANNtoMSstatsFormat( + converted_data <- bigDIANNtoMSstatsFormat( input_file = local_big_file_path, + annotation = getAnnot(input), output_file_name = "output_file.csv", backend = "arrow", MBR = isTRUE(input$diann_MBR), diff --git a/tests/testthat/test_parallel_simulation.R b/tests/testthat/test_parallel_simulation.R new file mode 100644 index 0000000..166ef4b --- /dev/null +++ b/tests/testthat/test_parallel_simulation.R @@ -0,0 +1,159 @@ +library(data.table) +library(survival) +library(parallel) + +# ------------------------------------------------------------------------- +# 1. Mock Internal MSstats Functions +# ------------------------------------------------------------------------- + +.fitSurvival <- function(data, iterations) { + tryCatch({ + # Create a real survreg object to simulate memory usage + # Use a subset of data to ensure stability/speed, as we only care about object size + fit <- survreg(Surv(newABUNDANCE, !cen, type="left") ~ 1, + data = head(data, 500), dist = "gaussian") + # Artificially bloat the object to simulate a complex model (approx 100MB) + # Real MSstats models can be very large due to model frames and environments + fit$bloat <- numeric(12.5 * 1024 * 1024) + return(fit) + }, error = function(e) return(NULL)) +} + +.isSummarizable <- function(data, remove50missing) return(data) + +.runTukey <- function(data, is_labeled, censored_symbol, remove50missing) { + return(data.table(Protein = "TestProtein", LogIntensities = mean(data$newABUNDANCE, na.rm=TRUE))) +} + +# ------------------------------------------------------------------------- +# 2. Define Functions with "Work Simulation" (Sleep) +# ------------------------------------------------------------------------- + +# LEAKY VERSION +MSstatsSummarizeSingleTMP_Leaky_Sim <- function (single_protein, impute, censored_symbol, remove50missing, aft_iterations = 90) { + # ... Setup ... + newABUNDANCE = n_obs = n_obs_run = RUN = FEATURE = LABEL = NULL + predicted = censored = NULL + cols = intersect(colnames(single_protein), c("newABUNDANCE", "cen", "RUN", "FEATURE", "ref")) + single_protein = single_protein[(n_obs > 1 & !is.na(n_obs)) & (n_obs_run > 0 & !is.na(n_obs_run))] + if (nrow(single_protein) == 0) return(list(NULL, NULL)) + single_protein[, `:=`(RUN, factor(RUN))] + single_protein[, `:=`(FEATURE, factor(FEATURE))] + + if (impute & any(single_protein[["censored"]])) { + converged = TRUE + survival_fit = withCallingHandlers({ + .fitSurvival(single_protein[LABEL == "L", cols, with = FALSE], aft_iterations) + }, warning = function(w) { if (grepl("converge", conditionMessage(w), ignore.case = TRUE)) converged <<- FALSE }) + + if (converged && !is.null(survival_fit)) { + single_protein[, `:=`(predicted, predict(survival_fit, newdata = .SD))] + } else { + single_protein[, `:=`(predicted, NA_real_)] + } + + # --- LEAK SIMULATION --- + # The object 'survival_fit' is still in memory here. + # We simulate "doing other work" (predictions, formatting) by sleeping. + Sys.sleep(1) + + # Report Memory Usage of this Worker + mem_used <- sum(gc()[,2]) + msg <- sprintf("[Worker %d] LEAKY State - Holding Memory: %.2f MB\n", Sys.getpid(), mem_used) + cat(msg) + cat(msg, file = "parallel_log.txt", append = TRUE) + + single_protein[, `:=`(predicted, ifelse(censored & (LABEL == "L"), predicted, NA))] + single_protein[, `:=`(newABUNDANCE, ifelse(censored & LABEL == "L", predicted, newABUNDANCE))] + survival = single_protein[, c(cols, "predicted"), with = FALSE] + } else { + survival = single_protein[, cols, with = FALSE] + survival[, `:=`(predicted, NA)] + } + # ... Finalize ... + single_protein = .isSummarizable(single_protein, remove50missing) + if (is.null(single_protein)) return(list(NULL, NULL)) + result = .runTukey(single_protein, TRUE, censored_symbol, remove50missing) + list(result, survival) +} + +# FIXED VERSION +MSstatsSummarizeSingleTMP_Fixed_Sim <- function (single_protein, impute, censored_symbol, remove50missing, aft_iterations = 90) { + # ... Setup ... + newABUNDANCE = n_obs = n_obs_run = RUN = FEATURE = LABEL = NULL + predicted = censored = NULL + cols = intersect(colnames(single_protein), c("newABUNDANCE", "cen", "RUN", "FEATURE", "ref")) + single_protein = single_protein[(n_obs > 1 & !is.na(n_obs)) & (n_obs_run > 0 & !is.na(n_obs_run))] + if (nrow(single_protein) == 0) return(list(NULL, NULL)) + single_protein[, `:=`(RUN, factor(RUN))] + single_protein[, `:=`(FEATURE, factor(FEATURE))] + + if (impute & any(single_protein[["censored"]])) { + converged = TRUE + survival_fit = withCallingHandlers({ + .fitSurvival(single_protein[LABEL == "L", cols, with = FALSE], aft_iterations) + }, warning = function(w) { if (grepl("converge", conditionMessage(w), ignore.case = TRUE)) converged <<- FALSE }) + + if (converged && !is.null(survival_fit)) { + single_protein[, `:=`(predicted, predict(survival_fit, newdata = .SD))] + } else { + single_protein[, `:=`(predicted, NA_real_)] + } + + # --- FIX APPLIED --- + rm(survival_fit) + + # --- FIXED SIMULATION --- + # We simulate "doing other work" by sleeping. + Sys.sleep(1) + + # Report Memory Usage of this Worker + mem_used <- sum(gc()[,2]) + msg <- sprintf("[Worker %d] FIXED State - Holding Memory: %.2f MB\n", Sys.getpid(), mem_used) + cat(msg) + cat(msg, file = "parallel_log.txt", append = TRUE) + + single_protein[, `:=`(predicted, ifelse(censored & (LABEL == "L"), predicted, NA))] + single_protein[, `:=`(newABUNDANCE, ifelse(censored & LABEL == "L", predicted, newABUNDANCE))] + survival = single_protein[, c(cols, "predicted"), with = FALSE] + } else { + survival = single_protein[, cols, with = FALSE] + survival[, `:=`(predicted, NA)] + } + # ... Finalize ... + single_protein = .isSummarizable(single_protein, remove50missing) + if (is.null(single_protein)) return(list(NULL, NULL)) + result = .runTukey(single_protein, TRUE, censored_symbol, remove50missing) + list(result, survival) +} + +# ------------------------------------------------------------------------- +# 3. Run Simulation +# ------------------------------------------------------------------------- + +set.seed(123) +n_rows <- 20000 +dt <- data.table( + newABUNDANCE = rnorm(n_rows, 20, 5), + censored = sample(c(TRUE, FALSE), n_rows, replace=TRUE, prob=c(0.3, 0.7)), + LABEL = "L", RUN = sample(1:20, n_rows, replace=TRUE), FEATURE = sample(1:500, n_rows, replace=TRUE), + n_obs = 5, n_obs_run = 5, cen = FALSE, ref = "ref" +) +dt$cen <- dt$censored +dt$newABUNDANCE[dt$censored] <- dt$newABUNDANCE[dt$censored] - 5 + +# Clear log file +file.create("parallel_log.txt") + +cat("\n--- Simulating LEAKY Parallel Execution ---\n") +# We run 2 cores. Both will hit the 'sleep' at the same time. +# Both will report HIGH memory because they haven't cleaned up yet. +invisible(mclapply(1:2, function(i) MSstatsSummarizeSingleTMP_Leaky_Sim(copy(dt), TRUE, "NA", FALSE), mc.cores = 2)) + +cat("\n--- Simulating FIXED Parallel Execution ---\n") +# We run 2 cores. Both will hit the 'sleep' at the same time. +# Both will report LOW memory because they cleaned up BEFORE sleeping. +invisible(mclapply(1:2, function(i) MSstatsSummarizeSingleTMP_Fixed_Sim(copy(dt), TRUE, "NA", FALSE), mc.cores = 2)) + +cat("\n--- Log File Content ---\n") +cat(readLines("parallel_log.txt"), sep = "\n") \ No newline at end of file From b86880d5f3523d3322199ecbb6b3fd38f454678f Mon Sep 17 00:00:00 2001 From: Rudhik1904 Date: Wed, 18 Feb 2026 18:08:58 -0600 Subject: [PATCH 14/14] Updating Namespace --- NAMESPACE | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NAMESPACE b/NAMESPACE index 1690c4b..7ecb09d 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -37,6 +37,8 @@ importFrom(DT,datatable) importFrom(DT,renderDT) importFrom(DT,renderDataTable) importFrom(Hmisc,describe) +importFrom(MSstatsBig,bigDIANNtoMSstatsFormat) +importFrom(MSstatsBig,bigSpectronauttoMSstatsFormat) importFrom(MSstatsBioNet,annotateProteinInfoFromIndra) importFrom(MSstatsBioNet,generateCytoscapeConfig) importFrom(MSstatsBioNet,getSubnetworkFromIndra)