From 88346f5720318075795a9e8d532ff90a8959b932 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 12:43:46 +0100 Subject: [PATCH 01/65] Version bump --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index a8ca6b9..ef7060c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: einprot Type: Package Title: A collection of proteomics analysis utilities and workflows -Version: 0.9.8 +Version: 0.10.0 Authors@R: c( person("Charlotte", "Soneson", email = "charlotte.soneson@fmi.ch", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-3833-2169")), From 602b022e7f6891e1ece6a96d3db06870b9eb1799 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 14:52:52 +0100 Subject: [PATCH 02/65] Define new filterFeaturesSE function, replacing tool-specific filtering functions --- R/doFilter.R | 766 ++++++++++++--------------------------- man/defaultFiltersSE.Rd | 39 ++ man/filterFeaturesSE.Rd | 83 +++++ man/filterFragPipe.Rd | 57 --- man/filterMaxQuant.Rd | 48 --- man/filterPDTMT.Rd | 114 ------ man/filterSpectronaut.Rd | 56 --- 7 files changed, 354 insertions(+), 809 deletions(-) create mode 100644 man/defaultFiltersSE.Rd create mode 100644 man/filterFeaturesSE.Rd delete mode 100644 man/filterFragPipe.Rd delete mode 100644 man/filterMaxQuant.Rd delete mode 100644 man/filterPDTMT.Rd delete mode 100644 man/filterSpectronaut.Rd diff --git a/R/doFilter.R b/R/doFilter.R index 689e361..0c7a237 100644 --- a/R/doFilter.R +++ b/R/doFilter.R @@ -28,20 +28,21 @@ } } -#' Filter out features in MaxQuant data +#' Filter features in a SummarizedExperiment object #' -#' Exclude features with 'Score' below \code{minScore}, 'Peptides' below -#' \code{minPeptides}, or identified as either 'Reverse', -#' 'Potential.contaminant' or 'Only.identified.by.site' by \code{MaxQuant}. +#' Filter the features (rows) in a SummarizedExperiment object based on a +#' user-defined combination of filters. #' #' @author Charlotte Soneson #' @export #' #' @param sce A \code{SummarizedExperiment} object (or a derivative). -#' @param minScore Numeric scalar, the minimum allowed value in the 'Score' -#' column in order to retain the feature. -#' @param minPeptides Numeric scalar, the minimum allowed value in the -#' 'Peptides' column in order to retain the feature. +#' @param filtersSE A named \code{list}, where each element is a function +#' that takes a \code{SummarizedExperiment} object as input and returns +#' a logical vector of the same length as the number of rows in \code{sce}, +#' and where \code{TRUE} implies that the row should be retained. Default +#' sets of filtering functions for input data from different tools are +#' provided (see \code{?defaultFiltersSE}). #' @param plotUpset Logical scalar, whether to generate an UpSet plot #' detailing the reasons for features being filtered out. Only #' generated if any feature is in fact filtered out. @@ -52,156 +53,18 @@ #' @returns A filtered object of the same type as \code{sce}. #' #' @examples +#' # MaxQuant #' sce <- importExperiment(inFile = system.file("extdata", "mq_example", #' "1356_proteinGroups.txt", #' package = "einprot"), #' iColPattern = "^LFQ.intensity.")$sce #' #' dim(sce) -#' sce <- filterMaxQuant(sce = sce, minScore = 2, minPeptides = 2, -#' plotUpset = TRUE) +#' sce <- filterFeaturesSE(sce = sce, filtersSE = einprotMQfilters, +#' plotUpset = TRUE) #' dim(sce) #' -#' @importFrom SummarizedExperiment rowData -#' @importFrom dplyr select mutate across -#' @importFrom ComplexUpset upset -#' @importFrom rlang .data -#' -filterMaxQuant <- function(sce, minScore, minPeptides, plotUpset = TRUE, - exclFile = NULL) { - .assertVector(x = sce, type = "SummarizedExperiment") - .assertScalar(x = minScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPeptides, type = "numeric", allowNULL = TRUE) - .assertScalar(x = plotUpset, type = "logical") - .assertScalar(x = exclFile, type = "character", allowNULL = TRUE) - - ## Make sure that the columns used for filtering are character vectors - if ("Reverse" %in% colnames(rowData(sce))) { - rowData(sce)$Reverse[is.na(rowData(sce)$Reverse)] <- "" - } - if ("Potential.contaminant" %in% colnames(rowData(sce))) { - rowData(sce)$Potential.contaminant[is.na( - rowData(sce)$Potential.contaminant)] <- "" - } - if ("Only.identified.by.site" %in% colnames(rowData(sce))) { - rowData(sce)$Only.identified.by.site[is.na( - rowData(sce)$Only.identified.by.site)] <- "" - } - - filtdf <- as.data.frame(SummarizedExperiment::rowData(sce)) %>% - dplyr::select(dplyr::any_of(c("Reverse", "Potential.contaminant", - "Only.identified.by.site", "Score", - "Peptides"))) %>% - dplyr::mutate(across(dplyr::any_of(c("Reverse", "Potential.contaminant", - "Only.identified.by.site")), - function(x) as.numeric(x == "+"))) - if ("Score" %in% colnames(filtdf) && !is.null(minScore)) { - filtdf <- filtdf %>% - dplyr::mutate(Score = as.numeric((.data$Score < minScore) | - is.na(.data$Score))) - } else { - filtdf$Score <- NULL - } - if ("Peptides" %in% colnames(filtdf) && !is.null(minPeptides)) { - filtdf <- filtdf %>% - dplyr::mutate( - Peptides = as.numeric((.data$Peptides < minPeptides) | - is.na(.data$Peptides))) - } else { - filtdf$Peptides <- NULL - } - - keep <- seq_len(nrow(sce)) - if ("Reverse" %in% colnames(rowData(sce))) { - keep <- intersect(keep, which(rowData(sce)$Reverse == "")) - } - if ("Potential.contaminant" %in% colnames(rowData(sce))) { - keep <- intersect(keep, which(rowData(sce)$Potential.contaminant == "")) - } - if ("Only.identified.by.site" %in% colnames(rowData(sce))) { - keep <- intersect( - keep, which(rowData(sce)$Only.identified.by.site == "")) - } - if ("Score" %in% colnames(rowData(sce)) && !is.null(minScore)) { - keep <- intersect(keep, which(rowData(sce)$Score >= minScore)) - } - if ("Peptides" %in% colnames(rowData(sce)) && !is.null(minPeptides)) { - keep <- intersect(keep, which(rowData(sce)$Peptides >= minPeptides)) - } - exclude <- rowData(sce[setdiff(seq_len(nrow(sce)), keep), ]) - sce <- sce[keep, ] - - if (nrow(filtdf[rowSums(filtdf) == 0, , drop = FALSE]) != nrow(sce)) { - ## This should not happen - #nocov start - stop("Something went wrong in the filtering - filtdf and sce are of ", - "different sizes") - #nocov end - } - .makeFilterPlot(filtdf = filtdf, plotUpset = plotUpset) - - if (!is.null(exclFile)) { - write.table(exclude, file = exclFile, quote = FALSE, sep = "\t", - row.names = FALSE, col.names = TRUE) - } - sce -} - -#' Filter out features in PD/TMT data -#' -#' If \code{inputLevel} is "Proteins", exclude features with -#' 'Score.Sequest.HT.Sequest.HT' below \code{minScore}, -#' 'Number.of.Peptides' below \code{minPeptides}, or identified as -#' 'Contaminant' by \code{ProteomeDiscoverer}. -#' If \code{inputLevel} is "PeptideGroups", exclude features with -#' 'Delta.Score.by.Search.Engine.Sequest.HT' below \code{minDeltaScore}, -#' 'Number.of.PSMs' below \code{minPSMs}, or identified as -#' 'Contaminant' by \code{ProteomeDiscoverer}. -#' -#' @author Charlotte Soneson -#' @export -#' -#' @param sce A \code{SummarizedExperiment} object (or a derivative). -#' @param inputLevel Either \code{"Proteins"} or \code{"PeptideGroups"}, -#' indicating the type of features in \code{sce}. -#' @param minScore Numeric scalar, the minimum allowed value in the -#' 'Score.Sequest.HT.Sequest.HT' column in order to retain the feature. -#' Only used if \code{inputLevel} is \code{"Proteins"}. -#' @param minPeptides Numeric scalar, the minimum allowed value in the -#' 'Number.of.Peptides' column in order to retain the feature. -#' Only used if \code{inputLevel} is \code{"Proteins"}. -#' @param minDeltaScore Numeric scalar, the minimum allowed value in the -#' 'Delta.Score.by.Search.Engine.Sequest.HT' column in order to retain the -#' feature. Only used if \code{inputLevel} is \code{"PeptideGroups"}. -#' @param minPSMs Numeric scalar, the minimum allowed value in the -#' 'Number.of.PSMs' column in order to retain the feature. -#' Only used if \code{inputLevel} is \code{"PeptideGroups"}. -#' @param masterProteinsOnly Logical scalar indicating whether only master -#' proteins (where the \code{Master} column value is -#' \code{IsMasterProtein}) should be retained. -#' @param modificationsCol Character string pointing to a column containing -#' modification details. \code{excludeUnmodifiedPeptides} and -#' \code{keepModifications} will use information from this column. Only -#' used if \code{inputLevel} is \code{"PeptideGroups"}. -#' @param excludeUnmodifiedPeptides Logical scalar, whether to filter out -#' peptides without modifications. Only used if \code{inputLevel} is -#' \code{"PeptideGroups"}. -#' @param keepModifications Character string (or \code{NULL}) indicating -#' which modifications to retain in the analysis. Can be a regular -#' expression, which will be matched against the \code{modificationsCol}. -#' If \code{NULL} (the default), all rows are retained. Only used if -#' \code{inputLevel} is \code{"PeptideGroups"}. -#' @param plotUpset Logical scalar, whether to generate an UpSet plot -#' detailing the reasons for features being filtered out. Only -#' generated if any feature is in fact filtered out. -#' @param exclFile Character scalar, the path to a text file where the -#' features that are filtered out are written. If \code{NULL} (default), -#' excluded features are not recorded. -#' -#' @returns A filtered object of the same type as \code{sce}. -#' -#' @examples -#' ## Proteins +#' # ProteomeDiscoverer, Proteins #' sce <- importExperiment( #' inFile = system.file("extdata", "pdtmt_example", #' "Fig2_m23139_RTS_QC_varMods_Proteins.txt", @@ -209,11 +72,11 @@ filterMaxQuant <- function(sce, minScore, minPeptides, plotUpset = TRUE, #' iColPattern = "^Abundance.F.+.Sample.")$sce #' #' dim(sce) -#' sce <- filterPDTMT(sce = sce, inputLevel = "Proteins", minScore = 2, -#' minPeptides = 2, plotUpset = TRUE) +#' sce <- filterFeaturesSE(sce = sce, filtersSE = einprotPDTMTProteinFilters, +#' plotUpset = TRUE) #' dim(sce) #' -#' ## PeptideGroups +#' # ProteomeDiscoverer, PeptideGroups #' sce <- importExperiment( #' inFile = system.file("extdata", "pdtmt_example", #' "Fig2_m23139_RTS_QC_varMods_PeptideGroups.txt", @@ -221,422 +84,257 @@ filterMaxQuant <- function(sce, minScore, minPeptides, plotUpset = TRUE, #' iColPattern = "^Abundance.F.+.Sample.")$sce #' #' dim(sce) -#' sce <- filterPDTMT(sce = sce, inputLevel = "PeptideGroups", -#' minPSMs = 2, plotUpset = TRUE, minDeltaScore = 0.2, -#' modificationsCol = "Modifications.in.Master.Proteins", -#' excludeUnmodifiedPeptides = TRUE) +#' sce <- filterFeaturesSE(sce = sce, filtersSE = einprotPDTMTPeptideGroupFilters) #' dim(sce) #' -#' @importFrom SummarizedExperiment rowData -#' @importFrom dplyr select mutate -#' @importFrom ComplexUpset upset +#' # FragPipe +#' sce <- importExperiment(inFile = system.file("extdata", "fp_example", +#' "combined_protein.tsv", +#' package = "einprot"), +#' iColPattern = ".MaxLFQ.Intensity$")$sce +#' +#' dim(sce) +#' sce <- filterFeaturesSE(sce = sce, filtersSE = einprotFragPipeFilters, +#' plotUpset = TRUE) +#' dim(sce) #' -filterPDTMT <- function(sce, inputLevel, minScore = 0, minPeptides = 0, - minDeltaScore = 0, minPSMs = 0, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL) { +filterFeaturesSE <- function(sce, filtersSE = list(), plotUpset = TRUE, + exclFile = NULL) { .assertVector(x = sce, type = "SummarizedExperiment") - .assertScalar(x = inputLevel, type = "character", - validValues = c("Proteins", "PeptideGroups")) - .assertScalar(x = minScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPeptides, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minDeltaScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPSMs, type = "numeric", allowNULL = TRUE) - .assertScalar(x = masterProteinsOnly, type = "logical") - if (inputLevel == "PeptideGroups") { - .assertScalar(x = modificationsCol, type = "character", - validValues = colnames(rowData(sce)), allowNULL = TRUE) - .assertScalar(x = excludeUnmodifiedPeptides, type = "logical") - .assertScalar(x = keepModifications, type = "character", - allowNULL = TRUE) + .assertVector(x = filtersSE, type = "list") + if (length(filtersSE) > 0) { + .assertVector(x = names(filtersSE), type = "character") + for (f in filtersSE) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } } .assertScalar(x = plotUpset, type = "logical") .assertScalar(x = exclFile, type = "character", allowNULL = TRUE) - ## Make sure that the columns used for filtering are character vectors - if ("Contaminant" %in% colnames(rowData(sce))) { - rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant) + if (length(filtersSE) > 0) { + # apply filters and check that all vectors are logical and have + # no NA values + tokeep <- lapply(filtersSE, function(f) f(sce)) + for (filts in names(tokeep)) { + stopifnot(is.logical(tokeep[[filts]]) && + all(!is.na(tokeep[[filts]]))) + } + + filtdf <- do.call(bind_rows, lapply(tokeep, function(x) !x)) + exclude <- rowData(sce)[which(rowSums(filtdf) > 0), , drop = FALSE] + sce <- sce[which(rowSums(filtdf) == 0), ] + + .makeFilterPlot(filtdf = filtdf, plotUpset = plotUpset) + + if (!is.null(exclFile)) { + write.table(exclude, file = exclFile, quote = FALSE, sep = "\t", + row.names = FALSE, col.names = TRUE) + } + sce + } else { + sce } +} - if (inputLevel == "Proteins") { - filtdf <- as.data.frame(SummarizedExperiment::rowData(sce)) %>% - dplyr::select(dplyr::any_of(c("Contaminant", "Number.of.Peptides", - "Score.Sequest.HT.Sequest.HT", - "Master"))) %>% - dplyr::mutate(across(dplyr::any_of(c("Contaminant")), - function(x) as.numeric(x == "True"))) - if ("Number.of.Peptides" %in% colnames(filtdf) && - !is.null(minPeptides)) { - filtdf <- filtdf %>% - dplyr::mutate( - Number.of.Peptides = - as.numeric((.data$Number.of.Peptides < minPeptides) | - is.na(.data$Number.of.Peptides))) +#' Default filters +#' +#' einprot provides default filters for data imported from various tools. +#' To see the functions and thresholds used in each of these, print the +#' corresponding object below. +#' +#' @export +#' @name defaultFiltersSE +#' @rdname defaultFiltersSE +einprotMQFilters <- list( + Score = function(sce) { + if ("Score" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Score >= 2 & !is.na(rowData(sce)$Score) } else { - filtdf$Number.of.Peptides <- NULL + kp <- rep(TRUE, nrow(sce)) } - if ("Score.Sequest.HT.Sequest.HT" %in% colnames(filtdf) && - !is.null(minScore)) { - filtdf <- filtdf %>% - dplyr::mutate( - Score.Sequest.HT.Sequest.HT = - as.numeric((.data$Score.Sequest.HT.Sequest.HT < minScore) | - is.na(.data$Score.Sequest.HT.Sequest.HT))) + kp + }, + Peptides = function(sce) { + if ("Peptides" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Peptides >= 2 & !is.na(rowData(sce)$Peptides) } else { - filtdf$Score.Sequest.HT.Sequest.HT <- NULL - } - if ("Master" %in% colnames(filtdf)) { - filtdf <- filtdf %>% - dplyr::mutate( - Master = as.numeric((.data$Master != "IsMasterProtein") | - is.na(.data$Master))) - } - - keep <- seq_len(nrow(sce)) - if ("Contaminant" %in% colnames(rowData(sce))) { - keep <- intersect(keep, which(rowData(sce)$Contaminant == "False")) - } - if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce)) && - !is.null(minScore)) { - keep <- intersect( - keep, - which(rowData(sce)$Score.Sequest.HT.Sequest.HT >= minScore) - ) - } - if ("Number.of.Peptides" %in% colnames(rowData(sce)) && - !is.null(minPeptides)) { - keep <- intersect( - keep, which(rowData(sce)$Number.of.Peptides >= minPeptides) - ) + kp <- rep(TRUE, nrow(sce)) } - if (masterProteinsOnly) { - if ("Master" %in% colnames(rowData(sce))) { - keep <- intersect( - keep, which(rowData(sce)$Master == "IsMasterProtein") - ) - } + kp + }, + Reverse = function(sce) { + if ("Reverse" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Reverse == "" | is.na(rowData(sce)$Reverse) } else { - filtdf$Master <- NULL + kp <- rep(TRUE, nrow(sce)) } - exclude <- rowData(sce[setdiff(seq_len(nrow(sce)), keep), ]) - sce <- sce[keep, ] - } else if (inputLevel == "PeptideGroups") { - colsToExtract <- c("Contaminant", "Number.of.PSMs", - "Delta.Score.by.Search.Engine.Sequest.HT") - if (!is.null(modificationsCol)) { - colsToExtract <- c(colsToExtract, modificationsCol) + kp + }, + Contaminant = function(sce) { + if ("Potential.contaminant" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Potential.contaminant == "" | + is.na(rowData(sce)$Potential.contaminant) + } else { + kp <- rep(TRUE, nrow(sce)) } - filtdf <- as.data.frame(SummarizedExperiment::rowData(sce)) %>% - dplyr::select(dplyr::any_of(colsToExtract)) %>% - dplyr::mutate(across(dplyr::any_of(c("Contaminant")), - function(x) as.numeric(x == "True"))) - if (!is.null(modificationsCol) && excludeUnmodifiedPeptides) { - filtdf <- filtdf %>% - dplyr::mutate(Unmodified.peptide = - .data[[modificationsCol]] == "") + kp + }, + OnlySite = function(sce) { + if ("Only.identified.by.site" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Only.identified.by.site == "" | + is.na(rowData(sce)$Only.identified.by.site) } else { - filtdf$Unmodified.peptide <- NULL + kp <- rep(TRUE, nrow(sce)) } - if (!is.null(modificationsCol) && !is.null(keepModifications)) { - filtdf <- filtdf %>% - dplyr::mutate(No.modification.tokeep = - !grepl(keepModifications, - .data[[modificationsCol]])) + kp + } +) + +#' @export +#' @rdname defaultFiltersSE +einprotPDTMTProteinFilters <- list( + Score = function(sce) { + if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Score.Sequest.HT.Sequest.HT >= 2 & + !is.na(rowData(sce)$Score.Sequest.HT.Sequest.HT) } else { - filtdf$No.modification.tokeep <- NULL + kp <- rep(TRUE, nrow(sce)) } - if ("Number.of.PSMs" %in% colnames(filtdf) && !is.null(minPSMs)) { - filtdf <- filtdf %>% - dplyr::mutate(Number.of.PSMs = - as.numeric((.data$Number.of.PSMs < minPSMs) | - is.na(.data$Number.of.PSMs))) + kp + }, + Peptides = function(sce) { + if ("Number.of.Peptides" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Number.of.Peptides >= 2 & !is.na(rowData(sce)$Number.of.Peptides) } else { - filtdf$Number.of.PSMs <- NULL + kp <- rep(TRUE, nrow(sce)) } - if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(filtdf) && - !is.null(minDeltaScore)) { - filtdf <- filtdf %>% - dplyr::mutate( - Delta.Score.by.Search.Engine.Sequest.HT = - as.numeric((.data$Delta.Score.by.Search.Engine.Sequest.HT < - minDeltaScore) | - is.na(.data$Delta.Score.by.Search.Engine.Sequest.HT))) + kp + }, + Contaminant = function(sce) { + if ("Contaminant" %in% colnames(rowData(sce))) { + rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant) + kp <- rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else { - filtdf$Delta.Score.by.Search.Engine.Sequest.HT <- NULL + kp <- rep(TRUE, nrow(sce)) } - ## Remove the modifications column (not needed anymore) - if (!is.null(modificationsCol) && - modificationsCol %in% colnames(filtdf)) { - filtdf[[modificationsCol]] <- NULL + kp + }, + Master = function(sce) { + if ("Master" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Master == "IsMasterProtein" & !is.na(rowData(sce)$Master) + } else { + kp <- rep(TRUE, nrow(sce)) } + kp + } +) - keep <- seq_len(nrow(sce)) - if ("Contaminant" %in% colnames(rowData(sce))) { - keep <- intersect(keep, which(rowData(sce)$Contaminant == "False")) - } - if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% - colnames(rowData(sce)) && - !is.null(minDeltaScore)) { - keep <- intersect( - keep, - which(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= minDeltaScore) - ) +#' @export +#' @rdname defaultFiltersSE +einprotPDTMTPeptideGroupFilters <- list( + DeltaScore = function(sce) { + if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.2 & + !is.na(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT) + } else { + kp <- rep(TRUE, nrow(sce)) } - if ("Number.of.PSMs" %in% colnames(rowData(sce)) && !is.null(minPSMs)) { - keep <- intersect( - keep, which(rowData(sce)$Number.of.PSMs >= minPSMs) - ) + kp + }, + PSMs = function(sce) { + if ("Number.of.PSMs" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Number.of.PSMs >= 2 & !is.na(rowData(sce)$Number.of.PSMs) + } else { + kp <- rep(TRUE, nrow(sce)) } - if (!is.null(modificationsCol) && excludeUnmodifiedPeptides) { - keep <- intersect( - keep, which(rowData(sce)[[modificationsCol]] != "") - ) + kp + }, + Contaminant = function(sce) { + if ("Contaminant" %in% colnames(rowData(sce))) { + rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant) + kp <- rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) + } else { + kp <- rep(TRUE, nrow(sce)) } - if (!is.null(modificationsCol) && !is.null(keepModifications)) { - keep <- intersect( - keep, which(grepl(keepModifications, - rowData(sce)[[modificationsCol]])) - ) + kp + }, + ExclUnmodified = function(sce) { + if ("Modifications" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Modifications != "" & !is.na(rowData(sce)$Modifications) + } else { + kp <- rep(TRUE, nrow(sce)) } - - exclude <- rowData(sce[setdiff(seq_len(nrow(sce)), keep), ]) - sce <- sce[keep, ] + kp } +) - if (nrow(filtdf[rowSums(filtdf) == 0, , drop = FALSE]) != nrow(sce)) { - ## This should not happen - could end up here though if the values in - ## the Contaminant column are not the expected ones - stop("Something went wrong in the filtering - filtdf and sce are of ", - "different sizes") - } - .makeFilterPlot(filtdf = filtdf, plotUpset = plotUpset) - - if (!is.null(exclFile)) { - write.table(exclude, file = exclFile, quote = FALSE, sep = "\t", - row.names = FALSE, col.names = TRUE) - } - - sce -} - -#' Filter out features in FragPipe data -#' -#' Exclude features with 'Combined.Total.Peptides' below \code{minPeptides}, -#' or identified as either 'Reverse' (Protein name matching -#' \code{revPattern}) or 'Potential.contaminant' (Protein name starting -#' with \code{contam_}) by \code{FragPipe}. -#' -#' @author Charlotte Soneson #' @export -#' -#' @param sce A \code{SummarizedExperiment} object (or a derivative). -#' @param minPeptides Numeric scalar, the minimum allowed value in the -#' 'Combined.Total.Peptides' column in order to retain the feature. -#' @param plotUpset Logical scalar, whether to generate an UpSet plot -#' detailing the reasons for features being filtered out. Only -#' generated if any feature is in fact filtered out. -#' @param revPattern Character scalar providing the pattern (a regular -#' expression) used to identify decoys (reverse hits). The pattern is -#' matched against the IDs in the FragPipe \code{Protein} column. -#' @param exclFile Character scalar, the path to a text file where the -#' features that are filtered out are written. If \code{NULL} (default), -#' excluded features are not recorded. -#' -#' @returns A filtered object of the same type as \code{sce}. -#' -#' @examples -#' sce <- importExperiment(inFile = system.file("extdata", "fp_example", -#' "combined_protein.tsv", -#' package = "einprot"), -#' iColPattern = ".MaxLFQ.Intensity$")$sce -#' -#' dim(sce) -#' sce <- filterFragPipe(sce = sce, minPeptides = 2, -#' plotUpset = TRUE, -#' revPattern = "^rev_") -#' dim(sce) -#' -#' @importFrom SummarizedExperiment rowData -#' @importFrom dplyr select mutate across -#' @importFrom ComplexUpset upset -#' @importFrom rlang .data -#' -filterFragPipe <- function(sce, minPeptides, plotUpset = TRUE, - revPattern = "^rev_", exclFile = NULL) { - .assertVector(x = sce, type = "SummarizedExperiment") - .assertScalar(x = minPeptides, type = "numeric", allowNULL = TRUE) - .assertScalar(x = plotUpset, type = "logical") - .assertScalar(x = revPattern, type = "character") - .assertScalar(x = exclFile, type = "character", allowNULL = TRUE) - - ## Make sure that the columns used for filtering later are character vectors - rowData(sce)$Potential.contaminant <- - ifelse(grepl("^contam_", rowData(sce)$Protein), "+", "") - rowData(sce)$Reverse <- ifelse(grepl(revPattern, rowData(sce)$Protein), - "+", "") - - filtdf <- as.data.frame(SummarizedExperiment::rowData(sce)) %>% - dplyr::select(dplyr::any_of(c("Reverse", "Potential.contaminant", - "Combined.Total.Peptides"))) %>% - dplyr::mutate(across(dplyr::any_of(c("Reverse", - "Potential.contaminant")), - function(x) as.numeric(x == "+"))) - if ("Combined.Total.Peptides" %in% colnames(filtdf) && - !is.null(minPeptides)) { - filtdf <- filtdf %>% - dplyr::mutate( - Combined.Total.Peptides = as.numeric( - (.data$Combined.Total.Peptides < minPeptides) | - is.na(.data$Combined.Total.Peptides))) - } else { - filtdf$Combined.Total.Peptides <- NULL - } - - keep <- seq_len(nrow(sce)) - if ("Reverse" %in% colnames(rowData(sce))) { - keep <- intersect(keep, which(rowData(sce)$Reverse == "")) - } - if ("Potential.contaminant" %in% colnames(rowData(sce))) { - keep <- intersect(keep, which(rowData(sce)$Potential.contaminant == "")) - } - if ("Combined.Total.Peptides" %in% colnames(rowData(sce)) && - !is.null(minPeptides)) { - keep <- intersect( - keep, which(rowData(sce)$Combined.Total.Peptides >= minPeptides) - ) - } - exclude <- rowData(sce[setdiff(seq_len(nrow(sce)), keep), ]) - sce <- sce[keep, ] - - if (nrow(filtdf[rowSums(filtdf) == 0, , drop = FALSE]) != nrow(sce)) { - ## This should not happen - #nocov start - stop("Something went wrong in the filtering - filtdf and sce are of ", - "different sizes") - #nocov end - } - .makeFilterPlot(filtdf = filtdf, plotUpset = plotUpset) - - if (!is.null(exclFile)) { - write.table(exclude, file = exclFile, quote = FALSE, sep = "\t", - row.names = FALSE, col.names = TRUE) +#' @rdname defaultFiltersSE +einprotFragPipeFilters <- list( + Contaminant = function(sce) { + if ("Protein" %in% colnames(rowData(sce))) { + kp <- !grepl("^contam_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) + } else { + kp <- rep(TRUE, nrow(sce)) + } + kp + }, + Reverse = function(sce) { + if ("Protein" %in% colnames(rowData(sce))) { + kp <- !grepl("^rev_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) + } else { + kp <- rep(TRUE, nrow(sce)) + } + kp + }, + Peptides = function(sce) { + if ("Combined.Total.Peptides" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$Combined.Total.Peptides >= 2 & !is.na(rowData(sce)$Combined.Total.Peptides) + } else { + kp <- rep(TRUE, nrow(sce)) + } + kp } - sce -} +) -#' Filter out features in Spectronaut data -#' -#' Exclude features with 'PG.Cscore' below \code{minScore}, -#' 'PG.NrOfStrippedSequencesIdentified.Experiment.wide' below -#' \code{minPeptides}, or where the 'PG.ProteinGroups' column contains the -#' specified \code{revPattern}. -#' -#' @author Charlotte Soneson #' @export -#' -#' @param sce A \code{SummarizedExperiment} object (or a derivative). -#' @param minScore Numeric scalar, the minimum allowed value in the 'PG.Cscore' -#' column in order to retain the feature. -#' @param minPeptides Numeric scalar, the minimum allowed value in the -#' 'PG.NrOfStrippedSequencesIdentified.Experiment.wide' column in order to -#' retain the feature. -#' @param plotUpset Logical scalar, whether to generate an UpSet plot -#' detailing the reasons for features being filtered out. Only -#' generated if any feature is in fact filtered out. -#' @param revPattern Character scalar providing the pattern (a regular -#' expression) used to identify decoys (reverse hits). The pattern is -#' matched against the IDs in the Spectronaut \code{PG.ProteinGroups} -#' column. -#' @param contamPattern Character scalar providing the pattern (a regular -#' expression) used to identify contaminants. The pattern is -#' matched against the IDs in the Spectronaut \code{PG.ProteinGroups} -#' column. -#' @param exclFile Character scalar, the path to a text file where the -#' features that are filtered out are written. If \code{NULL} (default), -#' excluded features are not recorded. -#' -#' @returns A filtered object of the same type as \code{sce}. -#' -#' @importFrom SummarizedExperiment rowData -#' @importFrom dplyr select mutate across -#' @importFrom ComplexUpset upset -#' @importFrom rlang .data -#' -filterSpectronaut <- function(sce, minScore, minPeptides, plotUpset = TRUE, - revPattern = "_Decoy$", - contamPattern = "^contam_", exclFile = NULL) { - .assertVector(x = sce, type = "SummarizedExperiment") - .assertScalar(x = minScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPeptides, type = "numeric", allowNULL = TRUE) - .assertScalar(x = plotUpset, type = "logical") - .assertScalar(x = revPattern, type = "character") - .assertScalar(x = contamPattern, type = "character") - .assertScalar(x = exclFile, type = "character", allowNULL = TRUE) - - ## Make sure that the columns used for filtering later are character vectors - rowData(sce)$Reverse <- ifelse(grepl(revPattern, rowData(sce)$PG.ProteinGroups), - "+", "") - rowData(sce)$Contaminant <- ifelse(grepl(contamPattern, - rowData(sce)$PG.ProteinGroups), - "+", "") - - filtdf <- as.data.frame(SummarizedExperiment::rowData(sce)) %>% - dplyr::select(dplyr::any_of(c("Reverse", "PG.NrOfStrippedSequencesIdentified.Experiment.wide", - "PG.Cscore", "Contaminant"))) %>% - dplyr::mutate(across(dplyr::any_of(c("Reverse", "Contaminant")), - function(x) as.numeric(x == "+"))) - if ("PG.NrOfStrippedSequencesIdentified.Experiment.wide" %in% colnames(filtdf) && - !is.null(minPeptides)) { - filtdf <- filtdf %>% - dplyr::mutate( - PG.NrOfStrippedSequencesIdentified.Experiment.wide = as.numeric( - (.data$PG.NrOfStrippedSequencesIdentified.Experiment.wide < minPeptides) | - is.na(.data$PG.NrOfStrippedSequencesIdentified.Experiment.wide))) - } else { - filtdf$PG.NrOfStrippedSequencesIdentified.Experiment.wide <- NULL - } - if ("PG.Cscore" %in% colnames(filtdf) && !is.null(minScore)) { - filtdf <- filtdf %>% - dplyr::mutate(PG.Cscore = as.numeric((.data$PG.Cscore < minScore) | - is.na(.data$PG.Cscore))) - } else { - filtdf$PG.Cscore <- NULL - } - - keep <- seq_len(nrow(sce)) - if ("Reverse" %in% colnames(rowData(sce))) { - keep <- intersect(keep, which(rowData(sce)$Reverse == "")) - } - if ("Contaminant" %in% colnames(rowData(sce))) { - keep <- intersect(keep, which(rowData(sce)$Contaminant == "")) - } - if ("PG.NrOfStrippedSequencesIdentified.Experiment.wide" %in% colnames(rowData(sce)) && - !is.null(minPeptides)) { - keep <- intersect( - keep, which(rowData(sce)$PG.NrOfStrippedSequencesIdentified.Experiment.wide >= minPeptides) - ) - } - if ("PG.Cscore" %in% colnames(rowData(sce)) && !is.null(minScore)) { - keep <- intersect(keep, which(rowData(sce)$PG.Cscore >= minScore)) - } - exclude <- rowData(sce[setdiff(seq_len(nrow(sce)), keep), ]) - sce <- sce[keep, ] - - if (nrow(filtdf[rowSums(filtdf) == 0, , drop = FALSE]) != nrow(sce)) { - ## This should not happen - #nocov start - stop("Something went wrong in the filtering - filtdf and sce are of ", - "different sizes") - #nocov end - } - .makeFilterPlot(filtdf = filtdf, plotUpset = plotUpset) - - if (!is.null(exclFile)) { - write.table(exclude, file = exclFile, quote = FALSE, sep = "\t", - row.names = FALSE, col.names = TRUE) +#' @rdname defaultFiltersSE +einprotSpectronautFilters <- list( + Contaminant = function(sce) { + if ("PG.ProteinGroups" %in% colnames(rowData(sce))) { + kp <- !grepl("^contam_", rowData(sce)$PG.ProteinGroups) & + !is.na(rowData(sce)$PG.ProteinGroups) + } else { + kp <- rep(TRUE, nrow(sce)) + } + kp + }, + Reverse = function(sce) { + if ("PG.ProteinGroups" %in% colnames(rowData(sce))) { + kp <- !grepl("_Decoy$", rowData(sce)$PG.ProteinGroups) & + !is.na(rowData(sce)$PG.ProteinGroups) + } else { + kp <- rep(TRUE, nrow(sce)) + } + kp + }, + Peptides = function(sce) { + if ("PG.NrOfStrippedSequencesIdentified.Experiment.wide" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$PG.NrOfStrippedSequencesIdentified.Experiment.wide >= 2 & + !is.na(rowData(sce)$PG.NrOfStrippedSequencesIdentified.Experiment.wide) + } else { + kp <- rep(TRUE, nrow(sce)) + } + kp + }, + Score = function(sce) { + if ("PG.Cscore" %in% colnames(rowData(sce))) { + kp <- rowData(sce)$PG.Cscore >= 2 & + !is.na(rowData(sce)$PG.Cscore) + } else { + kp <- rep(TRUE, nrow(sce)) + } + kp } - sce -} +) diff --git a/man/defaultFiltersSE.Rd b/man/defaultFiltersSE.Rd new file mode 100644 index 0000000..7532291 --- /dev/null +++ b/man/defaultFiltersSE.Rd @@ -0,0 +1,39 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/doFilter.R +\docType{data} +\name{defaultFiltersSE} +\alias{defaultFiltersSE} +\alias{einprotMQFilters} +\alias{einprotPDTMTProteinFilters} +\alias{einprotPDTMTPeptideGroupFilters} +\alias{einprotFragPipeFilters} +\alias{einprotSpectronautFilters} +\title{Default filters} +\format{ +An object of class \code{list} of length 5. + +An object of class \code{list} of length 4. + +An object of class \code{list} of length 4. + +An object of class \code{list} of length 3. + +An object of class \code{list} of length 4. +} +\usage{ +einprotMQFilters + +einprotPDTMTProteinFilters + +einprotPDTMTPeptideGroupFilters + +einprotFragPipeFilters + +einprotSpectronautFilters +} +\description{ +einprot provides default filters for data imported from various tools. +To see the functions and thresholds used in each of these, print the +corresponding object below. +} +\keyword{datasets} diff --git a/man/filterFeaturesSE.Rd b/man/filterFeaturesSE.Rd new file mode 100644 index 0000000..452a000 --- /dev/null +++ b/man/filterFeaturesSE.Rd @@ -0,0 +1,83 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/doFilter.R +\name{filterFeaturesSE} +\alias{filterFeaturesSE} +\title{Filter features in a SummarizedExperiment object} +\usage{ +filterFeaturesSE(sce, filtersSE = list(), plotUpset = TRUE, exclFile = NULL) +} +\arguments{ +\item{sce}{A \code{SummarizedExperiment} object (or a derivative).} + +\item{filtersSE}{A named \code{list}, where each element is a function +that takes a \code{SummarizedExperiment} object as input and returns +a logical vector of the same length as the number of rows in \code{sce}, +and where \code{TRUE} implies that the row should be retained. Default +sets of filtering functions for input data from different tools are +provided (see \code{?defaultFiltersSE}).} + +\item{plotUpset}{Logical scalar, whether to generate an UpSet plot +detailing the reasons for features being filtered out. Only +generated if any feature is in fact filtered out.} + +\item{exclFile}{Character scalar, the path to a text file where the +features that are filtered out are written. If \code{NULL} (default), +excluded features are not recorded.} +} +\value{ +A filtered object of the same type as \code{sce}. +} +\description{ +Filter the features (rows) in a SummarizedExperiment object based on a +user-defined combination of filters. +} +\examples{ +# MaxQuant +sce <- importExperiment(inFile = system.file("extdata", "mq_example", + "1356_proteinGroups.txt", + package = "einprot"), + iColPattern = "^LFQ.intensity.")$sce + +dim(sce) +sce <- filterFeaturesSE(sce = sce, filtersSE = einprotMQfilters, + plotUpset = TRUE) +dim(sce) + +# ProteomeDiscoverer, Proteins +sce <- importExperiment( + inFile = system.file("extdata", "pdtmt_example", + "Fig2_m23139_RTS_QC_varMods_Proteins.txt", + package = "einprot"), + iColPattern = "^Abundance.F.+.Sample.")$sce + +dim(sce) +sce <- filterFeaturesSE(sce = sce, filtersSE = einprotPDTMTProteinFilters, + plotUpset = TRUE) +dim(sce) + +# ProteomeDiscoverer, PeptideGroups +sce <- importExperiment( + inFile = system.file("extdata", "pdtmt_example", + "Fig2_m23139_RTS_QC_varMods_PeptideGroups.txt", + package = "einprot"), + iColPattern = "^Abundance.F.+.Sample.")$sce + +dim(sce) +sce <- filterFeaturesSE(sce = sce, filtersSE = einprotPDTMTPeptideGroupFilters) +dim(sce) + +# FragPipe +sce <- importExperiment(inFile = system.file("extdata", "fp_example", + "combined_protein.tsv", + package = "einprot"), + iColPattern = ".MaxLFQ.Intensity$")$sce + +dim(sce) +sce <- filterFeaturesSE(sce = sce, filtersSE = einprotFragPipeFilters, + plotUpset = TRUE) +dim(sce) + +} +\author{ +Charlotte Soneson +} diff --git a/man/filterFragPipe.Rd b/man/filterFragPipe.Rd deleted file mode 100644 index bb90d9a..0000000 --- a/man/filterFragPipe.Rd +++ /dev/null @@ -1,57 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/doFilter.R -\name{filterFragPipe} -\alias{filterFragPipe} -\title{Filter out features in FragPipe data} -\usage{ -filterFragPipe( - sce, - minPeptides, - plotUpset = TRUE, - revPattern = "^rev_", - exclFile = NULL -) -} -\arguments{ -\item{sce}{A \code{SummarizedExperiment} object (or a derivative).} - -\item{minPeptides}{Numeric scalar, the minimum allowed value in the -'Combined.Total.Peptides' column in order to retain the feature.} - -\item{plotUpset}{Logical scalar, whether to generate an UpSet plot -detailing the reasons for features being filtered out. Only -generated if any feature is in fact filtered out.} - -\item{revPattern}{Character scalar providing the pattern (a regular -expression) used to identify decoys (reverse hits). The pattern is -matched against the IDs in the FragPipe \code{Protein} column.} - -\item{exclFile}{Character scalar, the path to a text file where the -features that are filtered out are written. If \code{NULL} (default), -excluded features are not recorded.} -} -\value{ -A filtered object of the same type as \code{sce}. -} -\description{ -Exclude features with 'Combined.Total.Peptides' below \code{minPeptides}, -or identified as either 'Reverse' (Protein name matching -\code{revPattern}) or 'Potential.contaminant' (Protein name starting -with \code{contam_}) by \code{FragPipe}. -} -\examples{ -sce <- importExperiment(inFile = system.file("extdata", "fp_example", - "combined_protein.tsv", - package = "einprot"), - iColPattern = ".MaxLFQ.Intensity$")$sce - -dim(sce) -sce <- filterFragPipe(sce = sce, minPeptides = 2, - plotUpset = TRUE, - revPattern = "^rev_") -dim(sce) - -} -\author{ -Charlotte Soneson -} diff --git a/man/filterMaxQuant.Rd b/man/filterMaxQuant.Rd deleted file mode 100644 index 6574a27..0000000 --- a/man/filterMaxQuant.Rd +++ /dev/null @@ -1,48 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/doFilter.R -\name{filterMaxQuant} -\alias{filterMaxQuant} -\title{Filter out features in MaxQuant data} -\usage{ -filterMaxQuant(sce, minScore, minPeptides, plotUpset = TRUE, exclFile = NULL) -} -\arguments{ -\item{sce}{A \code{SummarizedExperiment} object (or a derivative).} - -\item{minScore}{Numeric scalar, the minimum allowed value in the 'Score' -column in order to retain the feature.} - -\item{minPeptides}{Numeric scalar, the minimum allowed value in the -'Peptides' column in order to retain the feature.} - -\item{plotUpset}{Logical scalar, whether to generate an UpSet plot -detailing the reasons for features being filtered out. Only -generated if any feature is in fact filtered out.} - -\item{exclFile}{Character scalar, the path to a text file where the -features that are filtered out are written. If \code{NULL} (default), -excluded features are not recorded.} -} -\value{ -A filtered object of the same type as \code{sce}. -} -\description{ -Exclude features with 'Score' below \code{minScore}, 'Peptides' below -\code{minPeptides}, or identified as either 'Reverse', -'Potential.contaminant' or 'Only.identified.by.site' by \code{MaxQuant}. -} -\examples{ -sce <- importExperiment(inFile = system.file("extdata", "mq_example", - "1356_proteinGroups.txt", - package = "einprot"), - iColPattern = "^LFQ.intensity.")$sce - -dim(sce) -sce <- filterMaxQuant(sce = sce, minScore = 2, minPeptides = 2, - plotUpset = TRUE) -dim(sce) - -} -\author{ -Charlotte Soneson -} diff --git a/man/filterPDTMT.Rd b/man/filterPDTMT.Rd deleted file mode 100644 index e67f3d6..0000000 --- a/man/filterPDTMT.Rd +++ /dev/null @@ -1,114 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/doFilter.R -\name{filterPDTMT} -\alias{filterPDTMT} -\title{Filter out features in PD/TMT data} -\usage{ -filterPDTMT( - sce, - inputLevel, - minScore = 0, - minPeptides = 0, - minDeltaScore = 0, - minPSMs = 0, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, - plotUpset = TRUE, - exclFile = NULL -) -} -\arguments{ -\item{sce}{A \code{SummarizedExperiment} object (or a derivative).} - -\item{inputLevel}{Either \code{"Proteins"} or \code{"PeptideGroups"}, -indicating the type of features in \code{sce}.} - -\item{minScore}{Numeric scalar, the minimum allowed value in the -'Score.Sequest.HT.Sequest.HT' column in order to retain the feature. -Only used if \code{inputLevel} is \code{"Proteins"}.} - -\item{minPeptides}{Numeric scalar, the minimum allowed value in the -'Number.of.Peptides' column in order to retain the feature. -Only used if \code{inputLevel} is \code{"Proteins"}.} - -\item{minDeltaScore}{Numeric scalar, the minimum allowed value in the -'Delta.Score.by.Search.Engine.Sequest.HT' column in order to retain the -feature. Only used if \code{inputLevel} is \code{"PeptideGroups"}.} - -\item{minPSMs}{Numeric scalar, the minimum allowed value in the -'Number.of.PSMs' column in order to retain the feature. -Only used if \code{inputLevel} is \code{"PeptideGroups"}.} - -\item{masterProteinsOnly}{Logical scalar indicating whether only master -proteins (where the \code{Master} column value is -\code{IsMasterProtein}) should be retained.} - -\item{modificationsCol}{Character string pointing to a column containing -modification details. \code{excludeUnmodifiedPeptides} and -\code{keepModifications} will use information from this column. Only -used if \code{inputLevel} is \code{"PeptideGroups"}.} - -\item{excludeUnmodifiedPeptides}{Logical scalar, whether to filter out -peptides without modifications. Only used if \code{inputLevel} is -\code{"PeptideGroups"}.} - -\item{keepModifications}{Character string (or \code{NULL}) indicating -which modifications to retain in the analysis. Can be a regular -expression, which will be matched against the \code{modificationsCol}. -If \code{NULL} (the default), all rows are retained. Only used if -\code{inputLevel} is \code{"PeptideGroups"}.} - -\item{plotUpset}{Logical scalar, whether to generate an UpSet plot -detailing the reasons for features being filtered out. Only -generated if any feature is in fact filtered out.} - -\item{exclFile}{Character scalar, the path to a text file where the -features that are filtered out are written. If \code{NULL} (default), -excluded features are not recorded.} -} -\value{ -A filtered object of the same type as \code{sce}. -} -\description{ -If \code{inputLevel} is "Proteins", exclude features with -'Score.Sequest.HT.Sequest.HT' below \code{minScore}, -'Number.of.Peptides' below \code{minPeptides}, or identified as -'Contaminant' by \code{ProteomeDiscoverer}. -If \code{inputLevel} is "PeptideGroups", exclude features with -'Delta.Score.by.Search.Engine.Sequest.HT' below \code{minDeltaScore}, -'Number.of.PSMs' below \code{minPSMs}, or identified as -'Contaminant' by \code{ProteomeDiscoverer}. -} -\examples{ -## Proteins -sce <- importExperiment( - inFile = system.file("extdata", "pdtmt_example", - "Fig2_m23139_RTS_QC_varMods_Proteins.txt", - package = "einprot"), - iColPattern = "^Abundance.F.+.Sample.")$sce - -dim(sce) -sce <- filterPDTMT(sce = sce, inputLevel = "Proteins", minScore = 2, - minPeptides = 2, plotUpset = TRUE) -dim(sce) - -## PeptideGroups -sce <- importExperiment( - inFile = system.file("extdata", "pdtmt_example", - "Fig2_m23139_RTS_QC_varMods_PeptideGroups.txt", - package = "einprot"), - iColPattern = "^Abundance.F.+.Sample.")$sce - -dim(sce) -sce <- filterPDTMT(sce = sce, inputLevel = "PeptideGroups", - minPSMs = 2, plotUpset = TRUE, minDeltaScore = 0.2, - modificationsCol = "Modifications.in.Master.Proteins", - excludeUnmodifiedPeptides = TRUE) -dim(sce) - -} -\author{ -Charlotte Soneson -} diff --git a/man/filterSpectronaut.Rd b/man/filterSpectronaut.Rd deleted file mode 100644 index 9069330..0000000 --- a/man/filterSpectronaut.Rd +++ /dev/null @@ -1,56 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/doFilter.R -\name{filterSpectronaut} -\alias{filterSpectronaut} -\title{Filter out features in Spectronaut data} -\usage{ -filterSpectronaut( - sce, - minScore, - minPeptides, - plotUpset = TRUE, - revPattern = "_Decoy$", - contamPattern = "^contam_", - exclFile = NULL -) -} -\arguments{ -\item{sce}{A \code{SummarizedExperiment} object (or a derivative).} - -\item{minScore}{Numeric scalar, the minimum allowed value in the 'PG.Cscore' -column in order to retain the feature.} - -\item{minPeptides}{Numeric scalar, the minimum allowed value in the -'PG.NrOfStrippedSequencesIdentified.Experiment.wide' column in order to -retain the feature.} - -\item{plotUpset}{Logical scalar, whether to generate an UpSet plot -detailing the reasons for features being filtered out. Only -generated if any feature is in fact filtered out.} - -\item{revPattern}{Character scalar providing the pattern (a regular -expression) used to identify decoys (reverse hits). The pattern is -matched against the IDs in the Spectronaut \code{PG.ProteinGroups} -column.} - -\item{contamPattern}{Character scalar providing the pattern (a regular -expression) used to identify contaminants. The pattern is -matched against the IDs in the Spectronaut \code{PG.ProteinGroups} -column.} - -\item{exclFile}{Character scalar, the path to a text file where the -features that are filtered out are written. If \code{NULL} (default), -excluded features are not recorded.} -} -\value{ -A filtered object of the same type as \code{sce}. -} -\description{ -Exclude features with 'PG.Cscore' below \code{minScore}, -'PG.NrOfStrippedSequencesIdentified.Experiment.wide' below -\code{minPeptides}, or where the 'PG.ProteinGroups' column contains the -specified \code{revPattern}. -} -\author{ -Charlotte Soneson -} From ba6d957fbd3d868a55881f476740f7f901a6a86e Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 14:53:14 +0100 Subject: [PATCH 03/65] Adapt unit tests to use new filterFeaturesSE function --- tests/testthat/test-doFilter.R | 796 +++++++++++++-------------------- 1 file changed, 305 insertions(+), 491 deletions(-) diff --git a/tests/testthat/test-doFilter.R b/tests/testthat/test-doFilter.R index dddb6f0..83e18fe 100644 --- a/tests/testthat/test-doFilter.R +++ b/tests/testthat/test-doFilter.R @@ -1,45 +1,66 @@ test_that("filtering works (MaxQuant)", { ## Fails with wrong argument specification - expect_error(filterMaxQuant(sce = 1, minScore = 10, minPeptides = 2, - plotUpset = TRUE, exclFile = NULL), - "'sce' must be of class 'SummarizedExperiment'") - expect_error(filterMaxQuant(sce = sce_mq_final, minScore = "10", - minPeptides = 2, plotUpset = TRUE, - exclFile = NULL), - "'minScore' must be of class 'numeric'") - expect_error(filterMaxQuant(sce = sce_mq_final, minScore = c(1, 2), - minPeptides = 2, plotUpset = TRUE, - exclFile = NULL), - "'minScore' must have length 1") - expect_error(filterMaxQuant(sce = sce_mq_final, minScore = 10, - minPeptides = "2", plotUpset = TRUE, - exclFile = NULL), - "'minPeptides' must be of class 'numeric'") - expect_error(filterMaxQuant(sce = sce_mq_final, minScore = 10, - minPeptides = c(1, 2), plotUpset = TRUE, - exclFile = NULL), - "'minPeptides' must have length 1") - expect_error(filterMaxQuant(sce = sce_mq_final, minScore = 10, - minPeptides = 2, plotUpset = 1, - exclFile = NULL), - "'plotUpset' must be of class 'logical'") - expect_error(filterMaxQuant(sce = sce_mq_final, minScore = 10, - minPeptides = 2, plotUpset = c(TRUE, FALSE), - exclFile = NULL), - "'plotUpset' must have length 1") - expect_error(filterMaxQuant(sce = sce_mq_final, minScore = 10, - minPeptides = 2, plotUpset = TRUE, - exclFile = 1), - "'exclFile' must be of class 'character'") - expect_error(filterMaxQuant(sce = sce_mq_final, minScore = 10, - minPeptides = 2, plotUpset = TRUE, - exclFile = c(tempfile(), tempfile())), - "'exclFile' must have length 1") + expect_error(filterFeaturesSE( + sce = 1, + filtersSE = list(Score = function(se) rowData(se)$Score >= 10, + Peptides = function(se) rowData(se)$Peptides >= 2), + plotUpset = TRUE, exclFile = NULL), + "'sce' must be of class 'SummarizedExperiment'") + expect_error(filterFeaturesSE( + sce = sce_mq_final, + filtersSE = 1, + plotUpset = TRUE, exclFile = NULL), + "'filtersSE' must be of class 'list'") + expect_error(filterFeaturesSE( + sce = sce_mq_final, + filtersSE = list(function(se) rowData(se)$Score >= 10, + function(se) rowData(se)$Peptides >= 2), + plotUpset = TRUE, exclFile = NULL), + "'namesfiltersSE' must not be NULL") + expect_error(filterFeaturesSE( + sce = sce_mq_final, + filtersSE = list(Score = function(se, minScore) rowData(se)$Score >= minScore, + Peptides = function(se) rowData(se)$Peptides >= 2), + plotUpset = TRUE, exclFile = NULL), + "is not TRUE") + expect_error(filterFeaturesSE( + sce = sce_mq_final, + filtersSE = list(Score = function(se) rowData(se)$Score >= 10, + Peptides = function(se) rowData(se)$Peptides >= 2), + plotUpset = 1, exclFile = NULL), + "'plotUpset' must be of class 'logical'") + expect_error(filterFeaturesSE( + sce = sce_mq_final, + filtersSE = list(Score = function(se) rowData(se)$Score >= 10, + Peptides = function(se) rowData(se)$Peptides >= 2), + plotUpset = c(TRUE, FALSE), exclFile = NULL), + "'plotUpset' must have length 1") + expect_error(filterFeaturesSE( + sce = sce_mq_final, + filtersSE = list(Score = function(se) rowData(se)$Score >= 10, + Peptides = function(se) rowData(se)$Peptides >= 2), + plotUpset = TRUE, exclFile = 1), + "'exclFile' must be of class 'character'") + expect_error(filterFeaturesSE( + sce = sce_mq_final, + filtersSE = list(Score = function(se) rowData(se)$Score >= 10, + Peptides = function(se) rowData(se)$Peptides >= 2), + plotUpset = TRUE, exclFile = c(tempfile(), tempfile())), + "'exclFile' must have length 1") ## Works with correct argument specification tfl <- tempfile(fileext = ".txt") - out <- filterMaxQuant(sce_mq_final, minScore = 10, minPeptides = 3, - plotUpset = FALSE, exclFile = tfl) + out <- filterFeaturesSE( + sce_mq_final, + filtersSE = list(Score = function(se) rowData(se)$Score >= 10, + Peptides = function(se) rowData(se)$Peptides >= 3, + Reverse = function(se) as.character(rowData(se)$Reverse) == "" | + is.na(rowData(se)$Reverse), + Contaminant = function(se) as.character(rowData(se)$Potential.contaminant) == "" | + is.na(rowData(se)$Potential.contaminant), + OnlySite = function(se) as.character(rowData(se)$Only.identified.by.site) == "" | + is.na(rowData(se)$Only.identified.by.site)), + plotUpset = FALSE, exclFile = tfl) expect_equal(nrow(out), length(which( rowData(sce_mq_final)$Score >= 10 & rowData(sce_mq_final)$Peptides >= 3 & @@ -55,8 +76,17 @@ test_that("filtering works (MaxQuant)", { tmpin <- read.delim(tfl) expect_equal(nrow(tmpin), 97L) - out <- filterMaxQuant(sce_mq_final, minScore = 2, minPeptides = 1, - plotUpset = TRUE, exclFile = NULL) + out <- filterFeaturesSE( + sce_mq_final, + filtersSE = list(Score = function(se) rowData(se)$Score >= 2, + Peptides = function(se) rowData(se)$Peptides >= 1, + Reverse = function(se) as.character(rowData(se)$Reverse) == "" | + is.na(rowData(se)$Reverse), + Contaminant = function(se) as.character(rowData(se)$Potential.contaminant) == "" | + is.na(rowData(se)$Potential.contaminant), + OnlySite = function(se) as.character(rowData(se)$Only.identified.by.site) == "" | + is.na(rowData(se)$Only.identified.by.site)), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_mq_final)$Score >= 2 & rowData(sce_mq_final)$Peptides >= 1 & @@ -70,8 +100,16 @@ test_that("filtering works (MaxQuant)", { expect_equal(nrow(out), 97L) ## Don't filter on score - out <- filterMaxQuant(sce_mq_final, minScore = NULL, minPeptides = 1, - plotUpset = TRUE, exclFile = NULL) + out <- filterFeaturesSE( + sce_mq_final, + filtersSE = list(Peptides = function(se) rowData(se)$Peptides >= 1, + Reverse = function(se) as.character(rowData(se)$Reverse) == "" | + is.na(rowData(se)$Reverse), + Contaminant = function(se) as.character(rowData(se)$Potential.contaminant) == "" | + is.na(rowData(se)$Potential.contaminant), + OnlySite = function(se) as.character(rowData(se)$Only.identified.by.site) == "" | + is.na(rowData(se)$Only.identified.by.site)), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_mq_final)$Peptides >= 1 & (rowData(sce_mq_final)$Reverse == "" | @@ -84,8 +122,16 @@ test_that("filtering works (MaxQuant)", { expect_equal(nrow(out), 97L) ## Don't filter on minPeptides - out <- filterMaxQuant(sce_mq_final, minScore = 5, minPeptides = NULL, - plotUpset = TRUE, exclFile = NULL) + out <- filterFeaturesSE( + sce_mq_final, + filtersSE = list(Score = function(se) rowData(se)$Score >= 5, + Reverse = function(se) as.character(rowData(se)$Reverse) == "" | + is.na(rowData(se)$Reverse), + Contaminant = function(se) as.character(rowData(se)$Potential.contaminant) == "" | + is.na(rowData(se)$Potential.contaminant), + OnlySite = function(se) as.character(rowData(se)$Only.identified.by.site) == "" | + is.na(rowData(se)$Only.identified.by.site)), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_mq_final)$Score >= 5 & (rowData(sce_mq_final)$Reverse == "" | @@ -100,8 +146,17 @@ test_that("filtering works (MaxQuant)", { ## Missing columns - Score tmp <- sce_mq_final rowData(tmp)$Score <- NULL - out <- filterMaxQuant(tmp, minScore = 7, minPeptides = 1, - plotUpset = TRUE, exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list(Score = function(se) if ("Score" %in% colnames(rowData(se))) rowData(se)$Score >= 2 else rep(TRUE, nrow(se)), + Peptides = function(se) rowData(se)$Peptides >= 1, + Reverse = function(se) as.character(rowData(se)$Reverse) == "" | + is.na(rowData(se)$Reverse), + Contaminant = function(se) as.character(rowData(se)$Potential.contaminant) == "" | + is.na(rowData(se)$Potential.contaminant), + OnlySite = function(se) as.character(rowData(se)$Only.identified.by.site) == "" | + is.na(rowData(se)$Only.identified.by.site)), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_mq_final)$Peptides >= 1 & (rowData(sce_mq_final)$Reverse == "" | @@ -117,8 +172,17 @@ test_that("filtering works (MaxQuant)", { tmp <- sce_mq_final rowData(tmp)$Score <- NULL rowData(tmp)$Only.identified.by.site <- NULL - out <- filterMaxQuant(tmp, minScore = 7, minPeptides = 1, - plotUpset = TRUE, exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list(Score = function(se) if ("Score" %in% colnames(rowData(se))) rowData(se)$Score >= 7 else rep(TRUE, nrow(se)), + Peptides = function(se) rowData(se)$Peptides >= 1, + Reverse = function(se) as.character(rowData(se)$Reverse) == "" | + is.na(rowData(se)$Reverse), + Contaminant = function(se) as.character(rowData(se)$Potential.contaminant) == "" | + is.na(rowData(se)$Potential.contaminant), + OnlySite = function(se) if ("Only.identified.by.site" %in% colnames(rowData(se))) (as.character(rowData(se)$Only.identified.by.site) == "" | + is.na(rowData(se)$Only.identified.by.site)) else rep(TRUE, nrow(se))), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_mq_final)$Peptides >= 1 & (rowData(sce_mq_final)$Reverse == "" | @@ -128,25 +192,20 @@ test_that("filtering works (MaxQuant)", { ))) expect_equal(nrow(out), 112L) - ## Only one column present - tmp <- sce_mq_final - rowData(tmp)$Score <- NULL - rowData(tmp)$Only.identified.by.site <- NULL - rowData(tmp)$Peptides <- NULL - rowData(tmp)$Reverse <- NULL - out <- filterMaxQuant(tmp, minScore = 7, minPeptides = 1, - plotUpset = TRUE, exclFile = NULL) - expect_equal(nrow(out), length(which( - (rowData(sce_mq_final)$Potential.contaminant == "" | - is.na(rowData(sce_mq_final)$Potential.contaminant)) - ))) - expect_equal(nrow(out), 112L) - ## Missing columns - Potential.contaminant tmp <- sce_mq_final rowData(tmp)$Potential.contaminant <- NULL - out <- filterMaxQuant(tmp, minScore = 2, minPeptides = 1, - plotUpset = TRUE, exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list(Score = function(se) if ("Score" %in% colnames(rowData(se))) rowData(se)$Score >= 2 else rep(TRUE, nrow(se)), + Peptides = function(se) rowData(se)$Peptides >= 1, + Reverse = function(se) as.character(rowData(se)$Reverse) == "" | + is.na(rowData(se)$Reverse), + Contaminant = function(se) if ("Potential.contaminant" %in% colnames(rowData(se))) (as.character(rowData(se)$Potential.contaminant) == "" | + is.na(rowData(se)$Potential.contaminant)) else rep(TRUE, nrow(se)), + OnlySite = function(se) if ("Only.identified.by.site" %in% colnames(rowData(se))) (as.character(rowData(se)$Only.identified.by.site) == "" | + is.na(rowData(se)$Only.identified.by.site)) else rep(TRUE, nrow(se))), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_mq_final)$Score >= 2 & rowData(sce_mq_final)$Peptides >= 1 & @@ -159,95 +218,14 @@ test_that("filtering works (MaxQuant)", { }) test_that("filtering works (PD/TMT - proteins)", { - ## Fails with wrong argument specification - expect_error(filterPDTMT(sce = 1, inputLevel = "Proteins", minScore = 10, - minPeptides = 2, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL), - "'sce' must be of class 'SummarizedExperiment'") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = 1, - minScore = 10, minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL), - "'inputLevel' must be of class 'character'") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = c("Proteins", "PeptideGroups"), - minScore = 10, minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL), - "'inputLevel' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Nonsense", - minScore = 10, minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL), - "All values in 'inputLevel' must be one of") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = "10", minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL), - "'minScore' must be of class 'numeric'") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = c(1, 2), minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL), - "'minScore' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = 10, minPeptides = "2", - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL), - "'minPeptides' must be of class 'numeric'") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = 10, minPeptides = c(1, 2), - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL), - "'minPeptides' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = 10, minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = "TRUE", plotUpset = TRUE, - exclFile = NULL), - "'masterProteinsOnly' must be of class 'logical'") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = 10, minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = c(TRUE, FALSE), plotUpset = TRUE, - exclFile = NULL), - "'masterProteinsOnly' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = 10, minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = 1), - "'plotUpset' must be of class 'logical'") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = 10, minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = c(TRUE, FALSE), - exclFile = NULL), - "'plotUpset' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = 10, minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = 1), - "'exclFile' must be of class 'character'") - expect_error(filterPDTMT(sce = sce_pd_final, inputLevel = "Proteins", - minScore = 10, minPeptides = 2, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = c(tempfile(), tempfile())), - "'exclFile' must have length 1") - - ## Works with correct argument specification - out <- filterPDTMT(sce_pd_final, inputLevel = "Proteins", minScore = 10, - minPeptides = 5, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + sce_pd_final, + filtersSE = list( + Score = function(sce) if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Score.Sequest.HT.Sequest.HT >= 10 & !is.na(rowData(sce)$Score.Sequest.HT.Sequest.HT) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Number.of.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Number.of.Peptides >= 5 & !is.na(rowData(sce)$Number.of.Peptides) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_final)$Score.Sequest.HT.Sequest.HT >= 10 & rowData(sce_pd_final)$Number.of.Peptides >= 5 & @@ -256,10 +234,13 @@ test_that("filtering works (PD/TMT - proteins)", { expect_equal(nrow(out), 23L) ## same test as above, just with precomputed answer ## Don't filter on minPeptides - out <- filterPDTMT(sce_pd_final, inputLevel = "Proteins", minScore = 10, - minPeptides = NULL, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + sce_pd_final, + filtersSE = list( + Score = function(sce) if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Score.Sequest.HT.Sequest.HT >= 10 & !is.na(rowData(sce)$Score.Sequest.HT.Sequest.HT) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_final)$Score.Sequest.HT.Sequest.HT >= 10 & rowData(sce_pd_final)$Contaminant == "False" @@ -267,10 +248,13 @@ test_that("filtering works (PD/TMT - proteins)", { expect_equal(nrow(out), 30L) ## same test as above, just with precomputed answer ## Don't filter on minScore - out <- filterPDTMT(sce_pd_final, inputLevel = "Proteins", minScore = NULL, - minPeptides = 5, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + sce_pd_final, + filtersSE = list( + Peptides = function(sce) if ("Number.of.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Number.of.Peptides >= 5 & !is.na(rowData(sce)$Number.of.Peptides) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_final)$Number.of.Peptides >= 5 & rowData(sce_pd_final)$Contaminant == "False" @@ -278,10 +262,14 @@ test_that("filtering works (PD/TMT - proteins)", { expect_equal(nrow(out), 23L) ## same test as above, just with precomputed answer ## Don't filter on minScore, filter on master proteins - out <- filterPDTMT(sce_pd_final, inputLevel = "Proteins", minScore = NULL, - minPeptides = 5, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = TRUE, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + sce_pd_final, + filtersSE = list( + Peptides = function(sce) if ("Number.of.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Number.of.Peptides >= 5 & !is.na(rowData(sce)$Number.of.Peptides) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))}, + Master = function(sce) if ("Master" %in% colnames(rowData(sce))) rowData(sce)$Master == "IsMasterProtein" & !is.na(rowData(sce)$Master) else rep(TRUE, nrow(sce)) + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_final)$Number.of.Peptides >= 5 & rowData(sce_pd_final)$Contaminant == "False" & @@ -290,10 +278,15 @@ test_that("filtering works (PD/TMT - proteins)", { expect_equal(nrow(out), 20L) ## same test as above, just with precomputed answer ## Only master proteins - out <- filterPDTMT(sce_pd_final, inputLevel = "Proteins", minScore = 10, - minPeptides = 3, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = TRUE, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + sce_pd_final, + filtersSE = list( + Score = function(sce) if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Score.Sequest.HT.Sequest.HT >= 10 & !is.na(rowData(sce)$Score.Sequest.HT.Sequest.HT) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Number.of.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Number.of.Peptides >= 3 & !is.na(rowData(sce)$Number.of.Peptides) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))}, + Master = function(sce) if ("Master" %in% colnames(rowData(sce))) rowData(sce)$Master == "IsMasterProtein" & !is.na(rowData(sce)$Master) else rep(TRUE, nrow(sce)) + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_final)$Score.Sequest.HT.Sequest.HT >= 10 & rowData(sce_pd_final)$Number.of.Peptides >= 3 & @@ -306,10 +299,14 @@ test_that("filtering works (PD/TMT - proteins)", { tfl <- tempfile(fileext = ".txt") tmp <- sce_pd_final rowData(tmp)$Score.Sequest.HT.Sequest.HT <- NULL - out <- filterPDTMT(tmp, inputLevel = "Proteins", minScore = 10, - minPeptides = 3, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = FALSE, - exclFile = tfl) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + Score = function(sce) if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Score.Sequest.HT.Sequest.HT >= 10 & !is.na(rowData(sce)$Score.Sequest.HT.Sequest.HT) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Number.of.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Number.of.Peptides >= 3 & !is.na(rowData(sce)$Number.of.Peptides) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = tfl) expect_equal(nrow(out), length(which( rowData(sce_pd_final)$Number.of.Peptides >= 3 & rowData(sce_pd_final)$Contaminant == "False" @@ -322,10 +319,14 @@ test_that("filtering works (PD/TMT - proteins)", { ## Missing columns - Number.of.Peptides tmp <- sce_pd_final rowData(tmp)$Number.of.Peptides <- NULL - out <- filterPDTMT(tmp, inputLevel = "Proteins", minScore = 10, - minPeptides = 3, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + Score = function(sce) if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Score.Sequest.HT.Sequest.HT >= 10 & !is.na(rowData(sce)$Score.Sequest.HT.Sequest.HT) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Number.of.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Number.of.Peptides >= 3 & !is.na(rowData(sce)$Number.of.Peptides) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_final)$Score.Sequest.HT.Sequest.HT >= 10 & rowData(sce_pd_final)$Contaminant == "False" @@ -336,10 +337,14 @@ test_that("filtering works (PD/TMT - proteins)", { tmp <- sce_pd_final rowData(tmp)$Number.of.Peptides <- NULL rowData(tmp)$Contaminant <- NULL - out <- filterPDTMT(tmp, inputLevel = "Proteins", minScore = 10, - minPeptides = 3, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + Score = function(sce) if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Score.Sequest.HT.Sequest.HT >= 10 & !is.na(rowData(sce)$Score.Sequest.HT.Sequest.HT) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Number.of.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Number.of.Peptides >= 5 & !is.na(rowData(sce)$Number.of.Peptides) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_final)$Score.Sequest.HT.Sequest.HT >= 10 ))) @@ -349,20 +354,29 @@ test_that("filtering works (PD/TMT - proteins)", { tmp <- sce_pd_final rowData(tmp)$Number.of.Peptides <- NULL rowData(tmp)$Score.Sequest.HT.Sequest.HT <- NULL - out <- filterPDTMT(tmp, inputLevel = "Proteins", minScore = 10, - minPeptides = 3, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, plotUpset = TRUE, - exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + Score = function(sce) if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Score.Sequest.HT.Sequest.HT >= 10 & !is.na(rowData(sce)$Score.Sequest.HT.Sequest.HT) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Number.of.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Number.of.Peptides >= 5 & !is.na(rowData(sce)$Number.of.Peptides) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), nrow(tmp)) expect_equal(nrow(out), 70L) ## same test as above, just with precomputed answer ## Missing columns - Master tmp <- sce_pd_final rowData(tmp)$Master <- NULL - out <- filterPDTMT(tmp, inputLevel = "Proteins", minScore = 10, - minPeptides = 3, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = TRUE, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + Score = function(sce) if ("Score.Sequest.HT.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Score.Sequest.HT.Sequest.HT >= 10 & !is.na(rowData(sce)$Score.Sequest.HT.Sequest.HT) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Number.of.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Number.of.Peptides >= 3 & !is.na(rowData(sce)$Number.of.Peptides) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))}, + Master = function(sce) if ("Master" %in% colnames(rowData(sce))) rowData(sce)$Master == "IsMasterProtein" & !is.na(rowData(sce)$Master) else rep(TRUE, nrow(sce)) + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_final)$Score.Sequest.HT.Sequest.HT >= 10 & rowData(sce_pd_final)$Number.of.Peptides >= 3 & @@ -372,185 +386,14 @@ test_that("filtering works (PD/TMT - proteins)", { }) test_that("filtering works (PD/TMT - peptidegroups)", { - ## Fails with wrong argument specification - expect_error(filterPDTMT(sce = 1, inputLevel = "PeptideGroups", minScore = 10, - minPeptides = 2, minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'sce' must be of class 'SummarizedExperiment'") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = 1, - minScore = 0, minPeptides = 1, - minDeltaScore = 10, minPSMs = 2, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'inputLevel' must be of class 'character'") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = c("Proteins", "PeptideGroups"), - minScore = 0, minPeptides = 1, - minDeltaScore = 10, minPSMs = 2, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'inputLevel' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "Nonsense", - minScore = 0, minPeptides = 1, - minDeltaScore = 10, minPSMs = 2, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "All values in 'inputLevel' must be one of") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = "10", minPSMs = 2, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'minDeltaScore' must be of class 'numeric'") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = c(1, 2), minPSMs = 1, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'minDeltaScore' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 0, minPSMs = "2", - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'minPSMs' must be of class 'numeric'") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 0, minPSMs = c(1, 2), - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'minPSMs' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, - modificationsCol = 1, - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'modificationsCol' must be of class 'character'") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, - modificationsCol = c("Modifications", "Modifications"), - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'modificationsCol' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, - modificationsCol = "Nonsense", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "All values in 'modificationsCol' must be one of") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = 1, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'excludeUnmodifiedPeptides' must be of class 'logical'") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = c(TRUE, FALSE), - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "'excludeUnmodifiedPeptides' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = 1, plotUpset = TRUE, - exclFile = NULL), - "'keepModifications' must be of class 'character'") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 0, minPSMs = 1, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = c("mod1", "mod2"), plotUpset = TRUE, - exclFile = NULL), - "'keepModifications' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 10, minPSMs = 2, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = 1, - exclFile = NULL), - "'plotUpset' must be of class 'logical'") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 10, minPSMs = 2, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = c(TRUE, FALSE), - exclFile = NULL), - "'plotUpset' must have length 1") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 10, minPSMs = 2, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = 1), - "'exclFile' must be of class 'character'") - expect_error(filterPDTMT(sce = sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 1, - minDeltaScore = 10, minPSMs = 2, - masterProteinsOnly = FALSE, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = c(tempfile(), tempfile())), - "'exclFile' must have length 1") - - ## Works with correct argument specification - out <- filterPDTMT(sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.1, minPSMs = 2, - masterProteinsOnly = FALSE, modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + sce_pd_peptide_initial, + filtersSE = list( + DeltaScore = function(sce) if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & !is.na(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT) else rep(TRUE, nrow(sce)), + PSMs = function(sce) if ("Number.of.PSMs" %in% colnames(rowData(sce))) rowData(sce)$Number.of.PSMs >= 2 & !is.na(rowData(sce)$Number.of.PSMs) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_peptide_initial)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & rowData(sce_pd_peptide_initial)$Number.of.PSMs >= 2 & @@ -559,26 +402,14 @@ test_that("filtering works (PD/TMT - peptidegroups)", { expect_equal(nrow(out), 4L) ## same test as above, just with precomputed answer ## Works with correct argument specification - out <- filterPDTMT(sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.5, minPSMs = 1, - masterProteinsOnly = FALSE, modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL) - expect_equal(nrow(out), length(which( - rowData(sce_pd_peptide_initial)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.5 & - rowData(sce_pd_peptide_initial)$Number.of.PSMs >= 1 & - rowData(sce_pd_peptide_initial)$Contaminant == "False" - ))) - expect_equal(nrow(out), 20L) ## same test as above, just with precomputed answer - - ## Works with correct argument specification - with modificationsCol = NULL - out <- filterPDTMT(sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.5, minPSMs = 1, - masterProteinsOnly = FALSE, modificationsCol = NULL, - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL) + out <- filterFeaturesSE( + sce_pd_peptide_initial, + filtersSE = list( + DeltaScore = function(sce) if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.5 & !is.na(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT) else rep(TRUE, nrow(sce)), + PSMs = function(sce) if ("Number.of.PSMs" %in% colnames(rowData(sce))) rowData(sce)$Number.of.PSMs >= 1 & !is.na(rowData(sce)$Number.of.PSMs) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_peptide_initial)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.5 & rowData(sce_pd_peptide_initial)$Number.of.PSMs >= 1 & @@ -587,12 +418,13 @@ test_that("filtering works (PD/TMT - peptidegroups)", { expect_equal(nrow(out), 20L) ## same test as above, just with precomputed answer ## Don't filter on Number.of.PSMs - out <- filterPDTMT(sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.1, minPSMs = NULL, - masterProteinsOnly = FALSE, modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + sce_pd_peptide_initial, + filtersSE = list( + DeltaScore = function(sce) if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & !is.na(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_peptide_initial)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & rowData(sce_pd_peptide_initial)$Contaminant == "False" @@ -600,12 +432,13 @@ test_that("filtering works (PD/TMT - peptidegroups)", { expect_equal(nrow(out), 62L) ## same test as above, just with precomputed answer ## Don't filter on delta score - out <- filterPDTMT(sce_pd_peptide_initial, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = NULL, minPSMs = 2, - masterProteinsOnly = FALSE, modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + sce_pd_peptide_initial, + filtersSE = list( + PSMs = function(sce) if ("Number.of.PSMs" %in% colnames(rowData(sce))) rowData(sce)$Number.of.PSMs >= 2 & !is.na(rowData(sce)$Number.of.PSMs) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_peptide_initial)$Number.of.PSMs >= 2 & rowData(sce_pd_peptide_initial)$Contaminant == "False" @@ -616,12 +449,14 @@ test_that("filtering works (PD/TMT - peptidegroups)", { tfl <- tempfile(fileext = ".txt") tmp <- sce_pd_peptide_initial rowData(tmp)$Number.of.PSMs <- NULL - out <- filterPDTMT(tmp, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.1, minPSMs = 2, - masterProteinsOnly = FALSE, modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = FALSE, - exclFile = tfl) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + DeltaScore = function(sce) if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & !is.na(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT) else rep(TRUE, nrow(sce)), + PSMs = function(sce) if ("Number.of.PSMs" %in% colnames(rowData(sce))) rowData(sce)$Number.of.PSMs >= 2 & !is.na(rowData(sce)$Number.of.PSMs) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = tfl) expect_equal(nrow(out), length(which( rowData(sce_pd_peptide_initial)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & rowData(sce_pd_peptide_initial)$Contaminant == "False" @@ -634,12 +469,14 @@ test_that("filtering works (PD/TMT - peptidegroups)", { ## Missing column - Contaminant tmp <- sce_pd_peptide_initial rowData(tmp)$Contaminant <- NULL - out <- filterPDTMT(tmp, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.1, minPSMs = 2, - masterProteinsOnly = FALSE, modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = FALSE, - exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + DeltaScore = function(sce) if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & !is.na(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT) else rep(TRUE, nrow(sce)), + PSMs = function(sce) if ("Number.of.PSMs" %in% colnames(rowData(sce))) rowData(sce)$Number.of.PSMs >= 2 & !is.na(rowData(sce)$Number.of.PSMs) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))} + ), + plotUpset = FALSE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_pd_peptide_initial)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & rowData(sce_pd_peptide_initial)$Number.of.PSMs >= 2 @@ -649,12 +486,14 @@ test_that("filtering works (PD/TMT - peptidegroups)", { ## Exclude unmodified peptides tmp <- sce_pd_peptide_initial rowData(tmp)$Modifications[c(1, 5, 10, 20)] <- "" - out <- filterPDTMT(tmp, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.1, minPSMs = NULL, - masterProteinsOnly = FALSE, modificationsCol = "Modifications", - excludeUnmodifiedPeptides = TRUE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + DeltaScore = function(sce) if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & !is.na(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))}, + ExclUnmodified = function(sce) if ("Modifications" %in% colnames(rowData(sce))) rowData(sce)$Modifications != "" & !is.na(rowData(sce)$Modifications) else rep(TRUE, nrow(sce)) + ), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(tmp)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & rowData(tmp)$Contaminant == "False" & @@ -665,12 +504,15 @@ test_that("filtering works (PD/TMT - peptidegroups)", { ## Exclude unmodified peptides, keep only Carbamidomethyl modifications tmp <- sce_pd_peptide_initial rowData(tmp)$Modifications[c(1, 5, 10, 20)] <- "" - out <- filterPDTMT(tmp, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.1, minPSMs = NULL, - masterProteinsOnly = FALSE, modificationsCol = "Modifications", - excludeUnmodifiedPeptides = TRUE, - keepModifications = "Carbamidomethyl", plotUpset = TRUE, - exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + DeltaScore = function(sce) if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & !is.na(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))}, + ExclUnmodified = function(sce) if ("Modifications" %in% colnames(rowData(sce))) rowData(sce)$Modifications != "" & !is.na(rowData(sce)$Modifications) else rep(TRUE, nrow(sce)), + KeepMods = function(sce) if ("Modifications" %in% colnames(rowData(sce))) grepl("Carbamidomethyl", rowData(sce)$Modifications) & !is.na(rowData(sce)$Modifications) else rep(TRUE, nrow(sce)) + ), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(tmp)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & rowData(tmp)$Contaminant == "False" & @@ -682,12 +524,15 @@ test_that("filtering works (PD/TMT - peptidegroups)", { ## Exclude unmodified peptides, keep only Carbamidomethyl or 1xTMTpro [K12] modifications tmp <- sce_pd_peptide_initial rowData(tmp)$Modifications[c(1, 5, 10, 20)] <- "" - out <- filterPDTMT(tmp, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.1, minPSMs = NULL, - masterProteinsOnly = FALSE, modificationsCol = "Modifications", - excludeUnmodifiedPeptides = TRUE, - keepModifications = "Carbamidomethyl|1xTMTpro \\[K12\\]", plotUpset = TRUE, - exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + DeltaScore = function(sce) if ("Delta.Score.by.Search.Engine.Sequest.HT" %in% colnames(rowData(sce))) rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & !is.na(rowData(sce)$Delta.Score.by.Search.Engine.Sequest.HT) else rep(TRUE, nrow(sce)), + Contaminant = function(sce) { if ("Contaminant" %in% colnames(rowData(sce))) { rowData(sce)$Contaminant <- as.character(rowData(sce)$Contaminant);rowData(sce)$Contaminant == "False" & !is.na(rowData(sce)$Contaminant) } else rep(TRUE, nrow(sce))}, + ExclUnmodified = function(sce) if ("Modifications" %in% colnames(rowData(sce))) rowData(sce)$Modifications != "" & !is.na(rowData(sce)$Modifications) else rep(TRUE, nrow(sce)), + KeepMods = function(sce) if ("Modifications" %in% colnames(rowData(sce))) grepl("Carbamidomethyl|1xTMTpro \\[K12\\]", rowData(sce)$Modifications) & !is.na(rowData(sce)$Modifications) else rep(TRUE, nrow(sce)) + ), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(tmp)$Delta.Score.by.Search.Engine.Sequest.HT >= 0.1 & rowData(tmp)$Contaminant == "False" & @@ -696,64 +541,19 @@ test_that("filtering works (PD/TMT - peptidegroups)", { grepl("1xTMTpro \\[K12\\]", rowData(tmp)$Modifications)) ))) expect_equal(nrow(out), 13L) ## same test as above, just with precomputed answer - - ## Fails if the Contaminant column has the wrong type of values (not True/False) - tmp <- sce_pd_peptide_initial - rowData(tmp)$Contaminant[rowData(tmp)$Contaminant == "True"] <- "Trrue" - expect_error(filterPDTMT(tmp, inputLevel = "PeptideGroups", - minScore = 0, minPeptides = 0, minDeltaScore = 0.5, minPSMs = 1, - modificationsCol = "Modifications", - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = NULL), - "Something went wrong in the filtering") }) test_that("filtering works (FragPipe)", { - ## Fails with wrong argument specification - expect_error(filterFragPipe(sce = 1, minPeptides = 2, - plotUpset = TRUE, - revPattern = "^rev_", exclFile = NULL), - "'sce' must be of class 'SummarizedExperiment'") - expect_error(filterFragPipe(sce = sce_fp_final, - minPeptides = "2", plotUpset = TRUE, - revPattern = "^rev_", exclFile = NULL), - "'minPeptides' must be of class 'numeric'") - expect_error(filterFragPipe(sce = sce_fp_final, - minPeptides = c(1, 2), plotUpset = TRUE, - revPattern = "^rev_", exclFile = NULL), - "'minPeptides' must have length 1") - expect_error(filterFragPipe(sce = sce_fp_final, - minPeptides = 2, plotUpset = 1, - revPattern = "^rev_", exclFile = NULL), - "'plotUpset' must be of class 'logical'") - expect_error(filterFragPipe(sce = sce_fp_final, - minPeptides = 2, plotUpset = c(TRUE, FALSE), - revPattern = "^rev_", exclFile = NULL), - "'plotUpset' must have length 1") - expect_error(filterFragPipe(sce = sce_fp_final, - minPeptides = 2, plotUpset = TRUE, - revPattern = 1, exclFile = NULL), - "'revPattern' must be of class 'character'") - expect_error(filterFragPipe(sce = sce_fp_final, - minPeptides = 2, plotUpset = TRUE, - revPattern = c("p1", "p2"), exclFile = NULL), - "'revPattern' must have length 1") - expect_error(filterFragPipe(sce = sce_fp_final, - minPeptides = 2, plotUpset = TRUE, - revPattern = "^rev_", exclFile = 1), - "'exclFile' must be of class 'character'") - expect_error(filterFragPipe(sce = sce_fp_final, - minPeptides = 2, plotUpset = TRUE, - revPattern = "^rev_", - exclFile = c(tempfile(), tempfile())), - "'exclFile' must have length 1") - ## Works with correct argument specification tfl <- tempfile(fileext = ".txt") - out <- filterFragPipe(sce_fp_final, minPeptides = 3, - plotUpset = FALSE, revPattern = "^rev_", - exclFile = tfl) + out <- filterFeaturesSE( + sce_fp_final, + filtersSE = list( + Contaminant = function(sce) if ("Protein" %in% colnames(rowData(sce))) !grepl("^contam_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) else rep(TRUE, nrow(sce)), + Reverse = function(sce) if ("Protein" %in% colnames(rowData(sce))) !grepl("^rev_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Combined.Total.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Combined.Total.Peptides >= 3 & !is.na(rowData(sce)$Combined.Total.Peptides) else rep(TRUE, nrow(sce)) + ), + plotUpset = FALSE, exclFile = tfl) expect_equal(nrow(out), length(which( rowData(sce_fp_final)$Combined.Total.Peptides >= 3 & !grepl("^rev_", rownames(sce_fp_final)) & @@ -764,9 +564,14 @@ test_that("filtering works (FragPipe)", { tmpin <- read.delim(tfl) expect_equal(nrow(tmpin), 150L - 87L) - out <- filterFragPipe(sce_fp_final, minPeptides = 1, - plotUpset = TRUE, revPattern = "^rev_", - exclFile = NULL) + out <- filterFeaturesSE( + sce_fp_final, + filtersSE = list( + Contaminant = function(sce) if ("Protein" %in% colnames(rowData(sce))) !grepl("^contam_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) else rep(TRUE, nrow(sce)), + Reverse = function(sce) if ("Protein" %in% colnames(rowData(sce))) !grepl("^rev_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Combined.Total.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Combined.Total.Peptides >= 1 & !is.na(rowData(sce)$Combined.Total.Peptides) else rep(TRUE, nrow(sce)) + ), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( rowData(sce_fp_final)$Combined.Total.Peptides >= 1 & !grepl("^rev_", rownames(sce_fp_final)) & @@ -775,9 +580,13 @@ test_that("filtering works (FragPipe)", { expect_equal(nrow(out), 113L) ## Don't filter on minPeptides - out <- filterFragPipe(sce_fp_final, minPeptides = NULL, - plotUpset = TRUE, revPattern = "^rev_", - exclFile = NULL) + out <- filterFeaturesSE( + sce_fp_final, + filtersSE = list( + Contaminant = function(sce) if ("Protein" %in% colnames(rowData(sce))) !grepl("^contam_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) else rep(TRUE, nrow(sce)), + Reverse = function(sce) if ("Protein" %in% colnames(rowData(sce))) !grepl("^rev_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) else rep(TRUE, nrow(sce)) + ), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( !grepl("^rev_", rownames(sce_fp_final)) & !grepl("^contam_", rownames(sce_fp_final)) @@ -787,9 +596,14 @@ test_that("filtering works (FragPipe)", { ## Missing columns - Combined.Total.Peptides tmp <- sce_fp_final rowData(tmp)$Combined.Total.Peptides <- NULL - out <- filterFragPipe(tmp, minPeptides = 3, - plotUpset = TRUE, revPattern = "^rev_", - exclFile = NULL) + out <- filterFeaturesSE( + tmp, + filtersSE = list( + Contaminant = function(sce) if ("Protein" %in% colnames(rowData(sce))) !grepl("^contam_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) else rep(TRUE, nrow(sce)), + Reverse = function(sce) if ("Protein" %in% colnames(rowData(sce))) !grepl("^rev_", rowData(sce)$Protein) & !is.na(rowData(sce)$Protein) else rep(TRUE, nrow(sce)), + Peptides = function(sce) if ("Combined.Total.Peptides" %in% colnames(rowData(sce))) rowData(sce)$Combined.Total.Peptides >= 3 & !is.na(rowData(sce)$Combined.Total.Peptides) else rep(TRUE, nrow(sce)) + ), + plotUpset = TRUE, exclFile = NULL) expect_equal(nrow(out), length(which( !grepl("^rev_", rownames(sce_fp_final)) & !grepl("^contam_", rownames(sce_fp_final)) From 823957ccc583be2deff35ff15050e2b4f4a803f4 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 14:56:23 +0100 Subject: [PATCH 04/65] Add filtersDF argument to importDIANN, allow importing parquet files --- R/importDIANN.R | 63 ++++++++++++++++++++++--------- man/importDIANN.Rd | 17 +++++++-- tests/testthat/test-importDIANN.R | 4 +- 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/R/importDIANN.R b/R/importDIANN.R index b78ad86..dd3616b 100644 --- a/R/importDIANN.R +++ b/R/importDIANN.R @@ -6,9 +6,11 @@ #' interpret results with caution. #' #' @param inFile Path to a tab-delimited input text file from DIA-NN; either -#' \code{pg_matrix.tsv}, \code{pr_matrix.tsv} or the main \code{report.tsv}. +#' \code{*.parquet}, \code{pg_matrix.tsv}, \code{pr_matrix.tsv} or +#' \code{report.tsv}. #' @param fileType Character scalar indicating the type of input file; either -#' \code{"pg_matrix"}, \code{"pr_matrix"} or \code{"main_report"}. +#' \code{"parquet"}, \code{"pg_matrix"}, \code{"pr_matrix"} or +#' \code{"main_report"}. #' @param outLevel Character scalar indicating the desired output level; #' either \code{"pg"} (protein group) or \code{"pr"} (precursor). #' @param includeOnlySamples,excludeSamples Character vectors defining @@ -19,7 +21,14 @@ #' @param aName Character scalar giving the name of the main assay (if #' \code{fileType} is \code{"pg_matrix"} or \code{"pr_matrix"}), or the #' column from which to get the values for the main assay (if -#' \code{fileType} is \code{"main_report"}). +#' \code{fileType} is \code{"main_report"} or \code{"parquet"}). +#' @param filtersDF Named list where each element is a filtering function +#' that takes a \code{data.frame} (or \code{duckplyr} data frame) as +#' input and returns a filtered object of the same class. No other +#' arguments are allowed. The filtering functions will be applied in the +#' order they are provided in the list, after reading the long-format text +#' file or parquet file, and before creating the wide-format assay. Only +#' applies if \code{fileType} is \code{"main_report"} or \code{"parquet"}. #' @param ... Additional arguments that will be passed on to #' \code{QFeatures::readSummarizedExperiment} (e.g., the number of rows #' to import). @@ -41,18 +50,27 @@ importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", includeOnlySamples = "", excludeSamples = "", stopIfEmpty = FALSE, - aName = "MaxLFQ", ...) { + aName = "MaxLFQ", filtersDF = list(), ...) { ## Check input arguments .assertScalar(x = inFile, type = "character") stopifnot(file.exists(inFile)) .assertScalar(x = fileType, type = "character", - validValues = c("pg_matrix", "pr_matrix", "main_report")) + validValues = c("pg_matrix", "pr_matrix", "main_report", + "parquet")) .assertScalar(x = outLevel, type = "character", validValues = c("pg", "pr")) .assertVector(x = includeOnlySamples, type = "character") .assertVector(x = excludeSamples, type = "character") .assertScalar(x = stopIfEmpty, type = "logical") .assertScalar(x = aName, type = "character") + .assertVector(x = filtersDF, type = "list") + if (length(filtersDF) > 0) { + .assertVector(x = names(filtersDF), type = "character") + for (f in filtersDF) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } if (fileType == "pg_matrix" && outLevel == "pr") { stop("To obtain precursor output, please provide either the ", @@ -88,26 +106,37 @@ importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", sce <- methods::as(sce, "SingleCellExperiment") return(list(sce = sce, aName = aName)) - } else if (fileType == "main_report") { - tmp <- read.delim(inFile, header = TRUE, sep = "\t") - stopifnot(aName %in% colnames(tmp)) - iColsAll <- unique(tmp$Run) + } else if (fileType %in% c("main_report", "parquet")) { + if (fileType == "main_report") { + tmp <- read.delim(inFile, header = TRUE, sep = "\t") + } else { + .assertPackagesAvailable("duckplyr") + tmp <- duckplyr::read_parquet_duckdb(inFile) + } + iColsAll <- tmp |> dplyr::select(Run) |> distinct() |> pull() iCols <- .getiCols(iColsAll = iColsAll, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, stopIfEmpty = stopIfEmpty) - tmp <- tmp[tmp$Run %in% iCols, ] + tmp <- tmp |> dplyr::filter(Run %in% iCols) + stopifnot(aName %in% colnames(tmp)) + + # Filter + for (f in filtersDF) { + tmp <- f(tmp) + } + tmp <- dplyr::collect(dplyr::compute(tmp)) if (outLevel == "pg") { - tmp <- tmp[, unique(c("Run", "Protein.Group", "Protein.Ids", - aName, - grep("PG\\.", colnames(tmp), value = TRUE)))] - tmp <- tmp %>% + tmp <- tmp |> dplyr::select(all_of(c("Run", "Protein.Group", + "Protein.Ids", aName, + grep("^PG\\.", colnames(tmp), value = TRUE)))) + tmp <- tmp |> dplyr::group_by(dplyr::across(c(-"Protein.Ids"))) %>% dplyr::summarize(Protein.Ids = paste(unique(unlist(strsplit(.data$Protein.Ids, ";"))), collapse = ";"), - .groups = "drop") - tmp <- unique(tmp) + .groups = "drop") |> + dplyr::distinct() rd <- DataFrame(Protein.Group = unique(tmp$Protein.Group)) aL <- list() for (nm in setdiff(colnames(tmp), c("Run", "Protein.Group"))) { @@ -116,7 +145,7 @@ importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", dplyr::select(dplyr::all_of(c("Protein.Group", nm))) tmpcount <- tmpsub %>% dplyr::count(.data$Protein.Group) - if (all(tmpcount$n == 1)) { + if (all(tmpcount$n == 1) && length(iCols) != 1) { ## One value per protein group -> annotation tmpsub <- tmpsub %>% dplyr::distinct() diff --git a/man/importDIANN.Rd b/man/importDIANN.Rd index eef7340..28a7b94 100644 --- a/man/importDIANN.Rd +++ b/man/importDIANN.Rd @@ -12,15 +12,18 @@ importDIANN( excludeSamples = "", stopIfEmpty = FALSE, aName = "MaxLFQ", + filtersDF = list(), ... ) } \arguments{ \item{inFile}{Path to a tab-delimited input text file from DIA-NN; either -\code{pg_matrix.tsv}, \code{pr_matrix.tsv} or the main \code{report.tsv}.} +\code{*.parquet}, \code{pg_matrix.tsv}, \code{pr_matrix.tsv} or +\code{report.tsv}.} \item{fileType}{Character scalar indicating the type of input file; either -\code{"pg_matrix"}, \code{"pr_matrix"} or \code{"main_report"}.} +\code{"parquet"}, \code{"pg_matrix"}, \code{"pr_matrix"} or +\code{"main_report"}.} \item{outLevel}{Character scalar indicating the desired output level; either \code{"pg"} (protein group) or \code{"pr"} (precursor).} @@ -35,7 +38,15 @@ columns matching the patterns are found.} \item{aName}{Character scalar giving the name of the main assay (if \code{fileType} is \code{"pg_matrix"} or \code{"pr_matrix"}), or the column from which to get the values for the main assay (if -\code{fileType} is \code{"main_report"}).} +\code{fileType} is \code{"main_report"} or \code{"parquet"}).} + +\item{filtersDF}{Named list where each element is a filtering function +that takes a \code{data.frame} (or \code{duckplyr} data frame) as +input and returns a filtered object of the same class. No other +arguments are allowed. The filtering functions will be applied in the +order they are provided in the list, after reading the long-format text +file or parquet file, and before creating the wide-format assay. Only +applies if \code{fileType} is \code{"main_report"} or \code{"parquet"}.} \item{...}{Additional arguments that will be passed on to \code{QFeatures::readSummarizedExperiment} (e.g., the number of rows diff --git a/tests/testthat/test-importDIANN.R b/tests/testthat/test-importDIANN.R index 2aeb01a..593e37f 100644 --- a/tests/testthat/test-importDIANN.R +++ b/tests/testthat/test-importDIANN.R @@ -136,7 +136,7 @@ test_that("DIANN import works", { expect_equal(sort(rownames(out$sce)), sort(unique(maintmp$Protein.Group))) expect_equal(SummarizedExperiment::assayNames(out$sce), c("PG.MaxLFQ", "PG.Quantity", "PG.Normalised", "PG.Q.Value", - "Global.PG.Q.Value", "Lib.PG.Q.Value", "Protein.Ids")) + "Protein.Ids")) ## Manual checks aNames <- SummarizedExperiment::assayNames(out$sce) @@ -239,7 +239,7 @@ test_that("DIANN import works", { expect_equal(sort(rownames(out$sce)), sort(unique(maintmp$Protein.Group[grep("Condition_A", maintmp$Run)]))) expect_equal(SummarizedExperiment::assayNames(out$sce), c("PG.MaxLFQ", "PG.Quantity", "PG.Normalised", "PG.Q.Value", - "Global.PG.Q.Value", "Lib.PG.Q.Value", "Protein.Ids")) + "Protein.Ids")) ## Manual checks aNames <- SummarizedExperiment::assayNames(out$sce) From df1a1868bd9559f269305ac9b919da8fcfaaaa14 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 14:57:07 +0100 Subject: [PATCH 05/65] Add filtersDF argument to importSpectronaut --- R/importSpectronaut.R | 22 +++++++++++++++++++++- man/importSpectronaut.Rd | 9 +++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/R/importSpectronaut.R b/R/importSpectronaut.R index 92997f6..d031788 100644 --- a/R/importSpectronaut.R +++ b/R/importSpectronaut.R @@ -23,6 +23,13 @@ #' @param aName Character scalar giving the column from which to get the #' values for the main assay. Only used if \code{fileType} is #' \code{"long_format"}. +#' @param filtersDF Named list where each element is a filtering function +#' that takes a \code{data.frame} as +#' input and returns a filtered \code{data.frame}. No other +#' arguments are allowed. The filtering functions will be applied in the +#' order they are provided in the list, after reading the long-format text +#' file, and before creating the wide-format assay. Only +#' applies if \code{fileType} is \code{"long_format"}. #' @param ... Additional arguments that will be passed on to #' \code{QFeatures::readSummarizedExperiment} (e.g., the number of rows #' to import). @@ -37,7 +44,7 @@ importSpectronaut <- function(inFile, fileType = "pg_pivot", outLevel = "pg", iColPattern = ".PG.Quantity$", includeOnlySamples = "", excludeSamples = "", stopIfEmpty = FALSE, - aName = "PG.Quantity", ...) { + aName = "PG.Quantity", filtersDF = list(), ...) { ## Check input arguments .assertScalar(x = inFile, type = "character") stopifnot(file.exists(inFile)) @@ -54,6 +61,14 @@ importSpectronaut <- function(inFile, fileType = "pg_pivot", outLevel = "pg", if (fileType == "long_format") { .assertScalar(x = aName, type = "character") } + .assertVector(x = filtersDF, type = "list") + if (length(filtersDF) > 0) { + .assertVector(x = names(filtersDF), type = "character") + for (f in filtersDF) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } if (fileType == "long_format") { tmp <- read.delim(inFile, header = TRUE, sep = "\t") @@ -65,6 +80,11 @@ importSpectronaut <- function(inFile, fileType = "pg_pivot", outLevel = "pg", stopIfEmpty = stopIfEmpty) tmp <- tmp[tmp$R.FileName %in% iCols, ] + # Filter + for (f in filtersDF) { + tmp <- f(tmp) + } + if (outLevel == "pg") { tmp <- tmp[, unique(c("R.FileName", "PG.ProteinGroups", "PG.ProteinNames", aName, diff --git a/man/importSpectronaut.Rd b/man/importSpectronaut.Rd index 3572612..3482d7a 100644 --- a/man/importSpectronaut.Rd +++ b/man/importSpectronaut.Rd @@ -13,6 +13,7 @@ importSpectronaut( excludeSamples = "", stopIfEmpty = FALSE, aName = "PG.Quantity", + filtersDF = list(), ... ) } @@ -42,6 +43,14 @@ columns matching the patterns are found.} values for the main assay. Only used if \code{fileType} is \code{"long_format"}.} +\item{filtersDF}{Named list where each element is a filtering function +that takes a \code{data.frame} as +input and returns a filtered \code{data.frame}. No other +arguments are allowed. The filtering functions will be applied in the +order they are provided in the list, after reading the long-format text +file, and before creating the wide-format assay. Only +applies if \code{fileType} is \code{"long_format"}.} + \item{...}{Additional arguments that will be passed on to \code{QFeatures::readSummarizedExperiment} (e.g., the number of rows to import).} From c6b7a25150385dea9463c89d3fafe4ba9bed8512 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 14:59:04 +0100 Subject: [PATCH 06/65] Add duckplyr to Suggests --- DESCRIPTION | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index ef7060c..e6925f6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -83,7 +83,8 @@ Suggests: BiocManager, testthat (>= 3.0.0), iSEE, - matrixStats + matrixStats, + duckplyr Config/testthat/edition: 3 VignetteBuilder: knitr biocViews: Proteomics, DataImport From dd6e85f24cde4bdb0ea9cb8c9d2cbacb68d9c8bb Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 15:01:21 +0100 Subject: [PATCH 07/65] Update NAMESPACE --- NAMESPACE | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index 07789f6..ba47d42 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,14 +9,16 @@ export(defineAssayNames) export(doImputation) export(doNormalization) export(doPCA) +export(einprotFragPipeFilters) +export(einprotMQFilters) +export(einprotPDTMTPeptideGroupFilters) +export(einprotPDTMTProteinFilters) +export(einprotSpectronautFilters) export(emptySampleText) export(expDesignText) export(featureCollectionText) export(filterByModText) -export(filterFragPipe) -export(filterMaxQuant) -export(filterPDTMT) -export(filterSpectronaut) +export(filterFeaturesSE) export(fixFeatureIds) export(formatTableColumns) export(getCalibrationFrompdAnalysis) From 6fe21ffacb186a55f9726780c49dadc2b132090d Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 15:22:24 +0100 Subject: [PATCH 08/65] Add default DF filter for DIA-NN --- NAMESPACE | 1 + R/doFilter.R | 2 +- R/importDIANN.R | 32 ++++++++++++++++++++++++++++++++ man/defaultFiltersDF.Rd | 19 +++++++++++++++++++ man/defaultFiltersSE.Rd | 2 +- 5 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 man/defaultFiltersDF.Rd diff --git a/NAMESPACE b/NAMESPACE index ba47d42..e852793 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -9,6 +9,7 @@ export(defineAssayNames) export(doImputation) export(doNormalization) export(doPCA) +export(einprotDIANNFiltersDF) export(einprotFragPipeFilters) export(einprotMQFilters) export(einprotPDTMTPeptideGroupFilters) diff --git a/R/doFilter.R b/R/doFilter.R index 0c7a237..e1fc4e4 100644 --- a/R/doFilter.R +++ b/R/doFilter.R @@ -137,7 +137,7 @@ filterFeaturesSE <- function(sce, filtersSE = list(), plotUpset = TRUE, } } -#' Default filters +#' Default filters (SE) #' #' einprot provides default filters for data imported from various tools. #' To see the functions and thresholds used in each of these, print the diff --git a/R/importDIANN.R b/R/importDIANN.R index dd3616b..c3609cd 100644 --- a/R/importDIANN.R +++ b/R/importDIANN.R @@ -1,3 +1,35 @@ +#' Default filters (DF) +#' +#' einprot provides default filters for data imported from various tools. +#' To see the functions and thresholds used in each of these, print the +#' corresponding object below. +#' +#' @export +#' @name defaultFiltersDF +#' @rdname defaultFiltersDF +einprotDIANNFiltersDF <- list(Filter = function(df) { + if ("Proteotypic" %in% colnames(df)) { + df <- df |> dplyr::filter(Proteotypic == 1) + } + if ("PG.MaxLFQ.Quality" %in% colnames(df)) { + df <- df |> dplyr::filter(PG.MaxLFQ.Quality >= 0.7) + } + if ("Global.PG.Q.Value" %in% colnames(df)) { + df <- df |> dplyr::filter(Global.PG.Q.Value <= 0.01) + } + if ("Protein.Q.Value" %in% colnames(df)) { + df <- df |> dplyr::filter(Protein.Q.Value <= 0.005) + } + df <- dplyr::collect(dplyr::compute(df)) + if (all(c("Protein.Ids", "Precursor.Id") %in% colnames(df))) { + df <- df |> dplyr::group_by(Protein.Ids) |> + dplyr::mutate(N.Proteotypic.Sequences = dplyr::n_distinct(Precursor.Id)) |> + dplyr::ungroup() |> + dplyr::filter(N.Proteotypic.Sequences >= 2) + } + df +}) + #' Import data from DIA-NN into a SingleCellExperiment object #' #' Import data from a DIA-NN quantification file into a diff --git a/man/defaultFiltersDF.Rd b/man/defaultFiltersDF.Rd new file mode 100644 index 0000000..07d5036 --- /dev/null +++ b/man/defaultFiltersDF.Rd @@ -0,0 +1,19 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/importDIANN.R +\docType{data} +\name{defaultFiltersDF} +\alias{defaultFiltersDF} +\alias{einprotDIANNFiltersDF} +\title{Default filters (DF)} +\format{ +An object of class \code{list} of length 1. +} +\usage{ +einprotDIANNFiltersDF +} +\description{ +einprot provides default filters for data imported from various tools. +To see the functions and thresholds used in each of these, print the +corresponding object below. +} +\keyword{datasets} diff --git a/man/defaultFiltersSE.Rd b/man/defaultFiltersSE.Rd index 7532291..b869b77 100644 --- a/man/defaultFiltersSE.Rd +++ b/man/defaultFiltersSE.Rd @@ -8,7 +8,7 @@ \alias{einprotPDTMTPeptideGroupFilters} \alias{einprotFragPipeFilters} \alias{einprotSpectronautFilters} -\title{Default filters} +\title{Default filters (SE)} \format{ An object of class \code{list} of length 5. From 87fb10df3e4d11b4c27a8059c74493cb8cacd72e Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 15:22:39 +0100 Subject: [PATCH 09/65] Add test for importDIANN with filtering --- tests/testthat/test-importDIANN.R | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/testthat/test-importDIANN.R b/tests/testthat/test-importDIANN.R index 593e37f..df2e837 100644 --- a/tests/testthat/test-importDIANN.R +++ b/tests/testthat/test-importDIANN.R @@ -260,4 +260,40 @@ test_that("DIANN import works", { expect_false(any(grepl("scratch", colnames(out$sce)))) expect_false(any(grepl("mzML", colnames(out$sce)))) + ## ------------------------------------------------------------------------- + ## Specifying samples to include, and filtering - main_report/pg + ## ------------------------------------------------------------------------- + out <- importDIANN(inFile = mainrep, fileType = "main_report", + outLevel = "pg", filtersDF = einprotDIANNFiltersDF, + includeOnlySamples = "Condition_A", excludeSamples = "", + stopIfEmpty = FALSE, aName = "PG.MaxLFQ") + expect_type(out, "list") + expect_named(out, c("sce", "aName")) + expect_equal(out$aName, "PG.MaxLFQ") + expect_equal(nrow(out$sce), 3L) ## length(unique(maintmp$Protein.Group[grep("Condition_A", maintmp$Run)])) + expect_equal(ncol(out$sce), 3L) + expect_s4_class(out$sce, "SingleCellExperiment") + expect_equal(sort(rownames(out$sce)), c("P0A7J3", "P25694", "Q9NVA2")) + expect_equal(SummarizedExperiment::assayNames(out$sce), + c("PG.MaxLFQ", "PG.Quantity", "PG.Normalised", "PG.Q.Value", + "Protein.Ids")) + + ## Manual checks + aNames <- SummarizedExperiment::assayNames(out$sce) + sub1 <- subset(maintmp, Run == "LFQ_Orbitrap_AIF_Condition_A_Sample_Beta_01") + for (an in aNames) { + sub2 <- sub1[match(c("P0A7J3"), + sub1$Protein.Group), an] + sub2[is.na(sub2)] <- 0 + expect_equal(SummarizedExperiment::assay( + out$sce, an)[c("P0A7J3"), + "LFQ_Orbitrap_AIF_Condition_A_Sample_Beta_01"], + sub2, ignore_attr = TRUE) + } + + infoCols <- c("Protein.Group") + expect_true(all(infoCols %in% + colnames(SummarizedExperiment::rowData(out$sce)))) + expect_false(any(grepl("scratch", colnames(out$sce)))) + expect_false(any(grepl("mzML", colnames(out$sce)))) }) From 03386536e864abc3fd1f2c9fc17f49b6e374115b Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 16:11:55 +0100 Subject: [PATCH 10/65] Remove old filtering arguments in .check* functions, add checks for new filtering functions --- R/checkArgumentsDIANN.R | 22 ++++++++++++++++++---- R/checkArgumentsFragPipe.R | 18 ++++++++++++------ R/checkArgumentsMaxQuant.R | 18 ++++++++++++------ R/checkArgumentsPDTMT.R | 22 +++++++++++----------- R/checkArgumentsSpectronaut.R | 22 ++++++++++++++++++---- 5 files changed, 71 insertions(+), 31 deletions(-) diff --git a/R/checkArgumentsDIANN.R b/R/checkArgumentsDIANN.R index e80c80d..47c8099 100644 --- a/R/checkArgumentsDIANN.R +++ b/R/checkArgumentsDIANN.R @@ -10,7 +10,7 @@ forceOverwrite, experimentInfo, species, diannFile, diannFileType, outLevel, diannLogFile, aName, idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, sampleAnnot, includeOnlySamples, - excludeSamples, minScore, minPeptides, imputeMethod, assaysForExport, + excludeSamples, imputeMethod, filtersDF, filtersSE, assaysForExport, addAbundanceValues, addHeatmaps, mergeGroups, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, @@ -135,9 +135,23 @@ .assertVector(x = linkTableColumns, type = "character", allowNULL = TRUE) - ## Score thresholds - .assertScalar(x = minScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPeptides, type = "numeric", allowNULL = TRUE) + ## Filter functions + .assertVector(x = filtersDF, type = "list") + if (length(filtersDF) > 0) { + .assertVector(x = names(filtersDF), type = "character") + for (f in filtersDF) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } + .assertVector(x = filtersSE, type = "list") + if (length(filtersSE) > 0) { + .assertVector(x = names(filtersSE), type = "character") + for (f in filtersSE) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } ## Method choices .assertScalar(x = imputeMethod, type = "character", diff --git a/R/checkArgumentsFragPipe.R b/R/checkArgumentsFragPipe.R index cb546c4..584d1c7 100755 --- a/R/checkArgumentsFragPipe.R +++ b/R/checkArgumentsFragPipe.R @@ -9,9 +9,9 @@ templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, experimentInfo, species, fragpipeDir, idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, - iColPattern, sampleAnnot, includeOnlySamples, excludeSamples, minScore, - minPeptides, imputeMethod, assaysForExport, addAbundanceValues, - addHeatmaps, mergeGroups, + iColPattern, sampleAnnot, includeOnlySamples, excludeSamples, + imputeMethod, assaysForExport, addAbundanceValues, + addHeatmaps, mergeGroups, filtersSE, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, minNbrValidValues, minlFC, samSignificance, nperm, volcanoAdjPvalThr, @@ -141,9 +141,15 @@ .assertVector(x = linkTableColumns, type = "character", allowNULL = TRUE) - ## Score thresholds - .assertScalar(x = minScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPeptides, type = "numeric", allowNULL = TRUE) + ## Filter functions + .assertVector(x = filtersSE, type = "list") + if (length(filtersSE) > 0) { + .assertVector(x = names(filtersSE), type = "character") + for (f in filtersSE) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } ## Method choices .assertScalar(x = imputeMethod, type = "character", diff --git a/R/checkArgumentsMaxQuant.R b/R/checkArgumentsMaxQuant.R index abd760f..b9078ba 100644 --- a/R/checkArgumentsMaxQuant.R +++ b/R/checkArgumentsMaxQuant.R @@ -9,9 +9,9 @@ templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, experimentInfo, species, mqFile, mqParameterFile, idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, - iColPattern, sampleAnnot, includeOnlySamples, excludeSamples, minScore, - minPeptides, imputeMethod, assaysForExport, addAbundanceValues, - addHeatmaps, mergeGroups, + iColPattern, sampleAnnot, includeOnlySamples, excludeSamples, + imputeMethod, assaysForExport, addAbundanceValues, + addHeatmaps, mergeGroups, filtersSE, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, minNbrValidValues, minlFC, samSignificance, nperm, volcanoAdjPvalThr, @@ -126,9 +126,15 @@ .assertVector(x = linkTableColumns, type = "character", allowNULL = TRUE) - ## Score thresholds - .assertScalar(x = minScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPeptides, type = "numeric", allowNULL = TRUE) + ## Filter functions + .assertVector(x = filtersSE, type = "list") + if (length(filtersSE) > 0) { + .assertVector(x = names(filtersSE), type = "character") + for (f in filtersSE) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } ## Method choices .assertScalar(x = imputeMethod, type = "character", diff --git a/R/checkArgumentsPDTMT.R b/R/checkArgumentsPDTMT.R index 7b52cd7..318bf6e 100644 --- a/R/checkArgumentsPDTMT.R +++ b/R/checkArgumentsPDTMT.R @@ -9,10 +9,9 @@ templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, experimentInfo, species, pdOutputFolder, pdResultName, inputLevel, pdAnalysisFile, idCol, labelCol, geneIdCol, proteinIdCol, - stringIdCol, extraFeatureCols, + stringIdCol, extraFeatureCols, filtersSE, modificationsCol, excludeUnmodifiedPeptides, keepModifications, iColPattern, sampleAnnot, includeOnlySamples, excludeSamples, - minScore, minDeltaScore, minPeptides, minPSMs, masterProteinsOnly, imputeMethod, assaysForExport, addAbundanceValues, addHeatmaps, mergeGroups, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, @@ -136,15 +135,16 @@ .assertVector(x = linkTableColumns, type = "character", allowNULL = TRUE) - ## Score thresholds - if (inputLevel == "Proteins") { - .assertScalar(x = minScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPeptides, type = "numeric", allowNULL = TRUE) - .assertScalar(x = masterProteinsOnly, type = "logical") - } else if (inputLevel == "PeptideGroups") { - .assertScalar(x = minDeltaScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPSMs, type = "numeric", allowNULL = TRUE) - + ## Filters + .assertVector(x = filtersSE, type = "list") + if (length(filtersSE) > 0) { + .assertVector(x = names(filtersSE), type = "character") + for (f in filtersSE) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } + if (inputLevel == "PeptideGroups") { .assertScalar(x = modificationsCol, type = "character", allowNULL = TRUE) .assertScalar(x = excludeUnmodifiedPeptides, type = "logical") diff --git a/R/checkArgumentsSpectronaut.R b/R/checkArgumentsSpectronaut.R index 03baa99..051aeed 100644 --- a/R/checkArgumentsSpectronaut.R +++ b/R/checkArgumentsSpectronaut.R @@ -11,7 +11,7 @@ spectronautFileType, outLevel, spectronautLogFile, aName, idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, iColPattern, sampleAnnot, includeOnlySamples, - excludeSamples, minScore, minPeptides, imputeMethod, assaysForExport, + excludeSamples, imputeMethod, filtersDF, filtersSE, assaysForExport, addAbundanceValues, addHeatmaps, mergeGroups, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, @@ -138,9 +138,23 @@ .assertVector(x = linkTableColumns, type = "character", allowNULL = TRUE) - ## Score thresholds - .assertScalar(x = minScore, type = "numeric", allowNULL = TRUE) - .assertScalar(x = minPeptides, type = "numeric", allowNULL = TRUE) + ## Filter functions + .assertVector(x = filtersDF, type = "list") + if (length(filtersDF) > 0) { + .assertVector(x = names(filtersDF), type = "character") + for (f in filtersDF) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } + .assertVector(x = filtersSE, type = "list") + if (length(filtersSE) > 0) { + .assertVector(x = names(filtersSE), type = "character") + for (f in filtersSE) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } ## Method choices .assertScalar(x = imputeMethod, type = "character", From 9ed574d641d62c684b583e01b3c43b4d384cbd67 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 16:12:23 +0100 Subject: [PATCH 11/65] Update default filters to match previous defaults --- R/doFilter.R | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/R/doFilter.R b/R/doFilter.R index e1fc4e4..56755b4 100644 --- a/R/doFilter.R +++ b/R/doFilter.R @@ -149,7 +149,7 @@ filterFeaturesSE <- function(sce, filtersSE = list(), plotUpset = TRUE, einprotMQFilters <- list( Score = function(sce) { if ("Score" %in% colnames(rowData(sce))) { - kp <- rowData(sce)$Score >= 2 & !is.na(rowData(sce)$Score) + kp <- rowData(sce)$Score >= 10 & !is.na(rowData(sce)$Score) } else { kp <- rep(TRUE, nrow(sce)) } @@ -219,14 +219,6 @@ einprotPDTMTProteinFilters <- list( kp <- rep(TRUE, nrow(sce)) } kp - }, - Master = function(sce) { - if ("Master" %in% colnames(rowData(sce))) { - kp <- rowData(sce)$Master == "IsMasterProtein" & !is.na(rowData(sce)$Master) - } else { - kp <- rep(TRUE, nrow(sce)) - } - kp } ) @@ -258,14 +250,6 @@ einprotPDTMTPeptideGroupFilters <- list( kp <- rep(TRUE, nrow(sce)) } kp - }, - ExclUnmodified = function(sce) { - if ("Modifications" %in% colnames(rowData(sce))) { - kp <- rowData(sce)$Modifications != "" & !is.na(rowData(sce)$Modifications) - } else { - kp <- rep(TRUE, nrow(sce)) - } - kp } ) From eaa9482b2691b918ff5af95f2eecdf8bf285d479 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 16:45:50 +0100 Subject: [PATCH 12/65] Swap filtering arguments in run*() functions --- R/runDIANNAnalysis.R | 37 ++++++++++++++++++----------------- R/runFragPipeAnalysis.R | 22 +++++++-------------- R/runMaxQuantAnalysis.R | 26 ++++++++++++------------ R/runPDTMTAnalysis.R | 25 ++++------------------- R/runSpectronautAnalysis.R | 18 ++++++----------- man/defaultFiltersSE.Rd | 4 ++-- man/runDIANNAnalysis.Rd | 36 +++++++++++++++++++++------------- man/runFragPipeAnalysis.Rd | 17 ++++++++-------- man/runMaxQuantAnalysis.Rd | 17 ++++++++-------- man/runPDTMTAnalysis.Rd | 28 +++++++++----------------- man/runSpectronautAnalysis.Rd | 18 ++++++++--------- 11 files changed, 106 insertions(+), 142 deletions(-) diff --git a/R/runDIANNAnalysis.R b/R/runDIANNAnalysis.R index f4d2219..febfdc5 100644 --- a/R/runDIANNAnalysis.R +++ b/R/runDIANNAnalysis.R @@ -7,26 +7,28 @@ #' @inheritParams runMaxQuantAnalysis #' #' @param diannFile Character string pointing to the DIA-NN -#' \code{pg_matrix.tsv}, \code{pr_matrix.tsv} or main \code{report.tsv} -#' file. File paths will be expressed in canonical form (using -#' \code{normalizePath()}) before they are processed. +#' \code{pg_matrix.tsv}, \code{pr_matrix.tsv}, main \code{report.tsv} or +#' \code{*.parquet} file. File paths will be expressed in canonical form +#' (using \code{normalizePath()}) before they are processed. #' @param diannFileType Character string indicating what type of input file #' \code{diannFile} represents. Either \code{"pg_matrix"}, -#' \code{"pr_matrix"} or \code{"main_report"}. +#' \code{"pr_matrix"}, \code{"main_report"} or \code{"parquet"}. #' @param outLevel Character string indicating the desired output level. #' Either \code{"pg"} or \code{"pr"}. +#' @param filtersDF Named list where each element is a filtering function +#' that takes a \code{data.frame} (or \code{duckplyr} data frame) as +#' input and returns a filtered object of the same class. No other +#' arguments are allowed. The filtering functions will be applied in the +#' order they are provided in the list, after reading the long-format text +#' file or parquet file, and before creating the wide-format assay. Only +#' applies if \code{diannFileType} is \code{"main_report"} or \code{"parquet"}. #' @param diannLogFile Character string pointing to the DIA-NN log file. #' File paths will be expressed in canonical form (using #' \code{normalizePath()}) before they are processed. #' @param aName Character scalar indicating the desired name of the main #' assay (if \code{diannFileType} is \code{"pg_matrix"} or #' \code{"pr_matrix"}), or the column to use for the main assay (if -#' \code{diannFileType} is \code{"main_report"}). -#' @param minScore Numeric, minimum score for a protein to be retained in the -#' analysis. Set to \code{NULL} if no score filtering is desired. -#' @param minPeptides Numeric, minimum number of peptides for a protein to be -#' retained in the analysis. Set to \code{NULL} if no filtering on the -#' number of peptides is desired. +#' \code{diannFileType} is \code{"main_report"} or \code{"parquet"}). #' #' @export #' @author Charlotte Soneson @@ -128,10 +130,9 @@ runDIANNAnalysis <- function( "Majority.protein.IDs"), combineWhen = "missing", makeUnique = FALSE), - extraFeatureCols = NULL, - sampleAnnot, - includeOnlySamples = "", excludeSamples = "", - minScore = 10, minPeptides = 2, imputeMethod = "MinProb", + extraFeatureCols = NULL, sampleAnnot, + includeOnlySamples = "", excludeSamples = "", imputeMethod = "MinProb", + filtersDF = einprotDIANNFiltersDF, filtersSE = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, @@ -180,8 +181,8 @@ runDIANNAnalysis <- function( proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, extraFeatureCols = extraFeatureCols, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, minScore = minScore, - minPeptides = minPeptides, imputeMethod = imputeMethod, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, + filtersDF = filtersDF, filtersSE = filtersSE, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, @@ -235,8 +236,8 @@ runDIANNAnalysis <- function( reportTitle = reportTitle, reportAuthor = reportAuthor, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, minScore = minScore, - minPeptides = minPeptides, imputeMethod = imputeMethod, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, + filtersDF = filtersDF, filtersSE = filtersSE, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, diff --git a/R/runFragPipeAnalysis.R b/R/runFragPipeAnalysis.R index ffca915..3af5172 100755 --- a/R/runFragPipeAnalysis.R +++ b/R/runFragPipeAnalysis.R @@ -12,11 +12,6 @@ #' @param iColPattern Regular expression identifying the columns of the FragPipe #' \code{combined_protein.tsv} file to use for the analysis. Typically #' \code{"\\.MaxLFQ\\.Intensity$"}. -#' @param minScore Numeric, minimum score for a protein to be retained in the -#' analysis. Set to \code{NULL} if no score filtering is desired. -#' @param minPeptides Numeric, minimum number of peptides for a protein to be -#' retained in the analysis. Set to \code{NULL} if no filtering on the -#' number of peptides is desired. #' #' @export #' @author Charlotte Soneson @@ -79,10 +74,9 @@ runFragPipeAnalysis <- function( "Protein.ID"), combineWhen = "missing", makeUnique = FALSE), - extraFeatureCols = NULL, - iColPattern, sampleAnnot, - includeOnlySamples = "", excludeSamples = "", - minScore = 10, minPeptides = 2, imputeMethod = "MinProb", + extraFeatureCols = NULL, filtersSE = einprotFragPipeFilters, + iColPattern, sampleAnnot, includeOnlySamples = "", excludeSamples = "", + imputeMethod = "MinProb", assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, @@ -128,10 +122,9 @@ runFragPipeAnalysis <- function( fragpipeDir = fragpipeDir, idCol = idCol, labelCol = labelCol, geneIdCol = geneIdCol, proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, extraFeatureCols = extraFeatureCols, - iColPattern = iColPattern, + filtersSE = filtersSE, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, minScore = minScore, - minPeptides = minPeptides, imputeMethod = imputeMethod, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, @@ -182,9 +175,8 @@ runFragPipeAnalysis <- function( reportTitle = reportTitle, reportAuthor = reportAuthor, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, minScore = minScore, - minPeptides = minPeptides, imputeMethod = imputeMethod, - assaysForExport = assaysForExport, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, + assaysForExport = assaysForExport, filtersSE = filtersSE, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, diff --git a/R/runMaxQuantAnalysis.R b/R/runMaxQuantAnalysis.R index 9546b24..fcb7164 100644 --- a/R/runMaxQuantAnalysis.R +++ b/R/runMaxQuantAnalysis.R @@ -65,11 +65,12 @@ #' input file, after removing the \code{iColPattern}. #' @param includeOnlySamples,excludeSamples Character vectors defining specific #' samples to include or exclude from all analyses. -#' @param minScore Numeric, minimum score for a protein to be retained in the -#' analysis. Set to \code{NULL} if no score filtering is desired. -#' @param minPeptides Numeric, minimum number of peptides for a protein to be -#' retained in the analysis. Set to \code{NULL} if no filtering on the -#' number of peptides is desired. +#' @param filtersSE A named \code{list}, where each element is a function +#' that takes a \code{SummarizedExperiment} object as input and returns +#' a logical vector of the same length as the number of rows in \code{sce}, +#' and where \code{TRUE} implies that the row should be retained. Default +#' sets of filtering functions for input data from different tools are +#' provided (see \code{?defaultFiltersSE}). #' @param imputeMethod Character string defining the imputation method to use. #' Currently, \code{"impSeqRob"}, \code{"MinProb"}, and #' \code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for @@ -266,10 +267,9 @@ runMaxQuantAnalysis <- function( "Majority.protein.IDs"), combineWhen = "missing", makeUnique = FALSE), - extraFeatureCols = NULL, + extraFeatureCols = NULL, filtersSE = einprotMQFilters, iColPattern, sampleAnnot, - includeOnlySamples = "", excludeSamples = "", - minScore = 10, minPeptides = 2, imputeMethod = "MinProb", + includeOnlySamples = "", excludeSamples = "", imputeMethod = "MinProb", assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), @@ -316,11 +316,10 @@ runMaxQuantAnalysis <- function( mqFile = mqFile, mqParameterFile = mqParameterFile, idCol = idCol, labelCol = labelCol, geneIdCol = geneIdCol, proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, - extraFeatureCols = extraFeatureCols, + extraFeatureCols = extraFeatureCols, filtersSE = filtersSE, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, minScore = minScore, - minPeptides = minPeptides, imputeMethod = imputeMethod, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, @@ -369,12 +368,11 @@ runMaxQuantAnalysis <- function( mqFile = mqFile, mqParameterFile = mqParameterFile, idCol = idCol, labelCol = labelCol, geneIdCol = geneIdCol, proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, - extraFeatureCols = extraFeatureCols, + extraFeatureCols = extraFeatureCols, filtersSE = filtersSE, reportTitle = reportTitle, reportAuthor = reportAuthor, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, minScore = minScore, - minPeptides = minPeptides, imputeMethod = imputeMethod, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, diff --git a/R/runPDTMTAnalysis.R b/R/runPDTMTAnalysis.R index 8426382..7dff8ba 100644 --- a/R/runPDTMTAnalysis.R +++ b/R/runPDTMTAnalysis.R @@ -37,17 +37,6 @@ #' \code{inputLevel} is "PeptideGroups". #' @param iColPattern Regular expression identifying the columns of the PD #' \code{Proteins.txt} file to use for the analysis. -#' @param minScore,minDeltaScore Numeric, minimum score for a protein (or -#' delta score for a peptide group) to be retained in the analysis. -#' Set to \code{NULL} if no score filtering is desired. -#' @param minPeptides,minPSMs Numeric, minimum number of peptides for a protein -#' (or PSMs for a peptide group) to be retained in the analysis. -#' Set to \code{NULL} if no filtering on the number of peptides/PTMs is -#' desired. -#' @param masterProteinsOnly Logical scalar indicating whether only master -#' proteins (where the \code{Master} column value is -#' \code{IsMasterProtein}) should be retained. Only used if -#' \code{inputLevel} is \code{"Proteins"}. #' #' @export #' @author Charlotte Soneson @@ -125,11 +114,11 @@ runPDTMTAnalysis <- function( combineWhen = "missing", makeUnique = FALSE), extraFeatureCols = NULL, + filtersSE = if (inputLevel == "Proteins") einprotPDTMTProteinFilters else einprotPDTMTPeptideGroupFilters, modificationsCol = "Modifications", excludeUnmodifiedPeptides = FALSE, keepModifications = NULL, iColPattern, sampleAnnot, includeOnlySamples = "", excludeSamples = "", - minScore = 2, minDeltaScore = 0.2, minPeptides = 2, minPSMs = 2, - masterProteinsOnly = FALSE, imputeMethod = "MinProb", + imputeMethod = "MinProb", assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, @@ -178,15 +167,12 @@ runPDTMTAnalysis <- function( pdAnalysisFile = pdAnalysisFile, idCol = idCol, labelCol = labelCol, geneIdCol = geneIdCol, proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, extraFeatureCols = extraFeatureCols, - modificationsCol = modificationsCol, + filtersSE = filtersSE, modificationsCol = modificationsCol, excludeUnmodifiedPeptides = excludeUnmodifiedPeptides, keepModifications = keepModifications, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, - minScore = minScore, minDeltaScore = minDeltaScore, - minPeptides = minPeptides, minPSMs = minPSMs, - masterProteinsOnly = masterProteinsOnly, imputeMethod = imputeMethod, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, @@ -237,16 +223,13 @@ runPDTMTAnalysis <- function( idCol = idCol, labelCol = labelCol, geneIdCol = geneIdCol, proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, extraFeatureCols = extraFeatureCols, - modificationsCol = modificationsCol, + filtersSE = filtersSE, modificationsCol = modificationsCol, excludeUnmodifiedPeptides = excludeUnmodifiedPeptides, keepModifications = keepModifications, reportTitle = reportTitle, reportAuthor = reportAuthor, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, - minScore = minScore, minDeltaScore = minDeltaScore, - minPeptides = minPeptides, minPSMs = minPSMs, - masterProteinsOnly = masterProteinsOnly, imputeMethod = imputeMethod, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, diff --git a/R/runSpectronautAnalysis.R b/R/runSpectronautAnalysis.R index 67db4d8..3279455 100644 --- a/R/runSpectronautAnalysis.R +++ b/R/runSpectronautAnalysis.R @@ -24,11 +24,6 @@ #' \code{".PG.IBAQ$"}. #' @param aName Character scalar indicating the column to use for the main #' assay. -#' @param minScore Numeric, minimum score for a protein to be retained in the -#' analysis. Set to \code{NULL} if no score filtering is desired. -#' @param minPeptides Numeric, minimum number of peptides for a protein to be -#' retained in the analysis. Set to \code{NULL} if no filtering on the -#' number of peptides is desired. #' #' @export #' @author Charlotte Soneson @@ -78,9 +73,8 @@ runSpectronautAnalysis <- function( combineWhen = "missing", makeUnique = FALSE), extraFeatureCols = NULL, iColPattern = ".PG.Quantity$", - sampleAnnot, - includeOnlySamples = "", excludeSamples = "", - minScore = 10, minPeptides = 2, imputeMethod = "MinProb", + sampleAnnot, includeOnlySamples = "", excludeSamples = "", + imputeMethod = "MinProb", filtersDF = list(), filtersSE = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), @@ -131,8 +125,8 @@ runSpectronautAnalysis <- function( proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, extraFeatureCols = extraFeatureCols, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, minScore = minScore, - minPeptides = minPeptides, imputeMethod = imputeMethod, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, + filtersDF = filtersDF, filtersSE = filtersSE, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, @@ -187,8 +181,8 @@ runSpectronautAnalysis <- function( reportTitle = reportTitle, reportAuthor = reportAuthor, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, minScore = minScore, - minPeptides = minPeptides, imputeMethod = imputeMethod, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, + filtersDF = filtersDF, filtersSE = filtersSE, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, diff --git a/man/defaultFiltersSE.Rd b/man/defaultFiltersSE.Rd index b869b77..6bbd795 100644 --- a/man/defaultFiltersSE.Rd +++ b/man/defaultFiltersSE.Rd @@ -12,9 +12,9 @@ \format{ An object of class \code{list} of length 5. -An object of class \code{list} of length 4. +An object of class \code{list} of length 3. -An object of class \code{list} of length 4. +An object of class \code{list} of length 3. An object of class \code{list} of length 3. diff --git a/man/runDIANNAnalysis.Rd b/man/runDIANNAnalysis.Rd index c28e06f..97427b0 100644 --- a/man/runDIANNAnalysis.Rd +++ b/man/runDIANNAnalysis.Rd @@ -30,9 +30,9 @@ runDIANNAnalysis( sampleAnnot, includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, imputeMethod = "MinProb", + filtersDF = einprotDIANNFiltersDF, + filtersSE = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -99,13 +99,13 @@ supported species (see \code{getSupportedSpecies()}). Either the common or the scientific name can be used.} \item{diannFile}{Character string pointing to the DIA-NN -\code{pg_matrix.tsv}, \code{pr_matrix.tsv} or main \code{report.tsv} -file. File paths will be expressed in canonical form (using -\code{normalizePath()}) before they are processed.} +\code{pg_matrix.tsv}, \code{pr_matrix.tsv}, main \code{report.tsv} or +\code{*.parquet} file. File paths will be expressed in canonical form +(using \code{normalizePath()}) before they are processed.} \item{diannFileType}{Character string indicating what type of input file \code{diannFile} represents. Either \code{"pg_matrix"}, -\code{"pr_matrix"} or \code{"main_report"}.} +\code{"pr_matrix"}, \code{"main_report"} or \code{"parquet"}.} \item{outLevel}{Character string indicating the desired output level. Either \code{"pg"} or \code{"pr"}.} @@ -117,7 +117,7 @@ File paths will be expressed in canonical form (using \item{aName}{Character scalar indicating the desired name of the main assay (if \code{diannFileType} is \code{"pg_matrix"} or \code{"pr_matrix"}), or the column to use for the main assay (if -\code{diannFileType} is \code{"main_report"}).} +\code{diannFileType} is \code{"main_report"} or \code{"parquet"}).} \item{idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol}{Arguments defining the feature identifiers (row names, should be unique), @@ -160,18 +160,26 @@ input file, after removing the \code{iColPattern}.} \item{includeOnlySamples, excludeSamples}{Character vectors defining specific samples to include or exclude from all analyses.} -\item{minScore}{Numeric, minimum score for a protein to be retained in the -analysis. Set to \code{NULL} if no score filtering is desired.} - -\item{minPeptides}{Numeric, minimum number of peptides for a protein to be -retained in the analysis. Set to \code{NULL} if no filtering on the -number of peptides is desired.} - \item{imputeMethod}{Character string defining the imputation method to use. Currently, \code{"impSeqRob"}, \code{"MinProb"}, and \code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for more details about the methods.} +\item{filtersDF}{Named list where each element is a filtering function +that takes a \code{data.frame} (or \code{duckplyr} data frame) as +input and returns a filtered object of the same class. No other +arguments are allowed. The filtering functions will be applied in the +order they are provided in the list, after reading the long-format text +file or parquet file, and before creating the wide-format assay. Only +applies if \code{diannFileType} is \code{"main_report"} or \code{"parquet"}.} + +\item{filtersSE}{A named \code{list}, where each element is a function +that takes a \code{SummarizedExperiment} object as input and returns +a logical vector of the same length as the number of rows in \code{sce}, +and where \code{TRUE} implies that the row should be retained. Default +sets of filtering functions for input data from different tools are +provided (see \code{?defaultFiltersSE}).} + \item{assaysForExport}{Character vector defining the name(s) of the assays to use for exported abundances and barplots. This could, for example, be set to an assay containing 'absolute' abundances, if available, even diff --git a/man/runFragPipeAnalysis.Rd b/man/runFragPipeAnalysis.Rd index 3b524af..a9280a6 100644 --- a/man/runFragPipeAnalysis.Rd +++ b/man/runFragPipeAnalysis.Rd @@ -21,12 +21,11 @@ runFragPipeAnalysis( stringIdCol = function(df) combineIds(df, combineCols = c("Gene", "Protein.ID"), combineWhen = "missing", makeUnique = FALSE), extraFeatureCols = NULL, + filtersSE = einprotFragPipeFilters, iColPattern, sampleAnnot, includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, imputeMethod = "MinProb", assaysForExport = NULL, addAbundanceValues = TRUE, @@ -129,6 +128,13 @@ created after the standard columns (\code{einprotId}, \code{einprotGene}, thus these columns can be used as well to create the user-specified ones.} +\item{filtersSE}{A named \code{list}, where each element is a function +that takes a \code{SummarizedExperiment} object as input and returns +a logical vector of the same length as the number of rows in \code{sce}, +and where \code{TRUE} implies that the row should be retained. Default +sets of filtering functions for input data from different tools are +provided (see \code{?defaultFiltersSE}).} + \item{iColPattern}{Regular expression identifying the columns of the FragPipe \code{combined_protein.tsv} file to use for the analysis. Typically \code{"\\.MaxLFQ\\.Intensity$"}.} @@ -144,13 +150,6 @@ input file, after removing the \code{iColPattern}.} \item{includeOnlySamples, excludeSamples}{Character vectors defining specific samples to include or exclude from all analyses.} -\item{minScore}{Numeric, minimum score for a protein to be retained in the -analysis. Set to \code{NULL} if no score filtering is desired.} - -\item{minPeptides}{Numeric, minimum number of peptides for a protein to be -retained in the analysis. Set to \code{NULL} if no filtering on the -number of peptides is desired.} - \item{imputeMethod}{Character string defining the imputation method to use. Currently, \code{"impSeqRob"}, \code{"MinProb"}, and \code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for diff --git a/man/runMaxQuantAnalysis.Rd b/man/runMaxQuantAnalysis.Rd index 370eabf..5c67a81 100644 --- a/man/runMaxQuantAnalysis.Rd +++ b/man/runMaxQuantAnalysis.Rd @@ -24,12 +24,11 @@ runMaxQuantAnalysis( stringIdCol = function(df) combineIds(df, combineCols = c("Gene.names", "Majority.protein.IDs"), combineWhen = "missing", makeUnique = FALSE), extraFeatureCols = NULL, + filtersSE = einprotMQFilters, iColPattern, sampleAnnot, includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, imputeMethod = "MinProb", assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, @@ -135,6 +134,13 @@ created after the standard columns (\code{einprotId}, \code{einprotGene}, thus these columns can be used as well to create the user-specified ones.} +\item{filtersSE}{A named \code{list}, where each element is a function +that takes a \code{SummarizedExperiment} object as input and returns +a logical vector of the same length as the number of rows in \code{sce}, +and where \code{TRUE} implies that the row should be retained. Default +sets of filtering functions for input data from different tools are +provided (see \code{?defaultFiltersSE}).} + \item{iColPattern}{Regular expression identifying the columns of the MaxQuant \code{proteinGroups.txt} file to use for the analysis. Typically either \code{"^Intensity\\."}, \code{"^LFQ\\.intensity\\."} or @@ -151,13 +157,6 @@ input file, after removing the \code{iColPattern}.} \item{includeOnlySamples, excludeSamples}{Character vectors defining specific samples to include or exclude from all analyses.} -\item{minScore}{Numeric, minimum score for a protein to be retained in the -analysis. Set to \code{NULL} if no score filtering is desired.} - -\item{minPeptides}{Numeric, minimum number of peptides for a protein to be -retained in the analysis. Set to \code{NULL} if no filtering on the -number of peptides is desired.} - \item{imputeMethod}{Character string defining the imputation method to use. Currently, \code{"impSeqRob"}, \code{"MinProb"}, and \code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for diff --git a/man/runPDTMTAnalysis.Rd b/man/runPDTMTAnalysis.Rd index ed8247f..61968f5 100644 --- a/man/runPDTMTAnalysis.Rd +++ b/man/runPDTMTAnalysis.Rd @@ -24,6 +24,8 @@ runPDTMTAnalysis( stringIdCol = function(df) combineIds(df, combineCols = c("Gene.Symbol", "Accession"), combineWhen = "missing", makeUnique = FALSE), extraFeatureCols = NULL, + filtersSE = if (inputLevel == "Proteins") einprotPDTMTProteinFilters else + einprotPDTMTPeptideGroupFilters, modificationsCol = "Modifications", excludeUnmodifiedPeptides = FALSE, keepModifications = NULL, @@ -31,11 +33,6 @@ runPDTMTAnalysis( sampleAnnot, includeOnlySamples = "", excludeSamples = "", - minScore = 2, - minDeltaScore = 0.2, - minPeptides = 2, - minPSMs = 2, - masterProteinsOnly = FALSE, imputeMethod = "MinProb", assaysForExport = NULL, addAbundanceValues = TRUE, @@ -154,6 +151,13 @@ created after the standard columns (\code{einprotId}, \code{einprotGene}, thus these columns can be used as well to create the user-specified ones.} +\item{filtersSE}{A named \code{list}, where each element is a function +that takes a \code{SummarizedExperiment} object as input and returns +a logical vector of the same length as the number of rows in \code{sce}, +and where \code{TRUE} implies that the row should be retained. Default +sets of filtering functions for input data from different tools are +provided (see \code{?defaultFiltersSE}).} + \item{modificationsCol}{Character string pointing to a column containing modification details. \code{excludeUnmodifiedPeptides} and \code{keepModifications} will use information from this column. Only @@ -183,20 +187,6 @@ input file, after removing the \code{iColPattern}.} \item{includeOnlySamples, excludeSamples}{Character vectors defining specific samples to include or exclude from all analyses.} -\item{minScore, minDeltaScore}{Numeric, minimum score for a protein (or -delta score for a peptide group) to be retained in the analysis. -Set to \code{NULL} if no score filtering is desired.} - -\item{minPeptides, minPSMs}{Numeric, minimum number of peptides for a protein -(or PSMs for a peptide group) to be retained in the analysis. -Set to \code{NULL} if no filtering on the number of peptides/PTMs is -desired.} - -\item{masterProteinsOnly}{Logical scalar indicating whether only master -proteins (where the \code{Master} column value is -\code{IsMasterProtein}) should be retained. Only used if -\code{inputLevel} is \code{"Proteins"}.} - \item{imputeMethod}{Character string defining the imputation method to use. Currently, \code{"impSeqRob"}, \code{"MinProb"}, and \code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for diff --git a/man/runSpectronautAnalysis.Rd b/man/runSpectronautAnalysis.Rd index a871507..3800fd5 100644 --- a/man/runSpectronautAnalysis.Rd +++ b/man/runSpectronautAnalysis.Rd @@ -29,9 +29,9 @@ runSpectronautAnalysis( sampleAnnot, includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, imputeMethod = "MinProb", + filtersDF = list(), + filtersSE = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -161,18 +161,18 @@ input file, after removing the \code{iColPattern}.} \item{includeOnlySamples, excludeSamples}{Character vectors defining specific samples to include or exclude from all analyses.} -\item{minScore}{Numeric, minimum score for a protein to be retained in the -analysis. Set to \code{NULL} if no score filtering is desired.} - -\item{minPeptides}{Numeric, minimum number of peptides for a protein to be -retained in the analysis. Set to \code{NULL} if no filtering on the -number of peptides is desired.} - \item{imputeMethod}{Character string defining the imputation method to use. Currently, \code{"impSeqRob"}, \code{"MinProb"}, and \code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for more details about the methods.} +\item{filtersSE}{A named \code{list}, where each element is a function +that takes a \code{SummarizedExperiment} object as input and returns +a logical vector of the same length as the number of rows in \code{sce}, +and where \code{TRUE} implies that the row should be retained. Default +sets of filtering functions for input data from different tools are +provided (see \code{?defaultFiltersSE}).} + \item{assaysForExport}{Character vector defining the name(s) of the assays to use for exported abundances and barplots. This could, for example, be set to an assay containing 'absolute' abundances, if available, even From bc2d928a4219b0783e285a13dd8f5510c5295118 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 16:59:16 +0100 Subject: [PATCH 13/65] Update filtering step in Rmd template --- inst/extdata/process_basic_template.Rmd | 94 ++++++++++--------------- 1 file changed, 37 insertions(+), 57 deletions(-) diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index 1a0028d..66f3928 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -185,8 +185,7 @@ settingsList <- list( "Include only samples (if applicable)" = paste(includeOnlySamples, collapse = ", "), "Exclude samples" = paste(excludeSamples, collapse = ", "), - "Min. number of peptides" = minPeptides, - "Min. protein score" = minScore, + "Filter names (SE)" = names(filtersSE), "Imputation method" = imputeMethod, "Assays(s) to use for exported values" = paste(assaysForExport, collapse = ", "), "Add abundance values" = addAbundanceValues, @@ -252,26 +251,19 @@ if (expType %in% c("MaxQuant", "FragPipe", "ProteomeDiscoverer")) { if (expType == "ProteomeDiscoverer") { settingsList <- c(settingsList, list("Input type" = inputLevel, - "Min. number of PSMs" = minPSMs, - "Min. delta score" = minDeltaScore, - "Only retain master proteins" = masterProteinsOnly, "Modifications column" = modificationsCol, "Exclude unmodified peptides" = excludeUnmodifiedPeptides, "Modifications to keep" = paste(keepModifications, collapse = ", "))) if (inputLevel == "Proteins") { settingsList <- settingsList[!(names(settingsList) %in% - c("Min. number of PSMs", - "Min. delta score", - "Modifications column", + c("Modifications column", "Exclude unmodified peptides", "Modifications to keep"))] } - if (inputLevel == "PeptideGroups") { - settingsList <- settingsList[!(names(settingsList) %in% - c("Min. number of peptides", - "Min. protein score", - "Only retain master proteins"))] - } +} +if (expType %in% c("Spectronaut", "DIANN")) { + settingsList <- c(settingsList, + list("Filter names (DF)" = names(filtersDF))) } makeTableFromList(settingsList) @@ -317,7 +309,7 @@ if (expType == "MaxQuant") { outLevel = outLevel, stopIfEmpty = FALSE, aName = aName, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples) + excludeSamples = excludeSamples, filtersDF = filtersDF) } else if (expType == "Spectronaut") { ## Read Spectronaut output tmp <- importSpectronaut(inFile = spectronautFile, @@ -325,7 +317,8 @@ if (expType == "MaxQuant") { iColPattern = iColPattern, stopIfEmpty = FALSE, aName = aName, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples) + excludeSamples = excludeSamples, + filtersDF = filtersDF) } else { stop("Unknown 'expType'") } @@ -422,39 +415,9 @@ filtered out based on the different criteria (vertical bars). ```{r filter-features} nbrFeaturesBefore <- nrow(sce) -if (expType == "MaxQuant") { - sce <- filterMaxQuant(sce = sce, minScore = minScore, minPeptides = minPeptides, - plotUpset = TRUE, - exclFile = sub("\\.Rmd$", paste0("_filtered_out_features.txt"), - knitr::current_input(dir = TRUE))) -} else if (expType == "FragPipe") { - sce <- filterFragPipe(sce = sce, minPeptides = minPeptides, - plotUpset = TRUE, - revPattern = paste0("^", tb[["Database decoy tag"]]), - exclFile = sub("\\.Rmd$", paste0("_filtered_out_features.txt"), - knitr::current_input(dir = TRUE))) -} else if (expType == "ProteomeDiscoverer") { - ## At this point, don't filter by modification type - this will be done later - sce <- filterPDTMT(sce = sce, inputLevel = inputLevel, minScore = minScore, - minPeptides = minPeptides, minDeltaScore = minDeltaScore, - minPSMs = minPSMs, masterProteinsOnly = masterProteinsOnly, - modificationsCol = modificationsCol, - excludeUnmodifiedPeptides = FALSE, - keepModifications = NULL, plotUpset = TRUE, - exclFile = sub("\\.Rmd$", paste0("_filtered_out_features.txt"), - knitr::current_input(dir = TRUE))) -} else if (expType == "Spectronaut") { - sce <- filterSpectronaut(sce = sce, minScore = minScore, - minPeptides = minPeptides, plotUpset = TRUE, - revPattern = "_Decoy$", - contamPattern = "^contam_", - exclFile = sub("\\.Rmd$", paste0("_filtered_out_features.txt"), - knitr::current_input(dir = TRUE))) -} else if (expType == "DIANN") { - sce <- sce -} else { - stop("Unknown 'expType'") -} +sce <- filterFeaturesSE(sce = sce, filtersSE = filtersSE, plotUpset = TRUE, + exclFile = sub("\\.Rmd$", paste0("_filtered_out_features.txt"), + knitr::current_input(dir = TRUE))) nbrFeaturesAfter <- nrow(sce) if (nbrFeaturesAfter == 0) { @@ -578,14 +541,31 @@ cat(filterByModText(excludeUnmodifiedPeptides = excludeUnmodifiedPeptides, ```{r filter-by-mod} if (expType == "ProteomeDiscoverer" && inputLevel == "PeptideGroups") { ## Filter by modification type - sce <- filterPDTMT(sce = sce, inputLevel = inputLevel, minScore = minScore, - minPeptides = minPeptides, minDeltaScore = minDeltaScore, - minPSMs = minPSMs, masterProteinsOnly = masterProteinsOnly, - modificationsCol = modificationsCol, - excludeUnmodifiedPeptides = excludeUnmodifiedPeptides, - keepModifications = keepModifications, plotUpset = TRUE, - exclFile = sub("\\.Rmd$", paste0("_filtered_out_features_modifications.txt"), - knitr::current_input(dir = TRUE))) + sce <- filterFeaturesSE( + sce = sce, + filtersSE = list( + ExclUnmod = function(se) { + if (modificationsCol %in% colnames(rowData(sce)) && + excludeUnmodifiedPeptides) { + rowData(sce)[[modificationsCol]] != "" & + !is.na(rowData(sce)[[modificationsCol]]) + } else { + rep(TRUE, nrow(sce)) + } + }, + KeepMods = function(se) { + if (modificationsCol %in% colnames(rowData(sce)) && + !is.null(keepModifications)) { + grepl(keepModifications, rowData(sce)[[modificationsCol]]) & + !is.na(rowData(sce)[[modificationsCol]]) + } else { + rep(TRUE, nrow(sce)) + } + } + ) + plotUpset = TRUE, + exclFile = sub("\\.Rmd$", paste0("_filtered_out_features_modifications.txt"), + knitr::current_input(dir = TRUE))) } ``` From df905d4853e9a3dcecab0743f2ff2234099d8104 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 17:00:48 +0100 Subject: [PATCH 14/65] Add test for no filtering --- tests/testthat/test-doFilter.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/testthat/test-doFilter.R b/tests/testthat/test-doFilter.R index 83e18fe..4b1abb0 100644 --- a/tests/testthat/test-doFilter.R +++ b/tests/testthat/test-doFilter.R @@ -609,4 +609,9 @@ test_that("filtering works (FragPipe)", { !grepl("^contam_", rownames(sce_fp_final)) ))) expect_equal(nrow(out), 113L) + + ## Empty filter list -> no filtering + out <- filterFeaturesSE(sce_fp_final, filtersSE = list(), plotUpset = TRUE, + exclFile = NULL) + expect_identical(sce_fp_final, out) }) From 033b872489f366d3ef0d2f3c6dfec14252be739a Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 18:58:59 +0100 Subject: [PATCH 15/65] Update tests --- tests/testthat/test-checkArgumentsDIANN.R | 32 ++++++---- tests/testthat/test-checkArgumentsFragPipe.R | 27 ++++----- tests/testthat/test-checkArgumentsMaxQuant.R | 25 +++----- tests/testthat/test-checkArgumentsPDTMT.R | 63 +++----------------- tests/testthat/test-runDIANNAnalysis.R | 34 ++++++----- tests/testthat/test-runFragPipeAnalysis.R | 27 ++++----- tests/testthat/test-runMaxQuantAnalysis.R | 27 ++++----- tests/testthat/test-runPDTMTAnalysis.R | 57 ++++-------------- 8 files changed, 96 insertions(+), 196 deletions(-) diff --git a/tests/testthat/test-checkArgumentsDIANN.R b/tests/testthat/test-checkArgumentsDIANN.R index d960291..3d1d3b5 100644 --- a/tests/testthat/test-checkArgumentsDIANN.R +++ b/tests/testthat/test-checkArgumentsDIANN.R @@ -34,8 +34,8 @@ test_that("argument checking for DIANN works", { group = c("A", "A", "A", "B", "B", "B")), includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, + filtersDF = list(), + filtersSE = list(), imputeMethod = "MinProb", assaysForExport = NULL, addAbundanceValues = TRUE, @@ -307,23 +307,29 @@ test_that("argument checking for DIANN works", { expect_error(do.call(.checkArgumentsDIANN, args), "Please specify max one of includeOnlySamples") - ## minScore + ## filtersDF args <- args0 - args$minScore <- "1" + args$filtersDF <- "1" expect_error(do.call(.checkArgumentsDIANN, args), - "'minScore' must be of class 'numeric'") - args$minScore <- c(1, 2) + "'filtersDF' must be of class 'list'") + args$filtersDF <- list(function(x) x) expect_error(do.call(.checkArgumentsDIANN, args), - "'minScore' must have length 1") + "'namesfiltersDF' must not be NULL") + args$filtersDF <- list(f1 = function(x, y) x + y) + expect_error(do.call(.checkArgumentsDIANN, args), + "is not TRUE") - ## minPeptides + ## filtersSE args <- args0 - args$minPeptides <- "1" + args$filtersSE <- "1" + expect_error(do.call(.checkArgumentsDIANN, args), + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) expect_error(do.call(.checkArgumentsDIANN, args), - "'minPeptides' must be of class 'numeric'") - args$minPeptides <- c(1, 2) + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) expect_error(do.call(.checkArgumentsDIANN, args), - "'minPeptides' must have length 1") + "is not TRUE") ## imputeMethod args <- args0 @@ -351,7 +357,7 @@ test_that("argument checking for DIANN works", { args$addAbundanceValues <- c(TRUE, FALSE) expect_error(do.call(.checkArgumentsDIANN, args), "'addAbundanceValues' must have length 1") - + ## addHeatmaps args <- args0 args$addHeatmaps <- 1 diff --git a/tests/testthat/test-checkArgumentsFragPipe.R b/tests/testthat/test-checkArgumentsFragPipe.R index 81ec684..48b94a6 100644 --- a/tests/testthat/test-checkArgumentsFragPipe.R +++ b/tests/testthat/test-checkArgumentsFragPipe.R @@ -31,11 +31,10 @@ test_that("argument checking for FP works", { "Chd4BF", "RBC_ctrl", "RBC_ctrl", "RBC_ctrl")), includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, + filtersSE = einprotFragPipeFilters, imputeMethod = "MinProb", assaysForExport = NULL, - addAbundanceValues = TRUE, + addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), @@ -299,23 +298,17 @@ test_that("argument checking for FP works", { expect_error(do.call(.checkArgumentsFragPipe, args), "Please specify max one of includeOnlySamples") - ## minScore + ## filtersSE args <- args0 - args$minScore <- "1" + args$filtersSE <- "1" expect_error(do.call(.checkArgumentsFragPipe, args), - "'minScore' must be of class 'numeric'") - args$minScore <- c(1, 2) + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) expect_error(do.call(.checkArgumentsFragPipe, args), - "'minScore' must have length 1") - - ## minPeptides - args <- args0 - args$minPeptides <- "1" + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) expect_error(do.call(.checkArgumentsFragPipe, args), - "'minPeptides' must be of class 'numeric'") - args$minPeptides <- c(1, 2) - expect_error(do.call(.checkArgumentsFragPipe, args), - "'minPeptides' must have length 1") + "is not TRUE") ## imputeMethod args <- args0 @@ -343,7 +336,7 @@ test_that("argument checking for FP works", { args$addAbundanceValues <- c(TRUE, FALSE) expect_error(do.call(.checkArgumentsFragPipe, args), "'addAbundanceValues' must have length 1") - + ## addHeatmaps args <- args0 args$addHeatmaps <- 1 diff --git a/tests/testthat/test-checkArgumentsMaxQuant.R b/tests/testthat/test-checkArgumentsMaxQuant.R index 667b878..cb6d8b1 100644 --- a/tests/testthat/test-checkArgumentsMaxQuant.R +++ b/tests/testthat/test-checkArgumentsMaxQuant.R @@ -39,8 +39,7 @@ test_that("argument checking for MQ works", { "Chd4BF", "RBC_ctrl", "RBC_ctrl", "RBC_ctrl")), includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, + filtersSE = einprotMQFilters, imputeMethod = "MinProb", assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, @@ -298,23 +297,17 @@ test_that("argument checking for MQ works", { expect_error(do.call(.checkArgumentsMaxQuant, args), "Please specify max one of includeOnlySamples") - ## minScore + ## filtersSE args <- args0 - args$minScore <- "1" + args$filtersSE <- "1" expect_error(do.call(.checkArgumentsMaxQuant, args), - "'minScore' must be of class 'numeric'") - args$minScore <- c(1, 2) + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) expect_error(do.call(.checkArgumentsMaxQuant, args), - "'minScore' must have length 1") - - ## minPeptides - args <- args0 - args$minPeptides <- "1" + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) expect_error(do.call(.checkArgumentsMaxQuant, args), - "'minPeptides' must be of class 'numeric'") - args$minPeptides <- c(1, 2) - expect_error(do.call(.checkArgumentsMaxQuant, args), - "'minPeptides' must have length 1") + "is not TRUE") ## imputeMethod args <- args0 @@ -342,7 +335,7 @@ test_that("argument checking for MQ works", { args$addAbundanceValues <- c(TRUE, FALSE) expect_error(do.call(.checkArgumentsMaxQuant, args), "'addAbundanceValues' must have length 1") - + ## addHeatmaps args <- args0 args$addHeatmaps <- 1 diff --git a/tests/testthat/test-checkArgumentsPDTMT.R b/tests/testthat/test-checkArgumentsPDTMT.R index e17f7c3..e33cfc0 100644 --- a/tests/testthat/test-checkArgumentsPDTMT.R +++ b/tests/testthat/test-checkArgumentsPDTMT.R @@ -45,11 +45,7 @@ test_that("argument checking for PD-TMT works", { rep("WT", 4))), includeOnlySamples = "", excludeSamples = "", - minScore = 2, - minDeltaScore = 0.2, - minPeptides = 2, - minPSMs = 2, - masterProteinsOnly = FALSE, + filtersSE = einprotPDTMTProteinFilters, imputeMethod = "MinProb", assaysForExport = NULL, addAbundanceValues = TRUE, @@ -374,60 +370,17 @@ test_that("argument checking for PD-TMT works", { expect_error(do.call(.checkArgumentsPDTMT, args), "Please specify max one of includeOnlySamples") - ## minScore/minDeltaScore + ## filtersSE args <- args0 - args$minScore <- "1" + args$filtersSE <- "1" expect_error(do.call(.checkArgumentsPDTMT, args), - "'minScore' must be of class 'numeric'") - args$minScore <- c(1, 2) + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) expect_error(do.call(.checkArgumentsPDTMT, args), - "'minScore' must have length 1") - args$inputLevel <- "PeptideGroups" - expect_null(do.call(.checkArgumentsPDTMT, args)) - - args <- args0 - args$inputLevel <- "PeptideGroups" - args$minDeltaScore <- "1" - expect_error(do.call(.checkArgumentsPDTMT, args), - "'minDeltaScore' must be of class 'numeric'") - args$minDeltaScore <- c(1, 2) + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) expect_error(do.call(.checkArgumentsPDTMT, args), - "'minDeltaScore' must have length 1") - args$inputLevel <- "Proteins" - expect_null(do.call(.checkArgumentsPDTMT, args)) - - ## minPeptides/minPSMs - args <- args0 - args$minPeptides <- "1" - expect_error(do.call(.checkArgumentsPDTMT, args), - "'minPeptides' must be of class 'numeric'") - args$minPeptides <- c(1, 2) - expect_error(do.call(.checkArgumentsPDTMT, args), - "'minPeptides' must have length 1") - args$inputLevel <- "PeptideGroups" - expect_null(do.call(.checkArgumentsPDTMT, args)) - - args <- args0 - args$inputLevel <- "PeptideGroups" - args$minPSMs <- "1" - expect_error(do.call(.checkArgumentsPDTMT, args), - "'minPSMs' must be of class 'numeric'") - args$minPSMs <- c(1, 2) - expect_error(do.call(.checkArgumentsPDTMT, args), - "'minPSMs' must have length 1") - args$inputLevel <- "Proteins" - expect_null(do.call(.checkArgumentsPDTMT, args)) - - ## masterProteinsOnly - args <- args0 - args$masterProteinsOnly <- 1 - expect_error(do.call(.checkArgumentsPDTMT, args), - "'masterProteinsOnly' must be of class 'logical'") - args$masterProteinsOnly <- c(TRUE, FALSE) - expect_error(do.call(.checkArgumentsPDTMT, args), - "'masterProteinsOnly' must have length 1") - args$inputLevel <- "PeptideGroups" - expect_null(do.call(.checkArgumentsPDTMT, args)) + "is not TRUE") ## imputeMethod args <- args0 diff --git a/tests/testthat/test-runDIANNAnalysis.R b/tests/testthat/test-runDIANNAnalysis.R index e5f6091..f127373 100644 --- a/tests/testthat/test-runDIANNAnalysis.R +++ b/tests/testthat/test-runDIANNAnalysis.R @@ -35,11 +35,11 @@ test_that("runDIANNAnalysis works", { group = c("A", "A", "A", "B", "B", "B")), includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, + filtersDF = list(), + filtersSE = list(), imputeMethod = "MinProb", assaysForExport = NULL, - addAbundanceValues = TRUE, + addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), @@ -300,23 +300,29 @@ test_that("runDIANNAnalysis works", { expect_error(do.call(runDIANNAnalysis, args), "Please specify max one of includeOnlySamples") - ## minScore + ## filtersDF args <- args0 - args$minScore <- "1" + args$filtersDF <- "1" expect_error(do.call(runDIANNAnalysis, args), - "'minScore' must be of class 'numeric'") - args$minScore <- c(1, 2) + "'filtersDF' must be of class 'list'") + args$filtersDF <- list(function(x) x) expect_error(do.call(runDIANNAnalysis, args), - "'minScore' must have length 1") + "'namesfiltersDF' must not be NULL") + args$filtersDF <- list(f1 = function(x, y) x + y) + expect_error(do.call(runDIANNAnalysis, args), + "is not TRUE") - ## minPeptides + ## filtersSE args <- args0 - args$minPeptides <- "1" + args$filtersSE <- "1" + expect_error(do.call(runDIANNAnalysis, args), + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) expect_error(do.call(runDIANNAnalysis, args), - "'minPeptides' must be of class 'numeric'") - args$minPeptides <- c(1, 2) + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) expect_error(do.call(runDIANNAnalysis, args), - "'minPeptides' must have length 1") + "is not TRUE") ## imputeMethod args <- args0 @@ -344,7 +350,7 @@ test_that("runDIANNAnalysis works", { args$addAbundanceValues <- c(TRUE, FALSE) expect_error(do.call(runDIANNAnalysis, args), "'addAbundanceValues' must have length 1") - + ## addHeatmaps args <- args0 args$addHeatmaps <- 1 diff --git a/tests/testthat/test-runFragPipeAnalysis.R b/tests/testthat/test-runFragPipeAnalysis.R index 83421d1..3eec537 100644 --- a/tests/testthat/test-runFragPipeAnalysis.R +++ b/tests/testthat/test-runFragPipeAnalysis.R @@ -33,11 +33,10 @@ test_that("runFragPipeAnalysis works", { "Chd4BF", "RBC_ctrl", "RBC_ctrl", "RBC_ctrl")), includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, + filtersSE = einprotFragPipeFilters, imputeMethod = "MinProb", assaysForExport = NULL, - addAbundanceValues = TRUE, + addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), @@ -263,23 +262,17 @@ test_that("runFragPipeAnalysis works", { expect_error(do.call(runFragPipeAnalysis, args), "Please specify max one of includeOnlySamples") - ## minScore + ## filtersSE args <- args0 - args$minScore <- "1" + args$filtersSE <- "1" expect_error(do.call(runFragPipeAnalysis, args), - "'minScore' must be of class 'numeric'") - args$minScore <- c(1, 2) + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) expect_error(do.call(runFragPipeAnalysis, args), - "'minScore' must have length 1") - - ## minPeptides - args <- args0 - args$minPeptides <- "1" + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) expect_error(do.call(runFragPipeAnalysis, args), - "'minPeptides' must be of class 'numeric'") - args$minPeptides <- c(1, 2) - expect_error(do.call(runFragPipeAnalysis, args), - "'minPeptides' must have length 1") + "is not TRUE") ## imputeMethod args <- args0 @@ -307,7 +300,7 @@ test_that("runFragPipeAnalysis works", { args$addAbundanceValues <- c(TRUE, FALSE) expect_error(do.call(runFragPipeAnalysis, args), "'addAbundanceValues' must have length 1") - + ## addHeatmaps args <- args0 args$addHeatmaps <- 1 diff --git a/tests/testthat/test-runMaxQuantAnalysis.R b/tests/testthat/test-runMaxQuantAnalysis.R index 1452654..17070f0 100644 --- a/tests/testthat/test-runMaxQuantAnalysis.R +++ b/tests/testthat/test-runMaxQuantAnalysis.R @@ -37,11 +37,10 @@ test_that("runMaxQuantAnalysis works", { "Chd4BF", "RBC_ctrl", "RBC_ctrl", "RBC_ctrl")), includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minPeptides = 2, + filtersSE = einprotMQFilters, imputeMethod = "MinProb", assaysForExport = c("iBAQ", "Top3"), - addAbundanceValues = TRUE, + addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), @@ -288,23 +287,17 @@ test_that("runMaxQuantAnalysis works", { expect_error(do.call(runMaxQuantAnalysis, args), "Please specify max one of includeOnlySamples") - ## minScore + ## filtersSE args <- args0 - args$minScore <- "1" + args$filtersSE <- "1" expect_error(do.call(runMaxQuantAnalysis, args), - "'minScore' must be of class 'numeric'") - args$minScore <- c(1, 2) + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) expect_error(do.call(runMaxQuantAnalysis, args), - "'minScore' must have length 1") - - ## minPeptides - args <- args0 - args$minPeptides <- "1" + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) expect_error(do.call(runMaxQuantAnalysis, args), - "'minPeptides' must be of class 'numeric'") - args$minPeptides <- c(1, 2) - expect_error(do.call(runMaxQuantAnalysis, args), - "'minPeptides' must have length 1") + "is not TRUE") ## imputeMethod args <- args0 @@ -332,7 +325,7 @@ test_that("runMaxQuantAnalysis works", { args$addAbundanceValues <- c(TRUE, FALSE) expect_error(do.call(runMaxQuantAnalysis, args), "'addAbundanceValues' must have length 1") - + ## addHeatmaps args <- args0 args$addHeatmaps <- 1 diff --git a/tests/testthat/test-runPDTMTAnalysis.R b/tests/testthat/test-runPDTMTAnalysis.R index eec08dc..14b3407 100644 --- a/tests/testthat/test-runPDTMTAnalysis.R +++ b/tests/testthat/test-runPDTMTAnalysis.R @@ -44,14 +44,10 @@ test_that("runPDTMTAnalysis works", { rep("WT", 4))), includeOnlySamples = "", excludeSamples = "", - minScore = 10, - minDeltaScore = 0.2, - minPeptides = 2, - minPSMs = 2, - masterProteinsOnly = FALSE, + filtersSE = einprotPDTMTProteinFilters, imputeMethod = "MinProb", assaysForExport = NULL, - addAbundanceValues = TRUE, + addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), @@ -354,50 +350,17 @@ test_that("runPDTMTAnalysis works", { expect_error(do.call(runPDTMTAnalysis, args), "Please specify max one of includeOnlySamples") - ## minScore/minDeltaScore + ## filtersSE args <- args0 - args$minScore <- "1" + args$filtersSE <- "1" expect_error(do.call(runPDTMTAnalysis, args), - "'minScore' must be of class 'numeric'") - args$minScore <- c(1, 2) + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) expect_error(do.call(runPDTMTAnalysis, args), - "'minScore' must have length 1") - - args <- args0 - args$inputLevel <- "PeptideGroups" - args$minDeltaScore <- "1" - expect_error(do.call(runPDTMTAnalysis, args), - "'minDeltaScore' must be of class 'numeric'") - args$minDeltaScore <- c(1, 2) - expect_error(do.call(runPDTMTAnalysis, args), - "'minDeltaScore' must have length 1") - - ## minPeptides/minPSMs - args <- args0 - args$minPeptides <- "1" - expect_error(do.call(runPDTMTAnalysis, args), - "'minPeptides' must be of class 'numeric'") - args$minPeptides <- c(1, 2) - expect_error(do.call(runPDTMTAnalysis, args), - "'minPeptides' must have length 1") - - args <- args0 - args$inputLevel <- "PeptideGroups" - args$minPSMs <- "1" + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) expect_error(do.call(runPDTMTAnalysis, args), - "'minPSMs' must be of class 'numeric'") - args$minPSMs <- c(1, 2) - expect_error(do.call(runPDTMTAnalysis, args), - "'minPSMs' must have length 1") - - ## masterProteinsOnly - args <- args0 - args$masterProteinsOnly <- 1 - expect_error(do.call(runPDTMTAnalysis, args), - "'masterProteinsOnly' must be of class 'logical'") - args$masterProteinsOnly <- c(TRUE, FALSE) - expect_error(do.call(runPDTMTAnalysis, args), - "'masterProteinsOnly' must have length 1") + "is not TRUE") ## imputeMethod args <- args0 @@ -425,7 +388,7 @@ test_that("runPDTMTAnalysis works", { args$addAbundanceValues <- c(TRUE, FALSE) expect_error(do.call(runPDTMTAnalysis, args), "'addAbundanceValues' must have length 1") - + ## addHeatmaps args <- args0 args$addHeatmaps <- 1 From 6f7d9236401d3c122d40f8d812e163d09649e1b4 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 18:59:13 +0100 Subject: [PATCH 16/65] Update NEWS --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index cbaccdb..b1dbf2c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# einprot 0.10.0 + +* Breaking change: redesign the feature filtering framework to make it more flexible and consistent across tools + # einprot 0.9.8 * Expose argument to include or exclude abundance values from result tables From 6fee918bfa14139093ecf7e53a30348b31da56c9 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 19:42:28 +0100 Subject: [PATCH 17/65] Add p-value histograms to pdfs with test result plots --- NEWS.md | 1 + R/plotVolcano.R | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/NEWS.md b/NEWS.md index b1dbf2c..f49e98c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,7 @@ # einprot 0.10.0 * Breaking change: redesign the feature filtering framework to make it more flexible and consistent across tools +* Add p-value histograms to pdfs with test result plots # einprot 0.9.8 diff --git a/R/plotVolcano.R b/R/plotVolcano.R index 9412af1..e0160f7 100644 --- a/R/plotVolcano.R +++ b/R/plotVolcano.R @@ -157,6 +157,27 @@ getComplexesToPlot <- function(featureCollections, ggbar } +#' @author Charlotte Soneson +#' @noRd +#' @keywords internal +#' @importFrom ggplot2 ggplot aes geom_histogram theme_bw labs +#' +.makePvalueHistogram <- function(res, xv = "PValue", xlab = "PValue", + title = "", nbins = 20) { + .assertVector(x = res, type = "data.frame") + .assertScalar(x = xv, type = "character", validValues = colnames(res)) + .assertScalar(x = nbins, type = "numeric") + .assertVector(x = res[[xv]], type = "numeric", rngIncl = c(0, 1)) + + ggplot2::ggplot(res, ggplot2::aes(x = .data[[xv]])) + + ggplot2::geom_histogram(fill = "lightgrey", color = "grey", + binwidth = 1 / nbins, boundary = 0, + closed = "left") + + ggplot2::theme_bw() + + ggplot2::labs(x = xlab, title = title) +} + + #' @author Charlotte Soneson #' @noRd #' @keywords internal @@ -290,6 +311,7 @@ getComplexesToPlot <- function(featureCollections, xvma <- "AveExpr" apv <- "adj.P.Val" tv <- NULL + pv <- "P.Value" volcind <- "showInVolcano" } else if (testType == "ttest") { xv <- "logFC" @@ -297,6 +319,7 @@ getComplexesToPlot <- function(featureCollections, xvma <- NULL apv <- "adj.P.Val" tv <- "sam" + pv <- "P.Value" volcind <- "showInVolcano" } else if (testType == "proDA") { xv <- "logFC" @@ -304,6 +327,7 @@ getComplexesToPlot <- function(featureCollections, xvma <- NULL apv <- "adj.P.Val" tv <- NULL + pv <- "P.Value" volcind <- "showInVolcano" } else if (testType == "welch") { ## PTM test (limma/welch) @@ -312,6 +336,7 @@ getComplexesToPlot <- function(featureCollections, xvma <- NULL apv <- "adj.P.Val" tv <- NULL + pv <- "P.Value" volcind <- "showInVolcano" } list(xv = xv, yv = yv, xvma = xvma, apv = apv, volcind = volcind, tv = tv) @@ -719,6 +744,12 @@ plotVolcano <- function(sce, res, testType, xv = NULL, yv = NULL, xvma = NULL, ggwf <- NULL } + ## ------------------------------------------------------------------------- + ## p-value histogram + ## ------------------------------------------------------------------------- + ggp <- .makePvalueHistogram(res = res, xv = cols$pv, xlab = "PValue", + title = plottitle, nbins = 20) + ## ------------------------------------------------------------------------- ## Bar plot of significant features ## ------------------------------------------------------------------------- @@ -779,6 +810,7 @@ plotVolcano <- function(sce, res, testType, xv = NULL, yv = NULL, xvma = NULL, if (!is.null(ggwf)) { print(ggwf) } + print(ggp) grDevices::dev.off() } From 5edde4d7bb6902ff847140a3bbee187a1b0c2e50 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 31 Oct 2025 20:02:51 +0100 Subject: [PATCH 18/65] Capture MSBooster information in readFragPipeInfo --- R/readFragPipeInfo.R | 13 ++++++++++++- tests/testthat/test-readFragPipeInfo.R | 19 ++++++++++++------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/R/readFragPipeInfo.R b/R/readFragPipeInfo.R index 6c1fdf8..472d29e 100755 --- a/R/readFragPipeInfo.R +++ b/R/readFragPipeInfo.R @@ -224,6 +224,16 @@ readFragPipeInfo <- function(fragpipeDir) { # fpContaminants <- NULL } + ## ------------------------------------------------------------------------- + ## MSBooster + ## ------------------------------------------------------------------------- + if (!is.null(dataDf)) { + fpBooster <- dataDf$value[which(dataDf$parameter == + "msbooster.run-msbooster")] + } else { + fpBooster <- NULL + } + ## ------------------------------------------------------------------------- ## Masses ## ------------------------------------------------------------------------- @@ -362,6 +372,7 @@ readFragPipeInfo <- function(fragpipeDir) { "Enzymes" = fpEnzymes, "Variable modifications" = fpVariableModifications, "Fixed modifications" = fpFixedModifications, - "Database decoy tag" = fpDecoyTag) + "Database decoy tag" = fpDecoyTag, + "MSBooster" = fpBooster) l[vapply(l, length, 0) > 0] } diff --git a/tests/testthat/test-readFragPipeInfo.R b/tests/testthat/test-readFragPipeInfo.R index bf7bcde..eebd30f 100644 --- a/tests/testthat/test-readFragPipeInfo.R +++ b/tests/testthat/test-readFragPipeInfo.R @@ -13,14 +13,15 @@ test_that("readFragPipeInfo works", { system.file("extdata", "fp_example", package = "einprot")) expect_type(fp, "list") - expect_equal(length(fp), 15) + expect_equal(length(fp), 16) expect_named(fp, c("FragPipe version", "FragPipe parameter file", "FragPipe log file", "Search engine", "Raw file location", "Raw files", "Sample names", "Databases", "Peptides (ranges)", "Mass error tolerances", "Quantification settings (LFQ)", "Enzymes", "Variable modifications", - "Fixed modifications", "Database decoy tag")) + "Fixed modifications", "Database decoy tag", + "MSBooster")) ## All entries should be scalar values expect_true(all(vapply(fp, length, 0) == 1)) @@ -42,6 +43,7 @@ test_that("readFragPipeInfo works", { expect_equal(fp$`Variable modifications`, "M(15.9949), N-term(42.0106)") expect_equal(fp$`Fixed modifications`, "C(57.0215)") expect_equal(fp$`Database decoy tag`, "rev_") + expect_equal(fp$MSBooster, "false") ## Workflow file missing -> get values from log file ## ------------------------------------------------------------------------- @@ -51,14 +53,15 @@ test_that("readFragPipeInfo works", { to = file.path(tempdir(), "tempfp")) fp <- readFragPipeInfo(file.path(tempdir(), "tempfp")) expect_type(fp, "list") - expect_equal(length(fp), 14) + expect_equal(length(fp), 15) expect_named(fp, c("FragPipe version", "FragPipe log file", "Search engine", "Raw file location", "Raw files", "Sample names", "Databases", "Peptides (ranges)", "Mass error tolerances", "Quantification settings (LFQ)", "Enzymes", "Variable modifications", - "Fixed modifications", "Database decoy tag")) + "Fixed modifications", "Database decoy tag", + "MSBooster")) ## All entries should be scalar values expect_true(all(vapply(fp, length, 0) == 1)) @@ -79,6 +82,7 @@ test_that("readFragPipeInfo works", { expect_equal(fp$`Variable modifications`, "M(15.9949), N-term(42.0106)") expect_equal(fp$`Fixed modifications`, "C(57.0215)") expect_equal(fp$`Database decoy tag`, "rev_") + expect_equal(fp$MSBooster, "false") ## ------------------------------------------------------------------------- ## Create file where msfragger.search_enzyme_name_2 is not null @@ -93,14 +97,15 @@ test_that("readFragPipeInfo works", { writeLines(tmp, file.path(tempdir(), "tempfp", "fragpipe.workflow")) fp <- readFragPipeInfo(file.path(tempdir(), "tempfp")) expect_type(fp, "list") - expect_equal(length(fp), 15) + expect_equal(length(fp), 16) expect_named(fp, c("FragPipe version", "FragPipe parameter file", "FragPipe log file", "Search engine", "Raw file location", "Raw files", "Sample names", "Databases", "Peptides (ranges)", "Mass error tolerances", "Quantification settings (LFQ)", "Enzymes", "Variable modifications", - "Fixed modifications", "Database decoy tag")) + "Fixed modifications", "Database decoy tag", + "MSBooster")) ## All entries should be scalar values expect_true(all(vapply(fp, length, 0) == 1)) @@ -122,5 +127,5 @@ test_that("readFragPipeInfo works", { expect_equal(fp$`Variable modifications`, "M(15.9949), N-term(42.0106)") expect_equal(fp$`Fixed modifications`, "C(57.0215)") expect_equal(fp$`Database decoy tag`, "rev_") - + expect_equal(fp$MSBooster, "false") }) From 9f81299531c6f68241ca0084b44b1f9ea254f868 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sat, 1 Nov 2025 12:31:56 +0100 Subject: [PATCH 19/65] Bug fixes to p-value histogram plotting --- R/plotVolcano.R | 17 +++++++++++++---- inst/extdata/process_basic_template.Rmd | 8 ++++---- man/plotVolcano.Rd | 5 +++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/R/plotVolcano.R b/R/plotVolcano.R index e0160f7..955e800 100644 --- a/R/plotVolcano.R +++ b/R/plotVolcano.R @@ -162,12 +162,13 @@ getComplexesToPlot <- function(featureCollections, #' @keywords internal #' @importFrom ggplot2 ggplot aes geom_histogram theme_bw labs #' -.makePvalueHistogram <- function(res, xv = "PValue", xlab = "PValue", +.makePvalueHistogram <- function(res, xv = "P.Value", xlab = "PValue", title = "", nbins = 20) { .assertVector(x = res, type = "data.frame") .assertScalar(x = xv, type = "character", validValues = colnames(res)) .assertScalar(x = nbins, type = "numeric") - .assertVector(x = res[[xv]], type = "numeric", rngIncl = c(0, 1)) + .assertVector(x = res[[xv]][!is.na(res[[xv]])], type = "numeric", + rngIncl = c(0, 1)) ggplot2::ggplot(res, ggplot2::aes(x = .data[[xv]])) + ggplot2::geom_histogram(fill = "lightgrey", color = "grey", @@ -339,7 +340,8 @@ getComplexesToPlot <- function(featureCollections, pv <- "P.Value" volcind <- "showInVolcano" } - list(xv = xv, yv = yv, xvma = xvma, apv = apv, volcind = volcind, tv = tv) + list(xv = xv, yv = yv, xvma = xvma, apv = apv, volcind = volcind, tv = tv, + pv = pv) } #' Make volcano plots @@ -364,6 +366,9 @@ getComplexesToPlot <- function(featureCollections, #' column of \code{res} should be used as the x-axis for an MA plot. The #' y-axis column will be \code{xv}. If \code{NULL}, no MA plot is #' generated. +#' @param pv If not \code{NULL}, a character scalar indicating which column +#' of \code{res} should be used to create p-value histograms. If +#' \code{NULL}, will be determined based on \code{testType}. #' @param volcind Character scalar indicating which column in \code{res} that #' represents the "significance" column. This should be a logical #' column; rows with a value equal to \code{TRUE} will be colored @@ -459,7 +464,7 @@ getComplexesToPlot <- function(featureCollections, #' @importFrom grDevices pdf dev.off #' plotVolcano <- function(sce, res, testType, xv = NULL, yv = NULL, xvma = NULL, - volcind = NULL, plotnote = "", plottitle = "", + pv = NULL, volcind = NULL, plotnote = "", plottitle = "", plotsubtitle = "", volcanoFeaturesToLabel = c(""), volcanoMaxFeatures = 25, volcanoLabelSign = "both", baseFileName = NULL, @@ -508,11 +513,15 @@ plotVolcano <- function(sce, res, testType, xv = NULL, yv = NULL, xvma = NULL, if (!is.null(volcind)) { cols$volcind <- volcind } + if (!is.null(pv)) { + cols$pv <- pv + } .assertScalar(x = cols$xv, type = "character", validValues = colnames(res)) .assertScalar(x = cols$yv, type = "character", validValues = colnames(res)) .assertScalar(x = cols$xvma, type = "character", allowNULL = TRUE, validValues = colnames(res)) + .assertScalar(x = cols$pv, type = "character", validValues = colnames(res)) .assertScalar(x = cols$volcind, type = "character", validValues = colnames(res)) .assertScalar(x = plotnote, type = "character") diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index 66f3928..8ce1ae9 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -185,7 +185,7 @@ settingsList <- list( "Include only samples (if applicable)" = paste(includeOnlySamples, collapse = ", "), "Exclude samples" = paste(excludeSamples, collapse = ", "), - "Filter names (SE)" = names(filtersSE), + "Filter names (SE)" = paste(names(filtersSE), collapse = ", "), "Imputation method" = imputeMethod, "Assays(s) to use for exported values" = paste(assaysForExport, collapse = ", "), "Add abundance values" = addAbundanceValues, @@ -263,7 +263,7 @@ if (expType == "ProteomeDiscoverer") { } if (expType %in% c("Spectronaut", "DIANN")) { settingsList <- c(settingsList, - list("Filter names (DF)" = names(filtersDF))) + list("Filter names (DF)" = paste(names(filtersDF), collapse = ", "))) } makeTableFromList(settingsList) @@ -562,7 +562,7 @@ if (expType == "ProteomeDiscoverer" && inputLevel == "PeptideGroups") { rep(TRUE, nrow(sce)) } } - ) + ), plotUpset = TRUE, exclFile = sub("\\.Rmd$", paste0("_filtered_out_features_modifications.txt"), knitr::current_input(dir = TRUE))) @@ -940,7 +940,7 @@ interactiveVolcanos <- htmltools::tagList() for (nm in names(testres$tests)) { plots <- plotVolcano( sce = sce, res = testres$tests[[nm]], testType = stattest, - xv = NULL, yv = NULL, xvma = NULL, volcind = NULL, + xv = NULL, yv = NULL, xvma = NULL, volcind = NULL, pv = NULL, plotnote = testres$plotnotes[[nm]], plottitle = testres$plottitles[[nm]], plotsubtitle = testres$plotsubtitles[[nm]], diff --git a/man/plotVolcano.Rd b/man/plotVolcano.Rd index cce60f6..77382c4 100644 --- a/man/plotVolcano.Rd +++ b/man/plotVolcano.Rd @@ -11,6 +11,7 @@ plotVolcano( xv = NULL, yv = NULL, xvma = NULL, + pv = NULL, volcind = NULL, plotnote = "", plottitle = "", @@ -59,6 +60,10 @@ column of \code{res} should be used as the x-axis for an MA plot. The y-axis column will be \code{xv}. If \code{NULL}, no MA plot is generated.} +\item{pv}{If not \code{NULL}, a character scalar indicating which column +of \code{res} should be used to create p-value histograms. If +\code{NULL}, will be determined based on \code{testType}.} + \item{volcind}{Character scalar indicating which column in \code{res} that represents the "significance" column. This should be a logical column; rows with a value equal to \code{TRUE} will be colored From 56dfeabac05342d1ad8a41a84bbf8a6db30ca4d1 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Mon, 3 Nov 2025 17:03:22 +0100 Subject: [PATCH 20/65] Add dependency on R >= 4.1 (pipe) --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index e6925f6..a6f3871 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -17,7 +17,7 @@ Description: The einprot package contains utilities for performing analysis functionality for assembling databases of known protein complexes. License: MIT + file LICENSE Encoding: UTF-8 -Depends: R (>= 4.0) +Depends: R (>= 4.1) Imports: babelgene, circlize, From c5f426e64a4770f5c75e54fd86e4e14b42f11635 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Mon, 3 Nov 2025 17:03:53 +0100 Subject: [PATCH 21/65] Allow parquet format for DIANN also in argument checking --- R/checkArgumentsDIANN.R | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/R/checkArgumentsDIANN.R b/R/checkArgumentsDIANN.R index 47c8099..fe0d1cd 100644 --- a/R/checkArgumentsDIANN.R +++ b/R/checkArgumentsDIANN.R @@ -55,7 +55,8 @@ } .assertScalar(x = diannFileType, type = "character", - validValues = c("pg_matrix", "pr_matrix", "main_report")) + validValues = c("pg_matrix", "pr_matrix", "main_report", + "parquet")) .assertScalar(x = outLevel, type = "character", validValues = c("pg", "pr")) if (diannFileType == "pg_matrix" && outLevel == "pr") { From 568b3064a49e8cdd3f68ee06e6e4e2007891a549 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Mon, 3 Nov 2025 17:04:14 +0100 Subject: [PATCH 22/65] Properly handle situation where DIANN log file is NULL --- R/runDIANNAnalysis.R | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/R/runDIANNAnalysis.R b/R/runDIANNAnalysis.R index febfdc5..1914613 100644 --- a/R/runDIANNAnalysis.R +++ b/R/runDIANNAnalysis.R @@ -218,7 +218,9 @@ runDIANNAnalysis <- function( ## Normalize paths ## ------------------------------------------------------------------------- diannFile <- normalizePath(diannFile) - diannLogFile <- normalizePath(diannLogFile) + if (!is.null(diannLogFile)) { + diannLogFile <- normalizePath(diannLogFile) + } ## ------------------------------------------------------------------------- ## Copy Rmd template and insert arguments From 8a9e1461d2c4eefe1bd3855936df4121ecc246cd Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Mon, 8 Dec 2025 19:06:30 +0100 Subject: [PATCH 23/65] Ignore 'iBAQ.peptides' column if present --- R/getIntensityColumns.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/R/getIntensityColumns.R b/R/getIntensityColumns.R index d4ba11b..abd7cfa 100644 --- a/R/getIntensityColumns.R +++ b/R/getIntensityColumns.R @@ -142,5 +142,10 @@ getIntensityColumns <- function(inFile, iColPattern, "Abundances\\.Grouped\\.Count"), iCols)] } + ## ------------------------------------------------------------------------- + ## Don't consider 'iBAQ.peptides' column + ## ------------------------------------------------------------------------- + iCols <- iCols[!grepl("^iBAQ.peptides$", iCols)] + list(iColsAll = iColsAll, iCols = iCols) } From 17955c0de690e11f0238e4d66c10e6fad0cf0003 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Mon, 8 Dec 2025 19:06:57 +0100 Subject: [PATCH 24/65] Update unit tests (ggplot2 changes) --- tests/testthat/test-plotVolcano.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/testthat/test-plotVolcano.R b/tests/testthat/test-plotVolcano.R index a5e0781..772e6b4 100644 --- a/tests/testthat/test-plotVolcano.R +++ b/tests/testthat/test-plotVolcano.R @@ -1588,7 +1588,7 @@ test_that("volcano plots work", { maxTextWidthBarplot = NULL) }) for (wn in wns) { - expect_match(wn, "containing missing values") + expect_match(wn, "containing") # 'containing non-finite' or 'containing missing' } expect_type(outl, "list") expect_length(outl, 6) @@ -1640,7 +1640,7 @@ test_that("volcano plots work", { maxTextWidthBarplot = NULL) }) for (wn in wns) { - expect_match(wn, "containing missing values") + expect_match(wn, "containing") # 'containing non-finite' or 'containing missing' } expect_type(outl, "list") expect_length(outl, 6) @@ -1692,7 +1692,7 @@ test_that("volcano plots work", { maxTextWidthBarplot = NULL) }) for (wn in wns) { - expect_match(wn, "containing missing values") + expect_match(wn, "containing") # 'containing non-finite' or 'containing missing' } expect_type(outl, "list") expect_length(outl, 6) From 658a23a0146830098688cd2d3ff8500b516b6962 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Thu, 22 Jan 2026 14:47:01 +0100 Subject: [PATCH 25/65] Add genes, Protein.Names and Protein.Ids columns to DIA-NN import (assays and rowData columns) --- R/importDIANN.R | 29 ++++++++++++++++++++++++++--- tests/testthat/test-importDIANN.R | 16 ++++++++++------ 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/R/importDIANN.R b/R/importDIANN.R index c3609cd..59a3a21 100644 --- a/R/importDIANN.R +++ b/R/importDIANN.R @@ -160,13 +160,27 @@ importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", tmp <- dplyr::collect(dplyr::compute(tmp)) if (outLevel == "pg") { + # if Genes and/or Protein.Names columns are not present, add NA + # columns + if (!"Genes" %in% colnames(tmp)) { + tmp$Genes <- NA_character_ + } + if (!"Protein.Names" %in% colnames(tmp)) { + tmp$Protein.Names <- NA_character_ + } tmp <- tmp |> dplyr::select(all_of(c("Run", "Protein.Group", + "Genes", "Protein.Names", "Protein.Ids", aName, grep("^PG\\.", colnames(tmp), value = TRUE)))) tmp <- tmp |> - dplyr::group_by(dplyr::across(c(-"Protein.Ids"))) %>% - dplyr::summarize(Protein.Ids = - paste(unique(unlist(strsplit(.data$Protein.Ids, ";"))), collapse = ";"), + dplyr::group_by(dplyr::across(-c("Protein.Ids", "Genes", "Protein.Names"))) %>% + dplyr::summarize( + Protein.Ids = + paste(unique(unlist(strsplit(.data$Protein.Ids, ";"))), collapse = ";"), + Protein.Names = + paste(unique(unlist(strsplit(.data$Protein.Names, ";"))), collapse = ";"), + Genes = + paste(unique(unlist(strsplit(.data$Genes, ";"))), collapse = ";"), .groups = "drop") |> dplyr::distinct() rd <- DataFrame(Protein.Group = unique(tmp$Protein.Group)) @@ -198,6 +212,15 @@ importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", match(iCols, colnames(tmpsub)), drop = FALSE] } } + ## Add annotation columns corresponding to specific assays + for (aNm in c("Genes", "Protein.Names", "Protein.Ids")) { + if (aNm %in% names(aL)) { + ## Aggregate across columns for each row + rd[[aNm]] <- apply(aL[[aNm]], 1, function(x) { + paste(setdiff(unique(unlist(strsplit(x, ";"))), "0"), collapse = ";") + }) + } + } sce <- SingleCellExperiment::SingleCellExperiment( assays = aL, rowData = rd, diff --git a/tests/testthat/test-importDIANN.R b/tests/testthat/test-importDIANN.R index df2e837..27cf477 100644 --- a/tests/testthat/test-importDIANN.R +++ b/tests/testthat/test-importDIANN.R @@ -136,7 +136,7 @@ test_that("DIANN import works", { expect_equal(sort(rownames(out$sce)), sort(unique(maintmp$Protein.Group))) expect_equal(SummarizedExperiment::assayNames(out$sce), c("PG.MaxLFQ", "PG.Quantity", "PG.Normalised", "PG.Q.Value", - "Protein.Ids")) + "Protein.Ids", "Protein.Names", "Genes")) ## Manual checks aNames <- SummarizedExperiment::assayNames(out$sce) @@ -151,9 +151,13 @@ test_that("DIANN import works", { sub2, ignore_attr = TRUE) } - infoCols <- c("Protein.Group") - expect_true(all(infoCols %in% - colnames(SummarizedExperiment::rowData(out$sce)))) + infoCols <- c("Protein.Group", "Genes", "Protein.Names", "Protein.Ids") + rd <- SummarizedExperiment::rowData(out$sce) + expect_true(all(infoCols %in% colnames(rd))) + expect_identical(unname(rd$Genes[rd$Protein.Group == "O60716"]), "CTNND1") + expect_identical(unname(rd$Protein.Ids[rd$Protein.Group == "O60716"]), + "O60716;A0A669KB05;A0A669KB62;C9JZR2;A0A804HIL4;A0A804HK15;H0YC95") + expect_identical(unname(rd$Protein.Names[rd$Protein.Group == "O60716"]), "CTND1_HUMAN") expect_false(any(grepl("scratch", colnames(out$sce)))) expect_false(any(grepl("mzML", colnames(out$sce)))) @@ -239,7 +243,7 @@ test_that("DIANN import works", { expect_equal(sort(rownames(out$sce)), sort(unique(maintmp$Protein.Group[grep("Condition_A", maintmp$Run)]))) expect_equal(SummarizedExperiment::assayNames(out$sce), c("PG.MaxLFQ", "PG.Quantity", "PG.Normalised", "PG.Q.Value", - "Protein.Ids")) + "Protein.Ids", "Protein.Names", "Genes")) ## Manual checks aNames <- SummarizedExperiment::assayNames(out$sce) @@ -276,7 +280,7 @@ test_that("DIANN import works", { expect_equal(sort(rownames(out$sce)), c("P0A7J3", "P25694", "Q9NVA2")) expect_equal(SummarizedExperiment::assayNames(out$sce), c("PG.MaxLFQ", "PG.Quantity", "PG.Normalised", "PG.Q.Value", - "Protein.Ids")) + "Protein.Ids", "Protein.Names", "Genes")) ## Manual checks aNames <- SummarizedExperiment::assayNames(out$sce) From e5eafbdc32a40d5351f43f364c294092ce4a7945 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 23 Jan 2026 11:02:56 +0100 Subject: [PATCH 26/65] Fix filtering in importDIANN for large parquet files and add missing imports --- NAMESPACE | 7 +++++++ R/importDIANN.R | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/NAMESPACE b/NAMESPACE index e852793..58f5cc5 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -111,6 +111,7 @@ importFrom(SummarizedExperiment,"rowData<-") importFrom(SummarizedExperiment,assay) importFrom(SummarizedExperiment,assayNames) importFrom(SummarizedExperiment,colData) +importFrom(SummarizedExperiment,colnames) importFrom(SummarizedExperiment,rowData) importFrom(XML,xmlParse) importFrom(XML,xmlToList) @@ -125,7 +126,10 @@ importFrom(dplyr,arrange) importFrom(dplyr,between) importFrom(dplyr,bind_cols) importFrom(dplyr,bind_rows) +importFrom(dplyr,collect) +importFrom(dplyr,compute) importFrom(dplyr,contains) +importFrom(dplyr,count) importFrom(dplyr,desc) importFrom(dplyr,distinct) importFrom(dplyr,everything) @@ -133,6 +137,7 @@ importFrom(dplyr,filter) importFrom(dplyr,full_join) importFrom(dplyr,group_by) importFrom(dplyr,if_any) +importFrom(dplyr,inner_join) importFrom(dplyr,last_col) importFrom(dplyr,left_join) importFrom(dplyr,matches) @@ -272,6 +277,7 @@ importFrom(stats,setNames) importFrom(stringdist,amatch) importFrom(stringr,str_extract) importFrom(tibble,rownames_to_column) +importFrom(tibble,tibble) importFrom(tidyr,gather) importFrom(tidyr,pivot_longer) importFrom(tidyr,pivot_wider) @@ -282,6 +288,7 @@ importFrom(tidyr,unite) importFrom(tidyr,unnest) importFrom(tidyselect,any_of) importFrom(tools,file_ext) +importFrom(tools,file_path_sans_ext) importFrom(utils,combn) importFrom(utils,download.file) importFrom(utils,head) diff --git a/R/importDIANN.R b/R/importDIANN.R index 59a3a21..39080e8 100644 --- a/R/importDIANN.R +++ b/R/importDIANN.R @@ -79,6 +79,17 @@ einprotDIANNFiltersDF <- list(Filter = function(df) { #' aName = "MaxLFQ") #' sceL #' +#' @importFrom tibble tibble +#' @importFrom dplyr select filter inner_join collect compute all_of group_by +#' summarize count distinct +#' @importFrom tidyr pivot_wider +#' @importFrom QFeatures readSummarizedExperiment +#' @importFrom SummarizedExperiment assayNames colnames +#' @importFrom SingleCellExperiment SingleCellExperiment +#' @importFrom S4Vectors metadata DataFrame +#' @importFrom tools file_path_sans_ext +#' @importFrom methods as +#' @importFrom utils read.delim importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", includeOnlySamples = "", excludeSamples = "", stopIfEmpty = FALSE, @@ -150,7 +161,13 @@ importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, stopIfEmpty = stopIfEmpty) - tmp <- tmp |> dplyr::filter(Run %in% iCols) + if (fileType == "main_report") { + tmp <- tmp |> dplyr::filter(Run %in% iCols) + } else { + iCols_tbl <- duckplyr::as_duckdb_tibble(tibble(Run = iCols)) + tmp <- tmp |> + dplyr::inner_join(iCols_tbl, by = "Run") + } stopifnot(aName %in% colnames(tmp)) # Filter From 6dd6b4d2d24769dd60c5a88d56cdfa5cf8680e61 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 13 Feb 2026 09:26:42 +0100 Subject: [PATCH 27/65] Bump version --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index a6f3871..8c94591 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: einprot Type: Package Title: A collection of proteomics analysis utilities and workflows -Version: 0.10.0 +Version: 0.10.1 Authors@R: c( person("Charlotte", "Soneson", email = "charlotte.soneson@fmi.ch", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-3833-2169")), From 8723b7e17891bc6bb6531ae21d86792bd9d129dd Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 13 Feb 2026 11:29:56 +0100 Subject: [PATCH 28/65] Add runSEAnalysis function --- NAMESPACE | 1 + R/checkArgumentsSE.R | 198 +++++++++++++++++++++++ R/runSEAnalysis.R | 297 +++++++++++++++++++++++++++++++++++ man/runSEAnalysis.Rd | 362 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 858 insertions(+) create mode 100644 R/checkArgumentsSE.R create mode 100644 R/runSEAnalysis.R create mode 100644 man/runSEAnalysis.Rd diff --git a/NAMESPACE b/NAMESPACE index 58f5cc5..1c8c38f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -76,6 +76,7 @@ export(runMaxQuantAnalysis) export(runPDTMTAnalysis) export(runPDTMTptmAnalysis) export(runPTMTest) +export(runSEAnalysis) export(runSpectronautAnalysis) export(runTest) export(saText) diff --git a/R/checkArgumentsSE.R b/R/checkArgumentsSE.R new file mode 100644 index 0000000..a156043 --- /dev/null +++ b/R/checkArgumentsSE.R @@ -0,0 +1,198 @@ +#' Check validity of arguments for SE analysis +#' +#' @keywords internal +#' @noRd +#' @author Charlotte Soneson +#' +#' @importFrom MsCoreUtils normalizeMethods +#' @importFrom SummarizedExperiment assayNames colData +#' @importFrom tools file_ext +.checkArgumentsSE <- function( + templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, + forceOverwrite, experimentInfo, species, inFile, summaryInfo, aName, + idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, + imputeMethod, assaysForExport, addAbundanceValues, + addHeatmaps, mergeGroups, filtersSE, + comparisons, ctrlGroup, allPairwiseComparisons, singleFit, + subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, + minNbrValidValues, minlFC, samSignificance, nperm, volcanoAdjPvalThr, + volcanoLog2FCThr, volcanoMaxFeatures, volcanoLabelSign, volcanoS0, + volcanoFeaturesToLabel, addInteractiveVolcanos, interactiveDisplayColumns, + interactiveGroupColumn, complexFDRThr, maxNbrComplexesToPlot, + maxComplexSimilarity, seed, + includeFeatureCollections, minSizeToKeepSet, customComplexes, + complexSpecies, complexDbPath, stringVersion, stringDir, linkTableColumns, + customYml, doRender +) { + ## templateRmd + .assertScalar(x = templateRmd, type = "character") + if (!file.exists(templateRmd)) { + stop("'templateRmd' must point to an existing file") + } + + ## Output specifications + .assertScalar(x = outputDir, type = "character") + .assertScalar(x = outputBaseName, type = "character") + .assertScalar(x = reportTitle, type = "character") + .assertScalar(x = reportAuthor, type = "character") + .assertScalar(x = forceOverwrite, type = "logical") + .assertScalar(x = doRender, type = "logical") + + ## Experiment info + .assertVector(x = experimentInfo, type = "list") + if (length(experimentInfo) > 0) { + .assertVector(x = names(experimentInfo), type = "character") + } + tmp <- getSpeciesInfo(species) ## gives an error for unsupported species + + ## Input file + .assertScalar(x = inFile, type = "character") + if (!file.exists(inFile)) { + stop("'inFile' must point to an existing file") + } + .assertScalar(x = file_ext(inFile), type = "character", + validValues = "rds") + tmpse <- readRDS(inFile) + .assertVector(x = tmpse, type = "SummarizedExperiment") + .assertScalar(x = aName, type = "character", + validValues = assayNames(tmpse)) + stopifnot(all(c("sample", "group") %in% colnames(colData(tmpse)))) + .assertVector(x = tmpse$group, type = "character") + rm(tmpse) + + ## Settings info + .assertVector(x = summaryInfo, type = "list") + if (length(summaryInfo) > 0) { + .assertVector(x = names(summaryInfo), type = "character") + } + + if (is(idCol, "function")) { + stopifnot(length(formals(idCol)) == 1) + } else { + .assertVector(x = idCol, type = "character") + } + if (is(labelCol, "function")) { + stopifnot(length(formals(labelCol)) == 1) + } else { + .assertVector(x = labelCol, type = "character") + } + if (is(geneIdCol, "function")) { + stopifnot(length(formals(geneIdCol)) == 1) + } else { + .assertVector(x = geneIdCol, type = "character", allowNULL = TRUE) + } + if (is(proteinIdCol, "function")) { + stopifnot(length(formals(proteinIdCol)) == 1) + } else { + .assertVector(x = proteinIdCol, type = "character", allowNULL = TRUE) + } + if (is(stringIdCol, "function")) { + stopifnot(length(formals(stringIdCol)) == 1) + } else { + .assertVector(x = stringIdCol, type = "character", allowNULL = TRUE) + } + .assertVector(x = extraFeatureCols, type = "list", allowNULL = TRUE) + if (!is.null(extraFeatureCols)) { + .assertVector(x = names(extraFeatureCols), type = "character") + for (i in extraFeatureCols) { + if (is(i, "function")) { + stopifnot(length(formals(i)) == 1) + } else { + .assertVector(x = i, type = "character", allowNULL = TRUE) + } + } + } + + .assertVector(x = linkTableColumns, type = "character", allowNULL = TRUE) + + ## Filter functions + .assertVector(x = filtersSE, type = "list") + if (length(filtersSE) > 0) { + .assertVector(x = names(filtersSE), type = "character") + for (f in filtersSE) { + stopifnot(is(f, "function")) + stopifnot(length(formals(f)) == 1) + } + } + + ## Method choices + .assertScalar(x = imputeMethod, type = "character", + validValues = c("impSeqRob", "MinProb", "MinProbGlobal")) + .assertVector(x = assaysForExport, type = "character", allowNULL = TRUE) + .assertScalar(x = addAbundanceValues, type = "logical") + .assertScalar(x = addHeatmaps, type = "logical") + .assertScalar(x = normMethod, type = "character", + validValues = c(MsCoreUtils::normalizeMethods(), "none", + "center.mean.shared", "center.median.shared")) + .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) + .assertScalar(x = stattest, type = "character", + validValues = c("limma", "ttest", "proDA", "none")) + + ## Test parameters + .assertScalar(x = minNbrValidValues, type = "numeric", rngIncl = c(0, Inf)) + .assertScalar(x = minlFC, type = "numeric", rngIncl = c(0, Inf)) + .assertScalar(x = samSignificance, type = "logical") + .assertScalar(x = nperm, type = "numeric", rngIncl = c(1, Inf)) + .assertScalar(x = volcanoAdjPvalThr, type = "numeric", rngIncl = c(0, 1)) + .assertScalar(x = volcanoLog2FCThr, type = "numeric", rngIncl = c(0, Inf)) + .assertScalar(x = volcanoMaxFeatures, type = "numeric", rngIncl = c(0, Inf)) + .assertScalar(x = volcanoLabelSign, type = "character", + validValues = c("both", "pos", "neg")) + .assertScalar(x = volcanoS0, type = "numeric", rngIncl = c(0, Inf)) + .assertScalar(x = complexFDRThr, type = "numeric", rngIncl = c(0, 1)) + .assertScalar(x = maxNbrComplexesToPlot, type = "numeric", + rngIncl = c(0, Inf)) + .assertScalar(x = maxComplexSimilarity, type = "numeric") + .assertScalar(x = minSizeToKeepSet, type = "numeric", rngIncl = c(0, Inf)) + .assertVector(x = volcanoFeaturesToLabel, type = "character") + .assertVector(x = mergeGroups, type = "list") + .assertVector(x = comparisons, type = "list") + .assertScalar(x = ctrlGroup, type = "character") + .assertScalar(x = allPairwiseComparisons, type = "logical") + .assertScalar(x = addInteractiveVolcanos, type = "logical") + .assertVector(x = interactiveDisplayColumns, type = "character", + allowNULL = TRUE) + .assertScalar(x = interactiveGroupColumn, type = "character", + allowNULL = TRUE) + .assertScalar(x = singleFit, type = "logical") + .assertScalar(x = subtractBaseline, type = "logical") + .assertScalar(x = baselineGroup, type = "character") + + if (length(mergeGroups) > 0) { + if (is.null(names(mergeGroups)) || any(names(mergeGroups) == "") || + any(duplicated(names(mergeGroups)))) { + stop("'mergeGroups' must be a named list, without duplicated names") + } + } + + if (length(comparisons) > 0) { + if (!all(vapply(comparisons, length, 0) == 2)) { + stop("Each entry in 'comparisons' must have exactly two elements") + } + } + + ## seed + .assertScalar(x = seed, type = "numeric", rngIncl = c(1, Inf)) + + ## Complexes + .assertVector(x = includeFeatureCollections, type = "character", + validValues = c("complexes", "GO", "pathways"), allowNULL = TRUE) + .assertVector(x = customComplexes, type = "list") + if (length(customComplexes) > 0) { + .assertVector(x = names(customComplexes), type = "character") + } + .assertScalar(x = complexSpecies, type = "character", + validValues = c("current", "all"), allowNULL = TRUE) + .assertScalar(x = complexDbPath, type = "character", allowNULL = TRUE) + if (!is.null(complexDbPath) && !file.exists(complexDbPath)) { + stop("'complexDbPath' must point to an existing file") + } + + .assertScalar(x = stringVersion, type = "character") + .assertScalar(x = stringDir, type = "character", allowNULL = TRUE) + + .assertScalar(x = customYml, type = "character", allowNULL = TRUE) + if (!is.null(customYml) && !file.exists(customYml)) { + stop("'customYml' must point to an existing file") + } +} diff --git a/R/runSEAnalysis.R b/R/runSEAnalysis.R new file mode 100644 index 0000000..b60b5cb --- /dev/null +++ b/R/runSEAnalysis.R @@ -0,0 +1,297 @@ +#' Run analysis on pre-imported data +#' +#' Launch an analysis workflow on data stored in a +#' \code{\link[SummarizedExperiment]{SummarizedExperiment}} object, generated +#' from arbitrary source data. +#' +#' @inheritParams runMaxQuantAnalysis +#' +#' @param inFile Character string pointing to an \code{.rds} file containing +#' a serialized \code{\link[SummarizedExperiment]{SummarizedExperiment}} +#' object (or a derivative, such as +#' \code{\link[SingleCellExperiment]{SingleCellExperiment}}, with features +#' (e.g. proteins) in rows and samples in columns. The object +#' must have at least one assay (\code{aName}), containing raw intensity +#' values or equivalent (these will be automatically log-transformed by +#' einprot). +#' @param summaryInfo Named list with information about the identification +#' and quantification workflow. This information will be displayed in +#' table format in the report, under the 'Analysis summary' header. einprot +#' provides functions to generate such lists for common processing tools, +#' such as \code{\link{readMaxQuantXML}}, \code{\link{readFragPipeInfo}}, +#' \code{\link{readProteomeDiscovererInfo}} and \code{\link{readDIANNInfo}}. +#' @param aName Character scalar providing the name of the assay of the +#' \code{SummarizedExperiment} object in \code{inputFile} that should be +#' used as the main assay for the analysis. +#' The assay should contain values on the "raw intensity scale" (i.e., +#' they will be log-transformed as part of the analysis workflow). Missing +#' values should be represented by either zeros or NA values. +#' +#' @export +#' @author Charlotte Soneson +#' +#' @returns Invisibly, the path to the compiled html report. +#' +#' @examples +#' if (interactive()) { +#' sampleAnnot <- read.delim( +#' system.file("extdata/mq_example/1356_sampleAnnot.txt", +#' package = "einprot")) +#' se <- importExperiment( +#' system.file("extdata/mq_example/1356_proteinGroups.txt", +#' package = "einprot"), +#' iColPattern = "^LFQ.intensity.")$sce +#' se <- addSampleAnnots(se, sampleAnnot) +#' tmpf <- tempfile(fileext = ".rds") +#' saveRDS(se, file = tmpf) +#' settings <- readMaxQuantXML( +#' system.file("extdata/mq_example/1356_mqpar.xml", +#' package = "einprot")) +#' +#' ## Basic analysis +#' out <- runSEAnalysis( +#' outputDir = tempdir(), +#' outputBaseName = "MQ_LFQ_basic", +#' species = "mouse", +#' inFile = tmpf, +#' summaryInfo = settings, +#' aName = "LFQ.intensity", +#' idCol = function(df) getFirstId(df, "Majority.protein.IDs", ";"), +#' labelCol = function(df) getFirstId(df, "Majority.protein.IDs", ";"), +#' filtersSE = list() +#' ) +#' ## Output file +#' out +#' } +#' +#' @importFrom xfun Rscript_call +#' @importFrom rmarkdown render +#' @importFrom readr read_file write_file +#' @import STRINGdb +#' @importFrom SummarizedExperiment rowData colData assay assayNames +#' @importFrom DT datatable +#' @importFrom limma removeBatchEffect +#' @importFrom ExploreModelMatrix VisualizeDesign +#' @importFrom cowplot plot_grid theme_cowplot +#' @importFrom htmltools tagList +#' @importFrom dplyr %>% select starts_with full_join filter matches everything +#' mutate +#' @importFrom knitr current_input +#' @importFrom ComplexUpset upset +#' @importFrom ggplot2 ggplot aes geom_bar coord_flip theme_bw labs theme +#' element_text geom_point ggtitle +#' @importFrom tibble rownames_to_column +#' @importFrom S4Vectors metadata +#' @importFrom scater runPCA +#' @importFrom SingleCellExperiment reducedDim +#' @importFrom BiocSingular ExactParam +#' @importFrom plotly ggplotly +#' @importFrom ComplexHeatmap Heatmap columnAnnotation draw +#' +runSEAnalysis <- function( + templateRmd = system.file("extdata/process_basic_template.Rmd", + package = "einprot"), + outputDir = ".", outputBaseName = "SEAnalysis", + reportTitle = "Pregenerated data processing", reportAuthor = "", + forceOverwrite = FALSE, + experimentInfo = list(), species, + inFile, summaryInfo = NULL, aName, + idCol, labelCol, geneIdCol = NULL, proteinIdCol = NULL, stringIdCol = NULL, + extraFeatureCols = NULL, filtersSE, + imputeMethod = "MinProb", assaysForExport = c("iBAQ", "Top3"), + addAbundanceValues = TRUE, addHeatmaps = TRUE, + mergeGroups = list(), comparisons = list(), + ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, + subtractBaseline = FALSE, baselineGroup = "", normMethod = "none", + spikeFeatures = NULL, stattest = "limma", minNbrValidValues = 2, + minlFC = 0, samSignificance = TRUE, nperm = 250, volcanoAdjPvalThr = 0.05, + volcanoLog2FCThr = 1, volcanoMaxFeatures = 25, volcanoLabelSign = "both", + volcanoS0 = 0.1, volcanoFeaturesToLabel = "", + addInteractiveVolcanos = FALSE, interactiveDisplayColumns = NULL, + interactiveGroupColumn = NULL, complexFDRThr = 0.1, + maxNbrComplexesToPlot = Inf, maxComplexSimilarity = 1, seed = 42, + includeFeatureCollections = c(), minSizeToKeepSet = 2, + customComplexes = list(), + complexSpecies = "all", complexDbPath = NULL, stringVersion = "11.5", + stringDir = NULL, linkTableColumns = c(), customYml = NULL, doRender = TRUE +) { + ## ------------------------------------------------------------------------- + ## Fix ctrlGroup/mergeGroups + ## ------------------------------------------------------------------------- + ## For backward compatibility: If mergeGroups is list(), and ctrlGroup + ## is a vector (the way things were specified before v0.3.2), add the + ## merged ctrl group to mergeGroups. Raise an error if mergeGroups is + ## not empty and ctrlGroup is a vector. + if (length(mergeGroups) > 0 && length(ctrlGroup) > 1) { + stop("If 'mergeGroups' is specified, 'ctrlGroup' should not ", + "be a vector.") + } + if (length(mergeGroups) == 0 && length(ctrlGroup) > 1) { + mergeGroups <- list() + newCtrlName <- paste(ctrlGroup, collapse = ".") + mergeGroups[[newCtrlName]] <- ctrlGroup + ctrlGroup <- newCtrlName + } + + ## ------------------------------------------------------------------------- + ## Check arguments + ## ------------------------------------------------------------------------- + .checkArgumentsSE( + templateRmd = templateRmd, outputDir = outputDir, + outputBaseName = outputBaseName, reportTitle = reportTitle, + reportAuthor = reportAuthor, forceOverwrite = forceOverwrite, + experimentInfo = experimentInfo, species = species, + inFile = inFile, summaryInfo = summaryInfo, aName = aName, + idCol = idCol, labelCol = labelCol, geneIdCol = geneIdCol, + proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, + extraFeatureCols = extraFeatureCols, filtersSE = filtersSE, + imputeMethod = imputeMethod, assaysForExport = assaysForExport, + addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, + mergeGroups = mergeGroups, + comparisons = comparisons, ctrlGroup = ctrlGroup, + allPairwiseComparisons = allPairwiseComparisons, singleFit = singleFit, + subtractBaseline = subtractBaseline, baselineGroup = baselineGroup, + normMethod = normMethod, spikeFeatures = spikeFeatures, + stattest = stattest, minNbrValidValues = minNbrValidValues, + minlFC = minlFC, samSignificance = samSignificance, + nperm = nperm, volcanoAdjPvalThr = volcanoAdjPvalThr, + volcanoLog2FCThr = volcanoLog2FCThr, + volcanoMaxFeatures = volcanoMaxFeatures, + volcanoLabelSign = volcanoLabelSign, + volcanoS0 = volcanoS0, volcanoFeaturesToLabel = volcanoFeaturesToLabel, + addInteractiveVolcanos = addInteractiveVolcanos, + interactiveDisplayColumns = interactiveDisplayColumns, + interactiveGroupColumn = interactiveGroupColumn, + complexFDRThr = complexFDRThr, + maxNbrComplexesToPlot = maxNbrComplexesToPlot, + maxComplexSimilarity = maxComplexSimilarity, seed = seed, + includeFeatureCollections = includeFeatureCollections, + minSizeToKeepSet = minSizeToKeepSet, + customComplexes = customComplexes, complexSpecies = complexSpecies, + complexDbPath = complexDbPath, stringVersion = stringVersion, + stringDir = stringDir, linkTableColumns = linkTableColumns, + customYml = customYml, doRender = doRender) + + ## If pandoc is not available, don't run it (just generate .md file) + ## Gives a warning if pandoc and/or pandoc-citeproc is not available + pandocOK <- .checkPandoc(ignorePandoc = TRUE) + + ## ------------------------------------------------------------------------- + ## Normalize paths + ## ------------------------------------------------------------------------- + inFile <- normalizePath(inFile) + + ## ------------------------------------------------------------------------- + ## Copy Rmd template and insert arguments + ## ------------------------------------------------------------------------- + confighook <- "ConfigParameters" + + ## Concatenate Rmd chunk yml + configchunk <- .generateConfigChunk( + list(experimentInfo = experimentInfo, species = species, + inFile = inFile, summaryInfo = summaryInfo, aName = aName, + idCol = idCol, labelCol = labelCol, geneIdCol = geneIdCol, + proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, + extraFeatureCols = extraFeatureCols, filtersSE = filtersSE, + reportTitle = reportTitle, reportAuthor = reportAuthor, + imputeMethod = imputeMethod, assaysForExport = assaysForExport, + addAbundanceValues = addAbundanceValues, + addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, + comparisons = comparisons, ctrlGroup = ctrlGroup, + allPairwiseComparisons = allPairwiseComparisons, + singleFit = singleFit, + subtractBaseline = subtractBaseline, baselineGroup = baselineGroup, + normMethod = normMethod, spikeFeatures = spikeFeatures, + stattest = stattest, minNbrValidValues = minNbrValidValues, + minlFC = minlFC, samSignificance = samSignificance, + nperm = nperm, volcanoAdjPvalThr = volcanoAdjPvalThr, + volcanoLog2FCThr = volcanoLog2FCThr, + volcanoMaxFeatures = volcanoMaxFeatures, + volcanoLabelSign = volcanoLabelSign, + volcanoS0 = volcanoS0, + volcanoFeaturesToLabel = volcanoFeaturesToLabel, + addInteractiveVolcanos = addInteractiveVolcanos, + interactiveDisplayColumns = interactiveDisplayColumns, + interactiveGroupColumn = interactiveGroupColumn, + complexFDRThr = complexFDRThr, + maxNbrComplexesToPlot = maxNbrComplexesToPlot, + maxComplexSimilarity = maxComplexSimilarity, seed = seed, + includeFeatureCollections = includeFeatureCollections, + minSizeToKeepSet = minSizeToKeepSet, + customComplexes = customComplexes, complexSpecies = complexSpecies, + complexDbPath = complexDbPath, stringVersion = stringVersion, + stringDir = stringDir, linkTableColumns = linkTableColumns, + expType = "SE") + ) + + ## Read Rmd + rmd <- readr::read_file(templateRmd) + + ## Determine where to insert the config chunk + ## From https://community.rstudio.com/t/how-to-write-r-script-into-rmd-as-functioning-code-chunk/37453/2 + # header_regex <- sprintf("\\{\\{%sStart\\}\\}(.*?)\\{\\{%sEnd\\}\\}", + # confighook, + # confighook) + header_regex <- sprintf("{{%sStart}}\n\n{{%sEnd}}", + confighook, + confighook) + + ## Replace hooks with config chunk + output <- sub(header_regex, configchunk, rmd, fixed = TRUE) + + ## Similarly, add any custom yaml + ymlhook <- "YmlParameters" + # header_regex_yml <- sprintf("\\{\\{%sStart\\}\\}(.*?)\\{\\{%sEnd\\}\\}", + # ymlhook, + # ymlhook) + header_regex_yml <- sprintf("{{%sStart}}\n\n{{%sEnd}}", + ymlhook, + ymlhook) + if (!is.null(customYml)) { + customYml <- paste(readLines(customYml), collapse = "\n") + } else { + customYml <- "" + } + output <- sub(header_regex_yml, customYml, output, fixed = TRUE) + + ## Write output to file + if (!dir.exists(outputDir)) { + dir.create(outputDir, recursive = TRUE) + } + outputFile <- file.path(outputDir, paste0(outputBaseName, ".Rmd")) + if (file.exists(outputFile) && !forceOverwrite) { + stop(outputFile, + " already exists and forceOverwrite = FALSE, stopping.") + } else if (file.exists(outputFile) && forceOverwrite) { + message(outputFile, + " already exists but forceOverwrite = TRUE, overwriting.") + } + readr::write_file(output, file = outputFile) + + ## ------------------------------------------------------------------------- + ## Render the Rmd file + ## ------------------------------------------------------------------------- + args <- list() + args$input <- outputFile + args$output_format <- "html_document" + args$output_dir <- outputDir + args$intermediates_dir <- outputDir + args$quiet <- FALSE + args$run_pandoc <- pandocOK + + if (doRender) { + #nocov start + outputReport <- xfun::Rscript_call( + rmarkdown::render, + args + ) + #nocov end + } else { + outputReport <- outputFile + } + + ## ------------------------------------------------------------------------- + ## Return (invisibly) the path to the rendered html file + ## ------------------------------------------------------------------------- + invisible(outputReport) +} diff --git a/man/runSEAnalysis.Rd b/man/runSEAnalysis.Rd new file mode 100644 index 0000000..48f2f1a --- /dev/null +++ b/man/runSEAnalysis.Rd @@ -0,0 +1,362 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/runSEAnalysis.R +\name{runSEAnalysis} +\alias{runSEAnalysis} +\title{Run analysis on pre-imported data} +\usage{ +runSEAnalysis( + templateRmd = system.file("extdata/process_basic_template.Rmd", package = "einprot"), + outputDir = ".", + outputBaseName = "SEAnalysis", + reportTitle = "Pregenerated data processing", + reportAuthor = "", + forceOverwrite = FALSE, + experimentInfo = list(), + species, + inFile, + summaryInfo = NULL, + aName, + idCol, + labelCol, + geneIdCol = NULL, + proteinIdCol = NULL, + stringIdCol = NULL, + extraFeatureCols = NULL, + filtersSE, + imputeMethod = "MinProb", + assaysForExport = c("iBAQ", "Top3"), + addAbundanceValues = TRUE, + addHeatmaps = TRUE, + mergeGroups = list(), + comparisons = list(), + ctrlGroup = "", + allPairwiseComparisons = TRUE, + singleFit = TRUE, + subtractBaseline = FALSE, + baselineGroup = "", + normMethod = "none", + spikeFeatures = NULL, + stattest = "limma", + minNbrValidValues = 2, + minlFC = 0, + samSignificance = TRUE, + nperm = 250, + volcanoAdjPvalThr = 0.05, + volcanoLog2FCThr = 1, + volcanoMaxFeatures = 25, + volcanoLabelSign = "both", + volcanoS0 = 0.1, + volcanoFeaturesToLabel = "", + addInteractiveVolcanos = FALSE, + interactiveDisplayColumns = NULL, + interactiveGroupColumn = NULL, + complexFDRThr = 0.1, + maxNbrComplexesToPlot = Inf, + maxComplexSimilarity = 1, + seed = 42, + includeFeatureCollections = c(), + minSizeToKeepSet = 2, + customComplexes = list(), + complexSpecies = "all", + complexDbPath = NULL, + stringVersion = "11.5", + stringDir = NULL, + linkTableColumns = c(), + customYml = NULL, + doRender = TRUE +) +} +\arguments{ +\item{templateRmd}{Path to the template R Markdown file. Typically does not +need to be modified.} + +\item{outputDir}{Path to a directory where all output files will be +written. Will be created if it doesn't exist.} + +\item{outputBaseName}{Character string providing the 'base name' of the +output files. All output files will start with this prefix.} + +\item{reportTitle, reportAuthor}{Character scalars, giving the title and +author for the result report.} + +\item{forceOverwrite}{Logical, whether to force overwrite an existing +Rmd file with the same \code{outputBaseName} in the \code{outputDir}.} + +\item{experimentInfo}{Named list with information about the experiment. +Each entry of the list must be a scalar value.} + +\item{species}{Character scalar providing the species. Must be one of the +supported species (see \code{getSupportedSpecies()}). Either the common +or the scientific name can be used.} + +\item{inFile}{Character string pointing to an \code{.rds} file containing +a serialized \code{\link[SummarizedExperiment]{SummarizedExperiment}} +object (or a derivative, such as +\code{\link[SingleCellExperiment]{SingleCellExperiment}}, with features +(e.g. proteins) in rows and samples in columns. The object +must have at least one assay (\code{aName}), containing raw intensity +values or equivalent (these will be automatically log-transformed by +einprot).} + +\item{summaryInfo}{Named list with information about the identification +and quantification workflow. This information will be displayed in +table format in the report, under the 'Analysis summary' header. einprot +provides functions to generate such lists for common processing tools, +such as \code{\link{readMaxQuantXML}}, \code{\link{readFragPipeInfo}}, +\code{\link{readProteomeDiscovererInfo}} and \code{\link{readDIANNInfo}}.} + +\item{aName}{Character scalar providing the name of the assay of the +\code{SummarizedExperiment} object in \code{inputFile} that should be +used as the main assay for the analysis. +The assay should contain values on the "raw intensity scale" (i.e., +they will be log-transformed as part of the analysis workflow). Missing +values should be represented by either zeros or NA values.} + +\item{idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol}{Arguments defining +the feature identifiers (row names, should be unique), +feature labels (for plots, can be anything), +gene IDs (single gene symbols, will be matched against complexes and +GO terms, can be \code{NULL}), +protein IDs (UniProt IDs, will be used to create automatic URLs and +match to species-specific identifiers, each entry can consist of +multiple UniProt IDs separated by semicolons), and +stringIdCol (single gene or protein ID, will be used to retrieve +STRING networks, can be \code{NULL}). +Each of these arguments can be either a character vector of column +names in the input file (after application of \code{make.names}), +in which case the corresponding feature ID +is generated by simply concatenating the values in these columns, or a +function with one input argument (a data.frame, corresponding to the +annotation columns of the input file), returning a +character vector corresponding to the desired feature IDs.} + +\item{extraFeatureCols}{Named list (or \code{NULL}) defining additional, +user-specified feature annotation columns to add to the object (in +addition to the ones defined by \code{idCol}, \code{labelCol}, +\code{geneIdCol}, \code{proteinIdCol} and \code{stringIdCol}). Similar +to these column definitions, each entry of the list must be either a +character vector of column names or a function taking a data.frame as +input and returning a single character column. These columns will be +created after the standard columns (\code{einprotId}, \code{einprotGene}, +\code{einprotProtein}, \code{einprotLabel}, \code{IDsForSTRING}), and +thus these columns can be used as well to create the user-specified +ones.} + +\item{filtersSE}{A named \code{list}, where each element is a function +that takes a \code{SummarizedExperiment} object as input and returns +a logical vector of the same length as the number of rows in \code{sce}, +and where \code{TRUE} implies that the row should be retained. Default +sets of filtering functions for input data from different tools are +provided (see \code{?defaultFiltersSE}).} + +\item{imputeMethod}{Character string defining the imputation method to use. +Currently, \code{"impSeqRob"}, \code{"MinProb"}, and +\code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for +more details about the methods.} + +\item{assaysForExport}{Character vector defining the name(s) of the assays +to use for exported abundances and barplots. This could, for example, +be set to an assay containing 'absolute' abundances, if available, even +if another assay is used for the actual analysis and comparison of +groups. If set to \code{NULL} or an assay name that does not exist in +the SingleCellExperiment object, the 'main' assay will be used.} + +\item{addAbundanceValues}{Logical scalar defining whether abundance values +for individual samples should be added to the exported result tables.} + +\item{addHeatmaps}{Logical scalar indicating whether to include heatmaps +or not. This controls both the heatmap showing the missing value +pattern in the data, as well as the summary heatmaps of the +quantitative information in the data. For large data sets, excluding +the heatmaps can significantly speed up the processing time.} + +\item{mergeGroups}{Named list of character vectors defining sample groups +to merge to create new groups, that will be used for comparisons. +Any specification of \code{comparisons} or \code{ctrlGroup} should +be done in terms of the new (merged) group names.} + +\item{comparisons}{List of character vectors defining comparisons to +perform. The first element of each vector represents the +denominator of the comparison. If not empty, \code{ctrlGroup} and +\code{allPairwiseComparisons} are ignored.} + +\item{ctrlGroup}{Character vector defining the sample group(s) to use as +control group in comparisons.} + +\item{allPairwiseComparisons}{Logical, should all pairwise comparisons be +performed?} + +\item{singleFit}{Logical scalar indicating whether a single model fit +should be used (and results for pairwise comparisons extracted via +contrasts). If \code{FALSE}, the data set will be subset to the +relevant samples for each comparison. Only applicable if +\code{stattest} is \code{"limma"} or \code{"proDA"}.} + +\item{subtractBaseline}{Logical scalar, whether to subtract the background/ +reference value for each feature in each batch before fitting the +model. If \code{TRUE}, requires that a 'batch' column is available.} + +\item{baselineGroup}{Character scalar representing the reference group. +Only used if \code{subtractBaseline} is \code{TRUE}, in which case the +abundance values for a given sample will be adjusted by subtracting the +average value across all samples in the \code{baselineGroup} from the +same batch as the original sample.} + +\item{normMethod}{Character scalar indicating the normalization method to +use. Currently, any method from \code{MsCoreUtils::normalizeMethods()} +or \code{"none"} are valid values.} + +\item{spikeFeatures}{Character vector indicating the 'spike-in' features +to use for estimation of normalization factors. If \code{NULL} +(default), all features are used.} + +\item{stattest}{Either \code{"ttest"}, \code{"limma"} or \code{"proDA"}, +the testing framework to use. Could also be \code{"none"} if no test +should be performed.} + +\item{minNbrValidValues}{Numeric, the minimum number of valid values for a +protein to be used for statistical testing.} + +\item{minlFC}{Numeric, minimum log fold change to test against (only used +if \code{stattest = "limma"}).} + +\item{samSignificance}{Logical scalar, indicating whether the SAM statistic +should be used to determine significance (similar to the approach used by +Perseus). Only used if \code{stattest = "ttest"}. If \code{FALSE}, the +p-values are adjusted using the Benjamini-Hochberg approach and used +to determine significance.} + +\item{nperm}{Numeric, number of permutations to use in the statistical +testing (only used if \code{stattest = "ttest"}).} + +\item{volcanoAdjPvalThr}{Numeric, adjusted p-value threshold to determine +which proteins to highlight in the volcano plots.} + +\item{volcanoLog2FCThr}{Numeric, log-fold change threshold to determine +which proteins to highlight in the volcano plots.} + +\item{volcanoMaxFeatures}{Numeric, maximum number of significant features +to label in the volcano plots.} + +\item{volcanoLabelSign}{Character scalar, either 'both', 'pos', or 'neg', +indicating whether to label the most significant features regardless of +sign, or only those with positive/negative log-fold changes.} + +\item{volcanoS0}{Numeric, S0 value to use to generate the significance +curve in the volcano plots (only used if \code{stattest = "ttest"}).} + +\item{volcanoFeaturesToLabel}{Character vector with features to always +label in the volcano plots (regardless of significance).} + +\item{addInteractiveVolcanos}{Logical scalar indicating whether to add +interactive volcano plots to the html report. For experiments with +many quantified features or many comparisons, setting this to +\code{TRUE} can make the html report very large and difficult to +interact with.} + +\item{interactiveDisplayColumns}{Character vector (or \code{NULL}) +indicating which columns to include in the tooltip for the +interactive volcano plots. The default shows the feature ID.} + +\item{interactiveGroupColumn}{Character scalar (or \code{NULL}, default) +indicating the column to group points by in the interactive volcano +plot. Hovering over a point will highlight all other points with the +same value of this column.} + +\item{complexFDRThr}{Numeric, FDR threshold for significance in testing +of complexes.} + +\item{maxNbrComplexesToPlot}{Numeric, the maximum number of significant +complexes for which to make separate volcano plots. Defaults to +\code{Inf}, i.e., no limit.} + +\item{maxComplexSimilarity}{Numeric scalar. If a significant complex C has a +Jaccard similarity exceeding this value with a complex for which a +volcano plot has already been generated, no volcano plot will be +generated for C. The default value is 1, which means that no complex +will be excluded.} + +\item{seed}{Numeric, random seed to use for any non-deterministic +calculations.} + +\item{includeFeatureCollections}{Character vector, a subset of +\code{c("complexes", "GO", "pathways")}.} + +\item{minSizeToKeepSet}{Numeric scalar indicating the smallest number of +features that have to overlap with the current data set in order to +retain a feature set for testing.} + +\item{customComplexes}{List of character vectors providing custom complexes +to test for significant differences between groups.} + +\item{complexSpecies}{Either \code{"all"} or \code{"current"}, depending +on whether complexes defined for all species, or only those defined +for the current species, should be tested for significance.} + +\item{complexDbPath}{Character string providing path to the complex DB +file (generated with \code{makeComplexDB()}).} + +\item{stringVersion}{Character scalar giving the version of the STRING +database to query.} + +\item{stringDir}{Character scalar (or \code{NULL}) providing the path to a +folder where the STRING files will be downloaded (or loaded from, if +they already exist). If \code{NULL} (default), they will be downloaded +to a temporary directory.} + +\item{linkTableColumns}{Character vector with regular expressions that will +be matched against the column names of the rowData of the generated +SingleCellExperiment object and included in the link table in the end +of the report.} + +\item{customYml}{Character string providing the path to a custom YAML file +that can be used to overwrite default settings in the report. If set +to \code{NULL} (default), no alterations are made.} + +\item{doRender}{Logical scalar. If \code{FALSE}, the Rmd file will be +generated (and any parameters injected), but not rendered.} +} +\value{ +Invisibly, the path to the compiled html report. +} +\description{ +Launch an analysis workflow on data stored in a +\code{\link[SummarizedExperiment]{SummarizedExperiment}} object, generated +from arbitrary source data. +} +\examples{ +if (interactive()) { + sampleAnnot <- read.delim( + system.file("extdata/mq_example/1356_sampleAnnot.txt", + package = "einprot")) + se <- importExperiment( + system.file("extdata/mq_example/1356_proteinGroups.txt", + package = "einprot"), + iColPattern = "^LFQ.intensity.")$sce + se <- addSampleAnnots(se, sampleAnnot) + tmpf <- tempfile(fileext = ".rds") + saveRDS(se, file = tmpf) + settings <- readMaxQuantXML( + system.file("extdata/mq_example/1356_mqpar.xml", + package = "einprot")) + + ## Basic analysis + out <- runSEAnalysis( + outputDir = tempdir(), + outputBaseName = "MQ_LFQ_basic", + species = "mouse", + inFile = tmpf, + summaryInfo = settings, + aName = "LFQ.intensity", + idCol = function(df) getFirstId(df, "Majority.protein.IDs", ";"), + labelCol = function(df) getFirstId(df, "Majority.protein.IDs", ";"), + filtersSE = list() + ) + ## Output file + out +} + +} +\author{ +Charlotte Soneson +} From 1975c3e8c39ecac723119c41c435f21df0185072 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 13 Feb 2026 11:31:24 +0100 Subject: [PATCH 29/65] Adapt functions to allow 'SE' expType --- R/prepareFinalSCE.R | 4 +- R/textSnippets.R | 11 ++- inst/extdata/process_basic_template.Rmd | 101 ++++++++++++++---------- 3 files changed, 72 insertions(+), 44 deletions(-) diff --git a/R/prepareFinalSCE.R b/R/prepareFinalSCE.R index 5fcd4ef..e66ba5c 100644 --- a/R/prepareFinalSCE.R +++ b/R/prepareFinalSCE.R @@ -33,7 +33,7 @@ prepareFinalSCE <- function(sce, baseFileName, featureCollections, expType) { .assertVector(x = featureCollections, type = "list", allowNULL = TRUE) .assertScalar(x = expType, type = "character", validValues = c("MaxQuant", "ProteomeDiscoverer", - "FragPipe", "DIANN", "Spectronaut")) + "FragPipe", "DIANN", "Spectronaut", "SE")) if (expType == "MaxQuant") { ## If not already there, also include log-transformed iBAQ values @@ -73,6 +73,8 @@ prepareFinalSCE <- function(sce, baseFileName, featureCollections, expType) { colsToRemove <- c() } else if (expType == "Spectronaut") { colsToRemove <- c() + } else if (expType == "SE") { + colsToRemove <- c() } ## Write removed columns to text file diff --git a/R/textSnippets.R b/R/textSnippets.R index 6a02cfb..8abd548 100644 --- a/R/textSnippets.R +++ b/R/textSnippets.R @@ -209,7 +209,7 @@ expDesignText <- function(testType) { introText <- function(expType) { .assertScalar(x = expType, type = "character", validValues = c("MaxQuant", "ProteomeDiscoverer", - "FragPipe", "DIANN", "Spectronaut")) + "FragPipe", "DIANN", "Spectronaut", "SE")) if (expType == "MaxQuant") { paste0("This report describes a reproducible end-to-end analysis of ", @@ -235,6 +235,10 @@ introText <- function(expType) { "a proteomics dataset quantified with ", "[Spectronaut](https://biognosys.com/resources/spectronaut-", "a-groundbreaking-increase-in-identifications/).") + } else if (expType == "SE") { + paste0("This report describes a reproducible end-to-end analysis of ", + "a proteomics dataset loaded as a pre-imported ", + "SummarizedExperiment object.") } } @@ -262,7 +266,7 @@ inputText <- function(expTypeLevel) { .assertScalar(x = expTypeLevel, type = "character", validValues = c("MaxQuant", "ProteomeDiscovererProteins", "ProteomeDiscovererPeptideGroups", - "FragPipe", "DIANN", "Spectronaut")) + "FragPipe", "DIANN", "Spectronaut", "SE")) if (expTypeLevel == "MaxQuant") { paste0("The input to this workflow is a `proteinGroups.txt` file ", @@ -300,6 +304,9 @@ inputText <- function(expTypeLevel) { "Spectronaut intensities into `R` and store them in a ", "[SingleCellExperiment](https://bioconductor.org/packages/SingleCellExperiment/) ", "object. ") + } else if (expTypeLevel == "SE") { + paste0("The input to this workflow is a serialized SummarizedExperiment ", + "object (or a derivative, such as a SingleCellExperiment object).") } } diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index 8ce1ae9..b2566d0 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -77,7 +77,7 @@ available from [GitHub](https://fmicompbio.github.io/einprot/). ```{r get-basic-info} ## Get species info and define STRINGdb object speciesInfo <- getSpeciesInfo(species) -if (expType %in% c("MaxQuant", "FragPipe", "DIANN", "Spectronaut") || +if (expType %in% c("MaxQuant", "FragPipe", "DIANN", "Spectronaut", "SE") || (expType == "ProteomeDiscoverer" && inputLevel == "Proteins")) { if (is.null(stringDir)) stringDir <- "" if (is.null(stringIdCol) || is.na(speciesInfo$taxId)) { @@ -157,6 +157,8 @@ if (expType == "MaxQuant") { tb <- readSpectronautSetup(spectronautSetupFile = spectronautLogFile) tb <- c(list("Spectronaut file" = spectronautFile, "Analysis level" = outLevel), tb) +} else if (expType == "SE") { + tb <- c(list("Input file" = inFile), summaryInfo) } else { stop("Unknown 'expType'") } @@ -182,9 +184,6 @@ running `einprot`. ```{r settings-table} settingsList <- list( - "Include only samples (if applicable)" = paste(includeOnlySamples, - collapse = ", "), - "Exclude samples" = paste(excludeSamples, collapse = ", "), "Filter names (SE)" = paste(names(filtersSE), collapse = ", "), "Imputation method" = imputeMethod, "Assays(s) to use for exported values" = paste(assaysForExport, collapse = ", "), @@ -232,6 +231,14 @@ settingsList <- list( "Interactive group column" = interactiveGroupColumn ) +if (expType != "SE") { + settingsList <- c( + list("Include only samples (if applicable)" = paste(includeOnlySamples, + collapse = ", "), + "Exclude samples" = paste(excludeSamples, collapse = ", ")), + settingsList + ) +} if (stattest == "ttest") { settingsList <- settingsList[!(names(settingsList) %in% c("Minimal fold change (limma/treat)", @@ -265,6 +272,10 @@ if (expType %in% c("Spectronaut", "DIANN")) { settingsList <- c(settingsList, list("Filter names (DF)" = paste(names(filtersDF), collapse = ", "))) } +if (expType == "SE") { + settingsList <- c(settingsList, + list("Assay name" = aName)) +} makeTableFromList(settingsList) ``` @@ -287,45 +298,49 @@ At this point, the SingleCellExperiment object holds assays with the different types of intensities and annotations from the input file. ```{r read-data} -if (expType == "MaxQuant") { - ## Read MaxQuant output - tmp <- importExperiment(inFile = mqFile, iColPattern = iColPattern, - includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples) -} else if (expType == "FragPipe") { - ## Read FragPipe output - tmp <- importExperiment(inFile = file.path(fragpipeDir, "combined_protein.tsv"), - iColPattern = iColPattern, - includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples) -} else if (expType == "ProteomeDiscoverer") { - ## Read Proteome Discoverer output - tmp <- importExperiment(inFile = pdFile, iColPattern = iColPattern, - includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples) -} else if (expType == "DIANN") { - ## Read DIA-NN output - tmp <- importDIANN(inFile = diannFile, fileType = diannFileType, - outLevel = outLevel, - stopIfEmpty = FALSE, aName = aName, - includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, filtersDF = filtersDF) -} else if (expType == "Spectronaut") { - ## Read Spectronaut output - tmp <- importSpectronaut(inFile = spectronautFile, - fileType = spectronautFileType, outLevel = outLevel, - iColPattern = iColPattern, - stopIfEmpty = FALSE, aName = aName, - includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, - filtersDF = filtersDF) +if (expType == "SE") { + sce <- as(readRDS(inFile), "SingleCellExperiment") } else { - stop("Unknown 'expType'") + if (expType == "MaxQuant") { + ## Read MaxQuant output + tmp <- importExperiment(inFile = mqFile, iColPattern = iColPattern, + includeOnlySamples = includeOnlySamples, + excludeSamples = excludeSamples) + } else if (expType == "FragPipe") { + ## Read FragPipe output + tmp <- importExperiment(inFile = file.path(fragpipeDir, "combined_protein.tsv"), + iColPattern = iColPattern, + includeOnlySamples = includeOnlySamples, + excludeSamples = excludeSamples) + } else if (expType == "ProteomeDiscoverer") { + ## Read Proteome Discoverer output + tmp <- importExperiment(inFile = pdFile, iColPattern = iColPattern, + includeOnlySamples = includeOnlySamples, + excludeSamples = excludeSamples) + } else if (expType == "DIANN") { + ## Read DIA-NN output + tmp <- importDIANN(inFile = diannFile, fileType = diannFileType, + outLevel = outLevel, + stopIfEmpty = FALSE, aName = aName, + includeOnlySamples = includeOnlySamples, + excludeSamples = excludeSamples, filtersDF = filtersDF) + } else if (expType == "Spectronaut") { + ## Read Spectronaut output + tmp <- importSpectronaut(inFile = spectronautFile, + fileType = spectronautFileType, outLevel = outLevel, + iColPattern = iColPattern, + stopIfEmpty = FALSE, aName = aName, + includeOnlySamples = includeOnlySamples, + excludeSamples = excludeSamples, + filtersDF = filtersDF) + } else { + stop("Unknown 'expType'") + } + + sce <- tmp$sce + aName <- tmp$aName } -sce <- tmp$sce -aName <- tmp$aName - sce ``` @@ -350,9 +365,11 @@ section below for more details about the fitted model(s). Please check that the table below correspond to your expectations. -```{r add-sampleinfo} +```{r add-sampleinfo, eval=(expType != "SE"), echo=(expType != "SE")} sce <- addSampleAnnots(sce, sampleAnnot = sampleAnnot) +``` +```{r print-sampleinfo} DT::datatable(as.data.frame(colData(sce)), options = list(scrollX = TRUE, pageLength = 20)) ``` @@ -1228,6 +1245,8 @@ if (expType == "MaxQuant") { addmeta$keepModifications <- keepModifications } else if (expType == "Spectronaut") { addmeta$spectronautFile <- spectronautFile +} else if (expType == "SE") { + addmeta$inFile <- inFile } S4Vectors::metadata(sce) <- c( From 4c07cfa24d591fab4f2f7b60c33d05d856fff2e5 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sat, 21 Feb 2026 19:15:24 +0100 Subject: [PATCH 30/65] Set default labelCol to idCol, filtersSE = list() in runSEAnalysis() --- R/runSEAnalysis.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/runSEAnalysis.R b/R/runSEAnalysis.R index b60b5cb..dbde432 100644 --- a/R/runSEAnalysis.R +++ b/R/runSEAnalysis.R @@ -96,8 +96,8 @@ runSEAnalysis <- function( forceOverwrite = FALSE, experimentInfo = list(), species, inFile, summaryInfo = NULL, aName, - idCol, labelCol, geneIdCol = NULL, proteinIdCol = NULL, stringIdCol = NULL, - extraFeatureCols = NULL, filtersSE, + idCol, labelCol = idCol, geneIdCol = NULL, proteinIdCol = NULL, stringIdCol = NULL, + extraFeatureCols = NULL, filtersSE = list(), imputeMethod = "MinProb", assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), From c8ef25fb2bc98f784689732a6decd785043ddc59 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sat, 21 Feb 2026 19:15:39 +0100 Subject: [PATCH 31/65] Check that default assay is a matrix --- R/checkArgumentsSE.R | 3 +++ 1 file changed, 3 insertions(+) diff --git a/R/checkArgumentsSE.R b/R/checkArgumentsSE.R index a156043..b837dc0 100644 --- a/R/checkArgumentsSE.R +++ b/R/checkArgumentsSE.R @@ -56,6 +56,9 @@ .assertVector(x = tmpse, type = "SummarizedExperiment") .assertScalar(x = aName, type = "character", validValues = assayNames(tmpse)) + if (!is.matrix(assay(tmpse, aName))) { + stop("The ", aName, " assay must be a matrix") + } stopifnot(all(c("sample", "group") %in% colnames(colData(tmpse)))) .assertVector(x = tmpse$group, type = "character") rm(tmpse) From 55da78884ab457ad413f7155c9796453968d20eb Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sat, 21 Feb 2026 19:53:59 +0100 Subject: [PATCH 32/65] Generate test SE object --- inst/extdata/mq_example/1356_sce_init.rds | Bin 0 -> 476204 bytes inst/scripts/generate_mq_sce.R | 12 ++++++++++++ 2 files changed, 12 insertions(+) create mode 100644 inst/extdata/mq_example/1356_sce_init.rds diff --git a/inst/extdata/mq_example/1356_sce_init.rds b/inst/extdata/mq_example/1356_sce_init.rds new file mode 100644 index 0000000000000000000000000000000000000000..48a61099c97a229815d70470a0ab68a03c4f3ed7 GIT binary patch literal 476204 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

-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 Nd^Voog0RVN!8V2drp(C(C;ytV+`N@9uM0% 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
    s~1o~x%cw|Q}FqWmrv5EgPd>o*A`C836E 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˟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

    q4LlK?ZmmhN}3v+C9XcZy}y(n** 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

    v(7vCe^AlN&Pxm8?Aek!DO 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


    `OBfuY5q)^AUF0QS2GC2D zGbU``)&X$ges$A>^hnWqk%%?PGAgKbF*kRk9+G3{gc9x9kBb~MDac6q%m?*YZht=g zlzR;@ZHC~xDB}^sC2INAnyQ$@*S!TY^>Ga?5q!Lo3oi|~0@LW}=U9cu5b+B+BXk!I zupBetW7?LkV$txU>DH03PYLc8D2h}SP>Siy-6)tU{sIJ-YrD5&+5Es`kyr5=pMw-* zg)t?Ooy~J_UHincPiF-Bn6`L}KY?%JB!m(>;k%$yz&od+67~0%5`B zdbD=tA=sU&4T!AIr2K4Mgl@MD=}e`*SuzOJ<+rKGH8iMt2u^{iAFBtAY~?`{;BuFy z1q53LPcYTUCpH%AB4P0#T>$c{@UztIFI>A9u5F`!)0@O&nRUGS^0<&rSZvkBQw>R? z*LW9soM+|K1!DACN*W2}!hZ{Aqv>XfGZm}`bpP5PhFbDkQF4S|2xHaz|4?+@@l^f) zpA;>8CCR*%q>NOuxi_g)LXweL$;!C)yh9&FHf2;+QfBt%x>t#7#Fcf;d#&qQ*X3T< zUGBXmn$~vHBA?_ZQqfiWkqtVBZLA>^h->O_oMa!r?7s z`T(3!pmIPqq`jzT(G&1+2#|tiKcAz`KOIeomDCkX;ep{db`o)$Mro4x*r( zhxdNRGgr>fBgCpkd~Fv4rFY=HH0!D}QW@5?6FNZ_CTbHF1j*ovBTjR}MEaa3q)8Ia zR`O=giJ=3~C2AlRfsOB&fQ-!#Eq9pwME!h*opOSYyi2ARh6L$%E9w z#rH5Gs_L204bm?G>4E}wNa8_#-n``OHBo5BUN40h47?KpjN8F~ivw2K?HwKF+)jO%RkFp+YV>Q~Oh%Y^@555%Y-$nv0QKBb zLOhu4P{4Y{vj|62Lw%!r{?#9MNtnDjSG(c==FlL?Zphsz$X4zDe`2%MfG@`a=Z|iL7&go$Nt_9D;a=^sYNjSCG&x-7oUq5Y1*}Y{icXD(FXK;D zI`sNHOk2V08VUcIBQ?H7f8t>!^`X>iV# z%e2@9==4ySBqr?7j@%6#!w*H7rP}PyJZJ2Yq3nofWTI4<;RFL#6m_ZYXYpCTcG_RT zs_*S{181OlR6EKg8Ob3|^ti><@Hr<=6L~Lc&BCnNYY&6O9!NqWP#f)>zY`4y&3@o7 zK!2lA{aGPlhpfr=9FgJ#+=>FdU=laB+EYmu`y-AV*Dfc5W*sb@l*m)m##*M|&Wu`sy3l zY+)hnj)FO;q%SOVk^G^OCz@zwu#NMm-r{f>x@Mdh2a(9<2g;LYm~|HuPLJ^~IJf?w zv>ERAlr+qZfcA=v&a$|%*?CiJc4ZvA!&(9V42Is!2Q&fsxLcdzYP{>8_M8d{YXwN^ z6*=^+39O5!A*&AD4ckb~om&2wTN_yb%t_u1d*n|37m!`4$F?e+_iLZdg-zFl31eOg%fas(Wx*c2i%&MC~pHKP?zH#siuT`pbq1JnVqj z2bs>NPDx4w`ZZIv)=_O7cVO_Vf^iy^J!L2u1;Q|$U4R2(9CzRP?>o%xB9e}KmFN6R zdk?ZlKvRdad&GLtna==}!KbYO(<=LBdboo}E|?n>SxX43S8Uea3+w!0hAXMx(rasm zhjIGwPAGDKO9*xsIb#vQ_y(*nXRR;5nhS|y0&MLgjJH?b6uvf7FTx9U!y97XMxwdJ8)tqd`1B|Np6&mkgfKkszGUL zOP8vA+r_UkM(mODtsF0~!vq6@o8;1xG*ZBeg6_FqG>>{4O*}lzt82*5iRI2X+g9LE zqP^iF3PUX@r!uVSkFThX`r3-lc-cGjYYbM3i1<|y0 zwwCv!XksWPa5+ldZUC#$3<_|p&*vPR>;DUDS2^&}w}*%Iq1pK+-gFNw^2XiX!{hXw z#ALenBEY@!wU4bF{%??l-pqQL4Pw~emksxD!{Z$g;0cBSnI79EZ7|n^1tVQy>U>q1 z=JE|o1+PP@3+zM|UVG!Z&e%FcDDT8P@GD9^7CsRu^mAhGV-pN#$vLdx(98)e!ijqk z*ozqRgOeRlmdg~2Qc^7_8oIA9y|ni|c`v^dvGm>JD&t)E*Bn&fufuAXuz3xKYB&xP z{Iybh=uw3(&ScAbe``kRsvBD(>goi!xoQ#}9#b8my}w>b-`{fRko*~vHU7DQOEuNL!#?w1q`~^p`QyCotss$ZA z2mx@a?x0~5RZ{m}Jn3zM@4v=7;y{!*1yB=HNTDMk#M+k}TNW_0llcFm@3@Gj|$S z-2mAxgWrJ6m=yrMZj%q>9S1--Z38}Lct#WyIZP=Js9JcQ`o$+CH_^|RqCK?VNU*Y) zo>`1@Vcq@+hpmkZql!WXm?j7Omm}(HhPmnJomvk~ae+LVLlx>9W*yFQ*;uaK1Il#} z$Dt_rlu%Bazo^soR=lt;lrRM-99sCGn_cxBPR{LQ{iud7UXhDaN7J2nm699=vSb8) zP7Ok-yb0)-3D)q7tS6vXIwW9mN~EwIbvC7_DuDg0Ai{Ww`aB59u;n#6v%VqQj#*5T zd%@Bvi#?Uu!mlH?pk(_(!p~7*bOa~fy(Xjy^va>4==DZ&$|Qck1?lX{<`iNel%-EQ zU4rUQHip$$;O2e%Q7hm|Rw|9d{r1jfwH>xjWv|yJPj2mFpWvrCkUtv282H{l4pVrD z{i}YW4Kwgu&;ld`Q*HXf`H~@CR}pB-okqKFb66k|sFnYzXruj*{pJXf+YWyDl4CO%q8(_ZtW` zn7ih()y`e!*$*=iySA%}VIUX(J1z!B-Z+m+5%@%SS2KVGNi=ou+I7Wz{-c&U9AB`ozF7k|JAdKMKO(Ee-k>)ED~}{KMI# zk|*OOfjvfB9*J9Z9cCpKL~3{nHYKQQV!zvTMfQebVwdf5m+kkxgn*BdL#ZD{#>!tV zmj^DFmv^j7jb1X#4;rETDDnQW(L6$TklL(4kCj|Uif67{W9?AHAmflC`kwGi6BoWq zkM*VgEhJ0xSfrrvOj8uF%_*-L(KQ|Xm`!GdPcVbnI*}%uI%DvLq4qeaPq1%5QvPG! zd5m?lQ@VIzx87g(nI?EpFxwzX;h1;D)y>_tF!?-JfX3!5>)t$1R*X|f7i8|n?{LC=Fzi_?SAq9&~uG2Gn_dICrIgRw9&B7ZX=|-$K z7*q`XE0ZFvSAC3x(kFsKCj;n1+&o|&pQ%UtP<`W8X(7??IXmN15cP};=3&kXFQ!edLA4|o#*P4ffnn}up4SA^5c+VeV*1v zeH9)5>BRtUiJY6wrQN8ae|yU6kSU7d@}3v?#)0S^OZ}^RU_U&y6|ic|(~{QQ{V9yc zZ=<3#`10_8nfgJ&qS&e+b(xps>)>lXPGl60UQXrdb?r3tq{lX{)k#LpDeNI?mPhdrHlXtfuZ5 zZm>A`*02s@%}{AN#uDsrjSu;msp&*qAlGuYLe9s6gz^HK=wbTsR2i0mdrM}6HKuhJ zIKb%{#ho1bf)lTV6KhC&QsW)?jUi9))zJ!SuCLx0JZXi}SJZ}CU1jB>`;AATqm2`c zmbubGcB$)h3x~(U;(|JDt)rl(&)kr8+Np=!j7g41J76)R*}I4DiXm5WbJ-tmHmixSwlzSEyi3|kuktdN4b0nB?1v&n zuT{EAxkQ-R+ao>^ava3v(%0lG2@ej5Le)OqHs{>Epc*pQ2Z^5kNPLT~NI->@*`qB> zsD8{x5l1Br|7TdCywJZ>arFnaRrUq5=Pfnm$)f&`sWVkB(*-Psx)^*QsLO#LV4^Q1@UmLZf?4T%ATJB=rbbDGbUjEkIO_WLaFjtjw|>dxACk+G{~MEZTJk zk->0wls1+pD`k?4lOMd-0vbHcX-m|1gSJQCusJ7Sg>KA=&wtxXVIOo~u^tA$r=tqn_9Wl~yU#GQf7jsgfQwP5w9VEnZ*!5i0@YpKgklrx zpR?Y{sm1{++HyHEkcwz7h?;uMF5!0d#^JObztD+77|F{qHbh z1>(pdZ#yK?T09M-4Y2wwF9RlcGuHR5wp_jrXzQABdn^nSWRRcxiDQ7Xugj>4WMAcy zfuyIl2-8Sd@Yqccd1K@);eMUAK5}jx#jy5ae-ZVV{JLE$y)`M!xYCSU`Q>zW#4V`- zrH#j+1P5T-9cKRlzhvC)LaRC}w=b&qkAsp|!0+SBR zopS@WZI0&#lqt?1u(>BgbZtxcyHyKLo}=D+MkhTd$P4{;!1@L=pH7IyllzR&1uPG# z5hkzg(1^}}Z&0oT_)3;uHl?po6|}CCS@RTmKb(T9o>;F@4XL6TDpyOif+&>%&Z!T0 zp8sHI@We&eFr852!>?;L?55fZV&UI@d{jTT9Kn>KzdBDA zf>wFThRavQjGnkZ^Sk4veU4(ccEr^J3Af;9H>=ML*nG)7bF0o&T&=*3Ag_Q!`qV4R z94CusJLrJUzA6jU`gi!5&GCk(Gbf-+Od;v%aSP*k$!$H)s#1WW8s4DS_muFkSMAjk zpH`KpxDOxFw!@VMnz^MuUo*~J7?FO@a+6b=qWmNJm;GFPF>%`=0ZBhjDR;n@d07Oc z5^oCDIhkb3*t+%;Vap}M%=zd8RfV6MrG`lHfBsCm&|PjtcFvC67hj>3bG zH(27L1l5AvqG}1#hYEU^M}tQShz|m+&cRfv=JJ6n+F#Q%bHdgkKN_;k->@uCZydc` zK`0^*s^P!JE1_!QZn=D(3(X?Pv|pHpM~GTN>tyvu zD1(_lD1t&ayvQb$-9KEEeADP|^*^}O8``g|`_yasvmQbM-6*mA)4>t$CK zrp;ImS?BNY1YY5k$sf(00Hq5aW(79iE^I_>g;%<0-=|s1bpHsf%b$3}%2`+UE(@w0 zjdYgGs~?^1cdIzsi;7UAG99GKk{)R%+5xA#h|P5wE+UN=XOF5QxlCDc!(qZ5E+{VS z@K`Ox)ztMo)|j2J{O~hg>>#vLm-fNn_dsCI`e{r2L({xOn-QJi$m$>C7CNh;VqZXQ zJ$G9AHsUrJmRVo}FZjx?cJz=M174*sRwpA8+q4jFaRd7U_-Z5l$^d0(4*{fe@)h2%o0?uUqS*80)+^`|=&p1Tae<%#SM zsn=b=xqJWi#~LBj-)r%ZjXb~I%=*9r?SYL)q75KV&NF-G=D=C5)33*p?bU)c>^E zZO~ioe^iNk>RW2o)loHQ?yf%IaC}_O=C#4b7RNZG(ECeis+X%ZKcL>bTfvUMc>-)z ze=CV;fAVL}=e-{XvtY_kt3=SOaR+F%6IoI;_tMHXi7$^RK6KHv??yTVe$!srwXH}Y z`+S74XxpYjvqfZy0r`g9(k}AdkG)jSB-OBh$X^|XX@4sQ!u`%yckG0{MND%w+!%I& zpPxE=H@#c>OhtXVFPdgJd_U~60K?fGX~A#1vQG?4Zt>|R=3ojog^SAx4la;KZc`4- zkZ8#7?xcDLXLG9I5mgUqW6q-lA?Z68z>#+@p|Eln5 zsvpn&_&lUP_uI($OW9Aazn5G34#L5X|Cjg|{)i^=abzw$a|Y_t{B@MIzny_aI%=s$ zXji9YLsW!5Y&`nq%G`=tqpic+=b-*4{FZ(SeH`*=b10j+M;&$LdgVVG-qAiX3hBLJ z^JTBHk}%x#a_s`!+QmOS6JNdolM>(ze#s*5mPiRc*tf*^i z(-GY#D8B#wz%BHue3~fq8f>k!1@q5b@x1KGdmvPFE6h3kYx|e;Pn@036yK=J|4KeG zI$-cI@1E#I{eiZ_??$e=?apVJG;#VOf}_ga${J#WPw91yQP)|gzzTM(o|rY^cYVMd z+hL}_&Qk3Ydl=n^u${FV25U+Hb?MKokH)MeC&)gcd(j@J8LBLO8x3oY>N!2@|2NTt z*bR8)biI1NFt+YA!*N4ih|-1n3Z8lt_BD9+P$TNf>J52OmJZ==g(s+$*8UZGxmOxt zhx;s9pww3Ln0;?ZK5)YCAfDGlY)V9N%IQ!`)#NB$ZN4uTF*|de#1AA|LzxSRlJA6e zz^O~)IoUy^eHNi9h4LYeZSUSjwn@cR#b>RuGbqht8r;EOm8q1R!~IHl#rdGA{>Rw} z_ZYPTr~cEgaHdA^xSy$j@sf>()4Te@k}|744?yg#ss#nC%OA?Wyy@vMDaj61!Vlo5 z-64@9cW0vu6c-fng+eDx@2SI?_6RjbK?y%v%eW_+AEHxP=i3)jkD@sO^8nOXg$f&EDpv`k3r z)TaZN1x8V62fJ{z1gDuVbhdTXRS~d}I^>iu`#DXy1k9$z-Vt%tUhZpqR?jwi`s%@s z`US|cr6+y+ovoXav(NU2v(YEQDHXxGDb zK+Da44#2lCXMNE;0!aqqD;*aFD_AD@Q`Pz#`9r(ZPtLboTD=GvIzD;a$F)Z)r;FsB zG9AcGfAAPsKB+Je+Qqmjmwf>a=C=AQ4oXaJpAHU1f3qgCW47c>cbLcVY9}YRQp}pAM@?>O>hq$&{qp{?p~l%(!_L1Q zS^yV-Ro|C4I_$2C)drM+Ef=G#7EI82jPDN5oxdIW`FUZO`ylw#7}37_*wuEJqp)+z zTy00P)$^0t@8-ONKMZ7PC0V2|TJn374V2(y!0C7zG$WbTQMTWr zkhmYI+1aKpZEa?=l%IG*MuXj~s)$?H4i;==ZTcQj237z3H=tf2RjT(umf3$UI%f(k zC5x;3URFD+SpfA7YH1AcRgZH8ybI}b=DswNj%!8=4#gBDxa|)e6LOGYUl`MFRp2sy z)o~X1h%WW9Sd7+$=Lx)c&MnM>wFwC z?c_*%{>Cj|*LIU5zr$6-!zKS{JJBx8cQkGAceX>-gD(JEZ;mAUIsA&aXbNSW&d5O^ z;{W57r#5@^?zlgKi}MWc$jyGyfy+ zYj8d1mMJ@Y;su<`cduf>ih6SFg?|opy1AheD*ztV@&_y{*<2ypx0@-?aggnl5@=e| z;i$_`Z_EfoBqoZJx;GCEIKd@4(|c2&1=a}@BJ+D_>A@$q7565}8}5jd%*KF_ z8atPQN1zRUIdno~(Q5@H6n}^~w;3C?V`4Q8-n)gMH|qF>xY&&OhnnVzE(iFJbhvb% z{Q$)a(c@Iif2Oa`Xcm0?Y( zGq5niiOJl*QmvIH_I$AI%NnjmVq3xKXR7T^M_--DA57ZP4k?{{ai~1~5cA`L@|DCZ zIVDzZHoU=;(J}pBgPMmER{yHE8Yz}~jDLFkzC_N|tC+HA@&Uq-mlLho8VXi)AxO8F zOBx0%jOn+6PyH#e5rZtPSx=@m*3BFaL+IOD=lax*Uj=F1gokXL*3CCSfEw`nK=H+* ziI==8d}L8}mm72&tt%P)A`MLb>K9)eDemAW(2F!`19{!pN)5p;G2#ppH}uLpT1H@x zX1ub_*ZcrfTT()V@O8S7kTYBcspVenWAez&`2in-AfuM*Nb`x;FI#LC8nQWM-Ldp9G zGhLoZ<8>6D7}ZtPF!=jPy=j%!v%9<9Zcw~ygihu230Bxbdsj>S+Ie9qTEY4?KgZ5w zv<(x|z|XSUaFf5(`BNfg2}7-Ur-{{@17ZV!k4s|Y^PE&5PrNBeF-cNpLIROe|MtM= zR6AbrpDra953`+?nY$|Y91SalCsjm)R@%2cFLd&oT!)Td+Q0rO#n<`ib_7F;hV{E* zc-`&%GRn{7YvuRmLj}t|Gw~|n0zc7aqRND-pZkUXx%9|1NAiQmKi5#9e}pwE5GjlA49C z^i%_i8pAkvgpNDPsQZrdw|-~ct3{ERsM6|}a>d_4f)+$&n_c5jrs-78oYdgea*7Kd zB*37*qF&&f%nVX}a_EhvoS_BAe1YwMUFSt|Icw?^(rD7KVh0@%Wp=w{+N*K+vwyp2 zv_*Y>Z71&=qN7=Kk)VO)bz*g8f=g`_kxxCzwi!YPFUn_t6TUsG*Lwg1llTgf!wiJ4ad0N*peD)s=YfdB(+YmN{M|*$ zxc~h=az(53$(I|j9c$-n$rV)SZ;U0M_|WfE;aaWA^!yZjGzjR#JH1ui_(JA}&Suc5 z6id5Ju4lB$mv8bJ1~=}Og)ttT*G46Pl>dk2{H*1q8yjuCkbizccXwHbvf~U`gqU*= zt!?SYHG2fLR4iP_y~sNK);vRDx>BGen6F<-TuIqhXKFsE9Jgm5-)W}{>*iqn&sr-l zoEf?{`QKfJt8>6;^JT(>&=}!pU3)n+tLFtaGrphBkU0l(p!S{bte(kwL|`xD0{||l zkc$~$jdOA{98&JSw^EW^H$a^{c52KrEB6~EyJq@600FgT28;~k@HcTb--d|2p-mb* zidQlaj0DYu!|m@G^_QGhvE>`TAI3H*xE;BFC3pnoFR`tX)gVI3)6dy_xBp-UX_3BTZ{>A5X<)PZA8eF$ zJ|l5O@mIBZUB2d1=JSz%7(Tz0`%6n0cN?h4Vwby0=;NkMzx{wFXZm@-+=skloM%%< znZs?#r5LqO??T@%Lq8UFxS@AUew%lo#CyC~x^&28!Zx>=3)k<1KTLO}M%QN8eD$xr z+gFOhpwv$;hJknZU>Yf1slV%f-u>+M75R{CWZ=6d`551DKaJeW?Y6lZU#lzo;XK1$ zYiL6#pLxyvV-=cNpz&OBN0L51dY}PB_8vWM)Fsedlwd7XiO_kpSGgukTBRY=z4G-`eWGjRGGGjm^Y@l!F)?m{`k(kJrk+{<3_F~T`f@5smeob4ToDfrRoO&DbH+Yub zKJaV5jusYY#MQ!d>Aj2+Lj4@mUJ1!?IFtQO>6MRrJ&sS`;0=p7k)7#!OY7x@etuE- zPDIlYb`6RG-FEz*lhZ{HdTX~Fdab$kk6Nk#@C3rsLJoPDWV}%_E3&p>uKHws3aT*@ z{DVzgirirG4_9CLJv2d;|E}a}(bam~``U5Rp&+4gbSS$amhsafP5zO~ zn?2s8K})|{(!b`))+ zJ>vd+b!K*sbl&LHAGh_B|0- z%STT~-nPXU;FlYYrO4 zuVdHP!!P`gyj9ab0(y;po%Mu(5Ly!42~^eGQ~rUsYQ6xdC*Nt0it}oz7KFL3m@L(Q z$Y2!9=u=pB*t-c|q^w=H?EGq89m`7sO$<5zoS0TXI|wcAHv-DI4Ci&4VV%xqJV?6# zo8Y;Q1pJj8IS3Pk+&j)Z7dsbna?O14JDcww%tw{DY>_)HPoY81l1yAqM%dtbf95Ma z>*Fnl8i?9?W(&4-sQgSU4OL0;HQga3D$jXH0?ujC90=%vsV${LBg^6Kk?mJTvRsUK&Awq=t z>bLZMI;yYBGM^l%u%1cXVu@|9JD*~@Hj3!rmsXPFb{9-hPu4dm&RU&S9JioQKZ971 zp-a4l--{*?LZ5U9-?{lkX4vlx%20g7A#C7KOZjr&Xkkpk)5OHQ}Aa~(?kAt!o@h zEw*U^{d?+ye`^;VbYocLW~0iqZmMVnM&*@ucxS$h^`##-JT^Cnls!>v^~cx7az8T^ zz=^Myph>i2ioPP^GPbdduWRA*5%l#%1XRl}IN(02>Yi(~FVIBy#n|ZY{DyIc0%$Yc zJpP4jsH{5>AMYJYWdsm|$^u!XE{{|0o8N0~H`D63a4LVrk60M^6%EFeZ!1{(>^GMJ z@Y}d2XPdth!p&>kPfs41{Li{8GgJw4H|pPN(PgX;BIv}J_F6^)er065BM|Xx-M0tU z@C%UY`tjqo>?;Oa2jH4jG&`>Hq}AfrrQ^&xI`8q8J-o}WGidR~ACG-4gE0BI);hPN zn`2{dW;@Zvjknm??<&a0NIFWAK2?X=cJ%6T%usNLCa@qZV~g8SShe3lDeUP^-Bv4@ z|Dpae4ycPch1)%XGYVZ2zD(R3?AbA$jSXDOjzb6B{(H$Fk*M}@`uF&~P)HY)^5=kn zp9V6SiwVUf+q#H`@8aGQ-xU>f1^tL5{7bQCDvrs3%U|rLJ53aSOUDrVwHKoZ=-x?2 z<9(s34eg^WjY84&8S#QBi$1KY#{OTCc6U}@Q!aP9l$P-8%?Q3iGgroDYB+8@>bke; z{D3*+A*5UkQyZ{Cqj^;u^>_T(`OkbN%D*GIae*mg8cy*r4sdC`4!*7kD9T6x3tgw^nW1-iE1Ix14Up}?h%=!=bcNC6_u`()S=3OJd$v zksd*y#31i>MEx=JRFb+bOsC!6QRu0t&)g{R;qHYzbj)>St;?it`0V(ON*J_byEsjI zP2~s|ANQlv*eZLP?tJFYQYn2R<`Q;H; zKZxI$h-g(-H%mTgy{Lbqf9BfN_Kb}z)vES>kX9~SjH2f9A0%R1H=ugSXnY7}eI)$U z;G%k`#fRE@K=>OWDiw}IOqrabtn zrmCpU-!445*EE=1+yQMnnz2PQk%h?qnoCY&u1#OVpvQ4i>;Br z7bS(KRQLWAxfpRaw4x0%H`wpV!+z6D*I7}k3+&7I60K{_|2#h0UfxX?hM#6W^{Kmn zsS_im0a9L<^^*tOi}vkg|61wp*BnW5c$$96H(#Ss5+$$+Rc&o=G9DS>&2DnIr+KTZ0-NKi9X{yuDf8lJ~>{y z&n~VnlL|DFytH^u-g@y@I516hX(F!%e+isqOW~~e4ygaWdqgWzU~9oSZ~r1V zMGYZNT8#p7-YxwSIs7U@XEx$)`{~wwTH<4J?sSpdAEL$EAg3%Xh}VTUtS@rLh81-v zf9H;M1UVFz9xogvNc$a!%l(6n>yXi#)BzNkpnUG{LCH+9{E}dl1e3S?Vp}yUge$-1 z8Ds5_${h55Lthh&`zE&&4+y&yG%4gVd#&C4wKGYFB+;&Qw{MhObc7cgUNqdUKEAt@VKFxXMp_L^(?m5h9iMoBl5tf%y=yXfV4m zXI~T|9j9+#c%C=c;i0%PKGc}5W8S5^=ExRr9- zd`SZ&C2)-}sjju#YM$Np<+A$Kty7}c*tOjdx%!44H-s#zGY)oG|4;t=`|=*6u!pgf zL<{&ariW^DfCm=Ec_i2X;ExoM8CiZ1?S+9JCw!l!=p4a`goIW72 zzcQ;Oz78ig7d}Q3PyHw`M{X%R&Sw<95=hMja*k(oxo30-yxtdHG%QpbiIdarVHsC0GC`$1nZ|?z=B@zPrIRU{}E#x;tKZ-Y3PpQFs$~K z&x&XVNJ4m$_gk`r7J_0FyANHtHYQ0Fb&6T~-y3@Gy^K4p-|?E^ys&+%%OGJcU)x~Q z5roizii&V5x&l|=YYbi5#@)#-2|!^;|Aoe?)476lk;O}24G^CCII2w3F`B>48QR|X z2JyCGywXmt7s0B757@MkeDf?%;W8XFv%t1Hi}@Ii@+ECnyqN8glRe7R=CmE1r&~C7 zD}zvj1MpB}JL7T$WK;B)U*Ajl9%@ZTlnp;jPU*mC z!CK}(7y~voxG;@g+&7S~N(vh7((3%8y`*bIymzl%P9~5itq6?Rb#z5elC=4ADN>Yw z`G6(ne3^u?uCeO|z_4T))IuBYZhhVN0A95FMb)Z}^I;M8p474J_~nWV zrrAphfZH5{l@o}tn^VZVuF_65=b@U(^i=u5m1~O;4d4cu)W8QNVZ-MCvz3Bz-wAI; z2l5bKZJ+4Oj;WrQ-_(DS96qZV`ZPh-f_rQ^Xu@%F)B~El$CI!0-+yxTqDL>2D2EQ} zeK8;rX8tqmGLF7xD2F)#usHVjMkAhLnnBjeoAhGjDv*(RRrA`h-G?^WXK_@i8v;an zk7jQicH%Zn6R{%up3Lmdxtt9Eo>`lTHCb?0&v`6O22|}ii*j(%r8o=I@^6#ayxo?R z?}N7X1`B{Vhtcb8rWAyLBctPY5z|sc7z1q)JAYpx}az_CuS^`#!(Q z0*=EfRqgf9x|BJ;S_5kCeZ#urvOs>!@=I!>7H=HmxYkR=&T$GU#Ug<=%R2`G||gT-fOE8UBJl{3j$JB#Irr&aJok z3p}lSOdt9;?jgjMH#ls*DbugvtmW?2cFKhc93tlu>obC*3|9?(o?KqXlzG!BCso(h zJPL6ZDn5_5{es2!P+*Vm!~lU|5nbs2wz7@1aQH&HYlU~r+zsWHIeG564`x@W4I{9z z)62#cBJzhm8Jr;rt+BTTm;D+32IM^F>DEGN(e3v=42@`R5`T?MX7}Q9uO5X0JJ&b z|D~diubO6f_#4A4v(5AiaTPFSMS=D9`o&sRE0Df(MHO~%^mjNgYrez6Jhhy@JwAJ@ z>q*zbZ>9Dy@^jgeQ?|XFl=8iq7s$fN8vFrGex^%+$yd8;t35!f=XZF*pYkbT17e8| zxdy~TC;?5!FtmD0WW)=?ddu@Uc;LoxPg7S1q;lw{K6^6KEjR#26S$o~H3Ea2r=9>#(n;@Tw5;4miTaJiP^1*fcrRl z*_CXT_l!m$FO&1La$nRwQ{0j7G!xgp%?VK1o+Ev)NXMX|Kc@#J;}#4vcLHiBhnhXn zQf!qi>>sFnwD{HMTrQ?i4n!!oEu7s17drT?@=ayNRC60J3*16+3)cS#yIu;_8Y}SX zDS)h4Q|m;tm#j#{Lju`b|H_J<7U{WzB2wuv{seN~THdZU^ZJHQBHn69`X=8t@EBXA zJak^P4t%PKs%zlVHhNn(V$lFWZdzIM_SMCwJPXI|c)u%7_-cWd35kk>7=a9S<=nm2c}0YEA0+{D%I}6|7veTR#kz zg&E><;-%hG_aDX(GlSmGXkDfE@3`fqiT-?+NZDJmYA!M6^}}<2zhG1Q(vWqm87bL4 zm?$lFu5p4IIcX@geJC>}P6y)=H688z=M1)s3*?E({;7+SiOEwhANyt@a|_Cm{>~2+ zvP8OQA0hl5Z~nR;Or3f57>~$Fx4=Au$c~H(HNOk2uk#0Nb`W28s(d?2SzD&>a0Y8| zFAmyR65>QZoN?N5__+f438UgbtaTg_TJNP?N92 zsD2Iue;st6Zl66D7x;(NQNVu?f1}hXd@P&?aU)mzw9efv0RMv6ev?Ijoo4QcD8Z8U zFuH>`=Gk*OGVZ+uX|seQI@QFDZBB1~*?wobqOfuMhQ!BzTc|ZH*#ecEZ+QWhG6=N( z>nqcu9(hLX_eLHez;%^3d$0Tax|RXXhDOsfiih5Yt@-?6hr_*b(g;9!IodnSSif2F z9iyD)^xWVcCzc$14}4p%Ho(#2zRQ>-Y0eg!Nc;tujmWKbXd<#*Ud*~Ua97dMwt(4e zY;Nj*kxCtJ8Q^4wRF4w$#qG26)dL z4&j2DunSzM>ioFfDt;r}`TCD5({Tz-M|ImCp6zTf!$#@8QeWik5vz2Ci3y|d9`=hI zIO%iuf}ELi420uQWcVP;Vt)Q#am3G4V94>A&y){=ei(U!oV;U8|G>Tpx!j@L3&Xg# z&vQ#^U6%A%Ul)9qbUhvQ@&%G+j}re_^6yymq9l7G6~Dr+)hZ+P%{Rx4yvXnlv~sZf zj`qQ{nQZapnE&X_Wj5yU7(woZS3LRR?RL|EyyU1e8%|g$Lhfqidh1`xU^s>{IZADL zlO)_~-%4CsW@`oN4rd3%Z6hB-3^P=^%)|Lbt|pQ%u3Saa2g*3)ZDrD{h4!{1CCkKF zUDhBY&Pq3TTMZ`z*5xNj=Hc#f&_jPhS~kk``Ky{kjSWWyLu}Tc(SQ7wocc+c!~+XG zZBefcr=NwwIhBJ8)AHRKUuVMx&co2QoO8&Rqi0LuPp{=S`-Yz;&1>7_3SOhRHiT6o zkCT;VnSJi}QLmd=L$4$LlGFcI z^H+e)FD8%5F;Pca-WBe zeeT$vy{Xn=c_xmWnkPzvX+GB&F6bi#aAA&`v7+bN16BG96@323nEdL!1N*1Z@einV z^DI8*PgiZIh@pBiQEe1?JBnaTkA{8nHn8PH3z?tBhs)uVTAP%oL2)kLSpaZV68#QZ zGlqS}Qo><({%!Oi*-8Q(b&fLLSA79~*7gj^j;dWs@3QSkrk7~%zh7dk7*<~jY=IqJ zcPYv_km2C~7+c%mdmSNe_?$uxRbl?+VHrIkLPITffrCc_BXx4RJZ$fL51ItuI`bXG zM7LfO0jQ2%ni{JX@Xb#o_|qAN!`xj<@&8{=dlGE6p>yT#!}z5ipxCG!?+xJ5wOUKh zR2dg3Lj4Q1@i3+W$S87bZ|0h!VT5)0DKa&N>E!PysY8CLfswTxk(}irPLZ)E$ZSVO z)4Ra=a;)S5yf0)3?_!pR{croFn3?Ze0)nskQi{d0|47bdiyqRSpc<3kV3GPjA*}`Z z49Up31OnOAu}9@qJOlBzv9-CQ&~6Bsn?MUilYM^OK!9_`iw@_8ZO(1$R!3{T*$~$a zrZzi8V-M60Y26Fc1?ez9mt9S;PC4nXO3RxVT(^n!>yeo*6n;{4-$jRnpDv6 z3*gfeh~Kr1;QSwB?((X%S4y(lUpC}WO{MVYj4(wNuuX}7Oxenckt7LHeO%0ZCs7r4fHy; zT;yb-C;-_cI88kUL<+lK7m7Oj1@jCPWn1!TB+B|J8Qp{ekmA(IK72&9_8;U&cje~! zfQ${+mKEuPg7Glu&dPG0Ka3f;TDc~>6?vRIXdWrwbG|&;I`2f}Pu}xQ@({q^Wv)ef z$a3o536bBEa%fD>blCLFQ(-(YIO+r;tHorTJwwg!p=K7eL-@C`cjb|_&o(qKV^U{g zA}z;;?cQ5x+zqB13r1Zj;wsb-siVp!cow_tA)p5)V=wr510vIAw7TqrqoeD|3>&#kKv!}(c9c- zaeD->B1^cZY-P%53eCB*f~Pkdry-t(pZiy>zZ*n4zjM_Zp>1byf^o_<=rKq(PzYyC zdMZu0@S&tQi+Y*#eZmBK?WO(C4?yB;9njzL)b!>TP+MLSF=?slS5S1zYzdfCNi}SNlCydm> zNB9OM4E$m|`V>b`zL$&|xp7#l6sBm^Muk3!95g^-c()CCx}T>K0J-%vv&88@Rw9v{ zssFo2^^Sq)pqv*%d$gt~vKk&#W-+3ZRGL@#JxuR;TTixeb~Ug3yDZ@UIJ)jYw)^M* zbb2~aRXlBtwl+n#5u>$7MN!qN(H2D&H6xMHQngxp6KV!6YR@EXZ7Rf`NsWjd5=0`2 zU%tQplh3%j*WK&xbMHHX@PHsH)~E?)DZi!io8vzDL9u>4>53-B`{q%GE|-~TRu5mT zmEeU7(hpfT3dduj=Rz8#XYt~I<~>P6&7n?S%lVp3J;cMnxs2dgmLo5H?dNPGzwjaC zh5-)C+&$wrqx1u%!A@7`T9CM}6I#!+8^cSpxY*b4Q9`i&Z&#@TMew(j073-Wh#4kd z&O+sxi=6^)r-*;qv9mAfGJX@VFbKt`CoWjW<@ZLgcCGy~_Ocn)xsTDwj(2VM96W6! zfx*2wyD=8N&@RfLd$fXXdCzN^Vn|MTS}zSHpgj8>Q90BPYAqg4%`@U(>^P!;W0g0Ub!)_^>C4!eJ^)@QQIl5hIbmW^-(!1)Q)Z=D{; zbIdWSM?zP#ig)PSOVpNj-{}{E>N|xhdn&MPCC4U?QkTr-X>|Uo=tY%!#zab#DCX{| zU8W+DVb9Rs&?~H5kWlma?xPuVqnKZ`vR7-~!Mg%lCf)%f!#A0ZZb`yWVp>w-ID; zZ;RTPe;EXB<9Gk`Pftp=E7@k?`$`>k|83QBraL9$;2!gX=tYA304w2?JHJO5y+TeO z92<^Fwtb-B1izf@+}1qW7S#1Y;=jG^M7gZC4CGJZRZrMGYkB&7$*XtbO@7BPVU4%; zdH4!`Gj%2uBxMPeCnnJOp_t7MRpB$23s)6E8$iqX{;^}ZF$z>FBka-PLQ9+4`Yz#R zk<*i?i}nkzL=6v)ShtsdP%T-_DHN z)@WKLHya)l&6t${ut$grqWy$bJGLJ|Q~1c|*7_%x?4a|d<+2qt(|(G_QNWf1&;G6HQSAO(t~ z`0!sZu4T+J{|-Mxcmm3E+qQGJjMpy14NwUt8Ms3wE8l=a8=+Xb|Vt9 z+NhQL-!@e9AC}wVG$o?A!aR8)0p@%JqVvK3pMx~ti|2f($5#!sRK9W^@1AN^omhUo zEc5(nDdcnnZ7m;q!&zY`!hAK6%zSai_A6$5tvD-X=Sark?IlWTs_R1^|0Ldb-QlX- zNunWwwVor8|F((2ZAS~zDsW~ZEQAB}LK{@s$=XyE63o)xvWS|%-kCnSWD$pdlh8EggpL-Bo!Ff6k zawbu>H*#%OTi)Er?%VIxi6v)C=WCi`Y9(rTm)Tw)`nP@qq=j!UpIM;S#r=lyrq=RP zD$x_`U%2(@GoMJk($-? z1V8-@x|*#?17YIN-gwE&9FMkX=N#o3<9Hb$-C}YwQ(yI}(qj)61B8d}!$7~_`b}>a z&Nfc;zQ)n+Lnk~-RA+9y^WheFxgWN|MN+UUs!KcF7Uo@x>N|%#ts*vuFL&)hb`qLy z#yi@%*HCo(FhA?Jg?L=XH%?D!+$C;hkePB5_A52KbBBQZOj*mOENuP``NK=tky~kF zk^@_lY9tiD!3$2J-!n}Mw{!*zj$xdP|N96nvlU8l3`Z2?tm(|0WhMLw45`p6k#WmH=b-cbN^c@kJK`?-oDJa6$ zBw>N=W5xdG0^rQvP%~BkLI0az3u{Bq?_7`Smd7FtIePckU;B8@=C6J+1ZXK*N5H^zPR& zL(ZPtu^2?5Kc}gXrPsT}@b~-v=JBK-^OLZ@db7ye*7;WMv}tI97@)yI7t`=Uppv!*o_dHN~w3L>#yE# z8uKh=U5TvP_<_q04b8N94}I=!wc6B?bA6#}YU~JDs%)ar0^G9RA=he@`tSIk>~pT= z)4zls?-{*05v=-dSTNWEIN>&7?jzX>T90t#BilaSxHGNr85asc@5A5VoR<#|%5(;m z8(p73GxLa+9Vg$u56Kri`Hkrx*K;`~xXs3lx5Iz&Z)a)e&X2^Ee`2RM)Gn9Nlk#RgMB!?_&#LXcab5dBXZW)H`(v@Mb(v)r+c}ol ztRTgW?{;7IN=mHv#eEp|t!abcBh;j;k`D*|`Y#(^pK7@6=5qOjg4Us)YfxwJwqsOT zworg{K-gW*aSDF`za_y;onbXU$Uj;8D%eS(NpbOP?e*@S6?YMj%K-Q~wh;|O@Y&PxI^TaC`~YH5x|rCM1va70zv|@| z8^kEG^Yu~D5k89R=|f{2-O}NmL+rOu&Rc${)m?LrIKWQJhH#Xb98+ zbIH ziPs>ZihST#Uuqvf_UL3*U=c4;MI}bQfgCpgj89r9=C6%g{FKRRnN{`fYazO)BW$Kz zuZd$N(Z`kXM{*20+^I$ZX_Z3hUz1YY4zK$Y;Ji9`q;scENat=boqxYS zUHr*S-oN{9LtltI4q2+Xn~2AxHCydzt?;(Is^E&WbMGmSBY^jt3K71V7Hf@XcJUMH ztBmar?Qh?o!AkJ#PDse@^b5s#3C`sxhF~r8SG2>DeAX-I)ctJ0;ZBc-DYTb*rW0y; z#!hkj$n>Ml4NlCwNLJnFM~hEVtscUhW}ORONxaqy$ww3c`z|ym^yO0a^VJBF-j=vC z6F=S+Xr(DB)BwWUGPgeCUnwSD`@$Gg{T=dibdZy@c94A;_}?%<#Zdd6D^cPf!KL38 z$7@#RLCf_@%bDGC1w_NG=x1;0@o|{vFKjQgNyNT8AfOM*G_}b%opqoA$1Mq?%HfDl zLxz(-x2|T4$&IBG2u~PWEa1D%w(|;Q9H+7zO9o{NxJt3Ec90vNovqXE3adMCsG3%1 zyoMGN6ipuLHr-ObL~({~(Z+5tLZ*q^?8Op)xp53r7(>t4QzK+eMNu6d@87jyv?&O4 zc)DvW11rARDnqSdS&+}T_=P#~R>adPFB#Co0m8S&yQytgeO&rT`MqNkohN2WrY;Wn z9}{j_dif*bM@GxG{YGh7RjDX}19vjcz3~(#(R?V&V&iR!Vvn>k<%Sl%y@E$@L>n)% z_i8oMPOS)T8Zra+k|YfQcc*<86&WY)eSat^tt~m{=8S32A^;h;cTw8DdYv7Mh`Tq$ zaDd#F@(j!O{)%ah&*}sT>JltDz;+b-=VtrVNEb{@APlOq>6jw!IC-AvvEdb>aIIH!;bU)7T%)Y12*mOr0i*Jzf6z<0$F!-GAA?0*NmR z3xJq(+A`t=snf?=%|7KH$}>Nb+1oHq`s=&41LQsS%-t_-f&KB~5|luFFlstaZdFi^ z|8LW?-x0=@m5_7nKVQ5mm<2NsMYWRGVLbVWy*1S*HR^Ab zXSUaFKa*jhb2}Bb$32zEC;uBeQz?0)9tbmXG+TDEuX51vVH$-m%trPqD$97sfk|#+(yJl`OUf6E$+@zu;ctE6~qMcz4F@BSh0)v~$)5$S4*XxCHXz&{ww}W#p+6o{d0vU=+{>=}z{N;pW z-o9VBt#}k6ajJEo)P;L8;HUh{O=4>gR%GWf>Dzl)z>DXi~zFo28F z@LW>A8%TcR&*7ecG$zL@96rWB5aQJ8u4-|wC{4r76tn#Eb&ft~kbR;?>t)+$FZ$RX z^=`*)b4Dz0MN-FH@7M0Ld^}noDczT}Gf>EWc_+rn1@?tKJQ{piPTpy{72!12lNRtV z(cEVmKioIB#j`+DzS`R_#z#4I=F-G_<59TUGCk&{*(SHg@(RO(+vB0xdfV;!z0DeR zdl^EPG=DqjhDUH*Zx1m~dg^(>(~Hh9+lIrQ)CIAYw~FV)@uSHh7q0Iu@qcC%+y0`5 zey?VLK&fJI^^WA6{RcTg7i69M8d6PLcaJCYlhf8b{^*dN=8#LSyT^{F=R6gpj2aRX ziwfh6y}WuUP>EXms_|;RgGYxM>byjgJvc9Mjj#DMk+*YQ_dvhpSGwJWDIW zo@AKa$=Tz(AM&n2=h1+-P{oA6#A3A}-~s$Y|HH=!BAR?%(?Zn$DB}Uexc0Dfbkg;@ z&f!hgY)Vfiu|IJ;JrC5%r<{0JG%e*x9>(l)|!~- z8T;3n4DfoM&#<~ZYgS3VQ#g8Q*_F4cVt|MzvBZ8(pZUG>D92G?xLo4g82bI7;+5nD z&@)Z-!+~$$wI-61B9#ho`>;3k_UCbF1&IsblCvFJ@0~1T3xwKb{+NnS1MXe7vT!eu ztra>|RDgOI`FZtM(i92!CKpf|W~^{oLP#r!#cH}8{HjsDs!smqQM_{Eu#JU9%UqUb ztE=s7&#@^fztCB$_RFW_?v*>dIe&awaei0w%0%5wrv~f)#$`u0-`zK9nAE)P_VQpB z7#%b(u%zJhpmtj5Wkbz1glgJe>wJi_WyJ?~S+oA}4LAH@$%Q(o2VSL8PL{f|5x_-_ z;Pn@>c*opFAN%#$jtSmA?ZMxTDN3DJS!!^J&;=Lz#n@fx3nB|jt0HkHHX5VTE2Byp z8sDF~=H!EiOXGnh@3$ki4hC=ihYEs31}R2Crp`biwq2CpP1UoeBi0tRlFBb9ynI>f z#(~-E(bt?*zGJ5^ekU><^}moajqGwA19LX+-_-f-fvkZaZP(D0?KOxFs;PsVu>Rp0 z2`#E)o^Y(!CMvJ}+^n*l6)`YCi{C{w(B2?gV5l#)Hv`$zO2@^2XZ+LQF{{q_NNe+s zXjgkY#uU8P`Rz8&vw`TaXU@x;`_v254o87&e=$Bj#zar3knQ)(QgaePU$=c<)Vs_6 zc_{0*i;4on@XS2lACm2mIH7~9tH~O0-HBR2G;9bXf%Dd-MAIc8JYHd~Xcf z#qGt+*px`;Yk=wZT<>d$&+e-Fg>7*@ej4Okai{ENB)7_BrwU*f#maChBu`~(M zW38dCTjXl*%?JFF`kb*>_F10$#|04QN$qiY6Uee}2k}ectCFo#(HxMlFRv3yvpfQq zFaE~jJE-Mjq+o88c^gzIDsHa1$Pb9t7wNcKFsnBrxqN(k!@H|)+bUbqbOE^5?Uxr3 z>XX>|w=lvz7!TnW-u2LxFBg;*r#EUiKCZ}kufZPK!){O~+UI;~aVFxcsEs)0e2B~G zP7m!`_)?kEUxS19%NeteMnshMus5P7b>(Ft9-tAXfnSngS1BS$(0ucp`ud{}l|~)5 zxgc4s=W9k{S`o(lBU|nd*+Rj!=U29tb0|Jll(yrrE6u|XekSJIOV`195H~Apy~Whe zh}V!7D;KMk>&zB|^gg(GS;nPxJ|7u(oFxLAg6t3-aUK)D)p_uWMwpS$Lb0t+J}ln) zC0jCM;)Mb1+}`VA02d6_627Xj{PtA00eOo6-(DxjpWd%Tqb%-7L&|!1QVcXUE??@h zv%?U0ez0#}bJ{=R%8FDbt=d{V7&sZt5dzH}<9%4vlrwh=@k&hcpywAB{r>7D!reOX zrqrHCdNj5`Wia^8_>#{L4aA+zSWP{W-I-+}M&v;K*z$iHo>L-H4N3Nj`{Mn%H5Tr$ zy;FDNB)8S)7nWYK`+g;?WxZLLzSK4P%;!$UZe&gr^tbWrh%?5_moP8RCGH{u`4cZ) zy=M8X$bCH)5C_!Xw&EIK@&ps+uu^k$95%Z9DLnfQ&e6DZ2zUK7S|9eEQfS4q@ChmP9Xb2LtgJY)y zHzq8(`D|l52`Ow8q$N5SVCfXuycvidu27uvO?aLS9^3&&D4*9E+6CgB z94G;cj&X8EzC!@TmhXV62(ON2LF%zl%3yd-BXIp>?V$nsm8KXY?~@%ii?A}77xaqd z+|i1MXcpq@-hCLjs!+M%mN~xJXc55ZyU1VVgY@NI_XGBJoWQ7!<{k7fzM-8tzPo9^ zP`+{)i!ov6Z+j%soAF&8yMP{#W`YULWG%h3ASs=H-uidR`ZDrnrs#to3I849?(pAB z9o_|2C+!Ms1PE@#I~0U>SFRxW*i!16M=tXAR0fGu!4=(U7h^uQT^xU%&whu8Ti_)U z6(hw5F@w9fk}v5_6Oy9nqwA+j+*aN^5?Ah+-#_RLpRO{ zB8zVU=A*}s`c#jHM+Yrn+tgN&_4|Zpi*e_7I`d+#0e2Fj{AN2eB%y%FppLjVQ?nh7 z=h&$265X{&e+8!>{0S>@8U~0RT<@3Yk!9b7%P*TNqiYU)7vR!pYvKS-LpG$GgRZ|f z5O@MERry44-{yj^1Ld<0cl4=ruczNvn!`9?Eq&!f0Vt8ByJz3%B2AJExxRvQ0S)bX zCVGCHwHybCfxm_uyVVXHe_M*Td_YGsJJi&P&)wPQcJS&d%U9;xb4|3B6sPzv?4K-w zcBn(tr7`0oH2C8zBLxelFhJpG}P3tw};vM0#+J)s)wGtj26Uw^eycEK!#ii>Cxb9MamN5tq5JL|d zPy#xUJHSg4`3J~Rzn~LGOep!j*`))o0Ydn!P7L zG;#XMh&>lP%=q)-7iBe)%7j|7rBE@u8(aRQE*U9w($a_Zm$em9K12IS$RCwY-Pro- z5BQPW3X=2qSK>6$T^$LiAKI`>6s-GKNjaf82Xy{yzVF~tWnDe@?ao?_=W5BB{Vm!! zR!yUi4t0QKS8f8io+VS_K}0sp&Mr(V_S+hADBK}(ENV?HkuV9Y9d;Vjscu`@tC*;Z zwB^6aSDPF0q&=u5272lhI#JV@e7tacm5h`U+5Phq+JplT+n5p2dnK?C@#0}^$tIJ} zF%C}cCXSNL7YP<~F+s9kguCcr6V$Flhh~M86JyEx@96#enMSJU+h)m^ znjc}Mb`3x8y~lRc?>g>S2#SW~<Xy76;uUnE9f}8&K~riGQNC&wQ~{&P zAsk@W|LJKyY{CD~S;cS9#TC($%ohu@kl=AR9Hm^0gmeAny7-$G?+08X-os@*gE@Ove^R=bv3D{fqI6@42buA9hjjEiXy zusWQ6)O;`J)hvK2CP;b{Gwiq4rp%`waLS7tx>|CN&r&TNA;)? zYN&v9g>r-DJ}|1ycG`2nkc@QU57@EA=inJ{flYkMYbDsHDk=6$mcsyKfCzdx0X3bz z=D#QZC8eOIoNQs?KwJ^Z+J6+V21@yUWRGFGEE zUdxCQUh%C5i@S??l991UeNE0F#YsINa?#qiQu2@nTt$5ysKwTH?F2oytSkWSS+7bn zqQ@$&E9zuaPhmETK^ok{oRJlS@L`77HjXOqhIpKGK zvPc{tN_!Y)mEXVw%g**)U zQQSUoPxz8sV$8B;_`Y*RNd19ZpzpLuSdipteIS~a9M8Ywqvd2W*C5F{{0v` zryR~H90rr2=}X}_)XWaB%25=4>9)NCf7rKg+YdbcH9<%n1p8a&?C~PAEOj#eb7~&X zLxMDRhZ)Dq5B{~xS&rtK-IBpLU_~?nH@&PGx^s34R0ulna1DLiurP_RGIK_d_J9?I zOA~AlUPOU9J=W6qR3{(sM5@i!T!GvVc%nm*!ps$e5;2Cf2SKkN7>S~_O-aMJRy_I) zd)82{d|Wz`X9cOB_ODYzNWF{E-~hnnFpn99NrYvr2Liz#_*amHX~SUIZGX1ojh@K> zLXW$|X=A#WF-DT{9P74L3_9I}o9uE)_k$#f&F61C zYKvKSZm*Pg$o5Qh81CW5Xds+~um2c<8?Or_6|M!{9|G{Wuecy&F{TG8>rG(>_T0xl zsQlW1+fNj$BO9{Bzt@v%r#og;j5zLBwu=0f9VmtVU5~t2;jr;4Tf;LY!!+xBF%2?q zXbVqHSuf@~Q?HL)IbjON|J7caxqY?e9?4U2D9e2#!QtUD64)G+JE3lb9{>cRKkNXv zy}LI9O}#47#wZ$4BQ1Y-&Tn9>Nj52t;1Ofg8kBAOS8~YHotD30i_#|NDB+Q3|D;J{ zhPB@2`$*Srk5Hn{>3jt}*;a`O1n%7TbjrTW*a3bYNVxn_d}E>`CZ^FFA*CT?YdF~T zWwmivcuj8@03%1f632Ng%o=+}Ec?MBC^b-Vh%iuNRxfY6+kzqmU_IML?#OK9l;4gk z)RBn{%&(Miph%(LFW#QFMIT>R8d}M=$oA0)f=$Ve5d}p#h5_#vWi$}7O>v7+j&|^B zw?Wu%W;;&@C|~thILi>)_@yLb$pGtSM(zkZE+M>@y6Hbr6$%U-TK*U5wn_&@#tCTU z({i4wz)>{G&{@4vNFdt(eI#gb4_yHYQjgu()%yCmzydhF$eZ7GM@6rrIX^&yQW)|> zsuHjD+|H@iXC=o=#gbr75xv%H4cNgX?Zw5f`K=%y^G}}gJpJEQDrjt}%bfDrOpv|& z!FMXxMdym<3<2~hk~I%i$A;7cTUAD7S9{1Xl%TAZd6GpoMPDL{UqCEMh;~?KhzfvD zByEi^>k1|S=_|%Lqwtv+!etG*rJjKnZ(WcHc$cC~#)(=r>Rv8Tmu_5Wwv$r8sUD*MM~E7!Tt zk1(MG?3}1_I`t`%*^^^IY1*OTf?i4NUWnhc9%Az}(16X9+34l>a+G4wrx=P0_A3qb zF7y>~jq6nj^7HLZfYU4!$2W=?(1racQmrQn14~wrIM|0Ah1{UUn~*Ledi;v`L!K$w z(26(ox3p0}$3fFkV$FtpFTUki;`0EsHP%fL$1m1WlSMFL7*p02$3$k$iURX;L96t) zbq8Xx7(M!TcYFH*K%d+WUR-YL3Ip=qJ$F5IO6$4VnI@cciP+T!01K1mZmUCicy0k0 z^y0Tuk|dJlLOFF|w{yd0`IDZ-GV5uY)4`j7PdI={g$&#IYkDQ;@(F~%2AAnK(;vGg z+DWV+ee<)!8e${lEa-$fI`{1^%4`*I%{+Foo|~GzdZ&q`0rUNvwN3q0dTg3=&X;uT zhSdK)Roh0M!2t@|)vPp-Ji{Sg&9V=7A~s)g18M-)T<0+M{6-=a=y!H&NLxSEJ>zN#%CJ9L5w z8teK>Z)b|^4r*LOTdQ^W?H9lYQgI(%=_3NsHBt5ONS#5<__`fXn@Z!xv`}w>947i^ zobfmg0E0w!8|@iglBn)_lszW6^1LeFNuP)L>005zvpYHMpq1>{MPAyimgT?PMi+tL z1bYh_kgL24)EYemRh`Rd3MuiNufE6G=uk@Q?Y_^dlSjJY4-V3IK85Ch62-DvVdn}N zl91vxq%y}RdwYm78BK#3@r@8cozkybEB~sxV2eZQ(FPFRqn#nyy2cRlKijkYm5=yu zVw*N6b2=z`b$d~SVWw15)Z)YWngHzkeTAN1E=Dh0M_^e*Fbc1p>tZoT1f56AsOP^Y zBl)n#=@HwN2Ard-$O{2%d}+kazYdP)SUKEzW~|ebYy&X6HK|jJZ##%}7Ox?Txw>qH zAtUFJ+@vy)owifCFP(0&jO5d)P}$1cEoKNQxPPVP*mLKOp>AvI#_je?@|EbNwS(O@ z>93U!QpfnegmIxMi}iOh|HID^xuW%cIry{j_%3h?rH|kOY^lA{0HAeT9CrZxg>c}C zn_aZsbALJif20H8F`Gaz+4IT+X)sP`z1weu4~&6{@>GyeK*{NHiG`SdX$O>$4HcEa*(vR?p~hz zv66#HTJ(Ptx1(hj(XKLooWo!U4H0-I*aq6OmFMuco89iyLwYP8fy*7|b2G&gJ>l&k z#^V%o8_jGQ4!Sw10Ub4<{2_s4MYpdsb(#!x<)nYp#Jdhr+;jP@x<;!O62$Kx+{e0k z#z114{$9lZe7Uk+G#u)B9>WZ1P9dU~l@(y!fy&kvN|ZtDeXs15Ry8a)gX5AXoxB5y z0N}#RZ2{t%XTU?6;$q<%>&yD9X2MDk!6cZjUNKgHL@a+;E^ww_W$Fc;Omw#As_!2D z@&kGYxB-q(CDL&BjwWhaK(e2|+FvwWaHw2Ry#V8~2V;khY})jA{CG`ZA+gUV ztpao&CaJBzBaNlu-c=1OBjvvc&L&BLbpe!sRY`UHof5<~HF}8Mq8=hMgPt<1!)E)i zp<`9>fhRQQ#a2ZC>9(DZEV;Os2Q-A!Y zQjh3?9a0YuR(-(HK*F0_DXbP(=GBS*yG@-+Y7iTt zoA6BHHL{jlYKd(~*LtnWdUX>kbeaCtfz_hEe@;RYuKwt<>;}>we!!_b$rk%IT+s)9 zQvp?`MEH{=VIlYBaA8Y2xP+Ev&F3>UzKhz;X@n2^Qq+qr*a=&q4vxWHJ1sge;@qUW-gLH7D>$Ap2Q`9ilLG*5om4#slY2^ccl z4w@bCIF`n8wI4f%ROFq=B4YwKLlOy_OI(p2l3Huda~{tUxNwoIh3WYPg_w)uw*qlj zjWJglKezQ1I?b@-h7eSvh-a6$?Hcb`r>bdDS2JwVM1;)iy}+;yD`;@v8LCng#7b&C zZGDcj;h&|94K%D?tPv5#!h?QMNi@uGk5NF*qTWKB>C>hSgJuMY#o89ol7!t^oZr6~ zZtqu>b*SZqAUBUMw^A;wWh}m|E;v6cz&T3YMdi(k1PwixP~980Idor61VT`oaU8iX zsMOqb4@cF{bu6YJ33XhgWySLsvU}?qKX6u~x+61>+vAlsgZom(gJ5qWGw0 zJIGxcOP$dHC6(pk0t@PxJN{|!Zf-IHcYOG~nVAKkONe%bIHo;LOim3FW{CBe$!2fr z%4P?>@$hq4%q?g@*iqhw1q^ITE`R%fXQa-5pvNrT9H@``8AgyCLJQ2d1!yk?5hktM z{by^mVu%sDkER9Sc2EdPZs2&~fpvfrDxsUajFj>zE?F=RULhm(yY6r@VzirQYIb3^ zzexQo<5U zH=|p_t01zofWU8u7KgFXPjF2ZFwI=f#|!UfOn`#yqJwZ8hz#Rfy~SMcKaG50%$JThj-Gy(Fd3HL>7U zF<2LvY`*fU^{B(13PXtb4I(>-I*c`ii8TLRvMs6mnakoE0?b`GvY>G#fJJB=p>RcE z*+vF9jq=6@b%Jbmfzf55J8@c$Du%M1aJS^UE3ywNDDYqx56)zVEF=_w7B*o zv@U5)o4=<^Dz}N@>NiQ`zBcIbt#i$@HAS#xp^e-?kID>T5SW~+!%x$xupWM$|+v4 zchI2(9Eb^REB{V7jLzEb4L1Vef!RoqHaAzli{)^-T@RP4d6FFSa$8ooD;Yqm5cS!b z{;?@;?Xo$71zO>TRF!R*sur#(&XvQ8JE3|IT>ui^68$BQI;i<_V?39_{Z6axM|1l| z0_{w^O|VF7+X<=nraV@7v%!subV$gbHfjL&!{U>~h?7(Qq3E{*Btp8Mv4AW83m*k* z(#!R06j1RCj6E6Yk*=-(mt3ntJp-AWS;(jPhL(3U4r-kqvRD!Qz)Bz1=pnZbZ)hrF z{{{`*v(+}I1hBP4^CN1#mG@eca52v>JmkEa8fE6LwI&hN-C`((pw%7yi5l8yoBd{k z+I}R}n~ZE)9cEArhQdQX*>g9p$u%6&X>PE)Nda%KAn*O&Q-((3YO>a#HiN!Ygs|+Y z00iX^*W=t;&W>3APysrPKwyX+7knFtmiT=&HsM<(B7b&uO6~k1o+Ye7G@(c}PdscJ zR=hH2vY&wEGp7`CyH&st{?fj%c_`{DwaGbxQoefX#28>jyJnB;qa^fc3Y{@K*+6CJ zkU8b;x6x_a%lr^q?yfW8hU0yBw1Ea?ge|(vMJ4$jAMQX>6`K`6P=f2IjDx7wJF+Vc z))q$w07%wverN^6eh1h0*gAs_PS#VDwmNjsT zo8#PS78xgD9nR$4-08+9 zj$>t~&n3?_`H^t(bJ(JT!CGtpy2j)eGwmB(6;UtM74Jwz4FEctWA6=GmP8yiMPFnSQ=s8?zYDHf$YG)HqT!q<&~-OcS`m1}z!? zydpoK?!L)wl8FhHr z*3vpw@wp~8%at^X2$gaD9(mR0tFej_Oi}vXJaL~{2$sLLsw6WMZHKv8e(gnkQ7qs@ zX@y^uh~|JUOz-5^;BZW`FZMvm~MlwO^`1wUfNIc*0!@<~(l zq^F0UW0_M$#8{Jj_ksaTNv6)gSKQsgnz*=I8gXBKz*tWmR^9_sQs&quU`}{4uFF@t zGM15=;;DNe6Foc2Qr!9cN-DhmIx%>wwOwKHuEmFUd6W2c7&lVnzb|C3$>nm~I*wnU+L-^w(LUtf+@gf3tEVKUYI+!K0(_PR47S(IFB? zqyg&ruz0PQk?MQ>C1XDa2-wkjZL(cGL?m4&rdu5@V`tpn#J$V*D1A`8d&@ljQ06}c zc6p^ilSFx4?JKK{qInk@IXS27bbG8;ipS$SOG?kX5qp<23@!vuX2h65m97arV- z5U1uNU6qLG@3b$yWw=Xj+hl@b5%Hu2q$WC+wD60WGk=(6r@&iO@1f~xmp`2TDqkx@ z#aE67%n-p()fU#?m-qSlSano|mbW^jnz1J&=#ion2lB^C{>m|8i4y?P%IJvAzC;w_LoYZ_9$Z4Gr!U{FlXR;7>MO&1tgB$;sXqn) z+wu0viYy1DMR3n+Z>Q3e9$JJI29@wSbluqog3m7dfGbxQOal8@5fOHF@C?2!tV#Xt z?G!63qGD665{;WoC7vY-FJgLIvyYa4dn9k3!vpi_^Uxf4TAEAMP7Gsxi9156{6q7o zT!rQ=3zf&UPH~H4g>~|2DXv!&>F(Jii2KYUHpYY`mz=_>cwOo&JN}IZ6ayuqg0IXP zE2C)DudKw5*g0~r#iWIoJLbhr)xHj>%K}d}1OIzl>fCm01$m1%e^;6I7WURP!$Cqq zRVz8-%G$s-u!%yQS=k*V`Z<-v>X!o@h^Y+0h}}X~bH(-PGM7m?T?NSjbrq~>VMF4o zqT;B4H)HQCzn6ZdeeQQqYRzMS>5t@rewh-==s$fkh$$x-f`^AIc-g1)z*xolK`-|# zb!AaSpI>S-FgV`pWGZR8o(DadN1;=Q+V-}$#rQGjs)gHY8PjW@0h@}dlyWA`EagH^71^@epZhjtN7#QdNFc6`Z2aBC1JcGim#VF*X$s)(K6#U*{PEI zpg(zQ&bK;i*$YoZ7Pd0A7t)@0Cw%$?a7A7GcxHRhW}Y&Z>Lbx}Z~BIrgLO9OG?_nI zQx}%sR1i4V3GjI9uCnZD-sv=!V61%$F9Xg2|HVaW4S~Rw0u_E!Z%X0f%dIsf$%gkV zPFLp`H1=sEwCV)dlkUpI9l_t2%&~p^x1g_AgCYeE?_DM%JCsIWo3TFb=R9;S#eTUS z*y=jY$+O#Z<)3+AUd!CZ)CH&Y)sWkQWu8g=X(xIZMSC6wny>=!~sHjC^ zc%KQwo!?rk`j4U7c71EA#pXCUX5T^8%)zWV+upGDxt?bd)e>^IR1|+JR+fyc71^x` zFSZMjqOI1J*B0SwxGQV!dwH}5a-2+eN^?e38IGS@2_$f(4`ZE_ni_YMq$J5!S_8{C z7?aV>vAx5IwYOw4p5#iL?x<_^3%Nx(HB9Ysu71esr~H%9N=ev~%rS-4C`qWK`YOt& z6lwL-EzRXCvj`XeQi&so3}7E=7puOmuxsEQ!Krvrx{G>-xw%-Ims}KE>dIIGI)0;0 zC3>_zf42tc(7Rq&=@;S$z9g98$P)z@u=rmQQ1mh3;1(shYvw#7(_QU3cVB^cA9dR) zqcUUjv;c3xy~Vwqdpj`2X}XI>=b>%Gzl26k_#??x;>`-8Wmv%l&w{|BQ}N;=lpvrc?`BlTh7%?|xrj*mrBIl4~gmwBF!e zn$dxQ6J1(qH|Kat!Phn9ks*+sTYzJU&V|tfl}dhbGL7)Zir%7)YZoEyURf|?rI61I zGl6VrqtRbh&oV(6iAzsc?QZWnd0SK%5fFZ2EG#s~kQC!i>W$o~A5sO!_4)Q$k$Vb3 zneH$dsx)GYh=Z;Nd}}PW#KuT2#nBHA`AsY)r-vEn;^u zeIdr1HBfCcFpXQ=2&=d}_+L*fW_AoK|M^y7Q+bF3s_4pEtzZ1^v4$5@5=_Ux@xk?& z&czoRl=1Dv!_@KWhij#kkQhuziCh4G({R89a@v)*_#D@z@U z{SB=wg~J@pTlI8otImRVztSxTG^RH5_KrE93)l%O74kXTaNo#%g_^M?bgevW z#PCOQ8rK#!XT0$ytNe3@fr&Uh$dMjKsw>8A?H&d)6M#!{%En3$Z7f6L$I-xrzGqE+ z%dOU!7|C(6A&vSx;=~v0ONT00%emU9ttIE;X=|5|vhM&IE1#0A=uS_{)M7gfYt?d zB!2n4rTAAez&UruC8BjHoW^*R=EGzrdXX-;FoAg`!m8z}>Kt2=Qj_-ZrNLbwfj|F< zLHW$i{Uyndl9RH5=%lG{#iK7TQU{6Ee=Jy&Qp3XIL(~Oc89PI2EEMzP*DV^t4#h3w zXx~e{FbnEK6*OiX;`vR+s?h&w^qsMZbXF~t3xQR47r5R+s{*^GE!$$~5XTvt_!px+B-vUmJ0o zXXc}I9cePdp_-e5MSdFIx5RgHM@}&^jDMsKBvN~CsL=lC%th~9v`(v*Kg4EAh@Sh)9Y zMtMvqyfx?aYIOdWR6`3saK<O4p%Kx zJKGndb(fH$7PCSIu)eOaU9FBo)dnalHk5VNs94&?8^!DvI?;coPM-V4#WWrM*~sg- zsfRw+?wA7v+# zqVt>2IF4Xjxyr2$k}jp}Tz}p*SU8s6e*kMd^ZHmmwSTl8H+`L0Z-5INW!WEY3v&{;*hh4{S5tG4RqTs8os*l1n z?YU+&eLc{EE5|`sME=DQ9wc#$dx5HfqDt3N9>c3gEWi>y4>*Q*EZ}^K+&L5n+qsI-I^<|w zlpBJJ7wGKMv1b9ygfzwIOID7sx5vs`9V`a}J-|L&=Umw81bMnc4nPo@)TIl@rIn0k zaR00(k9goThv-MMPB$mXvS5E~QT&2zb4_&E`bC&F!=+)WV7R~urnhJi{TW)DWzuTE@;6axZ z`M5OHP7oT`TOrqLN(!6;YKE>RbSbFVUa0PW;z&P|YLV({*OJ^&M#?&*=Br3~t}>f} z11{Q%4HjUo^>4Vr2G%O%N}NR2qUT@t8LgdJjSjWHVx_UIt1Y}{0G__pl79tly@Z59 zqnw7GX4+d`XqW+$|LJp!7+v`SjB^!>T9j0sk{Zs62{iBxlRHc{_V+iy+q?;*N5!f4 zIO>nP!v2_drB=%JIN!I3_t55I-{3^)+1#`jeGMng?3%dpxlZDO7y(qA4vBuVJ!FM` zaDK$AqMD0>CsbCTQeY0LDj6*ME5_Hr(t5`v=d^Ck6%_;|^QfYxg*Evgmb#Lhk z`J>!L!&h2{<6Vz{|LoS7C{(VMXe#wsg<*oYDKY$#DVQt7Yr1_R*S_?>Nx)HbGVCq& zpOy9F>bEl9oG{bMz0s8acejZPM=$&0!(vX~oXPqDtOt40&LgKaRnmonWH*n zHLKoJT`r$Kmh#zF@rdh1f>c*eNGdhm-ZH*F6}QXv`!3`FuDrQBAM3x*gxnYBzhqHu zp1ygyhxHgkyV7^@)<*nc>P86n(xEOC1uKW*YOY}89>=+s(k=c)nWk!Zr>2Db;YOG{ ziJVji%jJ4Q@rQK1VB>R-2(n+kBM3qUfu*0bZN>kOrmv1`@{igE5l~W6krEYYP?Uzv zLb|(AKv24K3`JT(=`IPWjc!IaPU#*glA|^@#o>Lnc@B+$13ARo$TKbiD5L)xbWt&aH4^RQ2{ENb|eL{KlV zQSwd^8EXWi;Um~(4=-9TLIx}d2qmz48j$T{&XVs<>$bn#PqgRw z6b0^OiwIp+lcu!ZQ9tpQ_!)021aYh1K9SXEj2rfEP~G7RE+m0D%d-(-R!zx(&=Vl( zE=-CKvPBSn^~V2gtuDL@cw>D91d>i~(juB#Q@y!7ol!F(^<4wutXzIUn8JIj;xvCcF!@@lpmw?}i z?adKCv*R|AlUUF{_tn65{>SSo*GQ)ACT#+hI2Bh#uf)He}Y z@QS6n;PeVvsxPnefRmedYT&+-xv;<(+qk}K(=uT!@Vg5_LaAPco@cV!R_H&pQ>F>QiiBi3>62^`%aLv@b4xz;YI^AV zL{h1fk~Ue2=&L}j#+75??lWSHvwUD0MqxH#NoL_a0Wev$#GAL7k)f^1_Nif;K%x zn@MCsozdIL0zchaCqB0o73`RN(!YC1Z#GeO(n31jd)|#ZS(b|}M)fOfpB$@tKW`a* zsCkhH?C4h%Sh>4u|*Br zoF;9|uT5<27j$=mFRiBn>8p9IcqSb@G&DHhVbR~zc2eiwylpG`Jr^rFr*|()D3Lm9 zyLy>K$q~f@$^8m3L)oNX;>}>iLl2#KOk~yLW1V+X6Ld z=gl3ATa7-XtQe)`l6wev865w&|3RY`DBTw@>aK?MnG!`A&DO`NV@dqPzVoFxWNzf_ zYKnx+2z|uz;(rmYQ@Gg{dk?~f4|IESh?V?C)LP8GANTXV`jAucAKd3rxE&Kp_M?Xyz17Ur+Nlw%2aGCH?!}sky2h=B|YV z;poxer@U|`1XOGp-hc?Ox}tfu17TCn?n#5c*U*`aje~_#0!ZP*VI8gXd>Y0(48&DX zbWI_+^1PB?yo?S0y;X4}8nv{lD*erK*1?M#C>M{>>QJRkUn~ktIkbEhseC=*H2L0l z2Pa+~aO`LUQ_>W|CJk|Z&!0u$Z<=9L* zQ}558R$G|wD2wu1ZGcH(XZ^Fsn09*nED*G>_~r5qXrqaXlssmSiW*m+t0&JTk@5oa zrMV`~@~a5VhO2yhRrR_jDg3Gop5DqYFe8^iVTNT6*nk}pQz?-9>--65f+z880_g@> z08ChaB#z99ZPk|EJ2T4yE@;205gbl<2eGoYWp`R`dem}K&YS)hUq znv$B7?dxuJ_$U3RyLWT^gT$u!chxwF;`_O;4FX_Gu_vgHn`#}&e-i@u|FBL1uk{C9 znVGO!Ym}s>8P=4i3{n7*=h4VcF<*!oDK_`4F{C*EHf27l)Bdr4?=)vm`rpv?Y0^t) zUJGo>7Go+y4l=VpaLKD`HG-d&%{j(MsPR{4M@G>qR9nMaBS>-J19-1SkmS{zsmYT} zD%kR()Z{>c7Ypx$Nl{RIrzy+8RubeOqeE&ONY4Y*$u7WsFS1#;yzdCxtdNLk5~R`~ zxK=;#E-lZ0J+WhyTaBP#bs=l|*ze^L>2#X3Hed{l)b7yKY5j+Cs07#)Tg1@mpP5_x zB%tfiA$`2f6p&f>c{@00$Tg!5^ds6>B3^+A-@P$nJ{@^*nM3+-YgjVLfz*rNYb#Ea zOWnrhLXdkK;QtVB`M&53VP5q-v$=V(*LO-~YkSiYep=lsIOZQYG52IA`wFW|F{U!- zR6W_sbn@Do_k`RGx#g_dz>SsS^Z2?%9gy&Gn_}_-{Br zAo2BSY4}plr8B9EbN)?gBzIubr1|uRuQAjq(CIVnx?)m(rF_2>Nn}CDOf<+cN+KUF zqz#7uCGBC3yca=M{{L`3FxNWQFSWZOl2T~x?4Z`8GfLe-NuMLpkdd)pX3N)nM1M%D z)R|SYMlfJOI&Fb1!Y!`(zbbiw_6r1l(|odq;N>Hn*{Bz=qEgH+Od!edI| zrp(ysh|YgZ5)HI@Dpp0R@#FI=OTAl&Dm$#bLh|Z|joiziV!!PWd27~Kr{B|qh{M>= zQ!~UiXz^T~d9}6f{X+7=AN9%j*f{AJ zV^S;k`yRx1OfoTZN!B2`nS>3W{~F~pg1wXr;lNZrPSJspziOcSp=w^4q3;J?k<8RB zU}4Uxk`dogv~9)Q`IP)A*LP9= zO{{-=(GBQ0iQ?@nmtxcVY&?};U{6U`S@Kz4Z2J%X<$v63FZ!MTF^iD4Um(Bh&hqQr z3Q#syDR%h?@?b2<VYM$wI7?d8KuYN|e=Wc&9;EvGH}UDKGc?7fEb0-5dHND* zeDiX4UF^Tgy4|PXoB672^YRXBihtXQoVrXd_z&Xr(x;yH`kk&!wD2LWH0@(OX}zpe z)k2hT)gy{fA_3bs_Pz2Bx)r@2-mdabi=HxjZN7F!B0ZGfZb008J(s*l z#rHdB7iIO)gf;LafCaeiDUV1xd0?*N7*9Ix*hlL47<UeI^uuEgiS}WMdhi%gzezDAkd@M=lMudz5RBuvyx7N?Oy!Q z#5(c#=DolY=}A~^`rd@~5ojsGpRg0VG1*ciIAo;cRXxxu+sqV65w~JFg;D(qTI> z?wJ9lq1$n)&@avNWtb+bK8q&6!2W8zJlgk)-b4^+#-9aNCZGm&&xn z#rmHqI?l=0VU-|2W*kbpZ!sgU{}U20I@y&BD>e_UN=Xd4hPxdSM{SEIZ((R#PN#b;Drq)?+=WuC ztsAU!y)bt3&G>wwkY8#VczxEExr&9|P@}r}^@3>F_M9V`s3h42S_bk09Fxbwa^McVK&93HYQj48lBL|t!^(dD1p(#N?{wUSUjjR zGj6!Y;dk|xpGTiDi?yQ+T7AKEe)iQ2WEoE|03c-8TInNe`+|a_<3Z*cdJ3}8z+MW<(2Rf_-Q$r2rvdPnKs)YD4 z%4YSfL;Xx%Ty6GIV|(rT3|2n{q&6m!yEK{{q)R*^(-iejFzLQq{l(Lsf73PW=B#x; zvD&a-fO6MGO|i#TOu*g_gqw^Hu+|IhU}ogXBgcn=Io6!>4`Ty>sEyp8UR@jhFG1r9 zjB|Q1Wp!l6JQ3$&T~^E~MC_~n%02mleNj1v6C{eEG_i{Iu(n0UI_zo)h|OT8+w_(Cqqk<9=dnrtQOZ0=E9keD@Cay}b)%)-NZz#_9i2IHW@*VfC} zwTipwh@+Y89K2!_d z9cnj&>^*1m2k zEYD%vmu;7VOnk$ArtXQG+GpzWrReqEzQx#nv$t-p?A4{Ct84TH*6IoDF*2M7L$I3? zjSg1KBKzfhfD;{)pBwBKH6(M03Y3n#gy5>P)wbx~=4e{w+kcIIr~ z#m__cA!MNqA50U4ebhNmOW^C_G?$H5jKDm1L03LcJvjlb zP4xpY<)Bf-VI+DR_m_zhj%XvZ7Uh|%Pj=U(44@jm?wsq$dycNTX_ z+*UNZ1zszGPs)y8**LC}?W1md> zXMa0-*vLtXwTLRZvPO&d1CKl(7uUM)b&Sf$)5@lle;xZdm3f&Vd>GM5cK&HboO;vV zG3p(#l1}>*#$BNhVBAWwHwET>L88~O$GIqwAe}8~wYy}+#sg6wJ75!m*$wV2MIY9e zBNj6A--6hfpM#;WqZBIA^Iw_=#PQpO#Q4F@0ApZ00vO%zm1ng}rU9xtp98GQzS>2m zHg}NHgKo%k72ijbaBqK>=TIu2-ssqVqPb{wNm!H#Hw7@l9$Cd*9(T{_@+h9REV{KL z^hjFO^jF=4E2e(jJle!9?JQN6l@LP^3HEyx;cVfP}Xu`)%a}?%* zNVM=t^0uO`&4tc>#_9eiU-L#WqGb+4Q#@r$8+R48bfdz2vnd8uVeJ}Ks5tY$kF5KV zlmS`SbgPAYlwJOtU@hZjCX&~}$qvKwj@6ITaAv^g{i!Or_A+tGv&$^1v?Z})ppWdA z>6d+{-#TEj9Dlgp>a9wmNN8eJoR{UAb67K|?f%+Z-b9s87AIb!Tb7a`xWM15_o^{-{<)ts8VGW>Q5Dg*SOD} zbMa4rEIBRAw(lCCp*BC!4y42aC(C(OdcV8v;E8VzurGA>=4ZbHcT!!_4;6CnCRR6n zhpkJM4_#P=?<)q~KgveeGm2m@zxIU0BPrxkwHiHFE^@PJJdWF=ZL-bE{ry`xP}77V z7#SwW%UBH~e%`Lf`1xLcmSdqM0{!*2t$L+QAtIM?z3ZTT+c&JwrX>syIb-SOjLMDj z+~)wv^s7A78VZyRKa^PY5$V=0^Ryhh9zZte0A@I5fQJb3vR!fZFOcOV&dHc0(RWs1 zI&U~jJP(qubWnD%RNO8=pkJA#edj?1YmD7GFtBnlkazZG+;dnjA6hV@W={1|dE+RE z9dlJ3Bml0Rii(sjm_!*V{OG@J6Zlv;LB^U6RUdDY`h>6lepjr~#_R=0q>J3XyQgq> zmXb%#{y2-z8o6@Mdn@;sfn~T4Ti&Vdc0sAXB+b+CE9QT!?@l}ODntfIKkw=`{m9#I zbBuT*vl(Jyl|u*ZJ@pa1U>Qw!`7NR&81UL%cUM0-z$u^Y=F8zn<%n!ztRIBI^*Nv( z{6^jdmiP5|DTsp%Eb10ZXOidNTyUP<)MCf z;wRiQ98M`391V3GI2;Ia21H@r!xB*`xCa!QppILR3+Bm%!+KRx^CyE$|Y=30br0;@Q}2<(-4Gfe#-&mpGJ(y>Go9?unmmr#Wk;9mG}yaiNaa{I4UN+sV%Ox~g=V zik>e5U575Od310o%KN?O3K+;qH_)V5TU)xhn=tc?Vow3}MzVj?x;zZ85RWfkrrh=E zs~JXID~%-o8sF_*@jiR{tTsWQc9pC_r1r@7Z7wuVd5GM_hI6SFFivH9IcCb0zA;aP zNrc7$Za(~Xw5&sqSb-w1G0l0m2!Rby- znfz0h!j&@WpF6p=?^a|!8p)|4;iaFf2i0YwE)V;1CCPXsa^F{7#6sVC zb-DzIH6UfKc>finRgep%UfKkCo0r4`V3JmSC53wmYzetql;T+bKA(F2pIS3d=cx|x{ZdUCUzd!}mcpC{2^^{lH1Ul8RE?+2hV(Mp`Ll6epwXBB4rK zc5Gdd2~anwPp}09gPy{h0Pc2vCfkp<5)kQ3pYU6L>TWubNR?na*kWkKyqMXhPzz(I z7*pvqPU}_G1$NiL9!Fv$P0va?P!Z-wzo(j*iFHa%3QvC2-UKT_hv({2PGQT@0XeE_ zhNng`Y<}r8iPsJqHEX>uN0Eb9Gwu9#t#Wt3;+5=>qydsI1=-l1Rpx`?nmb?(8VP^n zWtAJPExI22P-v3{1fxY zn}|ZZB2=yFB-rC=ia*0w;$uy12tjn?&p{|xVnJ*SnTU3`Z?3Mw-NE%~%L8z~Obbz< zX}A{?xJ7&NPN`lxb|p~?#B$+!?q4oIZ_`0z8?N?Ny{qdATmttPEw+)Cy$TlZLfJ^=qf9aCNQ;s+IyF zuItdAmBd$y_Y`RhI_Y;d3;Uja=j)6)h6Sba{!eu*bVw!W} zF=;PB-)9>2e${xnM*i$DsF`dN<+s-Um(Qp3R*6L?Y<$3cAnW8#n|~`aW443H$)az4 zYQWCB5%mr=e$21EINV)VH_G5>{BbmEm_I1=XtQ%1{O!|r|^y3!x$o`j#WpLk^^9Ab>!A}qIbmrukbait~)7LdZ3wr z6QJQ$>^h#3{UpgN@pGVV&AafAIuv)f!hN2+WlJ!KtDO9bKlB{4whKCyC`hVhR(Pj{ zIGpO$w)$XOq%ZErH)>RMJBMN^)8GkF0_Upg-qGy@ZPdYMksrSnTpNiUvKK-5!Psyt zP+0>t3FAi`qxcrau^HJolmmeQRXZGH9Sy#;rs=kUT_5hv*S&Z(6>di6+{J)@n=0bK z2Z(L?So?PS&o5Is?n8OK!;~F^adS>u=*tgA$hg4UhYe%S71tP1#KafT?yry^`IXLm z`~8muJZ#;0#nTI?IptgHnVT8$jJDNQq~0_>7FOP`0E)b{pW`2v_?SkGodp1%AG$Ab zeq%^~)it^cBebJz!%$6z5$7nk;mA+?(!MnY&=OPhgn-}4MUdm0ftSqZ;VgCJkX6&sJe=I><5d$qs_qVU8Od+R0~<^?Mb{LTaY zb&{N&vaH3;4;^)TebVW_Ci>|R`sMm#+1kOUf4cnZ9<2?1p9DzGz!m=puey)7rO6EM z9cf0L!V+F%F3H&-q7B}4qX7w87jGAQV`K!`^gESw3KeC2;cBA(H?YTF>YK^0N(qdo zFCBHd$f&=FwuQ9mC*ny$@*-Uceg|dQIosS0-gkMoqHC#uU-eH*OvZ<|jtL7It_%vM zvcPzWd%4_`^gh?59d3ir-7H%a~sMPbLd0)!Gb=-y9ha|`>j_mL$pLH0JCKN@+?pQ_F;7;ri z=D-rAhS2tlhj`8Cz^jO!ogdhj6WQvn$tgyEkV+Yg?ffbdiW zeJ)3O-8rbA3Fr-m#utZnNH^yRfmo>)YSiaY6Rzw3l;{GW+CuaB&>p|ML%O5SmvMN& zsN8mt5N6Sa7xnV8n$u>Rt+wy5Ym0UaAS*>N`RaY~YVd}YQxvL&`|wf8n(VBJ&U^bv zW+Ht;xEQW>e|?p#^65Q}{e{$`H4{SQ<1q3v=rXVWU0h!tR!Z6|0ruOKTIK7EdCjnL z72hJaX-U6fm5e0!9)3^0(Mo5)G%T5FoI>{fmGhwAei1$_Lgp5F*IAb#NNH0pL6EbF zc1r-gp5@!|qN)6Ju7l6G*!untnNGGd6+m==)>*+y+2Dp~m{f29;5X5?zxiyU1u;1D z@g|9#v!i}xH3yRWg*;yeif*|!NJu~uE)6^=2i$hIM5Glt*D41b$EcWQ1E}^W;@v0} z>OpQdH&&P?Xhq+G1yq5+XOtUXr^{!X2 z=}cfG6R{bJ#B>AKdTuQF?GJs>b()0*R@Y0ITI=2U#@(tk2h49Tk z7bY$;vJ@NbQGMiWabQ(5Y8S0CLMhe4hCZHdMDEhBL1kp_Y?pp8LBKc(T_Q*AWjy?#!&kwq_t|O~m<>hH7GFRY*0;AQ zPv=yzw%?#|@Me1zxNpb_9{}t{pQPE)wqHoQl&x4hD4MFu9%To`nki7jmI{<`Q(~s# zYS>mrn=Tn-KT=EMToUBC4y;7y>IWS43roGh79G+xrf+kb%Ikvtj1mh;$E7lI=Wuk22GjE zh+rdOH*OP5E9+Es=2rZgPdnlE9gvN{8@tbcFZzU~g2|lnwaG#bW!UfWI39;YNG*Kw zlW=b;vVmq)%L1kT0LC667!$d$5Q%rJ4MRt`ny&qV*^%we$Rtz5k#x6yLFD#ue*I%5eB=Vh@6|=elq)sPAkXV& zy}QApuWSd}?1fKy>Q6!)xBG;ZLjEPjX8uN+oaAcdGntVM`Rj{|*O@HkUvF=^&>oj@ zbmDY^>FhIZhoC)AjtUmbM#!>l5`*mb3Uf)0nS3&ZTy?gtOyv;l(X;5G)d(MKf82oF z%ke2;iVb**>-siSqg>wosiTkKF-D9t%T8zc>>eaZ%slME^zJgQOaG_`8q6ZM>}^jT z!i%H3QsmzU0at->R&+ZJF1<u~NUT&7b~|75G^>pKDsI zr7BYgfjz!`8G+WA#oswi-cjkHU}N`k5bnqQ*pO4T@^qVPYoWG9O^ssNotvmJ;`_&3 zlb!cfo+x4b*1XaF0ikenM1)@|vL9DBxRi91-gy%ib)Yvm^Am_ZTS^J&WYo)}Z~Nsu zuHzSecdkfDLKdKhwJv^N$1LId+jzt7NEz$(LJMWBS@tYt^TNi4B-rl9U4O63-7n+o zjeSW4PTRA?madQBFK1I#>B8WLozrxCtv@k3gQ3@_2QOJJ7{7!JMUJP_=TxDDbJF*0 z_>{Dc3wVqwAExbJ$`uR!PplJRA>MqBi6DMZCKm@fUIb;LaCZ2eH*KY=qXC+&Xuz zsB7*$|141c21BW7S*w#tt`B2PqSsU%G!F^ooyaq|BfLM!d`RZ}XoUgqq&rmQ@5jDV zndtK6!nrDvdd@70F-gKVQ}owLormS!hJGxM0qZ}B%6W=86Yxn*u=cAAM)uU0bGv^J zQ&=unrA$6F8%U(gQ4u439DA0O3hw_!E#lr zLQETnjBkS=snI$i)%W^~={F3Vh0w)Q<_9v_G5AdM^@m>I~0UVUVkMkBI4rK|IJX+T}_CYcD zSknZL=O`Ys%KYB5u;`-L&Z(Zwasm%ip6mAXoSHOv>-Wz-=r>c6*KNvqDoiv2EkbG6 zx|V&bCTP( zZegg5&T{(EZz)P!F;AO%PlxVsUp&naWawH7Y#;Ufa$13Q*WWU6Ck5T7%hLc;%q;zp z7{rKqo6wO$d9nxzARlxKyNE(3DoNggv15J}EeSYX91>VocRnKMcL~t<9w+%?qCt-+ z2TNGd5?xfwHnlp_qe)ARK-b7aNQWQI79?nrVi&vySE|dS!@fGPS|A&udo3>qQ|uxl zS2>Q9p>0j~sdsw_SuaV@#coLHago7Rvzu+BBb14zF@ zTgF7L=hf_UP*r5o+2V5ix%bC7)3Ej1x~o6CZVlCj7**}`s#ldgnjL#XyhdG7wMw}R z{8K1PBzNMm%d0hYFMH4{PIy?8jrlNaqR%rB*s?i9-$54F!M%Oz(*UW>sKX@moUzZ7 zL^R$>dT`mi#$SGgS_BF6tuW$v##DaX=bAaDoC>I0w^}vYu&U_~p2b(*uTq#Bx3Rl; z9M&kZhT+1!=UrR4whY|*y$&fvpht3fFxKy~={>eE+UiQ=d_T!>S2vKc^H(Px+I^R~ zgWj=g09iLJa6f+XXyS30p7)UaY0{L#-FPR}hs4lO zv8uPta{`Uj5_2k=&IYWcf0Q*E(X#}Bj2oW~gDZ^gbR(187k2(|4UDe0Q%IlR8gqK#g=77e;=vyX;h7-RwOYQ?}t z$J}`6N3h5#%ruNc=FO+B97zxz?;T@D|I+Ug#S5t+51p)&jJW9F9`poaOIeS#)Ov!S)WfmG9tXZ>;2v+ zE%w+w&U0K;0jPxsd;`3t0o?w@2Wam|ChuzAep6#R&l`PKGnk+EydxsF0tcGgnaLC3 zSx0@nN~U}^5_U=K_SVIjYNma*Dec~@4Yy^i1=}!S7iblLG217Iqo&l$tlU_sMmsPh zD9TYPVO?(K3oeOQ=R-qZG--B!!4?E#2+0X7eU3l)#Urhf5f6wD^#WulMVSo0w@UOW zCveA*9Tp!yjjPh~{ee^yc>laLTHpzfhryk0J`Sg$M(Q=WApO3PB#AolJxAZB`#@S= zLg{Cla06MMxsDUr?z-5^Z|y)G0G1wZn;&`?_h)es#d`PCjsOh!t#C`{x%XFms(p=WQz6gySTF+o(5Y64zv-QeJ~TTF%~ef#6t6a#yK)j-0@M zlr+>JK*Lt}4oMF4c}H>fJ*AJt>j}XDRbHL>jRAMC-py9!Ccj9Vk_P8OAM_TAI?3pv10TuaT5^pNoFVc$L+B=;Uhm{ z=AfCboOthavqxgRy_gDepPYWs5`w`@B}@#hRdH*y2DsO1%|#8yr@5}-NPEZW{S{m zp_D#a%O&mfupbtq9o?_l7b;Eo)cz^k1JKf(U#o&LqioTSCMOnBHYj&w2ovYUsIzY#&H2uyU zkv`8{`tR{0!ZsJkvY)-i2H1t)9liJ>9(byRyOlL@Fc-laYeN-SU|Ap4N!FSB{8uA< zJy5hhGYApd>t622Ws98W^7~2aGXX%zYPkwbP+{!PA7Vi{_&%9=SOV=ak8-<<(-2|T zTGRbp)5{w36fHH0aQ|aN<@}6%f7grxX|{ZU`9;TI0ZTZyTz=|kWWE-nvi6h>N5}ID zT3D8fee3!w_i@wG53m?N4_9+KD$t6``u2NG36P>ej6Yav zCIhl=zuigi9DgcSiPM+h@0?^D;aY@2RKEoDb60c{kGlR?%UQFo7|bIfb*UkJtGWy| zhX#q)2vvAZ!Cwr9cfSw5-K5zIr7gdKf*vE1cp`m!e7R+AY@dfELh~+{^bi?l%y5eQ z6;B`}V`8*L`=|TXwSmKP(u2|u76pCI;Z(RX3xiY)G5|RyMQjb^@XBG6$gB?0q5TZ% zGoO`OP4^g`xdRo4BYmYeFI@z#yk@F1(gdujPplL1dhTqnub%vMr^2e+7lh9NCUVhd zTv?ImobAK{1k;K3Fd!CrkaKxk{3D#jbYDex)#;$;T&IFHW*ZAn*Z6{Afj=ra!=%ql zpB4v+cxB-?nKi);#=O>fVj0KM(>98-Rzm)3F-#x0RvX_*cJrm`0p0xhYv6opXIhe3!_F-iwr%X9pa6OsGgQASHdRSx%>QKH)&NT#@O zrQ{(o^|N{1$rOq9CW&9Kw#qft;-QhMj25duhCiT3%1!xg_RTl~H_j$iZZv~#Z1znm zu_SLJ9cRA?p^SvS=b3=OQu9Fm{;bK&oe}HObKr_LTIqwX``6>S<@<|O2v+)WGE=5R zi^HAXlhxyC9zjOmAliQ-?Ktv~mN(n#=Mi3Li3d+>^Wrn|JS6rm9GoMB4h`KSaixC( zeh94m4a;%Puh@+U820-i<4W~|^CYL^s4_%h;;e)rEARfmVZJ&!4|?4*$pw}$XKxmE zEWtW2&opU+;f4{t>ZvD-r*uiX4CLDl40OBZnEf|bIp|`EHLPtdJT;{12R5`3#60fM zItAwWiZRMa*$E`8Jl4Lu1-y8C9v&}|NRZk$eADiUw>CS*pZhghdY!$ zYqroMr6ST^<3_J<%XxKuil_+&5-phlQM^s@DANl^j;UgB7~X-`EVp#}%wUENW6dw8 zH)8WwV0h@7ZvKP>R5%JCT`HpuvDb8O84zq6nhckfj;h%RM7_B3MioWXiirZzQRT9+cYFM%*4%omc)S6m~K z&SclM{YSidSKkItXd`6iQdnyIS<_P0vqdAo>AMb;4D9aNfTqE}au6LHn~OxbWl9VY zk1le-(ojhgerrR5@Sr1wHWV35hH4%Lem|=!oBiH#p<&yp8MZR@d)1P9S<2eSHvBFJ z9<$pM%CxdyN40Xo^E@KW3237*81Oc7CX;T&$fvgzryRJhRW~Sl-OrZnmsv>vIj?8OnQSL7gc(|T3(!zd*_t46uM7{_?MW1 zHhnkOs`zO}8CH%)y8lFo*yx^n&iR#@=)!BWO@__}iZMKYx}ZRcwWC;%y!|a&3uY>) z(fd_g@zSPQAgCFZFsgfsM1o6lx?@wd^}6PAOOKh1A3SgmbXtPO;kZ)~jHMZqW*i;t zaA7Xc%=t>XguBwxh{Cp;jb*1c@x(W>^oXt^FDBYu26ra-iLL zXk4__vXih&#wo6XFOCfWKM8nn*580a z=+jkyTDmYK+U(qYXjCx_>#nKCWe8208g0KO0Q#%lG3R%gZ6AGM8R7R=nkhDna z;K8tZ%Fq=XOA5##AK>;XXyie(qd(-TzL(uOm|_ucZqjr1sXE_KD|swGP{?=$u?enF z0{p8qv;(%8bS|Xi*%aUkuzy9cuhjEM@45cyH(74i5Vr*{3s6k%#NjlJ_BUm}$P9qE zn1AHtw@@m0N4dHHTeX8(K93h)f*hoPX!al6#&Sw@i#WWY`HNG9?Ypq*)iTD+?6TL6 z#2~`~NbCZyb&Z!wz8*=GvWw9Z=Ws63_cAscN!=I(l!FAB4g0Dh-ym+{a`>$lZrk=x z1rQYcckD{N(BBG6w{pCsSPA1V)wcq5evO!%RQvE-Z?qYPZ5x*e?JBv>m!X3>qK7VY zmdN0S=-!LLOdTPtg-NgXxwl~1Jfz3h+y)4UuT8z8PoAOulCoNWiyJ&|Vw*skBxc}w z7HohFhqY_5gPsl!iTv4k6JKm>x$^-?n4z;C3WD&GCrq3yh7oOGFU6@QXj!E&+eoRV$Ym~Es$ zh?st+DuIqOQ6sv&$b@OCGDBO07$2;lzg_Q6o@wEKHDclo zne0mD3FTfzUb!%&RpxiiXFzsBf4d**kfsLf#G+na@zm9k-fL%DGUFdNTgxviexjXc zJuE2fx}x3VeI~Hh4(Wkh6apo}CupZ5$J7)5Nx^xXfRGmi^uF{om#tlbbM3hGn6T1i zi(VQs@2Xkk-RnrappET;3z;5_I;Z0PpiKmk^^YV9K>tRc^pVe+XWoPkS)Z!}$YMCe zyBAdYpsW6~@L|bDci@j2xPg#R5@D1Z;dy=Dh%xMek+G1xve3oy&5lIW-n3N?v`#!b zi7_Cmcv<$0?ox{@?>g>F#DJh5ZFl4D+q`___5U!aG)U^P*Qu z`08OtxBXguV(E`N-_Pi)&Qm|AE5W#%;<4sc6Bq;3l8$9cZhri~1OoOl;ql_i^5{1zi7a})@(`s@_k?bhQ1j5ACTc6$gnhW3?M zQ`+7c3Q0V6rEzj7P2&2i;8%P76OYwya zh^FqN?$P73H(lqIzt)0d%0FNj2q(&jvQbQ4bRC~kxyhYw^Zaf{X?wP--$Jx2kn^Zo zIS-q}h@967@TB;1I6DgrXUdCguM1M{IkMw5o)B>vU3b*NEFlRsf0w*{sNLrYY+#$w zuHTwHeS}B%DNfml_c=~NCW})T6~Ex`T+yx4COYLXJidI|QZ^uuGC4B=sjY9iJm-$C z-=mdGJ--`3Tmks%H2vgly+myui`rdg3&dm1o{!mwZ5s5F)!q2v2rAtO?i_69Ha-ou z_>>1nyPIbXzm+<{8G%%ULf_|a|ddjSIXliMqCRs^YPovc=4J;z~0Gi9J%qjNjf52s?c6k*>}KukqbYy3NlW41ZHhT$Jhbsx)o!oelY5p&U;z4GP43-CNp4rU8$ zLl{;Ml%_rDq8KqEh+ejmI71|CP06YK{t@!txkQ?KYY>DJ#WZ&{a7{^F5$dowkm6t6 ztPWQ^jIG(uD&%FCcH0CAaV6x3?VRRHtl_oZmKECOyMCY;&&qeIf)E~A#H2^V#c%MS zv06gvt#_BkFW;1n?mQ;m@y30V$b^3rcA1!B*xRbCT!Dxhi?S7rA+_p14xKh^&Rt?K@P>03GK3NPM&v+UMwP=cIE<>)scD!-v1CHg{vlz;D3MM4_q` z`iKIP-(de^tmv?RsMZ^j)9iMe&LED#WYyscWFlbm=NrizS-&E8Sq*ab?Ji>tWXsMf zG5hdW31W=w4y!E%oo18|4)-lay({ZxpzS`7X|LY^&s*3t%e6R$&xgr~ftMZkSC`;M zT{CH#!~W$DHg-<#&e6vyeU$$9`{GgaW|ycR-3A!8YSs|#AhiV(8f1LvO<-6Hz4-iC zDW%M$-)Eoti?77JbDA65n1w8X9b%xAyHaz9gR(V2Rq3~N7JY`#tF`CfqDFO>DKKo? zvgR*bdaJf-JMH@JqrON7y|nnID<3raWw@M2@u$_%4|Hp?eDsgH6TLrnL-2fsaDm8d zW{Yg*2e%II%Q*!H=^yH8bUaP|o0u=@J8S>VP*UJ0_+=&G=8+kVZUtH6Rcj={XEMM0 z-@8pPr(1eep@w32gykP#y()390vr4Y$NT&|pJNMbD|_YFFY!c8#JoeT$_iq%yTRsw6 zLo}|!65$*t8fL%YtVz9vW(2t2RpdQPruBx*Mr6KPS~}ts#mKSC+t-H(kWvX}k} z6_Qh1k9H0%D7Tb+xu?UNOPs>LOk)>cI6BwS-j>NS<&zliW!1%hxJiIbi-<2QiZ;Ax z5gqPA(An}0zak z4k<$^Na;ADG8*HvD;1Z7#Okm8OFpO?86CEk;PlHFirK%R=*LQ7_#5l<`zgn ze^>oYozftxW6$z4+ul$#@&;rV*I8^7>Y)Dy{3?vuWQy z+<4ptuu3)h`~+L$lIFx`0a*)hHfO8JJag*k{<&a+gw1qP~_PHK}ebe6$BNtz^0ikZd&_sBxhP z%Ylv%#GNKH;SCDx?I?u?I;B{ZUc36ZIJnUoh>MdqZTKr(YA8*s+-YZKO|a5nXNAe| zDO#jMTtsJ2V1SBk??R$IT`qiiT{ZY5)ZGK!5aUi*Lwt?ki^i(Zu&lMbUUg^@Y(e*G zVYx`Egoj9oU~-C9+atL0-SkfK%TdV7N*B9Kqedj5&EHp{`-?PveJ&DR&)a2b#|`ZeCIm1n(AtHh?kRYu&`{SQeXJrb zCO!9Nrj<>%`V3TeZ}+q3up4&J->NWjUvMRpxG&*hyX-xus5@M<0GD!Y_rUgvL=W}sX zFt87ZHc__vrtlc9fQPnPI8w~*pCU=7s&_mvJJE0PD{0V_uZR@0)_CkF&_+WE`qHQo zB&wT;66QfjHlc=60@%+z-yYlU%mG>ix^-;zaZQYPW(&Q*Bn~2mH5yZby(i(yX{eSl z_p=Ak=c~gegxHwT6+fXJdiAL3-eu?(hEtVlTrgH$>}oR`)?OUpqtKM1aaxMf76~}0 z65a`ZRn{zJwIj_};KK}%R7q!y`;VCZ=cClcD`~6@bpo|J*_v5@A>8gH_3b2e#TEHz zGC#;)M1bU!9mL&`4eQ<0JpaFG>m2&NaD!-k!O9U6%nOU=5rfY@tN+){ulSwuEoWDl z0Uu2yJRjmv5=T2<$RY3Fw+#ZtNZyZKS4ZzGxy2J`3EUnhUGt&8QyO-Ft7|cIi9`PG z&PmFx(GO)r0^7n|P;n+~hRh0fI=A@4SaOh!Ensb2+n49{Fg$T)^@lDCYS}m7Qq_xD zwwP9awssD|^9=rVMCa*#tq05Z7;h60-&6CKWx9&=*g$toU!JHZZ=m-go?2c?jtGZe z$i)%)q|9FDeJoe={0ho#3AsUMsKeK9Z*90-j3<=N`vg?q*Rs22K3JaF+q3TvtOc#? zpQ+JaxR05&9t-m+_+#~F)O6}aqH~A2M!Fu&iYH^WFHzezHVXn}B}bFl(O#Vcf8)1) jzPQgRC9JP!A=fT?;MpvxsIT+WB)Y4PPFQQx$7uW)v>~BY literal 0 HcmV?d00001 diff --git a/inst/scripts/generate_mq_sce.R b/inst/scripts/generate_mq_sce.R index dab431a..6a0e82f 100644 --- a/inst/scripts/generate_mq_sce.R +++ b/inst/scripts/generate_mq_sce.R @@ -17,3 +17,15 @@ out <- runMaxQuantAnalysis( stringIdCol = NULL, includeFeatureCollections = "complexes" ) + +# 'initial' SCE (from importExperiment) +sampleAnnot <- read.delim( + system.file("extdata/mq_example/1356_sampleAnnot.txt", + package = "einprot")) +se <- importExperiment( + system.file("extdata/mq_example/1356_proteinGroups.txt", + package = "einprot"), + iColPattern = "^LFQ.intensity.")$sce +se <- addSampleAnnots(se, sampleAnnot) +SummarizedExperiment::metadata(se) <- list() +saveRDS(se, file = "inst/extdata/mq_example/1356_sce_init.rds") From 54cb1c33a27bb75e9f41d8037387e92f96697f68 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sat, 21 Feb 2026 19:54:12 +0100 Subject: [PATCH 33/65] Reoxygenize --- man/runSEAnalysis.Rd | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/man/runSEAnalysis.Rd b/man/runSEAnalysis.Rd index 48f2f1a..ef7e152 100644 --- a/man/runSEAnalysis.Rd +++ b/man/runSEAnalysis.Rd @@ -17,12 +17,12 @@ runSEAnalysis( summaryInfo = NULL, aName, idCol, - labelCol, + labelCol = idCol, geneIdCol = NULL, proteinIdCol = NULL, stringIdCol = NULL, extraFeatureCols = NULL, - filtersSE, + filtersSE = list(), imputeMethod = "MinProb", assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, From caa96450106b1c08944922ffd88cfa4685f5d467 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sat, 21 Feb 2026 19:54:34 +0100 Subject: [PATCH 34/65] Add tests for checkArgumentsSE and runSEAnalysis --- tests/testthat/test-checkArgumentsSE.R | 671 ++++++++++++++++++++++ tests/testthat/test-runSEAnalysis.R | 735 +++++++++++++++++++++++++ 2 files changed, 1406 insertions(+) create mode 100644 tests/testthat/test-checkArgumentsSE.R create mode 100644 tests/testthat/test-runSEAnalysis.R diff --git a/tests/testthat/test-checkArgumentsSE.R b/tests/testthat/test-checkArgumentsSE.R new file mode 100644 index 0000000..7eda466 --- /dev/null +++ b/tests/testthat/test-checkArgumentsSE.R @@ -0,0 +1,671 @@ +test_that("argument checking for SE works", { + ## Working arguments + args0 <- list( + templateRmd = system.file("extdata", "process_basic_template.Rmd", + package = "einprot"), + outputDir = tempdir(), + outputBaseName = "baseName", + reportTitle = "reportTitle", + reportAuthor = "reportAuthor", + forceOverwrite = TRUE, + experimentInfo = list("Experiment type" = "experiment", + "Sample name" = "example"), + species = "roundworm", + inFile = system.file("extdata", "mq_example", "1356_sce_init.rds", + package = "einprot"), + summaryInfo = list(), + aName = "LFQ.intensity", + idCol = function(df) combineIds(df, combineCols = c("Gene.names", "Majority.protein.IDs"), + combineWhen = "nonunique", + splitSeparator = ";", joinSeparator = "."), + labelCol = function(df) combineIds(df, combineCols = c("Gene.names", "Majority.protein.IDs"), + combineWhen = "nonunique", + splitSeparator = ";", joinSeparator = "."), + geneIdCol = function(df) getFirstId(df, colName = "Gene.names", + separator = ";"), + proteinIdCol = "Majority.protein.IDs", + stringIdCol = function(df) combineIds(df, combineCols = c("Gene.names", "Majority.protein.IDs"), + combineWhen = "missing", splitSeparator = ";", + joinSeparator = ".", makeUnique = FALSE), + extraFeatureCols = NULL, + filtersSE = einprotMQFilters, + imputeMethod = "MinProb", + assaysForExport = c("iBAQ", "Top3"), + addAbundanceValues = TRUE, + addHeatmaps = TRUE, + mergeGroups = list(), + comparisons = list(), + ctrlGroup = "", + allPairwiseComparisons = TRUE, + singleFit = FALSE, + subtractBaseline = FALSE, + baselineGroup = "", + normMethod = "none", + spikeFeatures = NULL, + stattest = "limma", + minNbrValidValues = 2, + minlFC = 0, + samSignificance = TRUE, + nperm = 100, + volcanoAdjPvalThr = 0.05, + volcanoLog2FCThr = 1, + volcanoMaxFeatures = 10, + volcanoLabelSign = "both", + volcanoS0 = 0.1, + volcanoFeaturesToLabel = c(""), + addInteractiveVolcanos = FALSE, + interactiveDisplayColumns = NULL, + interactiveGroupColumn = NULL, + complexFDRThr = 0.1, + maxNbrComplexesToPlot = Inf, + maxComplexSimilarity = 1, + seed = 123, + includeFeatureCollections = "complexes", + minSizeToKeepSet = 2, + customComplexes = list(), + complexSpecies = "all", + complexDbPath = system.file( + "extdata", "complexes", + "complexdb_einprot0.5.0_20220323_orthologs.rds", + package = "einprot"), + stringVersion = "11.5", + stringDir = "", + linkTableColumns = c(), + customYml = NULL, + doRender = TRUE + ) + + ## Check that it works with these parameter values + expect_null(do.call(.checkArgumentsSE, args0)) + + ## templateRmd + args <- args0 + args$templateRmd <- c(args$templateRmd, args$templateRmd) + expect_error(do.call(.checkArgumentsSE, args), + "'templateRmd' must have length 1") + args$templateRmd <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'templateRmd' must be of class 'character'") + args$templateRmd <- "missing_file" + expect_error(do.call(.checkArgumentsSE, args), + "'templateRmd' must point to an existing file") + + ## outputDir + args <- args0 + args$outputDir <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'outputDir' must be of class 'character'") + args$outputDir <- c("dir1", "dir2") + expect_error(do.call(.checkArgumentsSE, args), + "'outputDir' must have length 1") + + ## outputBaseName + args <- args0 + args$outputBaseName <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'outputBaseName' must be of class 'character'") + args$outputBaseName <- c("name1", "name2") + expect_error(do.call(.checkArgumentsSE, args), + "'outputBaseName' must have length 1") + + ## reportTitle + args <- args0 + args$reportTitle <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'reportTitle' must be of class 'character'") + args$reportTitle <- c("name1", "name2") + expect_error(do.call(.checkArgumentsSE, args), + "'reportTitle' must have length 1") + + ## reportAuthor + args <- args0 + args$reportAuthor <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'reportAuthor' must be of class 'character'") + args$reportAuthor <- c("name1", "name2") + expect_error(do.call(.checkArgumentsSE, args), + "'reportAuthor' must have length 1") + + ## forceOverwrite + args <- args0 + args$forceOverwrite <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'forceOverwrite' must be of class 'logical'") + args$forceOverwrite <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSE, args), + "'forceOverwrite' must have length 1") + + ## experimentInfo + args <- args0 + args$experimentInfo <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'experimentInfo' must be of class 'list'") + args$experimentInfo <- list(1, 2) + expect_error(do.call(.checkArgumentsSE, args), + "'namesexperimentInfo' must not be NULL") + args$experimentInfo <- list() + expect_null(do.call(.checkArgumentsSE, args)) + + ## species + args <- args0 + args$species <- 1 + expect_warning(do.call(.checkArgumentsSE, args), + "Unknown species 1") + args$species <- c("Mouse", "Human") + expect_error(do.call(.checkArgumentsSE, args), + "length(species) == 1 is not TRUE", fixed = TRUE) + args$species <- "Mouse" + expect_null(do.call(.checkArgumentsSE, args)) + + ## inFile + args <- args0 + args$inFile <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'inFile' must be of class 'character'") + args$inFile <- c("name1", "name2") + expect_error(do.call(.checkArgumentsSE, args), + "'inFile' must have length 1") + args$inFile <- "missing" + expect_error(do.call(.checkArgumentsSE, args), + "'inFile' must point to an existing file") + + ## summaryInfo + args <- args0 + args$summaryInfo <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'summaryInfo' must be of class 'list'") + args$summaryInfo <- list("name1", "name2") + expect_error(do.call(.checkArgumentsSE, args), + "'namessummaryInfo' must not be NULL") + + ## aName + args <- args0 + args$aName <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'aName' must be of class 'character'") + args$aName <- c("iBAQ", "Unique.peptides") + expect_error(do.call(.checkArgumentsSE, args), + "'aName' must have length 1") + args$aName <- "missing" + expect_error(do.call(.checkArgumentsSE, args), + "All values in 'aName' must be one of") + + ## idCol + args <- args0 + args$idCol <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'idCol' must be of class 'character'") + + ## geneIdCol + args <- args0 + args$labelCol <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'labelCol' must be of class 'character'") + + ## geneIdCol + args <- args0 + args$geneIdCol <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'geneIdCol' must be of class 'character'") + + ## proteinIdCol + args <- args0 + args$proteinIdCol <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'proteinIdCol' must be of class 'character'") + + ## stringIdCol + args <- args0 + args$stringIdCol <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'stringIdCol' must be of class 'character'") + + ## extraFeatureCols + args <- args0 + args$extraFeatureCols <- function(df) df$einprotId + expect_error(do.call(.checkArgumentsSE, args), + "'extraFeatureCols' must be of class 'list'") + args$extraFeatureCols <- list(function(df) df$einprotId) + expect_error(do.call(.checkArgumentsSE, args), + "'namesextraFeatureCols' must not be NULL") + args$extraFeatureCols <- list(newCol = 1) + expect_error(do.call(.checkArgumentsSE, args), + "'i' must be of class 'character'") + args$extraFeatureCols <- list(newCol = function(x, y) "a") + expect_error(do.call(.checkArgumentsSE, args), + "length(formals(i)) == 1 is not TRUE", fixed = TRUE) + + ## filtersSE + args <- args0 + args$filtersSE <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) + expect_error(do.call(.checkArgumentsSE, args), + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) + expect_error(do.call(.checkArgumentsSE, args), + "is not TRUE") + + ## imputeMethod + args <- args0 + args$imputeMethod <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'imputeMethod' must be of class 'character'") + args$imputeMethod <- c("MinProb", "impSeqRob") + expect_error(do.call(.checkArgumentsSE, args), + "'imputeMethod' must have length 1") + args$imputeMethod <- "wrong" + expect_error(do.call(.checkArgumentsSE, args), + "All values in 'imputeMethod' must be one of") + + ## assaysForExport + args <- args0 + args$assaysForExport <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'assaysForExport' must be of class 'character'") + + ## addAbundanceValues + args <- args0 + args$addAbundanceValues <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'addAbundanceValues' must be of class 'logical'") + args$addAbundanceValues <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSE, args), + "'addAbundanceValues' must have length 1") + + ## addHeatmaps + args <- args0 + args$addHeatmaps <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'addHeatmaps' must be of class 'logical'") + args$addHeatmaps <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSE, args), + "'addHeatmaps' must have length 1") + + ## mergeGroups + args <- args0 + args$mergeGroups <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'mergeGroups' must be of class 'list'") + args$mergeGroups <- list(c("c1", "c2")) + expect_error(do.call(.checkArgumentsSE, args), + "'mergeGroups' must be a named list") + args$mergeGroups <- list(g1 = c("c1", "c2"), + g1 = c("c3", "c4")) + expect_error(do.call(.checkArgumentsSE, args), + "'mergeGroups' must be a named list") + args$mergeGroups <- list() + expect_null(do.call(.checkArgumentsSE, args)) + + ## comparisons + args <- args0 + args$comparisons <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'comparisons' must be of class 'list'") + args$comparisons <- list(g1 = c("c1", "c2"), + g2 = c("c2", "c4", "c3")) + expect_error(do.call(.checkArgumentsSE, args), + "Each entry in 'comparisons' must have exactly two elements") + args$comparisons <- list() + expect_null(do.call(.checkArgumentsSE, args)) + + ## ctrlGroup + args <- args0 + args$ctrlGroup <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'ctrlGroup' must be of class 'character'") + args$ctrlGroup <- c("name1", "name2") + expect_error(do.call(.checkArgumentsSE, args), + "'ctrlGroup' must have length 1") + + ## allPairwiseComparisons + args <- args0 + args$allPairwiseComparisons <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'allPairwiseComparisons' must be of class 'logical'") + args$allPairwiseComparisons <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSE, args), + "'allPairwiseComparisons' must have length 1") + + ## singleFit + args <- args0 + args$singleFit <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'singleFit' must be of class 'logical'") + args$singleFit <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSE, args), + "'singleFit' must have length 1") + + ## subtractBaseline + args <- args0 + args$subtractBaseline <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'subtractBaseline' must be of class 'logical'") + args$subtractBaseline <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSE, args), + "'subtractBaseline' must have length 1") + + ## baselineGroup + args <- args0 + args$baselineGroup <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'baselineGroup' must be of class 'character'") + + ## normMethod + args <- args0 + args$normMethod <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'normMethod' must be of class 'character'") + args$normMethod <- c("none", "center.median") + expect_error(do.call(.checkArgumentsSE, args), + "'normMethod' must have length 1") + args$normMethod <- "wrong" + expect_error(do.call(.checkArgumentsSE, args), + "All values in 'normMethod' must be one of") + + ## spikeFeatures + args <- args0 + args$spikeFeatures <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'spikeFeatures' must be of class 'character'") + + ## stattest + args <- args0 + args$stattest <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'stattest' must be of class 'character'") + args$stattest <- c("limma", "ttest") + expect_error(do.call(.checkArgumentsSE, args), + "'stattest' must have length 1") + args$stattest <- "wrong" + expect_error(do.call(.checkArgumentsSE, args), + "All values in 'stattest' must be one of") + + ## minNbrValidValues + args <- args0 + args$minNbrValidValues <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'minNbrValidValues' must be of class 'numeric'") + args$minNbrValidValues <- c(10, 20) + expect_error(do.call(.checkArgumentsSE, args), + "'minNbrValidValues' must have length 1") + args$minNbrValidValues <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'minNbrValidValues' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## minlFC + args <- args0 + args$minlFC <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'minlFC' must be of class 'numeric'") + args$minlFC <- c(10, 20) + expect_error(do.call(.checkArgumentsSE, args), + "'minlFC' must have length 1") + args$minlFC <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'minlFC' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## samSignificance + args <- args0 + args$samSignificance <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'samSignificance' must be of class 'logical'") + args$samSignificance <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSE, args), + "'samSignificance' must have length 1") + + ## nperm + args <- args0 + args$nperm <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'nperm' must be of class 'numeric'") + args$nperm <- c(10, 20) + expect_error(do.call(.checkArgumentsSE, args), + "'nperm' must have length 1") + args$nperm <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'nperm' must be within [1,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoAdjPvalThr + args <- args0 + args$volcanoAdjPvalThr <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoAdjPvalThr' must be of class 'numeric'") + args$volcanoAdjPvalThr <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoAdjPvalThr' must have length 1") + args$volcanoAdjPvalThr <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoAdjPvalThr' must be within [0,1] (inclusive)", + fixed = TRUE) + + ## volcanoLog2FCThr + args <- args0 + args$volcanoLog2FCThr <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoLog2FCThr' must be of class 'numeric'") + args$volcanoLog2FCThr <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoLog2FCThr' must have length 1") + args$volcanoLog2FCThr <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoLog2FCThr' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoMaxFeatures + args <- args0 + args$volcanoMaxFeatures <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoMaxFeatures' must be of class 'numeric'") + args$volcanoMaxFeatures <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoMaxFeatures' must have length 1") + args$volcanoMaxFeatures <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoMaxFeatures' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoLabelSign + args <- args0 + args$volcanoLabelSign <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoLabelSign' must be of class 'character'") + args$volcanoLabelSign <- c("both", "pos") + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoLabelSign' must have length 1") + args$volcanoLabelSign <- "missing" + expect_error(do.call(.checkArgumentsSE, args), + "All values in 'volcanoLabelSign' must be one of") + + ## volcanoS0 + args <- args0 + args$volcanoS0 <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoS0' must be of class 'numeric'") + args$volcanoS0 <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoS0' must have length 1") + args$volcanoS0 <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoS0' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoFeaturesToLabel + args <- args0 + args$volcanoFeaturesToLabel <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'volcanoFeaturesToLabel' must be of class 'character'") + + ## addInteractiveVolcanos + args <- args0 + args$addInteractiveVolcanos <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'addInteractiveVolcanos' must be of class 'logical'") + args$addInteractiveVolcanos <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSE, args), + "'addInteractiveVolcanos' must have length 1") + + ## interactiveDisplayColumns + args <- args0 + args$interactiveDisplayColumns <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'interactiveDisplayColumns' must be of class 'character'") + + # interactiveGroupColumn + args <- args0 + args$interactiveGroupColumn <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'interactiveGroupColumn' must be of class 'character'") + args$interactiveGroupColumn <- c("col1", "col2") + expect_error(do.call(.checkArgumentsSE, args), + "'interactiveGroupColumn' must have length 1") + + ## complexFDRThr + args <- args0 + args$complexFDRThr <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'complexFDRThr' must be of class 'numeric'") + args$complexFDRThr <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSE, args), + "'complexFDRThr' must have length 1") + args$complexFDRThr <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'complexFDRThr' must be within [0,1] (inclusive)", + fixed = TRUE) + + ## maxNbrComplexesToPlot + args <- args0 + args$maxNbrComplexesToPlot <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'maxNbrComplexesToPlot' must be of class 'numeric'") + args$maxNbrComplexesToPlot <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSE, args), + "'maxNbrComplexesToPlot' must have length 1") + args$maxNbrComplexesToPlot <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'maxNbrComplexesToPlot' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## maxComplexSimilarity + args <- args0 + args$maxComplexSimilarity <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'maxComplexSimilarity' must be of class 'numeric'") + args$maxComplexSimilarity <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSE, args), + "'maxComplexSimilarity' must have length 1") + + ## seed + args <- args0 + args$seed <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'seed' must be of class 'numeric'") + args$seed <- c(4, 5) + expect_error(do.call(.checkArgumentsSE, args), + "'seed' must have length 1") + args$seed <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'seed' must be within [1,Inf] (inclusive)", + fixed = TRUE) + + ## includeFeatureCollections + args <- args0 + args$includeFeatureCollections <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'includeFeatureCollections' must be of class 'character'") + args$includeFeatureCollections <- "wrong" + expect_error(do.call(.checkArgumentsSE, args), + "All values in 'includeFeatureCollections' must be one of") + + ## minSizeToKeepSet + args <- args0 + args$minSizeToKeepSet <- "1" + expect_error(do.call(.checkArgumentsSE, args), + "'minSizeToKeepSet' must be of class 'numeric'") + args$minSizeToKeepSet <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSE, args), + "'minSizeToKeepSet' must have length 1") + args$minSizeToKeepSet <- -1 + expect_error(do.call(.checkArgumentsSE, args), + "'minSizeToKeepSet' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## customComplexes + args <- args0 + args$customComplexes <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'customComplexes' must be of class 'list'") + args$customComplexes <- list(c("c1", "c2"), + c("c2", "c4", "c3")) + expect_error(do.call(.checkArgumentsSE, args), + "'namescustomComplexes' must not be NULL") + args$customComplexes <- list() + expect_null(do.call(.checkArgumentsSE, args)) + + ## complexSpecies + args <- args0 + args$complexSpecies <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'complexSpecies' must be of class 'character'") + args$complexSpecies <- c("all", "current") + expect_error(do.call(.checkArgumentsSE, args), + "'complexSpecies' must have length 1") + args$complexSpecies <- "wrong" + expect_error(do.call(.checkArgumentsSE, args), + "All values in 'complexSpecies' must be one of") + + ## complexDbPath + args <- args0 + args$complexDbPath <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'complexDbPath' must be of class 'character'") + args$complexDbPath <- "missing_file" + expect_error(do.call(.checkArgumentsSE, args), + "'complexDbPath' must point to an existing file") + + ## stringVersion + args <- args0 + args$stringVersion <- 11 + expect_error(do.call(.checkArgumentsSE, args), + "'stringVersion' must be of class 'character'") + args$stringVersion <- c("11.0", "11.5") + expect_error(do.call(.checkArgumentsSE, args), + "'stringVersion' must have length 1") + + ## stringDir + args <- args0 + args$stringDir <- 11 + expect_error(do.call(.checkArgumentsSE, args), + "'stringDir' must be of class 'character'") + args$stringDir <- c("", "") + expect_error(do.call(.checkArgumentsSE, args), + "'stringDir' must have length 1") + + ## linkTableColumns + args <- args0 + args$linkTableColumns <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'linkTableColumns' must be of class 'character'") + + ## customYml + args <- args0 + args$customYml <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'customYml' must be of class 'character'") + args$customYml <- "missing_file" + expect_error(do.call(.checkArgumentsSE, args), + "'customYml' must point to an existing file") + + ## doRender + args <- args0 + args$doRender <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'doRender' must be of class 'logical'") + args$doRender <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSE, args), + "'doRender' must have length 1") +}) diff --git a/tests/testthat/test-runSEAnalysis.R b/tests/testthat/test-runSEAnalysis.R new file mode 100644 index 0000000..71fac77 --- /dev/null +++ b/tests/testthat/test-runSEAnalysis.R @@ -0,0 +1,735 @@ +test_that("runSEAnalysis works", { + outDir <- tempdir() + outBaseName <- "SEAnalysis" + args0 <- list( + templateRmd = system.file("extdata/process_basic_template.Rmd", + package = "einprot"), + outputDir = outDir, + outputBaseName = outBaseName, + reportTitle = "SE LFQ data processing", + reportAuthor = "", + forceOverwrite = TRUE, + experimentInfo = list("Experiment type" = "type1"), + species = "mouse", + inFile = system.file("extdata", "mq_example", "1356_sce_init.rds", + package = "einprot"), + summaryInfo = list(), + aName = "LFQ.intensity", + idCol = function(df) combineIds(df, combineCols = c("Gene.names", "Majority.protein.IDs"), + combineWhen = "nonunique", + splitSeparator = ";", joinSeparator = "."), + labelCol = function(df) combineIds(df, combineCols = c("Gene.names", "Majority.protein.IDs"), + combineWhen = "nonunique", + splitSeparator = ";", joinSeparator = "."), + geneIdCol = function(df) getFirstId(df, colName = "Gene.names", + separator = ";"), + proteinIdCol = function(df) getFirstId(df, colName = "Majority.protein.IDs", + separator = ";"), + stringIdCol = function(df) combineIds(df, combineCols = c("Gene.names", "Majority.protein.IDs"), + combineWhen = "missing", makeUnique = FALSE), + extraFeatureCols = NULL, + filtersSE = einprotMQFilters, + imputeMethod = "MinProb", + assaysForExport = c("iBAQ", "Top3"), + addAbundanceValues = TRUE, + addHeatmaps = TRUE, + mergeGroups = list(), + comparisons = list(), + ctrlGroup = "", + allPairwiseComparisons = TRUE, + singleFit = FALSE, + subtractBaseline = FALSE, + baselineGroup = "", + normMethod = "none", + spikeFeatures = NULL, + stattest = "limma", + minNbrValidValues = 2, + minlFC = 0, + samSignificance = TRUE, + nperm = 250, + volcanoAdjPvalThr = 0.05, + volcanoLog2FCThr = 1, + volcanoMaxFeatures = 25, + volcanoLabelSign = "both", + volcanoS0 = 0.1, + volcanoFeaturesToLabel = "", + addInteractiveVolcanos = FALSE, + interactiveDisplayColumns = NULL, + interactiveGroupColumn = NULL, + complexFDRThr = 0.1, + maxNbrComplexesToPlot = Inf, + maxComplexSimilarity = 1, + seed = 42, + includeFeatureCollections = "complexes", + minSizeToKeepSet = 2, + customComplexes = list(), + complexSpecies = "all", + complexDbPath = system.file( + "extdata", "complexes", + "complexdb_einprot0.5.0_20220323_orthologs.rds", + package = "einprot"), + stringVersion = "11.5", + stringDir = "", + linkTableColumns = c(), + customYml = NULL, + doRender = FALSE + ) + + ## Fail with wrong parameter values (essentially the same tests as for + ## .checkArgumentsSE()) + ## ------------------------------------------------------------------------- + ## templateRmd + args <- args0 + args$templateRmd <- c(args$templateRmd, args$templateRmd) + expect_error(do.call(runSEAnalysis, args), + "'templateRmd' must have length 1") + args$templateRmd <- 1 + expect_error(do.call(runSEAnalysis, args), + "'templateRmd' must be of class 'character'") + args$templateRmd <- "missing_file" + expect_error(do.call(runSEAnalysis, args), + "'templateRmd' must point to an existing file") + + ## outputDir + args <- args0 + args$outputDir <- 1 + expect_error(do.call(runSEAnalysis, args), + "'outputDir' must be of class 'character'") + args$outputDir <- c("dir1", "dir2") + expect_error(do.call(runSEAnalysis, args), + "'outputDir' must have length 1") + + ## outputBaseName + args <- args0 + args$outputBaseName <- 1 + expect_error(do.call(runSEAnalysis, args), + "'outputBaseName' must be of class 'character'") + args$outputBaseName <- c("name1", "name2") + expect_error(do.call(runSEAnalysis, args), + "'outputBaseName' must have length 1") + + ## reportTitle + args <- args0 + args$reportTitle <- 1 + expect_error(do.call(runSEAnalysis, args), + "'reportTitle' must be of class 'character'") + args$reportTitle <- c("name1", "name2") + expect_error(do.call(runSEAnalysis, args), + "'reportTitle' must have length 1") + + ## reportAuthor + args <- args0 + args$reportAuthor <- 1 + expect_error(do.call(runSEAnalysis, args), + "'reportAuthor' must be of class 'character'") + args$reportAuthor <- c("name1", "name2") + expect_error(do.call(runSEAnalysis, args), + "'reportAuthor' must have length 1") + + ## forceOverwrite + args <- args0 + args$forceOverwrite <- 1 + expect_error(do.call(runSEAnalysis, args), + "'forceOverwrite' must be of class 'logical'") + args$forceOverwrite <- c(TRUE, FALSE) + expect_error(do.call(runSEAnalysis, args), + "'forceOverwrite' must have length 1") + + ## doRender + args <- args0 + args$doRender <- 1 + expect_error(do.call(runSEAnalysis, args), + "'doRender' must be of class 'logical'") + args$doRender <- c(TRUE, FALSE) + expect_error(do.call(runSEAnalysis, args), + "'doRender' must have length 1") + + ## experimentInfo + args <- args0 + args$experimentInfo <- 1 + expect_error(do.call(runSEAnalysis, args), + "'experimentInfo' must be of class 'list'") + args$experimentInfo <- list(1, 2) + expect_error(do.call(runSEAnalysis, args), + "'namesexperimentInfo' must not be NULL") + + ## species + args <- args0 + args$species <- c("Mouse", "Human") + expect_error(do.call(runSEAnalysis, args), + "length(species) == 1 is not TRUE", fixed = TRUE) + + ## inFile + args <- args0 + args$inFile <- 1 + expect_error(do.call(runSEAnalysis, args), + "'inFile' must be of class 'character'") + args$inFile <- c("name1", "name2") + expect_error(do.call(runSEAnalysis, args), + "'inFile' must have length 1") + args$inFile <- "missing" + expect_error(do.call(runSEAnalysis, args), + "'inFile' must point to an existing file") + + ## summaryInfo + args <- args0 + args$summaryInfo <- 1 + expect_error(do.call(runSEAnalysis, args), + "'summaryInfo' must be of class 'list'") + args$summaryInfo <- list("name1", "name2") + expect_error(do.call(runSEAnalysis, args), + "'namessummaryInfo' must not be NULL") + + ## aName + args <- args0 + args$aName <- 1 + expect_error(do.call(runSEAnalysis, args), + "'aName' must be of class 'character'") + args$aName <- c("iBAQ", "Unique.peptides") + expect_error(do.call(runSEAnalysis, args), + "'aName' must have length 1") + args$aName <- "missing" + expect_error(do.call(runSEAnalysis, args), + "All values in 'aName' must be one of") + + ## idCol + args <- args0 + args$idCol <- 1 + expect_error(do.call(runSEAnalysis, args), + "'idCol' must be of class 'character'") + + ## labelCol + args <- args0 + args$labelCol <- 1 + expect_error(do.call(runSEAnalysis, args), + "'labelCol' must be of class 'character'") + + ## geneIdCol + args <- args0 + args$geneIdCol <- 1 + expect_error(do.call(runSEAnalysis, args), + "'geneIdCol' must be of class 'character'") + + ## proteinIdCol + args <- args0 + args$proteinIdCol <- 1 + expect_error(do.call(runSEAnalysis, args), + "'proteinIdCol' must be of class 'character'") + + ## stringIdCol + args <- args0 + args$stringIdCol <- 1 + expect_error(do.call(runSEAnalysis, args), + "'stringIdCol' must be of class 'character'") + + ## extraFeatureCols + args <- args0 + args$extraFeatureCols <- function(df) df$einprotId + expect_error(do.call(runSEAnalysis, args), + "'extraFeatureCols' must be of class 'list'") + args$extraFeatureCols <- list(function(df) df$einprotId) + expect_error(do.call(runSEAnalysis, args), + "'namesextraFeatureCols' must not be NULL") + args$extraFeatureCols <- list(newCol = 1) + expect_error(do.call(runSEAnalysis, args), + "'i' must be of class 'character'") + args$extraFeatureCols <- list(newCol = function(x, y) "a") + expect_error(do.call(runSEAnalysis, args), + "length(formals(i)) == 1 is not TRUE", fixed = TRUE) + + ## filtersSE + args <- args0 + args$filtersSE <- "1" + expect_error(do.call(runSEAnalysis, args), + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) + expect_error(do.call(runSEAnalysis, args), + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) + expect_error(do.call(runSEAnalysis, args), + "is not TRUE") + + ## imputeMethod + args <- args0 + args$imputeMethod <- 1 + expect_error(do.call(runSEAnalysis, args), + "'imputeMethod' must be of class 'character'") + args$imputeMethod <- c("MinProb", "impSeqRob") + expect_error(do.call(runSEAnalysis, args), + "'imputeMethod' must have length 1") + args$imputeMethod <- "wrong" + expect_error(do.call(runSEAnalysis, args), + "All values in 'imputeMethod' must be one of") + + ## assaysForExport + args <- args0 + args$assaysForExport <- 1 + expect_error(do.call(runSEAnalysis, args), + "'assaysForExport' must be of class 'character'") + + ## addAbundanceValues + args <- args0 + args$addAbundanceValues <- 1 + expect_error(do.call(runSEAnalysis, args), + "'addAbundanceValues' must be of class 'logical'") + args$addAbundanceValues <- c(TRUE, FALSE) + expect_error(do.call(runSEAnalysis, args), + "'addAbundanceValues' must have length 1") + + ## addHeatmaps + args <- args0 + args$addHeatmaps <- 1 + expect_error(do.call(runSEAnalysis, args), + "'addHeatmaps' must be of class 'logical'") + args$addHeatmaps <- c(TRUE, FALSE) + expect_error(do.call(runSEAnalysis, args), + "'addHeatmaps' must have length 1") + + ## mergeGroups + args <- args0 + args$mergeGroups <- 1 + expect_error(do.call(runSEAnalysis, args), + "'mergeGroups' must be of class 'list'") + args$mergeGroups <- list(c("c1", "c2")) + expect_error(do.call(runSEAnalysis, args), + "'mergeGroups' must be a named list") + args$mergeGroups <- list(g1 = c("c1", "c2"), + g1 = c("c3", "c4")) + expect_error(do.call(runSEAnalysis, args), + "'mergeGroups' must be a named list") + + ## comparisons + args <- args0 + args$comparisons <- 1 + expect_error(do.call(runSEAnalysis, args), + "'comparisons' must be of class 'list'") + args$comparisons <- list(g1 = c("c1", "c2"), + g2 = c("c2", "c4", "c3")) + expect_error(do.call(runSEAnalysis, args), + "Each entry in 'comparisons' must have exactly two elements") + + ## ctrlGroup + args <- args0 + args$ctrlGroup <- 1 + expect_error(do.call(runSEAnalysis, args), + "'ctrlGroup' must be of class 'character'") + # args$ctrlGroup <- c("name1", "name2") + # expect_error(do.call(runSEAnalysis, args), + # "'ctrlGroup' must have length 1") + + ## allPairwiseComparisons + args <- args0 + args$allPairwiseComparisons <- 1 + expect_error(do.call(runSEAnalysis, args), + "'allPairwiseComparisons' must be of class 'logical'") + args$allPairwiseComparisons <- c(TRUE, FALSE) + expect_error(do.call(runSEAnalysis, args), + "'allPairwiseComparisons' must have length 1") + + ## singleFit + args <- args0 + args$singleFit <- 1 + expect_error(do.call(runSEAnalysis, args), + "'singleFit' must be of class 'logical'") + args$singleFit <- c(TRUE, FALSE) + expect_error(do.call(runSEAnalysis, args), + "'singleFit' must have length 1") + + ## subtractBaseline + args <- args0 + args$subtractBaseline <- 1 + expect_error(do.call(runSEAnalysis, args), + "'subtractBaseline' must be of class 'logical'") + args$subtractBaseline <- c(TRUE, FALSE) + expect_error(do.call(runSEAnalysis, args), + "'subtractBaseline' must have length 1") + + ## baselineGroup + args <- args0 + args$baselineGroup <- 1 + expect_error(do.call(runSEAnalysis, args), + "'baselineGroup' must be of class 'character'") + + ## normMethod + args <- args0 + args$normMethod <- 1 + expect_error(do.call(runSEAnalysis, args), + "'normMethod' must be of class 'character'") + args$normMethod <- c("none", "center.median") + expect_error(do.call(runSEAnalysis, args), + "'normMethod' must have length 1") + args$normMethod <- "wrong" + expect_error(do.call(runSEAnalysis, args), + "All values in 'normMethod' must be one of") + + ## spikeFeatures + args <- args0 + args$spikeFeatures <- 1 + expect_error(do.call(runSEAnalysis, args), + "'spikeFeatures' must be of class 'character'") + + ## stattest + args <- args0 + args$stattest <- 1 + expect_error(do.call(runSEAnalysis, args), + "'stattest' must be of class 'character'") + args$stattest <- c("limma", "ttest") + expect_error(do.call(runSEAnalysis, args), + "'stattest' must have length 1") + args$stattest <- "wrong" + expect_error(do.call(runSEAnalysis, args), + "All values in 'stattest' must be one of") + + ## minNbrValidValues + args <- args0 + args$minNbrValidValues <- "1" + expect_error(do.call(runSEAnalysis, args), + "'minNbrValidValues' must be of class 'numeric'") + args$minNbrValidValues <- c(10, 20) + expect_error(do.call(runSEAnalysis, args), + "'minNbrValidValues' must have length 1") + args$minNbrValidValues <- -1 + expect_error(do.call(runSEAnalysis, args), + "'minNbrValidValues' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## minlFC + args <- args0 + args$minlFC <- "1" + expect_error(do.call(runSEAnalysis, args), + "'minlFC' must be of class 'numeric'") + args$minlFC <- c(10, 20) + expect_error(do.call(runSEAnalysis, args), + "'minlFC' must have length 1") + args$minlFC <- -1 + expect_error(do.call(runSEAnalysis, args), + "'minlFC' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## samSignificance + args <- args0 + args$samSignificance <- "1" + expect_error(do.call(runSEAnalysis, args), + "'samSignificance' must be of class 'logical'") + args$samSignificance <- c(TRUE, FALSE) + expect_error(do.call(runSEAnalysis, args), + "'samSignificance' must have length 1") + + ## nperm + args <- args0 + args$nperm <- "1" + expect_error(do.call(runSEAnalysis, args), + "'nperm' must be of class 'numeric'") + args$nperm <- c(10, 20) + expect_error(do.call(runSEAnalysis, args), + "'nperm' must have length 1") + args$nperm <- -1 + expect_error(do.call(runSEAnalysis, args), + "'nperm' must be within [1,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoAdjPvalThr + args <- args0 + args$volcanoAdjPvalThr <- "1" + expect_error(do.call(runSEAnalysis, args), + "'volcanoAdjPvalThr' must be of class 'numeric'") + args$volcanoAdjPvalThr <- c(0.1, 0.2) + expect_error(do.call(runSEAnalysis, args), + "'volcanoAdjPvalThr' must have length 1") + args$volcanoAdjPvalThr <- -1 + expect_error(do.call(runSEAnalysis, args), + "'volcanoAdjPvalThr' must be within [0,1] (inclusive)", + fixed = TRUE) + + ## volcanoLog2FCThr + args <- args0 + args$volcanoLog2FCThr <- "1" + expect_error(do.call(runSEAnalysis, args), + "'volcanoLog2FCThr' must be of class 'numeric'") + args$volcanoLog2FCThr <- c(0.1, 0.2) + expect_error(do.call(runSEAnalysis, args), + "'volcanoLog2FCThr' must have length 1") + args$volcanoLog2FCThr <- -1 + expect_error(do.call(runSEAnalysis, args), + "'volcanoLog2FCThr' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoMaxFeatures + args <- args0 + args$volcanoMaxFeatures <- "1" + expect_error(do.call(runSEAnalysis, args), + "'volcanoMaxFeatures' must be of class 'numeric'") + args$volcanoMaxFeatures <- c(0.1, 0.2) + expect_error(do.call(runSEAnalysis, args), + "'volcanoMaxFeatures' must have length 1") + args$volcanoMaxFeatures <- -1 + expect_error(do.call(runSEAnalysis, args), + "'volcanoMaxFeatures' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoLabelSign + args <- args0 + args$volcanoLabelSign <- 1 + expect_error(do.call(runSEAnalysis, args), + "'volcanoLabelSign' must be of class 'character'") + args$volcanoLabelSign <- c("both", "pos") + expect_error(do.call(runSEAnalysis, args), + "'volcanoLabelSign' must have length 1") + args$volcanoLabelSign <- "missing" + expect_error(do.call(runSEAnalysis, args), + "All values in 'volcanoLabelSign' must be one of") + + ## volcanoS0 + args <- args0 + args$volcanoS0 <- "1" + expect_error(do.call(runSEAnalysis, args), + "'volcanoS0' must be of class 'numeric'") + args$volcanoS0 <- c(0.1, 0.2) + expect_error(do.call(runSEAnalysis, args), + "'volcanoS0' must have length 1") + args$volcanoS0 <- -1 + expect_error(do.call(runSEAnalysis, args), + "'volcanoS0' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoFeaturesToLabel + args <- args0 + args$volcanoFeaturesToLabel <- 1 + expect_error(do.call(runSEAnalysis, args), + "'volcanoFeaturesToLabel' must be of class 'character'") + + ## addInteractiveVolcanos + args <- args0 + args$addInteractiveVolcanos <- 1 + expect_error(do.call(runSEAnalysis, args), + "'addInteractiveVolcanos' must be of class 'logical'") + args$addInteractiveVolcanos <- c(TRUE, FALSE) + expect_error(do.call(runSEAnalysis, args), + "'addInteractiveVolcanos' must have length 1") + + # interactiveDisplayColumns + args <- args0 + args$interactiveDisplayColumns <- 1 + expect_error(do.call(runSEAnalysis, args), + "'interactiveDisplayColumns' must be of class 'character'") + + # interactiveGroupColumn + args <- args0 + args$interactiveGroupColumn <- 1 + expect_error(do.call(runSEAnalysis, args), + "'interactiveGroupColumn' must be of class 'character'") + args$interactiveGroupColumn <- c("col1", "col2") + expect_error(do.call(runSEAnalysis, args), + "'interactiveGroupColumn' must have length 1") + + ## complexFDRThr + args <- args0 + args$complexFDRThr <- "1" + expect_error(do.call(runSEAnalysis, args), + "'complexFDRThr' must be of class 'numeric'") + args$complexFDRThr <- c(0.1, 0.2) + expect_error(do.call(runSEAnalysis, args), + "'complexFDRThr' must have length 1") + args$complexFDRThr <- -1 + expect_error(do.call(runSEAnalysis, args), + "'complexFDRThr' must be within [0,1] (inclusive)", + fixed = TRUE) + + ## maxNbrComplexesToPlot + args <- args0 + args$maxNbrComplexesToPlot <- "1" + expect_error(do.call(runSEAnalysis, args), + "'maxNbrComplexesToPlot' must be of class 'numeric'") + args$maxNbrComplexesToPlot <- c(0.1, 0.2) + expect_error(do.call(runSEAnalysis, args), + "'maxNbrComplexesToPlot' must have length 1") + args$maxNbrComplexesToPlot <- -1 + expect_error(do.call(runSEAnalysis, args), + "'maxNbrComplexesToPlot' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## maxComplexSimilarity + args <- args0 + args$maxComplexSimilarity <- "1" + expect_error(do.call(runSEAnalysis, args), + "'maxComplexSimilarity' must be of class 'numeric'") + args$maxComplexSimilarity <- c(0.1, 0.2) + expect_error(do.call(runSEAnalysis, args), + "'maxComplexSimilarity' must have length 1") + + ## seed + args <- args0 + args$seed <- "1" + expect_error(do.call(runSEAnalysis, args), + "'seed' must be of class 'numeric'") + args$seed <- c(4, 5) + expect_error(do.call(runSEAnalysis, args), + "'seed' must have length 1") + args$seed <- -1 + expect_error(do.call(runSEAnalysis, args), + "'seed' must be within [1,Inf] (inclusive)", + fixed = TRUE) + + ## includeFeatureCollections + args <- args0 + args$includeFeatureCollections <- 1 + expect_error(do.call(runSEAnalysis, args), + "'includeFeatureCollections' must be of class 'character'") + args$includeFeatureCollections <- "wrong" + expect_error(do.call(runSEAnalysis, args), + "All values in 'includeFeatureCollections' must be one of") + + ## minSizeToKeepSet + args <- args0 + args$minSizeToKeepSet <- "1" + expect_error(do.call(runSEAnalysis, args), + "'minSizeToKeepSet' must be of class 'numeric'") + args$minSizeToKeepSet <- c(0.1, 0.2) + expect_error(do.call(runSEAnalysis, args), + "'minSizeToKeepSet' must have length 1") + args$minSizeToKeepSet <- -1 + expect_error(do.call(runSEAnalysis, args), + "'minSizeToKeepSet' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## customComplexes + args <- args0 + args$customComplexes <- 1 + expect_error(do.call(runSEAnalysis, args), + "'customComplexes' must be of class 'list'") + args$customComplexes <- list(c("c1", "c2"), + c("c2", "c4", "c3")) + expect_error(do.call(runSEAnalysis, args), + "'namescustomComplexes' must not be NULL") + + ## complexSpecies + args <- args0 + args$complexSpecies <- 1 + expect_error(do.call(runSEAnalysis, args), + "'complexSpecies' must be of class 'character'") + args$complexSpecies <- c("all", "current") + expect_error(do.call(runSEAnalysis, args), + "'complexSpecies' must have length 1") + args$complexSpecies <- "wrong" + expect_error(do.call(runSEAnalysis, args), + "All values in 'complexSpecies' must be one of") + + ## complexDbPath + args <- args0 + args$complexDbPath <- 1 + expect_error(do.call(runSEAnalysis, args), + "'complexDbPath' must be of class 'character'") + args$complexDbPath <- "missing_file" + expect_error(do.call(runSEAnalysis, args), + "'complexDbPath' must point to an existing file") + + ## stringVersion + args <- args0 + args$stringVersion <- 11 + expect_error(do.call(runSEAnalysis, args), + "'stringVersion' must be of class 'character'") + args$stringVersion <- c("11.0", "11.5") + expect_error(do.call(runSEAnalysis, args), + "'stringVersion' must have length 1") + + ## stringDir + args <- args0 + args$stringDir <- 11 + expect_error(do.call(runSEAnalysis, args), + "'stringDir' must be of class 'character'") + args$stringDir <- c("", "") + expect_error(do.call(runSEAnalysis, args), + "'stringDir' must have length 1") + + ## linkTableColumns + args <- args0 + args$linkTableColumns <- 1 + expect_error(do.call(runSEAnalysis, args), + "'linkTableColumns' must be of class 'character'") + + ## customYml + args <- args0 + args$customYml <- 1 + expect_error(do.call(runSEAnalysis, args), + "'customYml' must be of class 'character'") + args$customYml <- "missing_file" + expect_error(do.call(runSEAnalysis, args), + "'customYml' must point to an existing file") + + ## Test fix for backwards compatibility with mergeGroups/ctrlGroup + args <- args0 + args$mergeGroups <- list(G1 = c("Adnp", "RBC_ctrl")) + args$ctrlGroup <- c("Adnp", "RBC_ctrl") + expect_error(do.call(runSEAnalysis, args), + "If 'mergeGroups' is specified, 'ctrlGroup' should not") + + ## Note that the tests below depend on the value of 'fcn' in + ## generateConfigChunk ("deparse" or "dump") + args <- args0 + args$doRender <- FALSE + args$ctrlGroup <- c("Adnp", "RBC_ctrl") + res <- do.call(runSEAnalysis, args) + expect_type(res, "character") + tmp <- readLines(res) + ## if fcn = "dump" + idx <- grep("ctrlGroup <-", tmp, fixed = TRUE) + expect_length(length(idx), 1L) + # expect_equal(tmp[idx + 1], '\"Adnp.RBC_ctrl\"') + idx <- grep("mergeGroups <-", tmp, fixed = TRUE) + expect_length(length(idx), 1L) + # expect_equal(tmp[idx + 1], 'list(Adnp.RBC_ctrl = c(\"Adnp\", \"RBC_ctrl\"))') + ## if fcn = "deparse" + # expect_true(length( + # grep('ctrlGroup <- \n\"Adnp.RBC_ctrl\"', tmp, fixed = TRUE)) > 0) + # expect_true(length( + # grep('mergeGroups <- list(Adnp.RBC_ctrl = c(\"Adnp\", \"RBC_ctrl\"))', + # tmp, fixed = TRUE)) > 0) + + + ## Generate report + ## ------------------------------------------------------------------------- + ## Without rendering + args <- args0 + res <- do.call(runSEAnalysis, args) + expect_type(res, "character") + expect_equal(basename(res), paste0(outBaseName, ".Rmd")) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, ".Rmd")))) + + ## Stop if forceOverwrite = FALSE + args <- args0 + args$forceOverwrite <- FALSE + expect_error(res <- do.call(runSEAnalysis, args)) + + ## Message if forceOverwrite = TRUE + args <- args0 + args$forceOverwrite <- TRUE + expect_message(res <- do.call(runSEAnalysis, args), + "already exists but forceOverwrite = TRUE") + + ## In new, non-existing directory and with custom yml + args <- args0 + args$outputDir <- file.path(outDir, "new_se_dir") + args$customYml <- system.file("extdata", "custom.yml", + package = "einprot") + res <- do.call(runSEAnalysis, args) + expect_type(res, "character") + expect_equal(basename(res), paste0(outBaseName, ".Rmd")) + expect_true(file.exists(file.path(args$outputDir, paste0(outBaseName, ".Rmd")))) + tmp <- readLines(file.path(args$outputDir, paste0(outBaseName, ".Rmd"))) + # expect_true(grepl("theme: journal", tmp[4])) + + ## With rendering + skip_if(!capabilities()["X11"]) + args <- args0 + args$doRender <- TRUE + res <- do.call(runSEAnalysis, args) + + expect_type(res, "character") + expect_equal(basename(res), paste0(outBaseName, ".html")) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, ".Rmd")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, ".html")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, "_iSEE.R")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, "_sce.rds")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, "_heatmap_centered.pdf")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, "_feature_info.txt")))) +}) From 4f15a825f6032f26c9d506e5f7f91ae7b9a1c4e7 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 6 Mar 2026 13:19:30 +0100 Subject: [PATCH 35/65] Use arrow instead of duckplyr to read parquet files --- DESCRIPTION | 2 +- R/importDIANN.R | 21 ++++++++------------- R/runDIANNAnalysis.R | 2 +- man/importDIANN.Rd | 4 ++-- man/runDIANNAnalysis.Rd | 2 +- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 8c94591..c58857e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -84,7 +84,7 @@ Suggests: testthat (>= 3.0.0), iSEE, matrixStats, - duckplyr + arrow Config/testthat/edition: 3 VignetteBuilder: knitr biocViews: Proteomics, DataImport diff --git a/R/importDIANN.R b/R/importDIANN.R index 39080e8..876533b 100644 --- a/R/importDIANN.R +++ b/R/importDIANN.R @@ -20,7 +20,7 @@ einprotDIANNFiltersDF <- list(Filter = function(df) { if ("Protein.Q.Value" %in% colnames(df)) { df <- df |> dplyr::filter(Protein.Q.Value <= 0.005) } - df <- dplyr::collect(dplyr::compute(df)) + # df <- dplyr::collect(dplyr::compute(df)) if (all(c("Protein.Ids", "Precursor.Id") %in% colnames(df))) { df <- df |> dplyr::group_by(Protein.Ids) |> dplyr::mutate(N.Proteotypic.Sequences = dplyr::n_distinct(Precursor.Id)) |> @@ -55,7 +55,7 @@ einprotDIANNFiltersDF <- list(Filter = function(df) { #' column from which to get the values for the main assay (if #' \code{fileType} is \code{"main_report"} or \code{"parquet"}). #' @param filtersDF Named list where each element is a filtering function -#' that takes a \code{data.frame} (or \code{duckplyr} data frame) as +#' that takes a \code{data.frame} (or \code{tibble}) as #' input and returns a filtered object of the same class. No other #' arguments are allowed. The filtering functions will be applied in the #' order they are provided in the list, after reading the long-format text @@ -93,7 +93,7 @@ einprotDIANNFiltersDF <- list(Filter = function(df) { importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", includeOnlySamples = "", excludeSamples = "", stopIfEmpty = FALSE, - aName = "MaxLFQ", filtersDF = list(), ...) { + aName = "PG.MaxLFQ", filtersDF = list(), ...) { ## Check input arguments .assertScalar(x = inFile, type = "character") stopifnot(file.exists(inFile)) @@ -153,21 +153,16 @@ importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", if (fileType == "main_report") { tmp <- read.delim(inFile, header = TRUE, sep = "\t") } else { - .assertPackagesAvailable("duckplyr") - tmp <- duckplyr::read_parquet_duckdb(inFile) + # read data as tibble + .assertPackagesAvailable("arrow") + tmp <- arrow::read_parquet(inFile, as_data_frame = TRUE) } iColsAll <- tmp |> dplyr::select(Run) |> distinct() |> pull() iCols <- .getiCols(iColsAll = iColsAll, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, stopIfEmpty = stopIfEmpty) - if (fileType == "main_report") { - tmp <- tmp |> dplyr::filter(Run %in% iCols) - } else { - iCols_tbl <- duckplyr::as_duckdb_tibble(tibble(Run = iCols)) - tmp <- tmp |> - dplyr::inner_join(iCols_tbl, by = "Run") - } + tmp <- tmp |> dplyr::filter(Run %in% iCols) stopifnot(aName %in% colnames(tmp)) # Filter @@ -175,7 +170,7 @@ importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", tmp <- f(tmp) } - tmp <- dplyr::collect(dplyr::compute(tmp)) + # tmp <- dplyr::collect(dplyr::compute(tmp)) if (outLevel == "pg") { # if Genes and/or Protein.Names columns are not present, add NA # columns diff --git a/R/runDIANNAnalysis.R b/R/runDIANNAnalysis.R index 1914613..acd8a43 100644 --- a/R/runDIANNAnalysis.R +++ b/R/runDIANNAnalysis.R @@ -16,7 +16,7 @@ #' @param outLevel Character string indicating the desired output level. #' Either \code{"pg"} or \code{"pr"}. #' @param filtersDF Named list where each element is a filtering function -#' that takes a \code{data.frame} (or \code{duckplyr} data frame) as +#' that takes a \code{data.frame} (or \code{tibble}) as #' input and returns a filtered object of the same class. No other #' arguments are allowed. The filtering functions will be applied in the #' order they are provided in the list, after reading the long-format text diff --git a/man/importDIANN.Rd b/man/importDIANN.Rd index 28a7b94..6cae5b0 100644 --- a/man/importDIANN.Rd +++ b/man/importDIANN.Rd @@ -11,7 +11,7 @@ importDIANN( includeOnlySamples = "", excludeSamples = "", stopIfEmpty = FALSE, - aName = "MaxLFQ", + aName = "PG.MaxLFQ", filtersDF = list(), ... ) @@ -41,7 +41,7 @@ column from which to get the values for the main assay (if \code{fileType} is \code{"main_report"} or \code{"parquet"}).} \item{filtersDF}{Named list where each element is a filtering function -that takes a \code{data.frame} (or \code{duckplyr} data frame) as +that takes a \code{data.frame} (or \code{tibble}) as input and returns a filtered object of the same class. No other arguments are allowed. The filtering functions will be applied in the order they are provided in the list, after reading the long-format text diff --git a/man/runDIANNAnalysis.Rd b/man/runDIANNAnalysis.Rd index 97427b0..5848c7e 100644 --- a/man/runDIANNAnalysis.Rd +++ b/man/runDIANNAnalysis.Rd @@ -166,7 +166,7 @@ Currently, \code{"impSeqRob"}, \code{"MinProb"}, and more details about the methods.} \item{filtersDF}{Named list where each element is a filtering function -that takes a \code{data.frame} (or \code{duckplyr} data frame) as +that takes a \code{data.frame} (or \code{tibble}) as input and returns a filtered object of the same class. No other arguments are allowed. The filtering functions will be applied in the order they are provided in the list, after reading the long-format text From a18a6fc87453f84ed0a3bcea50149b23476a0787 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Mon, 16 Mar 2026 19:49:12 +0100 Subject: [PATCH 36/65] Protect against all abundances in barplot of significant features being NA (could happen if labelFeaturesInVolcano is given) --- R/plotVolcano.R | 82 ++++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/R/plotVolcano.R b/R/plotVolcano.R index 955e800..6e6ee00 100644 --- a/R/plotVolcano.R +++ b/R/plotVolcano.R @@ -114,47 +114,51 @@ getComplexesToPlot <- function(featureCollections, } else { bardata$mergegroup <- bardata$group } - ggbar <- ggplot( - bardata %>% dplyr::group_by(.data$pid, .data$mergegroup, - .data$direction) %>% - dplyr::summarize( - mean_abundance = mean(.data$Abundance, na.rm = TRUE), - sd_abundance = stats::sd(.data$Abundance, na.rm = TRUE), - .groups = "drop"), - aes(x = .data$pid, y = .data$mean_abundance, - fill = .data$mergegroup)) + - geom_bar(position = position_dodge(), stat = "identity", - colour = "black", linewidth = 0.3) + - geom_errorbar(aes(ymin = .data$mean_abundance - .data$sd_abundance, - ymax = .data$mean_abundance + .data$sd_abundance), - linewidth = 0.3, width = 0.2, - position = position_dodge(width = 0.9)) + - theme_bw() + - theme(axis.text.x = element_text(size = 12, angle = 90, - hjust = 1, vjust = 0.5), - axis.text.y = element_text(size = 12), - axis.title = element_text(size = 14), - title = element_text(size = 14)) + - labs(x = "", y = paste0("Mean +/- SD ", colpat), title = cplx) + - scale_fill_manual(name = "", values = c("steelblue", "firebrick2")) + - facet_grid(~ direction, scales = "free", space = "free") - if (length(unique(bardata$sample)) <= 6) { - ggbar <- ggbar + - geom_jitter(data = bardata, aes(y = .data$Abundance, - shape = .data$sample, - group = .data$mergegroup), size = 2, - position = position_jitterdodge(dodge.width = 0.9, - jitter.width = 0.2, - jitter.height = 0)) + if (all(is.na(bardata$Abundance))) { + return(NULL) } else { - ggbar <- ggbar + - geom_jitter(data = bardata, aes(y = .data$Abundance, - group = .data$mergegroup), size = 2, - position = position_jitterdodge(dodge.width = 0.9, - jitter.width = 0.2, - jitter.height = 0)) + ggbar <- ggplot( + bardata %>% dplyr::group_by(.data$pid, .data$mergegroup, + .data$direction) %>% + dplyr::summarize( + mean_abundance = mean(.data$Abundance, na.rm = TRUE), + sd_abundance = stats::sd(.data$Abundance, na.rm = TRUE), + .groups = "drop"), + aes(x = .data$pid, y = .data$mean_abundance, + fill = .data$mergegroup)) + + geom_bar(position = position_dodge(), stat = "identity", + colour = "black", linewidth = 0.3) + + geom_errorbar(aes(ymin = .data$mean_abundance - .data$sd_abundance, + ymax = .data$mean_abundance + .data$sd_abundance), + linewidth = 0.3, width = 0.2, + position = position_dodge(width = 0.9)) + + theme_bw() + + theme(axis.text.x = element_text(size = 12, angle = 90, + hjust = 1, vjust = 0.5), + axis.text.y = element_text(size = 12), + axis.title = element_text(size = 14), + title = element_text(size = 14)) + + labs(x = "", y = paste0("Mean +/- SD ", colpat), title = cplx) + + scale_fill_manual(name = "", values = c("steelblue", "firebrick2")) + + facet_grid(~ direction, scales = "free", space = "free") + if (length(unique(bardata$sample)) <= 6) { + ggbar <- ggbar + + geom_jitter(data = bardata, aes(y = .data$Abundance, + shape = .data$sample, + group = .data$mergegroup), size = 2, + position = position_jitterdodge(dodge.width = 0.9, + jitter.width = 0.2, + jitter.height = 0)) + } else { + ggbar <- ggbar + + geom_jitter(data = bardata, aes(y = .data$Abundance, + group = .data$mergegroup), size = 2, + position = position_jitterdodge(dodge.width = 0.9, + jitter.width = 0.2, + jitter.height = 0)) + } + return(ggbar) } - ggbar } #' @author Charlotte Soneson From 82cf1d24990187409e542d139fcedc349bc21d59 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 12 Apr 2026 16:13:15 +0200 Subject: [PATCH 37/65] Allow PEP.Quantity as iColPattern --- R/importExperiment.R | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/R/importExperiment.R b/R/importExperiment.R index 7e6a128..544a4d3 100644 --- a/R/importExperiment.R +++ b/R/importExperiment.R @@ -70,6 +70,8 @@ "Intensity" } else if (grepl("^.PG\\.", patmatch2)) { sub("^\\.", "", patmatch2) + } else if (grepl("^.PEP\\.", patmatch2)) { + sub("^\\.", "", patmatch2) } else { "ERROR" } @@ -183,7 +185,8 @@ importExperiment <- function(inFile, iColPattern, includeOnlySamples = "", "\\.PG\\.PEP\\.\\.Run\\.Wise\\.$", "\\.PG\\.PValue\\.\\.Run\\.Wise\\.$", "\\.PG\\.IsIdentified$", - "\\.PG\\.RunEvidenceCount$") + "\\.PG\\.RunEvidenceCount$", + "\\.PEP\\.Quantity$") ## Without the escaped characters patsexp <- gsub("\\", "", pats, fixed = TRUE) From 529ff78507c3510fff0b86b0aa291234eeae7fe3 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 12 Apr 2026 16:17:56 +0200 Subject: [PATCH 38/65] Add clusterRows argument to makeAbundanceHeatmap --- R/makeAbundanceHeatmap.R | 9 ++++++++- tests/testthat/test-makeAbundanceHeatmap.R | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/R/makeAbundanceHeatmap.R b/R/makeAbundanceHeatmap.R index 3f84d05..fcc2b26 100644 --- a/R/makeAbundanceHeatmap.R +++ b/R/makeAbundanceHeatmap.R @@ -23,6 +23,10 @@ #' settings used in \code{einprot} reports and when exporting the heatmap #' to a pdf. Setting it to \code{NULL} allows any argument to be passed to #' \code{ComplexHeatmap::Heatmap} via the \code{...} argument. +#' @param clusterRows Logical scalar. For \code{settings="report"} or +#' \code{"export"}, indicates whether rows should be clustered or not. +#' Typically set to \code{FALSE} for large experiments where clustering +#' rows would be prohibitive. #' @param ... If \code{settings} is \code{NULL}, additional arguments passed to #' \code{ComplexHeatmap::Heatmap}. #' @@ -44,13 +48,14 @@ #' @importFrom circlize colorRamp2 #' makeAbundanceHeatmap <- function(sce, assayToPlot, doCenter, - settings = "report", ...) { + settings = "report", clusterRows = TRUE, ...) { .assertVector(x = sce, type = "SummarizedExperiment") .assertScalar(x = assayToPlot, type = "character", validValues = SummarizedExperiment::assayNames(sce)) .assertScalar(x = doCenter, type = "logical") .assertScalar(x = settings, type = "character", allowNULL = TRUE, validValues = c("report", "export")) + .assertScalar(x = clusterRows, type = "logical") if (!"pNA" %in% colnames(SummarizedExperiment::rowData(sce))) { SummarizedExperiment::rowData(sce)$pNA <- NA_real_ @@ -73,6 +78,7 @@ makeAbundanceHeatmap <- function(sce, assayToPlot, doCenter, mat, name = nm, show_row_names = FALSE, + cluster_rows = clusterRows, use_raster = TRUE, top_annotation = ComplexHeatmap::columnAnnotation( group = sce$group, @@ -83,6 +89,7 @@ makeAbundanceHeatmap <- function(sce, assayToPlot, doCenter, mat, name = nm, show_row_names = TRUE, show_row_dend = FALSE, + cluster_rows = clusterRows, cluster_columns = TRUE, column_split = sce$group, use_raster = TRUE, top_annotation = ComplexHeatmap::columnAnnotation( diff --git a/tests/testthat/test-makeAbundanceHeatmap.R b/tests/testthat/test-makeAbundanceHeatmap.R index 2797c0a..c191afc 100644 --- a/tests/testthat/test-makeAbundanceHeatmap.R +++ b/tests/testthat/test-makeAbundanceHeatmap.R @@ -26,6 +26,14 @@ test_that("makeAbundanceHeatmap works", { expect_error(makeAbundanceHeatmap(sce = sce_mq_final, assayToPlot = "log2_iBAQ", doCenter = FALSE, settings = "missing"), "All values in 'settings' must be one of") + expect_error(makeAbundanceHeatmap(sce = sce_mq_final, assayToPlot = "log2_iBAQ", + doCenter = FALSE, settings = "report", + clusterRows = 1), + "'clusterRows' must be of class 'logical'") + expect_error(makeAbundanceHeatmap(sce = sce_mq_final, assayToPlot = "log2_iBAQ", + doCenter = FALSE, settings = "report", + clusterRows = c(TRUE, FALSE)), + "'clusterRows' must have length 1") ## settings = 'report' ht <- makeAbundanceHeatmap(sce = sce_mq_final, assayToPlot = "log2_iBAQ", @@ -36,6 +44,16 @@ test_that("makeAbundanceHeatmap works", { expect_equal(ht@row_names_param$labels, rownames(sce_mq_final)) expect_equal(ht@row_names_param$show, FALSE) + ## ... clusterRows = FALSE + ht <- makeAbundanceHeatmap(sce = sce_mq_final, assayToPlot = "log2_iBAQ", + doCenter = FALSE, settings = "report", + clusterRows = FALSE) + expect_s4_class(ht, "Heatmap") + expect_equal(ht@name, "log2_iBAQ") + expect_equal(dim(ht@matrix), dim(sce_mq_final)) + expect_equal(ht@row_names_param$labels, rownames(sce_mq_final)) + expect_equal(ht@row_names_param$show, FALSE) + ## settings = 'export' ht <- makeAbundanceHeatmap(sce = sce_mq_final, assayToPlot = "log2_iBAQ", doCenter = TRUE, settings = "export") From 85cad32a5428ff2be357879fc728248d5bdbc307 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 12 Apr 2026 16:21:20 +0200 Subject: [PATCH 39/65] Add "sorted" settings to plotMissingValuesHeatmap --- R/plotMissingValues.R | 20 ++++++++++++++++++-- tests/testthat/test-plotMissingValues.R | 11 +++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/R/plotMissingValues.R b/R/plotMissingValues.R index 12a295f..57ccc76 100644 --- a/R/plotMissingValues.R +++ b/R/plotMissingValues.R @@ -12,7 +12,10 @@ #' include rows with at least one missing (\code{TRUE}) value. #' @param settings Character scalar or \code{NULL}. Setting this to #' \code{"clustered"} creates a heatmap with rows and columns -#' clustered (used in the \code{einprot} report). +#' clustered (used in the \code{einprot} report). Setting it to +#' \code{"sorted"} sorts the rows first by the row sums across all columns, +#' then successively by the value in each column. This is typically used +#' for large matrices, where clustering rows would be prohibitive. #' Setting it to \code{NULL} allows any argument to be passed to #' \code{ComplexHeatmap::Heatmap} via the \code{...} argument. #' @param ... Additional arguments passed to \code{ComplexHeatmap::Heatmap}. @@ -47,7 +50,8 @@ plotMissingValuesHeatmap <- function(sce, assayMissing, stop("Assay contains missing values") } .assertScalar(x = onlyRowsWithMissing, type = "logical") - .assertScalar(x = settings, type = "character", validValues = "clustered", + .assertScalar(x = settings, type = "character", + validValues = c("clustered", "sorted"), allowNULL = TRUE) ## rows to plot @@ -64,6 +68,18 @@ plotMissingValuesHeatmap <- function(sce, assayMissing, column_title = "Missing value pattern (white = missing)", cluster_rows = TRUE, cluster_columns = TRUE, show_row_names = FALSE, show_heatmap_legend = FALSE) + } else if (!is.null(settings) && settings == "sorted") { + # sort by row sum, then by value in columns + col_fun <- circlize::colorRamp2(c(0, 1), c("grey50", "white")) + tmp <- SummarizedExperiment::assay(sce, assayMissing)[idx, ] + 0 + ordr <- do.call(order, c(list(rowSums(tmp, na.rm = TRUE)), + lapply(seq_len(ncol(tmp)), function(i) tmp[, i]))) + ComplexHeatmap::Heatmap( + tmp[rev(ordr), , drop = FALSE], + col = col_fun, name = "imputed", + column_title = "Missing value pattern (white = missing)", + cluster_rows = FALSE, cluster_columns = TRUE, show_row_names = FALSE, + show_heatmap_legend = FALSE) } else if (is.null(settings)) { ComplexHeatmap::Heatmap( SummarizedExperiment::assay(sce, assayMissing)[idx, ] + 0, ... diff --git a/tests/testthat/test-plotMissingValues.R b/tests/testthat/test-plotMissingValues.R index 9692e7e..1eef884 100644 --- a/tests/testthat/test-plotMissingValues.R +++ b/tests/testthat/test-plotMissingValues.R @@ -51,6 +51,17 @@ test_that("missing value plots work", { expect_equal(out@column_title, "Missing value pattern (white = missing)") expect_equal(out@name, "imputed") + out <- plotMissingValuesHeatmap(sce = sce_mq_preimputation, + assayMissing = "imputed_iBAQ", + onlyRowsWithMissing = FALSE, + settings = "sorted") + expect_s4_class(out, "Heatmap") + expect_length(out@column_names_param$labels, 9L) + expect_true(out@column_names_param$show) + expect_length(out@row_names_param$labels, 150L) + expect_equal(out@column_title, "Missing value pattern (white = missing)") + expect_equal(out@name, "imputed") + out <- plotMissingValuesHeatmap(sce = sce_mq_preimputation, assayMissing = "imputed_iBAQ", onlyRowsWithMissing = FALSE, From debb0affdbec44d49a2d12c56f3932dc54c383d0 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 12 Apr 2026 16:23:57 +0200 Subject: [PATCH 40/65] Update analysis template to only cluster heatmaps if the number of features is <= 10000 --- inst/extdata/process_basic_template.Rmd | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index b2566d0..725d9ce 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -617,7 +617,7 @@ values correspond to missing values, and the grey ones to observed intensities. if (addHeatmaps) { plotMissingValuesHeatmap(sce, assayMissing = aNames$assayImputIndic, onlyRowsWithMissing = FALSE, - settings = "clustered") + settings = ifelse(nrow(sce) <= 10000, "clustered", "sorted")) } ``` @@ -1305,12 +1305,12 @@ the values for each protein have been centered to mean 0. ```{r heatmap, message=FALSE, fig.width = min(14, max(7, 0.5 * ncol(sce))), fig.height = 8/7 * min(14, max(7, 0.5 * ncol(sce)))} if (addHeatmaps) { ht <- makeAbundanceHeatmap(sce, assayToPlot = aNames$assayImputed, doCenter = FALSE, - settings = "report") + settings = "report", clusterRows = (nrow(sce) <= 10000)) draw(ht, merge_legends = TRUE) ## Centered ht <- makeAbundanceHeatmap(sce, assayToPlot = aNames$assayImputed, doCenter = TRUE, - settings = "report") + settings = "report", clusterRows = (nrow(sce) <= 10000)) draw(ht, merge_legends = TRUE) } ``` @@ -1327,7 +1327,7 @@ if (addHeatmaps) { pdf(sub("\\.Rmd$", "_heatmap_centered.pdf", knitr::current_input(dir = TRUE)), width = ncol(sce)/3.3 + 3.375, height = nrow(sce)/6.33 + 4) ht <- makeAbundanceHeatmap(sce, assayToPlot = aNames$assayImputed, doCenter = TRUE, - settings = "export") + settings = "export", clusterRows = (nrow(sce) <= 10000)) draw(ht, merge_legends = TRUE, heatmap_legend_side = "bottom", annotation_legend_side = "bottom") dev.off() From 175d9e84f08b979165ee0073b1cc0bdf2578971a Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 26 Apr 2026 16:16:19 +0200 Subject: [PATCH 41/65] Add valueType argument to plotFractionDetectedPerSample, specify "percentage" in basic report --- R/plotMissingValues.R | 36 +++++++++++++++---- inst/extdata/process_basic_template.Rmd | 3 +- man/plotFractionDetectedPerSample.Rd | 11 ++++-- tests/testthat/test-plotMissingValues.R | 46 +++++++++++++++++++++---- 4 files changed, 80 insertions(+), 16 deletions(-) diff --git a/R/plotMissingValues.R b/R/plotMissingValues.R index 57ccc76..e6dd227 100644 --- a/R/plotMissingValues.R +++ b/R/plotMissingValues.R @@ -99,6 +99,11 @@ plotMissingValuesHeatmap <- function(sce, assayMissing, #' @param dfNA A \code{DFrame} or \code{data.frame} with at least columns #' named \code{"name"} and \code{"pNA"} representing the sample name and #' the fraction of missing values. +#' @param valueType A \code{character} scalar indicating whether the values +#' in the \code{"pNA"} column represent fractions +#' (\code{valueType = "fraction"}, values in [0, 1]) or +#' percentages (\code{valueType = "percentage"}, values in [0, 100]). If +#' \code{NULL}, the type will be guessed from the observed values. #' #' @export #' @author Charlotte Soneson @@ -108,7 +113,8 @@ plotMissingValuesHeatmap <- function(sce, assayMissing, #' @examples #' sce <- readRDS(system.file("extdata", "mq_example", "1356_sce.rds", #' package = "einprot")) -#' plotFractionDetectedPerSample(SummarizedExperiment::colData(sce)) +#' plotFractionDetectedPerSample(SummarizedExperiment::colData(sce), +#' valueType = "percentage") #' #' @importFrom ggplot2 ggplot aes theme geom_bar theme_bw labs expand_limits #' geom_text @@ -116,18 +122,36 @@ plotMissingValuesHeatmap <- function(sce, assayMissing, #' @importFrom rlang .data #' @importFrom methods is #' -plotFractionDetectedPerSample <- function(dfNA) { +plotFractionDetectedPerSample <- function(dfNA, valueType = NULL) { if (methods::is(dfNA, "DFrame")) { dfNA <- as.data.frame(dfNA) } .assertVector(x = dfNA, type = "data.frame") stopifnot(all(c("sample", "pNA") %in% colnames(dfNA))) + .assertScalar(x = valueType, type = "character", + validValues = c("fraction", "percentage"), allowNULL = TRUE) + if (!is.null(valueType) && valueType == "fraction") { + .assertVector(x = dfNA$pNA, type = "numeric", rngIncl = c(0, 1)) + } + if (!is.null(valueType) && valueType == "percentage") { + .assertVector(x = dfNA$pNA, type = "numeric", rngIncl = c(0, 100)) + } - ## Guess whether pNA are proportions or percentages - if (all(dfNA$pNA <= 1)) { - multfact <- 100 + if (is.null(valueType)) { + ## Guess whether pNA are proportions or percentages + if (all(dfNA$pNA <= 1)) { + message("Inferred valueType: fraction") + multfact <- 100 + } else { + message("Inferred valueType: percentage") + multfact <- 1 + } } else { - multfact <- 1 + if (valueType == "percentage") { + multfact <- 1 + } else if (valueType == "fraction") { + multfact <- 100 + } } ggplot2::ggplot(dfNA, ggplot2::aes(x = .data$sample, y = 100 - multfact * .data$pNA, diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index 725d9ce..4408415 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -600,7 +600,8 @@ colData(sce)$pNA <- 100 * sce$nNA/nrow(sce) rowData(sce)$nNA <- rowSums(is.na(assay(sce, aNames$assayInput))) rowData(sce)$pNA <- 100 * rowData(sce)$nNA/ncol(sce) -plotFractionDetectedPerSample(dfNA = as.data.frame(colData(sce)[, c("sample", "nNA", "pNA")])) +plotFractionDetectedPerSample(dfNA = as.data.frame(colData(sce)[, c("sample", "nNA", "pNA")]), + valueType = "percentage") ``` We also plot the number of features that are detected in a given number diff --git a/man/plotFractionDetectedPerSample.Rd b/man/plotFractionDetectedPerSample.Rd index 4bfbcd4..1882455 100644 --- a/man/plotFractionDetectedPerSample.Rd +++ b/man/plotFractionDetectedPerSample.Rd @@ -4,12 +4,18 @@ \alias{plotFractionDetectedPerSample} \title{Plot detection rate per sample} \usage{ -plotFractionDetectedPerSample(dfNA) +plotFractionDetectedPerSample(dfNA, valueType = NULL) } \arguments{ \item{dfNA}{A \code{DFrame} or \code{data.frame} with at least columns named \code{"name"} and \code{"pNA"} representing the sample name and the fraction of missing values.} + +\item{valueType}{A \code{character} scalar indicating whether the values +in the \code{"pNA"} column represent fractions +(\code{valueType = "fraction"}, values in [0, 1]) or +percentages (\code{valueType = "percentage"}, values in [0, 100]). If +\code{NULL}, the type will be guessed from the observed values.} } \value{ A \code{ggplot} object. @@ -20,7 +26,8 @@ Plot the fraction of detected features in each sample. \examples{ sce <- readRDS(system.file("extdata", "mq_example", "1356_sce.rds", package = "einprot")) -plotFractionDetectedPerSample(SummarizedExperiment::colData(sce)) +plotFractionDetectedPerSample(SummarizedExperiment::colData(sce), + valueType = "percentage") } \author{ diff --git a/tests/testthat/test-plotMissingValues.R b/tests/testthat/test-plotMissingValues.R index 1eef884..1d3d5c7 100644 --- a/tests/testthat/test-plotMissingValues.R +++ b/tests/testthat/test-plotMissingValues.R @@ -110,18 +110,50 @@ test_that("missing value plots work", { dfNA = dfNA), 'all(c("sample", "pNA") %in% colnames(dfNA)) is not TRUE', fixed = TRUE) + dfNA <- as.data.frame(nbr_na_mq$nNAcols) %>% + dplyr::rename(sample = name) + expect_error(plotFractionDetectedPerSample( + dfNA = dfNA, valueType = 1 + ), "'valueType' must be of class 'character'") + expect_error(plotFractionDetectedPerSample( + dfNA = dfNA, valueType = c("fraction", "percentage") + ), "'valueType' must have length 1") + expect_error(plotFractionDetectedPerSample( + dfNA = dfNA, valueType = "error" + ), "'valueType' must be one of") + dfNA$pNA <- 1 + dfNA$pNA + expect_error(plotFractionDetectedPerSample( + dfNA = dfNA, valueType = "fraction" + ), "'$dfNApNA' must be within", fixed = TRUE) + dfNA$pNA <- 99 + dfNA$pNA + expect_error(plotFractionDetectedPerSample( + dfNA = dfNA, valueType = "percentage" + ), "'$dfNApNA' must be within", fixed = TRUE) - out <- plotFractionDetectedPerSample( - dfNA = as.data.frame(nbr_na_mq$nNAcols) %>% - dplyr::rename(sample = name)) + expect_message( + out <- plotFractionDetectedPerSample( + dfNA = as.data.frame(nbr_na_mq$nNAcols) %>% + dplyr::rename(sample = name)), + "Inferred valueType: fraction") expect_s3_class(out, "ggplot") expect_named(out$data, c("sample", "nNA", "pNA", "assay")) - out <- plotFractionDetectedPerSample( + out2 <- plotFractionDetectedPerSample( dfNA = DataFrame(as.data.frame(nbr_na_mq$nNAcols) %>% - dplyr::rename(sample = name))) - expect_s3_class(out, "ggplot") - expect_named(out$data, c("sample", "nNA", "pNA", "assay")) + dplyr::rename(sample = name)), + valueType = "fraction") + expect_s3_class(out2, "ggplot") + expect_named(out2$data, c("sample", "nNA", "pNA", "assay")) + expect_identical(out$data, out2$data) + + expect_message( + out3 <- plotFractionDetectedPerSample( + dfNA = DataFrame(as.data.frame(nbr_na_mq$nNAcols) %>% + dplyr::rename(sample = name) %>% + dplyr::mutate(pNA = 100 * pNA))), + "Inferred valueType: percentage") + expect_s3_class(out3, "ggplot") + expect_named(out3$data, c("sample", "nNA", "pNA", "assay")) ## ------------------------------------------------------------------------- ## plotDetectedInSamples From 42d00d3ef629a1b700ec3aeab642524a6ce7b943 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 26 Apr 2026 16:16:33 +0200 Subject: [PATCH 42/65] Update documentation --- man/makeAbundanceHeatmap.Rd | 14 +++++++++++++- man/plotMissingValuesHeatmap.Rd | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/man/makeAbundanceHeatmap.Rd b/man/makeAbundanceHeatmap.Rd index f0fbff3..df4b73d 100644 --- a/man/makeAbundanceHeatmap.Rd +++ b/man/makeAbundanceHeatmap.Rd @@ -4,7 +4,14 @@ \alias{makeAbundanceHeatmap} \title{Generate heatmap} \usage{ -makeAbundanceHeatmap(sce, assayToPlot, doCenter, settings = "report", ...) +makeAbundanceHeatmap( + sce, + assayToPlot, + doCenter, + settings = "report", + clusterRows = TRUE, + ... +) } \arguments{ \item{sce}{A \code{SummarizedExperiment} object (or a derivative).} @@ -21,6 +28,11 @@ settings used in \code{einprot} reports and when exporting the heatmap to a pdf. Setting it to \code{NULL} allows any argument to be passed to \code{ComplexHeatmap::Heatmap} via the \code{...} argument.} +\item{clusterRows}{Logical scalar. For \code{settings="report"} or +\code{"export"}, indicates whether rows should be clustered or not. +Typically set to \code{FALSE} for large experiments where clustering +rows would be prohibitive.} + \item{...}{If \code{settings} is \code{NULL}, additional arguments passed to \code{ComplexHeatmap::Heatmap}.} } diff --git a/man/plotMissingValuesHeatmap.Rd b/man/plotMissingValuesHeatmap.Rd index ac17fc8..2048068 100644 --- a/man/plotMissingValuesHeatmap.Rd +++ b/man/plotMissingValuesHeatmap.Rd @@ -25,7 +25,10 @@ include rows with at least one missing (\code{TRUE}) value.} \item{settings}{Character scalar or \code{NULL}. Setting this to \code{"clustered"} creates a heatmap with rows and columns -clustered (used in the \code{einprot} report). +clustered (used in the \code{einprot} report). Setting it to +\code{"sorted"} sorts the rows first by the row sums across all columns, +then successively by the value in each column. This is typically used +for large matrices, where clustering rows would be prohibitive. Setting it to \code{NULL} allows any argument to be passed to \code{ComplexHeatmap::Heatmap} via the \code{...} argument.} From 10c5b1bd3cf8a17c3c3de53b08e211adf335ce64 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 29 Apr 2026 17:34:09 +0200 Subject: [PATCH 43/65] fix exclusion of se.logFC columns in prepareFinalSCE --- R/prepareFinalSCE.R | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/R/prepareFinalSCE.R b/R/prepareFinalSCE.R index e66ba5c..425836f 100644 --- a/R/prepareFinalSCE.R +++ b/R/prepareFinalSCE.R @@ -96,8 +96,10 @@ prepareFinalSCE <- function(sce, baseFileName, featureCollections, expType) { sce, setdiff( grep("logFC$", colnames(SummarizedExperiment::rowData(sce)), value = TRUE), - grep("se\\.logFC$", colnames(SummarizedExperiment::rowData(sce)), - value = TRUE)) + # don't just grep for se.logFC, as the group name could end with se + sub("P\\.Value$", "se.logFC", + grep("P\\.Value$", colnames(SummarizedExperiment::rowData(sce)), + value = TRUE))) ) sce <- iSEEu::registerAveAbFields( sce, grep("AveExpr$", colnames(SummarizedExperiment::rowData(sce)), From de8efe331d9558420add994ee4cceaa4face7246 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 29 Apr 2026 17:35:46 +0200 Subject: [PATCH 44/65] exclude some lines from coverage calculations --- R/geomEncircle.R | 4 +-- R/makeComplexDB.R | 68 +++++++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/R/geomEncircle.R b/R/geomEncircle.R index 9aed545..9bfd4df 100644 --- a/R/geomEncircle.R +++ b/R/geomEncircle.R @@ -8,7 +8,7 @@ #' @importFrom scales alpha #' @importFrom grid grobTree rectGrob #' @importFrom ggplot2 draw_key_path -draw_key_hack <- function(data, params, size) { +draw_key_hack <- function(data, params, size) { # nocov start data$fill <- alpha(data$fill, data$alpha) data$alpha <- 1 @@ -16,7 +16,7 @@ draw_key_hack <- function(data, params, size) { if (!is.na(data$fill)) rectGrob(gp = gpar(col = NA, fill = data$fill)), draw_key_path(data, params) ) -} +} # nocov end #' @noRd #' @keywords internal diff --git a/R/makeComplexDB.R b/R/makeComplexDB.R index 6972f1c..3b708bd 100644 --- a/R/makeComplexDB.R +++ b/R/makeComplexDB.R @@ -225,55 +225,55 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, ## ------------------------------------------------------------------------- if (!is.null(Cyc2008Db)) { YEAST.in <- Cyc2008Db - } else { + } else { # nocov start YEAST.in <- .getCyc2008Db(dbDir = dbDir) - } + } # nocov end if (!is.null(CorumDb)) { CORUM.in <- CorumDb - } else { + } else { # nocov start CORUM.in <- .getCorumDb(dbDir = dbDir) - } + } # nocov end if (!is.null(PombaseDb)) { SCHPO.in <- PombaseDb - } else { + } else { # nocov start SCHPO.in <- .getPombaseDb(dbDir = dbDir) - } + } # nocov end if (!is.null(HuMAP2Db)) { HUMAP2.in <- HuMAP2Db - } else { + } else { # nocov start HUMAP2.in <- .getHuMAP2Db(dbDir = dbDir) - } + } # nocov end if (!is.null(CPortal9606Db)) { CPortal9606.in <- CPortal9606Db - } else { + } else { # nocov start CPortal9606.in <- .getComplexPortalDb(dbDir = dbDir, species = getSpeciesInfo(9606)$speciesCommon) - } + } # nocov end if (!is.null(CPortal10090Db)) { CPortal10090.in <- CPortal10090Db - } else { + } else { # nocov start CPortal10090.in <- .getComplexPortalDb(dbDir = dbDir, species = getSpeciesInfo(10090)$speciesCommon) - } + } # nocov end if (!is.null(CPortal6239Db)) { CPortal6239.in <- CPortal6239Db - } else { + } else { # nocov start CPortal6239.in <- .getComplexPortalDb(dbDir = dbDir, species = getSpeciesInfo(6239)$speciesCommon) - } + } # nocov end if (!is.null(CPortal284812Db)) { CPortal284812.in <- CPortal284812Db - } else { + } else { # nocov start CPortal284812.in <- .getComplexPortalDb(dbDir = dbDir, species = getSpeciesInfo(284812)$speciesCommon) - } + } # nocov end ### custom complexes source: please provide. ## (format: c("Complex.name", "Gene.names", "Species.common", "Source") @@ -305,9 +305,9 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, S4Vectors::mcols(YEAST.chl)$PMID <- YEAST.pmid$PubMed_id[match(names(YEAST.chl), YEAST.pmid$Complex)] - } else { + } else { # nocov start YEAST.chl <- NULL - } + } # nocov end ## CORUM if (!is.null(CORUM.in)) { @@ -339,9 +339,9 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, l$ComplexName))] l0 }) - } else { + } else { # nocov start CORUM.chl <- NULL - } + } # nocov end ## S. pombe if (!is.null(SCHPO.in)) { @@ -367,9 +367,9 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, S4Vectors::mcols(SCHPO.chl)$PMID <- SCHPO.pmid$source[match(names(SCHPO.chl), SCHPO.pmid$GO_name)] - } else { + } else { # nocov start SCHPO.chl <- NULL - } + } # nocov end ## HuMAP2 if (!is.null(HUMAP2.in)) { @@ -384,9 +384,9 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, Source = "HuMAP2", PMID = "" ) - } else { + } else { # nocov start HUMAP2.chl <- NULL - } + } # nocov end ## ComplexPortal if (!is.null(CPortal9606.in)) { @@ -408,9 +408,9 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, Source = "ComplexPortal", PMID = "" ) - } else { + } else { # nocov start CPortal9606.chl <- NULL - } + } # nocov end if (!is.null(CPortal10090.in)) { mapdf <- getUniProtToIDMapping(10090, targetId = "Gene_Name") @@ -431,9 +431,9 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, Source = "ComplexPortal", PMID = "" ) - } else { + } else { # nocov start CPortal10090.chl <- NULL - } + } # nocov end if (!is.null(CPortal284812.in)) { mapdf <- getUniProtToIDMapping(284812, targetId = "Gene_Name") @@ -454,9 +454,9 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, Source = "ComplexPortal", PMID = "" ) - } else { + } else { # nocov start CPortal284812.chl <- NULL - } + } # nocov end if (!is.null(CPortal6239.in)) { mapdf <- getUniProtToIDMapping(6239, targetId = "Gene_Name") @@ -477,9 +477,9 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, Source = "ComplexPortal", PMID = "" ) - } else { + } else { # nocov start CPortal6239.chl <- NULL - } + } # nocov end ## Custom if (!is.null(custom.in)) { @@ -501,9 +501,9 @@ makeComplexDB <- function(dbDir, customComplexTxt = NULL, Cyc2008Db = NULL, PMID = NA_character_ ) } - } else { + } else { # nocov start custom.chl <- NULL - } + } # nocov end ## ------------------------------------------------------------------------- ## Combine From 0e3bc7e8128ff95e3e385343cc282287ea7ab6ca Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 29 Apr 2026 17:36:08 +0200 Subject: [PATCH 45/65] Add assertions in text snippet functions --- R/textSnippets.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/R/textSnippets.R b/R/textSnippets.R index 8abd548..c9d2454 100644 --- a/R/textSnippets.R +++ b/R/textSnippets.R @@ -245,6 +245,9 @@ introText <- function(expType) { #' @rdname textSnippets #' @export filterByModText <- function(excludeUnmodifiedPeptides, keepModifications) { + .assertScalar(x = excludeUnmodifiedPeptides, type = "logical") + .assertVector(x = keepModifications, type = "character", allowNULL = TRUE) + if (excludeUnmodifiedPeptides && !is.null(keepModifications)) { paste0("Next, we filter out unmodified peptides and peptides ", "without any of the requested modifications ", @@ -313,6 +316,8 @@ inputText <- function(expTypeLevel) { #' @rdname textSnippets #' @export featureCollectionText <- function(featureCollections) { + .assertVector(x = featureCollections, type = "character", allowNULL = TRUE) + if (length(featureCollections) == 0) { out <- paste0("No feature collections were tested.") } else { From 814c3f27a667f0d65b5187b6a0f1858b55d03833 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 29 Apr 2026 17:36:54 +0200 Subject: [PATCH 46/65] Expand unit tests --- tests/testthat/test-doFilter.R | 10 ++++ tests/testthat/test-importDIANN.R | 4 +- tests/testthat/test-makeComplexDB.R | 3 +- .../testthat/test-prepareFeatureCollections.R | 31 +++++++++++- tests/testthat/test-textSnippets.R | 50 +++++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) diff --git a/tests/testthat/test-doFilter.R b/tests/testthat/test-doFilter.R index 4b1abb0..39407c9 100644 --- a/tests/testthat/test-doFilter.R +++ b/tests/testthat/test-doFilter.R @@ -99,6 +99,16 @@ test_that("filtering works (MaxQuant)", { ))) expect_equal(nrow(out), 97L) + ## A single filter, with plot + out <- filterFeaturesSE( + sce_mq_final, + filtersSE = list(Score = function(se) rowData(se)$Score >= 2), + plotUpset = TRUE, exclFile = NULL) + expect_equal(nrow(out), length(which( + rowData(sce_mq_final)$Score >= 2 + ))) + expect_equal(nrow(out), 130L) + ## Don't filter on score out <- filterFeaturesSE( sce_mq_final, diff --git a/tests/testthat/test-importDIANN.R b/tests/testthat/test-importDIANN.R index 27cf477..37e04db 100644 --- a/tests/testthat/test-importDIANN.R +++ b/tests/testthat/test-importDIANN.R @@ -10,9 +10,9 @@ test_that("DIANN import works", { ## ------------------------------------------------------------------------- expect_error(importDIANN(inFile = 1), "'inFile' must be of class 'character'") - expect_error(importExperiment(inFile = c(pgmat, prmat)), + expect_error(importDIANN(inFile = c(pgmat, prmat)), "'inFile' must have length 1") - expect_error(importExperiment(inFile = "missing"), + expect_error(importDIANN(inFile = "missing"), "file.exists(inFile) is not TRUE", fixed = TRUE) expect_error(importDIANN(inFile = pgmat, fileType = 1), diff --git a/tests/testthat/test-makeComplexDB.R b/tests/testthat/test-makeComplexDB.R index c2f44b1..054b1a9 100644 --- a/tests/testthat/test-makeComplexDB.R +++ b/tests/testthat/test-makeComplexDB.R @@ -330,7 +330,8 @@ test_that("makeComplexDB works", { Source = "Custom") write.table(custom, file = custompath2, col.names = TRUE, row.names = FALSE, quote = FALSE, sep = "\t") - dbs <- makeComplexDB(dbDir = dbdir, customComplexTxt = custompath2, + dbs <- makeComplexDB(dbDir = file.path(dbdir, "newdir"), + customComplexTxt = custompath2, Cyc2008Db = cyc2008db, CorumDb = corumdb, PombaseDb = pombasedb, HuMAP2Db = humap2db, CPortal9606Db = cportal9606db, diff --git a/tests/testthat/test-prepareFeatureCollections.R b/tests/testthat/test-prepareFeatureCollections.R index c441d8d..40add75 100644 --- a/tests/testthat/test-prepareFeatureCollections.R +++ b/tests/testthat/test-prepareFeatureCollections.R @@ -211,7 +211,7 @@ test_that("preparing feature collections works", { ## Works with correct arguments args <- args0 args$includeFeatureCollections <- c("complexes", "GO") - fcoll <- do.call(prepareFeatureCollections, args) + suppressMessages(fcoll <- do.call(prepareFeatureCollections, args)) expect_type(fcoll, "list") expect_named(fcoll, c("complexes", "GO")) expect_s4_class(fcoll$complexes, "CharacterList") @@ -236,6 +236,35 @@ test_that("preparing feature collections works", { expect_named(mcc, c("genes", "nGenes", "sharedGenes", "nSharedGenes")) expect_equal(lengths(fcoll$GO), mcc$nSharedGenes) + ## Use built-in complexes file + args <- args0 + args$includeFeatureCollections <- c("complexes", "pathways") + args$complexDbPath <- NULL + args <- c(args, list(complexDbPath = NULL)) + suppressMessages(fcoll <- do.call(prepareFeatureCollections, args)) + expect_type(fcoll, "list") + expect_named(fcoll, c("complexes", "pathways")) + expect_s4_class(fcoll$complexes, "CharacterList") + expect_length(fcoll$complexes, 19) + expect_true(all(lengths(fcoll$complexes) >= 2)) + expect_true(all(unlist(fcoll$complexes) %in% rownames(sce))) + mcc <- S4Vectors::mcols(fcoll$complexes) + expect_s4_class(mcc, "DFrame") + expect_named(mcc, c("Species.common", "Source", "PMID", "All.names", + "genes", "nGenes", "sharedGenes", "nSharedGenes")) + expect_equal(lengths(fcoll$complexes), mcc$nSharedGenes) + expect_equal(mcc$Species.common, rep("mouse", 19)) + + expect_s4_class(fcoll$pathways, "CharacterList") + # expect_length(fcoll$pathways, 1537) + expect_true(all(lengths(fcoll$pathways) >= 2)) + expect_equal(lengths(fcoll$pathways)[1:5], c(2, 4, 2, 2, 2), ignore_attr = TRUE) + expect_true(all(unlist(fcoll$pathways) %in% rownames(sce))) + mcc <- S4Vectors::mcols(fcoll$pathways) + expect_s4_class(mcc, "DFrame") + expect_named(mcc, c("genes", "nGenes", "sharedGenes", "nSharedGenes")) + expect_equal(lengths(fcoll$pathways), mcc$nSharedGenes) + ## Wrong column for IDs - empty output args <- args0 args$idCol <- "Majority.protein.IDs" diff --git a/tests/testthat/test-textSnippets.R b/tests/testthat/test-textSnippets.R index 765bfbe..b75086b 100644 --- a/tests/testthat/test-textSnippets.R +++ b/tests/testthat/test-textSnippets.R @@ -141,6 +141,11 @@ test_that("text snippet generation works", { expect_true(grepl("[Spectronaut](https://biognosys.com/", introText(expType = "Spectronaut"), fixed = TRUE)) + expect_type(introText(expType = "SE"), "character") + expect_equal(length(introText(expType = "SE")), 1) + expect_true(grepl("SummarizedExperiment object", + introText(expType = "SE"), fixed = TRUE)) + ## inputText expect_error(inputText(expTypeLevel = 1), "'expTypeLevel' must be of class 'character'") @@ -179,6 +184,11 @@ test_that("text snippet generation works", { expect_true(grepl("The input to this workflow is a `Report.tsv`", inputText(expTypeLevel = "Spectronaut"), fixed = TRUE)) + expect_type(inputText(expTypeLevel = "SE"), "character") + expect_equal(length(inputText(expTypeLevel = "SE")), 1) + expect_true(grepl("The input to this workflow is a serialized SummarizedExperiment", + inputText(expTypeLevel = "SE"), fixed = TRUE)) + ## emptySampleText sce0 <- sce_mq_final expect_error(emptySampleText(sce = 1, assayName = "log2_iBAQ_withNA"), @@ -205,4 +215,44 @@ test_that("text snippet generation works", { "features and will be removed from further analysis: ", "Adnp_IP05, Chd4BF_IP08.")) + ## filterByModText + expect_error(filterByModText(excludeUnmodifiedPeptides = 1, + keepModifications = "mod1"), + "'excludeUnmodifiedPeptides' must be of class 'logical'") + expect_error(filterByModText(excludeUnmodifiedPeptides = c(TRUE, FALSE), + keepModifications = "mod1"), + "'excludeUnmodifiedPeptides' must have length 1") + expect_error(filterByModText(excludeUnmodifiedPeptides = TRUE, + keepModifications = 1), + "'keepModifications' must be of class 'character'") + + expect_type(filterByModText(excludeUnmodifiedPeptides = TRUE, + keepModifications = NULL), "character") + expect_equal(filterByModText(excludeUnmodifiedPeptides = TRUE, + keepModifications = NULL), + "Next, we filter out unmodified peptides.") + expect_equal(filterByModText(excludeUnmodifiedPeptides = TRUE, + keepModifications = "mod1"), + "Next, we filter out unmodified peptides and peptides without any of the requested modifications (mod1).") + expect_equal(filterByModText(excludeUnmodifiedPeptides = FALSE, + keepModifications = NULL), + "") + expect_equal(filterByModText(excludeUnmodifiedPeptides = FALSE, + keepModifications = "mod1"), + "Next, we filter out peptides without any of the requested modifications (mod1).") + + ## featureCollectionText + expect_error(featureCollectionText(featureCollections = 1), + "'featureCollections' must be of class 'character'") + + expect_type(featureCollectionText(featureCollections = "complexes"), "character") + expect_equal(featureCollectionText(featureCollections = c()), + "No feature collections were tested.") + expect_true(grepl("Complexes are obtained from Corum", + featureCollectionText(featureCollections = c("complexes", "GO", "pathways")))) + expect_true(grepl("GO terms are obtained from MSigDB.", + featureCollectionText(featureCollections = c("complexes", "GO", "pathways")))) + expect_true(grepl("Pathway information is obtained from", + featureCollectionText(featureCollections = c("complexes", "GO", "pathways")))) + }) From 72683c0a3f4c8c3a0fb64eaa0d5fffa4ca1628d3 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 29 Apr 2026 17:50:46 +0200 Subject: [PATCH 47/65] Add Spectronaut example data --- ...PrecInt_0_reduced_pivot_filtered.setup.txt | 3162 +++++++++++++++++ .../SN_unit_test_subset.tsv | 201 ++ 2 files changed, 3363 insertions(+) create mode 100644 inst/extdata/spectronaut_example/PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.setup.txt create mode 100644 inst/extdata/spectronaut_example/SN_unit_test_subset.tsv diff --git a/inst/extdata/spectronaut_example/PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.setup.txt b/inst/extdata/spectronaut_example/PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.setup.txt new file mode 100644 index 0000000..67348a3 --- /dev/null +++ b/inst/extdata/spectronaut_example/PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.setup.txt @@ -0,0 +1,3162 @@ +Spectronaut 20.4.260109.92449 +Computer Name: cmp +User Domain Name: dom +User Name: user +Analysis Mode: UI +Analysis Type: directDIA +Analysis Date: 19-February-2026 15:08:07 UTC +01:00 + + +[BEGIN-SETTINGS] +Settings Used: PAF_einprot (default BGS) + ├─ DIA Analysis\Calibration + │ ├─ MZ Extraction Strategy: Maximum Intensity + │ ├─ Precision iRT: True + │ │ ├─ Exclude De-amidated Peptides: True + │ │ └─ iRT <-> RT Regression Type: Local (Non-Linear) Regression + │ ├─ MS1 Mass Tolerance Strategy: System Default + │ └─ MS2 Mass Tolerance Strategy: System Default + ├─ DIA Analysis\Identification + │ ├─ Precursor Qvalue Cutoff: 0.01 + │ ├─ Precursor Qvalue Cutoff (Experiment): 0.01 + │ ├─ Precursor PEP Cutoff: 0.2 + │ ├─ Protein Qvalue Cutoff (Experiment): 0.01 + │ ├─ Protein Qvalue Cutoff (Run): 0.05 + │ ├─ Protein PEP Cutoff: 0.75 + │ ├─ Single Hit Definition: By Stripped Sequence + │ ├─ Single Hit Protein Rule: Stratified Single Hit Protein FDR + │ ├─ Run-Level Protein Scoring: Highest Scoring Observation (SN19 default) + │ ├─ Exclude Duplicate Assays: True + │ ├─ Exclude Predicted Fragment Scores: False + │ ├─ Generate Decoys: True + │ │ ├─ Decoy Generation Method: Mutated + │ │ │ └─ Preferred Fragment Source: NN Predicted Fragments + │ │ └─ Decoy Limit Strategy: Dynamic + │ │ └─ Library Size Fraction: 0.1 + │ ├─ ISF Filter Rule: Keep "Likely In-Source Fragmented" Peptides + │ └─ Pvalue Estimator: Kernel Density Estimator + ├─ DIA Analysis\Pipeline Mode + │ ├─ Export All XICs: False + │ ├─ Export report in Parquet format: True + │ ├─ Generate SNE File: True + │ │ └─ Store Ion traces in SNE: False + │ ├─ Post Analysis Reports: + │ │ ├─ Binned CVs: True + │ │ ├─ Binned Identification: True + │ │ ├─ CV Density Line Chart: True + │ │ ├─ CVs Below X Bar Chart: True + │ │ ├─ Data Completeness Bar Chart: True + │ │ ├─ Modification Enrichment: True + │ │ ├─ Run Identifications Bar Chart: True + │ │ ├─ Scoring Histograms: True + │ │ └─ TIC Overlay: True + │ ├─ PTM Report Schema: + │ ├─ Report Schema: einprot_Pivot_PG_reduced (Pivot) + │ └─ Reporting Unit: Across Experiment + ├─ DIA Analysis\Post Analysis + │ ├─ Differential Abundance Testing: Unpaired t-test + │ │ ├─ Assume Equal Variance: False + │ │ ├─ Group-Wise Testing Correction: False + │ │ ├─ Log2 Ratio Candidate Filter: 0.58 + │ │ └─ Confidence Candidate Filter: Qvalue + │ │ └─ Confidence: 0.05 + │ ├─ Differential Abundance Grouping: Major Group (Quantification Settings) + │ │ └─ Smallest Quantitative Unit: Major Group (Quantification Settings) + │ │ └─ Use All MS-Level Quantities: True + │ ├─ Calculate Explained TIC: None + │ ├─ Calculate Sample Correlation Matrix: False + │ ├─ Gene Ontology: C:\Users\user\AppData\Roaming\Spectronaut\geneOntology\Ontologies\bgs_default_go-basic.obo + │ └─ Hierarchical Clustering: True + │ ├─ Distance Metric: Manhattan Distance + │ ├─ Linkage Strategy: Ward's Method + │ ├─ Order Runs by Clustering: True + │ └─ Z-score Transformation: False + ├─ DIA Analysis\Protein Inference + │ └─ Protein Inference Workflow: Automatic + │ └─ Inference Algorithm: IDPicker + ├─ DIA Analysis\PTM Workflow + │ ├─ Input Normalization Strategy: None + │ └─ PTM Localization: False + ├─ DIA Analysis\Quantification + │ ├─ Precursor Filtering: Identified (Qvalue) + │ │ ├─ Imputation Strategy: None + │ │ └─ Multi Channel Qvalue Filter: Group Qvalue + │ ├─ Proteotypicity Filter: None + │ ├─ Protein LFQ Method: Automatic + │ ├─ Quantity MS Level: MS2 + │ ├─ Quantity Type: Area + │ ├─ Cross-Run Normalization: False + │ ├─ Perform background noise removal: True + │ ├─ Quantification window: Synchronized + │ ├─ Interference Correction: True + │ │ ├─ Only Identified Peptides: True + │ │ ├─ Exclude All Multi-Channel Interferences: True + │ │ ├─ MS1 Min: 2 + │ │ └─ MS2 Min: 3 + │ ├─ Major (Protein) Grouping: by Protein Group Id + │ ├─ Minor (Peptide) Grouping: by Stripped Sequence + │ ├─ Major Group Quantity: Mean peptide quantity + │ ├─ Major Group Top N: True + │ │ ├─ Max: 3 + │ │ └─ Min: 1 + │ ├─ Minor Group Quantity: Mean precursor quantity + │ ├─ Minor Group Top N: True + │ │ ├─ Max: 3 + │ │ └─ Min: 1 + │ └─ Use Log2 Quantity Filter: True + │ └─ Minimum Log2 Precursor Quantity: 0 + ├─ DIA Analysis\Workflow + │ ├─ Method Evaluation: False + │ ├─ Profiling Strategy: None + │ ├─ Run Limit for directDIA Library: -1 + │ ├─ Hybrid (DDA + DIA) Library: False + │ └─ Unify Peptide Peaks Strategy: None + ├─ DIA Analysis\XIC Extraction + │ ├─ XIC IM Extraction Window: Dynamic + │ │ └─ Correction Factor: 1 + │ ├─ XIC RT Extraction Window: Dynamic + │ │ └─ Correction Factor: 1 + │ ├─ MS1 Mass Tolerance Strategy: Dynamic + │ │ └─ Correction Factor: 1 + │ └─ MS2 Mass Tolerance Strategy: Dynamic + │ └─ Correction Factor: 1 + ├─ Pulsar Search\Identification + │ ├─ Grouped Peptide FDR: None + │ ├─ PSM FDR: 0.01 + │ ├─ Peptide FDR: 0.01 + │ ├─ Protein Group FDR: 0.01 + │ ├─ directDIA Workflow: directDIA+ (Deep) + │ │ ├─ IM Sampling Reduction: 7 + │ │ └─ RT Sampling Reduction: 1 + │ ├─ PTM Localization Filter: False + │ └─ Semi-Specific Pipeline: Smart Enumeration (SN20) + ├─ Pulsar Search\iRT Calibration + │ ├─ Calibrate from Empirical RT: False + │ ├─ iRT Reference Strategy: Deep Learning Assisted iRT Regression + │ └─ Minimum Rsquare: 0.8 + ├─ Pulsar Search\Labeling + │ └─ Channels: + │ ├─ Channel 1: False + │ ├─ Channel 2: False + │ ├─ Channel 3: False + │ ├─ Channel 4: False + │ └─ Channel 5: False + ├─ Pulsar Search\Modifications + │ └─ Search Mode: Closed Search + │ ├─ Max Variable Modifications: 5 + │ ├─ Fixed Modifications:: Carbamidomethyl (C) + │ └─ Variable Modifications:: Acetyl (Protein N-term), Oxidation (M) + ├─ Pulsar Search\Peptides + │ ├─ Enzymes / Cleavage Rules: Trypsin/P + │ ├─ Digest Type: Specific + │ ├─ Decoy Generation Rule: KR + │ ├─ Max Peptide Length: 52 + │ ├─ Min Peptide Length: 7 + │ ├─ Missed Cleavages: 2 + │ └─ Toggle N-terminal M: True + ├─ Pulsar Search\Result Filters + │ ├─ Fragment Ions: + │ │ ├─ Ion AA Length: True + │ │ │ └─ N: 3 + │ │ ├─ Ion Charge: False + │ │ ├─ Ion Loss Type: False + │ │ ├─ Ion Type: False + │ │ ├─ m/z : True + │ │ │ ├─ Max: 3000 + │ │ │ └─ Min: 200 + │ │ ├─ Overlapping between Channels: False + │ │ └─ Relative Intensity: True + │ │ └─ Min: 1 + │ └─ Precursors: + │ ├─ Amino Acids: False + │ ├─ Best N Fragments per Peptide: True + │ │ ├─ Max: 6 + │ │ └─ Min: 3 + │ ├─ Best N Peptides per Protein Group: False + │ ├─ Channel Count: False + │ ├─ FASTA Matched: False + │ ├─ Missed Cleavage: False + │ ├─ Modifications: None + │ ├─ Peptide Charge: False + │ └─ Proteotypicity: False + ├─ Pulsar Search\Speed-Up + │ ├─ MS2 Index: Automatic + │ └─ diaPASEF Pre-Processing: Automatic + ├─ Pulsar Search\Tolerances + │ └─ Tolerance Parameters: + │ ├─ Thermo IonTrap: + │ │ ├─ Calibration Search: Dynamic + │ │ │ ├─ MS1 Correction Factor: 1 + │ │ │ └─ MS2 Correction Factor: 1 + │ │ └─ Main Search: Dynamic + │ │ ├─ MS1 Correction Factor: 1 + │ │ └─ MS2 Correction Factor: 1 + │ ├─ Thermo Orbitrap: + │ │ ├─ Calibration Search: Dynamic + │ │ │ ├─ MS1 Correction Factor: 1 + │ │ │ └─ MS2 Correction Factor: 1 + │ │ └─ Main Search: Dynamic + │ │ ├─ MS1 Correction Factor: 1 + │ │ └─ MS2 Correction Factor: 1 + │ └─ TOF: + │ ├─ Calibration Search: Dynamic + │ │ ├─ MS1 Correction Factor: 1 + │ │ └─ MS2 Correction Factor: 1 + │ └─ Main Search: Dynamic + │ ├─ MS1 Correction Factor: 1 + │ └─ MS2 Correction Factor: 1 + └─ Pulsar Search\Workflow + ├─ Fragment Ion Selection Strategy: Intensity Based + ├─ In-Silico Generate Missing Channels: False + └─ Use DNN Predicted Ion Mobility: Auto +[END-SETTINGS] + +[BEGIN-SETUP] +Run: LFQ_24min_500ng_2Th_3ms_A_01.raw + ├─ Vendor: Thermo + ├─ File: LFQ_24min_500ng_2Th_3ms_A_01 + ├─ Path: "LFQ_24min_500ng_2Th_3ms_A_01.raw" + ├─ Condition: Mix_A + ├─ Replicate: 1 + ├─ HTRMS Version: 20.4.260109.92449 + ├─ Protein Databases Used + │ ├─ ECOLI__250312 + │ │ ├─ Protein Entries: 4’273 + │ │ ├─ Original File: ECOLI__250312.fasta + │ │ ├─ Date Created: 30-October-2025 16:07:09 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ ├─ HUMAN__230321 + │ │ ├─ Protein Entries: 20’591 + │ │ ├─ Original File: HUMAN__230321.fasta + │ │ ├─ Date Created: 04-March-2025 12:31:56 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ └─ YEAST__230321 + │ ├─ Protein Entries: 6’060 + │ ├─ Original File: YEAST__230321.fasta + │ ├─ Date Created: 28-January-2026 14:06:11 UTC +01:00 + │ ├─ Date Modified: 17-February-2026 17:25:50 UTC +01:00 + │ └─ Parsing Rule: UniProt FASTA + └─ MS2 Method + ├─ [380.4 - 382.4] + ├─ [382.4 - 384.4] + ├─ [384.4 - 386.4] + ├─ [386.4 - 388.4] + ├─ [388.4 - 390.4] + ├─ [390.4 - 392.4] + ├─ [392.4 - 394.4] + ├─ [394.4 - 396.4] + ├─ [396.4 - 398.4] + ├─ [398.4 - 400.4] + ├─ [400.4 - 402.4] + ├─ [402.4 - 404.4] + ├─ [404.4 - 406.4] + ├─ [406.4 - 408.4] + ├─ [408.4 - 410.4] + ├─ [410.4 - 412.4] + ├─ [412.4 - 414.4] + ├─ [414.4 - 416.4] + ├─ [416.4 - 418.4] + ├─ [418.4 - 420.4] + ├─ [420.4 - 422.4] + ├─ [422.4 - 424.4] + ├─ [424.4 - 426.4] + ├─ [426.4 - 428.4] + ├─ [428.4 - 430.4] + ├─ [430.4 - 432.4] + ├─ [432.4 - 434.4] + ├─ [434.4 - 436.4] + ├─ [436.4 - 438.4] + ├─ [438.4 - 440.5] + ├─ [440.5 - 442.5] + ├─ [442.5 - 444.5] + ├─ [444.5 - 446.5] + ├─ [446.5 - 448.5] + ├─ [448.5 - 450.5] + ├─ [450.5 - 452.5] + ├─ [452.5 - 454.5] + ├─ [454.5 - 456.5] + ├─ [456.5 - 458.5] + ├─ [458.5 - 460.5] + ├─ [460.5 - 462.5] + ├─ [462.5 - 464.5] + ├─ [464.5 - 466.5] + ├─ [466.5 - 468.5] + ├─ [468.5 - 470.5] + ├─ [470.5 - 472.5] + ├─ [472.5 - 474.5] + ├─ [474.5 - 476.5] + ├─ [476.5 - 478.5] + ├─ [478.5 - 480.5] + ├─ [480.5 - 482.5] + ├─ [482.5 - 484.5] + ├─ [484.5 - 486.5] + ├─ [486.5 - 488.5] + ├─ [488.5 - 490.5] + ├─ [490.5 - 492.5] + ├─ [492.5 - 494.5] + ├─ [494.5 - 496.5] + ├─ [496.5 - 498.5] + ├─ [498.5 - 500.5] + ├─ [500.5 - 502.5] + ├─ [502.5 - 504.5] + ├─ [504.5 - 506.5] + ├─ [506.5 - 508.5] + ├─ [508.5 - 510.5] + ├─ [510.5 - 512.5] + ├─ [512.5 - 514.5] + ├─ [514.5 - 516.5] + ├─ [516.5 - 518.5] + ├─ [518.5 - 520.5] + ├─ [520.5 - 522.5] + ├─ [522.5 - 524.5] + ├─ [524.5 - 526.5] + ├─ [526.5 - 528.5] + ├─ [528.5 - 530.5] + ├─ [530.5 - 532.5] + ├─ [532.5 - 534.5] + ├─ [534.5 - 536.5] + ├─ [536.5 - 538.5] + ├─ [538.5 - 540.5] + ├─ [540.5 - 542.5] + ├─ [542.5 - 544.5] + ├─ [544.5 - 546.5] + ├─ [546.5 - 548.5] + ├─ [548.5 - 550.5] + ├─ [550.5 - 552.5] + ├─ [552.5 - 554.5] + ├─ [554.5 - 556.5] + ├─ [556.5 - 558.5] + ├─ [558.5 - 560.5] + ├─ [560.5 - 562.5] + ├─ [562.5 - 564.5] + ├─ [564.5 - 566.5] + ├─ [566.5 - 568.5] + ├─ [568.5 - 570.5] + ├─ [570.5 - 572.5] + ├─ [572.5 - 574.5] + ├─ [574.5 - 576.5] + ├─ [576.5 - 578.5] + ├─ [578.5 - 580.5] + ├─ [580.5 - 582.5] + ├─ [582.5 - 584.5] + ├─ [584.5 - 586.5] + ├─ [586.5 - 588.5] + ├─ [588.5 - 590.5] + ├─ [590.5 - 592.5] + ├─ [592.5 - 594.5] + ├─ [594.5 - 596.5] + ├─ [596.5 - 598.5] + ├─ [598.5 - 600.5] + ├─ [600.5 - 602.5] + ├─ [602.5 - 604.5] + ├─ [604.5 - 606.5] + ├─ [606.5 - 608.5] + ├─ [608.5 - 610.5] + ├─ [610.5 - 612.5] + ├─ [612.5 - 614.5] + ├─ [614.5 - 616.5] + ├─ [616.5 - 618.5] + ├─ [618.5 - 620.5] + ├─ [620.5 - 622.5] + ├─ [622.5 - 624.5] + ├─ [624.5 - 626.5] + ├─ [626.5 - 628.5] + ├─ [628.5 - 630.5] + ├─ [630.5 - 632.5] + ├─ [632.5 - 634.5] + ├─ [634.5 - 636.5] + ├─ [636.5 - 638.5] + ├─ [638.5 - 640.5] + ├─ [640.5 - 642.5] + ├─ [642.5 - 644.5] + ├─ [644.5 - 646.5] + ├─ [646.5 - 648.5] + ├─ [648.5 - 650.5] + ├─ [650.5 - 652.5] + ├─ [652.5 - 654.5] + ├─ [654.5 - 656.5] + ├─ [656.5 - 658.5] + ├─ [658.5 - 660.6] + ├─ [660.6 - 662.6] + ├─ [662.6 - 664.6] + ├─ [664.6 - 666.6] + ├─ [666.6 - 668.6] + ├─ [668.6 - 670.6] + ├─ [670.6 - 672.6] + ├─ [672.6 - 674.6] + ├─ [674.6 - 676.6] + ├─ [676.6 - 678.6] + ├─ [678.6 - 680.6] + ├─ [680.6 - 682.6] + ├─ [682.6 - 684.6] + ├─ [684.6 - 686.6] + ├─ [686.6 - 688.6] + ├─ [688.6 - 690.6] + ├─ [690.6 - 692.6] + ├─ [692.6 - 694.6] + ├─ [694.6 - 696.6] + ├─ [696.6 - 698.6] + ├─ [698.6 - 700.6] + ├─ [700.6 - 702.6] + ├─ [702.6 - 704.6] + ├─ [704.6 - 706.6] + ├─ [706.6 - 708.6] + ├─ [708.6 - 710.6] + ├─ [710.6 - 712.6] + ├─ [712.6 - 714.6] + ├─ [714.6 - 716.6] + ├─ [716.6 - 718.6] + ├─ [718.6 - 720.6] + ├─ [720.6 - 722.6] + ├─ [722.6 - 724.6] + ├─ [724.6 - 726.6] + ├─ [726.6 - 728.6] + ├─ [728.6 - 730.6] + ├─ [730.6 - 732.6] + ├─ [732.6 - 734.6] + ├─ [734.6 - 736.6] + ├─ [736.6 - 738.6] + ├─ [738.6 - 740.6] + ├─ [740.6 - 742.6] + ├─ [742.6 - 744.6] + ├─ [744.6 - 746.6] + ├─ [746.6 - 748.6] + ├─ [748.6 - 750.6] + ├─ [750.6 - 752.6] + ├─ [752.6 - 754.6] + ├─ [754.6 - 756.6] + ├─ [756.6 - 758.6] + ├─ [758.6 - 760.6] + ├─ [760.6 - 762.6] + ├─ [762.6 - 764.6] + ├─ [764.6 - 766.6] + ├─ [766.6 - 768.6] + ├─ [768.6 - 770.6] + ├─ [770.6 - 772.6] + ├─ [772.6 - 774.6] + ├─ [774.6 - 776.6] + ├─ [776.6 - 778.6] + ├─ [778.6 - 780.6] + ├─ [780.6 - 782.6] + ├─ [782.6 - 784.6] + ├─ [784.6 - 786.6] + ├─ [786.6 - 788.6] + ├─ [788.6 - 790.6] + ├─ [790.6 - 792.6] + ├─ [792.6 - 794.6] + ├─ [794.6 - 796.6] + ├─ [796.6 - 798.6] + ├─ [798.6 - 800.6] + ├─ [800.6 - 802.6] + ├─ [802.6 - 804.6] + ├─ [804.6 - 806.6] + ├─ [806.6 - 808.6] + ├─ [808.6 - 810.6] + ├─ [810.6 - 812.6] + ├─ [812.6 - 814.6] + ├─ [814.6 - 816.6] + ├─ [816.6 - 818.6] + ├─ [818.6 - 820.6] + ├─ [820.6 - 822.6] + ├─ [822.6 - 824.6] + ├─ [824.6 - 826.6] + ├─ [826.6 - 828.6] + ├─ [828.6 - 830.6] + ├─ [830.6 - 832.6] + ├─ [832.6 - 834.6] + ├─ [834.6 - 836.6] + ├─ [836.6 - 838.6] + ├─ [838.6 - 840.6] + ├─ [840.6 - 842.6] + ├─ [842.6 - 844.6] + ├─ [844.6 - 846.6] + ├─ [846.6 - 848.6] + ├─ [848.6 - 850.6] + ├─ [850.6 - 852.6] + ├─ [852.6 - 854.6] + ├─ [854.6 - 856.6] + ├─ [856.6 - 858.6] + ├─ [858.6 - 860.6] + ├─ [860.6 - 862.6] + ├─ [862.6 - 864.6] + ├─ [864.6 - 866.6] + ├─ [866.6 - 868.6] + ├─ [868.6 - 870.6] + ├─ [870.6 - 872.6] + ├─ [872.6 - 874.6] + ├─ [874.6 - 876.6] + ├─ [876.6 - 878.6] + ├─ [878.6 - 880.7] + ├─ [880.7 - 882.7] + ├─ [882.7 - 884.7] + ├─ [884.7 - 886.7] + ├─ [886.7 - 888.7] + ├─ [888.7 - 890.7] + ├─ [890.7 - 892.7] + ├─ [892.7 - 894.7] + ├─ [894.7 - 896.7] + ├─ [896.7 - 898.7] + ├─ [898.7 - 900.7] + ├─ [900.7 - 902.7] + ├─ [902.7 - 904.7] + ├─ [904.7 - 906.7] + ├─ [906.7 - 908.7] + ├─ [908.7 - 910.7] + ├─ [910.7 - 912.7] + ├─ [912.7 - 914.7] + ├─ [914.7 - 916.7] + ├─ [916.7 - 918.7] + ├─ [918.7 - 920.7] + ├─ [920.7 - 922.7] + ├─ [922.7 - 924.7] + ├─ [924.7 - 926.7] + ├─ [926.7 - 928.7] + ├─ [928.7 - 930.7] + ├─ [930.7 - 932.7] + ├─ [932.7 - 934.7] + ├─ [934.7 - 936.7] + ├─ [936.7 - 938.7] + ├─ [938.7 - 940.7] + ├─ [940.7 - 942.7] + ├─ [942.7 - 944.7] + ├─ [944.7 - 946.7] + ├─ [946.7 - 948.7] + ├─ [948.7 - 950.7] + ├─ [950.7 - 952.7] + ├─ [952.7 - 954.7] + ├─ [954.7 - 956.7] + ├─ [956.7 - 958.7] + ├─ [958.7 - 960.7] + ├─ [960.7 - 962.7] + ├─ [962.7 - 964.7] + ├─ [964.7 - 966.7] + ├─ [966.7 - 968.7] + ├─ [968.7 - 970.7] + ├─ [970.7 - 972.7] + ├─ [972.7 - 974.7] + ├─ [974.7 - 976.7] + ├─ [976.7 - 978.7] + └─ [978.7 - 980.7] + +Run: LFQ_24min_500ng_2Th_3ms_A_02.raw + ├─ Vendor: Thermo + ├─ File: LFQ_24min_500ng_2Th_3ms_A_02 + ├─ Path: "LFQ_24min_500ng_2Th_3ms_A_02.raw" + ├─ Condition: Mix_A + ├─ Replicate: 2 + ├─ HTRMS Version: 20.4.260109.92449 + ├─ Protein Databases Used + │ ├─ ECOLI__250312 + │ │ ├─ Protein Entries: 4’273 + │ │ ├─ Original File: ECOLI__250312.fasta + │ │ ├─ Date Created: 30-October-2025 16:07:09 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ ├─ HUMAN__230321 + │ │ ├─ Protein Entries: 20’591 + │ │ ├─ Original File: HUMAN__230321.fasta + │ │ ├─ Date Created: 04-March-2025 12:31:56 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ └─ YEAST__230321 + │ ├─ Protein Entries: 6’060 + │ ├─ Original File: YEAST__230321.fasta + │ ├─ Date Created: 28-January-2026 14:06:11 UTC +01:00 + │ ├─ Date Modified: 17-February-2026 17:25:50 UTC +01:00 + │ └─ Parsing Rule: UniProt FASTA + └─ MS2 Method + ├─ [380.4 - 382.4] + ├─ [382.4 - 384.4] + ├─ [384.4 - 386.4] + ├─ [386.4 - 388.4] + ├─ [388.4 - 390.4] + ├─ [390.4 - 392.4] + ├─ [392.4 - 394.4] + ├─ [394.4 - 396.4] + ├─ [396.4 - 398.4] + ├─ [398.4 - 400.4] + ├─ [400.4 - 402.4] + ├─ [402.4 - 404.4] + ├─ [404.4 - 406.4] + ├─ [406.4 - 408.4] + ├─ [408.4 - 410.4] + ├─ [410.4 - 412.4] + ├─ [412.4 - 414.4] + ├─ [414.4 - 416.4] + ├─ [416.4 - 418.4] + ├─ [418.4 - 420.4] + ├─ [420.4 - 422.4] + ├─ [422.4 - 424.4] + ├─ [424.4 - 426.4] + ├─ [426.4 - 428.4] + ├─ [428.4 - 430.4] + ├─ [430.4 - 432.4] + ├─ [432.4 - 434.4] + ├─ [434.4 - 436.4] + ├─ [436.4 - 438.4] + ├─ [438.4 - 440.5] + ├─ [440.5 - 442.5] + ├─ [442.5 - 444.5] + ├─ [444.5 - 446.5] + ├─ [446.5 - 448.5] + ├─ [448.5 - 450.5] + ├─ [450.5 - 452.5] + ├─ [452.5 - 454.5] + ├─ [454.5 - 456.5] + ├─ [456.5 - 458.5] + ├─ [458.5 - 460.5] + ├─ [460.5 - 462.5] + ├─ [462.5 - 464.5] + ├─ [464.5 - 466.5] + ├─ [466.5 - 468.5] + ├─ [468.5 - 470.5] + ├─ [470.5 - 472.5] + ├─ [472.5 - 474.5] + ├─ [474.5 - 476.5] + ├─ [476.5 - 478.5] + ├─ [478.5 - 480.5] + ├─ [480.5 - 482.5] + ├─ [482.5 - 484.5] + ├─ [484.5 - 486.5] + ├─ [486.5 - 488.5] + ├─ [488.5 - 490.5] + ├─ [490.5 - 492.5] + ├─ [492.5 - 494.5] + ├─ [494.5 - 496.5] + ├─ [496.5 - 498.5] + ├─ [498.5 - 500.5] + ├─ [500.5 - 502.5] + ├─ [502.5 - 504.5] + ├─ [504.5 - 506.5] + ├─ [506.5 - 508.5] + ├─ [508.5 - 510.5] + ├─ [510.5 - 512.5] + ├─ [512.5 - 514.5] + ├─ [514.5 - 516.5] + ├─ [516.5 - 518.5] + ├─ [518.5 - 520.5] + ├─ [520.5 - 522.5] + ├─ [522.5 - 524.5] + ├─ [524.5 - 526.5] + ├─ [526.5 - 528.5] + ├─ [528.5 - 530.5] + ├─ [530.5 - 532.5] + ├─ [532.5 - 534.5] + ├─ [534.5 - 536.5] + ├─ [536.5 - 538.5] + ├─ [538.5 - 540.5] + ├─ [540.5 - 542.5] + ├─ [542.5 - 544.5] + ├─ [544.5 - 546.5] + ├─ [546.5 - 548.5] + ├─ [548.5 - 550.5] + ├─ [550.5 - 552.5] + ├─ [552.5 - 554.5] + ├─ [554.5 - 556.5] + ├─ [556.5 - 558.5] + ├─ [558.5 - 560.5] + ├─ [560.5 - 562.5] + ├─ [562.5 - 564.5] + ├─ [564.5 - 566.5] + ├─ [566.5 - 568.5] + ├─ [568.5 - 570.5] + ├─ [570.5 - 572.5] + ├─ [572.5 - 574.5] + ├─ [574.5 - 576.5] + ├─ [576.5 - 578.5] + ├─ [578.5 - 580.5] + ├─ [580.5 - 582.5] + ├─ [582.5 - 584.5] + ├─ [584.5 - 586.5] + ├─ [586.5 - 588.5] + ├─ [588.5 - 590.5] + ├─ [590.5 - 592.5] + ├─ [592.5 - 594.5] + ├─ [594.5 - 596.5] + ├─ [596.5 - 598.5] + ├─ [598.5 - 600.5] + ├─ [600.5 - 602.5] + ├─ [602.5 - 604.5] + ├─ [604.5 - 606.5] + ├─ [606.5 - 608.5] + ├─ [608.5 - 610.5] + ├─ [610.5 - 612.5] + ├─ [612.5 - 614.5] + ├─ [614.5 - 616.5] + ├─ [616.5 - 618.5] + ├─ [618.5 - 620.5] + ├─ [620.5 - 622.5] + ├─ [622.5 - 624.5] + ├─ [624.5 - 626.5] + ├─ [626.5 - 628.5] + ├─ [628.5 - 630.5] + ├─ [630.5 - 632.5] + ├─ [632.5 - 634.5] + ├─ [634.5 - 636.5] + ├─ [636.5 - 638.5] + ├─ [638.5 - 640.5] + ├─ [640.5 - 642.5] + ├─ [642.5 - 644.5] + ├─ [644.5 - 646.5] + ├─ [646.5 - 648.5] + ├─ [648.5 - 650.5] + ├─ [650.5 - 652.5] + ├─ [652.5 - 654.5] + ├─ [654.5 - 656.5] + ├─ [656.5 - 658.5] + ├─ [658.5 - 660.6] + ├─ [660.6 - 662.6] + ├─ [662.6 - 664.6] + ├─ [664.6 - 666.6] + ├─ [666.6 - 668.6] + ├─ [668.6 - 670.6] + ├─ [670.6 - 672.6] + ├─ [672.6 - 674.6] + ├─ [674.6 - 676.6] + ├─ [676.6 - 678.6] + ├─ [678.6 - 680.6] + ├─ [680.6 - 682.6] + ├─ [682.6 - 684.6] + ├─ [684.6 - 686.6] + ├─ [686.6 - 688.6] + ├─ [688.6 - 690.6] + ├─ [690.6 - 692.6] + ├─ [692.6 - 694.6] + ├─ [694.6 - 696.6] + ├─ [696.6 - 698.6] + ├─ [698.6 - 700.6] + ├─ [700.6 - 702.6] + ├─ [702.6 - 704.6] + ├─ [704.6 - 706.6] + ├─ [706.6 - 708.6] + ├─ [708.6 - 710.6] + ├─ [710.6 - 712.6] + ├─ [712.6 - 714.6] + ├─ [714.6 - 716.6] + ├─ [716.6 - 718.6] + ├─ [718.6 - 720.6] + ├─ [720.6 - 722.6] + ├─ [722.6 - 724.6] + ├─ [724.6 - 726.6] + ├─ [726.6 - 728.6] + ├─ [728.6 - 730.6] + ├─ [730.6 - 732.6] + ├─ [732.6 - 734.6] + ├─ [734.6 - 736.6] + ├─ [736.6 - 738.6] + ├─ [738.6 - 740.6] + ├─ [740.6 - 742.6] + ├─ [742.6 - 744.6] + ├─ [744.6 - 746.6] + ├─ [746.6 - 748.6] + ├─ [748.6 - 750.6] + ├─ [750.6 - 752.6] + ├─ [752.6 - 754.6] + ├─ [754.6 - 756.6] + ├─ [756.6 - 758.6] + ├─ [758.6 - 760.6] + ├─ [760.6 - 762.6] + ├─ [762.6 - 764.6] + ├─ [764.6 - 766.6] + ├─ [766.6 - 768.6] + ├─ [768.6 - 770.6] + ├─ [770.6 - 772.6] + ├─ [772.6 - 774.6] + ├─ [774.6 - 776.6] + ├─ [776.6 - 778.6] + ├─ [778.6 - 780.6] + ├─ [780.6 - 782.6] + ├─ [782.6 - 784.6] + ├─ [784.6 - 786.6] + ├─ [786.6 - 788.6] + ├─ [788.6 - 790.6] + ├─ [790.6 - 792.6] + ├─ [792.6 - 794.6] + ├─ [794.6 - 796.6] + ├─ [796.6 - 798.6] + ├─ [798.6 - 800.6] + ├─ [800.6 - 802.6] + ├─ [802.6 - 804.6] + ├─ [804.6 - 806.6] + ├─ [806.6 - 808.6] + ├─ [808.6 - 810.6] + ├─ [810.6 - 812.6] + ├─ [812.6 - 814.6] + ├─ [814.6 - 816.6] + ├─ [816.6 - 818.6] + ├─ [818.6 - 820.6] + ├─ [820.6 - 822.6] + ├─ [822.6 - 824.6] + ├─ [824.6 - 826.6] + ├─ [826.6 - 828.6] + ├─ [828.6 - 830.6] + ├─ [830.6 - 832.6] + ├─ [832.6 - 834.6] + ├─ [834.6 - 836.6] + ├─ [836.6 - 838.6] + ├─ [838.6 - 840.6] + ├─ [840.6 - 842.6] + ├─ [842.6 - 844.6] + ├─ [844.6 - 846.6] + ├─ [846.6 - 848.6] + ├─ [848.6 - 850.6] + ├─ [850.6 - 852.6] + ├─ [852.6 - 854.6] + ├─ [854.6 - 856.6] + ├─ [856.6 - 858.6] + ├─ [858.6 - 860.6] + ├─ [860.6 - 862.6] + ├─ [862.6 - 864.6] + ├─ [864.6 - 866.6] + ├─ [866.6 - 868.6] + ├─ [868.6 - 870.6] + ├─ [870.6 - 872.6] + ├─ [872.6 - 874.6] + ├─ [874.6 - 876.6] + ├─ [876.6 - 878.6] + ├─ [878.6 - 880.7] + ├─ [880.7 - 882.7] + ├─ [882.7 - 884.7] + ├─ [884.7 - 886.7] + ├─ [886.7 - 888.7] + ├─ [888.7 - 890.7] + ├─ [890.7 - 892.7] + ├─ [892.7 - 894.7] + ├─ [894.7 - 896.7] + ├─ [896.7 - 898.7] + ├─ [898.7 - 900.7] + ├─ [900.7 - 902.7] + ├─ [902.7 - 904.7] + ├─ [904.7 - 906.7] + ├─ [906.7 - 908.7] + ├─ [908.7 - 910.7] + ├─ [910.7 - 912.7] + ├─ [912.7 - 914.7] + ├─ [914.7 - 916.7] + ├─ [916.7 - 918.7] + ├─ [918.7 - 920.7] + ├─ [920.7 - 922.7] + ├─ [922.7 - 924.7] + ├─ [924.7 - 926.7] + ├─ [926.7 - 928.7] + ├─ [928.7 - 930.7] + ├─ [930.7 - 932.7] + ├─ [932.7 - 934.7] + ├─ [934.7 - 936.7] + ├─ [936.7 - 938.7] + ├─ [938.7 - 940.7] + ├─ [940.7 - 942.7] + ├─ [942.7 - 944.7] + ├─ [944.7 - 946.7] + ├─ [946.7 - 948.7] + ├─ [948.7 - 950.7] + ├─ [950.7 - 952.7] + ├─ [952.7 - 954.7] + ├─ [954.7 - 956.7] + ├─ [956.7 - 958.7] + ├─ [958.7 - 960.7] + ├─ [960.7 - 962.7] + ├─ [962.7 - 964.7] + ├─ [964.7 - 966.7] + ├─ [966.7 - 968.7] + ├─ [968.7 - 970.7] + ├─ [970.7 - 972.7] + ├─ [972.7 - 974.7] + ├─ [974.7 - 976.7] + ├─ [976.7 - 978.7] + └─ [978.7 - 980.7] + +Run: LFQ_24min_500ng_2Th_3ms_A_03.raw + ├─ Vendor: Thermo + ├─ File: LFQ_24min_500ng_2Th_3ms_A_03 + ├─ Path: "LFQ_24min_500ng_2Th_3ms_A_03.raw" + ├─ Condition: Mix_A + ├─ Replicate: 3 + ├─ HTRMS Version: 20.4.260109.92449 + ├─ Protein Databases Used + │ ├─ ECOLI__250312 + │ │ ├─ Protein Entries: 4’273 + │ │ ├─ Original File: ECOLI__250312.fasta + │ │ ├─ Date Created: 30-October-2025 16:07:09 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ ├─ HUMAN__230321 + │ │ ├─ Protein Entries: 20’591 + │ │ ├─ Original File: HUMAN__230321.fasta + │ │ ├─ Date Created: 04-March-2025 12:31:56 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ └─ YEAST__230321 + │ ├─ Protein Entries: 6’060 + │ ├─ Original File: YEAST__230321.fasta + │ ├─ Date Created: 28-January-2026 14:06:11 UTC +01:00 + │ ├─ Date Modified: 17-February-2026 17:25:50 UTC +01:00 + │ └─ Parsing Rule: UniProt FASTA + └─ MS2 Method + ├─ [380.4 - 382.4] + ├─ [382.4 - 384.4] + ├─ [384.4 - 386.4] + ├─ [386.4 - 388.4] + ├─ [388.4 - 390.4] + ├─ [390.4 - 392.4] + ├─ [392.4 - 394.4] + ├─ [394.4 - 396.4] + ├─ [396.4 - 398.4] + ├─ [398.4 - 400.4] + ├─ [400.4 - 402.4] + ├─ [402.4 - 404.4] + ├─ [404.4 - 406.4] + ├─ [406.4 - 408.4] + ├─ [408.4 - 410.4] + ├─ [410.4 - 412.4] + ├─ [412.4 - 414.4] + ├─ [414.4 - 416.4] + ├─ [416.4 - 418.4] + ├─ [418.4 - 420.4] + ├─ [420.4 - 422.4] + ├─ [422.4 - 424.4] + ├─ [424.4 - 426.4] + ├─ [426.4 - 428.4] + ├─ [428.4 - 430.4] + ├─ [430.4 - 432.4] + ├─ [432.4 - 434.4] + ├─ [434.4 - 436.4] + ├─ [436.4 - 438.4] + ├─ [438.4 - 440.5] + ├─ [440.5 - 442.5] + ├─ [442.5 - 444.5] + ├─ [444.5 - 446.5] + ├─ [446.5 - 448.5] + ├─ [448.5 - 450.5] + ├─ [450.5 - 452.5] + ├─ [452.5 - 454.5] + ├─ [454.5 - 456.5] + ├─ [456.5 - 458.5] + ├─ [458.5 - 460.5] + ├─ [460.5 - 462.5] + ├─ [462.5 - 464.5] + ├─ [464.5 - 466.5] + ├─ [466.5 - 468.5] + ├─ [468.5 - 470.5] + ├─ [470.5 - 472.5] + ├─ [472.5 - 474.5] + ├─ [474.5 - 476.5] + ├─ [476.5 - 478.5] + ├─ [478.5 - 480.5] + ├─ [480.5 - 482.5] + ├─ [482.5 - 484.5] + ├─ [484.5 - 486.5] + ├─ [486.5 - 488.5] + ├─ [488.5 - 490.5] + ├─ [490.5 - 492.5] + ├─ [492.5 - 494.5] + ├─ [494.5 - 496.5] + ├─ [496.5 - 498.5] + ├─ [498.5 - 500.5] + ├─ [500.5 - 502.5] + ├─ [502.5 - 504.5] + ├─ [504.5 - 506.5] + ├─ [506.5 - 508.5] + ├─ [508.5 - 510.5] + ├─ [510.5 - 512.5] + ├─ [512.5 - 514.5] + ├─ [514.5 - 516.5] + ├─ [516.5 - 518.5] + ├─ [518.5 - 520.5] + ├─ [520.5 - 522.5] + ├─ [522.5 - 524.5] + ├─ [524.5 - 526.5] + ├─ [526.5 - 528.5] + ├─ [528.5 - 530.5] + ├─ [530.5 - 532.5] + ├─ [532.5 - 534.5] + ├─ [534.5 - 536.5] + ├─ [536.5 - 538.5] + ├─ [538.5 - 540.5] + ├─ [540.5 - 542.5] + ├─ [542.5 - 544.5] + ├─ [544.5 - 546.5] + ├─ [546.5 - 548.5] + ├─ [548.5 - 550.5] + ├─ [550.5 - 552.5] + ├─ [552.5 - 554.5] + ├─ [554.5 - 556.5] + ├─ [556.5 - 558.5] + ├─ [558.5 - 560.5] + ├─ [560.5 - 562.5] + ├─ [562.5 - 564.5] + ├─ [564.5 - 566.5] + ├─ [566.5 - 568.5] + ├─ [568.5 - 570.5] + ├─ [570.5 - 572.5] + ├─ [572.5 - 574.5] + ├─ [574.5 - 576.5] + ├─ [576.5 - 578.5] + ├─ [578.5 - 580.5] + ├─ [580.5 - 582.5] + ├─ [582.5 - 584.5] + ├─ [584.5 - 586.5] + ├─ [586.5 - 588.5] + ├─ [588.5 - 590.5] + ├─ [590.5 - 592.5] + ├─ [592.5 - 594.5] + ├─ [594.5 - 596.5] + ├─ [596.5 - 598.5] + ├─ [598.5 - 600.5] + ├─ [600.5 - 602.5] + ├─ [602.5 - 604.5] + ├─ [604.5 - 606.5] + ├─ [606.5 - 608.5] + ├─ [608.5 - 610.5] + ├─ [610.5 - 612.5] + ├─ [612.5 - 614.5] + ├─ [614.5 - 616.5] + ├─ [616.5 - 618.5] + ├─ [618.5 - 620.5] + ├─ [620.5 - 622.5] + ├─ [622.5 - 624.5] + ├─ [624.5 - 626.5] + ├─ [626.5 - 628.5] + ├─ [628.5 - 630.5] + ├─ [630.5 - 632.5] + ├─ [632.5 - 634.5] + ├─ [634.5 - 636.5] + ├─ [636.5 - 638.5] + ├─ [638.5 - 640.5] + ├─ [640.5 - 642.5] + ├─ [642.5 - 644.5] + ├─ [644.5 - 646.5] + ├─ [646.5 - 648.5] + ├─ [648.5 - 650.5] + ├─ [650.5 - 652.5] + ├─ [652.5 - 654.5] + ├─ [654.5 - 656.5] + ├─ [656.5 - 658.5] + ├─ [658.5 - 660.6] + ├─ [660.6 - 662.6] + ├─ [662.6 - 664.6] + ├─ [664.6 - 666.6] + ├─ [666.6 - 668.6] + ├─ [668.6 - 670.6] + ├─ [670.6 - 672.6] + ├─ [672.6 - 674.6] + ├─ [674.6 - 676.6] + ├─ [676.6 - 678.6] + ├─ [678.6 - 680.6] + ├─ [680.6 - 682.6] + ├─ [682.6 - 684.6] + ├─ [684.6 - 686.6] + ├─ [686.6 - 688.6] + ├─ [688.6 - 690.6] + ├─ [690.6 - 692.6] + ├─ [692.6 - 694.6] + ├─ [694.6 - 696.6] + ├─ [696.6 - 698.6] + ├─ [698.6 - 700.6] + ├─ [700.6 - 702.6] + ├─ [702.6 - 704.6] + ├─ [704.6 - 706.6] + ├─ [706.6 - 708.6] + ├─ [708.6 - 710.6] + ├─ [710.6 - 712.6] + ├─ [712.6 - 714.6] + ├─ [714.6 - 716.6] + ├─ [716.6 - 718.6] + ├─ [718.6 - 720.6] + ├─ [720.6 - 722.6] + ├─ [722.6 - 724.6] + ├─ [724.6 - 726.6] + ├─ [726.6 - 728.6] + ├─ [728.6 - 730.6] + ├─ [730.6 - 732.6] + ├─ [732.6 - 734.6] + ├─ [734.6 - 736.6] + ├─ [736.6 - 738.6] + ├─ [738.6 - 740.6] + ├─ [740.6 - 742.6] + ├─ [742.6 - 744.6] + ├─ [744.6 - 746.6] + ├─ [746.6 - 748.6] + ├─ [748.6 - 750.6] + ├─ [750.6 - 752.6] + ├─ [752.6 - 754.6] + ├─ [754.6 - 756.6] + ├─ [756.6 - 758.6] + ├─ [758.6 - 760.6] + ├─ [760.6 - 762.6] + ├─ [762.6 - 764.6] + ├─ [764.6 - 766.6] + ├─ [766.6 - 768.6] + ├─ [768.6 - 770.6] + ├─ [770.6 - 772.6] + ├─ [772.6 - 774.6] + ├─ [774.6 - 776.6] + ├─ [776.6 - 778.6] + ├─ [778.6 - 780.6] + ├─ [780.6 - 782.6] + ├─ [782.6 - 784.6] + ├─ [784.6 - 786.6] + ├─ [786.6 - 788.6] + ├─ [788.6 - 790.6] + ├─ [790.6 - 792.6] + ├─ [792.6 - 794.6] + ├─ [794.6 - 796.6] + ├─ [796.6 - 798.6] + ├─ [798.6 - 800.6] + ├─ [800.6 - 802.6] + ├─ [802.6 - 804.6] + ├─ [804.6 - 806.6] + ├─ [806.6 - 808.6] + ├─ [808.6 - 810.6] + ├─ [810.6 - 812.6] + ├─ [812.6 - 814.6] + ├─ [814.6 - 816.6] + ├─ [816.6 - 818.6] + ├─ [818.6 - 820.6] + ├─ [820.6 - 822.6] + ├─ [822.6 - 824.6] + ├─ [824.6 - 826.6] + ├─ [826.6 - 828.6] + ├─ [828.6 - 830.6] + ├─ [830.6 - 832.6] + ├─ [832.6 - 834.6] + ├─ [834.6 - 836.6] + ├─ [836.6 - 838.6] + ├─ [838.6 - 840.6] + ├─ [840.6 - 842.6] + ├─ [842.6 - 844.6] + ├─ [844.6 - 846.6] + ├─ [846.6 - 848.6] + ├─ [848.6 - 850.6] + ├─ [850.6 - 852.6] + ├─ [852.6 - 854.6] + ├─ [854.6 - 856.6] + ├─ [856.6 - 858.6] + ├─ [858.6 - 860.6] + ├─ [860.6 - 862.6] + ├─ [862.6 - 864.6] + ├─ [864.6 - 866.6] + ├─ [866.6 - 868.6] + ├─ [868.6 - 870.6] + ├─ [870.6 - 872.6] + ├─ [872.6 - 874.6] + ├─ [874.6 - 876.6] + ├─ [876.6 - 878.6] + ├─ [878.6 - 880.7] + ├─ [880.7 - 882.7] + ├─ [882.7 - 884.7] + ├─ [884.7 - 886.7] + ├─ [886.7 - 888.7] + ├─ [888.7 - 890.7] + ├─ [890.7 - 892.7] + ├─ [892.7 - 894.7] + ├─ [894.7 - 896.7] + ├─ [896.7 - 898.7] + ├─ [898.7 - 900.7] + ├─ [900.7 - 902.7] + ├─ [902.7 - 904.7] + ├─ [904.7 - 906.7] + ├─ [906.7 - 908.7] + ├─ [908.7 - 910.7] + ├─ [910.7 - 912.7] + ├─ [912.7 - 914.7] + ├─ [914.7 - 916.7] + ├─ [916.7 - 918.7] + ├─ [918.7 - 920.7] + ├─ [920.7 - 922.7] + ├─ [922.7 - 924.7] + ├─ [924.7 - 926.7] + ├─ [926.7 - 928.7] + ├─ [928.7 - 930.7] + ├─ [930.7 - 932.7] + ├─ [932.7 - 934.7] + ├─ [934.7 - 936.7] + ├─ [936.7 - 938.7] + ├─ [938.7 - 940.7] + ├─ [940.7 - 942.7] + ├─ [942.7 - 944.7] + ├─ [944.7 - 946.7] + ├─ [946.7 - 948.7] + ├─ [948.7 - 950.7] + ├─ [950.7 - 952.7] + ├─ [952.7 - 954.7] + ├─ [954.7 - 956.7] + ├─ [956.7 - 958.7] + ├─ [958.7 - 960.7] + ├─ [960.7 - 962.7] + ├─ [962.7 - 964.7] + ├─ [964.7 - 966.7] + ├─ [966.7 - 968.7] + ├─ [968.7 - 970.7] + ├─ [970.7 - 972.7] + ├─ [972.7 - 974.7] + ├─ [974.7 - 976.7] + ├─ [976.7 - 978.7] + └─ [978.7 - 980.7] + +Run: LFQ_24min_500ng_2Th_3ms_B_01.raw + ├─ Vendor: Thermo + ├─ File: LFQ_24min_500ng_2Th_3ms_B_01 + ├─ Path: "LFQ_24min_500ng_2Th_3ms_B_01.raw" + ├─ Condition: Mix_B + ├─ Replicate: 1 + ├─ HTRMS Version: 20.4.260109.92449 + ├─ Protein Databases Used + │ ├─ ECOLI__250312 + │ │ ├─ Protein Entries: 4’273 + │ │ ├─ Original File: ECOLI__250312.fasta + │ │ ├─ Date Created: 30-October-2025 16:07:09 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ ├─ HUMAN__230321 + │ │ ├─ Protein Entries: 20’591 + │ │ ├─ Original File: HUMAN__230321.fasta + │ │ ├─ Date Created: 04-March-2025 12:31:56 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ └─ YEAST__230321 + │ ├─ Protein Entries: 6’060 + │ ├─ Original File: YEAST__230321.fasta + │ ├─ Date Created: 28-January-2026 14:06:11 UTC +01:00 + │ ├─ Date Modified: 17-February-2026 17:25:50 UTC +01:00 + │ └─ Parsing Rule: UniProt FASTA + └─ MS2 Method + ├─ [380.4 - 382.4] + ├─ [382.4 - 384.4] + ├─ [384.4 - 386.4] + ├─ [386.4 - 388.4] + ├─ [388.4 - 390.4] + ├─ [390.4 - 392.4] + ├─ [392.4 - 394.4] + ├─ [394.4 - 396.4] + ├─ [396.4 - 398.4] + ├─ [398.4 - 400.4] + ├─ [400.4 - 402.4] + ├─ [402.4 - 404.4] + ├─ [404.4 - 406.4] + ├─ [406.4 - 408.4] + ├─ [408.4 - 410.4] + ├─ [410.4 - 412.4] + ├─ [412.4 - 414.4] + ├─ [414.4 - 416.4] + ├─ [416.4 - 418.4] + ├─ [418.4 - 420.4] + ├─ [420.4 - 422.4] + ├─ [422.4 - 424.4] + ├─ [424.4 - 426.4] + ├─ [426.4 - 428.4] + ├─ [428.4 - 430.4] + ├─ [430.4 - 432.4] + ├─ [432.4 - 434.4] + ├─ [434.4 - 436.4] + ├─ [436.4 - 438.4] + ├─ [438.4 - 440.5] + ├─ [440.5 - 442.5] + ├─ [442.5 - 444.5] + ├─ [444.5 - 446.5] + ├─ [446.5 - 448.5] + ├─ [448.5 - 450.5] + ├─ [450.5 - 452.5] + ├─ [452.5 - 454.5] + ├─ [454.5 - 456.5] + ├─ [456.5 - 458.5] + ├─ [458.5 - 460.5] + ├─ [460.5 - 462.5] + ├─ [462.5 - 464.5] + ├─ [464.5 - 466.5] + ├─ [466.5 - 468.5] + ├─ [468.5 - 470.5] + ├─ [470.5 - 472.5] + ├─ [472.5 - 474.5] + ├─ [474.5 - 476.5] + ├─ [476.5 - 478.5] + ├─ [478.5 - 480.5] + ├─ [480.5 - 482.5] + ├─ [482.5 - 484.5] + ├─ [484.5 - 486.5] + ├─ [486.5 - 488.5] + ├─ [488.5 - 490.5] + ├─ [490.5 - 492.5] + ├─ [492.5 - 494.5] + ├─ [494.5 - 496.5] + ├─ [496.5 - 498.5] + ├─ [498.5 - 500.5] + ├─ [500.5 - 502.5] + ├─ [502.5 - 504.5] + ├─ [504.5 - 506.5] + ├─ [506.5 - 508.5] + ├─ [508.5 - 510.5] + ├─ [510.5 - 512.5] + ├─ [512.5 - 514.5] + ├─ [514.5 - 516.5] + ├─ [516.5 - 518.5] + ├─ [518.5 - 520.5] + ├─ [520.5 - 522.5] + ├─ [522.5 - 524.5] + ├─ [524.5 - 526.5] + ├─ [526.5 - 528.5] + ├─ [528.5 - 530.5] + ├─ [530.5 - 532.5] + ├─ [532.5 - 534.5] + ├─ [534.5 - 536.5] + ├─ [536.5 - 538.5] + ├─ [538.5 - 540.5] + ├─ [540.5 - 542.5] + ├─ [542.5 - 544.5] + ├─ [544.5 - 546.5] + ├─ [546.5 - 548.5] + ├─ [548.5 - 550.5] + ├─ [550.5 - 552.5] + ├─ [552.5 - 554.5] + ├─ [554.5 - 556.5] + ├─ [556.5 - 558.5] + ├─ [558.5 - 560.5] + ├─ [560.5 - 562.5] + ├─ [562.5 - 564.5] + ├─ [564.5 - 566.5] + ├─ [566.5 - 568.5] + ├─ [568.5 - 570.5] + ├─ [570.5 - 572.5] + ├─ [572.5 - 574.5] + ├─ [574.5 - 576.5] + ├─ [576.5 - 578.5] + ├─ [578.5 - 580.5] + ├─ [580.5 - 582.5] + ├─ [582.5 - 584.5] + ├─ [584.5 - 586.5] + ├─ [586.5 - 588.5] + ├─ [588.5 - 590.5] + ├─ [590.5 - 592.5] + ├─ [592.5 - 594.5] + ├─ [594.5 - 596.5] + ├─ [596.5 - 598.5] + ├─ [598.5 - 600.5] + ├─ [600.5 - 602.5] + ├─ [602.5 - 604.5] + ├─ [604.5 - 606.5] + ├─ [606.5 - 608.5] + ├─ [608.5 - 610.5] + ├─ [610.5 - 612.5] + ├─ [612.5 - 614.5] + ├─ [614.5 - 616.5] + ├─ [616.5 - 618.5] + ├─ [618.5 - 620.5] + ├─ [620.5 - 622.5] + ├─ [622.5 - 624.5] + ├─ [624.5 - 626.5] + ├─ [626.5 - 628.5] + ├─ [628.5 - 630.5] + ├─ [630.5 - 632.5] + ├─ [632.5 - 634.5] + ├─ [634.5 - 636.5] + ├─ [636.5 - 638.5] + ├─ [638.5 - 640.5] + ├─ [640.5 - 642.5] + ├─ [642.5 - 644.5] + ├─ [644.5 - 646.5] + ├─ [646.5 - 648.5] + ├─ [648.5 - 650.5] + ├─ [650.5 - 652.5] + ├─ [652.5 - 654.5] + ├─ [654.5 - 656.5] + ├─ [656.5 - 658.5] + ├─ [658.5 - 660.6] + ├─ [660.6 - 662.6] + ├─ [662.6 - 664.6] + ├─ [664.6 - 666.6] + ├─ [666.6 - 668.6] + ├─ [668.6 - 670.6] + ├─ [670.6 - 672.6] + ├─ [672.6 - 674.6] + ├─ [674.6 - 676.6] + ├─ [676.6 - 678.6] + ├─ [678.6 - 680.6] + ├─ [680.6 - 682.6] + ├─ [682.6 - 684.6] + ├─ [684.6 - 686.6] + ├─ [686.6 - 688.6] + ├─ [688.6 - 690.6] + ├─ [690.6 - 692.6] + ├─ [692.6 - 694.6] + ├─ [694.6 - 696.6] + ├─ [696.6 - 698.6] + ├─ [698.6 - 700.6] + ├─ [700.6 - 702.6] + ├─ [702.6 - 704.6] + ├─ [704.6 - 706.6] + ├─ [706.6 - 708.6] + ├─ [708.6 - 710.6] + ├─ [710.6 - 712.6] + ├─ [712.6 - 714.6] + ├─ [714.6 - 716.6] + ├─ [716.6 - 718.6] + ├─ [718.6 - 720.6] + ├─ [720.6 - 722.6] + ├─ [722.6 - 724.6] + ├─ [724.6 - 726.6] + ├─ [726.6 - 728.6] + ├─ [728.6 - 730.6] + ├─ [730.6 - 732.6] + ├─ [732.6 - 734.6] + ├─ [734.6 - 736.6] + ├─ [736.6 - 738.6] + ├─ [738.6 - 740.6] + ├─ [740.6 - 742.6] + ├─ [742.6 - 744.6] + ├─ [744.6 - 746.6] + ├─ [746.6 - 748.6] + ├─ [748.6 - 750.6] + ├─ [750.6 - 752.6] + ├─ [752.6 - 754.6] + ├─ [754.6 - 756.6] + ├─ [756.6 - 758.6] + ├─ [758.6 - 760.6] + ├─ [760.6 - 762.6] + ├─ [762.6 - 764.6] + ├─ [764.6 - 766.6] + ├─ [766.6 - 768.6] + ├─ [768.6 - 770.6] + ├─ [770.6 - 772.6] + ├─ [772.6 - 774.6] + ├─ [774.6 - 776.6] + ├─ [776.6 - 778.6] + ├─ [778.6 - 780.6] + ├─ [780.6 - 782.6] + ├─ [782.6 - 784.6] + ├─ [784.6 - 786.6] + ├─ [786.6 - 788.6] + ├─ [788.6 - 790.6] + ├─ [790.6 - 792.6] + ├─ [792.6 - 794.6] + ├─ [794.6 - 796.6] + ├─ [796.6 - 798.6] + ├─ [798.6 - 800.6] + ├─ [800.6 - 802.6] + ├─ [802.6 - 804.6] + ├─ [804.6 - 806.6] + ├─ [806.6 - 808.6] + ├─ [808.6 - 810.6] + ├─ [810.6 - 812.6] + ├─ [812.6 - 814.6] + ├─ [814.6 - 816.6] + ├─ [816.6 - 818.6] + ├─ [818.6 - 820.6] + ├─ [820.6 - 822.6] + ├─ [822.6 - 824.6] + ├─ [824.6 - 826.6] + ├─ [826.6 - 828.6] + ├─ [828.6 - 830.6] + ├─ [830.6 - 832.6] + ├─ [832.6 - 834.6] + ├─ [834.6 - 836.6] + ├─ [836.6 - 838.6] + ├─ [838.6 - 840.6] + ├─ [840.6 - 842.6] + ├─ [842.6 - 844.6] + ├─ [844.6 - 846.6] + ├─ [846.6 - 848.6] + ├─ [848.6 - 850.6] + ├─ [850.6 - 852.6] + ├─ [852.6 - 854.6] + ├─ [854.6 - 856.6] + ├─ [856.6 - 858.6] + ├─ [858.6 - 860.6] + ├─ [860.6 - 862.6] + ├─ [862.6 - 864.6] + ├─ [864.6 - 866.6] + ├─ [866.6 - 868.6] + ├─ [868.6 - 870.6] + ├─ [870.6 - 872.6] + ├─ [872.6 - 874.6] + ├─ [874.6 - 876.6] + ├─ [876.6 - 878.6] + ├─ [878.6 - 880.7] + ├─ [880.7 - 882.7] + ├─ [882.7 - 884.7] + ├─ [884.7 - 886.7] + ├─ [886.7 - 888.7] + ├─ [888.7 - 890.7] + ├─ [890.7 - 892.7] + ├─ [892.7 - 894.7] + ├─ [894.7 - 896.7] + ├─ [896.7 - 898.7] + ├─ [898.7 - 900.7] + ├─ [900.7 - 902.7] + ├─ [902.7 - 904.7] + ├─ [904.7 - 906.7] + ├─ [906.7 - 908.7] + ├─ [908.7 - 910.7] + ├─ [910.7 - 912.7] + ├─ [912.7 - 914.7] + ├─ [914.7 - 916.7] + ├─ [916.7 - 918.7] + ├─ [918.7 - 920.7] + ├─ [920.7 - 922.7] + ├─ [922.7 - 924.7] + ├─ [924.7 - 926.7] + ├─ [926.7 - 928.7] + ├─ [928.7 - 930.7] + ├─ [930.7 - 932.7] + ├─ [932.7 - 934.7] + ├─ [934.7 - 936.7] + ├─ [936.7 - 938.7] + ├─ [938.7 - 940.7] + ├─ [940.7 - 942.7] + ├─ [942.7 - 944.7] + ├─ [944.7 - 946.7] + ├─ [946.7 - 948.7] + ├─ [948.7 - 950.7] + ├─ [950.7 - 952.7] + ├─ [952.7 - 954.7] + ├─ [954.7 - 956.7] + ├─ [956.7 - 958.7] + ├─ [958.7 - 960.7] + ├─ [960.7 - 962.7] + ├─ [962.7 - 964.7] + ├─ [964.7 - 966.7] + ├─ [966.7 - 968.7] + ├─ [968.7 - 970.7] + ├─ [970.7 - 972.7] + ├─ [972.7 - 974.7] + ├─ [974.7 - 976.7] + ├─ [976.7 - 978.7] + └─ [978.7 - 980.7] + +Run: LFQ_24min_500ng_2Th_3ms_B_02.raw + ├─ Vendor: Thermo + ├─ File: LFQ_24min_500ng_2Th_3ms_B_02 + ├─ Path: "LFQ_24min_500ng_2Th_3ms_B_02.raw" + ├─ Condition: Mix_B + ├─ Replicate: 2 + ├─ HTRMS Version: 20.4.260109.92449 + ├─ Protein Databases Used + │ ├─ ECOLI__250312 + │ │ ├─ Protein Entries: 4’273 + │ │ ├─ Original File: ECOLI__250312.fasta + │ │ ├─ Date Created: 30-October-2025 16:07:09 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ ├─ HUMAN__230321 + │ │ ├─ Protein Entries: 20’591 + │ │ ├─ Original File: HUMAN__230321.fasta + │ │ ├─ Date Created: 04-March-2025 12:31:56 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ └─ YEAST__230321 + │ ├─ Protein Entries: 6’060 + │ ├─ Original File: YEAST__230321.fasta + │ ├─ Date Created: 28-January-2026 14:06:11 UTC +01:00 + │ ├─ Date Modified: 17-February-2026 17:25:50 UTC +01:00 + │ └─ Parsing Rule: UniProt FASTA + └─ MS2 Method + ├─ [380.4 - 382.4] + ├─ [382.4 - 384.4] + ├─ [384.4 - 386.4] + ├─ [386.4 - 388.4] + ├─ [388.4 - 390.4] + ├─ [390.4 - 392.4] + ├─ [392.4 - 394.4] + ├─ [394.4 - 396.4] + ├─ [396.4 - 398.4] + ├─ [398.4 - 400.4] + ├─ [400.4 - 402.4] + ├─ [402.4 - 404.4] + ├─ [404.4 - 406.4] + ├─ [406.4 - 408.4] + ├─ [408.4 - 410.4] + ├─ [410.4 - 412.4] + ├─ [412.4 - 414.4] + ├─ [414.4 - 416.4] + ├─ [416.4 - 418.4] + ├─ [418.4 - 420.4] + ├─ [420.4 - 422.4] + ├─ [422.4 - 424.4] + ├─ [424.4 - 426.4] + ├─ [426.4 - 428.4] + ├─ [428.4 - 430.4] + ├─ [430.4 - 432.4] + ├─ [432.4 - 434.4] + ├─ [434.4 - 436.4] + ├─ [436.4 - 438.4] + ├─ [438.4 - 440.5] + ├─ [440.5 - 442.5] + ├─ [442.5 - 444.5] + ├─ [444.5 - 446.5] + ├─ [446.5 - 448.5] + ├─ [448.5 - 450.5] + ├─ [450.5 - 452.5] + ├─ [452.5 - 454.5] + ├─ [454.5 - 456.5] + ├─ [456.5 - 458.5] + ├─ [458.5 - 460.5] + ├─ [460.5 - 462.5] + ├─ [462.5 - 464.5] + ├─ [464.5 - 466.5] + ├─ [466.5 - 468.5] + ├─ [468.5 - 470.5] + ├─ [470.5 - 472.5] + ├─ [472.5 - 474.5] + ├─ [474.5 - 476.5] + ├─ [476.5 - 478.5] + ├─ [478.5 - 480.5] + ├─ [480.5 - 482.5] + ├─ [482.5 - 484.5] + ├─ [484.5 - 486.5] + ├─ [486.5 - 488.5] + ├─ [488.5 - 490.5] + ├─ [490.5 - 492.5] + ├─ [492.5 - 494.5] + ├─ [494.5 - 496.5] + ├─ [496.5 - 498.5] + ├─ [498.5 - 500.5] + ├─ [500.5 - 502.5] + ├─ [502.5 - 504.5] + ├─ [504.5 - 506.5] + ├─ [506.5 - 508.5] + ├─ [508.5 - 510.5] + ├─ [510.5 - 512.5] + ├─ [512.5 - 514.5] + ├─ [514.5 - 516.5] + ├─ [516.5 - 518.5] + ├─ [518.5 - 520.5] + ├─ [520.5 - 522.5] + ├─ [522.5 - 524.5] + ├─ [524.5 - 526.5] + ├─ [526.5 - 528.5] + ├─ [528.5 - 530.5] + ├─ [530.5 - 532.5] + ├─ [532.5 - 534.5] + ├─ [534.5 - 536.5] + ├─ [536.5 - 538.5] + ├─ [538.5 - 540.5] + ├─ [540.5 - 542.5] + ├─ [542.5 - 544.5] + ├─ [544.5 - 546.5] + ├─ [546.5 - 548.5] + ├─ [548.5 - 550.5] + ├─ [550.5 - 552.5] + ├─ [552.5 - 554.5] + ├─ [554.5 - 556.5] + ├─ [556.5 - 558.5] + ├─ [558.5 - 560.5] + ├─ [560.5 - 562.5] + ├─ [562.5 - 564.5] + ├─ [564.5 - 566.5] + ├─ [566.5 - 568.5] + ├─ [568.5 - 570.5] + ├─ [570.5 - 572.5] + ├─ [572.5 - 574.5] + ├─ [574.5 - 576.5] + ├─ [576.5 - 578.5] + ├─ [578.5 - 580.5] + ├─ [580.5 - 582.5] + ├─ [582.5 - 584.5] + ├─ [584.5 - 586.5] + ├─ [586.5 - 588.5] + ├─ [588.5 - 590.5] + ├─ [590.5 - 592.5] + ├─ [592.5 - 594.5] + ├─ [594.5 - 596.5] + ├─ [596.5 - 598.5] + ├─ [598.5 - 600.5] + ├─ [600.5 - 602.5] + ├─ [602.5 - 604.5] + ├─ [604.5 - 606.5] + ├─ [606.5 - 608.5] + ├─ [608.5 - 610.5] + ├─ [610.5 - 612.5] + ├─ [612.5 - 614.5] + ├─ [614.5 - 616.5] + ├─ [616.5 - 618.5] + ├─ [618.5 - 620.5] + ├─ [620.5 - 622.5] + ├─ [622.5 - 624.5] + ├─ [624.5 - 626.5] + ├─ [626.5 - 628.5] + ├─ [628.5 - 630.5] + ├─ [630.5 - 632.5] + ├─ [632.5 - 634.5] + ├─ [634.5 - 636.5] + ├─ [636.5 - 638.5] + ├─ [638.5 - 640.5] + ├─ [640.5 - 642.5] + ├─ [642.5 - 644.5] + ├─ [644.5 - 646.5] + ├─ [646.5 - 648.5] + ├─ [648.5 - 650.5] + ├─ [650.5 - 652.5] + ├─ [652.5 - 654.5] + ├─ [654.5 - 656.5] + ├─ [656.5 - 658.5] + ├─ [658.5 - 660.6] + ├─ [660.6 - 662.6] + ├─ [662.6 - 664.6] + ├─ [664.6 - 666.6] + ├─ [666.6 - 668.6] + ├─ [668.6 - 670.6] + ├─ [670.6 - 672.6] + ├─ [672.6 - 674.6] + ├─ [674.6 - 676.6] + ├─ [676.6 - 678.6] + ├─ [678.6 - 680.6] + ├─ [680.6 - 682.6] + ├─ [682.6 - 684.6] + ├─ [684.6 - 686.6] + ├─ [686.6 - 688.6] + ├─ [688.6 - 690.6] + ├─ [690.6 - 692.6] + ├─ [692.6 - 694.6] + ├─ [694.6 - 696.6] + ├─ [696.6 - 698.6] + ├─ [698.6 - 700.6] + ├─ [700.6 - 702.6] + ├─ [702.6 - 704.6] + ├─ [704.6 - 706.6] + ├─ [706.6 - 708.6] + ├─ [708.6 - 710.6] + ├─ [710.6 - 712.6] + ├─ [712.6 - 714.6] + ├─ [714.6 - 716.6] + ├─ [716.6 - 718.6] + ├─ [718.6 - 720.6] + ├─ [720.6 - 722.6] + ├─ [722.6 - 724.6] + ├─ [724.6 - 726.6] + ├─ [726.6 - 728.6] + ├─ [728.6 - 730.6] + ├─ [730.6 - 732.6] + ├─ [732.6 - 734.6] + ├─ [734.6 - 736.6] + ├─ [736.6 - 738.6] + ├─ [738.6 - 740.6] + ├─ [740.6 - 742.6] + ├─ [742.6 - 744.6] + ├─ [744.6 - 746.6] + ├─ [746.6 - 748.6] + ├─ [748.6 - 750.6] + ├─ [750.6 - 752.6] + ├─ [752.6 - 754.6] + ├─ [754.6 - 756.6] + ├─ [756.6 - 758.6] + ├─ [758.6 - 760.6] + ├─ [760.6 - 762.6] + ├─ [762.6 - 764.6] + ├─ [764.6 - 766.6] + ├─ [766.6 - 768.6] + ├─ [768.6 - 770.6] + ├─ [770.6 - 772.6] + ├─ [772.6 - 774.6] + ├─ [774.6 - 776.6] + ├─ [776.6 - 778.6] + ├─ [778.6 - 780.6] + ├─ [780.6 - 782.6] + ├─ [782.6 - 784.6] + ├─ [784.6 - 786.6] + ├─ [786.6 - 788.6] + ├─ [788.6 - 790.6] + ├─ [790.6 - 792.6] + ├─ [792.6 - 794.6] + ├─ [794.6 - 796.6] + ├─ [796.6 - 798.6] + ├─ [798.6 - 800.6] + ├─ [800.6 - 802.6] + ├─ [802.6 - 804.6] + ├─ [804.6 - 806.6] + ├─ [806.6 - 808.6] + ├─ [808.6 - 810.6] + ├─ [810.6 - 812.6] + ├─ [812.6 - 814.6] + ├─ [814.6 - 816.6] + ├─ [816.6 - 818.6] + ├─ [818.6 - 820.6] + ├─ [820.6 - 822.6] + ├─ [822.6 - 824.6] + ├─ [824.6 - 826.6] + ├─ [826.6 - 828.6] + ├─ [828.6 - 830.6] + ├─ [830.6 - 832.6] + ├─ [832.6 - 834.6] + ├─ [834.6 - 836.6] + ├─ [836.6 - 838.6] + ├─ [838.6 - 840.6] + ├─ [840.6 - 842.6] + ├─ [842.6 - 844.6] + ├─ [844.6 - 846.6] + ├─ [846.6 - 848.6] + ├─ [848.6 - 850.6] + ├─ [850.6 - 852.6] + ├─ [852.6 - 854.6] + ├─ [854.6 - 856.6] + ├─ [856.6 - 858.6] + ├─ [858.6 - 860.6] + ├─ [860.6 - 862.6] + ├─ [862.6 - 864.6] + ├─ [864.6 - 866.6] + ├─ [866.6 - 868.6] + ├─ [868.6 - 870.6] + ├─ [870.6 - 872.6] + ├─ [872.6 - 874.6] + ├─ [874.6 - 876.6] + ├─ [876.6 - 878.6] + ├─ [878.6 - 880.7] + ├─ [880.7 - 882.7] + ├─ [882.7 - 884.7] + ├─ [884.7 - 886.7] + ├─ [886.7 - 888.7] + ├─ [888.7 - 890.7] + ├─ [890.7 - 892.7] + ├─ [892.7 - 894.7] + ├─ [894.7 - 896.7] + ├─ [896.7 - 898.7] + ├─ [898.7 - 900.7] + ├─ [900.7 - 902.7] + ├─ [902.7 - 904.7] + ├─ [904.7 - 906.7] + ├─ [906.7 - 908.7] + ├─ [908.7 - 910.7] + ├─ [910.7 - 912.7] + ├─ [912.7 - 914.7] + ├─ [914.7 - 916.7] + ├─ [916.7 - 918.7] + ├─ [918.7 - 920.7] + ├─ [920.7 - 922.7] + ├─ [922.7 - 924.7] + ├─ [924.7 - 926.7] + ├─ [926.7 - 928.7] + ├─ [928.7 - 930.7] + ├─ [930.7 - 932.7] + ├─ [932.7 - 934.7] + ├─ [934.7 - 936.7] + ├─ [936.7 - 938.7] + ├─ [938.7 - 940.7] + ├─ [940.7 - 942.7] + ├─ [942.7 - 944.7] + ├─ [944.7 - 946.7] + ├─ [946.7 - 948.7] + ├─ [948.7 - 950.7] + ├─ [950.7 - 952.7] + ├─ [952.7 - 954.7] + ├─ [954.7 - 956.7] + ├─ [956.7 - 958.7] + ├─ [958.7 - 960.7] + ├─ [960.7 - 962.7] + ├─ [962.7 - 964.7] + ├─ [964.7 - 966.7] + ├─ [966.7 - 968.7] + ├─ [968.7 - 970.7] + ├─ [970.7 - 972.7] + ├─ [972.7 - 974.7] + ├─ [974.7 - 976.7] + ├─ [976.7 - 978.7] + └─ [978.7 - 980.7] + +Run: LFQ_24min_500ng_2Th_3ms_B_03.raw + ├─ Vendor: Thermo + ├─ File: LFQ_24min_500ng_2Th_3ms_B_03 + ├─ Path: "LFQ_24min_500ng_2Th_3ms_B_03.raw" + ├─ Condition: Mix_B + ├─ Replicate: 3 + ├─ HTRMS Version: 20.4.260109.92449 + ├─ Protein Databases Used + │ ├─ ECOLI__250312 + │ │ ├─ Protein Entries: 4’273 + │ │ ├─ Original File: ECOLI__250312.fasta + │ │ ├─ Date Created: 30-October-2025 16:07:09 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ ├─ HUMAN__230321 + │ │ ├─ Protein Entries: 20’591 + │ │ ├─ Original File: HUMAN__230321.fasta + │ │ ├─ Date Created: 04-March-2025 12:31:56 UTC +01:00 + │ │ ├─ Date Modified: 17-February-2026 17:25:48 UTC +01:00 + │ │ └─ Parsing Rule: UniProt FASTA + │ └─ YEAST__230321 + │ ├─ Protein Entries: 6’060 + │ ├─ Original File: YEAST__230321.fasta + │ ├─ Date Created: 28-January-2026 14:06:11 UTC +01:00 + │ ├─ Date Modified: 17-February-2026 17:25:50 UTC +01:00 + │ └─ Parsing Rule: UniProt FASTA + └─ MS2 Method + ├─ [380.4 - 382.4] + ├─ [382.4 - 384.4] + ├─ [384.4 - 386.4] + ├─ [386.4 - 388.4] + ├─ [388.4 - 390.4] + ├─ [390.4 - 392.4] + ├─ [392.4 - 394.4] + ├─ [394.4 - 396.4] + ├─ [396.4 - 398.4] + ├─ [398.4 - 400.4] + ├─ [400.4 - 402.4] + ├─ [402.4 - 404.4] + ├─ [404.4 - 406.4] + ├─ [406.4 - 408.4] + ├─ [408.4 - 410.4] + ├─ [410.4 - 412.4] + ├─ [412.4 - 414.4] + ├─ [414.4 - 416.4] + ├─ [416.4 - 418.4] + ├─ [418.4 - 420.4] + ├─ [420.4 - 422.4] + ├─ [422.4 - 424.4] + ├─ [424.4 - 426.4] + ├─ [426.4 - 428.4] + ├─ [428.4 - 430.4] + ├─ [430.4 - 432.4] + ├─ [432.4 - 434.4] + ├─ [434.4 - 436.4] + ├─ [436.4 - 438.4] + ├─ [438.4 - 440.5] + ├─ [440.5 - 442.5] + ├─ [442.5 - 444.5] + ├─ [444.5 - 446.5] + ├─ [446.5 - 448.5] + ├─ [448.5 - 450.5] + ├─ [450.5 - 452.5] + ├─ [452.5 - 454.5] + ├─ [454.5 - 456.5] + ├─ [456.5 - 458.5] + ├─ [458.5 - 460.5] + ├─ [460.5 - 462.5] + ├─ [462.5 - 464.5] + ├─ [464.5 - 466.5] + ├─ [466.5 - 468.5] + ├─ [468.5 - 470.5] + ├─ [470.5 - 472.5] + ├─ [472.5 - 474.5] + ├─ [474.5 - 476.5] + ├─ [476.5 - 478.5] + ├─ [478.5 - 480.5] + ├─ [480.5 - 482.5] + ├─ [482.5 - 484.5] + ├─ [484.5 - 486.5] + ├─ [486.5 - 488.5] + ├─ [488.5 - 490.5] + ├─ [490.5 - 492.5] + ├─ [492.5 - 494.5] + ├─ [494.5 - 496.5] + ├─ [496.5 - 498.5] + ├─ [498.5 - 500.5] + ├─ [500.5 - 502.5] + ├─ [502.5 - 504.5] + ├─ [504.5 - 506.5] + ├─ [506.5 - 508.5] + ├─ [508.5 - 510.5] + ├─ [510.5 - 512.5] + ├─ [512.5 - 514.5] + ├─ [514.5 - 516.5] + ├─ [516.5 - 518.5] + ├─ [518.5 - 520.5] + ├─ [520.5 - 522.5] + ├─ [522.5 - 524.5] + ├─ [524.5 - 526.5] + ├─ [526.5 - 528.5] + ├─ [528.5 - 530.5] + ├─ [530.5 - 532.5] + ├─ [532.5 - 534.5] + ├─ [534.5 - 536.5] + ├─ [536.5 - 538.5] + ├─ [538.5 - 540.5] + ├─ [540.5 - 542.5] + ├─ [542.5 - 544.5] + ├─ [544.5 - 546.5] + ├─ [546.5 - 548.5] + ├─ [548.5 - 550.5] + ├─ [550.5 - 552.5] + ├─ [552.5 - 554.5] + ├─ [554.5 - 556.5] + ├─ [556.5 - 558.5] + ├─ [558.5 - 560.5] + ├─ [560.5 - 562.5] + ├─ [562.5 - 564.5] + ├─ [564.5 - 566.5] + ├─ [566.5 - 568.5] + ├─ [568.5 - 570.5] + ├─ [570.5 - 572.5] + ├─ [572.5 - 574.5] + ├─ [574.5 - 576.5] + ├─ [576.5 - 578.5] + ├─ [578.5 - 580.5] + ├─ [580.5 - 582.5] + ├─ [582.5 - 584.5] + ├─ [584.5 - 586.5] + ├─ [586.5 - 588.5] + ├─ [588.5 - 590.5] + ├─ [590.5 - 592.5] + ├─ [592.5 - 594.5] + ├─ [594.5 - 596.5] + ├─ [596.5 - 598.5] + ├─ [598.5 - 600.5] + ├─ [600.5 - 602.5] + ├─ [602.5 - 604.5] + ├─ [604.5 - 606.5] + ├─ [606.5 - 608.5] + ├─ [608.5 - 610.5] + ├─ [610.5 - 612.5] + ├─ [612.5 - 614.5] + ├─ [614.5 - 616.5] + ├─ [616.5 - 618.5] + ├─ [618.5 - 620.5] + ├─ [620.5 - 622.5] + ├─ [622.5 - 624.5] + ├─ [624.5 - 626.5] + ├─ [626.5 - 628.5] + ├─ [628.5 - 630.5] + ├─ [630.5 - 632.5] + ├─ [632.5 - 634.5] + ├─ [634.5 - 636.5] + ├─ [636.5 - 638.5] + ├─ [638.5 - 640.5] + ├─ [640.5 - 642.5] + ├─ [642.5 - 644.5] + ├─ [644.5 - 646.5] + ├─ [646.5 - 648.5] + ├─ [648.5 - 650.5] + ├─ [650.5 - 652.5] + ├─ [652.5 - 654.5] + ├─ [654.5 - 656.5] + ├─ [656.5 - 658.5] + ├─ [658.5 - 660.6] + ├─ [660.6 - 662.6] + ├─ [662.6 - 664.6] + ├─ [664.6 - 666.6] + ├─ [666.6 - 668.6] + ├─ [668.6 - 670.6] + ├─ [670.6 - 672.6] + ├─ [672.6 - 674.6] + ├─ [674.6 - 676.6] + ├─ [676.6 - 678.6] + ├─ [678.6 - 680.6] + ├─ [680.6 - 682.6] + ├─ [682.6 - 684.6] + ├─ [684.6 - 686.6] + ├─ [686.6 - 688.6] + ├─ [688.6 - 690.6] + ├─ [690.6 - 692.6] + ├─ [692.6 - 694.6] + ├─ [694.6 - 696.6] + ├─ [696.6 - 698.6] + ├─ [698.6 - 700.6] + ├─ [700.6 - 702.6] + ├─ [702.6 - 704.6] + ├─ [704.6 - 706.6] + ├─ [706.6 - 708.6] + ├─ [708.6 - 710.6] + ├─ [710.6 - 712.6] + ├─ [712.6 - 714.6] + ├─ [714.6 - 716.6] + ├─ [716.6 - 718.6] + ├─ [718.6 - 720.6] + ├─ [720.6 - 722.6] + ├─ [722.6 - 724.6] + ├─ [724.6 - 726.6] + ├─ [726.6 - 728.6] + ├─ [728.6 - 730.6] + ├─ [730.6 - 732.6] + ├─ [732.6 - 734.6] + ├─ [734.6 - 736.6] + ├─ [736.6 - 738.6] + ├─ [738.6 - 740.6] + ├─ [740.6 - 742.6] + ├─ [742.6 - 744.6] + ├─ [744.6 - 746.6] + ├─ [746.6 - 748.6] + ├─ [748.6 - 750.6] + ├─ [750.6 - 752.6] + ├─ [752.6 - 754.6] + ├─ [754.6 - 756.6] + ├─ [756.6 - 758.6] + ├─ [758.6 - 760.6] + ├─ [760.6 - 762.6] + ├─ [762.6 - 764.6] + ├─ [764.6 - 766.6] + ├─ [766.6 - 768.6] + ├─ [768.6 - 770.6] + ├─ [770.6 - 772.6] + ├─ [772.6 - 774.6] + ├─ [774.6 - 776.6] + ├─ [776.6 - 778.6] + ├─ [778.6 - 780.6] + ├─ [780.6 - 782.6] + ├─ [782.6 - 784.6] + ├─ [784.6 - 786.6] + ├─ [786.6 - 788.6] + ├─ [788.6 - 790.6] + ├─ [790.6 - 792.6] + ├─ [792.6 - 794.6] + ├─ [794.6 - 796.6] + ├─ [796.6 - 798.6] + ├─ [798.6 - 800.6] + ├─ [800.6 - 802.6] + ├─ [802.6 - 804.6] + ├─ [804.6 - 806.6] + ├─ [806.6 - 808.6] + ├─ [808.6 - 810.6] + ├─ [810.6 - 812.6] + ├─ [812.6 - 814.6] + ├─ [814.6 - 816.6] + ├─ [816.6 - 818.6] + ├─ [818.6 - 820.6] + ├─ [820.6 - 822.6] + ├─ [822.6 - 824.6] + ├─ [824.6 - 826.6] + ├─ [826.6 - 828.6] + ├─ [828.6 - 830.6] + ├─ [830.6 - 832.6] + ├─ [832.6 - 834.6] + ├─ [834.6 - 836.6] + ├─ [836.6 - 838.6] + ├─ [838.6 - 840.6] + ├─ [840.6 - 842.6] + ├─ [842.6 - 844.6] + ├─ [844.6 - 846.6] + ├─ [846.6 - 848.6] + ├─ [848.6 - 850.6] + ├─ [850.6 - 852.6] + ├─ [852.6 - 854.6] + ├─ [854.6 - 856.6] + ├─ [856.6 - 858.6] + ├─ [858.6 - 860.6] + ├─ [860.6 - 862.6] + ├─ [862.6 - 864.6] + ├─ [864.6 - 866.6] + ├─ [866.6 - 868.6] + ├─ [868.6 - 870.6] + ├─ [870.6 - 872.6] + ├─ [872.6 - 874.6] + ├─ [874.6 - 876.6] + ├─ [876.6 - 878.6] + ├─ [878.6 - 880.7] + ├─ [880.7 - 882.7] + ├─ [882.7 - 884.7] + ├─ [884.7 - 886.7] + ├─ [886.7 - 888.7] + ├─ [888.7 - 890.7] + ├─ [890.7 - 892.7] + ├─ [892.7 - 894.7] + ├─ [894.7 - 896.7] + ├─ [896.7 - 898.7] + ├─ [898.7 - 900.7] + ├─ [900.7 - 902.7] + ├─ [902.7 - 904.7] + ├─ [904.7 - 906.7] + ├─ [906.7 - 908.7] + ├─ [908.7 - 910.7] + ├─ [910.7 - 912.7] + ├─ [912.7 - 914.7] + ├─ [914.7 - 916.7] + ├─ [916.7 - 918.7] + ├─ [918.7 - 920.7] + ├─ [920.7 - 922.7] + ├─ [922.7 - 924.7] + ├─ [924.7 - 926.7] + ├─ [926.7 - 928.7] + ├─ [928.7 - 930.7] + ├─ [930.7 - 932.7] + ├─ [932.7 - 934.7] + ├─ [934.7 - 936.7] + ├─ [936.7 - 938.7] + ├─ [938.7 - 940.7] + ├─ [940.7 - 942.7] + ├─ [942.7 - 944.7] + ├─ [944.7 - 946.7] + ├─ [946.7 - 948.7] + ├─ [948.7 - 950.7] + ├─ [950.7 - 952.7] + ├─ [952.7 - 954.7] + ├─ [954.7 - 956.7] + ├─ [956.7 - 958.7] + ├─ [958.7 - 960.7] + ├─ [960.7 - 962.7] + ├─ [962.7 - 964.7] + ├─ [964.7 - 966.7] + ├─ [966.7 - 968.7] + ├─ [968.7 - 970.7] + ├─ [970.7 - 972.7] + ├─ [972.7 - 974.7] + ├─ [974.7 - 976.7] + ├─ [976.7 - 978.7] + └─ [978.7 - 980.7] + +[END-SETUP] + +[BEGIN-LOG] +INFO: [17/02/2026 17:28:59] -> Initializing Pipeline... +INFO: [17/02/2026 17:29:00] -> Loading... +INFO: [17/02/2026 17:29:00] -> Loading SNE File: 20260217_171737_4438_3ProteomeMix_Astral_SN20_4_filteredBZ.sne +INFO: [17/02/2026 17:17:38] -> Initializing Pipeline... +INFO: [17/02/2026 17:17:38] -> Loading... +INFO: [17/02/2026 17:17:38] -> Loading SNE File: 20260128_151258_4438_3ProteomeMix_Astral_SN20_4\20260128_151258_4438_3ProteomeMix_Astral_SN20_4.sne +INFO: [29/01/2026 12:33:50] -> Initializing Pipeline... +INFO: [29/01/2026 12:33:50] -> Preprocessing Run #1 of 6... +INFO: [29/01/2026 12:33:51] -> Searching DIA with Pulsar... +INFO: [29/01/2026 12:33:52] -> Initialize Pipeline... +INFO: [29/01/2026 12:33:52] -> Initialize Pipeline +INFO: [29/01/2026 12:34:18] -> Initialize Pipeline... +INFO: [29/01/2026 12:34:18] -> Creating Experiment Environment +INFO: [29/01/2026 12:34:18] -> SuperRun 1/6: Initializing (LFQ_24min_500ng_2Th_3ms_A_01.raw) +INFO: [29/01/2026 12:34:18] -> Run 1/6: Organizing Data from Run +INFO: [29/01/2026 12:34:18] -> Run 1/6: Creating Calibration Search Space... +INFO: [29/01/2026 12:34:20] -> Collecting Search Space Parameters... +INFO: [29/01/2026 12:34:23] -> Creating the Search Space... +INFO: [29/01/2026 12:34:24] -> Run 1/6: Creating Search Space... +INFO: [29/01/2026 12:34:24] -> Collecting Search Space Parameters... +INFO: [29/01/2026 12:34:50] -> Creating the Search Space... +INFO: [29/01/2026 12:34:51] -> Size of search space [MB]: 0 +INFO: [29/01/2026 12:34:51] -> Number of IMPGs in search space: 0 +INFO: [29/01/2026 12:34:51] -> Run 1/6: MS2 Index Generation +INFO: [29/01/2026 12:37:08] -> MS2 Index Generation Time: 2.29m +INFO: [29/01/2026 12:37:08] -> Run 1/6: Method-Specific Pre-Processing... +INFO: [29/01/2026 12:37:09] -> Initializing... +INFO: [29/01/2026 12:39:53] -> Finalizing HTRMS File... +INFO: [29/01/2026 12:46:43] -> Processed in 9.6m +INFO: [29/01/2026 12:46:47] -> Done +INFO: [29/01/2026 12:46:48] -> Run 1/6: Process Raw File... +INFO: [29/01/2026 12:52:59] -> Run 1/6: Preparing Partitions... +INFO: [29/01/2026 12:52:59] -> Part 1/1 - Run 1/6: Preparing Calibration Searches... +INFO: [29/01/2026 12:53:00] -> Part 1/1 - Run 1/6: First Pass Calibration Search... +INFO: [29/01/2026 12:53:24] -> Part 1/1 - Run 1/6: Calibration Search... +INFO: [29/01/2026 12:53:39] -> The number of PSMs identified during calibration with FDR <= 0.01 is 5101 [14.8s] +INFO: [29/01/2026 12:53:44] -> Part 1/1 - Run 1/6: Preparing Main Search... +INFO: [29/01/2026 12:53:45] -> Part 1/1 - Run 1/6: Main Search... +INFO: [29/01/2026 12:58:40] -> Extracting 555174 MS1 XICs +INFO: [29/01/2026 13:02:03] -> Run 1/6: Cleaning Up Run... +INFO: [29/01/2026 13:02:04] -> SuperRun 2/6: Initializing (LFQ_24min_500ng_2Th_3ms_A_02.raw) +INFO: [29/01/2026 13:02:04] -> Run 2/6: Organizing Data from Run +INFO: [29/01/2026 13:02:04] -> Run 2/6: Creating Calibration Search Space... +INFO: [29/01/2026 13:02:04] -> Run 2/6: Creating Search Space... +INFO: [29/01/2026 13:02:04] -> Size of search space [MB]: 0 +INFO: [29/01/2026 13:02:04] -> Number of IMPGs in search space: 0 +INFO: [29/01/2026 13:02:04] -> Run 2/6: MS2 Index Generation +INFO: [29/01/2026 13:02:05] -> MS2 Index Generation Time: 606.41ms +INFO: [29/01/2026 13:02:05] -> Run 2/6: Method-Specific Pre-Processing... +INFO: [29/01/2026 13:02:06] -> Initializing... +INFO: [29/01/2026 13:04:27] -> Finalizing HTRMS File... +INFO: [29/01/2026 13:10:20] -> Processed in 8.2m +INFO: [29/01/2026 13:10:22] -> Done +INFO: [29/01/2026 13:10:22] -> Run 2/6: Process Raw File... +INFO: [29/01/2026 13:15:48] -> Run 2/6: Preparing Partitions... +INFO: [29/01/2026 13:15:48] -> Part 1/1 - Run 2/6: Preparing Calibration Searches... +INFO: [29/01/2026 13:15:49] -> Part 1/1 - Run 2/6: First Pass Calibration Search... +INFO: [29/01/2026 13:16:14] -> Part 1/1 - Run 2/6: Calibration Search... +INFO: [29/01/2026 13:16:27] -> The number of PSMs identified during calibration with FDR <= 0.01 is 5500 [13.5s] +INFO: [29/01/2026 13:16:33] -> Part 1/1 - Run 2/6: Preparing Main Search... +INFO: [29/01/2026 13:16:34] -> Part 1/1 - Run 2/6: Main Search... +INFO: [29/01/2026 13:20:17] -> Extracting 495297 MS1 XICs +INFO: [29/01/2026 13:23:22] -> Run 2/6: Cleaning Up Run... +INFO: [29/01/2026 13:23:23] -> SuperRun 3/6: Initializing (LFQ_24min_500ng_2Th_3ms_A_03.raw) +INFO: [29/01/2026 13:23:23] -> Run 3/6: Organizing Data from Run +INFO: [29/01/2026 13:23:23] -> Run 3/6: Creating Calibration Search Space... +INFO: [29/01/2026 13:23:23] -> Run 3/6: Creating Search Space... +INFO: [29/01/2026 13:23:23] -> Size of search space [MB]: 0 +INFO: [29/01/2026 13:23:23] -> Number of IMPGs in search space: 0 +INFO: [29/01/2026 13:23:23] -> Run 3/6: MS2 Index Generation +INFO: [29/01/2026 13:23:24] -> MS2 Index Generation Time: 653.51ms +INFO: [29/01/2026 13:23:24] -> Run 3/6: Method-Specific Pre-Processing... +INFO: [29/01/2026 13:23:25] -> Initializing... +INFO: [29/01/2026 13:25:45] -> Finalizing HTRMS File... +INFO: [29/01/2026 13:31:51] -> Processed in 8.4m +INFO: [29/01/2026 13:31:53] -> Done +INFO: [29/01/2026 13:31:53] -> Run 3/6: Process Raw File... +INFO: [29/01/2026 13:37:23] -> Run 3/6: Preparing Partitions... +INFO: [29/01/2026 13:37:23] -> Part 1/1 - Run 3/6: Preparing Calibration Searches... +INFO: [29/01/2026 13:37:24] -> Part 1/1 - Run 3/6: First Pass Calibration Search... +INFO: [29/01/2026 13:37:48] -> Part 1/1 - Run 3/6: Calibration Search... +INFO: [29/01/2026 13:38:02] -> The number of PSMs identified during calibration with FDR <= 0.01 is 5190 [13.3s] +INFO: [29/01/2026 13:38:07] -> Part 1/1 - Run 3/6: Preparing Main Search... +INFO: [29/01/2026 13:38:07] -> Part 1/1 - Run 3/6: Main Search... +INFO: [29/01/2026 13:41:46] -> Extracting 487241 MS1 XICs +INFO: [29/01/2026 13:44:50] -> Run 3/6: Cleaning Up Run... +INFO: [29/01/2026 13:44:50] -> SuperRun 4/6: Initializing (LFQ_24min_500ng_2Th_3ms_B_01.raw) +INFO: [29/01/2026 13:44:50] -> Run 4/6: Organizing Data from Run +INFO: [29/01/2026 13:44:50] -> Run 4/6: Creating Calibration Search Space... +INFO: [29/01/2026 13:44:50] -> Run 4/6: Creating Search Space... +INFO: [29/01/2026 13:44:50] -> Size of search space [MB]: 0 +INFO: [29/01/2026 13:44:50] -> Number of IMPGs in search space: 0 +INFO: [29/01/2026 13:44:50] -> Run 4/6: MS2 Index Generation +INFO: [29/01/2026 13:44:51] -> MS2 Index Generation Time: 643.46ms +INFO: [29/01/2026 13:44:51] -> Run 4/6: Method-Specific Pre-Processing... +INFO: [29/01/2026 13:44:52] -> Initializing... +INFO: [29/01/2026 13:47:29] -> Finalizing HTRMS File... +INFO: [29/01/2026 13:54:22] -> Processed in 9.5m +INFO: [29/01/2026 13:54:25] -> Done +INFO: [29/01/2026 13:54:25] -> Run 4/6: Process Raw File... +INFO: [29/01/2026 14:00:39] -> Run 4/6: Preparing Partitions... +INFO: [29/01/2026 14:00:39] -> Part 1/1 - Run 4/6: Preparing Calibration Searches... +INFO: [29/01/2026 14:00:39] -> Part 1/1 - Run 4/6: First Pass Calibration Search... +INFO: [29/01/2026 14:01:05] -> Part 1/1 - Run 4/6: Calibration Search... +INFO: [29/01/2026 14:01:22] -> The number of PSMs identified during calibration with FDR <= 0.01 is 5098 [17s] +INFO: [29/01/2026 14:01:27] -> Part 1/1 - Run 4/6: Preparing Main Search... +INFO: [29/01/2026 14:01:27] -> Part 1/1 - Run 4/6: Main Search... +INFO: [29/01/2026 14:06:29] -> Extracting 573728 MS1 XICs +INFO: [29/01/2026 14:10:00] -> Run 4/6: Cleaning Up Run... +INFO: [29/01/2026 14:10:00] -> SuperRun 5/6: Initializing (LFQ_24min_500ng_2Th_3ms_B_02.raw) +INFO: [29/01/2026 14:10:00] -> Run 5/6: Organizing Data from Run +INFO: [29/01/2026 14:10:00] -> Run 5/6: Creating Calibration Search Space... +INFO: [29/01/2026 14:10:00] -> Run 5/6: Creating Search Space... +INFO: [29/01/2026 14:10:00] -> Size of search space [MB]: 0 +INFO: [29/01/2026 14:10:00] -> Number of IMPGs in search space: 0 +INFO: [29/01/2026 14:10:00] -> Run 5/6: MS2 Index Generation +INFO: [29/01/2026 14:10:00] -> MS2 Index Generation Time: 650.1ms +INFO: [29/01/2026 14:10:00] -> Run 5/6: Method-Specific Pre-Processing... +INFO: [29/01/2026 14:10:02] -> Initializing... +INFO: [29/01/2026 14:12:42] -> Finalizing HTRMS File... +INFO: [29/01/2026 14:18:51] -> Processed in 8.8m +INFO: [29/01/2026 14:18:54] -> Done +INFO: [29/01/2026 14:18:54] -> Run 5/6: Process Raw File... +INFO: [29/01/2026 14:24:16] -> Run 5/6: Preparing Partitions... +INFO: [29/01/2026 14:24:16] -> Part 1/1 - Run 5/6: Preparing Calibration Searches... +INFO: [29/01/2026 14:24:16] -> Part 1/1 - Run 5/6: First Pass Calibration Search... +INFO: [29/01/2026 14:24:42] -> Part 1/1 - Run 5/6: Calibration Search... +INFO: [29/01/2026 14:24:56] -> The number of PSMs identified during calibration with FDR <= 0.01 is 5700 [14s] +INFO: [29/01/2026 14:25:01] -> Part 1/1 - Run 5/6: Preparing Main Search... +INFO: [29/01/2026 14:25:02] -> Part 1/1 - Run 5/6: Main Search... +INFO: [29/01/2026 14:28:45] -> Extracting 494505 MS1 XICs +INFO: [29/01/2026 14:32:03] -> Run 5/6: Cleaning Up Run... +INFO: [29/01/2026 14:32:03] -> SuperRun 6/6: Initializing (LFQ_24min_500ng_2Th_3ms_B_03.raw) +INFO: [29/01/2026 14:32:03] -> Run 6/6: Organizing Data from Run +INFO: [29/01/2026 14:32:03] -> Run 6/6: Creating Calibration Search Space... +INFO: [29/01/2026 14:32:03] -> Run 6/6: Creating Search Space... +INFO: [29/01/2026 14:32:03] -> Size of search space [MB]: 0 +INFO: [29/01/2026 14:32:03] -> Number of IMPGs in search space: 0 +INFO: [29/01/2026 14:32:03] -> Run 6/6: MS2 Index Generation +INFO: [29/01/2026 14:32:04] -> MS2 Index Generation Time: 653.77ms +INFO: [29/01/2026 14:32:04] -> Run 6/6: Method-Specific Pre-Processing... +INFO: [29/01/2026 14:32:05] -> Initializing... +INFO: [29/01/2026 14:34:43] -> Finalizing HTRMS File... +INFO: [29/01/2026 14:40:46] -> Processed in 8.7m +INFO: [29/01/2026 14:40:49] -> Done +INFO: [29/01/2026 14:40:49] -> Run 6/6: Process Raw File... +INFO: [29/01/2026 14:46:19] -> Run 6/6: Preparing Partitions... +INFO: [29/01/2026 14:46:19] -> Part 1/1 - Run 6/6: Preparing Calibration Searches... +INFO: [29/01/2026 14:46:20] -> Part 1/1 - Run 6/6: First Pass Calibration Search... +INFO: [29/01/2026 14:46:43] -> Part 1/1 - Run 6/6: Calibration Search... +INFO: [29/01/2026 14:46:57] -> The number of PSMs identified during calibration with FDR <= 0.01 is 5716 [13.8s] +INFO: [29/01/2026 14:47:02] -> Part 1/1 - Run 6/6: Preparing Main Search... +INFO: [29/01/2026 14:47:03] -> Part 1/1 - Run 6/6: Main Search... +INFO: [29/01/2026 14:50:49] -> Extracting 497534 MS1 XICs +INFO: [29/01/2026 14:53:56] -> Run 6/6: Cleaning Up Run... +INFO: [29/01/2026 14:54:22] -> Remove Aborted Runs (if any) from the Experiment... +INFO: [29/01/2026 14:54:22] -> Score Post-Processing +INFO: [29/01/2026 14:58:48] -> Score Post-Processing: 4.43m +INFO: [29/01/2026 14:58:48] -> PSM FDR... +INFO: [29/01/2026 15:00:06] -> PSM FDR: 1.3m +INFO: [29/01/2026 15:00:06] -> Converting to non-redundant data structure... +INFO: [29/01/2026 15:00:52] -> Performing Peptide FDR... +INFO: [29/01/2026 15:01:04] -> Performing Protein Inference... +INFO: [29/01/2026 15:01:09] -> Performing Protein FDR... +INFO: [29/01/2026 15:01:10] -> Calculating Result Values at Run and Experiment Level... +INFO: [29/01/2026 15:01:37] -> Pulsar identified 510028 PSMs, 108426 stripped sequences, 126701 peptide precursors, 11289 protein groups. +INFO: [29/01/2026 15:01:37] -> iRT Calibration... +INFO: [29/01/2026 15:01:43] -> Performing directDIA+ search fine tuning... +INFO: [29/01/2026 15:05:45] -> Performing directDIA+ search... +INFO: [29/01/2026 15:05:46] -> directDIA+ search: LFQ_24min_500ng_2Th_3ms_A_01.raw +INFO: [29/01/2026 15:10:36] -> directDIA+ search: LFQ_24min_500ng_2Th_3ms_A_02.raw +INFO: [29/01/2026 15:13:20] -> directDIA+ search: LFQ_24min_500ng_2Th_3ms_A_03.raw +INFO: [29/01/2026 15:17:27] -> directDIA+ search: LFQ_24min_500ng_2Th_3ms_B_01.raw +INFO: [29/01/2026 15:23:37] -> directDIA+ search: LFQ_24min_500ng_2Th_3ms_B_02.raw +INFO: [29/01/2026 15:26:46] -> directDIA+ search: LFQ_24min_500ng_2Th_3ms_B_03.raw +INFO: [29/01/2026 15:29:34] -> Total nr of Searched Peptides: 1,817,496 of 2,709,933 +INFO: [29/01/2026 15:29:34] -> Initializing Combined Protein Inference... +INFO: [29/01/2026 15:29:37] -> Performing Protein Inference... +INFO: [29/01/2026 15:29:47] -> Performing Protein FDR... +INFO: [29/01/2026 15:29:48] -> Calculating Result Values at Run and Experiment Level... +INFO: [29/01/2026 15:30:04] -> Pulsar identified 510031 PSMs, 108428 stripped sequences, 126703 peptide precursors, 11321 protein groups. +INFO: [29/01/2026 15:30:04] -> Calculating Summary... +INFO: [29/01/2026 15:30:15] -> Identifying Calibration Peptides... +INFO: [29/01/2026 15:30:18] -> Performing directDIA+ Post Processing... +INFO: [29/01/2026 15:30:33] -> Combining Protein-Group FDR Results... +INFO: [29/01/2026 15:30:34] -> Combined Protein-Group Identifications at 1% Qvalue: 13,801 +INFO: [29/01/2026 15:30:34] -> Writing experiment store.. +INFO: [29/01/2026 15:31:13] -> Assigning iRT Source... +INFO: [29/01/2026 15:31:13] -> Calculating iRT... +INFO: [29/01/2026 15:31:13] -> Cleaning Up Experiment... +INFO: [29/01/2026 15:31:14] -> Summarizing Identifications +INFO: [29/01/2026 15:36:05] -> Identifiying Significant Precursors... +INFO: [29/01/2026 15:37:13] -> Predicting Ion Mobility +INFO: [29/01/2026 15:37:29] -> Calculating Median iRT +INFO: [29/01/2026 15:37:32] -> Building BGS Protein Grouping... +INFO: [29/01/2026 15:37:33] -> Digesting Fasta... +INFO: [29/01/2026 15:37:33] -> Annotating Proteins... +INFO: [29/01/2026 15:37:34] -> Grouping Proteins... +INFO: [29/01/2026 15:37:38] -> Calculating Run Summary Statistics... +INFO: [29/01/2026 15:40:07] -> Building Consensus Fragment Spectra... +INFO: [29/01/2026 15:43:04] -> Selecting best fragment ions +INFO: [29/01/2026 15:53:10] -> Protein Groups: 13791 (13995), Protein Id: 13929 (14121), Modified Peptides: 191699, Precursors: 228573, Peptides: 190204, Fragments: 1368730 +INFO: [29/01/2026 15:53:48] -> Initializing Experiment... +INFO: [29/01/2026 15:53:48] -> Loading Spectral Libraries... +INFO: [29/01/2026 15:56:11] -> Initialize Scoring... +INFO: [29/01/2026 15:56:11] -> Generating Scan Map... +INFO: [29/01/2026 15:56:11] -> Initializing Workpackages... +INFO: [29/01/2026 15:56:11] -> Performing Basic Calibration... +INFO: [29/01/2026 15:56:13] -> Calibration successful +INFO: [29/01/2026 15:56:13] -> Identifying Calibration Peptides... +INFO: [29/01/2026 15:56:13] -> Calibration successful +INFO: [29/01/2026 15:56:13] -> Correcting Gradient Fine Structure... +INFO: [29/01/2026 15:56:21] -> Initializing Pipeline... +INFO: [29/01/2026 15:56:21] -> Preprocessing Run #1 of 6... +INFO: [29/01/2026 15:56:21] -> Initialize Scoring... +INFO: [29/01/2026 15:56:21] -> Extracting Ion Currents... +INFO: [29/01/2026 15:57:11] -> Machine Learning... +INFO: [29/01/2026 15:57:17] -> Pipeline executed in 55.54s - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 15:57:22] -> Calibration successful +INFO: [29/01/2026 15:57:32] -> Initializing Pipeline... +INFO: [29/01/2026 15:57:32] -> Preprocessing Run #1 of 6... +INFO: [29/01/2026 15:57:32] -> Initialize Scoring... +INFO: [29/01/2026 15:57:32] -> Extracting Ion Currents... +INFO: [29/01/2026 15:59:26] -> Machine Learning... +INFO: [29/01/2026 15:59:51] -> Pipeline executed in 2.31m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:00:40] -> Calibration successful +INFO: [29/01/2026 16:00:40] -> Initialize Scoring... +INFO: [29/01/2026 16:00:40] -> Preprocessing Run #2 of 6... +INFO: [29/01/2026 16:00:40] -> Searching DIA with Pulsar... +INFO: [29/01/2026 16:00:40] -> Initializing Experiment... +INFO: [29/01/2026 16:00:40] -> Loading Spectral Libraries... +INFO: [29/01/2026 16:00:40] -> Initialize Scoring... +INFO: [29/01/2026 16:00:40] -> Generating Scan Map... +INFO: [29/01/2026 16:00:40] -> Initializing Workpackages... +INFO: [29/01/2026 16:00:40] -> Performing Basic Calibration... +INFO: [29/01/2026 16:00:41] -> Calibration successful +INFO: [29/01/2026 16:00:41] -> Identifying Calibration Peptides... +INFO: [29/01/2026 16:00:41] -> Calibration successful +INFO: [29/01/2026 16:00:41] -> Correcting Gradient Fine Structure... +INFO: [29/01/2026 16:00:48] -> Initializing Pipeline... +INFO: [29/01/2026 16:00:48] -> Preprocessing Run #2 of 6... +INFO: [29/01/2026 16:00:48] -> Initialize Scoring... +INFO: [29/01/2026 16:00:48] -> Extracting Ion Currents... +INFO: [29/01/2026 16:01:44] -> Machine Learning... +INFO: [29/01/2026 16:01:48] -> Pipeline executed in 1m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:01:53] -> Calibration successful +INFO: [29/01/2026 16:02:02] -> Initializing Pipeline... +INFO: [29/01/2026 16:02:02] -> Preprocessing Run #2 of 6... +INFO: [29/01/2026 16:02:02] -> Initialize Scoring... +INFO: [29/01/2026 16:02:02] -> Extracting Ion Currents... +INFO: [29/01/2026 16:04:00] -> Machine Learning... +INFO: [29/01/2026 16:04:25] -> Pipeline executed in 2.38m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:05:11] -> Calibration successful +INFO: [29/01/2026 16:05:11] -> Initialize Scoring... +INFO: [29/01/2026 16:05:11] -> Preprocessing Run #3 of 6... +INFO: [29/01/2026 16:05:11] -> Searching DIA with Pulsar... +INFO: [29/01/2026 16:05:11] -> Initializing Experiment... +INFO: [29/01/2026 16:05:11] -> Loading Spectral Libraries... +INFO: [29/01/2026 16:05:11] -> Initialize Scoring... +INFO: [29/01/2026 16:05:11] -> Generating Scan Map... +INFO: [29/01/2026 16:05:11] -> Initializing Workpackages... +INFO: [29/01/2026 16:05:11] -> Performing Basic Calibration... +INFO: [29/01/2026 16:05:13] -> Calibration successful +INFO: [29/01/2026 16:05:13] -> Identifying Calibration Peptides... +INFO: [29/01/2026 16:05:13] -> Calibration successful +INFO: [29/01/2026 16:05:13] -> Correcting Gradient Fine Structure... +INFO: [29/01/2026 16:05:23] -> Initializing Pipeline... +INFO: [29/01/2026 16:05:23] -> Preprocessing Run #3 of 6... +INFO: [29/01/2026 16:05:23] -> Initialize Scoring... +INFO: [29/01/2026 16:05:23] -> Extracting Ion Currents... +INFO: [29/01/2026 16:06:13] -> Machine Learning... +INFO: [29/01/2026 16:06:18] -> Pipeline executed in 55.01s - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:06:23] -> Calibration successful +INFO: [29/01/2026 16:06:31] -> Initializing Pipeline... +INFO: [29/01/2026 16:06:31] -> Preprocessing Run #3 of 6... +INFO: [29/01/2026 16:06:31] -> Initialize Scoring... +INFO: [29/01/2026 16:06:31] -> Extracting Ion Currents... +INFO: [29/01/2026 16:08:26] -> Machine Learning... +INFO: [29/01/2026 16:08:50] -> Pipeline executed in 2.33m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:09:38] -> Calibration successful +INFO: [29/01/2026 16:09:38] -> Initialize Scoring... +INFO: [29/01/2026 16:09:38] -> Preprocessing Run #4 of 6... +INFO: [29/01/2026 16:09:38] -> Searching DIA with Pulsar... +INFO: [29/01/2026 16:09:38] -> Initializing Experiment... +INFO: [29/01/2026 16:09:38] -> Loading Spectral Libraries... +INFO: [29/01/2026 16:09:38] -> Initialize Scoring... +INFO: [29/01/2026 16:09:38] -> Generating Scan Map... +INFO: [29/01/2026 16:09:38] -> Initializing Workpackages... +INFO: [29/01/2026 16:09:38] -> Performing Basic Calibration... +INFO: [29/01/2026 16:09:39] -> Calibration successful +INFO: [29/01/2026 16:09:39] -> Identifying Calibration Peptides... +INFO: [29/01/2026 16:09:39] -> Calibration successful +INFO: [29/01/2026 16:09:39] -> Correcting Gradient Fine Structure... +INFO: [29/01/2026 16:09:49] -> Initializing Pipeline... +INFO: [29/01/2026 16:09:49] -> Preprocessing Run #4 of 6... +INFO: [29/01/2026 16:09:49] -> Initialize Scoring... +INFO: [29/01/2026 16:09:49] -> Extracting Ion Currents... +INFO: [29/01/2026 16:10:39] -> Machine Learning... +INFO: [29/01/2026 16:10:48] -> Pipeline executed in 58.74s - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:10:53] -> Calibration successful +INFO: [29/01/2026 16:11:01] -> Initializing Pipeline... +INFO: [29/01/2026 16:11:01] -> Preprocessing Run #4 of 6... +INFO: [29/01/2026 16:11:01] -> Initialize Scoring... +INFO: [29/01/2026 16:11:01] -> Extracting Ion Currents... +INFO: [29/01/2026 16:12:54] -> Machine Learning... +INFO: [29/01/2026 16:13:18] -> Pipeline executed in 2.28m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:14:08] -> Calibration successful +INFO: [29/01/2026 16:14:08] -> Initialize Scoring... +INFO: [29/01/2026 16:14:08] -> Preprocessing Run #5 of 6... +INFO: [29/01/2026 16:14:08] -> Searching DIA with Pulsar... +INFO: [29/01/2026 16:14:08] -> Initializing Experiment... +INFO: [29/01/2026 16:14:08] -> Loading Spectral Libraries... +INFO: [29/01/2026 16:14:08] -> Initialize Scoring... +INFO: [29/01/2026 16:14:08] -> Generating Scan Map... +INFO: [29/01/2026 16:14:08] -> Initializing Workpackages... +INFO: [29/01/2026 16:14:08] -> Performing Basic Calibration... +INFO: [29/01/2026 16:14:09] -> Calibration successful +INFO: [29/01/2026 16:14:09] -> Identifying Calibration Peptides... +INFO: [29/01/2026 16:14:09] -> Calibration successful +INFO: [29/01/2026 16:14:09] -> Correcting Gradient Fine Structure... +INFO: [29/01/2026 16:14:18] -> Initializing Pipeline... +INFO: [29/01/2026 16:14:18] -> Preprocessing Run #5 of 6... +INFO: [29/01/2026 16:14:18] -> Initialize Scoring... +INFO: [29/01/2026 16:14:18] -> Extracting Ion Currents... +INFO: [29/01/2026 16:15:12] -> Machine Learning... +INFO: [29/01/2026 16:15:17] -> Pipeline executed in 59.1s - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:15:22] -> Calibration successful +INFO: [29/01/2026 16:15:32] -> Initializing Pipeline... +INFO: [29/01/2026 16:15:32] -> Preprocessing Run #5 of 6... +INFO: [29/01/2026 16:15:32] -> Initialize Scoring... +INFO: [29/01/2026 16:15:32] -> Extracting Ion Currents... +INFO: [29/01/2026 16:17:28] -> Machine Learning... +INFO: [29/01/2026 16:17:52] -> Pipeline executed in 2.34m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:18:40] -> Calibration successful +INFO: [29/01/2026 16:18:40] -> Initialize Scoring... +INFO: [29/01/2026 16:18:40] -> Preprocessing Run #6 of 6... +INFO: [29/01/2026 16:18:40] -> Searching DIA with Pulsar... +INFO: [29/01/2026 16:18:40] -> Initializing Experiment... +INFO: [29/01/2026 16:18:40] -> Loading Spectral Libraries... +INFO: [29/01/2026 16:18:40] -> Initialize Scoring... +INFO: [29/01/2026 16:18:40] -> Generating Scan Map... +INFO: [29/01/2026 16:18:40] -> Initializing Workpackages... +INFO: [29/01/2026 16:18:40] -> Performing Basic Calibration... +INFO: [29/01/2026 16:18:42] -> Calibration successful +INFO: [29/01/2026 16:18:42] -> Identifying Calibration Peptides... +INFO: [29/01/2026 16:18:42] -> Calibration successful +INFO: [29/01/2026 16:18:42] -> Correcting Gradient Fine Structure... +INFO: [29/01/2026 16:18:50] -> Initializing Pipeline... +INFO: [29/01/2026 16:18:50] -> Preprocessing Run #6 of 6... +INFO: [29/01/2026 16:18:50] -> Initialize Scoring... +INFO: [29/01/2026 16:18:50] -> Extracting Ion Currents... +INFO: [29/01/2026 16:19:46] -> Machine Learning... +INFO: [29/01/2026 16:19:51] -> Pipeline executed in 1.02m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:19:56] -> Calibration successful +INFO: [29/01/2026 16:20:04] -> Initializing Pipeline... +INFO: [29/01/2026 16:20:04] -> Preprocessing Run #6 of 6... +INFO: [29/01/2026 16:20:04] -> Initialize Scoring... +INFO: [29/01/2026 16:20:04] -> Extracting Ion Currents... +INFO: [29/01/2026 16:22:01] -> Machine Learning... +INFO: [29/01/2026 16:22:25] -> Pipeline executed in 2.35m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [29/01/2026 16:23:12] -> Calibration successful +INFO: [29/01/2026 16:23:12] -> Initialize Scoring... +INFO: [29/01/2026 16:23:12] -> Initializing QC... +INFO: [29/01/2026 16:23:12] -> Determining Calibration Parameter... +INFO: [29/01/2026 16:23:12] -> Assigning ML Features... +INFO: [29/01/2026 16:23:14] -> Generating Decoys... +INFO: [29/01/2026 16:23:30] -> Extracting Ion Currents... +INFO: [29/01/2026 16:25:25] -> Releasing Raw Files... +INFO: [29/01/2026 16:25:25] -> Machine Learning... +INFO: [29/01/2026 16:27:17] -> Initializing HTRMS... +INFO: [29/01/2026 16:27:17] -> Releasing Run Resources... +INFO: [29/01/2026 16:27:46] -> Reducing Score Cache... +INFO: [29/01/2026 16:28:11] -> Calculating Qvalues... +INFO: [29/01/2026 16:28:18] -> Unique precursors: 218,264 of 228,573 | modified peptides: 184,889 of 191,699 | peptides: 183,422 of 190,204 | protein groups: 13,736 (Qvalue <= 0.01) [LFQ_24min_500ng_2Th_3ms_A_01.raw] +INFO: [29/01/2026 16:28:18] -> Calibration successful +INFO: [29/01/2026 16:28:18] -> Annotating In-Source Fragmentation... +INFO: [29/01/2026 16:28:20] -> Collapsing Search Tree... +INFO: [29/01/2026 16:28:20] -> Processed in 5.14m +INFO: [29/01/2026 16:28:20] -> Initializing QC... +INFO: [29/01/2026 16:28:20] -> Determining Calibration Parameter... +INFO: [29/01/2026 16:28:21] -> Assigning ML Features... +INFO: [29/01/2026 16:28:21] -> Generating Decoys... +INFO: [29/01/2026 16:28:22] -> Extracting Ion Currents... +INFO: [29/01/2026 16:30:36] -> Releasing Raw Files... +INFO: [29/01/2026 16:30:36] -> Machine Learning... +INFO: [29/01/2026 16:32:30] -> Initializing HTRMS... +INFO: [29/01/2026 16:32:30] -> Releasing Run Resources... +INFO: [29/01/2026 16:32:58] -> Reducing Score Cache... +INFO: [29/01/2026 16:33:24] -> Calculating Qvalues... +INFO: [29/01/2026 16:33:30] -> Unique precursors: 216,905 of 228,573 | modified peptides: 183,806 of 191,699 | peptides: 182,351 of 190,204 | protein groups: 13,734 (Qvalue <= 0.01) [LFQ_24min_500ng_2Th_3ms_A_02.raw] +INFO: [29/01/2026 16:33:30] -> Calibration successful +INFO: [29/01/2026 16:33:30] -> Annotating In-Source Fragmentation... +INFO: [29/01/2026 16:33:32] -> Collapsing Search Tree... +INFO: [29/01/2026 16:33:32] -> Processed in 5.2m +INFO: [29/01/2026 16:33:32] -> Initializing QC... +INFO: [29/01/2026 16:33:32] -> Determining Calibration Parameter... +INFO: [29/01/2026 16:33:33] -> Assigning ML Features... +INFO: [29/01/2026 16:33:33] -> Generating Decoys... +INFO: [29/01/2026 16:33:35] -> Extracting Ion Currents... +INFO: [29/01/2026 16:35:55] -> Releasing Raw Files... +INFO: [29/01/2026 16:35:55] -> Machine Learning... +INFO: [29/01/2026 16:37:47] -> Initializing HTRMS... +INFO: [29/01/2026 16:37:47] -> Releasing Run Resources... +INFO: [29/01/2026 16:38:15] -> Reducing Score Cache... +INFO: [29/01/2026 16:38:40] -> Calculating Qvalues... +INFO: [29/01/2026 16:38:46] -> Unique precursors: 217,766 of 228,573 | modified peptides: 184,255 of 191,699 | peptides: 182,798 of 190,204 | protein groups: 13,737 (Qvalue <= 0.01) [LFQ_24min_500ng_2Th_3ms_A_03.raw] +INFO: [29/01/2026 16:38:46] -> Calibration successful +INFO: [29/01/2026 16:38:46] -> Annotating In-Source Fragmentation... +INFO: [29/01/2026 16:38:47] -> Collapsing Search Tree... +INFO: [29/01/2026 16:38:47] -> Processed in 5.26m +INFO: [29/01/2026 16:38:47] -> Initializing QC... +INFO: [29/01/2026 16:38:48] -> Determining Calibration Parameter... +INFO: [29/01/2026 16:38:48] -> Assigning ML Features... +INFO: [29/01/2026 16:38:48] -> Generating Decoys... +INFO: [29/01/2026 16:38:49] -> Extracting Ion Currents... +INFO: [29/01/2026 16:41:03] -> Releasing Raw Files... +INFO: [29/01/2026 16:41:03] -> Machine Learning... +INFO: [29/01/2026 16:42:54] -> Initializing HTRMS... +INFO: [29/01/2026 16:42:54] -> Releasing Run Resources... +INFO: [29/01/2026 16:43:22] -> Reducing Score Cache... +INFO: [29/01/2026 16:43:47] -> Calculating Qvalues... +INFO: [29/01/2026 16:43:53] -> Unique precursors: 221,226 of 228,573 | modified peptides: 186,176 of 191,699 | peptides: 184,714 of 190,204 | protein groups: 13,751 (Qvalue <= 0.01) [LFQ_24min_500ng_2Th_3ms_B_01.raw] +INFO: [29/01/2026 16:43:53] -> Calibration successful +INFO: [29/01/2026 16:43:53] -> Annotating In-Source Fragmentation... +INFO: [29/01/2026 16:43:54] -> Collapsing Search Tree... +INFO: [29/01/2026 16:43:54] -> Processed in 5.1m +INFO: [29/01/2026 16:43:54] -> Initializing QC... +INFO: [29/01/2026 16:43:54] -> Determining Calibration Parameter... +INFO: [29/01/2026 16:43:54] -> Assigning ML Features... +INFO: [29/01/2026 16:43:54] -> Generating Decoys... +INFO: [29/01/2026 16:43:55] -> Extracting Ion Currents... +INFO: [29/01/2026 16:46:04] -> Releasing Raw Files... +INFO: [29/01/2026 16:46:04] -> Machine Learning... +INFO: [29/01/2026 16:47:47] -> Initializing HTRMS... +INFO: [29/01/2026 16:47:47] -> Releasing Run Resources... +INFO: [29/01/2026 16:48:15] -> Reducing Score Cache... +INFO: [29/01/2026 16:48:41] -> Calculating Qvalues... +INFO: [29/01/2026 16:48:48] -> Unique precursors: 219,749 of 228,573 | modified peptides: 185,521 of 191,699 | peptides: 184,063 of 190,204 | protein groups: 13,719 (Qvalue <= 0.01) [LFQ_24min_500ng_2Th_3ms_B_02.raw] +INFO: [29/01/2026 16:48:48] -> Calibration successful +INFO: [29/01/2026 16:48:48] -> Annotating In-Source Fragmentation... +INFO: [29/01/2026 16:48:49] -> Collapsing Search Tree... +INFO: [29/01/2026 16:48:49] -> Processed in 4.91m +INFO: [29/01/2026 16:48:49] -> Initializing QC... +INFO: [29/01/2026 16:48:49] -> Determining Calibration Parameter... +INFO: [29/01/2026 16:48:49] -> Assigning ML Features... +INFO: [29/01/2026 16:48:49] -> Generating Decoys... +INFO: [29/01/2026 16:48:50] -> Extracting Ion Currents... +INFO: [29/01/2026 16:51:01] -> Releasing Raw Files... +INFO: [29/01/2026 16:51:01] -> Machine Learning... +INFO: [29/01/2026 16:52:55] -> Initializing HTRMS... +INFO: [29/01/2026 16:52:55] -> Releasing Run Resources... +INFO: [29/01/2026 16:53:23] -> Reducing Score Cache... +INFO: [29/01/2026 16:53:48] -> Calculating Qvalues... +INFO: [29/01/2026 16:53:53] -> Unique precursors: 220,242 of 228,573 | modified peptides: 185,858 of 191,699 | peptides: 184,395 of 190,204 | protein groups: 13,738 (Qvalue <= 0.01) [LFQ_24min_500ng_2Th_3ms_B_03.raw] +INFO: [29/01/2026 16:53:53] -> Calibration successful +INFO: [29/01/2026 16:53:53] -> Annotating In-Source Fragmentation... +INFO: [29/01/2026 16:53:54] -> Collapsing Search Tree... +INFO: [29/01/2026 16:53:54] -> Processed in 5.09m +INFO: [29/01/2026 16:53:54] -> Normalizing Cscores... +INFO: [29/01/2026 16:53:55] -> Calculating Global CVs +INFO: [29/01/2026 16:53:58] -> Calculating Profile Qvalues... +INFO: [29/01/2026 16:54:02] -> Initializing Protein Groups... +INFO: [29/01/2026 16:54:39] -> Correcting Interferences... +INFO: [29/01/2026 16:56:25] -> Calculating Global CVs +INFO: [29/01/2026 16:56:28] -> Calculating Precursor Quantities... +INFO: [29/01/2026 16:56:28] -> Normalizing Quantification... +INFO: [29/01/2026 16:56:28] -> Cross-run normalization disabled! +INFO: [29/01/2026 16:56:28] -> Calculating Global CVs +INFO: [29/01/2026 16:56:32] -> Calculating Condition CVs +INFO: [29/01/2026 16:56:44] -> Excluding Library Duplicates... +INFO: [29/01/2026 16:56:46] -> Calculating Global CVs +INFO: [29/01/2026 16:56:49] -> Building Protein Groups... +INFO: [29/01/2026 16:57:18] -> Calculating Run-Wise Protein Group FDR... +INFO: [29/01/2026 16:57:31] -> Annotating Protein Single Hits... +INFO: [29/01/2026 16:57:34] -> Calculating Global CVs +INFO: [29/01/2026 16:57:38] -> Updating Identification Counts... +INFO: [29/01/2026 16:57:43] -> Subtracting background noise... +INFO: [29/01/2026 16:57:43] -> Resetting existing imputation... +INFO: [29/01/2026 16:57:47] -> Calculating Protein Quantities... +INFO: [29/01/2026 16:57:49] -> Calculating Protein Quantities... +INFO: [29/01/2026 16:57:49] -> Calculating MaxLFQ Protein Quantities... +INFO: [29/01/2026 16:58:11] -> Collapsing PTM-Locations... +INFO: [29/01/2026 16:58:11] -> PTM Stoichiometry calculation... +INFO: [29/01/2026 16:58:11] -> Creating Protein Map... +INFO: [29/01/2026 16:58:11] -> Compiling Run Summary Information... +INFO: [29/01/2026 16:58:13] -> Running Post Analysis Processes... +INFO: [29/01/2026 16:59:06] -> Saving Qc Data... +INFO: [29/01/2026 16:59:26] -> Generating Output +INFO: [29/01/2026 16:59:26] -> Generating Experiment Report +INFO: [29/01/2026 16:59:32] -> 4438_3ProteomeMix_Astral_SN20_4_Report_einprot_Pivot_PG_reduced (Pivot).parquet +INFO: [29/01/2026 16:59:32] -> Generating RT Recalibration Report +INFO: [29/01/2026 16:59:32] -> LFQ_24min_500ng_2Th_3ms_A_01.raw_1_iRTCalibration.tsv +INFO: [29/01/2026 16:59:32] -> LFQ_24min_500ng_2Th_3ms_A_02.raw_2_iRTCalibration.tsv +INFO: [29/01/2026 16:59:32] -> LFQ_24min_500ng_2Th_3ms_A_03.raw_3_iRTCalibration.tsv +INFO: [29/01/2026 16:59:32] -> LFQ_24min_500ng_2Th_3ms_B_01.raw_4_iRTCalibration.tsv +INFO: [29/01/2026 16:59:32] -> LFQ_24min_500ng_2Th_3ms_B_02.raw_5_iRTCalibration.tsv +INFO: [29/01/2026 16:59:32] -> LFQ_24min_500ng_2Th_3ms_B_03.raw_6_iRTCalibration.tsv +INFO: [29/01/2026 16:59:32] -> Generating Run Meta Report +INFO: [29/01/2026 16:59:35] -> LFQ_24min_500ng_2Th_3ms_A_01.raw_1_RunOverview.tsv +INFO: [29/01/2026 16:59:35] -> LFQ_24min_500ng_2Th_3ms_A_02.raw_2_RunOverview.tsv +INFO: [29/01/2026 16:59:35] -> LFQ_24min_500ng_2Th_3ms_A_03.raw_3_RunOverview.tsv +INFO: [29/01/2026 16:59:35] -> LFQ_24min_500ng_2Th_3ms_B_01.raw_4_RunOverview.tsv +INFO: [29/01/2026 16:59:35] -> LFQ_24min_500ng_2Th_3ms_B_02.raw_5_RunOverview.tsv +INFO: [29/01/2026 16:59:35] -> LFQ_24min_500ng_2Th_3ms_B_03.raw_6_RunOverview.tsv +INFO: [29/01/2026 16:59:35] -> Generating Post Analysis Reports +INFO: [29/01/2026 17:01:17] -> 4438_3ProteomeMix_Astral_SN20_4_AnalyisOverview.txt +INFO: [29/01/2026 17:01:17] -> RunIdentifications.pdf +INFO: [29/01/2026 17:01:17] -> DataCompleteness.pdf +INFO: [29/01/2026 17:01:17] -> CVsBelowX.pdf +INFO: [29/01/2026 17:01:17] -> TICOverlay.pdf +INFO: [29/01/2026 17:01:17] -> CVDensityPerCondition.png +INFO: [29/01/2026 17:01:17] -> CVBoxPlotsPerCondition.png +INFO: [29/01/2026 17:01:17] -> Datapoints Per PeakDensityPerCondition.png +INFO: [29/01/2026 17:01:17] -> Datapoints Per PeakBoxPlotsPerCondition.png +INFO: [29/01/2026 17:01:17] -> Binned Identification.pdf +INFO: [29/01/2026 17:01:17] -> Binned Coefficients of Variation.pdf +INFO: [29/01/2026 17:01:17] -> Precursors [Label Free] - [1] LFQ_24min_500ng_2Th_3ms_A_01.raw.pdf +INFO: [29/01/2026 17:01:17] -> Precursors [Label Free] - [2] LFQ_24min_500ng_2Th_3ms_A_02.raw.pdf +INFO: [29/01/2026 17:01:17] -> Precursors [Label Free] - [3] LFQ_24min_500ng_2Th_3ms_A_03.raw.pdf +INFO: [29/01/2026 17:01:17] -> Precursors [Label Free] - [4] LFQ_24min_500ng_2Th_3ms_B_01.raw.pdf +INFO: [29/01/2026 17:01:17] -> Precursors [Label Free] - [5] LFQ_24min_500ng_2Th_3ms_B_02.raw.pdf +INFO: [29/01/2026 17:01:17] -> Precursors [Label Free] - [6] LFQ_24min_500ng_2Th_3ms_B_03.raw.pdf +INFO: [29/01/2026 17:01:17] -> ProteinFDR-Histogram.pdf +INFO: [29/01/2026 17:01:17] -> Candidates.tsv +INFO: [29/01/2026 17:01:17] -> VolcanoPlot.pdf +INFO: [29/01/2026 17:01:17] -> Heatmap.png +INFO: [29/01/2026 17:01:17] -> Generating Normalization Report +INFO: [29/01/2026 17:01:17] -> Generating Condition Setup Report +INFO: [29/01/2026 17:01:17] -> 4438_3ProteomeMix_Astral_SN20_4_ConditionSetup.tsv +INFO: [29/01/2026 17:01:17] -> Generating Memory Consumption Report +INFO: [29/01/2026 17:01:17] -> 4438_3ProteomeMix_Astral_SN20_4_MemoryConsumption.tsv +INFO: [29/01/2026 17:01:17] -> Generating Experiment Settings Report +INFO: [29/01/2026 17:01:21] -> 4438_3ProteomeMix_Astral_SN20_4_ExperimentSetupOverview_PAF_einprot (default BGS).txt +INFO: [29/01/2026 17:01:21] -> Writing Summary Information... +INFO: [29/01/2026 17:01:21] -> Unique precursors: 208,784 of 228,573 | modified peptides: 177,843 of 191,699 | peptides: 176,406 of 190,204 | protein groups: 13,482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [29/01/2026 17:01:21] -> Unique precursors: 208,083 of 228,573 | modified peptides: 177,257 of 191,699 | peptides: 175,827 of 190,204 | protein groups: 13,497 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [29/01/2026 17:01:21] -> Unique precursors: 208,542 of 228,573 | modified peptides: 177,430 of 191,699 | peptides: 175,998 of 190,204 | protein groups: 13,486 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [29/01/2026 17:01:21] -> Unique precursors: 210,919 of 228,573 | modified peptides: 178,760 of 191,699 | peptides: 177,323 of 190,204 | protein groups: 13,473 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [29/01/2026 17:01:21] -> Unique precursors: 210,065 of 228,573 | modified peptides: 178,425 of 191,699 | peptides: 176,992 of 190,204 | protein groups: 13,460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [29/01/2026 17:01:21] -> Unique precursors: 210,415 of 228,573 | modified peptides: 178,654 of 191,699 | peptides: 177,218 of 190,204 | protein groups: 13,477 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [29/01/2026 17:01:21] -> Unique precursors: 215,205 of 228,573 | modified peptides: 182,186 of 191,699 | peptides: 180,733 of 190,204 | protein groups: 13,560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [29/01/2026 17:01:21] -> Saving SNE File... +INFO: [29/01/2026 17:01:21] -> Storing SNE File: 20260128_151258_4438_3ProteomeMix_Astral_SN20_4\20260128_151258_4438_3ProteomeMix_Astral_SN20_4.sne +INFO: [29/01/2026 17:01:21] -> Initializing Pipeline... +INFO: [29/01/2026 17:01:21] -> Calculating Qvalues... +INFO: [29/01/2026 17:01:21] -> Saving... +INFO: [17/02/2026 17:18:32] -> Applying settings to loaded experiment... +INFO: [17/02/2026 17:18:32] -> Updating the condition setup for the experiment +INFO: [17/02/2026 17:18:32] -> Updating Quantities... +INFO: [17/02/2026 17:18:32] -> Unifying Meta... +INFO: [17/02/2026 17:18:32] -> Normalizing Cscores... +INFO: [17/02/2026 17:18:33] -> Calculating Global CVs +INFO: [17/02/2026 17:18:36] -> Calculating Profile Qvalues... +INFO: [17/02/2026 17:18:40] -> Linking References... +INFO: [17/02/2026 17:18:41] -> Initializing Protein Groups... +INFO: [17/02/2026 17:21:12] -> Building Protein Groups... +INFO: [17/02/2026 17:21:18] -> Annotating In-Source Fragmentation... +INFO: [17/02/2026 17:21:18] -> Calculating Run-Wise Protein Group FDR... +INFO: [17/02/2026 17:21:27] -> Annotating Protein Single Hits... +INFO: [17/02/2026 17:21:30] -> Normalizing Quantification... +INFO: [17/02/2026 17:21:30] -> Calculating Global CVs +INFO: [17/02/2026 17:21:32] -> Updating Identification Counts... +INFO: [17/02/2026 17:21:36] -> Subtracting background noise... +INFO: [17/02/2026 17:21:36] -> Resetting existing imputation... +INFO: [17/02/2026 17:21:37] -> Calculating Protein Quantities... +INFO: [17/02/2026 17:21:41] -> Calculating Protein Quantities... +INFO: [17/02/2026 17:21:41] -> Calculating MaxLFQ Protein Quantities... +INFO: [17/02/2026 17:21:58] -> Collapsing PTM-Locations... +INFO: [17/02/2026 17:21:58] -> PTM Stoichiometry calculation... +INFO: [17/02/2026 17:21:58] -> Calculating Condition CVs +INFO: [17/02/2026 17:22:15] -> Creating Protein Map... +INFO: [17/02/2026 17:22:15] -> Compiling Run Summary Information... +INFO: [17/02/2026 17:22:19] -> Running Post Analysis Processes... +INFO: [17/02/2026 17:23:33] -> Writing Summary Information... +INFO: [17/02/2026 17:23:33] -> Unique precursors: 208’784 of 228’573 | modified peptides: 177’843 of 191’699 | peptides: 176’406 of 190’204 | protein groups: 13’482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [17/02/2026 17:23:33] -> Unique precursors: 208’083 of 228’573 | modified peptides: 177’257 of 191’699 | peptides: 175’827 of 190’204 | protein groups: 13’497 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [17/02/2026 17:23:33] -> Unique precursors: 208’542 of 228’573 | modified peptides: 177’430 of 191’699 | peptides: 175’998 of 190’204 | protein groups: 13’486 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [17/02/2026 17:23:33] -> Unique precursors: 210’919 of 228’573 | modified peptides: 178’760 of 191’699 | peptides: 177’323 of 190’204 | protein groups: 13’473 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [17/02/2026 17:23:33] -> Unique precursors: 210’065 of 228’573 | modified peptides: 178’425 of 191’699 | peptides: 176’992 of 190’204 | protein groups: 13’460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [17/02/2026 17:23:33] -> Unique precursors: 210’415 of 228’573 | modified peptides: 178’654 of 191’699 | peptides: 177’218 of 190’204 | protein groups: 13’477 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [17/02/2026 17:23:33] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [17/02/2026 17:23:33] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [17/02/2026 17:23:33] -> Pipeline executed in 5.91m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [17/02/2026 17:25:47] -> Initializing Pipeline... +INFO: [17/02/2026 17:25:47] -> Calculating Qvalues... +INFO: [17/02/2026 17:25:47] -> Saving... +INFO: [17/02/2026 17:29:51] -> Applying settings to loaded experiment... +INFO: [17/02/2026 17:29:51] -> Updating the condition setup for the experiment +INFO: [17/02/2026 17:29:51] -> Updating Quantities... +INFO: [17/02/2026 17:29:51] -> Unifying Meta... +INFO: [17/02/2026 17:29:51] -> Normalizing Cscores... +INFO: [17/02/2026 17:29:52] -> Calculating Global CVs +INFO: [17/02/2026 17:29:55] -> Calculating Profile Qvalues... +INFO: [17/02/2026 17:29:59] -> Linking References... +INFO: [17/02/2026 17:30:00] -> Initializing Protein Groups... +INFO: [17/02/2026 17:31:49] -> Building Protein Groups... +INFO: [17/02/2026 17:31:53] -> Annotating In-Source Fragmentation... +INFO: [17/02/2026 17:31:53] -> Calculating Run-Wise Protein Group FDR... +INFO: [17/02/2026 17:32:02] -> Annotating Protein Single Hits... +INFO: [17/02/2026 17:32:04] -> Normalizing Quantification... +INFO: [17/02/2026 17:32:04] -> Calculating Global CVs +INFO: [17/02/2026 17:32:06] -> Updating Identification Counts... +INFO: [17/02/2026 17:32:17] -> Subtracting background noise... +INFO: [17/02/2026 17:32:17] -> Resetting existing imputation... +INFO: [17/02/2026 17:32:17] -> Calculating Protein Quantities... +INFO: [17/02/2026 17:32:20] -> Calculating Protein Quantities... +INFO: [17/02/2026 17:32:21] -> Calculating MaxLFQ Protein Quantities... +INFO: [17/02/2026 17:32:38] -> Collapsing PTM-Locations... +INFO: [17/02/2026 17:32:38] -> PTM Stoichiometry calculation... +INFO: [17/02/2026 17:32:38] -> Calculating Condition CVs +INFO: [17/02/2026 17:32:45] -> Creating Protein Map... +INFO: [17/02/2026 17:32:45] -> Compiling Run Summary Information... +INFO: [17/02/2026 17:32:46] -> Running Post Analysis Processes... +INFO: [17/02/2026 17:33:57] -> Writing Summary Information... +INFO: [17/02/2026 17:33:57] -> Unique precursors: 208’784 of 228’573 | modified peptides: 177’843 of 191’699 | peptides: 176’406 of 190’204 | protein groups: 13’482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [17/02/2026 17:33:57] -> Unique precursors: 208’083 of 228’573 | modified peptides: 177’257 of 191’699 | peptides: 175’827 of 190’204 | protein groups: 13’497 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [17/02/2026 17:33:57] -> Unique precursors: 208’542 of 228’573 | modified peptides: 177’430 of 191’699 | peptides: 175’998 of 190’204 | protein groups: 13’486 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [17/02/2026 17:33:58] -> Unique precursors: 210’919 of 228’573 | modified peptides: 178’760 of 191’699 | peptides: 177’323 of 190’204 | protein groups: 13’473 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [17/02/2026 17:33:58] -> Unique precursors: 210’065 of 228’573 | modified peptides: 178’425 of 191’699 | peptides: 176’992 of 190’204 | protein groups: 13’460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [17/02/2026 17:33:58] -> Unique precursors: 210’415 of 228’573 | modified peptides: 178’654 of 191’699 | peptides: 177’218 of 190’204 | protein groups: 13’477 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [17/02/2026 17:33:58] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [17/02/2026 17:34:01] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [17/02/2026 17:34:01] -> Pipeline executed in 5.02m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [17/02/2026 17:40:23] -> Initializing Pipeline... +INFO: [17/02/2026 17:40:24] -> Calculating Global CVs +INFO: [17/02/2026 17:40:26] -> Calculating Profile Qvalues... +INFO: [17/02/2026 17:40:31] -> Initializing Protein Groups... +INFO: [17/02/2026 17:41:28] -> Building Protein Groups... +INFO: [17/02/2026 17:42:19] -> Updating Quantities... +INFO: [17/02/2026 17:42:36] -> Subtracting background noise... +INFO: [17/02/2026 17:42:36] -> Calculating Precursor Quantities... +INFO: [17/02/2026 17:42:36] -> Normalizing Quantification... +INFO: [17/02/2026 17:42:36] -> Cross-run normalization disabled! +INFO: [17/02/2026 17:42:36] -> Resetting existing imputation... +INFO: [17/02/2026 17:42:36] -> Calculating Run-Wise Protein Group FDR... +INFO: [17/02/2026 17:42:49] -> Calculating Protein Quantities... +INFO: [17/02/2026 17:42:53] -> Calculating Protein Quantities... +INFO: [17/02/2026 17:42:53] -> Calculating MaxLFQ Protein Quantities... +INFO: [17/02/2026 17:43:10] -> Collapsing PTM-Locations... +INFO: [17/02/2026 17:43:10] -> PTM Stoichiometry calculation... +INFO: [17/02/2026 17:43:11] -> Annotating Protein Single Hits... +INFO: [17/02/2026 17:43:14] -> Calculating Global CVs +INFO: [17/02/2026 17:43:16] -> Updating Identification Counts... +INFO: [17/02/2026 17:43:36] -> Calculating Condition CVs +INFO: [17/02/2026 17:43:43] -> Creating Protein Map... +INFO: [17/02/2026 17:43:43] -> Running Post Analysis Processes... +INFO: [17/02/2026 17:44:52] -> Writing Summary Information... +INFO: [17/02/2026 17:44:52] -> Unique precursors: 210’596 of 228’573 | modified peptides: 179’152 of 191’699 | peptides: 177’709 of 190’204 | protein groups: 13’482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [17/02/2026 17:44:52] -> Unique precursors: 210’366 of 228’573 | modified peptides: 178’962 of 191’699 | peptides: 177’526 of 190’204 | protein groups: 13’498 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [17/02/2026 17:44:52] -> Unique precursors: 210’496 of 228’573 | modified peptides: 178’894 of 191’699 | peptides: 177’453 of 190’204 | protein groups: 13’488 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [17/02/2026 17:44:52] -> Unique precursors: 212’856 of 228’573 | modified peptides: 180’338 of 191’699 | peptides: 178’899 of 190’204 | protein groups: 13’477 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [17/02/2026 17:44:52] -> Unique precursors: 211’901 of 228’573 | modified peptides: 179’833 of 191’699 | peptides: 178’395 of 190’204 | protein groups: 13’460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [17/02/2026 17:44:52] -> Unique precursors: 212’501 of 228’573 | modified peptides: 180’261 of 191’699 | peptides: 178’820 of 190’204 | protein groups: 13’478 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [17/02/2026 17:44:52] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [17/02/2026 17:44:56] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [17/02/2026 17:44:56] -> Pipeline executed in 4.55m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 08:51:52] -> Initializing Pipeline... +INFO: [19/02/2026 08:51:52] -> Calculating Global CVs +INFO: [19/02/2026 08:51:54] -> Calculating Profile Qvalues... +INFO: [19/02/2026 08:51:57] -> Updating Quantities... +INFO: [19/02/2026 08:52:19] -> Subtracting background noise... +INFO: [19/02/2026 08:52:19] -> Calculating Precursor Quantities... +INFO: [19/02/2026 08:52:19] -> Normalizing Quantification... +INFO: [19/02/2026 08:52:19] -> Cross-run normalization disabled! +INFO: [19/02/2026 08:52:19] -> Resetting existing imputation... +INFO: [19/02/2026 08:52:19] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 08:52:29] -> Calculating Protein Quantities... +INFO: [19/02/2026 08:52:32] -> Calculating Protein Quantities... +INFO: [19/02/2026 08:52:32] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 08:52:46] -> Collapsing PTM-Locations... +INFO: [19/02/2026 08:52:46] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 08:52:46] -> Annotating Protein Single Hits... +INFO: [19/02/2026 08:52:48] -> Calculating Global CVs +INFO: [19/02/2026 08:52:50] -> Updating Identification Counts... +INFO: [19/02/2026 08:53:04] -> Calculating Condition CVs +INFO: [19/02/2026 08:53:11] -> Creating Protein Map... +INFO: [19/02/2026 08:53:11] -> Running Post Analysis Processes... +INFO: [19/02/2026 08:53:54] -> Writing Summary Information... +INFO: [19/02/2026 08:53:54] -> Unique precursors: 210’600 of 228’573 | modified peptides: 179’156 of 191’699 | peptides: 177’713 of 190’204 | protein groups: 13’484 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 08:53:54] -> Unique precursors: 210’388 of 228’573 | modified peptides: 178’984 of 191’699 | peptides: 177’548 of 190’204 | protein groups: 13’516 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 08:53:54] -> Unique precursors: 210’510 of 228’573 | modified peptides: 178’908 of 191’699 | peptides: 177’467 of 190’204 | protein groups: 13’498 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 08:53:54] -> Unique precursors: 212’884 of 228’573 | modified peptides: 180’366 of 191’699 | peptides: 178’927 of 190’204 | protein groups: 13’494 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 08:53:54] -> Unique precursors: 211’931 of 228’573 | modified peptides: 179’863 of 191’699 | peptides: 178’425 of 190’204 | protein groups: 13’478 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 08:53:54] -> Unique precursors: 212’534 of 228’573 | modified peptides: 180’294 of 191’699 | peptides: 178’853 of 190’204 | protein groups: 13’500 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 08:53:54] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 08:53:57] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 08:53:57] -> Pipeline executed in 2.09m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 11:38:08] -> Initializing Pipeline... +INFO: [19/02/2026 11:38:09] -> Calculating Global CVs +INFO: [19/02/2026 11:38:11] -> Calculating Profile Qvalues... +INFO: [19/02/2026 11:38:14] -> Initializing Protein Groups... +INFO: [19/02/2026 11:38:52] -> Building Protein Groups... +INFO: [19/02/2026 11:39:27] -> Updating Quantities... +INFO: [19/02/2026 11:39:52] -> Subtracting background noise... +INFO: [19/02/2026 11:39:52] -> Calculating Precursor Quantities... +INFO: [19/02/2026 11:39:52] -> Normalizing Quantification... +INFO: [19/02/2026 11:39:52] -> Cross-run normalization disabled! +INFO: [19/02/2026 11:39:52] -> Resetting existing imputation... +INFO: [19/02/2026 11:39:52] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 11:40:02] -> Calculating Protein Quantities... +INFO: [19/02/2026 11:40:04] -> Calculating Protein Quantities... +INFO: [19/02/2026 11:40:04] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 11:40:18] -> Collapsing PTM-Locations... +INFO: [19/02/2026 11:40:18] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 11:40:18] -> Annotating Protein Single Hits... +INFO: [19/02/2026 11:40:20] -> Calculating Global CVs +INFO: [19/02/2026 11:40:22] -> Updating Identification Counts... +INFO: [19/02/2026 11:40:36] -> Calculating Condition CVs +INFO: [19/02/2026 11:40:42] -> Creating Protein Map... +INFO: [19/02/2026 11:40:42] -> Running Post Analysis Processes... +INFO: [19/02/2026 11:41:25] -> Writing Summary Information... +INFO: [19/02/2026 11:41:25] -> Unique precursors: 208’784 of 228’573 | modified peptides: 177’843 of 191’699 | peptides: 176’406 of 190’204 | protein groups: 13’482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 11:41:25] -> Unique precursors: 208’083 of 228’573 | modified peptides: 177’257 of 191’699 | peptides: 175’827 of 190’204 | protein groups: 13’497 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 11:41:25] -> Unique precursors: 208’542 of 228’573 | modified peptides: 177’430 of 191’699 | peptides: 175’998 of 190’204 | protein groups: 13’486 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 11:41:25] -> Unique precursors: 210’919 of 228’573 | modified peptides: 178’760 of 191’699 | peptides: 177’323 of 190’204 | protein groups: 13’473 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 11:41:25] -> Unique precursors: 210’065 of 228’573 | modified peptides: 178’425 of 191’699 | peptides: 176’992 of 190’204 | protein groups: 13’460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 11:41:25] -> Unique precursors: 210’415 of 228’573 | modified peptides: 178’654 of 191’699 | peptides: 177’218 of 190’204 | protein groups: 13’477 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 11:41:25] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 11:41:28] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 11:41:28] -> Pipeline executed in 3.32m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 11:45:50] -> Initializing Pipeline... +INFO: [19/02/2026 11:45:50] -> Calculating Global CVs +INFO: [19/02/2026 11:45:52] -> Calculating Profile Qvalues... +INFO: [19/02/2026 11:45:55] -> Updating Quantities... +INFO: [19/02/2026 11:46:18] -> Subtracting background noise... +INFO: [19/02/2026 11:46:18] -> Calculating Precursor Quantities... +INFO: [19/02/2026 11:46:18] -> Normalizing Quantification... +INFO: [19/02/2026 11:46:18] -> Cross-run normalization disabled! +INFO: [19/02/2026 11:46:18] -> Resetting existing imputation... +INFO: [19/02/2026 11:46:18] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 11:46:31] -> Calculating Protein Quantities... +INFO: [19/02/2026 11:46:33] -> Calculating Protein Quantities... +INFO: [19/02/2026 11:46:33] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 11:46:48] -> Collapsing PTM-Locations... +INFO: [19/02/2026 11:46:48] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 11:46:48] -> Annotating Protein Single Hits... +INFO: [19/02/2026 11:46:50] -> Calculating Global CVs +INFO: [19/02/2026 11:46:52] -> Updating Identification Counts... +INFO: [19/02/2026 11:47:07] -> Calculating Condition CVs +INFO: [19/02/2026 11:47:13] -> Creating Protein Map... +INFO: [19/02/2026 11:47:13] -> Running Post Analysis Processes... +INFO: [19/02/2026 11:47:59] -> Writing Summary Information... +INFO: [19/02/2026 11:47:59] -> Unique precursors: 208’784 of 228’573 | modified peptides: 177’843 of 191’699 | peptides: 176’406 of 190’204 | protein groups: 13’482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 11:47:59] -> Unique precursors: 208’083 of 228’573 | modified peptides: 177’257 of 191’699 | peptides: 175’827 of 190’204 | protein groups: 13’497 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 11:47:59] -> Unique precursors: 208’542 of 228’573 | modified peptides: 177’430 of 191’699 | peptides: 175’998 of 190’204 | protein groups: 13’486 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 11:47:59] -> Unique precursors: 210’919 of 228’573 | modified peptides: 178’760 of 191’699 | peptides: 177’323 of 190’204 | protein groups: 13’473 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 11:47:59] -> Unique precursors: 210’065 of 228’573 | modified peptides: 178’425 of 191’699 | peptides: 176’992 of 190’204 | protein groups: 13’460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 11:47:59] -> Unique precursors: 210’415 of 228’573 | modified peptides: 178’654 of 191’699 | peptides: 177’218 of 190’204 | protein groups: 13’477 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 11:47:59] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 11:48:01] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 11:48:01] -> Pipeline executed in 2.2m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 11:51:52] -> Initializing Pipeline... +INFO: [19/02/2026 11:51:53] -> Calculating Global CVs +INFO: [19/02/2026 11:51:55] -> Calculating Profile Qvalues... +INFO: [19/02/2026 11:51:58] -> Updating Quantities... +INFO: [19/02/2026 11:52:18] -> Subtracting background noise... +INFO: [19/02/2026 11:52:18] -> Calculating Precursor Quantities... +INFO: [19/02/2026 11:52:19] -> Normalizing Quantification... +INFO: [19/02/2026 11:52:19] -> Cross-run normalization disabled! +INFO: [19/02/2026 11:52:19] -> Resetting existing imputation... +INFO: [19/02/2026 11:52:19] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 11:52:29] -> Calculating Protein Quantities... +INFO: [19/02/2026 11:52:32] -> Calculating Protein Quantities... +INFO: [19/02/2026 11:52:32] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 11:52:46] -> Collapsing PTM-Locations... +INFO: [19/02/2026 11:52:46] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 11:52:46] -> Annotating Protein Single Hits... +INFO: [19/02/2026 11:52:48] -> Calculating Global CVs +INFO: [19/02/2026 11:52:49] -> Updating Identification Counts... +INFO: [19/02/2026 11:53:04] -> Calculating Condition CVs +INFO: [19/02/2026 11:53:11] -> Creating Protein Map... +INFO: [19/02/2026 11:53:11] -> Running Post Analysis Processes... +INFO: [19/02/2026 11:53:58] -> Writing Summary Information... +INFO: [19/02/2026 11:53:58] -> Unique precursors: 208’784 of 228’573 | modified peptides: 177’843 of 191’699 | peptides: 176’406 of 190’204 | protein groups: 13’482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 11:53:58] -> Unique precursors: 208’083 of 228’573 | modified peptides: 177’257 of 191’699 | peptides: 175’827 of 190’204 | protein groups: 13’497 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 11:53:58] -> Unique precursors: 208’542 of 228’573 | modified peptides: 177’430 of 191’699 | peptides: 175’998 of 190’204 | protein groups: 13’486 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 11:53:58] -> Unique precursors: 210’919 of 228’573 | modified peptides: 178’760 of 191’699 | peptides: 177’323 of 190’204 | protein groups: 13’473 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 11:53:58] -> Unique precursors: 210’065 of 228’573 | modified peptides: 178’425 of 191’699 | peptides: 176’992 of 190’204 | protein groups: 13’460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 11:53:58] -> Unique precursors: 210’415 of 228’573 | modified peptides: 178’654 of 191’699 | peptides: 177’218 of 190’204 | protein groups: 13’477 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 11:53:58] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 11:54:01] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 11:54:01] -> Pipeline executed in 2.14m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 13:30:42] -> Initializing Pipeline... +INFO: [19/02/2026 13:30:42] -> Calculating Global CVs +INFO: [19/02/2026 13:30:44] -> Calculating Profile Qvalues... +INFO: [19/02/2026 13:30:48] -> Updating Quantities... +INFO: [19/02/2026 13:31:11] -> Subtracting background noise... +INFO: [19/02/2026 13:31:11] -> Calculating Precursor Quantities... +INFO: [19/02/2026 13:31:11] -> Normalizing Quantification... +INFO: [19/02/2026 13:31:11] -> Cross-run normalization disabled! +INFO: [19/02/2026 13:31:11] -> Resetting existing imputation... +INFO: [19/02/2026 13:31:11] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 13:31:21] -> Calculating Protein Quantities... +INFO: [19/02/2026 13:31:24] -> Calculating Protein Quantities... +INFO: [19/02/2026 13:31:24] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 13:31:38] -> Collapsing PTM-Locations... +INFO: [19/02/2026 13:31:38] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 13:31:38] -> Annotating Protein Single Hits... +INFO: [19/02/2026 13:31:40] -> Calculating Global CVs +INFO: [19/02/2026 13:31:42] -> Updating Identification Counts... +INFO: [19/02/2026 13:31:48] -> Calculating Condition CVs +INFO: [19/02/2026 13:31:54] -> Creating Protein Map... +INFO: [19/02/2026 13:31:54] -> Running Post Analysis Processes... +INFO: [19/02/2026 13:32:40] -> Writing Summary Information... +INFO: [19/02/2026 13:32:40] -> Unique precursors: 208’784 of 228’573 | modified peptides: 177’843 of 191’699 | peptides: 176’406 of 190’204 | protein groups: 13’482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 13:32:40] -> Unique precursors: 208’083 of 228’573 | modified peptides: 177’257 of 191’699 | peptides: 175’827 of 190’204 | protein groups: 13’497 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 13:32:40] -> Unique precursors: 208’542 of 228’573 | modified peptides: 177’430 of 191’699 | peptides: 175’998 of 190’204 | protein groups: 13’486 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 13:32:40] -> Unique precursors: 210’919 of 228’573 | modified peptides: 178’760 of 191’699 | peptides: 177’323 of 190’204 | protein groups: 13’473 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 13:32:40] -> Unique precursors: 210’065 of 228’573 | modified peptides: 178’425 of 191’699 | peptides: 176’992 of 190’204 | protein groups: 13’460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 13:32:40] -> Unique precursors: 210’415 of 228’573 | modified peptides: 178’654 of 191’699 | peptides: 177’218 of 190’204 | protein groups: 13’477 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 13:32:40] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 13:32:44] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 13:32:44] -> Pipeline executed in 2.04m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 13:44:41] -> Initializing Pipeline... +INFO: [19/02/2026 13:44:42] -> Calculating Global CVs +INFO: [19/02/2026 13:44:44] -> Calculating Profile Qvalues... +INFO: [19/02/2026 13:44:47] -> Updating Quantities... +INFO: [19/02/2026 13:45:16] -> Subtracting background noise... +INFO: [19/02/2026 13:45:16] -> Calculating Precursor Quantities... +INFO: [19/02/2026 13:45:16] -> Normalizing Quantification... +INFO: [19/02/2026 13:45:16] -> Cross-run normalization disabled! +INFO: [19/02/2026 13:45:16] -> Resetting existing imputation... +INFO: [19/02/2026 13:45:16] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 13:45:26] -> Calculating Protein Quantities... +INFO: [19/02/2026 13:45:29] -> Calculating Protein Quantities... +INFO: [19/02/2026 13:45:29] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 13:45:43] -> Collapsing PTM-Locations... +INFO: [19/02/2026 13:45:43] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 13:45:43] -> Annotating Protein Single Hits... +INFO: [19/02/2026 13:45:45] -> Calculating Global CVs +INFO: [19/02/2026 13:45:47] -> Updating Identification Counts... +INFO: [19/02/2026 13:45:54] -> Calculating Condition CVs +INFO: [19/02/2026 13:46:01] -> Creating Protein Map... +INFO: [19/02/2026 13:46:01] -> Running Post Analysis Processes... +INFO: [19/02/2026 13:46:44] -> Writing Summary Information... +INFO: [19/02/2026 13:46:44] -> Unique precursors: 208’788 of 228’573 | modified peptides: 177’847 of 191’699 | peptides: 176’410 of 190’204 | protein groups: 13’484 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 13:46:44] -> Unique precursors: 208’104 of 228’573 | modified peptides: 177’278 of 191’699 | peptides: 175’848 of 190’204 | protein groups: 13’515 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 13:46:44] -> Unique precursors: 208’555 of 228’573 | modified peptides: 177’443 of 191’699 | peptides: 176’011 of 190’204 | protein groups: 13’496 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 13:46:44] -> Unique precursors: 210’944 of 228’573 | modified peptides: 178’785 of 191’699 | peptides: 177’348 of 190’204 | protein groups: 13’490 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 13:46:44] -> Unique precursors: 210’092 of 228’573 | modified peptides: 178’452 of 191’699 | peptides: 177’019 of 190’204 | protein groups: 13’478 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 13:46:44] -> Unique precursors: 210’447 of 228’573 | modified peptides: 178’686 of 191’699 | peptides: 177’250 of 190’204 | protein groups: 13’499 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 13:46:44] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 13:46:48] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 13:46:48] -> Pipeline executed in 2.12m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 13:50:54] -> Initializing Pipeline... +INFO: [19/02/2026 13:50:55] -> Updating Quantities... +INFO: [19/02/2026 13:51:18] -> Subtracting background noise... +INFO: [19/02/2026 13:51:18] -> Calculating Precursor Quantities... +INFO: [19/02/2026 13:51:18] -> Normalizing Quantification... +INFO: [19/02/2026 13:51:18] -> Cross-run normalization disabled! +INFO: [19/02/2026 13:51:18] -> Resetting existing imputation... +INFO: [19/02/2026 13:51:18] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 13:51:28] -> Calculating Protein Quantities... +INFO: [19/02/2026 13:51:30] -> Calculating Protein Quantities... +INFO: [19/02/2026 13:51:30] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 13:51:44] -> Collapsing PTM-Locations... +INFO: [19/02/2026 13:51:44] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 13:51:45] -> Calculating Global CVs +INFO: [19/02/2026 13:51:46] -> Updating Identification Counts... +INFO: [19/02/2026 13:52:00] -> Calculating Condition CVs +INFO: [19/02/2026 13:52:06] -> Creating Protein Map... +INFO: [19/02/2026 13:52:06] -> Running Post Analysis Processes... +INFO: [19/02/2026 13:52:52] -> Writing Summary Information... +INFO: [19/02/2026 13:52:52] -> Unique precursors: 208’788 of 228’573 | modified peptides: 177’847 of 191’699 | peptides: 176’410 of 190’204 | protein groups: 13’484 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 13:52:52] -> Unique precursors: 208’104 of 228’573 | modified peptides: 177’278 of 191’699 | peptides: 175’848 of 190’204 | protein groups: 13’515 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 13:52:52] -> Unique precursors: 208’555 of 228’573 | modified peptides: 177’443 of 191’699 | peptides: 176’011 of 190’204 | protein groups: 13’496 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 13:52:52] -> Unique precursors: 210’944 of 228’573 | modified peptides: 178’785 of 191’699 | peptides: 177’348 of 190’204 | protein groups: 13’490 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 13:52:52] -> Unique precursors: 210’092 of 228’573 | modified peptides: 178’452 of 191’699 | peptides: 177’019 of 190’204 | protein groups: 13’478 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 13:52:52] -> Unique precursors: 210’447 of 228’573 | modified peptides: 178’686 of 191’699 | peptides: 177’250 of 190’204 | protein groups: 13’499 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 13:52:52] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 13:52:56] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 13:52:56] -> Pipeline executed in 2.02m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 14:31:26] -> Initializing Pipeline... +INFO: [19/02/2026 14:31:26] -> Updating Quantities... +INFO: [19/02/2026 14:31:49] -> Subtracting background noise... +INFO: [19/02/2026 14:31:49] -> Calculating Precursor Quantities... +INFO: [19/02/2026 14:31:49] -> Normalizing Quantification... +INFO: [19/02/2026 14:31:49] -> Cross-run normalization disabled! +INFO: [19/02/2026 14:31:49] -> Resetting existing imputation... +INFO: [19/02/2026 14:31:49] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 14:31:59] -> Calculating Protein Quantities... +INFO: [19/02/2026 14:32:02] -> Calculating Protein Quantities... +INFO: [19/02/2026 14:32:02] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 14:32:15] -> Collapsing PTM-Locations... +INFO: [19/02/2026 14:32:15] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 14:32:16] -> Calculating Global CVs +INFO: [19/02/2026 14:32:17] -> Updating Identification Counts... +INFO: [19/02/2026 14:32:23] -> Calculating Condition CVs +INFO: [19/02/2026 14:32:30] -> Creating Protein Map... +INFO: [19/02/2026 14:32:30] -> Running Post Analysis Processes... +INFO: [19/02/2026 14:33:14] -> Writing Summary Information... +INFO: [19/02/2026 14:33:14] -> Unique precursors: 208’788 of 228’573 | modified peptides: 177’847 of 191’699 | peptides: 176’410 of 190’204 | protein groups: 13’484 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 14:33:14] -> Unique precursors: 208’104 of 228’573 | modified peptides: 177’278 of 191’699 | peptides: 175’848 of 190’204 | protein groups: 13’515 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 14:33:14] -> Unique precursors: 208’555 of 228’573 | modified peptides: 177’443 of 191’699 | peptides: 176’011 of 190’204 | protein groups: 13’496 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 14:33:14] -> Unique precursors: 210’944 of 228’573 | modified peptides: 178’785 of 191’699 | peptides: 177’348 of 190’204 | protein groups: 13’490 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 14:33:14] -> Unique precursors: 210’092 of 228’573 | modified peptides: 178’452 of 191’699 | peptides: 177’019 of 190’204 | protein groups: 13’478 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 14:33:14] -> Unique precursors: 210’447 of 228’573 | modified peptides: 178’686 of 191’699 | peptides: 177’250 of 190’204 | protein groups: 13’499 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 14:33:14] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 14:33:18] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 14:33:18] -> Pipeline executed in 1.87m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 14:59:09] -> Initializing Pipeline... +INFO: [19/02/2026 14:59:09] -> Calculating Global CVs +INFO: [19/02/2026 14:59:11] -> Calculating Profile Qvalues... +INFO: [19/02/2026 14:59:14] -> Updating Quantities... +INFO: [19/02/2026 14:59:43] -> Subtracting background noise... +INFO: [19/02/2026 14:59:43] -> Calculating Precursor Quantities... +INFO: [19/02/2026 14:59:43] -> Normalizing Quantification... +INFO: [19/02/2026 14:59:43] -> Cross-run normalization disabled! +INFO: [19/02/2026 14:59:43] -> Resetting existing imputation... +INFO: [19/02/2026 14:59:44] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 14:59:54] -> Calculating Protein Quantities... +INFO: [19/02/2026 14:59:56] -> Calculating Protein Quantities... +INFO: [19/02/2026 14:59:56] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 15:00:10] -> Collapsing PTM-Locations... +INFO: [19/02/2026 15:00:10] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 15:00:10] -> Annotating Protein Single Hits... +INFO: [19/02/2026 15:00:13] -> Calculating Global CVs +INFO: [19/02/2026 15:00:14] -> Updating Identification Counts... +INFO: [19/02/2026 15:00:30] -> Calculating Condition CVs +INFO: [19/02/2026 15:00:36] -> Creating Protein Map... +INFO: [19/02/2026 15:00:36] -> Running Post Analysis Processes... +INFO: [19/02/2026 15:01:19] -> Writing Summary Information... +INFO: [19/02/2026 15:01:19] -> Unique precursors: 208’784 of 228’573 | modified peptides: 177’843 of 191’699 | peptides: 176’406 of 190’204 | protein groups: 13’482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 15:01:19] -> Unique precursors: 208’083 of 228’573 | modified peptides: 177’257 of 191’699 | peptides: 175’827 of 190’204 | protein groups: 13’497 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 15:01:19] -> Unique precursors: 208’542 of 228’573 | modified peptides: 177’430 of 191’699 | peptides: 175’998 of 190’204 | protein groups: 13’486 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 15:01:19] -> Unique precursors: 210’919 of 228’573 | modified peptides: 178’760 of 191’699 | peptides: 177’323 of 190’204 | protein groups: 13’473 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 15:01:19] -> Unique precursors: 210’065 of 228’573 | modified peptides: 178’425 of 191’699 | peptides: 176’992 of 190’204 | protein groups: 13’460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 15:01:19] -> Unique precursors: 210’415 of 228’573 | modified peptides: 178’654 of 191’699 | peptides: 177’218 of 190’204 | protein groups: 13’477 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 15:01:19] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 15:01:23] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 15:01:23] -> Pipeline executed in 2.24m - Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 15:04:05] -> Initializing Pipeline... +INFO: [19/02/2026 15:04:05] -> Calculating Global CVs +INFO: [19/02/2026 15:04:07] -> Calculating Profile Qvalues... +INFO: [19/02/2026 15:04:10] -> Updating Quantities... +INFO: [19/02/2026 15:04:31] -> Subtracting background noise... +INFO: [19/02/2026 15:04:31] -> Calculating Precursor Quantities... +INFO: [19/02/2026 15:04:31] -> Normalizing Quantification... +INFO: [19/02/2026 15:04:31] -> Cross-run normalization disabled! +INFO: [19/02/2026 15:04:31] -> Resetting existing imputation... +INFO: [19/02/2026 15:04:31] -> Calculating Run-Wise Protein Group FDR... +INFO: [19/02/2026 15:04:41] -> Calculating Protein Quantities... +INFO: [19/02/2026 15:04:44] -> Calculating Protein Quantities... +INFO: [19/02/2026 15:04:44] -> Calculating MaxLFQ Protein Quantities... +INFO: [19/02/2026 15:04:58] -> Collapsing PTM-Locations... +INFO: [19/02/2026 15:04:58] -> PTM Stoichiometry calculation... +INFO: [19/02/2026 15:04:58] -> Annotating Protein Single Hits... +INFO: [19/02/2026 15:05:00] -> Calculating Global CVs +INFO: [19/02/2026 15:05:02] -> Updating Identification Counts... +INFO: [19/02/2026 15:05:17] -> Calculating Condition CVs +INFO: [19/02/2026 15:05:23] -> Creating Protein Map... +INFO: [19/02/2026 15:05:23] -> Running Post Analysis Processes... +INFO: [19/02/2026 15:06:10] -> Writing Summary Information... +INFO: [19/02/2026 15:06:10] -> Unique precursors: 208’784 of 228’573 | modified peptides: 177’843 of 191’699 | peptides: 176’406 of 190’204 | protein groups: 13’482 (Qvalue <= 0.01) for run #1 "LFQ_24min_500ng_2Th_3ms_A_01.raw" +INFO: [19/02/2026 15:06:10] -> Unique precursors: 208’083 of 228’573 | modified peptides: 177’257 of 191’699 | peptides: 175’827 of 190’204 | protein groups: 13’497 (Qvalue <= 0.01) for run #2 "LFQ_24min_500ng_2Th_3ms_A_02.raw" +INFO: [19/02/2026 15:06:10] -> Unique precursors: 208’542 of 228’573 | modified peptides: 177’430 of 191’699 | peptides: 175’998 of 190’204 | protein groups: 13’486 (Qvalue <= 0.01) for run #3 "LFQ_24min_500ng_2Th_3ms_A_03.raw" +INFO: [19/02/2026 15:06:10] -> Unique precursors: 210’919 of 228’573 | modified peptides: 178’760 of 191’699 | peptides: 177’323 of 190’204 | protein groups: 13’473 (Qvalue <= 0.01) for run #4 "LFQ_24min_500ng_2Th_3ms_B_01.raw" +INFO: [19/02/2026 15:06:10] -> Unique precursors: 210’065 of 228’573 | modified peptides: 178’425 of 191’699 | peptides: 176’992 of 190’204 | protein groups: 13’460 (Qvalue <= 0.01) for run #5 "LFQ_24min_500ng_2Th_3ms_B_02.raw" +INFO: [19/02/2026 15:06:10] -> Unique precursors: 210’415 of 228’573 | modified peptides: 178’654 of 191’699 | peptides: 177’218 of 190’204 | protein groups: 13’477 (Qvalue <= 0.01) for run #6 "LFQ_24min_500ng_2Th_3ms_B_03.raw" +INFO: [19/02/2026 15:06:10] -> Unique precursors: 215’205 of 228’573 | modified peptides: 182’186 of 191’699 | peptides: 180’733 of 190’204 | protein groups: 13’560 (Qvalue <= 0.01) for whole experiment "4438_3ProteomeMix_Astral_SN20_4" + +INFO: [19/02/2026 15:06:13] -> Current Experiment had 0 Warnings and 0 Errors. +INFO: [19/02/2026 15:06:13] -> Pipeline executed in 2.15m - Current Experiment had 0 Warnings and 0 Errors. +[END-LOG] diff --git a/inst/extdata/spectronaut_example/SN_unit_test_subset.tsv b/inst/extdata/spectronaut_example/SN_unit_test_subset.tsv new file mode 100644 index 0000000..ee0681d --- /dev/null +++ b/inst/extdata/spectronaut_example/SN_unit_test_subset.tsv @@ -0,0 +1,201 @@ +PG.Qvalue PG.Cscore PG.ProteinGroups PG.Genes PG.ProteinDescriptions PG.NrOfStrippedSequencesIdentified..Experiment.wide. PG.Completeness PG.CV X.1..LFQ_24min_500ng_2Th_3ms_A_01.raw.PG.Cscore..Run.Wise. X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.Cscore..Run.Wise. X.3..LFQ_24min_500ng_2Th_3ms_A_03.raw.PG.Cscore..Run.Wise. X.4..LFQ_24min_500ng_2Th_3ms_B_01.raw.PG.Cscore..Run.Wise. X.5..LFQ_24min_500ng_2Th_3ms_B_02.raw.PG.Cscore..Run.Wise. X.6..LFQ_24min_500ng_2Th_3ms_B_03.raw.PG.Cscore..Run.Wise. X.1..LFQ_24min_500ng_2Th_3ms_A_01.raw.PG.QValue..Run.Wise. X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.QValue..Run.Wise. X.3..LFQ_24min_500ng_2Th_3ms_A_03.raw.PG.QValue..Run.Wise. X.4..LFQ_24min_500ng_2Th_3ms_B_01.raw.PG.QValue..Run.Wise. X.5..LFQ_24min_500ng_2Th_3ms_B_02.raw.PG.QValue..Run.Wise. X.6..LFQ_24min_500ng_2Th_3ms_B_03.raw.PG.QValue..Run.Wise. X.1..LFQ_24min_500ng_2Th_3ms_A_01.raw.PG.RunEvidenceCount X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.RunEvidenceCount X.3..LFQ_24min_500ng_2Th_3ms_A_03.raw.PG.RunEvidenceCount X.4..LFQ_24min_500ng_2Th_3ms_B_01.raw.PG.RunEvidenceCount X.5..LFQ_24min_500ng_2Th_3ms_B_02.raw.PG.RunEvidenceCount X.6..LFQ_24min_500ng_2Th_3ms_B_03.raw.PG.RunEvidenceCount X.1..LFQ_24min_500ng_2Th_3ms_A_01.raw.PG.IsSingleHit X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.IsSingleHit X.3..LFQ_24min_500ng_2Th_3ms_A_03.raw.PG.IsSingleHit X.4..LFQ_24min_500ng_2Th_3ms_B_01.raw.PG.IsSingleHit X.5..LFQ_24min_500ng_2Th_3ms_B_02.raw.PG.IsSingleHit X.6..LFQ_24min_500ng_2Th_3ms_B_03.raw.PG.IsSingleHit X.1..LFQ_24min_500ng_2Th_3ms_A_01.raw.PG.NrOfPrecursorsUsedForQuantification X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.NrOfPrecursorsUsedForQuantification X.3..LFQ_24min_500ng_2Th_3ms_A_03.raw.PG.NrOfPrecursorsUsedForQuantification X.4..LFQ_24min_500ng_2Th_3ms_B_01.raw.PG.NrOfPrecursorsUsedForQuantification X.5..LFQ_24min_500ng_2Th_3ms_B_02.raw.PG.NrOfPrecursorsUsedForQuantification X.6..LFQ_24min_500ng_2Th_3ms_B_03.raw.PG.NrOfPrecursorsUsedForQuantification X.1..LFQ_24min_500ng_2Th_3ms_A_01.raw.PG.Quantity X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.Quantity X.3..LFQ_24min_500ng_2Th_3ms_A_03.raw.PG.Quantity X.4..LFQ_24min_500ng_2Th_3ms_B_01.raw.PG.Quantity X.5..LFQ_24min_500ng_2Th_3ms_B_02.raw.PG.Quantity X.6..LFQ_24min_500ng_2Th_3ms_B_03.raw.PG.Quantity X.1..LFQ_24min_500ng_2Th_3ms_A_01.raw.PG.MS1Quantity X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.MS1Quantity X.3..LFQ_24min_500ng_2Th_3ms_A_03.raw.PG.MS1Quantity X.4..LFQ_24min_500ng_2Th_3ms_B_01.raw.PG.MS1Quantity X.5..LFQ_24min_500ng_2Th_3ms_B_02.raw.PG.MS1Quantity X.6..LFQ_24min_500ng_2Th_3ms_B_03.raw.PG.MS1Quantity X.1..LFQ_24min_500ng_2Th_3ms_A_01.raw.PG.IBAQ X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.IBAQ X.3..LFQ_24min_500ng_2Th_3ms_A_03.raw.PG.IBAQ X.4..LFQ_24min_500ng_2Th_3ms_B_01.raw.PG.IBAQ X.5..LFQ_24min_500ng_2Th_3ms_B_02.raw.PG.IBAQ X.6..LFQ_24min_500ng_2Th_3ms_B_03.raw.PG.IBAQ +1.1327686105987672e-5 6.6319942 P38738 OCA5 Oxidant-induced cell-cycle arrest protein 5 6 100 0.4031501316867918 6.6319942 4.3641124 6.5788507 5.042108 6.233877 6.21233 1.939275444717962e-5 1.7429758714240085e-4 2.8484252234695445e-5 1.2915557909475204e-4 5.88691699965434e-5 4.840532461751548e-5 4 5 5 6 5 6 False False False False False False 4 5 5 5 5 5 496.65643310546875 274.0443115234375 447.5696716308594 921.5775146484375 808.8209228515625 757.10906982421875 165247.75 530953.25 398301.03125 282941.5 366187.84375 466352.28125 45.2 44 62.6 133.4 119.4 127.4 +0 19.623491 P61513 RPL37A 60S ribosomal protein L37a 6 100 0.05424560491582894 19.623491 13.959788 13.323507 7.4273944 8.399146 18.897532000000002 3.353157644116831e-83 9.770296798790272e-51 3.231542505519457e-37 1.1331412120044977e-6 5.896607300492438e-6 8.444477606670243e-152 9 8 9 9 9 9 False False False False False False 8 8 9 8 8 7 95518.3828125 91731.9140625 97559.9375 103888.2109375 90951.2734375 101894.46875 1622660.625 2588721.25 2727701.75 1724313 2533432.75 2634618 92093.898 91045.102 92300.797 95221.398 87680.398 93488.297 +0 22.582043 Q6ZW49 PAXIP1 PAX-interacting protein 1 22 100 0.08659014959779181 13.498377 11.460211 7.725178 6.4951673 15.8968315 22.582043 1.225152984440797e-66 8.912508078567706e-46 8.206722729764626e-6 3.073677139262857e-5 1.8333414960670189e-68 2.68140687405456e-177 20 21 20 22 21 21 False False False False False False 12 12 11 10 9 13 1541.033203125 1276.5418701171875 1448.19140625 1403.119384765625 1358.52099609375 1621.431884765625 376178.5 791278.8125 783357 505300.4375 763879 743628.125 285.9 315.6 289.4 348.1 290.3 317.9 +0 23.025234 P16333 NCK1 Cytoplasmic protein NCK1 21 100 0.04465023053003773 19.171465 13.2194 13.895691 19.446474 12.657031 23.025234 6.258214427213085e-82 3.065230726576039e-49 1.4668982135235885e-47 9.457682347657007e-146 1.738424194787072e-29 1.7073719922372055e-180 22 22 22 23 22 23 False False False False False False 20 18 18 19 19 17 4583.63623046875 4946.19482421875 4779.00048828125 4874.5185546875 4464.123046875 4468.53564453125 103253.0234375 208286.046875 175926.015625 168105.734375 216046.25 207124.65625 1590.3 1717.5 1682.5 1702.5 1534.8 1539.1 +3.9419219827076184e-230 16.147139 P40038 PCL6 PHO85 cyclin-6 13 100 0.3806597609051969 7.404665 8.037031 7.4720874 16.041307 14.838529 16.147139 4.1402602211681096e-7 4.085257345615119e-11 9.815835598697338e-6 5.1692229178455344e-127 3.604901642634623e-61 3.551163748827659e-109 15 16 15 15 15 16 False False False False False False 10 9 7 13 10 13 1598.85009765625 1261.198486328125 1391.38037109375 2983.636474609375 2806.228515625 2913.803466796875 367010.625 625405.375 483676.15625 723044.5 986882.3125 1191445.375 492 408.8 401.2 920.1 836.8 904.8 +0 23.031116 Q9Y4C8 RBM19 Probable RNA-binding protein 19 23 100 0.038497010424555875 19.690264 19.000181 23.031116 21.15752 16.047846 19.859274 2.1745383951150256e-83 1.7732504754173994e-61 3.1792036286030708e-83 1.3083691425358488e-155 7.546715545452872e-69 2.7780064276009344e-158 24 24 24 24 21 23 False False False False False False 18 18 17 18 17 17 3087.5595703125 3327.0947265625 3230.35693359375 3386.784423828125 3117.45068359375 3123.149658203125 159552.984375 270939.15625 264119.21875 261372.03125 318317 360009.03125 604.6 679.3 659.4 673.1 596.8 556.9 +0 19.754065 Q2M389 WASHC4 WASH complex subunit 4 29 100 0.06450890090959742 17.661938 17.678797 15.511544 15.437175 14.965862 19.754065 9.898610987466769e-78 1.387535379082958e-58 6.832561178202413e-63 8.119043472713644e-124 1.9260351487505896e-63 1.4400150381041878e-157 31 31 29 31 31 31 False False False False False False 24 23 21 19 22 21 4124.6337890625 3690.35205078125 3944.508544921875 4424.98876953125 3964.98388671875 4282.39892578125 554614.875 1176032.875 1021452.75 839415.5625 1285735.625 1208109.125 684 659 640.7 706.1 656 668.8 +3.106755094733319e-95 12.5293 Q9NWT8 AURKAIP1 Aurora kinase A-interacting protein 2 100 0.03710500814420888 9.158568 7.6544385 7.709984 7.1366167 6.75706 12.5293 1.5382052679674985e-23 2.2752736608680596e-8 8.28557062505465e-6 6.4274963918435915e-6 3.043383206505967e-5 4.0896138822874314e-27 2 2 2 2 2 2 False False False False False False 2 2 2 2 2 2 14332.783203125 14708.5390625 15256.4775390625 15411.69921875 14730.8896484375 13959.1494140625 141717.328125 509512.3125 381016.75 415724.40625 773183.5625 785932.5625 3480.8 3680.3 3664.8 4106.9 3679.6 3687.4 +2.6552149436199465e-143 14.017916 Q3KQV9 UAP1L1 UDP-N-acetylhexosamine pyrophosphorylase-like protein 1 10 100 0.13478244919554938 7.2226048 5.8790627 14.017916 11.675113 7.190689 6.6716833 1.8036117960994436e-6 4.818927485314571e-5 5.726452158683896e-50 6.51588461233807e-69 1.5901245937633403e-5 2.6896207817223355e-5 8 8 10 9 9 9 False False False False False False 5 5 5 5 5 7 1106.2989501953125 1240.6295166015625 1548.9022216796875 1214.9554443359375 1534.1519775390625 1344.7745361328125 261969.65625 429041.71875 500730.1875 402975.5625 526026.5625 523395 190.4 213.2 227.9 209.1 229.4 209.8 +3.9090524132024634e-226 16.059443 Q8IXZ2 ZC3H3 Zinc finger CCCH domain-containing protein 3 24 100 0.04058608566986069 13.76453 8.010597 10.945071 8.369589 7.0072913 16.059443 2.5361021920441472e-67 6.799937651173319e-11 1.2817018601441016e-10 3.896048251857191e-14 2.072883817138914e-5 1.42047879528677e-106 25 25 25 24 24 24 False False False False False False 14 17 17 10 13 13 1055.7398681640625 1102.402587890625 1141.926513671875 1187.3031005859375 1134.0302734375 1094.02880859375 86502.5390625 91896.4296875 116103.2890625 80733.6328125 130445.5 119103.25 242.1 228.9 267.3 252.4 231.6 244.3 +0 22.271368 P18754 RCC1 Regulator of chromosome condensation 17 100 0.026277722778085193 22.271368 12.637164 18.541046 14.068678 18.93376 15.312104 8.629775564594893e-91 4.464004280943804e-48 9.19657169810019e-71 1.019148653330019e-116 2.221756539449279e-76 9.171393094609562e-85 22 22 22 22 22 22 False False False False False False 21 21 21 22 22 20 61067.87109375 63620.26953125 60537.23046875 61670.6640625 59304.95703125 59439.265625 644477.9375 1021464.3125 953946.75 897438.0625 1027582 1054406.125 19772.699 20665 19712.4 19478.301 19091 18884 +1.0236241143782522e-255 16.690882 P53049 YOR1 Oligomycin resistance ATP-dependent permease YOR1 30 100 0.3877254621945991 15.564349 8.308202 13.244938 14.532973 15.240679 16.690882 4.838107274284346e-72 1.257446933055545e-13 6.44906323223244e-36 4.1802658830807884e-119 8.534550871873142e-67 7.955493571327336e-125 28 30 28 32 32 31 False False False False False False 15 14 13 22 25 22 1485.990966796875 1372.0439453125 1371.4267578125 3000.512451171875 2837.091064453125 2999.536376953125 154950.484375 272473.84375 254373.5625 270409.78125 351016.1875 318015.1875 240.6 223.3 179.2 475 469.4 499.3 +5.301628266783444e-273 17.042543 P13134 KEX2 Kexin 8 100 0.3640782582311194 4.72701 7.244475 5.7024245 6.9442773 9.294029 17.042543 1.410688166135592e-4 1.5060484364703868e-6 7.484146995842161e-5 1.2032762050794369e-5 2.867318161024563e-6 2.4505724644444745e-135 7 8 7 8 8 8 False False False False False False 5 5 5 7 7 5 859.87786865234375 1102.6708984375 1040.5487060546875 2033.1102294921875 1640.4835205078125 2082.6982421875 1445223.125 1877262.375 2029723.5 2618007.75 3046601.75 2691616.75 132 159.7 147.2 314.8 259.2 305.3 +0 23.94269 Q6P1J9 CDC73 Parafibromin 34 100 0.024736414673410357 18.01476 23.94269 19.018562 16.278412 20.792772 21.86316 1.0478208981755765e-78 1.0038999401146202e-72 4.813517611207349e-72 2.816027675963836e-128 2.1609347651286654e-81 3.511587842883438e-172 38 38 39 40 40 39 False False False False False False 30 33 36 33 31 36 12896.9033203125 12577.791015625 12611.171875 13332.2138671875 12488.6484375 12609.2353515625 119965.375 222327.109375 231810.765625 161873.203125 228112.171875 244943.21875 5898.5 5830.5 5879.9 6133.2 5591.6 5777.1 +0 18.647291 Q86SX6 GLRX5 Glutaredoxin-related protein 5, mitochondrial 3 100 0.04934359978599035 16.281412 18.647291 18.597076 14.638437 15.920296 16.208258 5.7810957821512595e-74 1.05256190601537e-60 6.5254643115744334e-71 1.192304217904747e-119 1.5982321959005142e-68 5.846545595723212e-111 4 4 4 4 4 4 False False False False False False 4 4 4 4 4 4 6467.5517578125 6430.20751953125 6407.541015625 6577.80322265625 5784.2314453125 6012.35205078125 156295.796875 330087.09375 320948.65625 209524.953125 302519.09375 294903.0625 2549.2 2533.8 2521.6 2607.4 2286.3 2374.8 +4.249636805990629e-182 15.034363 Q9NS18 GLRX2 Glutaredoxin-2, mitochondrial 4 100 0.06757369478547756 8.616033 15.034363 9.914403 7.063113 7.389803 11.643617 1.117963360519825e-16 5.9694744088224835e-53 6.166472867791292e-7 8.521827332530877e-6 1.2685426350739626e-5 9.904924809813292e-17 5 5 5 5 5 5 False False False False False False 5 5 5 5 5 5 1321.4429931640625 1545.4105224609375 1500.17333984375 1392.431884765625 1348.399169921875 1327.449951171875 25772.576171875 37602.27734375 35538.32421875 31813.263671875 46082.19921875 46419.84375 573.2 662.5 660.5 635.4 587.5 602.7 +0 23.66284 P20424 SRP54 Signal recognition particle subunit SRP54 18 100 0.3421670730294102 18.885075 19.712383 23.66284 22.323166 16.19433 21.615503 3.9860474110348234e-81 4.620317994274596e-63 4.761496382994909e-85 1.654495085171066e-162 3.191055172017878e-69 1.9681733168809715e-170 21 21 21 21 21 21 False False False False False False 17 17 15 19 19 19 13029.07421875 13229.236328125 13089.904296875 25178.41015625 24429.81640625 25419.216796875 220165.875 424165.25 479659.53125 382510.40625 753385.25 770496.8125 3048.6 3190.3 3096.6 6006.9 5640.2 6022.9 +9.791864427023604e-239 16.33332 P09323 nagE PTS system N-acetylglucosamine-specific EIICBA component 5 100 0.3621016812670918 16.33332 8.455567 7.379491 5.764433 8.679898 10.987911 4.1876116897135345e-74 3.8680616772312005e-15 1.0237510136633803e-5 6.975317293228584e-5 4.695583520627154e-6 1.7341677368410823e-11 6 6 6 6 6 6 False False False False False False 6 6 6 5 5 5 1596.106201171875 1764.2073974609375 1768.525390625 976.5753173828125 755.3067626953125 887.870849609375 44329.97265625 76060.7890625 90404.5234375 37373.6875 54676.73828125 51491.2265625 242.1 278.4 288.2 154 122.8 140.4 +0 22.808651 O60610 DIAPH1 Protein diaphanous homolog 1 50 100 0.03256043711150509 22.239622 20.37884 13.915327 20.183142 22.60766 22.808651 1.0614600798666866e-90 1.4784800069446255e-64 6.079681460843626e-48 5.713544278516274e-150 2.2260539620526374e-86 6.261176542566087e-179 60 61 60 60 61 58 False False False False False False 51 51 51 48 51 49 22558.4140625 22637.30859375 22699.890625 23210.712890625 21241.99609375 21694.78125 317819.75 607831.375 563068.8125 378649.34375 554283.75 596969.625 5415.6 5842.7 5439.4 6092.9 5509 5710.6 +0 18.901325 P37297 STT4 Phosphatidylinositol 4-kinase STT4 35 100 0.3711978880987538 16.35014 15.427895 12.160047 17.89801 15.975809 18.901325 3.773323063269643e-74 8.994635914045791e-54 6.374347081337209e-21 4.383144041447661e-137 1.1515960761216102e-68 7.964567177867014e-152 32 34 35 38 38 38 False False False False False False 18 22 16 24 27 28 2464.052490234375 2483.302490234375 2330.830078125 4959.79296875 4971.125 4786.359375 324238.5 619465.875 517895.90625 668579.1875 911410.1875 902419.875 314 334 301.2 651.6 637.4 618.1 +1.6670574850637194e-153 14.29791 Q5JTD0 TJAP1 Tight junction-associated protein 1 15 100 0.09285448100162907 13.06288 8.604801 7.463939 6.9968348 6.6726727 14.29791 1.5851418855068624e-65 9.68934437482579e-17 9.840727847710192e-6 1.0403672126298127e-5 3.4485472887545134e-5 2.40617594491323e-59 16 16 13 14 16 16 False False False False False False 8 9 9 10 10 11 1645.22216796875 1452.405029296875 1588.8001708984375 1722.782958984375 1341.7060546875 1675.40087890625 696129.125 1017494.1875 1768782.25 912447.8125 1781408.125 1476500.75 447.7 438.1 385.5 388.4 403.8 509.8 +0.002804037814452812 7.018695 Q8WYQ3 CHCHD10 Coiled-coil-helix-coiled-coil-helix domain-containing protein 10, mitochondrial 1 100 0.17749623900999936 3.8909576 7.018695 3.0819943 5.01536 5.7436705 4.950551 2.539611479759603e-4 5.73009634964195e-6 5.219851195477537e-4 1.3150585622966797e-4 9.348318972136645e-5 1.450926871743491e-4 1 1 1 1 1 1 True True True True True True 1 1 1 1 1 1 879.4591064453125 974.64471435546875 869.39788818359375 1261.8343505859375 1094.6260986328125 790.59039306640625 817044.375 2125177.25 2302654 101924.8203125 199526.046875 151463.78125 439.7 487.3 434.7 630.9 547.3 395.3 +4.1731949624542817e-05 5.147626 Q8WVP7 LMBR1 Limb region 1 protein homolog 3 100 0.2098246611559227 4.69111 5.147626 5.06183 4.428954 2.2039325 4.939192 1.4471671541933678e-4 9.640851024103068e-5 1.2464384319991272e-4 2.0082247882694535e-4 0.0012468518847237433 1.4610830643076078e-4 3 1 3 3 3 2 False False False False False False 3 1 3 3 3 2 466.8295593261719 473.25408935546875 502.9867858886719 361.4078369140625 375.9543151855469 277.5375671386719 56629.890625 63875.90234375 63388.7890625 65933.9921875 78477.9609375 70621.359375 74.2 48.9 74.5 56.7 56.8 52 +1.6062339696456915e-262 16.830729 Q05932 FPGS Folylpolyglutamate synthase, mitochondrial 14 100 0.07938845777582144 16.26129 16.830729 9.170822 16.502798 13.64785 15.942106 6.5427807729734005e-74 9.386379675435957e-57 2.9155376571768884e-6 1.7643818411424248e-129 3.4905540051167595e-42 4.802917759791224e-103 13 14 14 14 11 14 False False False False False False 11 10 11 11 8 9 2056.894775390625 2182.1318359375 1857.28466796875 2348.459228515625 2103.3017578125 2240.961669921875 102259.5703125 131474.875 122452.2265625 151868.921875 164034.4375 177512.96875 451.9 479.4 377.9 516.2 468.7 484.3 +0 18.365267 Q86TM6 SYVN1 E3 ubiquitin-protein ligase synoviolin 11 100 0.04856827631870399 14.332947 11.841705 10.594595 11.576743 18.365267 16.857166 8.557567835028881e-69 1.6220768103929057e-46 6.916642202459137e-9 1.4001158785299397e-66 7.1786393313848145e-75 9.680142302215276e-130 14 14 14 14 14 14 False False False False False False 12 14 11 13 10 12 7231.0849609375 7073.142578125 7072.578125 7335.5546875 6735.42919921875 6425.09765625 128014.8359375 229291.5625 229021.75 172067.09375 212239.8125 199576.53125 1504.6 1423.1 1429.4 1544.7 1344.5 1325.3 +0 21.825422 Q16643 DBN1 Drebrin 22 100 0.04354942454690229 21.825422 15.906688 16.611528 17.270582 16.838676 19.442684 1.6614240721423533e-89 8.819526945945649e-55 1.053316151444039e-65 1.2175156767361854e-133 7.034040677798828e-71 1.859816448350417e-155 28 28 28 28 28 28 False False False False False False 25 25 25 24 25 25 12404.7626953125 12168.919921875 12942.998046875 13473.5791015625 12038.978515625 12304.662109375 201288.34375 326980.625 349303.5 284382.78125 372783.375 377304.53125 3645 3724.7 3884.9 3847.2 3494.3 3570.4 +0 21.145529 P00572 CDC8 Thymidylate kinase 3 100 0.4419825514982732 19.173126 12.206848 6.8344674 13.879678 21.145529 16.127337 6.191272637970129e-82 3.144722911690416e-47 2.0089508910898815e-5 9.388302889992346e-116 2.3296311375250724e-82 1.3599220833023479e-108 3 3 3 3 3 3 False False False False False False 3 3 3 3 3 3 1043.8743896484375 1088.092041015625 888.1077880859375 2135.953369140625 2367.2607421875 2492.458984375 231523.09375 407665.40625 404296.25 436481.78125 736219 851497.5 418.8 381.4 308.9 655.9 1052.2 1176.9 +0 19.424767 P77258 nemA N-ethylmaleimide reductase 12 100 0.33638289697608525 14.386536 13.669254 17.755201 11.689718 8.554283 19.424767 6.203349403772588e-69 3.792302114296466e-50 1.1083398127888372e-68 2.9103584384373735e-69 5.175804049843203e-6 2.4531378449664474e-155 16 14 16 12 15 15 False False False False False False 13 12 10 11 10 10 5538.974609375 5793.4443359375 5546.3271484375 2903.177001953125 2970.482177734375 3091.80126953125 816351.5 1883598.5 1571529.75 1142135.375 1936419.875 1502143.125 1633.5 1636.1 1696.6 869.7 917.3 964.9 +0 18.62783 P20795 VPS33 Vacuolar protein sorting-associated protein 33 10 100 0.37704290059642515 7.233512 7.261628 7.6059995 18.62783 15.255412 13.446356 1.6707455219575204e-6 1.326619411065506e-6 8.88459724267993e-6 3.89296951437513e-141 7.777006346771387e-67 6.571662840327853e-42 11 11 10 12 12 12 False False False False False False 9 6 7 10 10 11 1324.3558349609375 1314.5223388671875 1360.3896484375 2659.721435546875 2797.73828125 2728.05078125 55750.171875 95733.2421875 104994.5234375 115814.265625 180943.546875 183516.671875 264.4 270.3 259 583.2 599.1 541.5 +0 20.810669999999998 Q9NVR0 KLHL11 Kelch-like protein 11 11 100 0.027812008868819467 8.97659 11.711616 6.995275 10.876312 7.2409186 20.810669999999998 3.894518188691928e-21 2.9047842020668633e-46 1.609689747383505e-5 2.951625407320423e-51 1.4898188445930862e-5 8.054771769089201e-165 12 12 12 12 11 12 False False False False False False 6 7 7 8 9 9 987.17706298828125 1071.282958984375 1034.8682861328125 1018.46728515625 1046.15625 1017.974365234375 57761.48828125 123643.7734375 109147.609375 106802.53125 214240.796875 170093.15625 203.8 240.9 246.7 212.7 178.1 237.1 +0 20.043001 P23528 CFL1 Cofilin-1 16 100 0.014918485683425544 19.34229 12.142002 12.399499 15.9472 20.043001 18.252586 2.0760877490692862e-82 4.209787290445805e-47 9.298693903103059e-24 1.6336441830927917e-126 2.3562271191556947e-79 1.6053664386821233e-147 22 22 22 22 22 22 False False False False False False 22 22 21 20 21 21 469595.75 476039.96875 476063.9375 477878.3125 462451.625 462421.375 13352142 18001664 17593070 13005178 16947426 17798660 256636.703 269988.312 273178.312 246141.406 254685.5 255778.594 +0 22.778923 Q07457 BRE1 E3 ubiquitin-protein ligase BRE1 29 100 0.36072184133795754 14.9782095 15.3083515 10.022723 20.162924 11.043589 22.778923 1.735816152776502e-70 1.5991393555272468e-53 3.8734700525663497e-7 7.479377891818257e-150 8.68778806723861e-12 1.0230123330436865e-178 31 33 32 34 34 33 False False False False False False 25 23 25 31 29 31 3118.412841796875 3233.354248046875 3130.953369140625 6377.84130859375 6091.60595703125 6299.70556640625 103256.3671875 187201.671875 173722.6875 152948.296875 245628.1875 258864.03125 760.4 804 787.1 1684.7 1560.7 1589.3 +0 19.324873 P32802 EMP70 Transmembrane 9 superfamily member 1 11 100 0.41076855844910826 13.18069 19.324873 10.559111 16.623938 13.475389 16.510668 7.951428553170382e-66 3.369003896507772e-62 9.672541878553326e-9 3.92029353240006e-130 8.982599692712503e-40 1.3212955185410883e-119 12 12 12 12 12 12 False False False False False False 10 7 8 12 10 12 4223.9287109375 3754.40673828125 4074.969482421875 8980.4228515625 7977.84912109375 9224.4853515625 55998.30078125 114546.796875 124757.5859375 132998.671875 240145.640625 239180.765625 770 676 748 1359.4 1302 1404.8 +0 20.711401 Q12242 RUP1 UBA domain-containing protein RUP1 13 100 0.3117527328494492 15.71683 13.718 17.029512 20.711401 12.497969 16.478926 1.897902523824144e-72 3.0256916023010242e-50 8.750221180100641e-67 5.09966671526147e-153 1.2427400077147554e-27 1.0727444979365379e-118 15 14 15 15 15 14 False False False False False False 11 10 8 11 12 13 2725.51171875 2400.755615234375 2547.090576171875 4707.3408203125 4305.85546875 4661.2919921875 104213.4140625 206825.03125 170846.09375 166259.5625 289658.25 273429.625 596.2 452.9 583.3 1088.3 1002.3 1087.3 +0 22.280928 O14646 CHD1 Chromodomain-helicase-DNA-binding protein 1 44 100 0.025907060003430456 22.280928 17.124939 10.438513 15.711534 16.162357 16.168127 8.095946960279279e-91 2.196862627265486e-57 2.7713939851095625e-8 2.8960142414807365e-125 3.850795508564173e-69 8.6118967312258e-110 53 55 53 54 53 53 False False False False False False 38 39 40 31 36 36 3755.374267578125 3753.791015625 3836.984130859375 3874.2861328125 3615.83837890625 3868.16845703125 76890.546875 136025.578125 133614.09375 108113.140625 151585.109375 150242.1875 826 848.4 845.6 831.7 782.8 822.6 +2.870611037339408e-6 8.512539 Q5VZM2 RRAGB Ras-related GTP-binding protein B 1 100 0.2442045484396024 8.512539 4.759228 7.0144176 4.153858 6.6689925 5.15142 1.6673730615760614e-15 1.308948236318936e-4 1.590982330783018e-5 2.4458963300537725e-4 3.4633925077528305e-5 1.2827512403463997e-4 1 1 1 1 1 1 False False False False False False 1 1 1 1 1 1 292.0044250488281 487.2829895019531 576.5531005859375 454.18524169921875 586.921142578125 385.6746520996094 60593.90625 72932.390625 131503.609375 19569.509765625 112918.359375 125784.65625 15.4 25.6 30.3 23.9 30.9 20.3 +0 20.43592 Q08754 BUD7 Bud site selection protein 7 13 100 0.34074319840369244 8.541952 8.701169 6.7234416 20.43592 13.659582 17.085264 7.842598530189075e-16 8.301315542581447e-18 2.373628693414169e-5 1.9971836309721563e-151 2.377688845501733e-42 1.2075444271760987e-136 11 12 12 12 13 12 False False False False False False 8 5 7 9 9 9 1874.7237548828125 1771.8271484375 1605.1434326171875 3437.4228515625 3375.37451171875 3077.76611328125 43019.54296875 79575.0078125 67023.78125 73767.625 96963.734375 110304.0859375 280.4 761.2 193.6 417.5 456.3 420 +3.0701901651823435e-238 16.322678 Q92562 FIG4 Polyphosphoinositide phosphatase 16 100 0.09048613866509798 16.322678 7.3372674 7.118334 5.798957 6.220685 13.50817 4.472528285304836e-74 7.209083518961551e-7 1.3823078630640439e-5 6.800859038794889e-5 5.9687868635871056e-5 4.696956746485087e-43 16 16 16 16 16 16 False False False False False False 5 7 6 6 10 8 3300.604736328125 2549.53515625 3272.401123046875 3101.217041015625 2935.626708984375 3049.63623046875 250090.71875 409846.4375 389529.71875 351860.875 525722 497176.96875 448.1 482.4 376.2 157.8 153 153.3 +0 26.340227 P53128 MET13 Methylenetetrahydrofolate reductase 2 14 100 0.3480593276265926 16.898853 14.924904 15.151667 13.174509 19.493387 26.340227 1.2156199382946954e-75 1.0089748930110589e-52 5.512346084561475e-62 6.44524427025679e-110 7.1220730264541325e-78 4.3476671768025725e-205 15 15 16 16 16 16 False False False False False False 11 11 11 15 12 13 3126.370361328125 3091.895751953125 3173.005126953125 6296.85693359375 5802.47021484375 5973.6611328125 172886.1875 295382.9375 256794.8125 232327.78125 304592.8125 310893.0625 668.2 662 654.7 1409.5 1275.3 1318.8 +0 21.884396 Q14137 BOP1 Ribosome biogenesis protein BOP1 25 100 0.024502626554837054 21.884396 20.021133 19.21641 16.64224 16.789253 18.180092 1.1199817438027075e-89 9.390473212022152e-64 1.3995553231866944e-72 3.1279335385277875e-130 9.43221277963226e-71 4.827754231996229e-147 30 30 29 30 30 30 False False False False False False 26 25 25 24 26 24 13558.224609375 13802.5986328125 13535.6728515625 13903.6982421875 13197.935546875 13055.201171875 222401.5 389269.46875 408193.40625 273026 396277.09375 435212.375 4029.8 4207.9 4086.3 4340.2 3977.3 4107.1 +0 21.956352 P09064 SUI3 Eukaryotic translation initiation factor 2 subunit beta 16 100 0.36876153474153434 20.79462 21.956352 13.396171 19.852442 16.998817 15.095729 1.5592145716787083e-86 3.798964607986005e-68 1.9083590783771453e-38 4.557311940530619e-148 2.7113391991546725e-71 6.471131720876887e-79 18 19 19 19 19 19 False False False False False False 17 15 17 19 18 17 22807.044921875 22564.58984375 22151.470703125 46382.28125 44508.94921875 45023.875 407480.65625 707937.4375 644708.0625 740729.125 1131058.375 1150894.125 10451.6 9765.8 9108.5 17860.9 17477.6 17873.4 +0 21.653273 P35250 RFC2 Replication factor C subunit 2 23 100 0.020905731878766837 21.653273 18.505924 16.10442 17.516642 16.553171 16.290964 5.220352269971685e-89 2.165929344491811e-60 2.1137851328351376e-64 5.524287916861153e-135 3.8275142373973423e-70 2.4506383231584382e-113 28 30 30 30 29 30 False False False False False False 23 27 24 28 25 24 23979.568359375 24001.33984375 23699.048828125 24309.404296875 22884.767578125 23520.625 692242.3125 1181718.125 1154959.125 809348.6875 1175529.5 1202331.625 9292.3 9471 9295.1 9682.5 9034.9 9297.9 +0 17.997427 Q9UIM3 FKBPL FK506-binding protein-like 7 100 0.11861593641917001 17.997427 6.1920266 6.6594844 6.6741652 6.5546365 5.146533 1.1710120239683745e-78 3.40181999317429e-5 2.530377929143094e-5 2.2849693004236635e-5 4.002884688918652e-5 1.2827512403463997e-4 7 6 5 6 7 7 False False False False False False 5 5 5 5 5 5 5472.8291015625 5543.23046875 5740.548828125 6772.61767578125 4870.94873046875 6419.3759765625 879616.9375 1127914 1739773.875 1541433 2150898 2478629 861.7 576.5 1000.2 1434.4 911.7 1863.3 +0 22.63134 P32598 GLC7 Serine/threonine-protein phosphatase PP1-2 9 100 0.34221493547317333 21.370857 12.567963 8.654627 16.02332 22.63134 17.210083 3.4259098711694163e-88 6.119199931379574e-48 4.160139822833438e-6 6.435493274644292e-127 1.9072343097067664e-86 2.6693103981251823e-140 11 11 11 12 12 12 False False False False False False 11 10 10 12 11 11 16170.21484375 16898.537109375 15936.6259765625 32129.83984375 30534.12890625 30667.05859375 370828.21875 625447.8125 556187.5 924741.0625 1223486.75 1189317.25 3975.1 4281.4 4097.8 9322.5 8893.8 9057.4 +0 24.173077 P33322 CBF5 H/ACA ribonucleoprotein complex subunit CBF5 19 100 0.3320092463882606 22.085285 22.744265 16.46295 18.598866 21.31015 24.173077 2.9515356099700464e-90 5.762418053885125e-70 2.5400271165014823e-65 5.658527369314373e-141 8.257198247614788e-83 7.20734576516049e-189 25 25 25 25 24 25 False False False False False False 19 19 19 24 22 25 23844.169921875 23086.58984375 22966.845703125 44205.25390625 42307.38671875 43994.5859375 327722.53125 587607.375 556131.0625 679690.4375 994269.625 1050256.625 6180.1 5887.3 5899.8 11707.7 10759.2 10983.6 +0 20.89543 P18963 IRA1 Inhibitory regulator protein IRA1 36 100 0.3695990445770093 7.396513 11.139728 8.667084 20.89543 13.067929 16.878893 4.459606371498291e-7 3.6721548170559204e-45 4.1246107850216865e-6 4.407204483376631e-154 2.0330111351628882e-34 2.1663329187240947e-130 36 31 34 37 37 37 False False False False False False 17 13 14 24 24 23 1794.503662109375 1992.8480224609375 1865.7119140625 3811.873046875 3681.045166015625 3884.476318359375 607831 1031188.6875 1049985.625 673924 1032730.3125 1008919.9375 126.9 127.8 124.5 216.5 225.1 223.5 +0 22.776733 Q9NR12 PDLIM7 PDZ and LIM domain protein 7 21 100 0.028641747768259965 22.210716 22.776733 14.253797 18.016115 21.884209 16.15228 1.2869890057596008e-90 4.858506543752473e-70 8.144426995699464e-55 9.722292305387297e-138 2.1864483303683217e-84 2.5085912997838504e-109 26 26 26 26 26 26 False False False False False False 23 22 22 23 21 23 9892.3994140625 9588.1953125 9716.4482421875 10145.1787109375 9641.7978515625 9328.2919921875 133708.28125 250484.390625 229517.078125 184679.734375 267115.46875 266147.9375 2933.1 2910.9 2925.9 3001.7 2906.1 2908.4 +0 24.807486 O15067 PFAS Phosphoribosylformylglycinamidine synthase 51 100 0.020759257564241452 22.975729 24.807486 19.513636 17.694101 21.567919 20.822657 7.860646356806946e-93 8.367649362227657e-75 2.1979828437321523e-73 5.86166781841444e-136 1.6178557029370309e-83 6.645786959950388e-165 66 66 66 66 66 66 False False False False False False 59 59 61 56 56 58 32472.970703125 32320.798828125 32361.61328125 32471.11328125 31489.498046875 30874.6953125 263568.0625 424390.84375 435067.1875 307903.5625 429580.9375 452105 7403.2 7467.9 7541.9 7464.7 7053.2 7071.1 +0 19.065199 Q12450 ERP4 Protein ERP4 7 100 0.33987932791212766 19.065199 15.631074 13.157962 10.723852 15.746838 13.777331 1.244145458983883e-81 3.361218241385994e-54 1.6414627922354731e-34 3.229344409348638e-48 4.4122731528019065e-68 2.9109150946244165e-48 7 7 7 7 7 6 False False False False False False 6 6 6 6 6 6 10027.763671875 10286.6884765625 10078.80078125 19383.876953125 19701.080078125 18528.8359375 240907.1875 387827.25 399321.28125 598721.0625 779378.75 709413.9375 3348.5 3713.7 3681.3 7419.7 7218.5 6895.5 +0 22.114456 A5YKK6 CNOT1 CCR4-NOT transcription complex subunit 1 84 100 0.030073373228795146 19.565048 17.2119 22.10253 22.114456 18.360098 21.03607 4.8975855673505835e-83 1.4250943440147913e-57 1.4352101539081398e-80 2.8805703425958273e-161 7.405046771176066e-75 2.2032008850256606e-166 94 93 95 95 90 94 False False False False False False 71 75 77 66 75 73 8299.4794921875 8350.5068359375 8048.140625 8750.54296875 8136.33447265625 8473.666015625 184317.640625 326367.4375 309056.3125 261077.59375 344698.65625 346120.21875 1737.9 1766.3 1737.6 1865.9 1732.8 1796.8 +0 19.959183 Q12515 PAR32 Protein PAR32 10 100 0.37295128868513616 17.645908 13.817801 19.959183 9.650033 11.033552 16.079823 1.0956783297531302e-77 1.898046772725119e-50 1.3331245556505044e-74 8.256324416077722e-30 1.0268391741267597e-11 3.497357208779486e-107 14 15 14 15 15 14 False False False False False False 12 13 13 13 15 12 7188.33056640625 7195.64013671875 6957.8876953125 15194.5625 13740.3017578125 14203.1572265625 120325.9140625 212189.578125 195056.375 278003.21875 345695.59375 371602.53125 3235 3212.3 3210 6523.3 6003.9 6165.9 +4.391847294741519e-291 17.398384 Q02932 KAP120 Importin beta-like protein KAP120 13 100 0.3600689369944603 7.846141 17.398384 7.014753 13.086838 16.155062 16.980972 1.6801513031160957e-9 5.6315051430295184e-58 1.590842894524453e-5 3.067959861769667e-107 4.0222801867107194e-69 1.8263865489932207e-133 14 12 12 14 14 13 False False False False False False 6 9 8 11 11 12 2047.31396484375 2181.55126953125 1947.4971923828125 4257.23828125 4035.229248046875 3849.58056640625 12825.0390625 38425.73046875 35303.59765625 48766.4140625 78206.3046875 79797.4375 210.7 238.9 223.7 487.7 489.4 458 +0 17.615847 P60002 ELOF1 Transcription elongation factor 1 homolog 1 100 0.050676079642510974 12.219501 7.1718206 7.450978 17.615847 17.508013 8.774722 2.1076811226377748e-63 2.471909002839985e-6 9.874968070107913e-6 1.5731558541720842e-135 1.287035070035248e-72 3.1032830502556787e-6 1 1 1 1 1 1 True True True True True True 1 1 1 1 1 1 4343.69287109375 4229.11669921875 3852.921630859375 3818.533203125 4104.52099609375 4045.764404296875 150947.09375 129082.5625 168760.171875 127522.28125 144212.6875 159149.40625 2171.8 2114.6 1926.5 1909.3 2052.3 2022.9 +0 20.075071 Q01780 EXOSC10 Exosome component 10 46 100 0.02537111348394361 19.186628 15.805583 20.075071 15.649374 16.959017 20.016262 5.681457974868103e-82 1.4396378404011028e-54 6.479127808676288e-75 6.169263051711145e-125 3.4392033679006955e-71 2.35896685546728e-159 56 55 56 56 56 56 False False False False False False 44 45 43 40 44 45 13965.345703125 13774.7783203125 13895.240234375 13237.9609375 13296.9404296875 13247.8017578125 380224 655641.875 654544.5 489223.6875 663218 744843.4375 4281.7 4161.4 4335.7 3984.4 4053.7 4089.2 +1.4979548434073793e-292 17.426752 Q96B54 ZNF428 Zinc finger protein 428 4 100 0.13710282700875948 16.049486 14.509969 8.57713 9.991835 16.21072 17.426752 2.433804001671756e-73 7.289605456303413e-52 4.366881270892029e-6 4.2457853806748113e-35 2.897359128665528e-69 3.9739006028503315e-142 5 5 5 5 5 5 False False False False False False 5 5 5 5 5 5 2763.1279296875 2535.322998046875 2168.814208984375 3093.454833984375 2290.533935546875 2325.784912109375 91339.4765625 111644.78125 89166.671875 74428.796875 90966.96875 101879.625 1952.6 1751.8 1424.5 2078.5 1488.6 1663.7 +0 18.440598 Q96AA3 RFT1 Protein RFT1 homolog 9 100 0.05925599978046119 16.337406 9.900074 18.440598 8.516213 12.069027 15.961508 4.083997719949762e-74 1.6365290893311748e-34 1.7022296844368502e-70 1.112133419636124e-15 3.7168030192738312e-22 1.2500187700464262e-103 9 9 9 9 9 9 False False False False False False 7 6 7 8 7 6 4370.07568359375 4589.630859375 4740.04833984375 5217.2939453125 4829.4775390625 4725.0810546875 361387.1875 569219.25 650678.9375 401335.5 692576 849005.25 793.5 814.2 861.6 919.9 901.4 888 +0 24.928415 P11940 PABPC1 Polyadenylate-binding protein 1 38 100 0.018028012794819994 21.301294 20.716558 16.931509 18.916365 24.928415 22.966965 5.442046575669342e-88 2.5278029774190744e-65 1.5748693504137807e-66 9.342523359381074e-143 6.1114416854188635e-93 4.533702932952649e-180 51 51 51 51 51 51 False False False False False False 47 48 48 46 47 50 98866.703125 99832.953125 100175.5859375 98052.7109375 95238.3671875 98968.734375 1076703.125 1879017 1764420.75 1319606 1737700.75 1812480.375 34483.5 34747 34735.801 35785.699 34247.398 34601.301 +0 18.51269 P61326 MAGOH Protein mago nashi homolog 9 100 0.04657361798814635 16.073402 13.806226 11.688388 15.839006 14.52291 18.51269 2.10159193019519e-73 2.0025401575798725e-50 4.0889213019423435e-16 6.116429496726314e-126 9.75080165707023e-56 3.064110710438939e-149 11 11 11 11 11 11 False False False False False False 11 10 11 10 8 10 49001.29296875 49556.359375 50436.1796875 46684.33203125 44493.46484375 46730.25390625 574696 1095596 1137873.5 805916.1875 1239584.25 1152425.75 17086.6 17419.5 17422.1 17241 15675 16540.9 +2.720363544441124e-305 17.6701 Q9H490 PIGU Phosphatidylinositol glycan anchor biosynthesis class U protein 4 100 0.06450264303903935 8.52488 8.022075 17.6701 11.468101 9.739931 14.959745 1.216346903648178e-15 5.457074167422682e-11 1.8467718305011343e-68 4.67791697041288e-64 1.1725762614771972e-6 2.337551715699151e-75 4 4 4 4 4 4 False False False False False False 4 4 4 4 4 4 5544.326171875 5470.70703125 5363.2705078125 5762.833984375 4773.94873046875 5151.02392578125 312153.8125 718439.1875 659469.375 678787.6875 1020191.0625 930261.25 1438.2 1392.7 1401.6 1548.8 1441.9 1507.7 +1.0934735882651238e-5 6.6595244 Q5M7Z0 RNFT1 E3 ubiquitin-protein ligase RNFT1 2 100 0.513186020426354 3.457526 6.3176756 4.3379154 1.4626983 6.6595244 6.488275 3.4583793251372584e-4 2.971632349582108e-5 2.136073724112432e-4 0.0015269044059150936 3.4789793463971876e-5 3.413033907832674e-5 2 2 1 2 2 2 False False False False False False 2 2 1 2 2 2 74.60062408447266 266.0371398925781 344.3812255859375 98.528961181640625 186.80731201171875 275.99603271484375 6770.25048828125 4349.44970703125 36409.67578125 36494.5546875 5250.44677734375 11412.666015625 9.7 20.5 13.6 10.7 18.9 25.5 +1.3630950554936414e-5 6.477215 Q15119 PDK2 [Pyruvate dehydrogenase (acetyl-transferring)] kinase isozyme 2, mitochondrial 4 100 0.2070711142277585 4.2361755 3.7906861 4.6373243 5.529306 6.477215 5.1478868 2.0177782191710823e-4 2.6082301207569486e-4 1.7259298222062182e-4 8.540697018824218e-5 4.3931633730282786e-5 1.2827512403463997e-4 4 4 4 4 4 4 False False False False False False 4 4 4 4 4 4 544.9942626953125 329.6451721191406 413.552001953125 325.7838134765625 479.8831787109375 393.09893798828125 154105.40625 219843.671875 194914.421875 419989.03125 322756.96875 398824.59375 63.8 48.3 54.9 54 70.9 59.8 +0 19.295681 Q99190 TSC13 Very-long-chain enoyl-CoA reductase 7 100 0.37284340508134384 15.682139 17.291538 11.577773 19.295681 9.076423 16.45173 2.348820810932362e-72 9.564183301907521e-58 3.8860108134434576e-15 6.757486852230094e-145 3.5211334946312295e-6 6.4240834354074555e-118 10 10 9 10 10 10 False False False False False False 8 7 9 9 9 8 7649.7744140625 8133.6884765625 7957.88671875 15200.3134765625 16167.3466796875 16648 307594.5625 487278.5625 494101.28125 797139.4375 1055298.625 1034997.8125 2885.7 2711.1 2904.4 5048.5 5195.9 5335.5 +0 20.759367 Q12907 LMAN2 Vesicular integral-membrane protein VIP36 17 100 0.04817490395381558 16.63827 12.158512 8.748393 20.759367 14.131454 18.129276 6.220294938518198e-75 3.908391510149862e-47 3.943122890359888e-6 2.6943051894378127e-153 2.117144555767905e-49 1.0421391714439167e-146 20 20 20 19 20 19 False False False False False False 15 18 17 17 16 16 41427.77734375 41178.76953125 41584.7265625 45739.82421875 40267.45703125 40453.4609375 462480.625 820394.5 811429.0625 660100.625 821373.25 855178.3125 15177.3 15091.5 14941.3 17162.9 15302.5 15373.1 +0 20.815773 Q86VM9 ZC3H18 Zinc finger CCCH domain-containing protein 18 25 100 0.04780390255292843 20.658447 17.58953 15.053483 20.776253 19.119305 20.815773 3.83609043644334e-86 2.1646849857151693e-58 9.697987710843607e-62 2.1491781352178938e-153 7.117020795420801e-77 7.425837091891551e-165 29 28 29 29 29 28 False False False False False False 25 23 24 21 23 24 6579.5048828125 6058.45458984375 6738.2890625 6357.28662109375 5937.1943359375 6314.6787109375 302905.6875 521753.5 525457.4375 268001.59375 489238.3125 498866.375 1506.4 1397.6 1614.2 1450.6 1401.3 1425.8 +0 21.503117 P52756 RBM5 RNA-binding protein 5 15 100 0.04815289923106148 19.47202 15.805876 21.503117 18.109133 14.677316 16.907593 8.948974904559325e-83 1.437845081786489e-54 6.915720309665271e-79 2.966871150591055e-138 2.3319467428228595e-58 2.987699328294128e-131 15 13 15 15 15 15 False False False False False False 11 12 12 14 13 12 2986.9599609375 2992.0224609375 2866.26953125 3220.560791015625 2819.363525390625 2898.84033203125 323723.53125 422086.0625 447332.5 280190.625 424052.5 397776.34375 666.1 611.8 667.6 764.4 696.6 688.3 +0 24.711521 Q8N163 CCAR2 Cell cycle and apoptosis regulator protein 2 45 100 0.02825887798676012 21.997047 23.852028 23.272606 16.318758 24.711521 19.05116 5.290885364919962e-90 1.6197866864564693e-72 6.467371840102559e-84 1.7142152036257366e-128 2.6415723192033186e-92 7.94847375482715e-153 55 54 55 55 55 54 False False False False False False 44 42 45 43 43 40 25260.38671875 25803.26953125 25958.541015625 26158.021484375 24475.36328125 24593.107421875 280815.0625 509665.34375 494407.84375 382973.6875 493536.03125 504949.0625 6874.9 6971.6 6951.4 6946.5 6540.5 6641.9 +0 25.63988 P38225 FAT1 Very long-chain fatty acid transport protein 17 100 0.36921707319508384 18.828747 19.0942 20.027262 16.627237 25.63988 19.194864 5.720855667136926e-81 1.0938367625944318e-61 8.692312674455928e-75 3.7645318921529986e-130 6.292933408204593e-95 8.639349083027968e-154 16 16 17 18 17 18 False False False False False False 13 11 11 14 13 14 3648.72509765625 3476.41845703125 3328.4130859375 7287.2119140625 7034.48681640625 6651.1357421875 36913.203125 88119.5546875 74009.765625 80304.3125 137410.28125 129747.046875 630.3 575.3 541.1 1191.8 1200.3 1062.4 +2.870611037339408e-6 8.48425 Q9HB96 FANCE Fanconi anemia group E protein 7 100 0.10120034639128768 8.48425 6.6678834 6.2947445 6.30156 7.4217987 6.3292856 3.4111847780437006e-15 1.7233125719874543e-5 4.123218565048401e-5 3.984918644649911e-5 1.2411965843487069e-5 4.20642059411558e-5 7 6 7 7 7 7 False False False False False False 5 5 5 5 5 5 596.9727783203125 530.73260498046875 498.29150390625 623.57470703125 619.890380859375 650.82562255859375 89096.859375 153951.078125 104559.9375 113320.8828125 140081.0625 163488.171875 87.5 82.3 92.9 91.7 94.9 103.9 +0 24.129166 P0AD59 ivy Inhibitor of vertebrate lysozyme 6 100 0.3705929472293578 24.129166 8.729453 8.37992 11.065122 6.857181 5.1793556 3.5025535698418487e-96 3.995875882366957e-18 5.018174517375884e-6 3.634613023316936e-55 2.592965306270682e-5 1.2620676213882687e-4 7 7 7 6 5 6 False False False False False False 6 6 7 5 5 5 6557.2001953125 6687.28515625 6157.40283203125 3169.039306640625 3350.57177734375 3115.1181640625 252320.046875 395724.21875 416007.46875 310136.46875 324901.8125 430612.34375 2350.8 2457 2176.7 1193.6 1219.8 1140.2 +6.092295559521952e-6 7.008096 P52803 EFNA5 Ephrin-A5 2 100 0.07323698438367954 5.550169 6.51561 7.008096 5.1938252 6.4038024 6.293909 7.319317379242926e-5 2.2627275989409412e-5 1.595689957163385e-5 1.1455647036287257e-4 4.830470172987127e-5 4.3799907784230766e-5 2 2 2 1 2 2 False False False False False False 2 2 2 1 2 2 741.2740478515625 862.51153564453125 808.26495361328125 776.231689453125 727.3570556640625 861.8133544921875 257765.328125 413050.90625 351373.875 282077.5625 300682.25 229895.328125 103.9 117.7 109.5 63.3 98.6 116.8 +7.814236154840918e-129 13.602235 O75884 RBBP9 Serine hydrolase RBBP9 8 100 0.04541577109087814 13.602235 12.199447 9.013114 8.428362 10.175238 6.267615 6.630599864006758e-67 3.251791355796918e-47 3.285968950731225e-6 9.509587492540784e-15 1.6509960695510036e-7 4.5222445264233147e-5 9 9 9 9 9 9 False False False False False False 8 8 8 7 7 7 6329.7646484375 6907.5166015625 6553.06396484375 7210.0029296875 6796.43359375 6919.62548828125 64669.890625 145642.953125 132331.859375 101243.0859375 149772.125 174054.96875 2392.9 2529.9 2273.3 2506.4 2460.4 2525 +0 19.30794 P54619 PRKAG1 5'-AMP-activated protein kinase subunit gamma-1 15 100 0.043080745591053723 19.30794 12.158903 17.33593 14.52225 13.207257 14.108576 2.5893483783647874e-82 3.901925985939227e-47 1.3876293938970857e-67 4.750185875378642e-119 3.391244993204869e-36 3.6764181616662694e-55 17 17 15 17 17 17 False False False False False False 12 14 14 15 14 14 7074.3662109375 6650.2607421875 6804.64990234375 6580.69873046875 6205.931640625 6583.9130859375 2027292.625 3879384.75 2771870.75 2402245.25 3658758.25 4085221.75 2173.4 2083.7 2185.1 2082.8 1906.3 2005 +0 18.827082 P42226 STAT6 Signal transducer and activator of transcription 6 20 100 0.05291262479677499 18.827082 16.299862 14.100202 10.391476 10.293631 16.568617 5.783239863757312e-81 1.2883081736343011e-55 1.2479397800687908e-51 5.861406815127208e-42 7.244834589373919e-8 2.8383986754188236e-121 23 23 23 23 24 24 False False False False False False 18 16 15 16 16 19 5249.16552734375 5489.9873046875 5517.615234375 6070.69287109375 5304.63720703125 5611.5751953125 1385489.125 2331020.5 2260164.5 1724380.5 2602230.5 2509867.25 1350.9 1377.7 1344.5 1466 1295.6 1398.1 +1.0017188840041308e-24 9.154832 Q9Y4C1 KDM3A Lysine-specific demethylase 3A 15 100 0.16379229769541492 8.370343 7.874023 7.47024 6.6906104 7.3772264 9.154832 5.4659895255833755e-14 7.989611259956271e-10 9.826125191650297e-6 2.2477626014661064e-5 1.2825161997646293e-5 2.305867961504123e-6 14 14 14 15 13 12 False False False False False False 7 8 5 6 8 5 426.8070068359375 413.9820556640625 588.36041259765625 624.041748046875 509.7394714355469 512.965576171875 85481.7890625 127820.65625 87136.8984375 73524.4375 107191.1640625 93545.0625 40.2 53.1 59.3 58.2 50.9 40.4 +8.507695341823152e-6 6.8264885 Q3E7C1 TFB5 General transcription and DNA repair factor IIH subunit TFB5 2 100 0.28840209983833376 1.918774 4.910521 0.43727574 6.8264885 5.8586574 4.967324 0.0010166533335595473 1.1726676533363732e-4 0.002404476170048237 1.665777355266981e-5 8.493694502050777e-5 1.4361830234858235e-4 2 2 2 2 2 2 False False False False False False 2 2 2 2 2 2 465.0577392578125 556.33837890625 678.6044921875 964.71893310546875 960.61688232421875 672.861083984375 69632.640625 220765.34375 94670.453125 156496.46875 166407.859375 289534 277.2 270.4 269.4 514.9 446.4 398.5 +1.5586413779950045e-5 6.3391066 Q8WY91 THAP4 Peroxynitrite isomerase THAP4 5 100 0.3747804925066092 3.5814807 4.4309883 5.2157664 4.344965 6.3391066 4.165851 3.1608987239411477e-4 1.6712405614653249e-4 1.1062511008682094e-4 2.1342064278946366e-4 5.2409123073433226e-5 2.6064732213323103e-4 3 4 4 4 5 3 False False False False False False 3 4 4 4 5 3 60.08382797241211 152.06443786621094 105.12103271484375 163.65150451660156 151.4875030517578 214.5890350341797 123938.0078125 94521.296875 127308.515625 156515.0625 160321.3125 112369.8515625 11.6 26.7 17.3 30.8 37.3 24.2 +0 19.39445 P78356 PIP4K2B Phosphatidylinositol 5-phosphate 4-kinase type-2 beta 11 100 0.06261609819320993 15.529728 19.39445 17.1601 16.30075 19.209682 13.256374 5.98219791677121e-72 2.3619524893298386e-62 4.0079675794364363e-67 2.1386705530988632e-128 4.089887120290241e-77 1.6664991564523348e-38 12 12 12 12 12 12 False False False False False False 7 9 9 7 7 7 3900.09814453125 4304.58447265625 3688.470703125 3892.579345703125 3614.784423828125 3800.575927734375 513292.90625 1144284.875 955933.75 780399.375 1161154.25 1092403.875 903.7 947.4 916.1 859.7 838.6 867.1 +3.0868954327055047e-6 7.4401436 Q8NHG7 SVIP Small VCP/p97-interacting protein 2 100 0.07073175361160251 5.607206 7.4401436 7.282074 5.7575974 6.4862513999999996 6.9025464 6.936867891757817e-5 2.823360004958988e-7 1.1247652077569828e-5 7.018775900683343e-5 4.362700775143873e-5 1.995652450225071e-5 2 2 2 2 2 2 False False False False False False 2 2 2 2 2 2 1197.89453125 1169.391845703125 1189.3358154296875 1401.50390625 1248.578369140625 1308.62255859375 100659.1328125 203950.078125 203122.703125 139487.578125 278732.8125 235998.71875 474.4 453.3 480.8 582 497.8 515.6 +9.118743914908401e-206 15.599514 Q6P161 MRPL54 39S ribosomal protein L54, mitochondrial 2 100 0.12629126270934846 13.815454 7.452604 7.4823184 6.99518 7.762017 15.599514 1.8752162171136722e-67 2.497092809309563e-7 9.754485689251651e-6 1.0435953379698342e-5 9.742249963796502e-6 6.940100892018016e-93 3 3 3 3 3 3 False False False False False False 3 3 3 3 3 3 6152.94140625 6551.27001953125 7562.06787109375 7869.24853515625 7950.08251953125 5985.740234375 114729.1015625 269851.09375 248883.484375 206719.765625 278876.875 400874.03125 3474.2 3582.5 3903.1 3617.8 3434.3 3309.4 +0 21.38858 Q9H568 ACTL8 Actin-like protein 8 12 100 0.03692671731071849 17.269444 21.38858 13.870219 13.151733 8.113335 11.535126 1.186674812418033e-76 7.498709597842208e-67 4.567917859251339e-47 3.213804666328455e-109 7.593585598319958e-6 1.0134068794990337e-15 15 15 15 15 15 15 False False False False False False 13 12 15 12 12 11 3695.079345703125 3493.76953125 3321.46484375 3428.817626953125 3407.97412109375 3411.646484375 143794.84375 355171.53125 340651.40625 242988.4375 326487.4375 387062.5625 1231.4 1204.7 1157.1 1179.8 1170.1 1130.2 +0 22.062021 P25385 BOS1 Protein transport protein BOS1 9 100 0.35916389336106835 12.263975 12.07045 8.563152 9.333504 10.835721 22.062021 1.6315242626621644e-63 5.808990364230865e-47 4.4001792421665315e-06 2.2137694973973316e-25 2.1255203737480434e-10 1.353832431806634e-173 10 10 10 10 10 10 False False False False False False 6 6 5 7 10 10 2737.554443359375 2337.348876953125 2603.941162109375 4947.96875 4862.28466796875 5258.12353515625 536700.125 1216552 1160327.5 1009968.75 1626649.875 1574472.75 911.6 824.6 836.9 1714.3 1614 1759.3 +7.747860453025064e-305 17.661507 P32783 ABD1 mRNA cap guanine-N7 methyltransferase 15 100 0.39281763056166025 16.893875 11.28217 14.374694 17.661507 13.253906 16.112072 1.2539488300065136e-75 1.96085727286382e-45 2.1305013600789503e-57 8.824297168716305e-136 8.37560130932075e-37 3.845895039129125e-108 16 13 16 17 17 17 False False False False False False 9 10 10 13 15 17 2662.0302734375 2713.09716796875 2798.27099609375 5815.02099609375 5722.7265625 5767.6142578125 165697.40625 297873.125 312134.6875 272168.5 399701.3125 389849.1875 749.4 711.4 783 1661.3 1631.1 1731.1 +0 24.155762 Q9UHD2 TBK1 Serine/threonine-protein kinase TBK1 25 100 0.03579163832828478 24.155762 16.34941 14.65867 12.527066 21.272402 19.590218 2.917866194960259e-96 1.0094023375254857e-55 9.421688037291565e-61 5.91881714764067e-91 1.0496830259021398e-82 1.863719645711598e-156 28 27 28 25 26 28 False False False False False False 20 22 23 18 18 21 2912.376953125 2706.6953125 2824.007080078125 2919.732177734375 2685.567626953125 2858.4755859375 208320.90625 364766.65625 356333.0625 273042.84375 392537.71875 468617.4375 883 790.5 868.7 831.8 769.6 859.4 +1.166467433199342e-256 16.71038 Q5BKT4 ALG10 Dol-P-Glc:Glc(2)Man(9)GlcNAc(2)-PP-Dol alpha-1,2-glucosyltransferase 7 100 0.11863552185984229 12.302245 9.256008 7.45335 7.0074577 16.71038 7.0142775 1.3094987314474893e-63 1.1264375548893427e-24 9.873663891495308e-6 1.0158053171962794e-5 1.508686306166123e-70 1.7368268519043687e-5 8 8 7 8 8 7 False False False False False False 5 6 5 5 5 5 2211.6767578125 1875.615234375 2351.4755859375 2190.549560546875 1870.2918701171875 1743.3135986328125 272878.15625 451297.71875 356895.25 430649.59375 534226.375 481376.625 372.2 451.9 441.5 419.6 351.9 303.6 +3.092004736961555e-73 11.710892 P23797 GPI12 N-acetylglucosaminyl-phosphatidylinositol de-N-acetylase 7 100 0.3547657608194982 11.710892 7.416335 6.429057 7.420336 10.360153 10.68653 3.8375730439914416e-62 3.553285958481036e-7 3.5117414825492644e-5 1.201876129278184e-6 4.268000589047848e-8 9.226653328767715e-10 7 7 6 7 7 7 False False False False False False 5 5 5 5 5 5 1438.7025146484375 2029.515869140625 1690.4639892578125 3002.7373046875 3022.67431640625 3649.81591796875 159357.171875 582067.5625 407426.5 332589.34375 554802.125 527820 433.6 549.6 398.1 827.9 790.1 896.3 +3.2143683633393016e-306 17.687614 Q9Y3Q8 SPACDR Sperm acrosome developmental regulator 10 100 0.08511320213600486 14.090021 12.655299 17.554415 7.445747 8.844881 17.687614 3.661467583418994e-68 4.110780319565962e-48 3.724226042049739e-68 9.699606689063141e-7 4.1565438944048006e-6 8.031983893172077e-144 9 10 10 10 9 9 False False False False False False 8 8 8 5 7 7 1204.9130859375 1312.795654296875 1272.6575927734375 1481.424072265625 1167.0347900390625 1276.8658447265625 97209.1953125 286164.1875 254374.53125 385079.4375 489162.125 511790.40625 277.7 355.7 372.5 345.3 298.6 309 +0 21.71291 P46087 NOP2 Probable 28S rRNA (cytosine(4447)-C(5))-methyltransferase 30 100 0.023669353739052157 21.71291 19.519688 18.77956 18.251915 17.682993 21.65066 3.506463062815333e-89 1.240915498443222e-62 2.1200994992558227e-71 4.807702941385705e-139 4.487067939760535e-73 1.1060143051549034e-170 35 35 35 35 35 35 False False False False False False 31 30 32 30 31 31 47273.296875 47877.19140625 47143.53125 48431.69921875 45528.98046875 45939.6953125 345553.1875 632708.1875 604117.25 478934.6875 634135.0625 653810.4375 16065.4 16461.301 16196.1 16844 15826.2 15900.3 +1.4955298552166442e-5 6.3839383 Q8IV32 CCDC71 Coiled-coil domain-containing protein 71 3 100 0.14619323451689836 5.011429 1.6188115 5.2847743 5.011582 6.3839383 5.04454 1.1463423355201134e-4 0.0011723066555631844 1.0546051811824711e-4 1.3161887760822558e-4 4.956039566706805e-5 1.368680803697854e-4 3 2 3 2 3 3 False False False False False False 3 2 3 2 3 3 595.5164794921875 382.88592529296875 572.69268798828125 518.04559326171875 551.9229736328125 496.5498962402344 2855631 3780044 657257.375 5228625 2359424.75 1015984.5 65.3 48 79.8 28.5 63.1 54.3 +0 26.279709 Q6PGP7 SKIC3 SKI3 subunit of superkiller complex protein 54 100 0.01616533072532387 18.264614 17.343388 16.290802 18.236454 20.844149 26.279709 2.1330936793946327e-79 7.389966546961035e-58 7.030361997706824e-65 5.842073597879074e-139 1.5705079105361089e-81 1.2683887336708425e-204 57 58 58 57 57 56 False False False False False False 46 48 46 44 44 45 5839.91845703125 5888.59619140625 5776.38037109375 5958.4296875 5690.3095703125 5881.37158203125 132352.71875 236400.203125 221006.609375 144685.734375 255147.984375 244923.3125 1434.8 1417.8 1453.5 1544 1445.9 1462.2 +0 27.962545 P38164 SEA4 SEH-associated protein 4 23 100 0.3725995439199269 13.3866005 16.621374 12.342705 20.607977 27.962545 17.537579 2.369470266321111e-66 2.642840082192148e-56 4.626082981162773e-23 2.024304140760439e-152 1.5282823599101672e-101 7.573515762841594e-143 24 24 24 28 28 26 False False False False False False 17 17 15 23 18 21 1628.1124267578125 1618.167236328125 1638.0433349609375 3415.3935546875 3215.5048828125 3269.23974609375 88354.375 166500.859375 154249.921875 162801.75 271463.75 247714.734375 367.1 325.9 363.5 768.2 681.7 688.9 +0 23.85294 P31948 STIP1 Stress-induced-phosphoprotein 1 42 100 0.024208618054835034 23.106913 23.85294 19.793936 18.430882 17.27703 22.887087 3.330707031418603e-93 1.6176191915988684e-72 3.8239570857229995e-74 4.888108232745789e-140 5.133150935641734e-72 1.692638276699045e-179 65 65 65 64 65 65 False False False False False False 60 60 58 59 58 59 135654.296875 135184.25 138295.140625 135722.546875 129209.96875 131780.21875 1589993 2468512.75 2525348.25 1800421.375 2291427.5 2461842.25 53866.5 53321.699 54688.5 51314.602 49477.301 50979.398 +7.328425509811671e-262 16.817263 Q06152 YLR271W Uncharacterized protein YLR271W 7 100 0.4043779580550405 8.947719 6.3756113 7.023412 16.817263 7.949093 11.848462 9.195712358114603e-21 2.7612307195055444e-5 1.57697526602539e-5 3.540865747109493e-131 8.58252608163047e-6 8.583364625158698e-19 7 7 6 7 8 7 False False False False False False 5 5 5 5 5 5 679.70745849609375 793.22210693359375 613.36041259765625 1577.72216796875 1301.9591064453125 1527.8011474609375 17180.46484375 36302.2578125 26767.314453125 57914.48828125 62257.12109375 54068.109375 197.3 188.7 203.7 461.7 473.1 492.2 +3.0999620188001146e-6 7.429243 Q8IU60 DCP2 m7GpppN-mRNA hydrolase 6 100 0.08125216469824723 5.281715 7.429243 6.9885674 6.6713324 6.6574087 4.4050145 9.211109667728311e-5 3.140848486273649e-7 1.6171642622580774e-5 2.292748083598365e-5 3.4789793463971876e-5 2.2054809138935362e-4 6 6 6 5 5 5 False False False False False False 5 5 5 5 5 5 740.0443115234375 727.93389892578125 667.65283203125 811.54608154296875 818.62591552734375 817.474609375 43403.09765625 88291.078125 68816.453125 64833.87890625 100699.984375 107490.6328125 128.4 129.6 120.3 150 141.5 135.8 +0 24.929756 P50085 PHB2 Prohibitin-2 9 100 0.32571801205020373 16.447527 19.352262 13.667647 17.167328 13.395677 24.929756 2.0535811336224397e-74 2.932959039912618e-62 2.9718916068237696e-43 4.46011984196455e-133 1.0914811079798873e-38 1.8537516318655665e-194 10 11 11 11 11 11 False False False False False False 9 9 11 10 10 10 9381.03515625 9545.0458984375 9124.61328125 17352.4296875 17361.962890625 17048.587890625 138457.953125 290473.5625 302286.625 240726.21875 451108.25 480024.0625 2857.1 2941 2938.3 5457 5247.1 5177.5 +0 19.801744 Q53SF7 COBLL1 Cordon-bleu protein-like 1 32 100 0.06244971791772376 11.780593 11.36469 8.748837 12.509706 13.67477 19.801744 2.5823627685688574e-62 1.3613947482825383e-45 3.942602622405162e-6 1.7960116570505692e-90 1.4424732200256224e-42 6.847408622122358e-158 31 32 33 33 30 32 False False False False False False 24 23 24 19 17 18 3555.798095703125 3412.756591796875 3336.808349609375 3840.64990234375 3207.978271484375 3456.519287109375 61944.31640625 92386.2421875 87522.5625 78437.5 93926.828125 112674.21875 366.1 374.8 363.8 363.7 315.1 348.4 +2.3520149088296646e-147 14.130347 Q9NR77 PXMP2 Peroxisomal membrane protein 2 5 100 0.04413300143545917 7.437853 8.718008 7.5011425 6.993771 14.130347 10.232518 3.032122140326704e-7 5.379127363751778e-18 9.606867977125166e-6 1.0470089004765265e-5 2.2027323045251294e-49 6.231134242684468e-8 5 5 5 5 5 5 False False False False False False 5 5 5 5 5 5 4493.54248046875 4558.31396484375 4272.23291015625 4554.06787109375 4498.91796875 4074.33837890625 136160.296875 232116.359375 214101.84375 145118.828125 230373.703125 215867.234375 1343.9 1405.6 1366.9 1344.2 1319.4 1197.1 +0 22.542837 Q06103 RPN7 26S proteasome regulatory subunit RPN7 17 100 0.32930103961688817 16.26743 16.038742 14.123478 17.01927 19.972078 22.542837 6.301790921320972e-74 4.6300690261290095e-55 4.167836226705093e-52 2.8348060011522844e-132 3.6579614953943457e-79 5.124393286326138e-177 21 22 21 21 22 22 False False False False False False 19 20 17 20 21 22 24140.853515625 24452.798828125 24469.72265625 46976.5703125 44483.58203125 44066.74609375 665108.75 1331068 1190606.25 1764272 2372137 2384155.5 8291.4 8389 8246.8 16088.2 15383.1 15487 +4.7223469722761333e-238 16.318665 Q504Q3 PAN2 PAN2-PAN3 deadenylation complex catalytic subunit PAN2 19 100 0.05659037512705039 15.271809 11.565882 10.583506 10.405803 8.596922 16.318665 2.899306487390282e-71 5.5615983231358025e-46 7.694846192179238e-9 3.2263087636867323e-42 5.004388739642144e-6 3.967875272302986e-114 20 19 19 20 19 20 False False False False False False 12 16 12 13 11 12 1899.4720458984375 2080.816650390625 1839.758056640625 2021.3973388671875 1975.48828125 1796.1209716796875 51637.953125 93484.59375 95761.3046875 80579.046875 131524.28125 129521.0859375 322.9 344.1 310.7 337.4 309.6 318.9 +0 19.894705 Q96N64 PWWP2A PWWP domain-containing protein 2A 9 100 0.07405409804110732 19.894705 7.010451 8.59669 6.6667504 6.447081 11.786845 5.7126800269778e-84 5.93259620906305e-6 4.313365394026342e-6 2.3037890366894172e-5 4.5685745189518044e-05 3.76138103634361e-18 9 9 8 9 9 9 False False False False False False 5 5 5 5 5 5 807.1148681640625 826.1600341796875 746.5098876953125 669.066162109375 769.23895263671875 801.3350830078125 55750.5234375 118800.078125 125268.2109375 70989.375 98055.546875 130556.984375 122.3 114 119.6 119.3 121.3 140.3 +0 22.477463 P06778 RAD52 DNA repair and recombination protein RAD52 13 100 0.418867498896984 12.273969 12.61879 13.26984 22.477463 15.00378 15.027897 1.5404220266544866e-63 4.855756244648814e-48 2.5186618343520827e-36 1.9647360781092066e-163 3.996772499935144e-64 3.953034864979061e-77 11 12 12 13 13 13 False False False False False False 8 6 7 9 12 12 1389.2027587890625 1048.6295166015625 1405.0372314453125 2935.913818359375 2690.55517578125 2857.783935546875 91701.8671875 209601.0625 183876.65625 143172.296875 252320.796875 267089 306.2 274.7 300.8 716 673.6 739.6 +1.7886820409798573e-5 6.171322 Q8NDA2 HMCN2 Hemicentin-2 1 83.3 0.3380597856722808 -1.7757349 4.4279346 6.171322 -0.5055147 -0.58579063 5.4189787 0.005479656339291159 1.6735953344099654e-4 4.748763513574101e-5 0.0034891922057231394 0.004522910579238933 1.0668269691237295e-4 0 1 1 1 1 1 True True True True True True 0 1 1 1 1 1 NA 168.7701873779297 181.88436889648438 79.39869689941406 237.8707275390625 173.96490478515625 NA NA 14784.095703125 7554.7763671875 2417.034423828125 14532.2138671875 NaN 0.7 0.7 0.3 0.9 0.7 +0 24.307652 P38439 LEO1 RNA polymerase-associated protein LEO1 13 100 0.36214331184860016 20.92961 15.610308 22.668314 24.307652 15.785379 16.567268 6.387609649877915e-87 3.717155092730093e-54 3.4918882063018327e-82 1.5573490099750552e-174 3.5223801441954886e-68 3.104365357991226e-121 11 14 12 14 13 14 False False False False False False 10 9 10 13 11 12 4121.3974609375 4017.7275390625 4161.95751953125 8174.2568359375 8024.921875 8238.7890625 131664.65625 239223.015625 191614.046875 206546.0625 355234.9375 346965.375 817.5 799.3 840.1 1640.4 1616.8 1623.8 +0 21.25853 P0CX55;P0CX56 RPS18A;RPS18B 40S ribosomal protein S18-A;40S ribosomal protein S18-B 12 100 0.3555366660894304 21.25853 13.571096 17.35112 18.161627 14.940487 14.729956 7.212132456764242e-88 5.991988400095897e-50 1.2645872645885614e-67 1.5225392576575314e-138 5.497511098202338e-63 1.4861013140184404e-69 23 23 23 23 23 23 False False False False False False 23 23 22 21 23 22 208044.515625 200732.765625 216001.734375 418296.625 388306.9375 413843.71875 5741632.5 8222430.5 8650188 15549739 16855148 18554000 135367.406;135367.406 132162.203;132162.203 136915.797;136915.797 236026.406;236026.406 239897;239897 250779.906;250779.906 +0 18.35067 Q9NQA3 WASH6P WAS protein family homolog 6 1 100 0.026379578488174744 18.35067 13.889523 9.553705 6.9147677 12.994098 16.842428 1.2319840577738322e-79 1.35877991323451e-50 1.7483170207833263e-6 1.3127184466679519e-5 1.6869588275093895e-33 2.6615219541667178e-129 1 1 1 1 1 1 True True True True True True 1 1 1 1 1 1 2223.759033203125 2240.213134765625 2147.90087890625 2144.950439453125 2140.2529296875 2088.290771484375 32829.6015625 43703.59375 44710.8671875 32388.642578125 36428.640625 41315.015625 123.5 124.5 119.3 119.2 118.9 116 +0 20.65261 P32912 VAM7 Vacuolar morphogenesis protein 7 12 100 0.403132866321025 17.210516 12.357926 10.034007 16.928394 20.65261 14.92861 1.720488481172638e-76 1.5892345535045705e-47 3.6712481591198107e-7 8.828005100663505e-132 5.226683998474692e-81 1.4793173102637813e-74 13 13 13 13 13 13 False False False False False False 9 9 11 10 11 12 1256.5982666015625 1182.5103759765625 1206.29541015625 2836.40869140625 2460.575439453125 2491.670166015625 32002.875 45546.2578125 56548.69921875 84391.3671875 119189.4140625 101858.2578125 548 496.9 585.1 1148.3 1025.8 1076.6 +6.168683687515546e-120 13.334271 Q8NC42 RNF149 E3 ubiquitin-protein ligase RNF149 7 100 0.0889066699097382 13.334271 6.685493 6.7540884 11.776528 11.189158 6.4799323 3.2258084110378015e-66 1.668941974805817e-5 2.2674089728901254e-5 2.3100167518304526e-71 6.674416600732507e-13 3.4436137127763985e-5 8 8 7 6 7 8 False False False False False False 6 5 5 5 5 5 907.448486328125 1143.7208251953125 986.83319091796875 1009.5853881835938 927.21722412109375 929.14599609375 93148.71875 153365.984375 126798.8515625 81271.15625 167275.59375 134123.78125 206.7 242.9 197.4 188.3 194.4 228.1 +0 17.882930000000002 O75448 MED24 Mediator of RNA polymerase II transcription subunit 24 23 100 0.05287656868158891 11.189199 17.34136 8.715668 11.552359 13.961243 17.882930000000002 2.876673137888912e-60 7.455071417637848e-58 3.999888590091858e-6 5.217543369080535e-66 8.77215508242185e-47 4.2910890973974645e-145 25 23 25 24 25 25 False False False False False False 20 19 18 18 17 18 3804.758056640625 3647.614501953125 3608.092041015625 4098.29638671875 3549.411865234375 3717.644775390625 58880.74609375 94802.703125 77805.765625 78643.9765625 109314.0859375 105020.3671875 619.5 594.5 619.1 672.2 649.3 683 +0 21.712162 Q9H1I8 ASCC2 Activating signal cointegrator 1 complex subunit 2 15 100 0.09363134303372453 18.397694 21.712162 10.8625765 12.418297 12.480478 15.403346 9.105476047312126e-80 1.3535598820870809e-67 3.666415197329657e-10 5.893540944896636e-88 2.0073598324644513e-27 2.6760554477738763e-87 19 19 19 19 19 19 False False False False False False 16 13 16 16 14 14 2955.36083984375 2679.625732421875 2798.19091796875 2948.944580078125 2260.00830078125 2751.423095703125 156175 214536.984375 179652.125 149579.859375 233329.765625 186471.171875 861.4 782.2 850.4 880.7 673.1 846 +0 17.900158 P51884 LUM Lumican 5 100 0.05013931593808289 14.132531 17.900158 7.435782 15.360593 13.536853 7.2485514 2.8423597465267863e-68 4.598581095342383e-59 9.935146842014858e-6 2.0516225058430492e-123 1.2710163851967196e-40 1.2639014855963094e-5 6 6 6 6 6 6 False False False False False False 6 6 6 5 6 5 37876.15625 39040.63671875 36970.3046875 42731.9609375 39078.0625 39011.078125 213800.359375 537818.875 449451.3125 362140.625 549661.5625 550539 8140.8 8405.6 8044.2 9108.7 8213.7 8342.8 +0 19.66769 P39730 FUN12 Eukaryotic translation initiation factor 5B 30 100 0.336850593878015 19.66769 16.757908 15.927979 18.45965 18.205261 19.220402 2.518882886187583e-83 1.345852715006132e-56 5.969746396426122e-64 3.3881593515910003e-140 1.9032265402380897e-74 5.8220161457663616e-154 38 39 39 40 40 40 False False False False False False 35 36 34 36 36 38 30438.203125 30889.890625 30225.623046875 60033.62890625 54994.48046875 57135.93359375 400308.03125 643776.3125 655106.9375 857307.3125 1246328.625 1306121.375 8721.5 8725.9 8724.7 17221 16088.6 16519.4 +0 20.419588 P53633 YIP3 Prenylated Rab acceptor 1 7 100 0.33230772550526233 13.225904 17.0514 11.153209 15.930491 20.419588 17.972618 6.101434836246647e-66 3.157716679540007e-57 6.643628183251629e-12 2.001942921565855e-126 2.272592337110263e-80 1.1085162824649112e-145 7 8 8 8 8 8 False False False False False False 7 8 7 8 8 8 8190.13818359375 8275.5498046875 7115.3095703125 14327.5478515625 14529.5576171875 15028.8466796875 118673.3359375 242663.65625 226396.109375 250140.625 384165.3125 373467 3428 3536.2 3241.7 6279.1 6259.3 6588.3 +0 18.16333 Q15750 TAB1 TGF-beta-activated kinase 1 and MAP3K7-binding protein 1 13 100 0.04339344447431874 16.031082 8.708901 9.520743 13.68971 8.729471 18.16333 2.729453085100045e-73 6.801758983210292e-18 1.8621133655071036e-6 8.665633663361521e-115 4.533066485056182e-6 6.22357194270785e-147 13 14 14 14 13 14 False False False False False False 7 9 9 10 8 8 2428.3740234375 2477.01171875 2445.811767578125 2395.3330078125 2189.086181640625 2354.390625 630788.9375 1337633 1360219.125 1006427 1045990.1875 1280650.75 517.8 552.7 560.4 516.7 515.2 508.6 +0 26.616314 Q01662 MAP1 Methionine aminopeptidase 1 16 100 0.3363349285532858 20.265 15.668501 12.6152935 14.893092 26.616314 18.421173 5.042172744535836e-85 2.8080245841774083e-54 1.519003082677122e-26 5.699939604193069e-121 1.0976920483679546e-97 1.2367636537369338e-148 21 21 21 21 21 21 False False False False False False 19 17 17 20 20 20 17472.31640625 16832.0078125 17093.275390625 32376.20703125 31734.81640625 32760.662109375 365920.28125 544328.625 543832.25 705409.1875 924837.25 1030303.25 5638.4 5476.1 5714.9 10329 9963.1 10383.3 +5.589089012810809e-202 15.511428 Q330K2 NDUFAF6 NADH dehydrogenase (ubiquinone) complex I, assembly factor 6 4 100 0.070733254469412 7.0782547 8.470967 9.8296500000000009 7.128439 10.115527 15.511428 4.350361070366462e-6 2.6620134649070775e-15 8.411890127303258e-7 6.643640215141803e-6 2.3716988152643457e-7 2.3557367812787505e-90 5 5 5 5 5 5 False False False False False False 5 5 5 5 5 5 2586.01416015625 2258.01953125 2194.655029296875 2129.81494140625 2219.230224609375 2340.83740234375 59453.26171875 121529.234375 119320 73919.8828125 123352.7578125 125541 400.5 399.1 389.1 378.5 379.1 402 +2.177801890558284e-148 14.158754 P38615 RIM11 Serine/threonine-protein kinase RIM11/MSD1 6 100 0.37961276714293685 14.104424 6.7084503 7.117735 10.592452 10.996316 14.158754 3.3604202205876378e-68 1.5928251284536196e-5 1.3832771302646115e-5 1.1059689693517456e-45 1.8846816227063843e-11 2.975773451258884e-56 6 6 7 7 7 6 False False False False False False 5 5 5 6 7 6 1268.1839599609375 1294.37841796875 1340.1268310546875 2769.317138671875 2571.698486328125 2676.218994140625 75642.734375 112706.75 99681.875 119698.3515625 158031.34375 150261.796875 274.7 270.4 292.5 592.7 572.2 542.1 +0 21.112062 Q9NZM5 NOP53 Ribosome biogenesis protein NOP53 11 100 0.05741803679171308 21.112062 15.074737 14.293005 11.429825 13.975288 7.986682 1.8928885357038722e-87 4.917023182300447e-53 1.2064135781020924e-55 3.516932773053255e-63 5.372499435476082e-47 6.2486462641364815e-6 13 13 13 13 13 13 False False False False False False 8 11 10 10 10 9 8354.7236328125 7594.8447265625 8212.736328125 8692.5322265625 7491.7587890625 7923.31884765625 193986.578125 357745.65625 314677.28125 285304.15625 394734.875 376552.53125 2655.1 2513.2 2658.2 2832.3 2483.7 2623.5 +3.0761160188556246e-5 5.4630456 O00481 BTN3A1 Butyrophilin subfamily 3 member A1 2 100 0.15279104744737695 4.0968704 3.250947 3.2648025 5.4630456 3.8888133 5.0477076 2.2087197590697096e-4 3.9032375777536517e-4 4.5509329050725476e-4 9.067314762823959e-5 3.7931037098406215e-4 1.366040608539894e-4 2 2 2 2 2 2 False False False False False False 2 2 2 2 2 2 392.1623840332031 241.6154022216797 346.6889343261719 347.50244140625 316.7432556152344 353.9503173828125 77320.3046875 162003.046875 167807.578125 108032.5234375 254682.4375 320881.875 30.3 19.8 25.7 25.8 24.9 29.1 +0 22.561909 P62491 RAB11A Ras-related protein Rab-11A 14 100 0.03383356638414832 22.561909 20.882275 14.956987 15.882323 14.6566725 17.076828 1.2361398211397241e-91 1.064646631534126e-65 1.6920722446484303e-61 3.607940686947895e-126 5.273078925055293e-58 2.1901210858214085e-136 18 18 18 18 18 18 False False False False False False 16 16 16 15 16 17 80197.3359375 77714.3984375 78214.4921875 80090.046875 73240.0546875 76144.109375 1001105.9375 1720753.125 1602257.625 1155691.25 1570352.875 1747853.75 29291.699 28637.801 28986.6 31691.301 29508.801 30940.9 +0 20.952557 Q70J99 UNC13D Protein unc-13 homolog D 32 100 0.029678863107490322 20.254866 10.886061 17.768728 14.46272 16.332115 20.952557 5.38085279635742e-85 1.1173777966068218e-44 1.0237797059396884e-68 9.632764537860714e-119 1.4161631822239813e-69 8.332063232092574e-166 41 43 41 41 43 40 False False False False False False 35 35 32 30 29 31 5988.6708984375 6395.77490234375 5982.57177734375 6306.0205078125 6049.2255859375 6004.97314453125 334841.34375 606323.8125 560306.3125 447177.78125 610725.8125 671935.125 1424.5 1808.1 1521.8 1440.1 2818.7 1411.9 +1.8278634008021584e-270 16.991644 Q08921 TCO89 Target of rapamycin complex 1 subunit TCO89 22 100 0.37431338074058984 10.9308405 8.583056 7.285821 16.991644 10.422754 16.982841 1.394203133371225e-54 1.6706148641162845e-16 1.1203217725302378e-5 3.995940393718152e-132 2.4758308805520152e-8 1.6030445679675807e-133 22 22 20 22 23 22 False False False False False False 12 12 9 17 17 17 881.51129150390625 992.80322265625 867.26434326171875 1951.62060546875 1866.211181640625 1702.183837890625 106972.671875 181767.171875 137603.34375 172415.0625 201009.375 218340.109375 236 240 184.7 490.1 465.3 444.4 +3.437566443887345e-103 12.799632 P67338 yoaH UPF0181 protein YoaH 3 100 0.4903012960936833 6.2432256 7.6763988 8.257066 3.9299393 4.49292 12.799632 3.473500851918569e-5 1.684836274130162e-8 5.576359288467216e-6 2.864307011295879e-4 2.523706344333398e-4 4.7802578941475955e-31 4 3 3 3 3 3 False False False False False False 4 3 3 3 3 3 777.98699951171875 1098.8172607421875 1184.9002685546875 269.81512451171875 457.338623046875 609.36590576171875 119639.0703125 132772.515625 129841.8984375 106228.984375 93556.8203125 92671.625 628.6 732.9 731.9 248.8 329.8 425.2 +0 19.674065 P0AET8 hdhA 7alpha-hydroxysteroid dehydrogenase 10 100 0.34881184897337447 10.487714 18.40175 14.92898 14.559994 7.361296 19.674065 2.210721824092283e-45 3.67576372145923e-60 1.9870772366205618e-61 3.029361340658734e-119 1.3006230096359855e-5 5.019249706570745e-157 10 10 10 10 10 10 False False False False False False 8 9 8 6 5 5 9545.1376953125 9303.1328125 10259.6298828125 5471.4521484375 4466.53125 5282.9755859375 128878.03125 283952.8125 261431.8125 170468.765625 221551.4375 266944.4375 2827.2 2904.1 2990.1 1640.9 1304.4 1463.6 +6.623381015642176e-212 15.74052 Q8TGU7 YBR126W-A Uncharacterized protein YBR126W-A 2 100 0.41721182777580285 12.1420145 6.679459 15.74052 6.8370686 8.376493 15.456102 3.286417265047763e-63 1.686780834909597e-5 1.7910520585735997e-63 1.6226135633446664e-5 6.0143991514597165e-6 8.779709964943496e-89 3 2 2 3 2 3 False False False False False False 3 2 2 3 2 3 2196.25537109375 2387.466064453125 2050.6025390625 5531.41455078125 4363.6591796875 4199.8916015625 146732.59375 222496.796875 260086.84375 191382.34375 367447.03125 352224 1435.8 1476.4 1235.3 3672.8 2685 2726 +0 23.389826 P00452 nrdA Ribonucleoside-diphosphate reductase 1 subunit alpha 27 100 0.3370602511587826 21.897919 19.591507 15.600987 17.865948 18.189764 23.389826 1.0246661697937139e-89 8.622565923671445e-63 4.057742052377195e-63 6.571279740564204e-137 2.089344378419411e-74 3.85253729918028e-183 34 34 34 34 32 33 False False False False False False 30 30 29 25 22 26 12538.865234375 12958.802734375 12508.5712890625 6994.40478515625 6404.08251953125 6761.40966796875 181844.796875 339179.375 297063.59375 174082.65625 231344.421875 237726.078125 2847.3 2848.9 2837.5 1604 1441.4 1532.6 +0 17.552315 Q12672 RPL21B 60S ribosomal protein L21-B 1 100 0.30045339268040766 9.165252 17.552315 8.568226 12.137648 13.690664 14.560946 1.2499748996219873e-23 2.609477153348595e-58 4.387622509178655e-6 1.81159308458957e-80 8.542432902372185e-43 1.8833958396350112e-65 3 3 3 3 3 3 True True True True True True 3 3 3 3 3 3 11652.435546875 11454.8369140625 12170.736328125 20448.177734375 19058.677734375 21801.42578125 289552.625 462877.25 446221.625 362020.4375 518558.8125 670450.5625 5357.1 5102.7 5393.3 8658.3 7990.7 8563.3 +1.6707338859733573e-53 10.8509 Q9H1H9 KIF13A Kinesin-like protein KIF13A 21 100 0.06918617440745482 10.8509 6.8243594 6.982732 7.016226 7.7710667 6.702001 7.512847145833319e-53 1.172370454795399e-5 1.620600449066968e-5 9.96859743100263e-6 9.68035246732738e-6 2.609990259608553e-5 21 20 21 20 21 20 False False False False False False 17 13 15 9 12 12 2143.50634765625 2291.7763671875 2151.930908203125 2314.685791015625 2565.21435546875 2199.49072265625 670355.0625 1121635.25 1091913.875 946607.4375 1403827.5 1466591.875 162.8 166.2 488.9 164.8 523.9 155 +0 26.294552 Q03280 TOM1 E3 ubiquitin-protein ligase TOM1 86 100 0.3696365554708501 22.792406 26.294552 22.869719 17.61902 25.616262 23.790874 2.6711256666086405e-92 2.262073964774477e-78 9.218881972334416e-83 1.511625995899915e-135 7.39279527847744e-95 4.710098021532678e-186 92 92 93 99 96 97 False False False False False False 64 61 65 83 84 84 3598.625732421875 3445.938720703125 3505.251220703125 7338.52490234375 6908.84033203125 6999.61572265625 446427.8125 759221.0625 725595 808968.1875 1099759.25 1134010 632.2 648.7 651.8 1372.6 1258.2 1308.9 +0 21.504963 Q96BN8 OTULIN Ubiquitin thioesterase otulin 20 100 0.01677677415477904 15.738313 20.370861 16.787357 19.138027 21.504963 18.971691 1.6622302325592982e-72 1.5421212654908447e-64 3.7086164123242723e-66 5.270828240288471e-144 2.4303622883367493e-83 2.69609346587866e-152 23 22 23 23 22 22 False False False False False False 21 20 18 20 17 18 16405.109375 16127.26171875 16152.01171875 16324.603515625 15630.5302734375 16076.87109375 187037.84375 356496.40625 338384.0625 218020.9375 335609.71875 340466.59375 5669.7 5631.9 5729 5488 5150.1 5519.5 +0 17.989948 Q05471 SWR1 Helicase SWR1 17 100 0.40976075080122915 7.258782 7.360248 7.8078465 7.015769 7.061382 17.989948 1.3918169582163712e-6 5.898852888627299e-7 7.773367873913906e-6 9.979354606584323e-6 1.9169823514822895e-5 8.537080766720914e-146 15 16 14 17 17 17 False False False False False False 7 8 9 12 14 14 1354.161376953125 1173.5963134765625 1491.261962890625 3064.47021484375 2620.645263671875 2973.244384765625 259191.203125 378088.4375 428282.125 475824.25 522559.34375 580892.75 113.1 108.1 124.4 230.8 214.3 242.5 +0 21.475315 Q07651 FMP45 SUR7 family protein FMP45 5 100 0.332167487175565 17.346766 13.682602 12.690924 6.983778 21.475315 15.880454 7.284431177494018e-77 3.564692207146207e-50 1.4247096623820119e-27 1.0710437046003956e-5 2.9179175572956783e-83 3.450246966917758e-101 5 5 5 5 5 5 False False False False False False 5 5 5 5 5 5 2851.615966796875 2816.35302734375 3064.824951171875 5562.39501953125 5370.017578125 5366.32568359375 68162.515625 134025 153533.109375 152504.15625 159240.96875 182496.15625 795.7 846.1 871.6 1709.9 1599.9 1612.7 +1.5076311099674907e-297 17.522835 P0A9S1 fucO Lactaldehyde reductase 8 100 0.23828959006985703 9.768189 8.472814 17.522835 5.5399547 6.639019 5.495533 1.650166586947674e-32 2.5458174574409187e-15 4.4986522002796546e-68 8.470766837502396e-5 3.547811986818742e-5 1.0080943659340802e-4 9 8 9 9 6 7 False False False False False False 7 5 7 5 5 5 3166.797119140625 2897.195556640625 2976.718994140625 2355.8994140625 2012.2977294921875 1661.18310546875 122940.421875 272507.5625 237807.765625 238984.515625 396290 391699.28125 649.8 602.4 631.3 419.3 252.6 325.8 +6.129273460439473e-31 9.597303 Q08919 TRE1 Uncharacterized protein TRE1 7 100 0.29132365533099147 2.4505382 6.5616956 6.7321925 7.5692472 9.597303 6.6582556 7.238680534465349e-4 2.0929236237411546e-5 2.342458614313898e-5 2.753916538801893e-7 1.7109153513770975e-6 2.7216220441396797e-5 5 6 6 7 7 7 False False False False False False 5 5 5 5 5 5 436.4785461425781 464.5811767578125 461.5273742675781 782.77349853515625 815.3614501953125 736.7073974609375 330161.96875 577787.0625 544060.4375 491801.9375 584161.5625 641554.5 46.1 48.9 66.5 111.5 125 108.9 +0 18.612358 P40970 LCB2 Serine palmitoyltransferase 2 18 100 0.387249059063025 16.077578 18.315947 13.312849 14.335721 18.612358 13.566549 2.0479457868648448e-73 5.682404522085355e-60 4.869070107242774e-37 4.338264298510772e-118 1.5936525148072993e-75 3.739702913426443e-44 22 23 21 23 23 23 False False False False False False 16 17 16 20 18 19 7499.78466796875 7609.177734375 7194.5927734375 16304.81640625 14983.888671875 15196.2529296875 354946.96875 669947.875 625704 650908.3125 989220.375 1020253.25 2087.1 2231 2111 4441.6 4172.5 4266.6 +0 22.707216 P49321 NASP Nuclear autoantigenic sperm protein 44 100 0.01765518751221107 21.717964 22.707216 22.41864 18.051262 19.044582 21.196892 3.385798364734313e-89 6.97804476169634e-70 1.7909198242562422e-81 6.207254029702388e-138 1.121395853790201e-76 1.6822092392714237e-167 51 50 51 51 50 49 False False False False False False 45 44 45 45 41 44 80615.8515625 79995.4453125 80087.4296875 81560.3359375 77423.6953125 79171.1484375 467959.21875 784976.5 779438.3125 572701.875 736249 777656.375 17305.699 16926 17058.199 17302.9 16075.4 16614.301 +9.0720785453983e-310 17.754375 P33329 PPZ2 Serine/threonine-protein phosphatase PP-Z2 18 100 0.32729801648490786 11.334677 8.6592 7.0431476 10.041447 17.754375 12.314925 3.2409733584180934e-61 2.43965590606031e-17 1.5358567868441538e-5 6.543439986764946e-36 2.9162983290629195e-73 2.991924309786176e-24 19 18 19 19 19 19 False False False False False False 10 13 9 14 15 18 9088.5029296875 8313.5849609375 9436.7041015625 17170.794921875 15473.984375 16653.91796875 104302.90625 247342.4375 200693.625 203466.828125 333035.8125 354604.46875 984 728.2 1103.4 1398.5 1247.4 1402.9 +6.4617264603e-313 17.81326 O95999 BCL10 B-cell lymphoma/leukemia 10 7 100 0.061183677483753245 13.93691 14.423188 17.81326 6.937406 7.062004 6.691715 9.113698574780438e-68 1.0975381210405456e-51 7.831526974888063e-69 1.2276578572589282e-5 1.915487787660682e-5 2.638931657617731e-5 7 7 7 6 7 7 False False False False False False 5 6 6 5 5 5 1929.631591796875 1971.502197265625 1799.3748779296875 2083.365478515625 2145.06103515625 2025.320068359375 179198.5625 429777.1875 366191.71875 452764.40625 611956.6875 522676.625 749.9 750.9 769.7 632.9 641.6 720.9 +0 23.637068 P53075 SHE10 Outer spore wall assembly protein SHE10 19 100 0.3588830355931161 20.05691 17.979671 12.034542 17.703931999999998 23.637068 17.734003 1.9792175651156884e-84 3.080022061844749e-59 1.5269030754041464e-19 5.178865598493862e-136 3.0203521820934814e-89 4.001829056283645e-144 23 22 23 23 23 23 False False False False False False 19 17 20 23 22 20 10845.0712890625 11291.25 10649.5283203125 22108.8046875 21026.810546875 21486.595703125 141860.671875 248901.71875 232228.734375 293324.6875 420779.15625 413622.25 3762.6 3934 3833.1 7547.8 7107.7 7300.6 +0 21.329233 O96000 NDUFB10 NADH dehydrogenase [ubiquinone] 1 beta subcomplex subunit 10 11 100 0.02897846597776368 18.004116 14.059209 13.440951 15.528779 21.329233 17.570213 1.122735728592948e-78 6.129203631601033e-51 3.246572404372888e-39 2.670213990058328e-124 7.323871293724924e-83 4.66056747489976e-143 13 13 13 13 13 13 False False False False False False 9 10 10 11 10 11 23257.712890625 23900.63671875 23265.681640625 23044.21875 21943.33203125 22606.154296875 256658.671875 628580.75 532309.5625 373294 617360.375 667421.125 11398.1 10803.6 11479.1 10533.4 10039.2 10522.8 +4.378565091967e-311 17.779043 P35240 NF2 Merlin 15 100 0.04205102077068319 16.26683 8.679593 8.674474 17.779043 11.787928 13.59506 6.324699598993004e-74 1.4479648052185684e-17 4.105966103856124e-6 1.9955448334023493e-136 9.724972942742511e-19 1.0709494436452214e-44 16 16 17 17 16 17 False False False False False False 8 11 9 10 9 11 2101.060302734375 1869.1690673828125 1989.5128173828125 1925.9769287109375 2046.7908935546875 2019.0560302734375 81481.15625 129219.2890625 116134.0234375 95662.53125 136247.875 130099.8984375 350.2 344.8 361.9 387.8 383.6 432.7 +0 24.894474 Q8WUM4 PDCD6IP Programmed cell death 6-interacting protein 53 100 0.02226162704829174 24.894474 18.2818 18.208384 17.03154 21.226482 21.010973 1.9364526809117913e-98 6.749285622505254e-60 7.030914701249032e-70 2.4320947578477128e-132 1.4007232291731416e-82 3.293054839932904e-166 71 71 71 70 69 71 False False False False False False 61 65 66 62 60 57 44569.14453125 45411.4140625 44192.68359375 46516.984375 43672.921875 44942.3125 413004.15625 711949.8125 695485.25 478001.90625 693846.75 712240.0625 15319.1 15466.5 15101.3 15861.6 14522.4 15024 +5.538611811519355e-144 14.036918 Q7Z6K3 PTAR1 Protein prenyltransferase alpha subunit repeat-containing protein 1 13 100 0.04386768184236795 14.036918 8.540086 10.372484 10.402604 7.3768697 12.671641 5.023339265405187e-68 4.866342867949827e-16 4.6490982351666246e-8 3.686554960965393e-42 1.2827609742785403e-5 3.852175281942243e-29 12 13 13 13 13 13 False False False False False False 8 10 8 8 9 10 2241.61865234375 2279.185302734375 2208.32763671875 2354.732666015625 2060.83984375 2205.80126953125 100557.921875 157771.25 159315.796875 132819.296875 180278.5625 194889.5 555.6 591.3 554.6 473.6 549.3 573.8 +0 23.121532 P08839 ptsI Phosphoenolpyruvate-protein phosphotransferase 23 100 0.32309225829732074 23.121532 18.647135 16.583319 21.579102 18.210756 16.870201 3.0178463467912724e-93 1.0530137437642105e-60 1.2452314516895844e-65 4.41962301973154e-158 1.839416115055228e-74 3.944784438927758e-130 27 27 27 26 26 27 False False False False False False 24 25 24 19 17 17 26202.138671875 26390.52734375 26868.9140625 14715.2841796875 13975.8359375 14609.0244140625 249314.046875 479396.375 445019.5 288326 353081.5625 436015.6875 6528.4 6674.5 6666.3 3618.4 3510.9 3605 +6.267134210920348e-238 16.316025 P38342 TSC10 3-ketodihydrosphingosine reductase TSC10 7 100 0.4170406042335987 11.380291 8.2943125 12.572841 10.836789 13.473308 16.316025 2.5026591896610923e-61 1.7296537408914523e-13 5.582988762497983e-26 1.853201356483281e-50 9.587713441718007e-40 4.716878081257364e-114 7 7 6 7 7 7 False False False False False False 6 5 5 6 7 7 2587.42138671875 2255.81005859375 2723.80126953125 5908.7958984375 5251.6376953125 5534.291015625 96686.5390625 175985.671875 193175.4375 204076.5625 250643.53125 334730.09375 575.6 543.3 600.7 1292.3 1172 1249.8 +0 24.115328 Q06330 RBPJ Recombining binding protein suppressor of hairless 14 100 0.06823529485466921 12.089721 15.418301 24.115328 13.426149 8.416063 18.299887 4.42860777882652e-63 9.428877661729004e-54 2.395388748979242e-86 1.8643677546159674e-113 5.811264522611776e-6 7.837582712421953e-148 16 16 16 16 15 16 False False False False False False 11 13 12 10 10 12 1817.3463134765625 2192.118408203125 2106.5546875 2099.252685546875 2053.31689453125 2211.838623046875 267415.09375 349775.28125 349544.84375 419674.21875 453876.75 500243.71875 612.9 732.9 674.2 655.7 626.7 732.1 +0 18.026878 Q9HAB8 PPCS Phosphopantothenate--cysteine ligase 13 100 0.02968218502995235 16.871086 15.28589 10.201464 11.719404 18.026878 14.231475 1.4461217221264326e-75 1.780104442032127e-53 1.485885939749442e-7 5.6199928523198553e-70 5.583889718187801e-74 7.411380426460097e-58 13 13 13 13 13 13 False False False False False False 12 13 12 13 13 13 17093.037109375 17551.33984375 16432.1328125 17862.962890625 16930.318359375 16907.40625 323832.5 502283.75 487801.53125 365783.46875 673554.125 639454.875 6832.6 7013.7 6583.9 7168.1 6344 6661.3 +0 26.077126 Q8N1F7 NUP93 Nuclear pore complex protein Nup93 47 100 0.021286856392831982 22.79665 22.147127 17.396532 17.753942 26.077126 17.216541 2.6033000596590673e-92 1.417268726923271e-68 9.607079415436215e-68 2.74561848678483e-136 3.682919203054611e-96 1.957711228645954e-140 59 60 61 61 61 59 False False False False False False 53 49 49 54 50 50 31270.25 30901.701171875 31129.75 31612.89453125 29702.32421875 30726.90234375 245691.578125 471711.1875 448476.375 361373.5 480862.875 485770.625 9625.8 9467.6 9566.4 9703.4 9278.2 9503.1 +0 24.042097 Q8NBL1 POGLUT1 Protein O-glucosyltransferase 1 12 100 0.03339975792546757 15.605444 9.797566 14.510113 24.042097 20.720427 22.081211 3.761280072001769e-72 8.295102426828228e-33 4.4233466422537407e-60 6.647049129258373e-173 3.41957000709833e-81 9.885196545663066e-174 12 13 12 13 13 12 False False False False False False 9 11 10 11 9 9 6310.5126953125 6076.6494140625 5944.96728515625 6121.185546875 5712.181640625 6122.4541015625 115272.546875 251781.609375 240078.546875 297308.875 350289.65625 350555.75 1310 1348.6 1376.1 1331.8 1193.5 1340.1 +6.385353703821247e-6 6.9814215 Q86T24 ZBTB33 Transcriptional regulator Kaiso 5 100 0.1060724387546206 5.567951 5.874708 6.9814215 5.012561 6.4945993 5.6582036 7.199897388106567e-5 4.840394387473588e-5 1.6220808088182392e-5 1.3159252571013293e-4 4.339364262791294e-5 8.872414879929213e-5 5 5 4 5 5 4 False False False False False False 5 5 4 5 5 4 1249.0518798828125 989.259765625 1030.49755859375 1151.4130859375 1164.3199462890625 953.8917236328125 357949.3125 380131.34375 1187105.625 271982.71875 585102.125 672351.5 116.4 99.4 83.2 114.3 103.4 85.9 +0 23.1997 P38759 VPS29 Vacuolar protein sorting-associated protein 29 10 100 0.35616787058689187 14.80486 18.156052 14.814294 23.1997 17.291124 20.29053 4.963111858387713e-70 1.2680694412775452e-59 3.8465919812743254e-61 8.974589324152276e-168 4.71343815295563e-72 3.118215003193937e-161 12 12 12 12 12 12 False False False False False False 8 10 10 11 11 12 3377.609130859375 3271.34130859375 3323.414794921875 6628.81298828125 6534.24072265625 6403.81787109375 514041.09375 879919.9375 762850.5 1082164.875 1547613.875 1720198.75 1322.7 1230.8 1241.7 2273.2 2504.6 2498.9 +9.427648617549672e-196 15.364722 P56937 HSD17B7 3-keto-steroid reductase/17-beta-hydroxysteroid dehydrogenase 7 6 100 0.1320645805529938 6.9296412 7.4471817 6.6910667 15.364722 10.905694 6.290949 8.46827981364346e-6 2.635459205544066e-7 2.4766412945584102e-5 1.9520041276452297e-123 7.70001073270373e-11 4.393719417905404e-5 7 7 7 6 7 7 False False False False False False 5 5 6 5 5 5 6581.0517578125 6761.77978515625 8350.4580078125 6195.451171875 5738.419921875 6629.3046875 453865.96875 1083922.125 862467.3125 910386.5 1254328.875 845999.75 1235.7 1284.8 1282.1 1977.8 1307.9 1336.9 +0 21.080008 Q9Y4R8 TELO2 Telomere length regulation protein TEL2 homolog 33 100 0.016738081760080657 16.779364 18.82114 14.277692 15.326853 21.080008 19.289633 2.5719166701385723e-75 4.3833935306490125e-61 2.549075997713163e-55 3.083740222901374e-123 3.522067738708685e-82 1.9972938796363326e-154 35 34 35 35 34 35 False False False False False False 25 25 28 26 25 24 6870.61865234375 6854.87890625 6853.28271484375 7102.16796875 6764.42919921875 6948.43896484375 111868.265625 193823.671875 182816.296875 156797.421875 202878.4375 205442.203125 1846.3 1821.3 1841.1 2093.7 1956.4 1958.4 +5.3525503193383685e-52 10.778375 Q86U28 ISCA2 Iron-sulfur cluster assembly 2 homolog, mitochondrial 6 100 0.14202767706728764 9.536198 6.2666187 7.0116186 7.0120397 8.574452 10.778375 6.726932060563819e-29 3.118265618833392e-5 1.592254247163611e-5 1.0061961365099753e-5 5.089033427440906e-6 3.043982429752821e-10 6 6 5 6 6 6 False False False False False False 5 5 5 5 5 5 2608.39697265625 2266.2119140625 2427.094482421875 3358.43408203125 2563.879150390625 2702.936767578125 94583.4921875 139624.53125 154717.71875 153787.234375 232218.09375 214356.171875 469.1 697.3 664.6 553.2 694.3 754.4 +6.057666434715413e-6 7.0111365 Q12770 SCAP Sterol regulatory element-binding protein cleavage-activating protein 14 100 0.1483666681076057 5.508733 7.0111365 6.7018814 5.0079584 6.657938 5.1670823 7.583623087322203e-5 5.9155024302011035e-6 2.448321034154631e-5 1.3179377660800417e-4 3.4789793463971876e-5 1.2720335313519664e-4 14 13 14 12 14 14 False False False False False False 8 6 7 5 5 7 628.38671875 766.576904296875 544.07952880859375 646.0848388671875 523.50738525390625 554.77447509765625 326375.6875 543558.5 528644.1875 235984.96875 468051.4375 500579.1875 141.1 108.2 127.7 79 99.1 122.6 +9.83426958374845e-143 14.002005 Q03466 EBS1 Nonsense-mediated mRNA decay factor EBS1 13 100 0.3004042995443566 6.4812245 5.3005986 6.675425 7.0996714 6.9900584 14.002005 2.5211528230983934e-5 8.423666270189104e-5 2.5083631760889534e-5 7.438917883716237e-6 2.1151009010671652e-5 6.970661385820021e-53 12 13 11 13 13 13 False False False False False False 7 6 8 11 11 12 1101.7320556640625 964.37408447265625 950.43695068359375 1919.077880859375 1565.46435546875 1657.555908203125 276147.46875 609287.625 540625.1875 502733.6875 774427.5 717125.3125 159.8 142.8 139.9 303.6 245.7 283.3 +0 24.824781 P12004 PCNA Proliferating cell nuclear antigen 15 100 0.03513210770362393 18.650537 19.250322 11.718639 17.221615 18.934336 24.824781 1.7978871268445655e-80 4.9393907942071895e-62 2.1602181241395216e-16 2.2587103016474685e-133 2.2144540056725876e-76 1.1364343414763915e-193 21 21 21 21 21 21 False False False False False False 21 21 21 20 20 21 161347.75 169087.796875 164837.5625 163327.03125 152625.65625 158283.578125 2752979.75 3730814.5 3648718.25 3241834.5 3609575 3764759 72926.602 76852.102 74031.703 73455 71294.102 72173.398 +0 14.666674 Q96GE9 DMAC1 Distal membrane-arm assembly complex protein 1 1 100 0.19947636730970045 7.400291 6.492469 6.7527313 14.350612 14.666674 5.543337 4.3091898841387706e-7 2.3476847011209753e-5 2.2717501697270258e-5 3.637218102302215e-118 3.5530883568758044e-58 9.730514235183604e-5 2 2 2 2 2 2 True True True True True True 2 2 2 2 2 2 2967.61865234375 3211.8818359375 2532.9755859375 3684.445556640625 2927.203857421875 2001.5478515625 156483.515625 249507.890625 240366.6875 128394.09375 152852.828125 158096.875 1442 1561.1 1505.4 1806.2 1439.2 1353.3 +2.8170940533607923e-171 14.763263 Q96K58 ZNF668 Zinc finger protein 668 14 100 0.05587793484116442 12.582891 7.270171 12.817324 14.763263 6.813241 14.272221 2.5917639986340248e-64 1.243010585339045e-6 2.3811850649531656e-29 2.6942358597126005e-120 2.7805720671156153e-5 9.116250648312513e-59 14 15 14 14 14 15 False False False False False False 9 11 12 9 9 10 4148.564453125 3844.662841796875 3842.831787109375 4271.6865234375 3711.595703125 3806.300048828125 276465.625 559113.4375 448770.59375 534385.375 1032426.875 827827.4375 741.9 717.4 720.7 902.1 749.7 856.7 +0 23.482811 O14497 ARID1A AT-rich interactive domain-containing protein 1A 54 100 0.04351770245917772 22.349104 14.278915 12.718605 14.84619 18.600266 23.482811 5.136631085281216e-91 2.176763352876084e-51 5.903358736042533e-28 9.988403481794497e-121 1.7149964710671367e-75 8.274437460657018e-184 60 59 61 60 59 61 False False False False False False 44 45 52 43 44 47 4447.73828125 4215.23095703125 4508.14990234375 4521.65380859375 4180.865234375 4085.912353515625 189746.78125 331285.65625 287845.53125 214946.25 310514.3125 310492.15625 826.4 759.6 831.2 842.9 744.9 765.6 +0 26.039026 Q96S55 WRNIP1 ATPase WRNIP1 21 100 0.02527628808440222 23.346214 17.820673 16.053934 8.525974 26.039026 18.141075 6.722383362711303e-94 6.813819258375997e-59 2.8483200962566094e-64 8.737920106529775e-16 4.7193991258499194e-96 8.716233358511331e-147 22 23 22 23 23 23 False False False False False False 18 20 19 18 19 18 11515.4638671875 12095.4833984375 11577.9423828125 11990.248046875 11316.0859375 11688.3271484375 133866.9375 231612.046875 216134.671875 179617.46875 261061.4375 252938.25 2256.9 2296.9 2188.6 2244.2 2173.8 2223.8 +0 19.022552 P22276 RET1 DNA-directed RNA polymerase III subunit RPC2 24 100 0.3612949804935078 12.991953 8.644251 13.589217 16.326504 15.273684 19.022552 2.3999966548037388e-65 3.574368623040213e-17 7.89887377677463e-42 1.557231455842444e-128 6.953577635334001e-67 1.2361610070204068e-152 26 23 24 25 26 26 False False False False False False 16 14 14 22 19 22 2187.89697265625 2331.20068359375 2219.289306640625 4390.10400390625 4375.9150390625 4579.48876953125 116566.4296875 233873.671875 197908.984375 259598.5 404967.28125 363685.25 402.8 381.4 410.8 779.5 770.3 834.5 +0 21.98006 Q9UN86 G3BP2 Ras GTPase-activating protein-binding protein 2 19 100 0.04184115416186975 18.63291 13.9273 20.532486 18.435017 21.757608 21.98006 2.015295848202618e-80 1.1372334393514646e-50 3.503237791344566e-76 4.637758285905651e-140 4.867770195590143e-84 5.180258013832218e-173 30 30 30 29 30 30 False False False False False False 26 26 26 25 26 25 23102.318359375 24018.77734375 24563.822265625 23815.865234375 21992.421875 22498.46484375 336200.59375 556587.75 590868.875 390967.09375 581799.1875 602944.625 8144.2 8561.5 8643.9 8306.6 7860.1 7926.7 +0 20.062834 P60228 EIF3E Eukaryotic translation initiation factor 3 subunit E 23 100 0.03055312264647142 19.285276 13.892427 15.233119 19.916965 20.062834 17.640522 2.995981391074582e-82 1.340419617432652e-50 3.4399912361789605e-62 1.9434959018340352e-148 2.08688529296899e-79 1.6231388005099316e-143 27 27 27 27 27 27 False False False False False False 24 24 25 24 24 22 33191.9140625 33204.61328125 32953.5859375 31968.365234375 31071.26953125 31202.0390625 458969.25 827474.3125 758390.875 548449.875 793155.375 835578.3125 13339 12889.9 13237.2 12823.4 12003.4 12110.2 +6.594644956528047e-6 6.964682 Q8NB14 USP38 Ubiquitin carboxyl-terminal hydrolase 38 5 100 0.1662927442569621 5.4981647 6.8330536 6.964682 6.6798053 2.5892296 6.6774836 7.657403688254247e-5 1.1417598564202234e-5 1.6565609662526712e-5 2.2733798431315848e-5 9.689984725604254e-4 2.6756082278080138e-5 4 5 4 4 3 5 False False False False False False 4 5 4 4 3 5 246.69473266601562 378.3717346191406 322.97088623046875 260.0608215332031 302.6088562011719 354.3230895996094 74892.46875 128128.21875 98265.4375 69494.234375 103604.7578125 91153.9609375 15.1 30.1 25 24.6 13.6 30.3 +1.802462952160597e-69 11.557836 P38291 POP7 Ribonucleases P/MRP protein subunit POP7 2 100 0.4943982980360336 5.336984 6.072816 5.780657 8.827887 8.2165 11.557836 8.858960904429434e-5 3.9042212153717374e-5 6.986627198781783e-5 3.7390718465048934e-19 6.961201031633907e-6 6.298001887732789e-16 3 2 3 3 3 3 False False False False False False 3 2 3 3 3 3 624.02899169921875 566.31689453125 579.9686279296875 1706.8564453125 1500.5396728515625 1341.2369384765625 85802.4609375 185629.484375 84725.2421875 91389.6171875 165523.03125 176819.125 285.1 220 253.9 661 593 512.3 +6.822626329508722e-286 17.297417 Q96CS2 HAUS1 HAUS augmin-like complex subunit 1 10 100 0.08505128154400661 17.297417 10.365262 7.5482035 6.997255 13.517806 7.4751205 9.959067602329679e-77 7.531866852366417e-43 9.270665810163648e-6 1.0392848844990317e-5 2.3368217476095874e-40 9.726793355381315e-6 11 10 11 12 12 12 False False False False False False 7 9 9 6 7 9 1873.5865478515625 2157.00439453125 1808.195068359375 2160.9931640625 1932.9276123046875 2208.826416015625 103328.1484375 215299.34375 177987.109375 107284.3125 236000.484375 186514.609375 578.2 656.1 600.2 659.9 642.1 687.3 +3.189585263967032e-6 7.377127 P0ADK0 yiaF Uncharacterized protein YiaF 2 83.3 0.3923534074784337 6.1644716 4.870938 7.377127 -1.0643392 5.503917 3.0885081 3.81532782793919e-5 1.209567530223228e-4 1.0249569002363575e-5 0.004335285907795768 1.133404309806506e-4 5.596951390500246e-4 2 2 2 0 2 2 False False False False False False 2 2 2 0 2 2 521.37823486328125 491.7812805175781 407.5707702636719 NA 471.472412109375 130.61964416503906 89367.4140625 167302.15625 153314.375 NA 107996.375 349762.125 97.6 94.3 93.8 NaN 111.6 88.1 +0 21.634817 Q9H869 YY1AP1 YY1-associated protein 1 7 100 0.12615369104722285 7.373562 21.634817 7.2510815 7.0057034 14.112332 9.183931 5.472232905311963e-7 2.03137504807244e-67 1.1654476813901949e-5 1.0199856959701042e-5 4.2065410632348836e-49 2.2408489520117953e-6 8 8 8 7 8 6 False False False False False False 5 5 5 5 5 5 903.8499755859375 825.25469970703125 798.2010498046875 830.845947265625 608.6448974609375 847.71331787109375 37280.62109375 57063.39453125 46237.51171875 60434.9921875 88031.84375 91310.1640625 119.7 108.4 125.9 106.9 116.5 123.4 +1.1546499914699086e-281 17.214474 P26449 BUB3 Cell cycle arrest protein BUB3 8 100 0.4570624798315921 6.4209456 5.8804116 6.663464 6.9923987 11.884408 17.214474 2.755389457122034e-5 4.812319895714006e-5 2.5226077889045994e-5 1.0501067663851872e-5 7.246891167843693e-20 2.153777872580323e-140 8 7 8 8 8 8 False False False False False False 5 5 5 5 5 7 542.87078857421875 487.4093017578125 645.6689453125 1361.95703125 1408.2509765625 1247.6053466796875 278003.5 809815.1875 489179.5 515452.71875 756365.375 726449.875 113.3 90.8 164.1 263.1 267.6 272.3 +1.9230820762066446e-5 6.08063 Q63HQ0 AP1AR AP-1 complex-associated regulatory protein 3 100 0.3014405548385433 4.210531 5.7844954 6.08063 0.29448804 4.2341523 5.0107727 2.0522653490611433e-4 5.31052346666109e-5 5.255455190929526e-5 0.0026239283006062606 3.019024472919865e-4 1.3975116146401896e-4 2 2 1 2 3 2 False False False False False False 2 2 1 2 3 2 425.2494201660156 436.5215148925781 642.47955322265625 305.6153259277344 287.5849304199219 427.66033935546875 54531.8671875 126065.171875 137383.5 74232.7734375 110937.3828125 150182 53.9 59.8 65.4 45.5 45.5 62.5 +2.870611037339408e-6 8.405179 P32617 IES6 Chromatin-remodeling complex subunit IES6 7 100 0.3889731937021661 5.8439937 8.060444 6.680442 6.0381193 8.405179 6.9877906 5.446627508739704e-5 2.5774288951213704e-11 2.497140584026294e-5 5.33512023168714e-5 5.865713672414343e-6 1.7852420570049043e-5 7 8 8 8 8 8 False False False False False False 6 6 5 5 6 6 1906.520751953125 2197.443359375 1771.7642822265625 4544.62060546875 3593.325927734375 3739.63671875 268678.4375 528205.5625 494222.25 431889.40625 794863.9375 723427.5625 782.5 1070.7 933.3 2058.7 1390.3 1769.3 +0 24.687008 Q9UI26 IPO11 Importin-11 21 100 0.03682482111715074 20.941652 24.687008 19.730007 19.494738 20.847055 22.109297 5.889350088542362e-87 1.6203577872525716e-74 5.7182331929079816e-74 4.998759456619541e-146 1.5439983543258301e-81 6.276920742068718e-174 30 29 30 30 29 28 False False False False False False 23 25 23 22 22 22 6520.81005859375 6823.08447265625 6742.08203125 7160.1796875 6730.06591796875 6463.576171875 110366.4296875 194541.84375 180242.703125 125439.5546875 182324.796875 176852.5 1443 1533.7 1586.6 1639.7 1533.8 1444.3 +0 24.106613 Q92734 TFG Protein TFG 13 100 0.026649760687146796 24.106613 12.524736 18.276794 18.273754 7.6123238 16.235857 4.058714063989226e-96 7.45330641211789e-48 4.618579233202054e-70 3.635196951101707e-139 1.0791916574814376e-5 9.333794021654051e-112 15 15 14 15 15 15 False False False False False False 12 14 12 13 14 14 30839.349609375 31402.154296875 31392.400390625 29976.62890625 29581.88671875 29820.59375 657518.375 1172520.125 1095451.75 855776.25 1293738.375 1177447 11948.7 12004.5 11965.2 11276.3 11167 11368.8 +0 21.176558 Q05639 EEF1A2 Elongation factor 1-alpha 2 10 100 0.009500513303100845 18.025728 17.351116 15.892169 19.805908 21.176558 13.55397 9.776497951206796e-79 7.117628434259503e-58 7.381106919683777e-64 8.402129148562414e-148 1.911772489477426e-82 6.471946700407731e-44 14 14 14 14 14 14 False False False False False False 12 13 11 12 13 12 22907.595703125 23178.095703125 23352.00390625 22958.78515625 23348.869140625 23431.267578125 486834.625 894727.0625 813281.5 604338.6875 907675.5625 905018 5492.4 5494.7 5570.5 5601.7 5405.7 5471.5 +3.246830850746978e-6 7.35523 Q9BSK0 MARVELD1 MARVEL domain-containing protein 1 2 100 0.23131333738109833 4.168752 7.35523 3.2796855 5.6833334 6.8692384 4.842428 2.1086192046455284e-4 6.165285701537313e-7 4.5063945010104776e-4 7.471328079004101e-5 2.5452337332710196e-5 1.5696333504094912e-4 2 2 2 2 2 2 False False False False False False 2 2 2 2 2 2 2352.21728515625 2161.707275390625 1399.0006103515625 1825.689208984375 1441.679931640625 1454.464599609375 161829.203125 349632.03125 224731.90625 115607.453125 169669.5 190565.90625 394.6 392.2 250.1 369 331.4 359.9 +0 21.825691 P16370 RPB3 DNA-directed RNA polymerase II subunit RPB3 10 100 0.3993116702227452 12.591966 20.534122 13.030213 19.845457 13.837761 21.825691 2.459245145728068e-64 6.531892703462579e-65 1.638444241609501e-32 4.995454350172966e-148 6.147962765801196e-45 6.39717883405905e-172 14 14 14 14 14 14 False False False False False False 10 10 10 12 14 13 2793.38525390625 2820.848388671875 2966.95263671875 6497.107421875 5658.0703125 6091.14892578125 261823.796875 627570.75 526938.5 597162.5625 874635.5625 995956.9375 899.2 981.9 995.6 2121.3 1983.6 2056.5 +3.753375935219937e-159 14.448687 P32345 PPH3 Serine/threonine-protein phosphatase 4 catalytic subunit 5 100 0.33486842869859546 5.607448 3.8520496 5.122296 6.7566795 6.879863 14.448687 6.936364500873877e-5 2.4970742516421807e-4 1.1911614937244889e-4 1.967981815001568e-5 2.50244781754735e-5 8.367162243571911e-63 1 3 5 4 5 5 False False False False False False 1 3 5 4 5 5 806.04107666015625 975.73944091796875 873.75994873046875 1781.983642578125 1479.8497314453125 1616.8226318359375 216778.84375 331339.65625 262089.1875 368144.25 601249.875 453017.71875 51.9 181 202.1 357.3 319.3 356 +2.890938888860181e-196 15.376907 O75582 RPS6KA5 Ribosomal protein S6 kinase alpha-5 17 100 0.06231465302320852 10.677911 8.636185 10.758814 14.867035 9.014801 15.376907 3.2708052309846266e-49 4.3881320127967815e-17 1.2452872306935715e-9 7.781965797156779e-121 3.6873749960025894e-6 1.4658762496560786e-86 23 24 24 24 23 24 False False False False False False 17 16 15 16 14 18 2308.894775390625 2301.067138671875 2348.553955078125 2539.740966796875 2086.01708984375 2323.4033203125 60357.16015625 116093.96875 109117.75 92165.953125 141115.046875 138747.734375 457.5 481.2 429.5 532 398.7 496.8 +2.3744073563515727e-5 5.7973475 Q6P1L5 FAM117B Protein FAM117B 3 100 0.4449185494572167 2.3302355 3.1047318 4.056741 4.27634 5.7973475 0.60999876 7.82441543470869e-4 4.360225778893212e-4 2.58890751805409e-4 2.2431124144482828e-4 8.943932960463647e-5 0.002411889300806093 3 3 3 3 3 2 False False False False False False 3 3 3 3 3 2 193.2321319580078 228.77883911132812 313.120849609375 248.98696899414062 36.91852951049805 257.4150390625 192677.34375 270905.09375 276407.0625 176890.015625 382606.5625 305078.53125 17.6 28.3 28.5 22.5 5 15.9 +0 25.947332 Q9UHD8 SEPTIN9 Septin-9 39 100 0.028519892323595298 18.72206 25.947332 18.879627 23.777693 19.990267 19.171871 1.1361605376555894e-80 1.5199263004382689e-77 1.1380299562332471e-71 2.721650710076417e-171 3.2653028683590984e-79 1.2307202106432784e-153 44 44 44 44 44 44 False False False False False False 42 36 38 37 37 36 24359.677734375 23196.830078125 24461.826171875 24371.939453125 22876.03515625 23674.8828125 267334.09375 570687.625 539998.0625 391816.65625 562581.25 563601.5 7509.1 7192.8 7504.5 7324.7 6967.7 7203.7 +0 22.469715 P41208 CETN2 Centrin-2 5 100 0.1326346216263228 22.469715 10.562367 11.867718 6.688088 7.9097395 6.3160715 2.301621815969369e-91 4.587665064654505e-44 8.042147147884621e-18 2.2549295605364673e-5 8.822395242977867e-6 4.2780263118989874e-5 6 6 6 6 6 6 False False False False False False 5 5 5 5 5 5 3336.7919921875 2474.8359375 3198.19677734375 2383.727783203125 2883.715576171875 2857.44775390625 330062.46875 619366.625 489264.59375 423884.25 678688.8125 827148.6875 1477.8 894.5 1282.4 983.6 1191.3 1079.6 +0 20.421215 Q86UV5 USP48 Ubiquitin carboxyl-terminal hydrolase 48 31 100 0.03672998998472319 18.319489 12.894515 15.445489 14.869727 19.207323 20.421215 1.5025769865696056e-79 1.372396503946988e-48 1.002515275850952e-62 7.539627621388234e-121 4.147115607006649e-77 3.9698017230673477e-162 35 35 35 35 35 35 False False False False False False 24 29 27 19 24 24 4113.38427734375 4219.71435546875 4358.69677734375 4204.2060546875 4067.005859375 4484.138671875 106584 200978.875 201427.015625 165296.296875 218998.84375 239906.484375 765.4 791.1 812.4 784.2 773.1 837 +0 22.101425 P27348 YWHAQ 14-3-3 protein theta 11 100 0.02169985882840874 19.206041 14.01141 22.101425 19.651245 14.322549 21.336082 5.016476412837495e-82 7.663881950033418e-51 1.4438245135476818e-80 6.434025934173828e-147 1.9424616772394e-52 1.8087494904252265e-168 14 14 14 14 14 14 False False False False False False 14 13 14 14 14 14 142043.828125 150637.015625 143190.75 144217.953125 142576.359375 144541.34375 2035089.25 3730418.25 3763042 2353800.5 3636717 3681462.75 42746.801 45677.699 44293.5 43843.398 43110.398 43878.301 +1.6068516529582387e-5 6.303624 P21866 kdpE KDP operon transcriptional regulatory protein KdpE 3 100 0.37524776641699514 5.9584856 4.8225303 6.303624 3.4536772 5.9258904 3.3914812 4.8154457454095945e-5 1.2521536538436537e-4 4.094437720951218e-5 4.061540179567786e-4 7.99646509171882e-5 4.451194317949419e-4 3 3 3 3 3 3 False False False False False False 3 3 3 3 3 3 469.70440673828125 532.36962890625 542.43060302734375 286.3434753417969 228.28082275390625 255.38780212402344 52320.125 79489.7109375 63174.05859375 70850.0546875 116593.0234375 142758.546875 77.7 87.2 90.2 55.7 43.2 46.7 +1.054502786703447e-5 6.6905646 Q8TEA7 TBCK TBC domain-containing protein kinase-like protein 10 100 0.1310560787790476 6.445208 6.304005 6.6905646 5.318168 6.1720734 5.089801 2.6596706870201472e-5 3.0023930530491162e-5 2.47762321185105e-5 1.0308969664924193e-4 6.270352699509567e-5 1.3261992013107127e-4 9 10 10 10 10 10 False False False False False False 5 5 5 5 6 5 814.71954345703125 1087.6990966796875 786.72235107421875 998.9100341796875 1003.1926879882812 860.27001953125 789313 1069875.875 997002.8125 1390377 1808812.125 1431603.375 100.9 134.5 115 121.3 133.3 118.3 +0 21.981451 Q96IY1 NSL1 Kinetochore-associated protein NSL1 homolog 10 100 0.036294899556650835 14.306435 21.981451 12.419815 12.672717 17.83819 14.6812725 1.0029564026071766e-68 3.34143677536896e-68 5.189864516153312e-24 4.7064809447683244e-95 1.7529102890929142e-73 2.3354160177347e-68 14 13 14 14 14 14 False False False False False False 9 10 11 13 10 8 2355.071533203125 2256.580810546875 2237.9892578125 2445.179443359375 2259.37841796875 2242.236572265625 21293.267578125 37354.67578125 28273.302734375 29107.9140625 36565.07421875 28495.12890625 880.1 906.8 927.3 1156.9 934.9 936.9 +0 20.16024 Q13614 MTMR2 Myotubularin-related protein 2 16 100 0.045158587445615635 14.6404295 12.5598135 18.086279 16.222187 14.350903 20.16024 1.3452048002389808e-69 6.352393572407464e-48 1.4885530219664233e-69 5.618748841371701e-128 6.7402524696111725e-53 2.434005446501148e-160 18 18 18 18 18 18 False False False False False False 14 13 14 13 15 14 3202.191650390625 3375.5732421875 3138.15625 3375.85693359375 3312.877685546875 3009.963623046875 144046.328125 292145.0625 286080.65625 221722.78125 257194.46875 287824.5 951 1002.1 961 1037.7 982.1 954.4 +0 20.607874 P33136 mdoG Glucans biosynthesis protein G 23 100 0.31598880471092067 20.607874 18.831343 12.882393 19.63106 18.397953 14.74149 5.348257558790112e-86 4.162148157335475e-61 2.7107089993028272e-30 8.401769703915492e-147 5.880811377905653e-75 7.714167271637227e-70 31 31 31 30 31 31 False False False False False False 30 29 28 25 24 26 23246.865234375 22642.615234375 21872.400390625 12875.6650390625 12207.5498046875 12428.61328125 146411.90625 253360.84375 247408.28125 89826.1640625 148425.375 136206.328125 9459.8 9587.6 9232.6 5327.4 5294.1 5220.6 +1.0072324670999351e-280 17.195932 Q6UXD5 SEZ6L2 Seizure 6-like protein 2 7 100 0.06667826245896397 4.131999 6.772256 17.195932 6.8294253 8.540986 12.357161 2.1573105188629182e-4 1.3585735004106951e-5 3.226029675821028e-67 1.653695170436484e-5 5.228760579022365e-6 8.5053509583376e-25 7 7 7 7 7 7 False False False False False False 5 6 5 5 5 6 1263.420654296875 1181.822265625 1220.3677978515625 1113.9866943359375 1068.5982666015625 1256.9305419921875 72037.5546875 122005.0859375 102835.5234375 57111.046875 104269.03125 191901.171875 161.1 151.5 154.7 148.6 136.1 158.5 +7.155830188176804e-162 14.520309 O43760 SYNGR2 Synaptogyrin-2 5 100 0.051837817788223765 14.520309 8.822196 12.821382 14.517164 8.368657 6.6260056 2.7718618796407557e-69 3.4162097624465757e-19 2.0836916369044885e-29 5.044000422313165e-119 6.054838800628104e-6 2.8391654535927604e-5 5 5 5 5 5 5 False False False False False False 5 5 5 5 5 5 13309.7666015625 12324.5146484375 13149.5986328125 13675.2099609375 12034.626953125 12308.576171875 127606.1171875 271149.34375 242949.265625 143860.71875 239197.15625 250946.28125 5530.1 5270.6 5249 5697.1 5128.2 5293.7 +0 25.027157 P52292 KPNA2 Importin subunit alpha-1 14 100 0.024832392617621997 21.718729 25.027157 15.512024 20.24036 18.98721 17.652256 3.372390924241912e-89 2.532692566441531e-75 6.814649380987046e-63 2.653408870803811e-150 1.5982896730002686e-76 1.3617121852571041e-143 19 19 19 19 19 19 False False False False False False 19 19 19 18 18 19 34379.75390625 36562.94921875 34773.390625 34628.25 34284.3671875 34388.984375 274170.34375 451374.15625 431721.375 319417.0625 431931.28125 438617.09375 12042.5 12080.6 11824.4 11883.4 11508.1 11720.3 +0 19.093235 P02679 FGG Fibrinogen gamma chain 3 100 0.07298536712525223 10.485868 19.093235 7.432459 7.0686703 16.114859 13.084756 2.4024269882585917e-45 1.0990333918098778e-61 9.948148678640561e-6 8.351532160306708e-6 5.099046080464289e-69 1.3942254844793062e-35 4 4 4 4 4 4 False False False False False False 4 4 4 4 4 4 8447.220703125 8384.8994140625 9212.228515625 7714.52099609375 7544.28955078125 8054.9765625 174836.15625 395185.78125 391231.15625 275257.21875 524078.15625 449936.125 905 1102.4 1059.1 908.2 904.5 900.5 +0 21.284233 P0ABH9 clpA ATP-dependent Clp protease ATP-binding subunit ClpA 17 100 0.33990415418413183 18.413792 14.50538 13.749044 17.963766 13.312042 21.284233 8.209110334314037e-80 7.450318482953909e-52 9.208995235851755e-45 1.8938767225400117e-137 1.4349146344522272e-37 4.1357985088830816e-168 18 18 18 17 16 18 False False False False False False 15 15 14 9 10 9 5312.609375 5213.6435546875 5159.81396484375 2728.41845703125 2753.85009765625 2778.67529296875 181553.46875 376247.28125 344280.53125 186461.265625 346986.53125 294516.46875 801.2 770.7 754.7 395.4 389.4 392.6 +0 19.1529 P0AGJ9 tyrS Tyrosine--tRNA ligase 14 100 0.3250860318466905 18.144709 19.1529 13.074355 15.315206 12.478048 18.862162 4.601218320132648e-79 8.092010048482405e-62 3.4043206741220245e-33 3.546816582594143e-123 2.1461616302722594e-27 1.4497337117608647e-151 18 18 18 17 17 17 False False False False False False 17 17 16 15 13 13 10123.9892578125 10415.400390625 10367.240234375 5688.93896484375 5441.083984375 5644.44970703125 523408.15625 753815.1875 701347.375 532286.1875 821399.875 647708.8125 2769.4 2889.4 2812.2 1527.3 1448.4 1486 +1.922747243823834e-187 15.165148 Q96B70 LENG9 Leukocyte receptor cluster member 9 13 100 0.07278365851665312 13.398864 8.108308 13.804122 13.439115 7.3709016 15.165148 2.205082485524975e-66 9.822585743664334e-12 8.409939004413034e-46 1.6040058847760947e-113 1.2889541969426851e-5 9.105532523736602e-81 15 15 15 15 15 15 False False False False False False 10 13 13 13 10 13 2101.833984375 2141.8388671875 2256.98876953125 2518.964599609375 2099.859375 2151.25 158468.625 342546.09375 317884.28125 242788.046875 367857.71875 345473.21875 560 593 615.1 672.5 592.8 570.5 +3.37263796226557e-6 7.3191686 Q8IUQ4 SIAH1 E3 ubiquitin-protein ligase SIAH1 7 100 0.09990423683736403 6.6886315 6.9954424 7.3191686 5.4779873 6.89249 5.1855507 1.7455032731441155e-5 6.318034661525333e-6 1.0815983901411342e-5 8.946103257004715e-5 2.4533056619190904e-5 1.2570723674557908e-4 7 7 7 7 7 7 False False False False False False 6 5 5 5 5 5 1200.4100341796875 920.51727294921875 953.81658935546875 1130.5025634765625 1062.7847900390625 1046.089111328125 164157.25 390301.78125 319539.03125 263265.625 434843.40625 455577.625 383.3 276.5 291 340.7 328 352.2 +0 20.66866 Q05518 PAL1 Protein PAL1 16 100 0.36819210526601004 16.259075 11.809054 9.19824 17.328188 17.022835 20.66866 6.632997669006847e-74 1.8775976465102498e-46 2.8453762184031015e-6 5.910845219924978e-134 2.350384829286335e-71 7.848502728530642e-164 16 16 16 17 16 17 False False False False False False 13 13 14 13 15 14 2547.21044921875 2246.40869140625 2522.540283203125 5081.2001953125 4728.96484375 4839.92626953125 37937.73828125 74334.1640625 57608.78515625 95554.4921875 123890.859375 121253.8984375 776.7 691.1 773.1 1511.9 1459.5 1477 +5.394963970007242e-228 16.100355 Q99519 NEU1 Sialidase-1 8 100 0.041956874436290176 8.864108 7.612147 9.185678 11.131746 7.223344 16.100355 1.0725917932927211e-19 3.970744688453691e-8 2.8770182920178534e-6 1.3870714091572402e-56 1.5230979438923316e-5 8.56235182532138e-108 10 10 10 9 10 10 False False False False False False 9 8 7 9 7 8 3336.75390625 3203.642822265625 3563.25390625 3318.84228515625 3211.1689453125 3208.494384765625 94469.1796875 212762.1875 188954.59375 145905.734375 222518.84375 223009.0625 862.7 819 843.1 884.1 842.3 846.6 +0 25.75072 Q13347 EIF3I Eukaryotic translation initiation factor 3 subunit I 18 100 0.029693551358744418 22.39139 18.007318 25.75072 17.4999 20.684776 19.094574 3.870449195026625e-91 2.6783811586470917e-59 3.895802091847579e-91 6.810906104656742e-135 4.26412402378322e-81 4.070679717260286e-153 25 26 25 26 26 26 False False False False False False 23 25 23 23 23 25 37979.71875 37575.125 38192.8671875 39744.296875 36514.21875 36996.00390625 891262.875 1523047.5 1499871.125 953503.9375 1426827.375 1500175.625 20262.801 19861.699 19908.801 20109 18762.5 19440 +2.197617919174808e-55 10.939882 Q96RU2 USP28 Ubiquitin carboxyl-terminal hydrolase 28 27 100 0.08071571231843573 10.757341 8.50885 8.542746 7.01649 10.939882 9.525475 7.283839940979553e-51 1.054703729867801e-15 4.464189291595243e-6 9.964517819662203e-6 4.583756609459537e-11 1.338551469937265e-6 27 26 26 25 26 25 False False False False False False 13 16 15 16 15 19 1868.17431640625 2059.894775390625 1681.474365234375 1847.2802734375 1732.381591796875 2020.5357666015625 172941.984375 262241.53125 260645.140625 208456.328125 253048.9375 271712.78125 344.5 367 332.2 343.9 330.5 414.5 +7.062939185113079e-53 10.820893 Q9ULI3 HEG1 Protein HEG homolog 1 7 100 0.2550342121907677 10.820893 5.5860376 6.898199 6.3327384 6.2446265 5.5681224 3.2935509564797122e-52 6.471561573956519e-5 1.8268379588665833e-5 3.833878270133572e-5 5.8255313739159866e-5 9.545385581068702e-5 6 6 7 6 7 7 False False False False False False 5 5 5 5 5 5 624.0570068359375 632.15643310546875 377.40594482421875 719.392578125 422.0374755859375 459.7496337890625 1243833.75 1907801.625 2083198.25 2678366.5 3877928.25 3412616 42.1 44.7 41.8 56 48.8 55.9 From e9b24c963b7a1bfdc1544ba060c15e3d1fffb155 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 29 Apr 2026 17:53:32 +0200 Subject: [PATCH 48/65] Add tests for Spectronaut functions --- .../testthat/test-checkArgumentsSpectronaut.R | 775 +++++++++++++++++ tests/testthat/test-importSpectronaut.R | 158 ++++ tests/testthat/test-readSpectronautSetup.R | 33 + tests/testthat/test-runSpectronautAnalysis.R | 807 ++++++++++++++++++ 4 files changed, 1773 insertions(+) create mode 100644 tests/testthat/test-checkArgumentsSpectronaut.R create mode 100644 tests/testthat/test-importSpectronaut.R create mode 100644 tests/testthat/test-readSpectronautSetup.R create mode 100644 tests/testthat/test-runSpectronautAnalysis.R diff --git a/tests/testthat/test-checkArgumentsSpectronaut.R b/tests/testthat/test-checkArgumentsSpectronaut.R new file mode 100644 index 0000000..3d61fec --- /dev/null +++ b/tests/testthat/test-checkArgumentsSpectronaut.R @@ -0,0 +1,775 @@ +test_that("argument checking for Spectronaut works", { + ## Working arguments + args0 <- list( + templateRmd = system.file("extdata", "process_basic_template.Rmd", + package = "einprot"), + outputDir = tempdir(), + outputBaseName = "baseName", + reportTitle = "reportTitle", + reportAuthor = "reportAuthor", + forceOverwrite = TRUE, + experimentInfo = list("Experiment type" = "experiment", + "Sample name" = "example"), + species = "roundworm", + spectronautFile = system.file("extdata", "spectronaut_example", + "SN_unit_test_subset.tsv", package = "einprot"), + spectronautFileType = "pg_pivot", + outLevel = "pg", + spectronautLogFile = system.file( + "extdata", "spectronaut_example", + "PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.log.txt", + package = "einprot"), + aName = "PG.Quantity", + idCol = function(df) combineIds(df, combineCols = c("PG.Genes", + "PG.ProteinGroups")), + labelCol = function(df) combineIds(df, combineCols = c("PG.Genes", + "PG.ProteinGroups")), + geneIdCol = function(df) getFirstId(df, colName = "PG.Genes"), + proteinIdCol = "PG.ProteinGroups", + stringIdCol = function(df) combineIds(df, combineCols = c("PG.Genes", + "PG.ProteinGroups"), + combineWhen = "missing", + makeUnique = FALSE), + extraFeatureCols = NULL, + iColPattern = ".PG.Quantity$", + sampleAnnot = data.frame( + sample = c("LFQ_24min_500ng_2Th_3ms_A_01", + "LFQ_24min_500ng_2Th_3ms_A_02", + "LFQ_24min_500ng_2Th_3ms_A_03", + "LFQ_24min_500ng_2Th_3ms_B_01", + "LFQ_24min_500ng_2Th_3ms_B_02", + "LFQ_24min_500ng_2Th_3ms_B_03"), + group = c("A", "A", "A", "B", "B", "B")), + includeOnlySamples = "", + excludeSamples = "", + filtersDF = list(), + filtersSE = list(), + imputeMethod = "MinProb", + assaysForExport = NULL, + addAbundanceValues = TRUE, + addHeatmaps = TRUE, + mergeGroups = list(), + comparisons = list(), + ctrlGroup = "", + allPairwiseComparisons = TRUE, + singleFit = FALSE, + subtractBaseline = FALSE, + baselineGroup = "", + normMethod = "none", + spikeFeatures = NULL, + stattest = "limma", + minNbrValidValues = 2, + minlFC = 0, + samSignificance = TRUE, + nperm = 100, + volcanoAdjPvalThr = 0.05, + volcanoLog2FCThr = 1, + volcanoMaxFeatures = 10, + volcanoLabelSign = "both", + volcanoS0 = 0.1, + volcanoFeaturesToLabel = c(""), + addInteractiveVolcanos = FALSE, + interactiveDisplayColumns = NULL, + interactiveGroupColumn = NULL, + complexFDRThr = 0.1, + maxNbrComplexesToPlot = Inf, + maxComplexSimilarity = 1, + seed = 123, + includeFeatureCollections = "complexes", + minSizeToKeepSet = 2, + customComplexes = list(), + complexSpecies = "all", + complexDbPath = system.file( + "extdata", "complexes", + "complexdb_einprot0.5.0_20220323_orthologs.rds", + package = "einprot"), + stringVersion = "11.5", + stringDir = "", + linkTableColumns = c(), + customYml = NULL, + doRender = TRUE + ) + + ## Check that it works with these parameter values + expect_null(do.call(.checkArgumentsSpectronaut, args0)) + + ## templateRmd + args <- args0 + args$templateRmd <- c(args$templateRmd, args$templateRmd) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'templateRmd' must have length 1") + args$templateRmd <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'templateRmd' must be of class 'character'") + args$templateRmd <- "missing_file" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'templateRmd' must point to an existing file") + + ## outputDir + args <- args0 + args$outputDir <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'outputDir' must be of class 'character'") + args$outputDir <- c("dir1", "dir2") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'outputDir' must have length 1") + + ## outputBaseName + args <- args0 + args$outputBaseName <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'outputBaseName' must be of class 'character'") + args$outputBaseName <- c("name1", "name2") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'outputBaseName' must have length 1") + + ## reportTitle + args <- args0 + args$reportTitle <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'reportTitle' must be of class 'character'") + args$reportTitle <- c("name1", "name2") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'reportTitle' must have length 1") + + ## reportAuthor + args <- args0 + args$reportAuthor <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'reportAuthor' must be of class 'character'") + args$reportAuthor <- c("name1", "name2") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'reportAuthor' must have length 1") + + ## forceOverwrite + args <- args0 + args$forceOverwrite <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'forceOverwrite' must be of class 'logical'") + args$forceOverwrite <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'forceOverwrite' must have length 1") + + ## experimentInfo + args <- args0 + args$experimentInfo <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'experimentInfo' must be of class 'list'") + args$experimentInfo <- list(1, 2) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'namesexperimentInfo' must not be NULL") + args$experimentInfo <- list() + expect_null(do.call(.checkArgumentsSpectronaut, args)) + + ## species + args <- args0 + args$species <- 1 + expect_warning(do.call(.checkArgumentsSpectronaut, args), + "Unknown species 1") + args$species <- c("Mouse", "Human") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "length(species) == 1 is not TRUE", fixed = TRUE) + args$species <- "Mouse" + expect_null(do.call(.checkArgumentsSpectronaut, args)) + + ## spectronautFile + args <- args0 + args$spectronautFile <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'spectronautFile' must be of class 'character'") + args$spectronautFile <- "missing" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'spectronautFile' must point to an existing file") + + ## spectronautFileType + args <- args0 + args$spectronautFileType <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'spectronautFileType' must be of class 'character'") + args$spectronautFileType <- "wrong" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "values in 'spectronautFileType' must be one of") + args$spectronautFileType = c("pg_pivot", "pg_pivot") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'spectronautFileType' must have length 1") + + ## outLevel + args <- args0 + args$outLevel <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'outLevel' must be of class 'character'") + args$outLevel <- "wrong" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "values in 'outLevel' must be one of") + args$outLevel = c("pg", "pg") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'outLevel' must have length 1") + + ## spectronautLogFile + args <- args0 + args$spectronautLogFile <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'spectronautLogFile' must be of class 'character'") + args$spectronautLogFile <- "missing" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'spectronautLogFile' must point to an existing file") + + ## aName + # args <- args0 + # args$aName <- 1 + # expect_error(do.call(.checkArgumentsSpectronaut, args), + # "'aName' must be of class 'character'") + # args$aName <- NULL + # args <- c(args, list(aName = NULL)) + # expect_error(do.call(.checkArgumentsSpectronaut, args), + # "'aName' must not be NULL") + + ## idCol + args <- args0 + args$idCol <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'idCol' must be of class 'character'") + + ## geneIdCol + args <- args0 + args$labelCol <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'labelCol' must be of class 'character'") + + ## geneIdCol + args <- args0 + args$geneIdCol <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'geneIdCol' must be of class 'character'") + + ## proteinIdCol + args <- args0 + args$proteinIdCol <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'proteinIdCol' must be of class 'character'") + args <- args0 + args$proteinIdCol <- "Protein.ID" + expect_null(do.call(.checkArgumentsSpectronaut, args)) + + ## stringIdCol + args <- args0 + args$stringIdCol <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'stringIdCol' must be of class 'character'") + + ## extraFeatureCols + args <- args0 + args$extraFeatureCols <- function(df) df$einprotId + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'extraFeatureCols' must be of class 'list'") + args$extraFeatureCols <- list(function(df) df$einprotId) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'namesextraFeatureCols' must not be NULL") + args$extraFeatureCols <- list(newCol = 1) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'i' must be of class 'character'") + args$extraFeatureCols <- list(newCol = function(x, y) "a") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "length(formals(i)) == 1 is not TRUE", fixed = TRUE) + + ## iColPattern + args <- args0 + args$iColPattern <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'iColPattern' must be of class 'character'") + args$iColPattern <- c("\\.PG\\.Quantity$", "\\.PG\\.Quantity$") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'iColPattern' must have length 1") + # args$iColPattern <- c("\\\\.PG\\\\.Quantity$") + # expect_error(do.call(.checkArgumentsSpectronaut, args), + # "All values in 'iColPattern' must be one of") + ## Works without escaped periods + args <- args0 + args$iColPattern <- ".PG.Quantity$" + expect_null(do.call(.checkArgumentsSpectronaut, args)) + + ## sampleAnnot + args <- args0 + colnames(args$sampleAnnot) <- c("sample", "condition") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "colnames(sampleAnnot)) is not TRUE", fixed = TRUE) + colnames(args$sampleAnnot) <- NULL + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'colnamessampleAnnot' must not be NULL") + args$sampleAnnot <- as.matrix(args0$sampleAnnot) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'sampleAnnot' must be of class 'data.frame'") + args$sampleAnnot <- args0$sampleAnnot + args$sampleAnnot$group <- as.numeric(as.factor(args$sampleAnnot$group)) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'$sampleAnnotgroup' must be of class 'character'", + fixed = TRUE) + # args$sampleAnnot <- args0$sampleAnnot + # args$sampleAnnot$sample[1] <- paste0("iBAQ.", + # args$sampleAnnot$sample[1]) + # expect_error(do.call(.checkArgumentsSpectronaut, args), + # "Not all sample names are available in the sample") + + ## includeOnlySamples + args <- args0 + args$includeOnlySamples <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'includeOnlySamples' must be of class 'character'") + + ## excludeSamples + args <- args0 + args$excludeSamples <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'excludeSamples' must be of class 'character'") + args$includeOnlySamples <- "Adnp" + args$excludeSamples <- "RBC_ctrl" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "Please specify max one of includeOnlySamples") + + ## filtersDF + args <- args0 + args$filtersDF <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'filtersDF' must be of class 'list'") + args$filtersDF <- list(function(x) x) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'namesfiltersDF' must not be NULL") + args$filtersDF <- list(f1 = function(x, y) x + y) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "is not TRUE") + + ## filtersSE + args <- args0 + args$filtersSE <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "is not TRUE") + + ## imputeMethod + args <- args0 + args$imputeMethod <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'imputeMethod' must be of class 'character'") + args$imputeMethod <- c("MinProb", "impSeqRob") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'imputeMethod' must have length 1") + args$imputeMethod <- "wrong" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "All values in 'imputeMethod' must be one of") + + ## assaysForExport + args <- args0 + args$assaysForExport <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'assaysForExport' must be of class 'character'") + + ## addAbundanceValues + args <- args0 + args$addAbundanceValues <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'addAbundanceValues' must be of class 'logical'") + args$addAbundanceValues <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'addAbundanceValues' must have length 1") + + ## addHeatmaps + args <- args0 + args$addHeatmaps <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'addHeatmaps' must be of class 'logical'") + args$addHeatmaps <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'addHeatmaps' must have length 1") + + ## mergeGroups + args <- args0 + args$mergeGroups <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'mergeGroups' must be of class 'list'") + args$mergeGroups <- list(c("c1", "c2")) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'mergeGroups' must be a named list") + args$mergeGroups <- list(g1 = c("c1", "c2"), + g1 = c("c3", "c4")) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'mergeGroups' must be a named list") + args$mergeGroups <- list() + expect_null(do.call(.checkArgumentsSpectronaut, args)) + + ## comparisons + args <- args0 + args$comparisons <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'comparisons' must be of class 'list'") + args$comparisons <- list(g1 = c("c1", "c2"), + g2 = c("c2", "c4", "c3")) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "Each entry in 'comparisons' must have exactly two elements") + args$comparisons <- list() + expect_null(do.call(.checkArgumentsSpectronaut, args)) + + ## ctrlGroup + args <- args0 + args$ctrlGroup <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'ctrlGroup' must be of class 'character'") + args$ctrlGroup <- c("name1", "name2") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'ctrlGroup' must have length 1") + + ## allPairwiseComparisons + args <- args0 + args$allPairwiseComparisons <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'allPairwiseComparisons' must be of class 'logical'") + args$allPairwiseComparisons <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'allPairwiseComparisons' must have length 1") + + ## singleFit + args <- args0 + args$singleFit <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'singleFit' must be of class 'logical'") + args$singleFit <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'singleFit' must have length 1") + + ## subtractBaseline + args <- args0 + args$subtractBaseline <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'subtractBaseline' must be of class 'logical'") + args$subtractBaseline <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'subtractBaseline' must have length 1") + + ## baselineGroup + args <- args0 + args$baselineGroup <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'baselineGroup' must be of class 'character'") + + ## normMethod + args <- args0 + args$normMethod <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'normMethod' must be of class 'character'") + args$normMethod <- c("none", "center.median") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'normMethod' must have length 1") + args$normMethod <- "wrong" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "All values in 'normMethod' must be one of") + + ## spikeFeatures + args <- args0 + args$spikeFeatures <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'spikeFeatures' must be of class 'character'") + + ## stattest + args <- args0 + args$stattest <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'stattest' must be of class 'character'") + args$stattest <- c("limma", "ttest") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'stattest' must have length 1") + args$stattest <- "wrong" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "All values in 'stattest' must be one of") + + ## minNbrValidValues + args <- args0 + args$minNbrValidValues <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'minNbrValidValues' must be of class 'numeric'") + args$minNbrValidValues <- c(10, 20) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'minNbrValidValues' must have length 1") + args$minNbrValidValues <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'minNbrValidValues' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## minlFC + args <- args0 + args$minlFC <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'minlFC' must be of class 'numeric'") + args$minlFC <- c(10, 20) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'minlFC' must have length 1") + args$minlFC <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'minlFC' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## samSignificance + args <- args0 + args$samSignificance <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'samSignificance' must be of class 'logical'") + args$samSignificance <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'samSignificance' must have length 1") + + ## nperm + args <- args0 + args$nperm <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'nperm' must be of class 'numeric'") + args$nperm <- c(10, 20) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'nperm' must have length 1") + args$nperm <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'nperm' must be within [1,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoAdjPvalThr + args <- args0 + args$volcanoAdjPvalThr <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoAdjPvalThr' must be of class 'numeric'") + args$volcanoAdjPvalThr <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoAdjPvalThr' must have length 1") + args$volcanoAdjPvalThr <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoAdjPvalThr' must be within [0,1] (inclusive)", + fixed = TRUE) + + ## volcanoLog2FCThr + args <- args0 + args$volcanoLog2FCThr <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoLog2FCThr' must be of class 'numeric'") + args$volcanoLog2FCThr <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoLog2FCThr' must have length 1") + args$volcanoLog2FCThr <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoLog2FCThr' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoMaxFeatures + args <- args0 + args$volcanoMaxFeatures <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoMaxFeatures' must be of class 'numeric'") + args$volcanoMaxFeatures <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoMaxFeatures' must have length 1") + args$volcanoMaxFeatures <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoMaxFeatures' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoLabelSign + args <- args0 + args$volcanoLabelSign <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoLabelSign' must be of class 'character'") + args$volcanoLabelSign <- c("both", "pos") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoLabelSign' must have length 1") + args$volcanoLabelSign <- "missing" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "All values in 'volcanoLabelSign' must be one of") + + ## volcanoS0 + args <- args0 + args$volcanoS0 <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoS0' must be of class 'numeric'") + args$volcanoS0 <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoS0' must have length 1") + args$volcanoS0 <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoS0' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoFeaturesToLabel + args <- args0 + args$volcanoFeaturesToLabel <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'volcanoFeaturesToLabel' must be of class 'character'") + + ## addInteractiveVolcanos + args <- args0 + args$addInteractiveVolcanos <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'addInteractiveVolcanos' must be of class 'logical'") + args$addInteractiveVolcanos <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'addInteractiveVolcanos' must have length 1") + + ## interactiveDisplayColumns + args <- args0 + args$interactiveDisplayColumns <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'interactiveDisplayColumns' must be of class 'character'") + + # interactiveGroupColumn + args <- args0 + args$interactiveGroupColumn <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'interactiveGroupColumn' must be of class 'character'") + args$interactiveGroupColumn <- c("col1", "col2") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'interactiveGroupColumn' must have length 1") + + ## complexFDRThr + args <- args0 + args$complexFDRThr <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'complexFDRThr' must be of class 'numeric'") + args$complexFDRThr <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'complexFDRThr' must have length 1") + args$complexFDRThr <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'complexFDRThr' must be within [0,1] (inclusive)", + fixed = TRUE) + + ## maxNbrComplexesToPlot + args <- args0 + args$maxNbrComplexesToPlot <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'maxNbrComplexesToPlot' must be of class 'numeric'") + args$maxNbrComplexesToPlot <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'maxNbrComplexesToPlot' must have length 1") + args$maxNbrComplexesToPlot <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'maxNbrComplexesToPlot' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## maxComplexSimilarity + args <- args0 + args$maxComplexSimilarity <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'maxComplexSimilarity' must be of class 'numeric'") + args$maxComplexSimilarity <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'maxComplexSimilarity' must have length 1") + + ## seed + args <- args0 + args$seed <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'seed' must be of class 'numeric'") + args$seed <- c(4, 5) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'seed' must have length 1") + args$seed <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'seed' must be within [1,Inf] (inclusive)", + fixed = TRUE) + + ## includeFeatureCollections + args <- args0 + args$includeFeatureCollections <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'includeFeatureCollections' must be of class 'character'") + args$includeFeatureCollections <- "wrong" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "All values in 'includeFeatureCollections' must be one of") + + ## minSizeToKeepSet + args <- args0 + args$minSizeToKeepSet <- "1" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'minSizeToKeepSet' must be of class 'numeric'") + args$minSizeToKeepSet <- c(0.1, 0.2) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'minSizeToKeepSet' must have length 1") + args$minSizeToKeepSet <- -1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'minSizeToKeepSet' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## customComplexes + args <- args0 + args$customComplexes <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'customComplexes' must be of class 'list'") + args$customComplexes <- list(c("c1", "c2"), + c("c2", "c4", "c3")) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'namescustomComplexes' must not be NULL") + args$customComplexes <- list() + expect_null(do.call(.checkArgumentsSpectronaut, args)) + + ## complexSpecies + args <- args0 + args$complexSpecies <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'complexSpecies' must be of class 'character'") + args$complexSpecies <- c("all", "current") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'complexSpecies' must have length 1") + args$complexSpecies <- "wrong" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "All values in 'complexSpecies' must be one of") + + ## complexDbPath + args <- args0 + args$complexDbPath <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'complexDbPath' must be of class 'character'") + args$complexDbPath <- "missing_file" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'complexDbPath' must point to an existing file") + + ## stringVersion + args <- args0 + args$stringVersion <- 11 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'stringVersion' must be of class 'character'") + args$stringVersion <- c("11.0", "11.5") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'stringVersion' must have length 1") + + ## stringDir + args <- args0 + args$stringDir <- 11 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'stringDir' must be of class 'character'") + args$stringDir <- c("", "") + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'stringDir' must have length 1") + + ## linkTableColumns + args <- args0 + args$linkTableColumns <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'linkTableColumns' must be of class 'character'") + + ## customYml + args <- args0 + args$customYml <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'customYml' must be of class 'character'") + args$customYml <- "missing_file" + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'customYml' must point to an existing file") + + ## doRender + args <- args0 + args$doRender <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'doRender' must be of class 'logical'") + args$doRender <- c(TRUE, FALSE) + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'doRender' must have length 1") + +}) diff --git a/tests/testthat/test-importSpectronaut.R b/tests/testthat/test-importSpectronaut.R new file mode 100644 index 0000000..ab33f52 --- /dev/null +++ b/tests/testthat/test-importSpectronaut.R @@ -0,0 +1,158 @@ +test_that("Spectronaut import works", { + pgpivot <- system.file("extdata", "spectronaut_example", + "SN_unit_test_subset.tsv", + package = "einprot") + + ## Fails with wrong arguments + ## ------------------------------------------------------------------------- + expect_error(importSpectronaut(inFile = 1), + "'inFile' must be of class 'character'") + expect_error(importSpectronaut(inFile = c(pgpivot, pgpivot)), + "'inFile' must have length 1") + expect_error(importSpectronaut(inFile = "missing"), + "file.exists(inFile) is not TRUE", fixed = TRUE) + + expect_error(importSpectronaut(inFile = pgpivot, fileType = 1), + "'fileType' must be of class 'character'") + expect_error(importSpectronaut(inFile = pgpivot, fileType = c("long_format", "pg_pivot")), + "'fileType' must have length 1") + expect_error(importSpectronaut(inFile = pgpivot, fileType = "missing"), + "values in 'fileType' must be one of") + + expect_error(importSpectronaut(inFile = pgpivot, outLevel = 1), + "'outLevel' must be of class 'character'") + expect_error(importSpectronaut(inFile = pgpivot, outLevel = c("pg", "pg")), + "'outLevel' must have length 1") + expect_error(importSpectronaut(inFile = pgpivot, outLevel = "missing"), + "values in 'outLevel' must be one of") + + expect_error(importSpectronaut(inFile = pgpivot, outLevel = "pg", + iColPattern = 1), + "'iColPattern' must be of class 'character'") + expect_error(importSpectronaut(inFile = pgpivot, outLevel = "pg", + iColPattern = c(".PG.Quantity$", ".PG.Quantity$")), + "'iColPattern' must have length 1") + expect_error(importSpectronaut(inFile = pgpivot, outLevel = "pg", + iColPattern = "missing"), + "Invalid iColPattern") + + expect_error(importSpectronaut(inFile = pgpivot, includeOnlySamples = 1), + "'includeOnlySamples' must be of class 'character'") + expect_error(importSpectronaut(inFile = pgpivot, excludeSamples = 1), + "'excludeSamples' must be of class 'character'") + expect_error(importSpectronaut(inFile = pgpivot, includeOnlySamples = "s1", + excludeSamples = "s2"), + "Please specify max one of includeOnlySamples and exclude") + + expect_error(importSpectronaut(inFile = pgpivot, stopIfEmpty = 1), + "'stopIfEmpty' must be of class 'logical'") + expect_error(importSpectronaut(inFile = pgpivot, stopIfEmpty = c(TRUE, FALSE)), + "'stopIfEmpty' must have length 1") + + expect_error(importSpectronaut(inFile = pgpivot, fileType = "pg_pivot", + outLevel = "pg", filtersDF = 1), + "'filtersDF' must be of class 'list'") + + ## Extract some values to compare to later + pgtmp <- read.delim(pgpivot, sep = "\t", nrow = 20) + + ## Works with correct arguments + ## ------------------------------------------------------------------------- + ## Without specifying samples to include/exclude - pg_pivot + ## ------------------------------------------------------------------------- + out <- importSpectronaut(inFile = pgpivot, fileType = "pg_pivot", + outLevel = "pg", iColPattern = ".PG.Quantity$", + includeOnlySamples = "", excludeSamples = "", + stopIfEmpty = FALSE, aName = "PG.Quantity", + filtersDF = list(), nrows = 20) + expect_type(out, "list") + expect_named(out, c("sce", "aName")) + expect_equal(out$aName, "PG.Quantity") + expect_equal(nrow(out$sce), 20) + expect_equal(ncol(out$sce), 6L) + expect_s4_class(out$sce, "SingleCellExperiment") + expect_equal(rownames(out$sce), as.character(seq_len(20))) + expect_equal(SummarizedExperiment::assayNames(out$sce), + c("PG.Quantity", "PG.IsSingleHit", + "PG.NrOfPrecursorsUsedForQuantification", + "PG.MS1Quantity", "PG.IBAQ", "PG.Cscore..Run.Wise.", + "PG.QValue..Run.Wise.", "PG.RunEvidenceCount")) + expect_equal(SummarizedExperiment::assay( + out$sce, "PG.Quantity")[, "LFQ_24min_500ng_2Th_3ms_A_02"], + pgtmp[, "X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.Quantity"], ignore_attr = TRUE) + infoCols <- c("PG.Qvalue", "PG.Cscore", "PG.ProteinGroups", "PG.Genes", + "PG.ProteinDescriptions", + "PG.Completeness", "PG.CV") + expect_true(all(infoCols %in% + colnames(SummarizedExperiment::rowData(out$sce)))) + for (ic in infoCols) { + expect_equal(SummarizedExperiment::rowData(out$sce)[, ic], + pgtmp[, ic]) + } + + ## ------------------------------------------------------------------------- + ## Specifying samples to include - pg_pivot + ## ------------------------------------------------------------------------- + out <- importSpectronaut(inFile = pgpivot, fileType = "pg_pivot", + outLevel = "pg", iColPattern = ".PG.Quantity$", + includeOnlySamples = "_A_", excludeSamples = "", + stopIfEmpty = FALSE, aName = "PG.Quantity", + filtersDF = list(), nrows = 20) + expect_type(out, "list") + expect_named(out, c("sce", "aName")) + expect_equal(out$aName, "PG.Quantity") + expect_equal(nrow(out$sce), 20) + expect_equal(ncol(out$sce), 3L) + expect_s4_class(out$sce, "SingleCellExperiment") + expect_equal(rownames(out$sce), as.character(seq_len(20))) + expect_equal(SummarizedExperiment::assayNames(out$sce), + c("PG.Quantity", "PG.IsSingleHit", + "PG.NrOfPrecursorsUsedForQuantification", + "PG.MS1Quantity", "PG.IBAQ", "PG.Cscore..Run.Wise.", + "PG.QValue..Run.Wise.", "PG.RunEvidenceCount")) + expect_equal(SummarizedExperiment::assay( + out$sce, "PG.Quantity")[, "LFQ_24min_500ng_2Th_3ms_A_02"], + pgtmp[, "X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.Quantity"], ignore_attr = TRUE) + infoCols <- c("PG.Qvalue", "PG.Cscore", "PG.ProteinGroups", "PG.Genes", + "PG.ProteinDescriptions", + "PG.Completeness", "PG.CV") + expect_true(all(infoCols %in% + colnames(SummarizedExperiment::rowData(out$sce)))) + for (ic in infoCols) { + expect_equal(SummarizedExperiment::rowData(out$sce)[, ic], + pgtmp[, ic]) + } + + ## ------------------------------------------------------------------------- + ## Specifying samples to exclude - pg_pivot + ## ------------------------------------------------------------------------- + out <- importSpectronaut(inFile = pgpivot, fileType = "pg_pivot", + outLevel = "pg", iColPattern = ".PG.Quantity$", + includeOnlySamples = "", excludeSamples = "_B_", + stopIfEmpty = FALSE, aName = "PG.Quantity", + filtersDF = list(), nrows = 20) + expect_type(out, "list") + expect_named(out, c("sce", "aName")) + expect_equal(out$aName, "PG.Quantity") + expect_equal(nrow(out$sce), 20) + expect_equal(ncol(out$sce), 3L) + expect_s4_class(out$sce, "SingleCellExperiment") + expect_equal(rownames(out$sce), as.character(seq_len(20))) + expect_equal(SummarizedExperiment::assayNames(out$sce), + c("PG.Quantity", "PG.IsSingleHit", + "PG.NrOfPrecursorsUsedForQuantification", + "PG.MS1Quantity", "PG.IBAQ", "PG.Cscore..Run.Wise.", + "PG.QValue..Run.Wise.", "PG.RunEvidenceCount")) + expect_equal(SummarizedExperiment::assay( + out$sce, "PG.Quantity")[, "LFQ_24min_500ng_2Th_3ms_A_02"], + pgtmp[, "X.2..LFQ_24min_500ng_2Th_3ms_A_02.raw.PG.Quantity"], ignore_attr = TRUE) + infoCols <- c("PG.Qvalue", "PG.Cscore", "PG.ProteinGroups", "PG.Genes", + "PG.ProteinDescriptions", + "PG.Completeness", "PG.CV") + expect_true(all(infoCols %in% + colnames(SummarizedExperiment::rowData(out$sce)))) + for (ic in infoCols) { + expect_equal(SummarizedExperiment::rowData(out$sce)[, ic], + pgtmp[, ic]) + } +}) diff --git a/tests/testthat/test-readSpectronautSetup.R b/tests/testthat/test-readSpectronautSetup.R new file mode 100644 index 0000000..26e64d5 --- /dev/null +++ b/tests/testthat/test-readSpectronautSetup.R @@ -0,0 +1,33 @@ +test_that("reading Spectronaut setup file works", { + ## Mis-specified arguments + expect_error(readSpectronautSetup(1), + "'spectronautSetupFile' must be of class 'character'") + expect_error(readSpectronautSetup("missing"), "doesn't exist") + + ## Read proper file and check that output is a + ## correctly named list of the right length + setupfile <- system.file("extdata", "spectronaut_example", + "PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.setup.txt", + package = "einprot") + sn <- readSpectronautSetup(setupfile) + expect_type(sn, "list") + expect_equal(length(sn), 4) + expect_named(sn, c("Spectronaut version", "Setup file", "Raw files", + "Databases")) + + ## All entries should be scalar values + expect_true(all(vapply(sn, length, 0) == 1)) + + ## Check individual values + expect_equal(sn$`Spectronaut version`, "Spectronaut 20.4.260109.92449") + expect_equal(sn$`Setup file`, setupfile) + expect_equal(sn$`Raw files`, + "LFQ_24min_500ng_2Th_3ms_A_01.raw, LFQ_24min_500ng_2Th_3ms_A_02.raw, LFQ_24min_500ng_2Th_3ms_A_03.raw, LFQ_24min_500ng_2Th_3ms_B_01.raw, LFQ_24min_500ng_2Th_3ms_B_02.raw, LFQ_24min_500ng_2Th_3ms_B_03.raw") + expect_equal(sn$`Databases`, + "ECOLI__250312.fasta, HUMAN__230321.fasta, YEAST__230321.fasta") + + ## Missing file + sn <- readSpectronautSetup(spectronautSetupFile = NULL) + expect_type(sn, "list") + expect_equal(length(sn), 0) +}) diff --git a/tests/testthat/test-runSpectronautAnalysis.R b/tests/testthat/test-runSpectronautAnalysis.R new file mode 100644 index 0000000..cc5cb50 --- /dev/null +++ b/tests/testthat/test-runSpectronautAnalysis.R @@ -0,0 +1,807 @@ +test_that("runSpectronautAnalysis works", { + outDir <- tempdir() + outBaseName <- "SpectronautAnalysis" + args0 <- list( + templateRmd = system.file("extdata", "process_basic_template.Rmd", + package = "einprot"), + outputDir = outDir, + outputBaseName = outBaseName, + reportTitle = "reportTitle", + reportAuthor = "reportAuthor", + forceOverwrite = TRUE, + experimentInfo = list("Experiment type" = "experiment", + "Sample name" = "example"), + species = "roundworm", + spectronautFile = system.file("extdata", "spectronaut_example", + "SN_unit_test_subset.tsv", package = "einprot"), + spectronautFileType = "pg_pivot", + outLevel = "pg", + spectronautLogFile = system.file( + "extdata", "spectronaut_example", + "PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.log.txt", + package = "einprot"), + aName = "PG.Quantity", + idCol = function(df) combineIds(df, combineCols = c("PG.Genes", + "PG.ProteinGroups")), + labelCol = function(df) combineIds(df, combineCols = c("PG.Genes", + "PG.ProteinGroups")), + geneIdCol = function(df) getFirstId(df, colName = "PG.Genes"), + proteinIdCol = "PG.ProteinGroups", + stringIdCol = function(df) combineIds(df, combineCols = c("PG.Genes", + "PG.ProteinGroups"), + combineWhen = "missing", + makeUnique = FALSE), + extraFeatureCols = NULL, + iColPattern = ".PG.Quantity$", + sampleAnnot = data.frame( + sample = c("LFQ_24min_500ng_2Th_3ms_A_01", + "LFQ_24min_500ng_2Th_3ms_A_02", + "LFQ_24min_500ng_2Th_3ms_A_03", + "LFQ_24min_500ng_2Th_3ms_B_01", + "LFQ_24min_500ng_2Th_3ms_B_02", + "LFQ_24min_500ng_2Th_3ms_B_03"), + group = c("A", "A", "A", "B", "B", "B")), + includeOnlySamples = "", + excludeSamples = "", + filtersDF = list(), + filtersSE = list(), + imputeMethod = "MinProb", + assaysForExport = NULL, + addAbundanceValues = TRUE, + addHeatmaps = TRUE, + mergeGroups = list(), + comparisons = list(), + ctrlGroup = "", + allPairwiseComparisons = TRUE, + singleFit = FALSE, + subtractBaseline = FALSE, + baselineGroup = "", + normMethod = "none", + spikeFeatures = NULL, + stattest = "limma", + minNbrValidValues = 2, + minlFC = 0, + samSignificance = TRUE, + nperm = 100, + volcanoAdjPvalThr = 0.05, + volcanoLog2FCThr = 1, + volcanoMaxFeatures = 10, + volcanoLabelSign = "both", + volcanoS0 = 0.1, + volcanoFeaturesToLabel = c(""), + addInteractiveVolcanos = FALSE, + interactiveDisplayColumns = NULL, + interactiveGroupColumn = NULL, + complexFDRThr = 0.1, + maxNbrComplexesToPlot = Inf, + maxComplexSimilarity = 1, + seed = 123, + includeFeatureCollections = "complexes", + minSizeToKeepSet = 2, + customComplexes = list(), + complexSpecies = "all", + complexDbPath = system.file( + "extdata", "complexes", + "complexdb_einprot0.5.0_20220323_orthologs.rds", + package = "einprot"), + stringVersion = "11.5", + stringDir = "", + linkTableColumns = c(), + customYml = NULL, + doRender = FALSE + ) + + ## Fail with wrong parameter values (essentially the same tests as for + ## .checkArgumentsSpectronaut()) + ## ------------------------------------------------------------------------- + ## templateRmd + args <- args0 + args$templateRmd <- c(args$templateRmd, args$templateRmd) + expect_error(do.call(runSpectronautAnalysis, args), + "'templateRmd' must have length 1") + args$templateRmd <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'templateRmd' must be of class 'character'") + args$templateRmd <- "missing_file" + expect_error(do.call(runSpectronautAnalysis, args), + "'templateRmd' must point to an existing file") + + ## outputDir + args <- args0 + args$outputDir <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'outputDir' must be of class 'character'") + args$outputDir <- c("dir1", "dir2") + expect_error(do.call(runSpectronautAnalysis, args), + "'outputDir' must have length 1") + + ## outputBaseName + args <- args0 + args$outputBaseName <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'outputBaseName' must be of class 'character'") + args$outputBaseName <- c("name1", "name2") + expect_error(do.call(runSpectronautAnalysis, args), + "'outputBaseName' must have length 1") + + ## reportTitle + args <- args0 + args$reportTitle <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'reportTitle' must be of class 'character'") + args$reportTitle <- c("name1", "name2") + expect_error(do.call(runSpectronautAnalysis, args), + "'reportTitle' must have length 1") + + ## reportAuthor + args <- args0 + args$reportAuthor <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'reportAuthor' must be of class 'character'") + args$reportAuthor <- c("name1", "name2") + expect_error(do.call(runSpectronautAnalysis, args), + "'reportAuthor' must have length 1") + + ## forceOverwrite + args <- args0 + args$forceOverwrite <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'forceOverwrite' must be of class 'logical'") + args$forceOverwrite <- c(TRUE, FALSE) + expect_error(do.call(runSpectronautAnalysis, args), + "'forceOverwrite' must have length 1") + + ## experimentInfo + args <- args0 + args$experimentInfo <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'experimentInfo' must be of class 'list'") + args$experimentInfo <- list(1, 2) + expect_error(do.call(runSpectronautAnalysis, args), + "'namesexperimentInfo' must not be NULL") + + ## species + args <- args0 + args$species <- c("Mouse", "Human") + expect_error(do.call(runSpectronautAnalysis, args), + "length(species) == 1 is not TRUE", fixed = TRUE) + + ## spectronautFile + args <- args0 + args$spectronautFile <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'spectronautFile' must be of class 'character'") + args$spectronautFile <- "missing" + expect_error(do.call(runSpectronautAnalysis, args), + "'spectronautFile' must point to an existing file") + + ## spectronautFileType + args <- args0 + args$spectronautFileType <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'spectronautFileType' must be of class 'character'") + args$spectronautFileType <- "wrong" + expect_error(do.call(runSpectronautAnalysis, args), + "values in 'spectronautFileType' must be one of") + args$spectronautFileType = c("pg_pivot", "pg_pivot") + expect_error(do.call(runSpectronautAnalysis, args), + "'spectronautFileType' must have length 1") + + ## outLevel + args <- args0 + args$outLevel <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'outLevel' must be of class 'character'") + args$outLevel <- "wrong" + expect_error(do.call(runSpectronautAnalysis, args), + "values in 'outLevel' must be one of") + args$outLevel = c("pg", "pg") + expect_error(do.call(runSpectronautAnalysis, args), + "'outLevel' must have length 1") + + ## spectronautLogFile + args <- args0 + args$spectronautLogFile <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'spectronautLogFile' must be of class 'character'") + args$spectronautLogFile <- "missing" + expect_error(do.call(runSpectronautAnalysis, args), + "'spectronautLogFile' must point to an existing file") + + ## aName + # args <- args0 + # args$aName <- 1 + # expect_error(do.call(runSpectronautAnalysis, args), + # "'aName' must be of class 'character'") + # args$aName <- NULL + # args <- c(args, list(aName = NULL)) + # expect_error(do.call(runSpectronautAnalysis, args), + # "'aName' must not be NULL") + + ## idCol + args <- args0 + args$idCol <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'idCol' must be of class 'character'") + + ## geneIdCol + args <- args0 + args$labelCol <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'labelCol' must be of class 'character'") + + ## geneIdCol + args <- args0 + args$geneIdCol <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'geneIdCol' must be of class 'character'") + + ## proteinIdCol + args <- args0 + args$proteinIdCol <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'proteinIdCol' must be of class 'character'") + + ## stringIdCol + args <- args0 + args$stringIdCol <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'stringIdCol' must be of class 'character'") + + ## extraFeatureCols + args <- args0 + args$extraFeatureCols <- function(df) df$einprotId + expect_error(do.call(runSpectronautAnalysis, args), + "'extraFeatureCols' must be of class 'list'") + args$extraFeatureCols <- list(function(df) df$einprotId) + expect_error(do.call(runSpectronautAnalysis, args), + "'namesextraFeatureCols' must not be NULL") + args$extraFeatureCols <- list(newCol = 1) + expect_error(do.call(runSpectronautAnalysis, args), + "'i' must be of class 'character'") + args$extraFeatureCols <- list(newCol = function(x, y) "a") + expect_error(do.call(runSpectronautAnalysis, args), + "length(formals(i)) == 1 is not TRUE", fixed = TRUE) + + ## iColPattern + args <- args0 + args$iColPattern <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'iColPattern' must be of class 'character'") + args$iColPattern <- c("\\.PG\\.Quantity$", "\\.PG\\.Quantity$") + expect_error(do.call(runSpectronautAnalysis, args), + "'iColPattern' must have length 1") + # args$iColPattern <- c("\\\\.PG\\\\.Quantity$") + # expect_error(do.call(runSpectronautAnalysis, args), + # "All values in 'iColPattern' must be one of") + + ## sampleAnnot + args <- args0 + colnames(args$sampleAnnot) <- c("sample", "condition") + expect_error(do.call(runSpectronautAnalysis, args), + "colnames(sampleAnnot)) is not TRUE", fixed = TRUE) + colnames(args$sampleAnnot) <- NULL + expect_error(do.call(runSpectronautAnalysis, args), + "'colnamessampleAnnot' must not be NULL") + args$sampleAnnot <- as.matrix(args0$sampleAnnot) + expect_error(do.call(runSpectronautAnalysis, args), + "'sampleAnnot' must be of class 'data.frame'") + args$sampleAnnot <- args0$sampleAnnot + args$sampleAnnot$group <- as.numeric(as.factor(args$sampleAnnot$group)) + expect_error(do.call(runSpectronautAnalysis, args), + "'$sampleAnnotgroup' must be of class 'character'", + fixed = TRUE) + # args$sampleAnnot <- args0$sampleAnnot + # args$sampleAnnot$sample[1] <- paste0("iBAQ.", + # args$sampleAnnot$sample[1]) + # expect_error(do.call(runSpectronautAnalysis, args), + # "Not all sample names are available in the sample") + + ## includeOnlySamples + args <- args0 + args$includeOnlySamples <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'includeOnlySamples' must be of class 'character'") + + ## excludeSamples + args <- args0 + args$excludeSamples <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'excludeSamples' must be of class 'character'") + args$includeOnlySamples <- "Adnp" + args$excludeSamples <- "RBC_ctrl" + expect_error(do.call(runSpectronautAnalysis, args), + "Please specify max one of includeOnlySamples") + + ## filtersDF + args <- args0 + args$filtersDF <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'filtersDF' must be of class 'list'") + args$filtersDF <- list(function(x) x) + expect_error(do.call(runSpectronautAnalysis, args), + "'namesfiltersDF' must not be NULL") + args$filtersDF <- list(f1 = function(x, y) x + y) + expect_error(do.call(runSpectronautAnalysis, args), + "is not TRUE") + + ## filtersSE + args <- args0 + args$filtersSE <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'filtersSE' must be of class 'list'") + args$filtersSE <- list(function(x) x) + expect_error(do.call(runSpectronautAnalysis, args), + "'namesfiltersSE' must not be NULL") + args$filtersSE <- list(f1 = function(x, y) x + y) + expect_error(do.call(runSpectronautAnalysis, args), + "is not TRUE") + + ## imputeMethod + args <- args0 + args$imputeMethod <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'imputeMethod' must be of class 'character'") + args$imputeMethod <- c("MinProb", "impSeqRob") + expect_error(do.call(runSpectronautAnalysis, args), + "'imputeMethod' must have length 1") + args$imputeMethod <- "wrong" + expect_error(do.call(runSpectronautAnalysis, args), + "All values in 'imputeMethod' must be one of") + + ## assaysForExport + args <- args0 + args$assaysForExport <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'assaysForExport' must be of class 'character'") + + ## addAbundanceValues + args <- args0 + args$addAbundanceValues <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'addAbundanceValues' must be of class 'logical'") + args$addAbundanceValues <- c(TRUE, FALSE) + expect_error(do.call(runSpectronautAnalysis, args), + "'addAbundanceValues' must have length 1") + + ## addHeatmaps + args <- args0 + args$addHeatmaps <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'addHeatmaps' must be of class 'logical'") + args$addHeatmaps <- c(TRUE, FALSE) + expect_error(do.call(runSpectronautAnalysis, args), + "'addHeatmaps' must have length 1") + + ## mergeGroups + args <- args0 + args$mergeGroups <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'mergeGroups' must be of class 'list'") + args$mergeGroups <- list(c("c1", "c2")) + expect_error(do.call(runSpectronautAnalysis, args), + "'mergeGroups' must be a named list") + args$mergeGroups <- list(g1 = c("c1", "c2"), + g1 = c("c3", "c4")) + expect_error(do.call(runSpectronautAnalysis, args), + "'mergeGroups' must be a named list") + + ## comparisons + args <- args0 + args$comparisons <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'comparisons' must be of class 'list'") + args$comparisons <- list(g1 = c("c1", "c2"), + g2 = c("c2", "c4", "c3")) + expect_error(do.call(runSpectronautAnalysis, args), + "Each entry in 'comparisons' must have exactly two elements") + + ## ctrlGroup + args <- args0 + args$ctrlGroup <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'ctrlGroup' must be of class 'character'") + + ## allPairwiseComparisons + args <- args0 + args$allPairwiseComparisons <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'allPairwiseComparisons' must be of class 'logical'") + args$allPairwiseComparisons <- c(TRUE, FALSE) + expect_error(do.call(runSpectronautAnalysis, args), + "'allPairwiseComparisons' must have length 1") + + ## singleFit + args <- args0 + args$singleFit <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'singleFit' must be of class 'logical'") + args$singleFit <- c(TRUE, FALSE) + expect_error(do.call(runSpectronautAnalysis, args), + "'singleFit' must have length 1") + + ## subtractBaseline + args <- args0 + args$subtractBaseline <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'subtractBaseline' must be of class 'logical'") + args$subtractBaseline <- c(TRUE, FALSE) + expect_error(do.call(runSpectronautAnalysis, args), + "'subtractBaseline' must have length 1") + + ## baselineGroup + args <- args0 + args$baselineGroup <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'baselineGroup' must be of class 'character'") + + ## normMethod + args <- args0 + args$normMethod <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'normMethod' must be of class 'character'") + args$normMethod <- c("none", "center.median") + expect_error(do.call(runSpectronautAnalysis, args), + "'normMethod' must have length 1") + args$normMethod <- "wrong" + expect_error(do.call(runSpectronautAnalysis, args), + "All values in 'normMethod' must be one of") + + ## spikeFeatures + args <- args0 + args$spikeFeatures <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'spikeFeatures' must be of class 'character'") + + ## stattest + args <- args0 + args$stattest <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'stattest' must be of class 'character'") + args$stattest <- c("limma", "ttest") + expect_error(do.call(runSpectronautAnalysis, args), + "'stattest' must have length 1") + args$stattest <- "wrong" + expect_error(do.call(runSpectronautAnalysis, args), + "All values in 'stattest' must be one of") + + ## minNbrValidValues + args <- args0 + args$minNbrValidValues <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'minNbrValidValues' must be of class 'numeric'") + args$minNbrValidValues <- c(10, 20) + expect_error(do.call(runSpectronautAnalysis, args), + "'minNbrValidValues' must have length 1") + args$minNbrValidValues <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'minNbrValidValues' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## minlFC + args <- args0 + args$minlFC <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'minlFC' must be of class 'numeric'") + args$minlFC <- c(10, 20) + expect_error(do.call(runSpectronautAnalysis, args), + "'minlFC' must have length 1") + args$minlFC <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'minlFC' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## samSignificance + args <- args0 + args$samSignificance <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'samSignificance' must be of class 'logical'") + args$samSignificance <- c(TRUE, FALSE) + expect_error(do.call(runSpectronautAnalysis, args), + "'samSignificance' must have length 1") + + ## nperm + args <- args0 + args$nperm <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'nperm' must be of class 'numeric'") + args$nperm <- c(10, 20) + expect_error(do.call(runSpectronautAnalysis, args), + "'nperm' must have length 1") + args$nperm <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'nperm' must be within [1,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoAdjPvalThr + args <- args0 + args$volcanoAdjPvalThr <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoAdjPvalThr' must be of class 'numeric'") + args$volcanoAdjPvalThr <- c(0.1, 0.2) + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoAdjPvalThr' must have length 1") + args$volcanoAdjPvalThr <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoAdjPvalThr' must be within [0,1] (inclusive)", + fixed = TRUE) + + ## volcanoLog2FCThr + args <- args0 + args$volcanoLog2FCThr <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoLog2FCThr' must be of class 'numeric'") + args$volcanoLog2FCThr <- c(0.1, 0.2) + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoLog2FCThr' must have length 1") + args$volcanoLog2FCThr <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoLog2FCThr' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoMaxFeatures + args <- args0 + args$volcanoMaxFeatures <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoMaxFeatures' must be of class 'numeric'") + args$volcanoMaxFeatures <- c(0.1, 0.2) + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoMaxFeatures' must have length 1") + args$volcanoMaxFeatures <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoMaxFeatures' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoLabelSign + args <- args0 + args$volcanoLabelSign <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoLabelSign' must be of class 'character'") + args$volcanoLabelSign <- c("both", "pos") + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoLabelSign' must have length 1") + args$volcanoLabelSign <- "missing" + expect_error(do.call(runSpectronautAnalysis, args), + "All values in 'volcanoLabelSign' must be one of") + + ## volcanoS0 + args <- args0 + args$volcanoS0 <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoS0' must be of class 'numeric'") + args$volcanoS0 <- c(0.1, 0.2) + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoS0' must have length 1") + args$volcanoS0 <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoS0' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## volcanoFeaturesToLabel + args <- args0 + args$volcanoFeaturesToLabel <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'volcanoFeaturesToLabel' must be of class 'character'") + + ## addInteractiveVolcanos + args <- args0 + args$addInteractiveVolcanos <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'addInteractiveVolcanos' must be of class 'logical'") + args$addInteractiveVolcanos <- c(TRUE, FALSE) + expect_error(do.call(runSpectronautAnalysis, args), + "'addInteractiveVolcanos' must have length 1") + + ## interactiveDisplayColumns + args <- args0 + args$interactiveDisplayColumns <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'interactiveDisplayColumns' must be of class 'character'") + + # interactiveGroupColumn + args <- args0 + args$interactiveGroupColumn <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'interactiveGroupColumn' must be of class 'character'") + args$interactiveGroupColumn <- c("col1", "col2") + expect_error(do.call(runSpectronautAnalysis, args), + "'interactiveGroupColumn' must have length 1") + + ## complexFDRThr + args <- args0 + args$complexFDRThr <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'complexFDRThr' must be of class 'numeric'") + args$complexFDRThr <- c(0.1, 0.2) + expect_error(do.call(runSpectronautAnalysis, args), + "'complexFDRThr' must have length 1") + args$complexFDRThr <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'complexFDRThr' must be within [0,1] (inclusive)", + fixed = TRUE) + + ## maxNbrComplexesToPlot + args <- args0 + args$maxNbrComplexesToPlot <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'maxNbrComplexesToPlot' must be of class 'numeric'") + args$maxNbrComplexesToPlot <- c(0.1, 0.2) + expect_error(do.call(runSpectronautAnalysis, args), + "'maxNbrComplexesToPlot' must have length 1") + args$maxNbrComplexesToPlot <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'maxNbrComplexesToPlot' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## maxComplexSimilarity + args <- args0 + args$maxComplexSimilarity <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'maxComplexSimilarity' must be of class 'numeric'") + args$maxComplexSimilarity <- c(0.1, 0.2) + expect_error(do.call(runSpectronautAnalysis, args), + "'maxComplexSimilarity' must have length 1") + + ## seed + args <- args0 + args$seed <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'seed' must be of class 'numeric'") + args$seed <- c(4, 5) + expect_error(do.call(runSpectronautAnalysis, args), + "'seed' must have length 1") + args$seed <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'seed' must be within [1,Inf] (inclusive)", + fixed = TRUE) + + ## includeFeatureCollections + args <- args0 + args$includeFeatureCollections <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'includeFeatureCollections' must be of class 'character'") + args$includeFeatureCollections <- "wrong" + expect_error(do.call(runSpectronautAnalysis, args), + "All values in 'includeFeatureCollections' must be one of") + + ## minSizeToKeepSet + args <- args0 + args$minSizeToKeepSet <- "1" + expect_error(do.call(runSpectronautAnalysis, args), + "'minSizeToKeepSet' must be of class 'numeric'") + args$minSizeToKeepSet <- c(0.1, 0.2) + expect_error(do.call(runSpectronautAnalysis, args), + "'minSizeToKeepSet' must have length 1") + args$minSizeToKeepSet <- -1 + expect_error(do.call(runSpectronautAnalysis, args), + "'minSizeToKeepSet' must be within [0,Inf] (inclusive)", + fixed = TRUE) + + ## customComplexes + args <- args0 + args$customComplexes <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'customComplexes' must be of class 'list'") + args$customComplexes <- list(c("c1", "c2"), + c("c2", "c4", "c3")) + expect_error(do.call(runSpectronautAnalysis, args), + "'namescustomComplexes' must not be NULL") + + ## complexSpecies + args <- args0 + args$complexSpecies <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'complexSpecies' must be of class 'character'") + args$complexSpecies <- c("all", "current") + expect_error(do.call(runSpectronautAnalysis, args), + "'complexSpecies' must have length 1") + args$complexSpecies <- "wrong" + expect_error(do.call(runSpectronautAnalysis, args), + "All values in 'complexSpecies' must be one of") + + ## complexDbPath + args <- args0 + args$complexDbPath <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'complexDbPath' must be of class 'character'") + args$complexDbPath <- "missing_file" + expect_error(do.call(runSpectronautAnalysis, args), + "'complexDbPath' must point to an existing file") + + ## stringVersion + args <- args0 + args$stringVersion <- 11 + expect_error(do.call(runSpectronautAnalysis, args), + "'stringVersion' must be of class 'character'") + args$stringVersion <- c("11.0", "11.5") + expect_error(do.call(runSpectronautAnalysis, args), + "'stringVersion' must have length 1") + + ## stringDir + args <- args0 + args$stringDir <- 11 + expect_error(do.call(runSpectronautAnalysis, args), + "'stringDir' must be of class 'character'") + args$stringDir <- c("", "") + expect_error(do.call(runSpectronautAnalysis, args), + "'stringDir' must have length 1") + + ## linkTableColumns + args <- args0 + args$linkTableColumns <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'linkTableColumns' must be of class 'character'") + + ## customYml + args <- args0 + args$customYml <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'customYml' must be of class 'character'") + args$customYml <- "missing_file" + expect_error(do.call(runSpectronautAnalysis, args), + "'customYml' must point to an existing file") + + ## doRender + args <- args0 + args$doRender <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'doRender' must be of class 'logical'") + args$doRender <- c(TRUE, FALSE) + expect_error(do.call(runSpectronautAnalysis, args), + "'doRender' must have length 1") + + + ## Test fix for backwards compatibility with mergeGroups/ctrlGroup + args <- args0 + args$mergeGroups <- list(G1 = c("A", "B")) + args$ctrlGroup <- c("A", "B") + expect_error(do.call(runSpectronautAnalysis, args), + "If 'mergeGroups' is specified, 'ctrlGroup' should not") + + ## Generate report + ## ------------------------------------------------------------------------- + ## Without rendering + args <- args0 + res <- do.call(runSpectronautAnalysis, args) + expect_type(res, "character") + expect_equal(basename(res), paste0(outBaseName, ".Rmd")) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, ".Rmd")))) + + ## Stop if forceOverwrite = FALSE + args <- args0 + args$forceOverwrite <- FALSE + expect_error(res <- do.call(runSpectronautAnalysis, args)) + + ## Message if forceOverwrite = TRUE + args <- args0 + args$forceOverwrite <- TRUE + expect_message(res <- do.call(runSpectronautAnalysis, args), + "already exists but forceOverwrite = TRUE") + + ## In new, non-existing directory and with custom yml + args <- args0 + args$outputDir <- file.path(outDir, "new_Spectronaut_dir") + args$customYml <- system.file("extdata", "custom.yml", + package = "einprot") + res <- do.call(runSpectronautAnalysis, args) + expect_type(res, "character") + expect_equal(basename(res), paste0(outBaseName, ".Rmd")) + expect_true(file.exists(file.path(args$outputDir, paste0(outBaseName, ".Rmd")))) + tmp <- readLines(file.path(args$outputDir, paste0(outBaseName, ".Rmd"))) + # expect_true(grepl("theme: journal", tmp[4])) + + ## With rendering + skip_if(!capabilities()["X11"]) + args <- args0 + args$doRender <- TRUE + res <- do.call(runSpectronautAnalysis, args) + + expect_type(res, "character") + expect_equal(basename(res), paste0(outBaseName, ".html")) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, ".Rmd")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, ".html")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, "_iSEE.R")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, "_sce.rds")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, "_heatmap_centered.pdf")))) + expect_true(file.exists(file.path(outDir, paste0(outBaseName, "_feature_info.txt")))) +}) From 47207e798ad3d599a11ef9bcb5dcd2e5541a1df8 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 10 Jun 2026 12:52:50 +0200 Subject: [PATCH 49/65] Update .Rbuildignore --- .Rbuildignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.Rbuildignore b/.Rbuildignore index 31e3754..3b76295 100644 --- a/.Rbuildignore +++ b/.Rbuildignore @@ -2,3 +2,5 @@ ^\.Rproj\.user$ ^LICENSE\.md$ ^\.github$ +^\.positai$ +^\.claude$ From d8374337bfd37e75c1bc622a4644f8b11f4b2e0b Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 10 Jun 2026 15:25:21 +0200 Subject: [PATCH 50/65] Limit supported normalization methods in workflows --- NEWS.md | 8 ++++++++ R/checkArgumentsDIANN.R | 4 ++-- R/checkArgumentsFragPipe.R | 4 ++-- R/checkArgumentsMaxQuant.R | 4 ++-- R/checkArgumentsPDTMT.R | 4 ++-- R/checkArgumentsPDTMTptm.R | 1 - R/checkArgumentsSE.R | 4 ++-- R/checkArgumentsSpectronaut.R | 4 ++-- R/runMaxQuantAnalysis.R | 7 +++++-- inst/extdata/process_basic_template.Rmd | 19 +++++++++++++++---- man/constants.Rd | 9 --------- man/defaultFiltersDF.Rd | 5 ----- man/defaultFiltersSE.Rd | 13 ------------- man/runDIANNAnalysis.Rd | 7 +++++-- man/runFragPipeAnalysis.Rd | 7 +++++-- man/runMaxQuantAnalysis.Rd | 7 +++++-- man/runPDTMTAnalysis.Rd | 7 +++++-- man/runSEAnalysis.Rd | 7 +++++-- man/runSpectronautAnalysis.Rd | 7 +++++-- 19 files changed, 70 insertions(+), 58 deletions(-) diff --git a/NEWS.md b/NEWS.md index f49e98c..8ab6d13 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,11 @@ +# einprot 0.10.1 + +* Limit the set of supported normalization methods in the workflows to make sure they are suitable for the input type +* Add runSEAnalysis function to run workflow starting from a SummarizedExperiment object +* Add clusterRows argument to makeAbundanceHeatmap() +* Add "sorted" setting to plotMissingValuesHeatmap() +* Change settings to not cluster very large heatmaps (>10,000 rows) + # einprot 0.10.0 * Breaking change: redesign the feature filtering framework to make it more flexible and consistent across tools diff --git a/R/checkArgumentsDIANN.R b/R/checkArgumentsDIANN.R index fe0d1cd..0630e2f 100644 --- a/R/checkArgumentsDIANN.R +++ b/R/checkArgumentsDIANN.R @@ -4,7 +4,6 @@ #' @noRd #' @author Charlotte Soneson #' -#' @importFrom MsCoreUtils normalizeMethods .checkArgumentsDIANN <- function( templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, experimentInfo, species, diannFile, diannFileType, @@ -161,7 +160,8 @@ .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") .assertScalar(x = normMethod, type = "character", - validValues = c(MsCoreUtils::normalizeMethods(), "none", + validValues = c("center.mean", "center.median", + "diff.median", "vsn", "none", "center.mean.shared", "center.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", diff --git a/R/checkArgumentsFragPipe.R b/R/checkArgumentsFragPipe.R index 584d1c7..34ce745 100755 --- a/R/checkArgumentsFragPipe.R +++ b/R/checkArgumentsFragPipe.R @@ -4,7 +4,6 @@ #' @noRd #' @author Charlotte Soneson #' -#' @importFrom MsCoreUtils normalizeMethods .checkArgumentsFragPipe <- function( templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, experimentInfo, species, fragpipeDir, @@ -158,7 +157,8 @@ .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") .assertScalar(x = normMethod, type = "character", - validValues = c(MsCoreUtils::normalizeMethods(), "none", + validValues = c("center.mean", "center.median", + "diff.median", "vsn", "none", "center.mean.shared", "center.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", diff --git a/R/checkArgumentsMaxQuant.R b/R/checkArgumentsMaxQuant.R index b9078ba..c9742c8 100644 --- a/R/checkArgumentsMaxQuant.R +++ b/R/checkArgumentsMaxQuant.R @@ -4,7 +4,6 @@ #' @noRd #' @author Charlotte Soneson #' -#' @importFrom MsCoreUtils normalizeMethods .checkArgumentsMaxQuant <- function( templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, experimentInfo, species, mqFile, mqParameterFile, @@ -143,7 +142,8 @@ .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") .assertScalar(x = normMethod, type = "character", - validValues = c(MsCoreUtils::normalizeMethods(), "none", + validValues = c("center.mean", "center.median", + "diff.median", "vsn", "none", "center.mean.shared", "center.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", diff --git a/R/checkArgumentsPDTMT.R b/R/checkArgumentsPDTMT.R index 318bf6e..b5d05d2 100644 --- a/R/checkArgumentsPDTMT.R +++ b/R/checkArgumentsPDTMT.R @@ -4,7 +4,6 @@ #' @noRd #' @author Charlotte Soneson #' -#' @importFrom MsCoreUtils normalizeMethods .checkArgumentsPDTMT <- function( templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, experimentInfo, species, pdOutputFolder, pdResultName, @@ -159,7 +158,8 @@ .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") .assertScalar(x = normMethod, type = "character", - validValues = c(MsCoreUtils::normalizeMethods(), "none", + validValues = c("center.mean", "center.median", + "diff.median", "vsn", "none", "center.mean.shared", "center.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", diff --git a/R/checkArgumentsPDTMTptm.R b/R/checkArgumentsPDTMTptm.R index 8b820e6..47485df 100644 --- a/R/checkArgumentsPDTMTptm.R +++ b/R/checkArgumentsPDTMTptm.R @@ -4,7 +4,6 @@ #' @noRd #' @author Charlotte Soneson #' -#' @importFrom MsCoreUtils normalizeMethods .checkArgumentsPDTMTptm <- function( templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, diff --git a/R/checkArgumentsSE.R b/R/checkArgumentsSE.R index b837dc0..f962e83 100644 --- a/R/checkArgumentsSE.R +++ b/R/checkArgumentsSE.R @@ -4,7 +4,6 @@ #' @noRd #' @author Charlotte Soneson #' -#' @importFrom MsCoreUtils normalizeMethods #' @importFrom SummarizedExperiment assayNames colData #' @importFrom tools file_ext .checkArgumentsSE <- function( @@ -125,7 +124,8 @@ .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") .assertScalar(x = normMethod, type = "character", - validValues = c(MsCoreUtils::normalizeMethods(), "none", + validValues = c("center.mean", "center.median", + "diff.median", "vsn", "none", "center.mean.shared", "center.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", diff --git a/R/checkArgumentsSpectronaut.R b/R/checkArgumentsSpectronaut.R index 051aeed..2704e73 100644 --- a/R/checkArgumentsSpectronaut.R +++ b/R/checkArgumentsSpectronaut.R @@ -4,7 +4,6 @@ #' @noRd #' @author Charlotte Soneson #' -#' @importFrom MsCoreUtils normalizeMethods .checkArgumentsSpectronaut <- function( templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, experimentInfo, species, spectronautFile, @@ -163,7 +162,8 @@ .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") .assertScalar(x = normMethod, type = "character", - validValues = c(MsCoreUtils::normalizeMethods(), "none", + validValues = c("center.mean", "center.median", + "diff.median", "vsn", "none", "center.mean.shared", "center.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", diff --git a/R/runMaxQuantAnalysis.R b/R/runMaxQuantAnalysis.R index fcb7164..6de2bf9 100644 --- a/R/runMaxQuantAnalysis.R +++ b/R/runMaxQuantAnalysis.R @@ -114,8 +114,11 @@ #' average value across all samples in the \code{baselineGroup} from the #' same batch as the original sample. #' @param normMethod Character scalar indicating the normalization method to -#' use. Currently, any method from \code{MsCoreUtils::normalizeMethods()} -#' or \code{"none"} are valid values. +#' use. Currently, \code{"center.mean"}, \code{"center.median"}, +#' \code{"diff.median"}, \code{"center.mean.shared"}, +#' \code{"center.median.shared"} (applied on the log-transformed values), +#' \code{"vsn"} (applied on the natural-scale values), +#' and \code{"none"}, are valid values. #' @param spikeFeatures Character vector indicating the 'spike-in' features #' to use for estimation of normalization factors. If \code{NULL} #' (default), all features are used. diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index 4408415..28aaa89 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -527,6 +527,7 @@ assay(sce, aNames$assayImputIndic) <- !is.finite(assay(sce, aNames$assayLog2With ## Replace zeros/-Inf values by explicit NA values in the assays assay(sce, aNames$assayInput)[assay(sce, aNames$assayInput) == 0] <- NA +assay(sce, aNames$assayInput)[!is.finite(assay(sce, aNames$assayInput))] <- NA assay(sce, aNames$assayLog2WithNA)[!is.finite(assay(sce, aNames$assayLog2WithNA))] <- NA sce @@ -539,10 +540,20 @@ cat(normText(normMethod = normMethod)) ``` ```{r normalize, echo=(normMethod != "none"), eval=(normMethod != "none"), message=FALSE, fig.width = min(14, max(7, 0.5 * ncol(sce))), fig.height = 5/7 * min(14, max(7, 0.5 * ncol(sce)))} -sce <- doNormalization(sce, method = normMethod, - assayName = aNames$assayLog2WithNA, - normalizedAssayName = aNames$assayLog2NormWithNA, - spikeFeatures = spikeFeatures) +if (normMethod == "vsn") { + # natural-scale input -> log-scale output + sce <- doNormalization(sce, method = normMethod, + assayName = aNames$assayInput, + normalizedAssayName = aNames$assayLog2NormWithNA, + spikeFeatures = spikeFeatures) +} else { + # center.median, center.mean, diff.median, center.mean.shared, center.median.shared + # log-scale input -> log-scale output + sce <- doNormalization(sce, method = normMethod, + assayName = aNames$assayLog2WithNA, + normalizedAssayName = aNames$assayLog2NormWithNA, + spikeFeatures = spikeFeatures) +} makeIntensityBoxplots(sce = sce, assayName = aNames$assayLog2NormWithNA, doLog = FALSE, diff --git a/man/constants.Rd b/man/constants.Rd index 13db5e3..752c6ac 100644 --- a/man/constants.Rd +++ b/man/constants.Rd @@ -1,19 +1,11 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/constants.R -\docType{data} \name{constants} \alias{constants} \alias{EINPROT_COMPLEXES_FILE} \alias{EINPROT_WORMBASE_CONVTABLE} \alias{EINPROT_POMBASE_CONVTABLE} \title{Constants} -\format{ -An object of class \code{character} of length 1. - -An object of class \code{character} of length 1. - -An object of class \code{character} of length 1. -} \usage{ EINPROT_COMPLEXES_FILE @@ -31,4 +23,3 @@ complex database and ID conversion tables. \author{ Charlotte Soneson } -\keyword{datasets} diff --git a/man/defaultFiltersDF.Rd b/man/defaultFiltersDF.Rd index 07d5036..bea0095 100644 --- a/man/defaultFiltersDF.Rd +++ b/man/defaultFiltersDF.Rd @@ -1,13 +1,9 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/importDIANN.R -\docType{data} \name{defaultFiltersDF} \alias{defaultFiltersDF} \alias{einprotDIANNFiltersDF} \title{Default filters (DF)} -\format{ -An object of class \code{list} of length 1. -} \usage{ einprotDIANNFiltersDF } @@ -16,4 +12,3 @@ einprot provides default filters for data imported from various tools. To see the functions and thresholds used in each of these, print the corresponding object below. } -\keyword{datasets} diff --git a/man/defaultFiltersSE.Rd b/man/defaultFiltersSE.Rd index 6bbd795..f31b07b 100644 --- a/man/defaultFiltersSE.Rd +++ b/man/defaultFiltersSE.Rd @@ -1,6 +1,5 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/doFilter.R -\docType{data} \name{defaultFiltersSE} \alias{defaultFiltersSE} \alias{einprotMQFilters} @@ -9,17 +8,6 @@ \alias{einprotFragPipeFilters} \alias{einprotSpectronautFilters} \title{Default filters (SE)} -\format{ -An object of class \code{list} of length 5. - -An object of class \code{list} of length 3. - -An object of class \code{list} of length 3. - -An object of class \code{list} of length 3. - -An object of class \code{list} of length 4. -} \usage{ einprotMQFilters @@ -36,4 +24,3 @@ einprot provides default filters for data imported from various tools. To see the functions and thresholds used in each of these, print the corresponding object below. } -\keyword{datasets} diff --git a/man/runDIANNAnalysis.Rd b/man/runDIANNAnalysis.Rd index 5848c7e..c949a8f 100644 --- a/man/runDIANNAnalysis.Rd +++ b/man/runDIANNAnalysis.Rd @@ -229,8 +229,11 @@ average value across all samples in the \code{baselineGroup} from the same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to -use. Currently, any method from \code{MsCoreUtils::normalizeMethods()} -or \code{"none"} are valid values.} +use. Currently, \code{"center.mean"}, \code{"center.median"}, +\code{"diff.median"}, \code{"center.mean.shared"}, +\code{"center.median.shared"} (applied on the log-transformed values), +\code{"vsn"} (applied on the natural-scale values), +and \code{"none"}, are valid values.} \item{spikeFeatures}{Character vector indicating the 'spike-in' features to use for estimation of normalization factors. If \code{NULL} diff --git a/man/runFragPipeAnalysis.Rd b/man/runFragPipeAnalysis.Rd index a9280a6..f130d35 100644 --- a/man/runFragPipeAnalysis.Rd +++ b/man/runFragPipeAnalysis.Rd @@ -204,8 +204,11 @@ average value across all samples in the \code{baselineGroup} from the same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to -use. Currently, any method from \code{MsCoreUtils::normalizeMethods()} -or \code{"none"} are valid values.} +use. Currently, \code{"center.mean"}, \code{"center.median"}, +\code{"diff.median"}, \code{"center.mean.shared"}, +\code{"center.median.shared"} (applied on the log-transformed values), +\code{"vsn"} (applied on the natural-scale values), +and \code{"none"}, are valid values.} \item{spikeFeatures}{Character vector indicating the 'spike-in' features to use for estimation of normalization factors. If \code{NULL} diff --git a/man/runMaxQuantAnalysis.Rd b/man/runMaxQuantAnalysis.Rd index 5c67a81..bc479ab 100644 --- a/man/runMaxQuantAnalysis.Rd +++ b/man/runMaxQuantAnalysis.Rd @@ -211,8 +211,11 @@ average value across all samples in the \code{baselineGroup} from the same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to -use. Currently, any method from \code{MsCoreUtils::normalizeMethods()} -or \code{"none"} are valid values.} +use. Currently, \code{"center.mean"}, \code{"center.median"}, +\code{"diff.median"}, \code{"center.mean.shared"}, +\code{"center.median.shared"} (applied on the log-transformed values), +\code{"vsn"} (applied on the natural-scale values), +and \code{"none"}, are valid values.} \item{spikeFeatures}{Character vector indicating the 'spike-in' features to use for estimation of normalization factors. If \code{NULL} diff --git a/man/runPDTMTAnalysis.Rd b/man/runPDTMTAnalysis.Rd index 61968f5..4c74b89 100644 --- a/man/runPDTMTAnalysis.Rd +++ b/man/runPDTMTAnalysis.Rd @@ -241,8 +241,11 @@ average value across all samples in the \code{baselineGroup} from the same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to -use. Currently, any method from \code{MsCoreUtils::normalizeMethods()} -or \code{"none"} are valid values.} +use. Currently, \code{"center.mean"}, \code{"center.median"}, +\code{"diff.median"}, \code{"center.mean.shared"}, +\code{"center.median.shared"} (applied on the log-transformed values), +\code{"vsn"} (applied on the natural-scale values), +and \code{"none"}, are valid values.} \item{spikeFeatures}{Character vector indicating the 'spike-in' features to use for estimation of normalization factors. If \code{NULL} diff --git a/man/runSEAnalysis.Rd b/man/runSEAnalysis.Rd index ef7e152..d31a770 100644 --- a/man/runSEAnalysis.Rd +++ b/man/runSEAnalysis.Rd @@ -203,8 +203,11 @@ average value across all samples in the \code{baselineGroup} from the same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to -use. Currently, any method from \code{MsCoreUtils::normalizeMethods()} -or \code{"none"} are valid values.} +use. Currently, \code{"center.mean"}, \code{"center.median"}, +\code{"diff.median"}, \code{"center.mean.shared"}, +\code{"center.median.shared"} (applied on the log-transformed values), +\code{"vsn"} (applied on the natural-scale values), +and \code{"none"}, are valid values.} \item{spikeFeatures}{Character vector indicating the 'spike-in' features to use for estimation of normalization factors. If \code{NULL} diff --git a/man/runSpectronautAnalysis.Rd b/man/runSpectronautAnalysis.Rd index 3800fd5..2dd816b 100644 --- a/man/runSpectronautAnalysis.Rd +++ b/man/runSpectronautAnalysis.Rd @@ -222,8 +222,11 @@ average value across all samples in the \code{baselineGroup} from the same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to -use. Currently, any method from \code{MsCoreUtils::normalizeMethods()} -or \code{"none"} are valid values.} +use. Currently, \code{"center.mean"}, \code{"center.median"}, +\code{"diff.median"}, \code{"center.mean.shared"}, +\code{"center.median.shared"} (applied on the log-transformed values), +\code{"vsn"} (applied on the natural-scale values), +and \code{"none"}, are valid values.} \item{spikeFeatures}{Character vector indicating the 'spike-in' features to use for estimation of normalization factors. If \code{NULL} From 7c5173e31d14d325cad7cbed42700bcde2d8b049 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 10 Jun 2026 15:32:14 +0200 Subject: [PATCH 51/65] Adapt to roxygen 8.0.0 --- DESCRIPTION | 2 +- R/doPCA.R | 5 +++-- R/importDIANN.R | 2 +- R/makeAbundanceExport.R | 2 +- R/plotImputation.R | 2 +- R/plotMissingValues.R | 2 +- R/plotVolcano.R | 14 +++++++------- R/prepareFinalSCE.R | 2 +- R/runDIANNAnalysis.R | 4 ++-- R/runFragPipeAnalysis.R | 4 ++-- R/runMaxQuantAnalysis.R | 4 ++-- R/runPDTMTAnalysis.R | 4 ++-- R/runPDTMTptmAnalysis.R | 2 +- R/runSEAnalysis.R | 4 ++-- R/runSpectronautAnalysis.R | 4 ++-- R/runTests.R | 4 ++-- R/seqLogoApp.R | 4 ++-- R/summaryPlots.R | 4 ++-- 18 files changed, 35 insertions(+), 34 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 80e902b..fd30fbf 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -78,7 +78,6 @@ Imports: motifStack, msigdbrdata Remotes: msigdbrdata=github::igordot/msigdbrdata -RoxygenNote: 7.3.3 Suggests: BiocManager, testthat (>= 3.0.0), @@ -88,3 +87,4 @@ Suggests: Config/testthat/edition: 3 VignetteBuilder: knitr biocViews: Proteomics, DataImport +Config/roxygen2/version: 8.0.0 diff --git a/R/doPCA.R b/R/doPCA.R index d93fcaa..4be2fa7 100644 --- a/R/doPCA.R +++ b/R/doPCA.R @@ -61,8 +61,9 @@ #' @importFrom dplyr bind_rows filter arrange mutate #' @importFrom scales percent_format #' @importFrom ggplot2 ggplot aes geom_point ggtitle coord_fixed labs geom_col -#' scale_fill_manual scale_y_continuous scale_x_continuous theme coord_flip -#' geom_text element_blank element_line scale_fill_gradient2 theme_bw +#' @importFrom ggplot2 scale_fill_manual scale_y_continuous scale_x_continuous +#' @importFrom ggplot2 theme coord_flip geom_text element_blank element_line +#' @importFrom ggplot2 scale_fill_gradient2 theme_bw #' @importFrom cowplot theme_cowplot plot_grid #' @importFrom BiocSingular ExactParam #' @importFrom SummarizedExperiment rowData colData diff --git a/R/importDIANN.R b/R/importDIANN.R index 876533b..d3940f4 100644 --- a/R/importDIANN.R +++ b/R/importDIANN.R @@ -81,7 +81,7 @@ einprotDIANNFiltersDF <- list(Filter = function(df) { #' #' @importFrom tibble tibble #' @importFrom dplyr select filter inner_join collect compute all_of group_by -#' summarize count distinct +#' @importFrom dplyr summarize count distinct #' @importFrom tidyr pivot_wider #' @importFrom QFeatures readSummarizedExperiment #' @importFrom SummarizedExperiment assayNames colnames diff --git a/R/makeAbundanceExport.R b/R/makeAbundanceExport.R index b456540..3c3666e 100644 --- a/R/makeAbundanceExport.R +++ b/R/makeAbundanceExport.R @@ -22,7 +22,7 @@ #' head(abExp) #' #' @importFrom dplyr select filter starts_with contains if_any full_join -#' matches everything +#' @importFrom dplyr matches everything #' makeAbundanceExport <- function(testresList, abundancePrefix) { .assertVector(x = testresList, type = "list") diff --git a/R/plotImputation.R b/R/plotImputation.R index b2411e6..e2de2df 100644 --- a/R/plotImputation.R +++ b/R/plotImputation.R @@ -28,7 +28,7 @@ #' @importFrom tidyr gather #' @importFrom dplyr left_join mutate #' @importFrom ggplot2 ggplot aes geom_histogram theme_bw facet_wrap labs -#' scale_fill_manual +#' @importFrom ggplot2 scale_fill_manual #' @importFrom rlang .data #' plotImputationDistribution <- function(sce, assayToPlot, assayImputation, diff --git a/R/plotMissingValues.R b/R/plotMissingValues.R index e6dd227..50b40d4 100644 --- a/R/plotMissingValues.R +++ b/R/plotMissingValues.R @@ -117,7 +117,7 @@ plotMissingValuesHeatmap <- function(sce, assayMissing, #' valueType = "percentage") #' #' @importFrom ggplot2 ggplot aes theme geom_bar theme_bw labs expand_limits -#' geom_text +#' @importFrom ggplot2 geom_text #' @importFrom dplyr filter %>% #' @importFrom rlang .data #' @importFrom methods is diff --git a/R/plotVolcano.R b/R/plotVolcano.R index 6e6ee00..34295e9 100644 --- a/R/plotVolcano.R +++ b/R/plotVolcano.R @@ -81,13 +81,13 @@ getComplexesToPlot <- function(featureCollections, #' @noRd #' @keywords internal #' @importFrom dplyr filter select mutate left_join %>% matches -#' group_by summarize all_of +#' @importFrom dplyr group_by summarize all_of #' @importFrom tidyr gather #' @importFrom rlang .data #' @importFrom SummarizedExperiment colData #' @importFrom ggplot2 ggplot aes geom_bar position_jitterdodge geom_errorbar -#' theme_bw theme element_text labs scale_fill_manual geom_jitter -#' position_dodge facet_grid +#' @importFrom ggplot2 theme_bw theme element_text labs scale_fill_manual +#' @importFrom ggplot2 geom_jitter position_dodge facet_grid #' @importFrom stats sd #' .complexBarPlot <- function(res, prs, sce, cplx, colpat, groupmap) { @@ -190,8 +190,8 @@ getComplexesToPlot <- function(featureCollections, #' @importFrom forcats fct_reorder #' @importFrom rlang .data #' @importFrom ggplot2 ggplot aes geom_col coord_flip geom_text theme_minimal -#' theme element_blank ggtitle element_line scale_y_continuous -#' scale_fill_gradient2 +#' @importFrom ggplot2 theme element_blank ggtitle element_line scale_y_continuous +#' @importFrom ggplot2 scale_fill_gradient2 #' @importFrom scales muted #' .makeWaterfallPlot <- function(res, ntop, xv = "logFC", @@ -263,7 +263,7 @@ getComplexesToPlot <- function(featureCollections, #' @noRd #' @keywords internal #' @importFrom ggplot2 ggplot aes theme_bw theme annotate labs coord_cartesian -#' element_text geom_line +#' @importFrom ggplot2 element_text geom_line #' @importFrom rlang .data #' .makeBaseVolcano <- function(res, testType, xv, yv, plotnote, plottitle, @@ -459,7 +459,7 @@ getComplexesToPlot <- function(featureCollections, #' @author Charlotte Soneson #' @export #' @importFrom ggplot2 geom_point aes labs geom_hline theme_bw coord_cartesian -#' theme element_text +#' @importFrom ggplot2 theme element_text #' @importFrom ggrepel geom_text_repel #' @importFrom ggiraph geom_point_interactive opts_hover girafe_options #' @importFrom dplyr filter arrange between row_number desc diff --git a/R/prepareFinalSCE.R b/R/prepareFinalSCE.R index 425836f..767da13 100644 --- a/R/prepareFinalSCE.R +++ b/R/prepareFinalSCE.R @@ -23,7 +23,7 @@ #' @importFrom tibble rownames_to_column #' @importFrom dplyr filter #' @importFrom iSEEu registerLogFCFields registerAveAbFields -#' registerPValueFields registerFeatureSetCollections +#' @importFrom iSEEu registerPValueFields registerFeatureSetCollections #' @importFrom utils write.table #' @importFrom SingleCellExperiment SingleCellExperiment #' diff --git a/R/runDIANNAnalysis.R b/R/runDIANNAnalysis.R index acd8a43..4361ad6 100644 --- a/R/runDIANNAnalysis.R +++ b/R/runDIANNAnalysis.R @@ -99,11 +99,11 @@ #' @importFrom cowplot plot_grid theme_cowplot #' @importFrom htmltools tagList #' @importFrom dplyr %>% select starts_with full_join filter matches everything -#' mutate +#' @importFrom dplyr mutate #' @importFrom knitr current_input #' @importFrom ComplexUpset upset #' @importFrom ggplot2 ggplot aes geom_bar coord_flip theme_bw labs theme -#' element_text geom_point ggtitle +#' @importFrom ggplot2 element_text geom_point ggtitle #' @importFrom tibble rownames_to_column #' @importFrom S4Vectors metadata #' @importFrom scater runPCA diff --git a/R/runFragPipeAnalysis.R b/R/runFragPipeAnalysis.R index 3af5172..4bb2f0b 100755 --- a/R/runFragPipeAnalysis.R +++ b/R/runFragPipeAnalysis.R @@ -45,11 +45,11 @@ #' @importFrom cowplot plot_grid theme_cowplot #' @importFrom htmltools tagList #' @importFrom dplyr %>% select starts_with full_join filter matches everything -#' mutate +#' @importFrom dplyr mutate #' @importFrom knitr current_input #' @importFrom ComplexUpset upset #' @importFrom ggplot2 ggplot aes geom_bar coord_flip theme_bw labs theme -#' element_text geom_point ggtitle +#' @importFrom ggplot2 element_text geom_point ggtitle #' @importFrom tibble rownames_to_column #' @importFrom S4Vectors metadata #' @importFrom scater runPCA diff --git a/R/runMaxQuantAnalysis.R b/R/runMaxQuantAnalysis.R index 6de2bf9..3930080 100644 --- a/R/runMaxQuantAnalysis.R +++ b/R/runMaxQuantAnalysis.R @@ -240,11 +240,11 @@ #' @importFrom cowplot plot_grid theme_cowplot #' @importFrom htmltools tagList #' @importFrom dplyr %>% select starts_with full_join filter matches everything -#' mutate +#' @importFrom dplyr mutate #' @importFrom knitr current_input #' @importFrom ComplexUpset upset #' @importFrom ggplot2 ggplot aes geom_bar coord_flip theme_bw labs theme -#' element_text geom_point ggtitle +#' @importFrom ggplot2 element_text geom_point ggtitle #' @importFrom tibble rownames_to_column #' @importFrom S4Vectors metadata #' @importFrom scater runPCA diff --git a/R/runPDTMTAnalysis.R b/R/runPDTMTAnalysis.R index 7dff8ba..8198d95 100644 --- a/R/runPDTMTAnalysis.R +++ b/R/runPDTMTAnalysis.R @@ -82,11 +82,11 @@ #' @importFrom cowplot plot_grid theme_cowplot #' @importFrom htmltools tagList #' @importFrom dplyr %>% select starts_with full_join filter matches everything -#' mutate relocate last_col +#' @importFrom dplyr mutate relocate last_col #' @importFrom knitr current_input #' @importFrom ComplexUpset upset #' @importFrom ggplot2 ggplot aes geom_bar coord_flip theme_bw labs theme -#' element_text geom_point ggtitle +#' @importFrom ggplot2 element_text geom_point ggtitle #' @importFrom tibble rownames_to_column #' @importFrom S4Vectors metadata #' @importFrom scater runPCA diff --git a/R/runPDTMTptmAnalysis.R b/R/runPDTMTptmAnalysis.R index b18b287..8f9590e 100644 --- a/R/runPDTMTptmAnalysis.R +++ b/R/runPDTMTptmAnalysis.R @@ -121,7 +121,7 @@ #' @importFrom ComplexUpset upset #' @importFrom tibble rownames_to_column #' @importFrom ggplot2 ggplot aes geom_abline theme_bw labs theme -#' element_text geom_point ggtitle +#' @importFrom ggplot2 element_text geom_point ggtitle #' @importFrom ggrepel geom_text_repel #' runPDTMTptmAnalysis <- function( diff --git a/R/runSEAnalysis.R b/R/runSEAnalysis.R index dbde432..0c6ce33 100644 --- a/R/runSEAnalysis.R +++ b/R/runSEAnalysis.R @@ -75,11 +75,11 @@ #' @importFrom cowplot plot_grid theme_cowplot #' @importFrom htmltools tagList #' @importFrom dplyr %>% select starts_with full_join filter matches everything -#' mutate +#' @importFrom dplyr mutate #' @importFrom knitr current_input #' @importFrom ComplexUpset upset #' @importFrom ggplot2 ggplot aes geom_bar coord_flip theme_bw labs theme -#' element_text geom_point ggtitle +#' @importFrom ggplot2 element_text geom_point ggtitle #' @importFrom tibble rownames_to_column #' @importFrom S4Vectors metadata #' @importFrom scater runPCA diff --git a/R/runSpectronautAnalysis.R b/R/runSpectronautAnalysis.R index 3279455..1ab276c 100644 --- a/R/runSpectronautAnalysis.R +++ b/R/runSpectronautAnalysis.R @@ -41,11 +41,11 @@ #' @importFrom cowplot plot_grid theme_cowplot #' @importFrom htmltools tagList #' @importFrom dplyr %>% select starts_with full_join filter matches everything -#' mutate +#' @importFrom dplyr mutate #' @importFrom knitr current_input #' @importFrom ComplexUpset upset #' @importFrom ggplot2 ggplot aes geom_bar coord_flip theme_bw labs theme -#' element_text geom_point ggtitle +#' @importFrom ggplot2 element_text geom_point ggtitle #' @importFrom tibble rownames_to_column #' @importFrom S4Vectors metadata #' @importFrom scater runPCA diff --git a/R/runTests.R b/R/runTests.R index 0b68f52..21602ce 100644 --- a/R/runTests.R +++ b/R/runTests.R @@ -301,11 +301,11 @@ #' @importFrom SummarizedExperiment assay rowData colData #' @importFrom stats model.matrix p.adjust #' @importFrom limma lmFit treat topTreat cameraPR ids2indices eBayes -#' topTable +#' @importFrom limma topTable #' @importFrom proDA proDA test_diff #' @importFrom tibble rownames_to_column #' @importFrom dplyr %>% mutate select contains arrange filter left_join -#' across everything rename +#' @importFrom dplyr across everything rename #' @importFrom rlang .data #' @importFrom S4Vectors mcols #' @importFrom genefilter rowttests diff --git a/R/seqLogoApp.R b/R/seqLogoApp.R index a030c4a..2f08336 100644 --- a/R/seqLogoApp.R +++ b/R/seqLogoApp.R @@ -28,8 +28,8 @@ #' } #' #' @importFrom shiny fluidPage fluidPage sidebarLayout sidebarPanel -#' mainPanel downloadButton plotOutput renderPlot reactive downloadHandler -#' shinyApp textOutput renderText +#' @importFrom shiny mainPanel downloadButton plotOutput renderPlot reactive +#' @importFrom shiny downloadHandler shinyApp textOutput renderText #' @importFrom motifStack pcm2pfm colorset #' @importFrom Biostrings consensusMatrix AAStringSet #' @importFrom DT DTOutput renderDT diff --git a/R/summaryPlots.R b/R/summaryPlots.R index 680bfc1..93830ed 100644 --- a/R/summaryPlots.R +++ b/R/summaryPlots.R @@ -34,7 +34,7 @@ #' ylab = "log intensity") #' #' @importFrom ggplot2 ggplot aes geom_boxplot theme_bw scale_y_log10 -#' theme_bw theme labs element_text rel +#' @importFrom ggplot2 theme_bw theme labs element_text rel #' @importFrom tidyr gather #' @importFrom dplyr left_join #' @importFrom tibble rownames_to_column @@ -161,7 +161,7 @@ makeMeanSDPlot <- function(sce, assayName, xlab = "Mean", ylab = "SD") { #' @author Charlotte Soneson #' #' @importFrom ggplot2 ggplot geom_point geom_line aes labs coord_cartesian -#' theme_bw +#' @importFrom ggplot2 theme_bw #' @importFrom cowplot plot_grid #' makeSAPlot <- function(testList) { From 83562df07313c645da9506c82aa41ca626dc41e7 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Wed, 10 Jun 2026 16:43:09 +0200 Subject: [PATCH 52/65] Fix tests --- tests/testthat/test-checkArgumentsSpectronaut.R | 2 +- tests/testthat/test-runSpectronautAnalysis.R | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-checkArgumentsSpectronaut.R b/tests/testthat/test-checkArgumentsSpectronaut.R index 3d61fec..4ea819c 100644 --- a/tests/testthat/test-checkArgumentsSpectronaut.R +++ b/tests/testthat/test-checkArgumentsSpectronaut.R @@ -17,7 +17,7 @@ test_that("argument checking for Spectronaut works", { outLevel = "pg", spectronautLogFile = system.file( "extdata", "spectronaut_example", - "PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.log.txt", + "PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.setup.txt", package = "einprot"), aName = "PG.Quantity", idCol = function(df) combineIds(df, combineCols = c("PG.Genes", diff --git a/tests/testthat/test-runSpectronautAnalysis.R b/tests/testthat/test-runSpectronautAnalysis.R index cc5cb50..80e08d4 100644 --- a/tests/testthat/test-runSpectronautAnalysis.R +++ b/tests/testthat/test-runSpectronautAnalysis.R @@ -18,7 +18,7 @@ test_that("runSpectronautAnalysis works", { outLevel = "pg", spectronautLogFile = system.file( "extdata", "spectronaut_example", - "PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.log.txt", + "PG.QValRUN_005_PGPEP_075_Minlog2PrecInt_0_reduced_pivot_filtered.setup.txt", package = "einprot"), aName = "PG.Quantity", idCol = function(df) combineIds(df, combineCols = c("PG.Genes", From 241a1be870e2d16ec68ce45cefec8c59f6320cd9 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Tue, 23 Jun 2026 09:16:15 +0200 Subject: [PATCH 53/65] Bump version (0.10.2) --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index fd30fbf..ae77f8a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: einprot Type: Package Title: A collection of proteomics analysis utilities and workflows -Version: 0.10.1 +Version: 0.10.2 Authors@R: c( person("Charlotte", "Soneson", email = "charlotte.soneson@fmi.ch", role = c("aut", "cre"), comment = c(ORCID = "0000-0003-3833-2169")), From 1e9e00492f592087662003567cac703198158ee2 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 28 Jun 2026 13:52:36 +0200 Subject: [PATCH 54/65] Add unit tests --- R/plotVolcano.R | 4 +- tests/testthat/test-checkArgumentsDIANN.R | 4 ++ tests/testthat/test-checkArgumentsFragPipe.R | 4 ++ tests/testthat/test-checkArgumentsMaxQuant.R | 4 ++ tests/testthat/test-checkArgumentsPDTMT.R | 4 ++ tests/testthat/test-checkArgumentsSE.R | 4 ++ .../testthat/test-checkArgumentsSpectronaut.R | 4 ++ tests/testthat/test-doImputation.R | 5 ++ tests/testthat/test-doNormalization.R | 52 +++++++++++++++++++ tests/testthat/test-plotMissingValues.R | 8 +++ tests/testthat/test-plotVolcano.R | 19 ++++++- 11 files changed, 109 insertions(+), 3 deletions(-) diff --git a/R/plotVolcano.R b/R/plotVolcano.R index 34295e9..0b10fb8 100644 --- a/R/plotVolcano.R +++ b/R/plotVolcano.R @@ -794,7 +794,7 @@ plotVolcano <- function(sce, res, testType, xv = NULL, yv = NULL, xvma = NULL, ## Need to redo the filtering of res here - can't use labeldfVolcano ## since we don't want to include non-significant proteins (including ## those that are manually labeled). - if (!is.null(stringDb) && "IDsForSTRING" %in% colnames(res)) { + if (!is.null(stringDb) && "IDsForSTRING" %in% colnames(res)) { #nocov start res0 <- res %>% dplyr::filter(.data[[cols$volcind]] & .data$allowedSign) %>% dplyr::arrange(dplyr::desc(abs(.data[[cols$xv]]) + @@ -813,7 +813,7 @@ plotVolcano <- function(sce, res, testType, xv = NULL, yv = NULL, xvma = NULL, res0 %>% dplyr::filter(.data[[cols$xv]] < 0) %>% dplyr::pull("STRING_id")) } - } + } #nocov end if (!is.null(ggbar)) { for (ggb in ggbar) { diff --git a/tests/testthat/test-checkArgumentsDIANN.R b/tests/testthat/test-checkArgumentsDIANN.R index 3d1d3b5..5575ba4 100644 --- a/tests/testthat/test-checkArgumentsDIANN.R +++ b/tests/testthat/test-checkArgumentsDIANN.R @@ -246,6 +246,10 @@ test_that("argument checking for DIANN works", { args <- args0 args$proteinIdCol <- "Protein.ID" expect_null(do.call(.checkArgumentsDIANN, args)) + args <- args0 + args$proteinIdCol <- function(x, y) x + y + expect_error(do.call(.checkArgumentsDIANN, args), + "length(formals(proteinIdCol)) == 1 is not TRUE", fixed = TRUE) ## stringIdCol args <- args0 diff --git a/tests/testthat/test-checkArgumentsFragPipe.R b/tests/testthat/test-checkArgumentsFragPipe.R index 48b94a6..db4bf76 100644 --- a/tests/testthat/test-checkArgumentsFragPipe.R +++ b/tests/testthat/test-checkArgumentsFragPipe.R @@ -218,6 +218,10 @@ test_that("argument checking for FP works", { args <- args0 args$proteinIdCol <- "Protein.ID" expect_null(do.call(.checkArgumentsFragPipe, args)) + args <- args0 + args$proteinIdCol <- function(x, y) x + y + expect_error(do.call(.checkArgumentsFragPipe, args), + "length(formals(proteinIdCol)) == 1 is not TRUE", fixed = TRUE) ## stringIdCol args <- args0 diff --git a/tests/testthat/test-checkArgumentsMaxQuant.R b/tests/testthat/test-checkArgumentsMaxQuant.R index cb6d8b1..83cde27 100644 --- a/tests/testthat/test-checkArgumentsMaxQuant.R +++ b/tests/testthat/test-checkArgumentsMaxQuant.R @@ -218,6 +218,10 @@ test_that("argument checking for MQ works", { args$proteinIdCol <- 1 expect_error(do.call(.checkArgumentsMaxQuant, args), "'proteinIdCol' must be of class 'character'") + args <- args0 + args$proteinIdCol <- function(x, y) x + y + expect_error(do.call(.checkArgumentsMaxQuant, args), + "length(formals(proteinIdCol)) == 1 is not TRUE", fixed = TRUE) ## stringIdCol args <- args0 diff --git a/tests/testthat/test-checkArgumentsPDTMT.R b/tests/testthat/test-checkArgumentsPDTMT.R index e33cfc0..e815c2f 100644 --- a/tests/testthat/test-checkArgumentsPDTMT.R +++ b/tests/testthat/test-checkArgumentsPDTMT.R @@ -246,6 +246,10 @@ test_that("argument checking for PD-TMT works", { args$proteinIdCol <- 1 expect_error(do.call(.checkArgumentsPDTMT, args), "'proteinIdCol' must be of class 'character'") + args <- args0 + args$proteinIdCol <- function(x, y) x + y + expect_error(do.call(.checkArgumentsPDTMT, args), + "length(formals(proteinIdCol)) == 1 is not TRUE", fixed = TRUE) ## stringIdCol args <- args0 diff --git a/tests/testthat/test-checkArgumentsSE.R b/tests/testthat/test-checkArgumentsSE.R index 7eda466..fd9b626 100644 --- a/tests/testthat/test-checkArgumentsSE.R +++ b/tests/testthat/test-checkArgumentsSE.R @@ -213,6 +213,10 @@ test_that("argument checking for SE works", { args$proteinIdCol <- 1 expect_error(do.call(.checkArgumentsSE, args), "'proteinIdCol' must be of class 'character'") + args <- args0 + args$proteinIdCol <- function(x, y) x + y + expect_error(do.call(.checkArgumentsSE, args), + "length(formals(proteinIdCol)) == 1 is not TRUE", fixed = TRUE) ## stringIdCol args <- args0 diff --git a/tests/testthat/test-checkArgumentsSpectronaut.R b/tests/testthat/test-checkArgumentsSpectronaut.R index 4ea819c..965b321 100644 --- a/tests/testthat/test-checkArgumentsSpectronaut.R +++ b/tests/testthat/test-checkArgumentsSpectronaut.R @@ -250,6 +250,10 @@ test_that("argument checking for Spectronaut works", { args <- args0 args$proteinIdCol <- "Protein.ID" expect_null(do.call(.checkArgumentsSpectronaut, args)) + args <- args0 + args$proteinIdCol <- function(x, y) x + y + expect_error(do.call(.checkArgumentsSpectronaut, args), + "length(formals(proteinIdCol)) == 1 is not TRUE", fixed = TRUE) ## stringIdCol args <- args0 diff --git a/tests/testthat/test-doImputation.R b/tests/testthat/test-doImputation.R index 4989e92..5212745 100644 --- a/tests/testthat/test-doImputation.R +++ b/tests/testthat/test-doImputation.R @@ -1,6 +1,7 @@ test_that("minProbGlobalFun works", { set.seed(123L) mat <- matrix(runif(70, min = 10, max = 15), nrow = 10) + mat0 <- mat missingpos <- cbind(row = c(1, 1, 1, 4, 6, 9), col = c(2, 3, 7, 1, 6, 7)) mat[missingpos] <- NA expect_equal(sum(is.na(mat)), nrow(missingpos)) @@ -30,6 +31,10 @@ test_that("minProbGlobalFun works", { multSigma = c(1, 2)), "'multSigma' must have length 1") + set.seed(1L) + out <- .minProbGlobalFun(mat0, lowQuantile = 0.01, multSigma = 1) + expect_identical(out, mat0) + set.seed(1L) out <- .minProbGlobalFun(mat, lowQuantile = 0.01, multSigma = 1) trueq <- stats::quantile(mat[!is.na(mat)], 0.01) diff --git a/tests/testthat/test-doNormalization.R b/tests/testthat/test-doNormalization.R index c203cb6..49d011d 100644 --- a/tests/testthat/test-doNormalization.R +++ b/tests/testthat/test-doNormalization.R @@ -77,6 +77,32 @@ test_that("normalization works", { na.rm = TRUE), rep(0L, ncol(normout)), ignore_attr = TRUE) + ## center.median.shared + normout <- doNormalization(sce = sce_mq_preimputation, + method = "center.median.shared", + assayName = "iBAQ", + normalizedAssayName = "normalizedAssay", + spikeFeatures = NULL) + expect_s4_class(normout, "SummarizedExperiment") + expect_true("normalizedAssay" %in% SummarizedExperiment::assayNames(normout)) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "iBAQ"))) == 507) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "normalizedAssay"))) == 507) + shared <- which(rowSums(is.na(SummarizedExperiment::assay(normout, "iBAQ"))) == 0) + expect_equal(apply(assay(normout, "normalizedAssay")[shared, ], 2, stats::median, + na.rm = TRUE), rep(0L, ncol(normout)), + ignore_attr = TRUE) + + ## center.median.shared, no shared features + notshared <- which(rowSums(is.na(SummarizedExperiment::assay(sce_mq_preimputation, "iBAQ"))) != 0) + expect_error(doNormalization(sce = sce_mq_preimputation[notshared, ], + method = "center.median.shared", + assayName = "iBAQ", + normalizedAssayName = "normalizedAssay", + spikeFeatures = NULL), + "No features observed in all samples") + ## center.mean normout <- doNormalization(sce = sce_mq_preimputation, method = "center.mean", @@ -93,6 +119,32 @@ test_that("normalization works", { na.rm = TRUE), rep(0L, ncol(normout)), ignore_attr = TRUE, tolerance = 1e-5) + ## center.mean.shared + normout <- doNormalization(sce = sce_mq_preimputation, + method = "center.mean.shared", + assayName = "iBAQ", + normalizedAssayName = "normalizedAssay", + spikeFeatures = NULL) + expect_s4_class(normout, "SummarizedExperiment") + expect_true("normalizedAssay" %in% SummarizedExperiment::assayNames(normout)) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "iBAQ"))) == 507) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "normalizedAssay"))) == 507) + shared <- which(rowSums(is.na(SummarizedExperiment::assay(normout, "iBAQ"))) == 0) + expect_equal(apply(assay(normout, "normalizedAssay")[shared, ], 2, mean, + na.rm = TRUE), rep(0L, ncol(normout)), + ignore_attr = TRUE) + + ## center.mean.shared, no shared features + notshared <- which(rowSums(is.na(SummarizedExperiment::assay(sce_mq_preimputation, "iBAQ"))) != 0) + expect_error(doNormalization(sce = sce_mq_preimputation[notshared, ], + method = "center.mean.shared", + assayName = "iBAQ", + normalizedAssayName = "normalizedAssay", + spikeFeatures = NULL), + "No features observed in all samples") + ## div.mean normout <- doNormalization(sce = sce_mq_preimputation, method = "div.mean", diff --git a/tests/testthat/test-plotMissingValues.R b/tests/testthat/test-plotMissingValues.R index 1d3d5c7..1876264 100644 --- a/tests/testthat/test-plotMissingValues.R +++ b/tests/testthat/test-plotMissingValues.R @@ -155,6 +155,14 @@ test_that("missing value plots work", { expect_s3_class(out3, "ggplot") expect_named(out3$data, c("sample", "nNA", "pNA", "assay")) + out4 <- plotFractionDetectedPerSample( + dfNA = DataFrame(as.data.frame(nbr_na_mq$nNAcols) %>% + dplyr::rename(sample = name) %>% + dplyr::mutate(pNA = 100 * pNA)), + valueType = "percentage") + expect_s3_class(out4, "ggplot") + expect_named(out4$data, c("sample", "nNA", "pNA", "assay")) + ## ------------------------------------------------------------------------- ## plotDetectedInSamples ## ------------------------------------------------------------------------- diff --git a/tests/testthat/test-plotVolcano.R b/tests/testthat/test-plotVolcano.R index 772e6b4..bae554c 100644 --- a/tests/testthat/test-plotVolcano.R +++ b/tests/testthat/test-plotVolcano.R @@ -331,6 +331,23 @@ test_that("volcano plots work", { ## .complexBarPlot ## ------------------------------------------------------------------------- ## limma + ## ... all values NA + tmp <- out_limma$tests[[1]] + idx <- grep("iBAQ", colnames(tmp)) + for (i in idx) { + tmp[[i]] <- NA + } + out <- .complexBarPlot( + res = tmp, + prs = fcoll_mq_final$complexes[[1]], + sce = sce_mq_final, + cplx = names(fcoll_mq_final$complexes)[1], + colpat = "iBAQ", + groupmap = NULL + ) + expect_null(out) + + ## ... actual values out <- .complexBarPlot( res = out_limma$tests[[1]], prs = fcoll_mq_final$complexes[[1]], @@ -782,7 +799,7 @@ test_that("volcano plots work", { outl <- plotVolcano(sce = sce_mq_final, res = out_limma$tests[[1]], testType = "limma", xv = "logFC", yv = "mlog10p", xvma = "AveExpr", - volcind = "showInVolcano", + pv = "P.Value", volcind = "showInVolcano", plotnote = out_limma$plotnotes[[1]], plottitle = out_limma$plottitles[[1]], plotsubtitle = out_limma$plotsubtitles[[1]], From 5e52bd727f4703550cbee7154bdc0f7e7d819276 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 28 Jun 2026 14:38:35 +0200 Subject: [PATCH 55/65] Support additional imputation functions, rename method argument of doImputation to imputeMethod --- R/doImputation.R | 184 +++++++++++++++- man/doImputation.Rd | 54 ++++- tests/testthat/test-doImputation.R | 334 +++++++++++++++++++++++++++-- 3 files changed, 538 insertions(+), 34 deletions(-) diff --git a/R/doImputation.R b/R/doImputation.R index 5712e8e..a4e3156 100644 --- a/R/doImputation.R +++ b/R/doImputation.R @@ -28,6 +28,107 @@ } } +#' @importFrom stats quantile median rnorm +#' @keywords internal +#' @noRd +#' +.minProbGlobalOffsetFun <- function(mat, lowQuantile = 0.01, multSigma = 1, + constOffset = NULL, relOffset = NULL) { + .assertVector(x = mat, type = "matrix") + .assertScalar(x = lowQuantile, type = "numeric", rngExcl = c(0, 1)) + .assertScalar(x = multSigma, type = "numeric") + .assertScalar(x = constOffset, type = "numeric", allowNULL = TRUE) + .assertScalar(x = relOffset, type = "numeric", allowNULL = TRUE) + if (!is.null(constOffset) && !is.null(relOffset)) { + stop("At most one of constOffset and relOffset can be specified") + } + + nMissing <- sum(is.na(c(mat))) + if (nMissing == 0) { + return(mat) + } else { + ## Get a global low quantile + globalMin <- stats::quantile(c(mat), prob = lowQuantile, na.rm = TRUE) + + ## Only use features observed in at least half the samples to calculate + ## the standard deviation + keepRows <- which(apply(!is.na(mat), 1, sum) / ncol(mat) > 0.5) + globalSd <- stats::median(apply(mat[keepRows, ], 1, sd, na.rm = TRUE), + na.rm = TRUE) * multSigma + print(globalSd) + + if (!is.null(constOffset)) { + globalMin <- globalMin + constOffset + } else if (!is.null(relOffset)) { + globalMin <- globalMin + relOffset * globalSd + } + + ## Generate random values and replace NAs + randValues <- stats::rnorm(nMissing, mean = globalMin, sd = globalSd) + mat[is.na(mat)] <- randValues + return(mat) + } +} + +#' @importFrom stats quantile median rnorm +#' @keywords internal +#' @noRd +#' +.minProbOffsetFun <- function(mat, lowQuantile = 0.01, multSigma = 1, + margin = 2L, constOffset = NULL, relOffset = NULL) { + .assertVector(x = mat, type = "matrix") + .assertScalar(x = lowQuantile, type = "numeric", rngExcl = c(0, 1)) + .assertScalar(x = multSigma, type = "numeric") + .assertScalar(x = margin, type = "numeric", validValues = c(1, 2)) + .assertScalar(x = constOffset, type = "numeric", allowNULL = TRUE) + .assertScalar(x = relOffset, type = "numeric", allowNULL = TRUE) + if (!is.null(constOffset) && !is.null(relOffset)) { + stop("At most one of constOffset and relOffset can be specified") + } + + nMissing <- sum(is.na(c(mat))) + if (nMissing == 0) { + return(mat) + } else { + if (margin == 1) { + mat <- t(mat) + } + + ## Get a low quantile per sample + minPerSample <- apply(mat, MARGIN = 2, FUN = stats::quantile, + prob = lowQuantile, na.rm = TRUE) + + ## Only use features observed in at least half the samples to calculate + ## the standard deviation + keepRows <- which(apply(!is.na(mat), 1, sum) / ncol(mat) > 0.5) + globalSd <- stats::median(apply(mat[keepRows, ], 1, sd, na.rm = TRUE), + na.rm = TRUE) * multSigma + print(globalSd) + + if (!is.null(constOffset)) { + minPerSample <- minPerSample + constOffset + } else if (!is.null(relOffset)) { + minPerSample <- minPerSample + relOffset * globalSd + } + + ## Generate random values and replace NAs + for (i in seq_len(ncol(mat))) { + randValues <- stats::rnorm(nrow(mat), mean = minPerSample[i], + sd = globalSd) + naidx <- which(is.na(mat[, i])) + if (length(naidx) > 0) { + mat[naidx, i] <- randValues[naidx] + } + } + + if (margin == 1) { + mat <- t(mat) + } + + return(mat) + } +} + #' Perform imputation of NA values #' #' Perform imputation of missing values (represented by \code{NA}) in one assay @@ -35,12 +136,19 @@ #' complete data (including imputed values). #' #' @param sce A \code{SummarizedExperiment} object (or a derivative). -#' @param method Character scalar giving the imputation method. Currently, +#' @param imputeMethod Character scalar giving the imputation method. Currently, #' \code{"MinProb"} (provided in the \code{MsCoreUtils} package), -#' \code{"impSeqRob"} (provided in the \code{rrcovNA} package), and +#' \code{"impSeqRob"} (provided in the \code{rrcovNA} package), #' \code{"MinProbGlobal"} (a reimplementation of the MinProb algorithm -#' using a global mean value rather than sample-specific ones) are -#' supported. +#' using a global mean value rather than sample-specific ones), +#' \code{"MinProbGlobalOffset"} (like \code{"MinProbGlobal"}, but a +#' constant or relative offset can be specified to shift the mean of the +#' imputed values), and \code{"MinProbOffset"} (like +#' \code{"MinProbGlobalOffset"} but imputing separately for each row or +#' column) are supported. In addition, a custom function can be used by +#' setting \code{imputeMethod = "custom"} and providing the function in the +#' \code{FUN} argument (this will be passed on to +#' \code{MsCoreUtils::impute_matrix}). #' @param assayName Character scalar giving the name of the assay in \code{sce} #' to be imputed. The matrix should have missing values represented as #' \code{NA}. @@ -49,6 +157,29 @@ #' @param ... Additional arguments that will be passed on to the imputation #' function. #' +#' @details For the pre-defined imputation functions defined by einprot, +#' these are the available arguments: +#' \describe{ +#' \item{"MinProbGlobal":}{lowQuantile = 0.01, multSigma = 1 (similar to +#' \code{q} and \code{sigma} in +#' \code{MsCoreUtils::MsCoreUtils::impute_MinProb}).} +#' \item{"MinProbOffset":}{lowQuantile = 0.01, multSigma = 1, +#' margin = 2L, constOffset = NULL, relOffset = NULL. \code{margin} +#' indicates whether to impute along rows or columns. +#' \code{constOffset} defines a constant shift of the mean of the +#' imputed values relative to the low quantile. \code{relOffset} +#' instead defines the shift as a multiple of the standard deviation +#' of the distribution of imputed values.} +#' \item{"MinProbGlobalOffset":}{ lowQuantile = 0.01, multSigma = 1, +#' constOffset = NULL, relOffset = NULL. Similar interpretation as +#' for \code{"MinProbOffset"}.} +#' } +#' If \code{imputeMethod = "custom"}, any suitable function that accepts a +#' matrix as input and returns a matrix of the same shape can be provided to +#' the \code{FUN} argument. In addition, any argument to the function can +#' also be specified. The function and all arguments will be passed on to +#' \code{\link[MsCoreUtils]{impute_matrix}}. +#' #' @export #' @author Charlotte Soneson #' @@ -71,40 +202,71 @@ #' SummarizedExperiment::assay(sce, "log2_iBAQ"))] <- NA #' #' ## Impute missing values -#' sce <- doImputation(sce, method = "MinProb", assayName = "log2_iBAQ", +#' sce <- doImputation(sce, imputeMethod = "MinProb", assayName = "log2_iBAQ", #' imputedAssayName = "imputed_iBAQ") #' SummarizedExperiment::assayNames(sce) #' +#' ## Use another method from MsCoreUtils (specified as a custom function) +#' sce2 <- doImputation(sce, imputeMethod = "custom", assayName = "log2_iBAQ", +#' imputedAssayName = "imputed_iBAQ_knn", +#' FUN = MsCoreUtils::impute_knn) +#' ## Alternatively, just specify one of the methods supported by MsCoreUtils::impute_matrix +#' sce3 <- doImputation(sce, imputeMethod = "custom", assayName = "log2_iBAQ", +#' imputedAssayName = "imputed_iBAQ_knn", +#' method = "knn") +#' identical(SummarizedExperiment::assay(sce2, "imputed_iBAQ_knn"), +#' SummarizedExperiment::assay(sce3, "imputed_iBAQ_knn")) +#' #' @importFrom MsCoreUtils impute_matrix #' @importFrom rrcovNA impSeqRob #' @importFrom SummarizedExperiment assay assay<- #' @importFrom imputeLCMD impute.MinProb #' -doImputation <- function(sce, method, assayName, imputedAssayName, ...) { +doImputation <- function(sce, imputeMethod, assayName, imputedAssayName, ...) { .assertVector(x = sce, type = "SummarizedExperiment") - .assertScalar(x = method, type = "character", - validValues = c("MinProb", "impSeqRob", "MinProbGlobal")) + .assertScalar(x = imputeMethod, type = "character", + validValues = c("MinProb", "impSeqRob", "MinProbGlobal", + "MinProbOffset", "MinProbGlobalOffset", + "custom")) .assertScalar(x = assayName, type = "character", validValues = SummarizedExperiment::assayNames(sce)) .assertScalar(x = imputedAssayName, type = "character") - if (method == "MinProb") { + if (imputeMethod == "MinProb") { SummarizedExperiment::assay(sce, imputedAssayName) <- MsCoreUtils::impute_matrix( SummarizedExperiment::assay(sce, assayName), method = "MinProb", ... ) - } else if (method == "impSeqRob") { + } else if (imputeMethod == "impSeqRob") { tmp <- rrcovNA::impSeqRob( x = SummarizedExperiment::assay(sce, assayName), ... ) SummarizedExperiment::assay(sce, imputedAssayName) <- tmp$x - } else if (method == "MinProbGlobal") { + } else if (imputeMethod == "MinProbGlobal") { SummarizedExperiment::assay(sce, imputedAssayName) <- MsCoreUtils::impute_matrix( SummarizedExperiment::assay(sce, assayName), FUN = .minProbGlobalFun, ... ) + } else if (imputeMethod == "MinProbGlobalOffset") { + SummarizedExperiment::assay(sce, imputedAssayName) <- + MsCoreUtils::impute_matrix( + SummarizedExperiment::assay(sce, assayName), + FUN = .minProbGlobalOffsetFun, ... + ) + } else if (imputeMethod == "MinProbOffset") { + SummarizedExperiment::assay(sce, imputedAssayName) <- + MsCoreUtils::impute_matrix( + SummarizedExperiment::assay(sce, assayName), + FUN = .minProbOffsetFun, ... + ) + } else if (imputeMethod == "custom") { + SummarizedExperiment::assay(sce, imputedAssayName) <- + MsCoreUtils::impute_matrix( + SummarizedExperiment::assay(sce, assayName), + ... + ) } sce } diff --git a/man/doImputation.Rd b/man/doImputation.Rd index b46b110..a3e8066 100644 --- a/man/doImputation.Rd +++ b/man/doImputation.Rd @@ -4,17 +4,24 @@ \alias{doImputation} \title{Perform imputation of NA values} \usage{ -doImputation(sce, method, assayName, imputedAssayName, ...) +doImputation(sce, imputeMethod, assayName, imputedAssayName, ...) } \arguments{ \item{sce}{A \code{SummarizedExperiment} object (or a derivative).} -\item{method}{Character scalar giving the imputation method. Currently, +\item{imputeMethod}{Character scalar giving the imputation method. Currently, \code{"MinProb"} (provided in the \code{MsCoreUtils} package), -\code{"impSeqRob"} (provided in the \code{rrcovNA} package), and +\code{"impSeqRob"} (provided in the \code{rrcovNA} package), \code{"MinProbGlobal"} (a reimplementation of the MinProb algorithm -using a global mean value rather than sample-specific ones) are -supported.} +using a global mean value rather than sample-specific ones), +\code{"MinProbGlobalOffset"} (like \code{"MinProbGlobal"}, but a +constant or relative offset can be specified to shift the mean of the +imputed values), and \code{"MinProbOffset"} (like +\code{"MinProbGlobalOffset"} but imputing separately for each row or +column) are supported. In addition, a custom function can be used by +setting \code{imputeMethod = "custom"} and providing the function in the +\code{FUN} argument (this will be passed on to +\code{MsCoreUtils::impute_matrix}).} \item{assayName}{Character scalar giving the name of the assay in \code{sce} to be imputed. The matrix should have missing values represented as @@ -35,6 +42,30 @@ Perform imputation of missing values (represented by \code{NA}) in one assay in a \code{SummarizedExperiment}, and generate a new assay containing the complete data (including imputed values). } +\details{ +For the pre-defined imputation functions defined by einprot, +these are the available arguments: +\describe{ + \item{"MinProbGlobal":}{lowQuantile = 0.01, multSigma = 1 (similar to + \code{q} and \code{sigma} in + \code{MsCoreUtils::MsCoreUtils::impute_MinProb}).} + \item{"MinProbOffset":}{lowQuantile = 0.01, multSigma = 1, + margin = 2L, constOffset = NULL, relOffset = NULL. \code{margin} + indicates whether to impute along rows or columns. + \code{constOffset} defines a constant shift of the mean of the + imputed values relative to the low quantile. \code{relOffset} + instead defines the shift as a multiple of the standard deviation + of the distribution of imputed values.} + \item{"MinProbGlobalOffset":}{ lowQuantile = 0.01, multSigma = 1, + constOffset = NULL, relOffset = NULL. Similar interpretation as + for \code{"MinProbOffset"}.} +} +If \code{imputeMethod = "custom"}, any suitable function that accepts a +matrix as input and returns a matrix of the same shape can be provided to +the \code{FUN} argument. In addition, any argument to the function can +also be specified. The function and all arguments will be passed on to +\code{\link[MsCoreUtils]{impute_matrix}}. +} \examples{ ## Import data sce <- importExperiment(system.file("extdata", "mq_example", @@ -51,10 +82,21 @@ SummarizedExperiment::assay(sce, "log2_iBAQ")[!is.finite( SummarizedExperiment::assay(sce, "log2_iBAQ"))] <- NA ## Impute missing values -sce <- doImputation(sce, method = "MinProb", assayName = "log2_iBAQ", +sce <- doImputation(sce, imputeMethod = "MinProb", assayName = "log2_iBAQ", imputedAssayName = "imputed_iBAQ") SummarizedExperiment::assayNames(sce) +## Use another method from MsCoreUtils (specified as a custom function) +sce2 <- doImputation(sce, imputeMethod = "custom", assayName = "log2_iBAQ", + imputedAssayName = "imputed_iBAQ_knn", + FUN = MsCoreUtils::impute_knn) +## Alternatively, just specify one of the methods supported by MsCoreUtils::impute_matrix +sce3 <- doImputation(sce, imputeMethod = "custom", assayName = "log2_iBAQ", + imputedAssayName = "imputed_iBAQ_knn", + method = "knn") +identical(SummarizedExperiment::assay(sce2, "imputed_iBAQ_knn"), + SummarizedExperiment::assay(sce3, "imputed_iBAQ_knn")) + } \author{ Charlotte Soneson diff --git a/tests/testthat/test-doImputation.R b/tests/testthat/test-doImputation.R index 5212745..67910f2 100644 --- a/tests/testthat/test-doImputation.R +++ b/tests/testthat/test-doImputation.R @@ -50,51 +50,233 @@ test_that("minProbGlobalFun works", { expect_lte(abs(mean(imputed_values) - trueq), 0.06) }) +test_that("minProbGlobalOffsetFun works", { + set.seed(123L) + mat <- matrix(runif(70, min = 10, max = 15), nrow = 10) + mat0 <- mat + missingpos <- cbind(row = c(1, 1, 1, 4, 6, 9), col = c(2, 3, 7, 1, 6, 7)) + mat[missingpos] <- NA + expect_equal(sum(is.na(mat)), nrow(missingpos)) + for (i in seq_len(nrow(missingpos))) { + expect_equal(mat[missingpos[i, 1], missingpos[i, 2]], NA_real_) + } + + expect_error(.minProbGlobalOffsetFun(mat = 1, lowQuantile = 0.01, + multSigma = 1, constOffset = NULL, + relOffset = NULL), + "'mat' must be of class 'matrix'") + expect_error(.minProbGlobalOffsetFun(mat = data.frame(mat), lowQuantile = 0.01, + multSigma = 1, constOffset = NULL, + relOffset = NULL), + "'mat' must be of class 'matrix'") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = 1, + multSigma = 1, constOffset = NULL, + relOffset = NULL), + "'lowQuantile' must be within") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = "0.5", + multSigma = 1, constOffset = NULL, + relOffset = NULL), + "'lowQuantile' must be of class 'numeric'") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = c(0.2, 0.3), + multSigma = 1, constOffset = NULL, + relOffset = NULL), + "'lowQuantile' must have length 1") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = "1", constOffset = NULL, + relOffset = NULL), + "'multSigma' must be of class 'numeric'") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = c(1, 2), constOffset = NULL, + relOffset = NULL), + "'multSigma' must have length 1") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, constOffset = "1", + relOffset = NULL), + "'constOffset' must be of class 'numeric'") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, constOffset = c(1, 2), + relOffset = NULL), + "'constOffset' must have length 1") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, constOffset = NULL, + relOffset = "1"), + "'relOffset' must be of class 'numeric'") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, constOffset = NULL, + relOffset = c(1, 2)), + "'relOffset' must have length 1") + expect_error(.minProbGlobalOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, constOffset = -0.1, + relOffset = -0.5), + "At most one of constOffset and relOffset can be specified") + + set.seed(1L) + out <- .minProbGlobalOffsetFun(mat0, lowQuantile = 0.01, multSigma = 1, + constOffset = -0.5, relOffset = NULL) + expect_identical(out, mat0) + + set.seed(1L) + out <- .minProbGlobalOffsetFun(mat, lowQuantile = 0.01, multSigma = 1, + constOffset = NULL, relOffset = NULL) + trueq <- stats::quantile(mat[!is.na(mat)], 0.01) + expect_equal(mat[!is.na(mat)], out[!is.na(mat)]) + imputed_values <- out[missingpos] + expect_lte(abs(mean(imputed_values) - trueq), 0.06) + + set.seed(1L) + out <- .minProbGlobalOffsetFun(mat, lowQuantile = 0.01, multSigma = 1, + constOffset = -0.4, relOffset = NULL) + trueq <- stats::quantile(mat[!is.na(mat)], 0.01) - 0.4 + expect_equal(mat[!is.na(mat)], out[!is.na(mat)]) + imputed_values <- out[missingpos] + expect_lte(abs(mean(imputed_values) - trueq), 0.06) + + set.seed(1L) + out <- .minProbGlobalOffsetFun(mat, lowQuantile = 0.2, multSigma = 1, + constOffset = NULL, relOffset = -0.4) + trueq <- stats::quantile(mat[!is.na(mat)], 0.2) - 0.4 * 1.552274 + expect_equal(mat[!is.na(mat)], out[!is.na(mat)]) + imputed_values <- out[missingpos] + expect_lte(abs(mean(imputed_values) - trueq), 0.06) +}) + +test_that("minProbOffsetFun works", { + set.seed(123L) + mat <- matrix(runif(70, min = 10, max = 15), nrow = 10) + mat0 <- mat + missingpos <- cbind(row = c(1, 1, 1, 4, 6, 9), col = c(2, 3, 7, 1, 6, 7)) + mat[missingpos] <- NA + expect_equal(sum(is.na(mat)), nrow(missingpos)) + for (i in seq_len(nrow(missingpos))) { + expect_equal(mat[missingpos[i, 1], missingpos[i, 2]], NA_real_) + } + + expect_error(.minProbOffsetFun(mat = 1, lowQuantile = 0.01, + multSigma = 1, margin = 2L, constOffset = NULL, + relOffset = NULL), + "'mat' must be of class 'matrix'") + expect_error(.minProbOffsetFun(mat = data.frame(mat), lowQuantile = 0.01, + multSigma = 1, margin = 2L, constOffset = NULL, + relOffset = NULL), + "'mat' must be of class 'matrix'") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 1, + multSigma = 1, margin = 2L, constOffset = NULL, + relOffset = NULL), + "'lowQuantile' must be within") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = "0.5", + multSigma = 1, margin = 2L, constOffset = NULL, + relOffset = NULL), + "'lowQuantile' must be of class 'numeric'") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = c(0.2, 0.3), + multSigma = 1, margin = 2L, constOffset = NULL, + relOffset = NULL), + "'lowQuantile' must have length 1") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = "1", margin = 2L, constOffset = NULL, + relOffset = NULL), + "'multSigma' must be of class 'numeric'") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = c(1, 2), margin = 2L, constOffset = NULL, + relOffset = NULL), + "'multSigma' must have length 1") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, margin = "2", constOffset = NULL, + relOffset = NULL), + "'margin' must be of class 'numeric'") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, margin = 3L, constOffset = NULL, + relOffset = NULL), + "'margin' must be one of") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, margin = 2L, constOffset = "1", + relOffset = NULL), + "'constOffset' must be of class 'numeric'") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, margin = 2L, constOffset = c(1, 2), + relOffset = NULL), + "'constOffset' must have length 1") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, margin = 2L, constOffset = NULL, + relOffset = "1"), + "'relOffset' must be of class 'numeric'") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, margin = 2L, constOffset = NULL, + relOffset = c(1, 2)), + "'relOffset' must have length 1") + expect_error(.minProbOffsetFun(mat = mat, lowQuantile = 0.01, + multSigma = 1, margin = 2L, constOffset = -0.1, + relOffset = -0.5), + "At most one of constOffset and relOffset can be specified") + + set.seed(1L) + out <- .minProbOffsetFun(mat0, lowQuantile = 0.01, multSigma = 1, + margin = 2L, constOffset = NULL, relOffset = 0.2) + expect_identical(out, mat0) + + set.seed(1L) + out <- .minProbOffsetFun(mat, lowQuantile = 0.01, multSigma = 1, + margin = 2L, constOffset = NULL, relOffset = NULL) + set.seed(1L) + out2 <- MsCoreUtils::impute_MinProb(mat, q = 0.01, sigma = 1, MARGIN = 2L) + expect_equal(out, out2) + + set.seed(1L) + out <- .minProbOffsetFun(mat, lowQuantile = 0.01, multSigma = 1, + margin = 1L, constOffset = NULL, relOffset = NULL) + set.seed(1L) + out2 <- MsCoreUtils::impute_MinProb(mat, q = 0.01, sigma = 1, MARGIN = 1L) + expect_equal(out, out2) +}) + test_that("imputation works", { - expect_error(doImputation(sce = 1, method = "MinProb", assayName = "iBAQ", + expect_error(doImputation(sce = 1, imputeMethod = "MinProb", assayName = "iBAQ", imputedAssayName = "imputed"), "'sce' must be of class 'SummarizedExperiment'") - expect_error(doImputation(sce = sce_mq_preimputation, method = 1, + expect_error(doImputation(sce = sce_mq_preimputation, imputeMethod = 1, assayName = "iBAQ", imputedAssayName = "imputed"), - "'method' must be of class 'character'") + "'imputeMethod' must be of class 'character'") expect_error(doImputation(sce = sce_mq_preimputation, - method = c("impSeqRob", "MinProb"), + imputeMethod = c("impSeqRob", "MinProb"), assayName = "iBAQ", imputedAssayName = "imputed"), - "'method' must have length 1") + "'imputeMethod' must have length 1") expect_error(doImputation(sce = sce_mq_preimputation, - method = c("missing"), + imputeMethod = c("missing"), assayName = "iBAQ", imputedAssayName = "imputed"), - "All values in 'method' must be one of") - expect_error(doImputation(sce = sce_mq_preimputation, method = "MinProb", + "All values in 'imputeMethod' must be one of") + expect_error(doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProb", assayName = 1, imputedAssayName = "imputed"), "'assayName' must be of class 'character'") - expect_error(doImputation(sce = sce_mq_preimputation, method = "MinProb", + expect_error(doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProb", assayName = c("iBAQ", "iBAQ"), imputedAssayName = "imputed"), "'assayName' must have length 1") - expect_error(doImputation(sce = sce_mq_preimputation, method = "MinProb", + expect_error(doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProb", assayName = "missing", imputedAssayName = "imputed"), "All values in 'assayName' must be one of") - expect_error(doImputation(sce = sce_mq_preimputation, method = "MinProb", + expect_error(doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProb", assayName = "iBAQ", imputedAssayName = 1), "'imputedAssayName' must be of class 'character'") - expect_error(doImputation(sce = sce_mq_preimputation, method = "MinProb", + expect_error(doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProb", assayName = "iBAQ", imputedAssayName = c("imputed", "imputed")), "'imputedAssayName' must have length 1") + expect_error(doImputation(sce = sce_mq_preimputation, imputeMethod = "custom", + assayName = "iBAQ", + imputedAssayName = "imputed", FUN = "1"), + "Please specify an imputation method") expect_true(sum(is.na(SummarizedExperiment::assay(sce_mq_preimputation, "iBAQ"))) == 507) ## MinProb set.seed(42L) - impout <- doImputation(sce = sce_mq_preimputation, method = "MinProb", + impout <- doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProb", assayName = "log2_iBAQ_withNA", imputedAssayName = "imputedAssay") expect_s4_class(impout, "SummarizedExperiment") @@ -118,12 +300,121 @@ test_that("imputation works", { expect_lte(abs(mean(imputed_values) - stats::quantile(observed_values, 0.01)) / stats::quantile(observed_values, 0.01), 0.025) } + set.seed(42L) + impout2 <- doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProbOffset", + assayName = "log2_iBAQ_withNA", + imputedAssayName = "imputedAssay", + lowQuantile = 0.01, multSigma = 1, + margin = 2L, constOffset = NULL, relOffset = NULL) + expect_equal(SummarizedExperiment::assay(impout, "imputedAssay"), + SummarizedExperiment::assay(impout2, "imputedAssay")) + # with constant offset + set.seed(42L) + impout2 <- doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProbOffset", + assayName = "log2_iBAQ_withNA", + imputedAssayName = "imputedAssay", + lowQuantile = 0.01, multSigma = 1, + margin = 2L, constOffset = -0.5, relOffset = NULL) + expect_equal(SummarizedExperiment::assay(impout, "imputedAssay"), + SummarizedExperiment::assay(impout2, "imputedAssay") + + 0.5 * is.na(SummarizedExperiment::assay(impout2, "log2_iBAQ_withNA"))) + # with relative offset + set.seed(42L) + impout2 <- doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProbOffset", + assayName = "log2_iBAQ_withNA", + imputedAssayName = "imputedAssay", + lowQuantile = 0.01, multSigma = 1, + margin = 2L, constOffset = NULL, relOffset = -1) + expect_equal(SummarizedExperiment::assay(impout, "imputedAssay"), + SummarizedExperiment::assay(impout2, "imputedAssay") + + 1.159449 * is.na(SummarizedExperiment::assay(impout2, "log2_iBAQ_withNA")), + tolerance = 1e-5) + + ## MinProb, by row + # ... first make sure that there are no features with all NA values (removes 5 rows) + tmp <- sce_mq_preimputation[rowSums(!is.na(SummarizedExperiment::assay(sce_mq_preimputation))) > 0, ] + set.seed(42L) + impout <- doImputation(sce = tmp, imputeMethod = "MinProb", + assayName = "log2_iBAQ_withNA", MARGIN = 1L, + imputedAssayName = "imputedAssay", sigma = 0.25) + expect_s4_class(impout, "SummarizedExperiment") + expect_true("imputedAssay" %in% SummarizedExperiment::assayNames(impout)) + expect_true(sum(is.na(SummarizedExperiment::assay(impout, + "log2_iBAQ_withNA"))) == 507 - 5 * 9) + expect_true(all(!is.na(SummarizedExperiment::assay(impout, + "imputedAssay")))) + expect_equal(SummarizedExperiment::assay(impout, "log2_iBAQ_withNA"), + SummarizedExperiment::assay(tmp, "log2_iBAQ_withNA")) + expect_equal(SummarizedExperiment::assay(impout, "log2_iBAQ_withNA")[!is.na( + SummarizedExperiment::assay(impout, "log2_iBAQ_withNA"))], + SummarizedExperiment::assay(impout, "imputedAssay")[!is.na( + SummarizedExperiment::assay(impout, "log2_iBAQ_withNA"))]) + for (i in seq_len(145)) { + imputed_values <- SummarizedExperiment::assay(impout, "imputedAssay")[i, is.na( + SummarizedExperiment::assay(impout, "log2_iBAQ_withNA")[i, ])] + if (length(imputed_values) > 0) { + observed_values <- SummarizedExperiment::assay(impout, "log2_iBAQ_withNA")[i, !is.na( + SummarizedExperiment::assay(impout, "log2_iBAQ_withNA")[i, ])] + ## Relative error should be less than 10% + expect_lte(abs(mean(imputed_values) - stats::quantile(observed_values, 0.01)) / + stats::quantile(observed_values, 0.01), 0.1) + } + } + set.seed(42L) + impout2 <- doImputation(sce = tmp, imputeMethod = "MinProbOffset", + assayName = "log2_iBAQ_withNA", + imputedAssayName = "imputedAssay", + lowQuantile = 0.01, multSigma = 0.25, + margin = 1L, constOffset = NULL, relOffset = NULL) + expect_equal(SummarizedExperiment::assay(impout, "imputedAssay"), + SummarizedExperiment::assay(impout2, "imputedAssay")) + set.seed(42L) + impout3 <- doImputation(sce = tmp, imputeMethod = "custom", + assayName = "log2_iBAQ_withNA", + imputedAssayName = "imputedAssay", + FUN = einprot:::.minProbOffsetFun, + lowQuantile = 0.01, multSigma = 0.25, + margin = 1L, constOffset = NULL, relOffset = NULL) + expect_equal(SummarizedExperiment::assay(impout, "imputedAssay"), + SummarizedExperiment::assay(impout3, "imputedAssay")) ## MinProbGlobal set.seed(42L) - impout <- doImputation(sce = sce_mq_preimputation, method = "MinProbGlobal", + impout <- doImputation(sce = sce_mq_preimputation, imputeMethod = "MinProbGlobal", assayName = "log2_iBAQ_withNA", - imputedAssayName = "imputedAssay") + imputedAssayName = "imputedAssay", lowQuantile = 0.05) + expect_s4_class(impout, "SummarizedExperiment") + expect_true("imputedAssay" %in% SummarizedExperiment::assayNames(impout)) + expect_true(sum(is.na(SummarizedExperiment::assay(impout, + "log2_iBAQ_withNA"))) == 507) + expect_true(all(!is.na(SummarizedExperiment::assay(impout, + "imputedAssay")))) + expect_equal(SummarizedExperiment::assay(impout, "log2_iBAQ_withNA"), + SummarizedExperiment::assay(sce_mq_preimputation, "log2_iBAQ_withNA")) + expect_equal(SummarizedExperiment::assay(impout, "log2_iBAQ_withNA")[!is.na( + SummarizedExperiment::assay(impout, "log2_iBAQ_withNA"))], + SummarizedExperiment::assay(impout, "imputedAssay")[!is.na( + SummarizedExperiment::assay(impout, "log2_iBAQ_withNA"))]) + imputed_values <- SummarizedExperiment::assay(impout, "imputedAssay")[is.na( + SummarizedExperiment::assay(impout, "log2_iBAQ_withNA"))] + observed_values <- SummarizedExperiment::assay(impout, "log2_iBAQ_withNA")[!is.na( + SummarizedExperiment::assay(impout, "log2_iBAQ_withNA"))] + expect_lte(abs(mean(imputed_values) - stats::quantile(observed_values, 0.05)) / + stats::quantile(observed_values, 0.05), 0.01) + set.seed(42L) + impout2 <- doImputation(sce = sce_mq_preimputation, imputeMethod = "custom", + assayName = "log2_iBAQ_withNA", + imputedAssayName = "imputedAssay", + FUN = einprot:::.minProbGlobalFun, lowQuantile = 0.05) + expect_equal(SummarizedExperiment::assay(impout, "imputedAssay"), + SummarizedExperiment::assay(impout2, "imputedAssay")) + + ## MinProbGlobalOffset + set.seed(42L) + impout <- doImputation(sce = sce_mq_preimputation, + imputeMethod = "MinProbGlobalOffset", + assayName = "log2_iBAQ_withNA", + imputedAssayName = "imputedAssay", constOffset = -0.5) expect_s4_class(impout, "SummarizedExperiment") expect_true("imputedAssay" %in% SummarizedExperiment::assayNames(impout)) expect_true(sum(is.na(SummarizedExperiment::assay(impout, @@ -141,10 +432,19 @@ test_that("imputation works", { observed_values <- SummarizedExperiment::assay(impout, "log2_iBAQ_withNA")[!is.na( SummarizedExperiment::assay(impout, "log2_iBAQ_withNA"))] expect_lte(abs(mean(imputed_values) - stats::quantile(observed_values, 0.01)) / - stats::quantile(observed_values, 0.01), 0.01) + stats::quantile(observed_values, 0.01) - 0.5, 0.01) + set.seed(42L) + impout2 <- doImputation(sce = sce_mq_preimputation, imputeMethod = "custom", + assayName = "log2_iBAQ_withNA", + imputedAssayName = "imputedAssay", + FUN = einprot:::.minProbGlobalOffsetFun, + lowQuantile = 0.01, constOffset = -0.7) + expect_equal(SummarizedExperiment::assay(impout, "imputedAssay") - + 0.2 * is.na(SummarizedExperiment::assay(impout, "log2_iBAQ_withNA")), + SummarizedExperiment::assay(impout2, "imputedAssay")) ## impSeqRob - impout <- doImputation(sce = sce_mq_preimputation, method = "impSeqRob", + impout <- doImputation(sce = sce_mq_preimputation, imputeMethod = "impSeqRob", assayName = "iBAQ", imputedAssayName = "imputedAssay2") expect_s4_class(impout, "SummarizedExperiment") From a245cbb006a089cc244f3b8c9f55f574e40bcf69 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 28 Jun 2026 14:47:36 +0200 Subject: [PATCH 56/65] Expose additional arguments to doImputation in workflow functions --- NEWS.md | 5 +++++ R/checkArgumentsDIANN.R | 9 ++++++--- R/checkArgumentsFragPipe.R | 7 +++++-- R/checkArgumentsMaxQuant.R | 7 +++++-- R/checkArgumentsPDTMT.R | 9 ++++++--- R/checkArgumentsSE.R | 7 +++++-- R/checkArgumentsSpectronaut.R | 9 ++++++--- R/runDIANNAnalysis.R | 8 ++++---- R/runFragPipeAnalysis.R | 10 +++++----- R/runMaxQuantAnalysis.R | 15 +++++++++------ R/runPDTMTAnalysis.R | 10 +++++----- R/runSEAnalysis.R | 10 ++++++---- R/runSpectronautAnalysis.R | 10 +++++----- inst/extdata/process_basic_template.Rmd | 10 +++++++--- man/runDIANNAnalysis.Rd | 11 ++++++++--- man/runFragPipeAnalysis.Rd | 11 ++++++++--- man/runMaxQuantAnalysis.Rd | 11 ++++++++--- man/runPDTMTAnalysis.Rd | 11 ++++++++--- man/runSEAnalysis.Rd | 11 ++++++++--- man/runSpectronautAnalysis.Rd | 11 ++++++++--- tests/testthat/test-checkArgumentsDIANN.R | 7 +++++++ tests/testthat/test-checkArgumentsFragPipe.R | 7 +++++++ tests/testthat/test-checkArgumentsMaxQuant.R | 7 +++++++ tests/testthat/test-checkArgumentsPDTMT.R | 7 +++++++ tests/testthat/test-checkArgumentsSE.R | 7 +++++++ tests/testthat/test-checkArgumentsSpectronaut.R | 7 +++++++ tests/testthat/test-runDIANNAnalysis.R | 7 +++++++ tests/testthat/test-runFragPipeAnalysis.R | 7 +++++++ tests/testthat/test-runMaxQuantAnalysis.R | 7 +++++++ tests/testthat/test-runPDTMTAnalysis.R | 7 +++++++ tests/testthat/test-runSEAnalysis.R | 7 +++++++ tests/testthat/test-runSpectronautAnalysis.R | 7 +++++++ 32 files changed, 211 insertions(+), 65 deletions(-) diff --git a/NEWS.md b/NEWS.md index 8ab6d13..c08b9a7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,8 @@ +# einprot 0.10.2 + +* Expose additional arguments to doImputation() in workflow functions +* Rename 'method' argument of doImputation() to 'imputeMethod' + # einprot 0.10.1 * Limit the set of supported normalization methods in the workflows to make sure they are suitable for the input type diff --git a/R/checkArgumentsDIANN.R b/R/checkArgumentsDIANN.R index 0630e2f..3c5a6a8 100644 --- a/R/checkArgumentsDIANN.R +++ b/R/checkArgumentsDIANN.R @@ -9,8 +9,8 @@ forceOverwrite, experimentInfo, species, diannFile, diannFileType, outLevel, diannLogFile, aName, idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, sampleAnnot, includeOnlySamples, - excludeSamples, imputeMethod, filtersDF, filtersSE, assaysForExport, - addAbundanceValues, addHeatmaps, mergeGroups, + excludeSamples, imputeMethod, imputeArgs, filtersDF, filtersSE, + assaysForExport, addAbundanceValues, addHeatmaps, mergeGroups, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, minNbrValidValues, minlFC, samSignificance, nperm, volcanoAdjPvalThr, @@ -155,7 +155,10 @@ ## Method choices .assertScalar(x = imputeMethod, type = "character", - validValues = c("impSeqRob", "MinProb", "MinProbGlobal")) + validValues = c("impSeqRob", "MinProb", "MinProbGlobal", + "MinProbOffset", "MinProbGlobalOffset", + "custom")) + .assertVector(x = imputeArgs, type = "list") .assertVector(x = assaysForExport, type = "character", allowNULL = TRUE) .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") diff --git a/R/checkArgumentsFragPipe.R b/R/checkArgumentsFragPipe.R index 34ce745..10e3144 100755 --- a/R/checkArgumentsFragPipe.R +++ b/R/checkArgumentsFragPipe.R @@ -9,7 +9,7 @@ forceOverwrite, experimentInfo, species, fragpipeDir, idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, iColPattern, sampleAnnot, includeOnlySamples, excludeSamples, - imputeMethod, assaysForExport, addAbundanceValues, + imputeMethod, imputeArgs, assaysForExport, addAbundanceValues, addHeatmaps, mergeGroups, filtersSE, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, @@ -152,7 +152,10 @@ ## Method choices .assertScalar(x = imputeMethod, type = "character", - validValues = c("impSeqRob", "MinProb", "MinProbGlobal")) + validValues = c("impSeqRob", "MinProb", "MinProbGlobal", + "MinProbOffset", "MinProbGlobalOffset", + "custom")) + .assertVector(x = imputeArgs, type = "list") .assertVector(x = assaysForExport, type = "character", allowNULL = TRUE) .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") diff --git a/R/checkArgumentsMaxQuant.R b/R/checkArgumentsMaxQuant.R index c9742c8..7675ac0 100644 --- a/R/checkArgumentsMaxQuant.R +++ b/R/checkArgumentsMaxQuant.R @@ -9,7 +9,7 @@ forceOverwrite, experimentInfo, species, mqFile, mqParameterFile, idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, iColPattern, sampleAnnot, includeOnlySamples, excludeSamples, - imputeMethod, assaysForExport, addAbundanceValues, + imputeMethod, imputeArgs, assaysForExport, addAbundanceValues, addHeatmaps, mergeGroups, filtersSE, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, @@ -137,7 +137,10 @@ ## Method choices .assertScalar(x = imputeMethod, type = "character", - validValues = c("impSeqRob", "MinProb", "MinProbGlobal")) + validValues = c("impSeqRob", "MinProb", "MinProbGlobal", + "MinProbOffset", "MinProbGlobalOffset", + "custom")) + .assertVector(x = imputeArgs, type = "list") .assertVector(x = assaysForExport, type = "character", allowNULL = TRUE) .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") diff --git a/R/checkArgumentsPDTMT.R b/R/checkArgumentsPDTMT.R index b5d05d2..42318a8 100644 --- a/R/checkArgumentsPDTMT.R +++ b/R/checkArgumentsPDTMT.R @@ -11,8 +11,8 @@ stringIdCol, extraFeatureCols, filtersSE, modificationsCol, excludeUnmodifiedPeptides, keepModifications, iColPattern, sampleAnnot, includeOnlySamples, excludeSamples, - imputeMethod, assaysForExport, addAbundanceValues, addHeatmaps, mergeGroups, - comparisons, ctrlGroup, allPairwiseComparisons, singleFit, + imputeMethod, imputeArgs, assaysForExport, addAbundanceValues, addHeatmaps, + mergeGroups, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, minNbrValidValues, minlFC, samSignificance, nperm, volcanoAdjPvalThr, volcanoLog2FCThr, volcanoMaxFeatures, volcanoLabelSign, volcanoS0, @@ -153,7 +153,10 @@ ## Method choices .assertScalar(x = imputeMethod, type = "character", - validValues = c("impSeqRob", "MinProb", "MinProbGlobal")) + validValues = c("impSeqRob", "MinProb", "MinProbGlobal", + "MinProbOffset", "MinProbGlobalOffset", + "custom")) + .assertVector(x = imputeArgs, type = "list") .assertVector(x = assaysForExport, type = "character", allowNULL = TRUE) .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") diff --git a/R/checkArgumentsSE.R b/R/checkArgumentsSE.R index f962e83..9d87641 100644 --- a/R/checkArgumentsSE.R +++ b/R/checkArgumentsSE.R @@ -10,7 +10,7 @@ templateRmd, outputDir, outputBaseName, reportTitle, reportAuthor, forceOverwrite, experimentInfo, species, inFile, summaryInfo, aName, idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, - imputeMethod, assaysForExport, addAbundanceValues, + imputeMethod, imputeArgs, assaysForExport, addAbundanceValues, addHeatmaps, mergeGroups, filtersSE, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, @@ -119,7 +119,10 @@ ## Method choices .assertScalar(x = imputeMethod, type = "character", - validValues = c("impSeqRob", "MinProb", "MinProbGlobal")) + validValues = c("impSeqRob", "MinProb", "MinProbGlobal", + "MinProbOffset", "MinProbGlobalOffset", + "custom")) + .assertVector(x = imputeArgs, type = "list") .assertVector(x = assaysForExport, type = "character", allowNULL = TRUE) .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") diff --git a/R/checkArgumentsSpectronaut.R b/R/checkArgumentsSpectronaut.R index 2704e73..48d6115 100644 --- a/R/checkArgumentsSpectronaut.R +++ b/R/checkArgumentsSpectronaut.R @@ -10,8 +10,8 @@ spectronautFileType, outLevel, spectronautLogFile, aName, idCol, labelCol, geneIdCol, proteinIdCol, stringIdCol, extraFeatureCols, iColPattern, sampleAnnot, includeOnlySamples, - excludeSamples, imputeMethod, filtersDF, filtersSE, assaysForExport, - addAbundanceValues, addHeatmaps, mergeGroups, + excludeSamples, imputeMethod, imputeArgs, filtersDF, filtersSE, + assaysForExport, addAbundanceValues, addHeatmaps, mergeGroups, comparisons, ctrlGroup, allPairwiseComparisons, singleFit, subtractBaseline, baselineGroup, normMethod, spikeFeatures, stattest, minNbrValidValues, minlFC, samSignificance, nperm, volcanoAdjPvalThr, @@ -157,7 +157,10 @@ ## Method choices .assertScalar(x = imputeMethod, type = "character", - validValues = c("impSeqRob", "MinProb", "MinProbGlobal")) + validValues = c("impSeqRob", "MinProb", "MinProbGlobal", + "MinProbOffset", "MinProbGlobalOffset", + "custom")) + .assertVector(x = imputeArgs, type = "list") .assertVector(x = assaysForExport, type = "character", allowNULL = TRUE) .assertScalar(x = addAbundanceValues, type = "logical") .assertScalar(x = addHeatmaps, type = "logical") diff --git a/R/runDIANNAnalysis.R b/R/runDIANNAnalysis.R index 4361ad6..2af94c8 100644 --- a/R/runDIANNAnalysis.R +++ b/R/runDIANNAnalysis.R @@ -132,7 +132,7 @@ runDIANNAnalysis <- function( makeUnique = FALSE), extraFeatureCols = NULL, sampleAnnot, includeOnlySamples = "", excludeSamples = "", imputeMethod = "MinProb", - filtersDF = einprotDIANNFiltersDF, filtersSE = list(), + imputeArgs = list(), filtersDF = einprotDIANNFiltersDF, filtersSE = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, @@ -182,7 +182,7 @@ runDIANNAnalysis <- function( extraFeatureCols = extraFeatureCols, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, imputeMethod = imputeMethod, - filtersDF = filtersDF, filtersSE = filtersSE, + imputeArgs = imputeArgs, filtersDF = filtersDF, filtersSE = filtersSE, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, @@ -239,8 +239,8 @@ runDIANNAnalysis <- function( sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, imputeMethod = imputeMethod, - filtersDF = filtersDF, filtersSE = filtersSE, - assaysForExport = assaysForExport, + imputeArgs = imputeArgs, filtersDF = filtersDF, + filtersSE = filtersSE, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, diff --git a/R/runFragPipeAnalysis.R b/R/runFragPipeAnalysis.R index 4bb2f0b..0767643 100755 --- a/R/runFragPipeAnalysis.R +++ b/R/runFragPipeAnalysis.R @@ -76,7 +76,7 @@ runFragPipeAnalysis <- function( makeUnique = FALSE), extraFeatureCols = NULL, filtersSE = einprotFragPipeFilters, iColPattern, sampleAnnot, includeOnlySamples = "", excludeSamples = "", - imputeMethod = "MinProb", + imputeMethod = "MinProb", imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, @@ -125,7 +125,7 @@ runFragPipeAnalysis <- function( filtersSE = filtersSE, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, imputeMethod = imputeMethod, - assaysForExport = assaysForExport, + imputeArgs = imputeArgs, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, @@ -176,9 +176,9 @@ runFragPipeAnalysis <- function( reportAuthor = reportAuthor, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, imputeMethod = imputeMethod, - assaysForExport = assaysForExport, filtersSE = filtersSE, - addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, - mergeGroups = mergeGroups, + imputeArgs = imputeArgs, assaysForExport = assaysForExport, + filtersSE = filtersSE, addAbundanceValues = addAbundanceValues, + addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, allPairwiseComparisons = allPairwiseComparisons, singleFit = singleFit, diff --git a/R/runMaxQuantAnalysis.R b/R/runMaxQuantAnalysis.R index 3930080..506446e 100644 --- a/R/runMaxQuantAnalysis.R +++ b/R/runMaxQuantAnalysis.R @@ -72,9 +72,12 @@ #' sets of filtering functions for input data from different tools are #' provided (see \code{?defaultFiltersSE}). #' @param imputeMethod Character string defining the imputation method to use. -#' Currently, \code{"impSeqRob"}, \code{"MinProb"}, and -#' \code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for -#' more details about the methods. +#' Currently, \code{"impSeqRob"}, \code{"MinProb"}, +#' \code{"MinProbGlobal"}, \code{"MinProbOffset"} and +#' \code{"MinProbGlobalOffset"} are supported. +#' See \code{\link{doImputation}} for more details about the methods. +#' @param imputeArgs List with additional arguments to the imputation method. +#' Will be passed to \link{doImputation}. #' @param assaysForExport Character vector defining the name(s) of the assays #' to use for exported abundances and barplots. This could, for example, #' be set to an assay containing 'absolute' abundances, if available, even @@ -273,7 +276,7 @@ runMaxQuantAnalysis <- function( extraFeatureCols = NULL, filtersSE = einprotMQFilters, iColPattern, sampleAnnot, includeOnlySamples = "", excludeSamples = "", imputeMethod = "MinProb", - assaysForExport = c("iBAQ", "Top3"), + imputeArgs = list(), assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, @@ -323,7 +326,7 @@ runMaxQuantAnalysis <- function( iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, imputeMethod = imputeMethod, - assaysForExport = assaysForExport, + imputeArgs = imputeArgs, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, @@ -376,7 +379,7 @@ runMaxQuantAnalysis <- function( iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, imputeMethod = imputeMethod, - assaysForExport = assaysForExport, + imputeArgs = imputeArgs, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, diff --git a/R/runPDTMTAnalysis.R b/R/runPDTMTAnalysis.R index 8198d95..ee98f45 100644 --- a/R/runPDTMTAnalysis.R +++ b/R/runPDTMTAnalysis.R @@ -118,7 +118,7 @@ runPDTMTAnalysis <- function( modificationsCol = "Modifications", excludeUnmodifiedPeptides = FALSE, keepModifications = NULL, iColPattern, sampleAnnot, includeOnlySamples = "", excludeSamples = "", - imputeMethod = "MinProb", + imputeMethod = "MinProb", imputeArgs = imputeArgs, assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, @@ -172,8 +172,8 @@ runPDTMTAnalysis <- function( keepModifications = keepModifications, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, - imputeMethod = imputeMethod, assaysForExport = assaysForExport, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, + imputeArgs = imputeArgs, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, allPairwiseComparisons = allPairwiseComparisons, @@ -229,8 +229,8 @@ runPDTMTAnalysis <- function( reportTitle = reportTitle, reportAuthor = reportAuthor, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, - excludeSamples = excludeSamples, - imputeMethod = imputeMethod, assaysForExport = assaysForExport, + excludeSamples = excludeSamples, imputeMethod = imputeMethod, + imputeArgs = imputeArgs, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, diff --git a/R/runSEAnalysis.R b/R/runSEAnalysis.R index 0c6ce33..96d29f0 100644 --- a/R/runSEAnalysis.R +++ b/R/runSEAnalysis.R @@ -97,8 +97,8 @@ runSEAnalysis <- function( experimentInfo = list(), species, inFile, summaryInfo = NULL, aName, idCol, labelCol = idCol, geneIdCol = NULL, proteinIdCol = NULL, stringIdCol = NULL, - extraFeatureCols = NULL, filtersSE = list(), - imputeMethod = "MinProb", assaysForExport = c("iBAQ", "Top3"), + extraFeatureCols = NULL, filtersSE = list(), imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, @@ -145,7 +145,8 @@ runSEAnalysis <- function( idCol = idCol, labelCol = labelCol, geneIdCol = geneIdCol, proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, extraFeatureCols = extraFeatureCols, filtersSE = filtersSE, - imputeMethod = imputeMethod, assaysForExport = assaysForExport, + imputeMethod = imputeMethod, imputeArgs = imputeArgs, + assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, @@ -194,7 +195,8 @@ runSEAnalysis <- function( proteinIdCol = proteinIdCol, stringIdCol = stringIdCol, extraFeatureCols = extraFeatureCols, filtersSE = filtersSE, reportTitle = reportTitle, reportAuthor = reportAuthor, - imputeMethod = imputeMethod, assaysForExport = assaysForExport, + imputeMethod = imputeMethod, imputeArgs = imputeArgs, + assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, diff --git a/R/runSpectronautAnalysis.R b/R/runSpectronautAnalysis.R index 1ab276c..48e36cd 100644 --- a/R/runSpectronautAnalysis.R +++ b/R/runSpectronautAnalysis.R @@ -74,8 +74,8 @@ runSpectronautAnalysis <- function( makeUnique = FALSE), extraFeatureCols = NULL, iColPattern = ".PG.Quantity$", sampleAnnot, includeOnlySamples = "", excludeSamples = "", - imputeMethod = "MinProb", filtersDF = list(), filtersSE = list(), - assaysForExport = NULL, addAbundanceValues = TRUE, + imputeMethod = "MinProb", imputeArgs = list(), filtersDF = list(), + filtersSE = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, mergeGroups = list(), comparisons = list(), ctrlGroup = "", allPairwiseComparisons = TRUE, singleFit = TRUE, @@ -126,7 +126,7 @@ runSpectronautAnalysis <- function( extraFeatureCols = extraFeatureCols, iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, imputeMethod = imputeMethod, - filtersDF = filtersDF, filtersSE = filtersSE, + imputeArgs = imputeArgs, filtersDF = filtersDF, filtersSE = filtersSE, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, @@ -182,8 +182,8 @@ runSpectronautAnalysis <- function( iColPattern = iColPattern, sampleAnnot = sampleAnnot, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, imputeMethod = imputeMethod, - filtersDF = filtersDF, filtersSE = filtersSE, - assaysForExport = assaysForExport, + imputeArgs = imputeArgs, filtersDF = filtersDF, + filtersSE = filtersSE, assaysForExport = assaysForExport, addAbundanceValues = addAbundanceValues, addHeatmaps = addHeatmaps, mergeGroups = mergeGroups, comparisons = comparisons, ctrlGroup = ctrlGroup, diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index 28aaa89..408f4fc 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -186,6 +186,7 @@ running `einprot`. settingsList <- list( "Filter names (SE)" = paste(names(filtersSE), collapse = ", "), "Imputation method" = imputeMethod, + "Additional imputation arguments (names)" = names(imputeArgs), "Assays(s) to use for exported values" = paste(assaysForExport, collapse = ", "), "Add abundance values" = addAbundanceValues, "Show heatmaps" = addHeatmaps, @@ -678,9 +679,11 @@ non-imputed values in each sample. ```{r plot-imputation, message = FALSE, fig.width = min(14, max(7, 0.5 * ncol(sce))), fig.height = min(14, max(7, 0.5 * ncol(sce)))} set.seed(seed) -sce <- doImputation(sce, method = imputeMethod, - assayName = aNames$assayLog2NormWithNA, - imputedAssayName = aNames$assayImputed) +sce <- do.call(doImputation, + c(list(sce = sce, imputeMethod = imputeMethod, + assayName = aNames$assayLog2NormWithNA, + imputedAssayName = aNames$assayImputed), + imputeArgs)) plotImputationDistribution(sce, assayToPlot = aNames$assayImputed, assayImputation = aNames$assayImputIndic, xlab = aNames$assayImputed, plotType = "histogram") @@ -1234,6 +1237,7 @@ addmeta <- list( aNames = aNames, aName = aNames$assayInput, imputeMethod = imputeMethod, + imputeArgs = imputeArgs, ctrlGroup = ctrlGroup, allPairwiseComparisons = allPairwiseComparisons, normMethod = normMethod, diff --git a/man/runDIANNAnalysis.Rd b/man/runDIANNAnalysis.Rd index c949a8f..c4687b3 100644 --- a/man/runDIANNAnalysis.Rd +++ b/man/runDIANNAnalysis.Rd @@ -31,6 +31,7 @@ runDIANNAnalysis( includeOnlySamples = "", excludeSamples = "", imputeMethod = "MinProb", + imputeArgs = list(), filtersDF = einprotDIANNFiltersDF, filtersSE = list(), assaysForExport = NULL, @@ -161,9 +162,13 @@ input file, after removing the \code{iColPattern}.} samples to include or exclude from all analyses.} \item{imputeMethod}{Character string defining the imputation method to use. -Currently, \code{"impSeqRob"}, \code{"MinProb"}, and -\code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for -more details about the methods.} +Currently, \code{"impSeqRob"}, \code{"MinProb"}, +\code{"MinProbGlobal"}, \code{"MinProbOffset"} and +\code{"MinProbGlobalOffset"} are supported. +See \code{\link{doImputation}} for more details about the methods.} + +\item{imputeArgs}{List with additional arguments to the imputation method. +Will be passed to \link{doImputation}.} \item{filtersDF}{Named list where each element is a filtering function that takes a \code{data.frame} (or \code{tibble}) as diff --git a/man/runFragPipeAnalysis.Rd b/man/runFragPipeAnalysis.Rd index f130d35..7ddf151 100644 --- a/man/runFragPipeAnalysis.Rd +++ b/man/runFragPipeAnalysis.Rd @@ -27,6 +27,7 @@ runFragPipeAnalysis( includeOnlySamples = "", excludeSamples = "", imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -151,9 +152,13 @@ input file, after removing the \code{iColPattern}.} samples to include or exclude from all analyses.} \item{imputeMethod}{Character string defining the imputation method to use. -Currently, \code{"impSeqRob"}, \code{"MinProb"}, and -\code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for -more details about the methods.} +Currently, \code{"impSeqRob"}, \code{"MinProb"}, +\code{"MinProbGlobal"}, \code{"MinProbOffset"} and +\code{"MinProbGlobalOffset"} are supported. +See \code{\link{doImputation}} for more details about the methods.} + +\item{imputeArgs}{List with additional arguments to the imputation method. +Will be passed to \link{doImputation}.} \item{assaysForExport}{Character vector defining the name(s) of the assays to use for exported abundances and barplots. This could, for example, diff --git a/man/runMaxQuantAnalysis.Rd b/man/runMaxQuantAnalysis.Rd index bc479ab..535b2ff 100644 --- a/man/runMaxQuantAnalysis.Rd +++ b/man/runMaxQuantAnalysis.Rd @@ -30,6 +30,7 @@ runMaxQuantAnalysis( includeOnlySamples = "", excludeSamples = "", imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -158,9 +159,13 @@ input file, after removing the \code{iColPattern}.} samples to include or exclude from all analyses.} \item{imputeMethod}{Character string defining the imputation method to use. -Currently, \code{"impSeqRob"}, \code{"MinProb"}, and -\code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for -more details about the methods.} +Currently, \code{"impSeqRob"}, \code{"MinProb"}, +\code{"MinProbGlobal"}, \code{"MinProbOffset"} and +\code{"MinProbGlobalOffset"} are supported. +See \code{\link{doImputation}} for more details about the methods.} + +\item{imputeArgs}{List with additional arguments to the imputation method. +Will be passed to \link{doImputation}.} \item{assaysForExport}{Character vector defining the name(s) of the assays to use for exported abundances and barplots. This could, for example, diff --git a/man/runPDTMTAnalysis.Rd b/man/runPDTMTAnalysis.Rd index 4c74b89..a7baa6d 100644 --- a/man/runPDTMTAnalysis.Rd +++ b/man/runPDTMTAnalysis.Rd @@ -34,6 +34,7 @@ runPDTMTAnalysis( includeOnlySamples = "", excludeSamples = "", imputeMethod = "MinProb", + imputeArgs = imputeArgs, assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -188,9 +189,13 @@ input file, after removing the \code{iColPattern}.} samples to include or exclude from all analyses.} \item{imputeMethod}{Character string defining the imputation method to use. -Currently, \code{"impSeqRob"}, \code{"MinProb"}, and -\code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for -more details about the methods.} +Currently, \code{"impSeqRob"}, \code{"MinProb"}, +\code{"MinProbGlobal"}, \code{"MinProbOffset"} and +\code{"MinProbGlobalOffset"} are supported. +See \code{\link{doImputation}} for more details about the methods.} + +\item{imputeArgs}{List with additional arguments to the imputation method. +Will be passed to \link{doImputation}.} \item{assaysForExport}{Character vector defining the name(s) of the assays to use for exported abundances and barplots. This could, for example, diff --git a/man/runSEAnalysis.Rd b/man/runSEAnalysis.Rd index d31a770..e9c2bda 100644 --- a/man/runSEAnalysis.Rd +++ b/man/runSEAnalysis.Rd @@ -24,6 +24,7 @@ runSEAnalysis( extraFeatureCols = NULL, filtersSE = list(), imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -150,9 +151,13 @@ sets of filtering functions for input data from different tools are provided (see \code{?defaultFiltersSE}).} \item{imputeMethod}{Character string defining the imputation method to use. -Currently, \code{"impSeqRob"}, \code{"MinProb"}, and -\code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for -more details about the methods.} +Currently, \code{"impSeqRob"}, \code{"MinProb"}, +\code{"MinProbGlobal"}, \code{"MinProbOffset"} and +\code{"MinProbGlobalOffset"} are supported. +See \code{\link{doImputation}} for more details about the methods.} + +\item{imputeArgs}{List with additional arguments to the imputation method. +Will be passed to \link{doImputation}.} \item{assaysForExport}{Character vector defining the name(s) of the assays to use for exported abundances and barplots. This could, for example, diff --git a/man/runSpectronautAnalysis.Rd b/man/runSpectronautAnalysis.Rd index 2dd816b..c216f4f 100644 --- a/man/runSpectronautAnalysis.Rd +++ b/man/runSpectronautAnalysis.Rd @@ -30,6 +30,7 @@ runSpectronautAnalysis( includeOnlySamples = "", excludeSamples = "", imputeMethod = "MinProb", + imputeArgs = list(), filtersDF = list(), filtersSE = list(), assaysForExport = NULL, @@ -162,9 +163,13 @@ input file, after removing the \code{iColPattern}.} samples to include or exclude from all analyses.} \item{imputeMethod}{Character string defining the imputation method to use. -Currently, \code{"impSeqRob"}, \code{"MinProb"}, and -\code{"MinProbGlobal"} are supported. See \code{\link{doImputation}} for -more details about the methods.} +Currently, \code{"impSeqRob"}, \code{"MinProb"}, +\code{"MinProbGlobal"}, \code{"MinProbOffset"} and +\code{"MinProbGlobalOffset"} are supported. +See \code{\link{doImputation}} for more details about the methods.} + +\item{imputeArgs}{List with additional arguments to the imputation method. +Will be passed to \link{doImputation}.} \item{filtersSE}{A named \code{list}, where each element is a function that takes a \code{SummarizedExperiment} object as input and returns diff --git a/tests/testthat/test-checkArgumentsDIANN.R b/tests/testthat/test-checkArgumentsDIANN.R index 5575ba4..5689a26 100644 --- a/tests/testthat/test-checkArgumentsDIANN.R +++ b/tests/testthat/test-checkArgumentsDIANN.R @@ -37,6 +37,7 @@ test_that("argument checking for DIANN works", { filtersDF = list(), filtersSE = list(), imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -347,6 +348,12 @@ test_that("argument checking for DIANN works", { expect_error(do.call(.checkArgumentsDIANN, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(.checkArgumentsDIANN, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-checkArgumentsFragPipe.R b/tests/testthat/test-checkArgumentsFragPipe.R index db4bf76..e5b763f 100644 --- a/tests/testthat/test-checkArgumentsFragPipe.R +++ b/tests/testthat/test-checkArgumentsFragPipe.R @@ -33,6 +33,7 @@ test_that("argument checking for FP works", { excludeSamples = "", filtersSE = einprotFragPipeFilters, imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -326,6 +327,12 @@ test_that("argument checking for FP works", { expect_error(do.call(.checkArgumentsFragPipe, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(.checkArgumentsFragPipe, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-checkArgumentsMaxQuant.R b/tests/testthat/test-checkArgumentsMaxQuant.R index 83cde27..a63f853 100644 --- a/tests/testthat/test-checkArgumentsMaxQuant.R +++ b/tests/testthat/test-checkArgumentsMaxQuant.R @@ -41,6 +41,7 @@ test_that("argument checking for MQ works", { excludeSamples = "", filtersSE = einprotMQFilters, imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -325,6 +326,12 @@ test_that("argument checking for MQ works", { expect_error(do.call(.checkArgumentsMaxQuant, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(.checkArgumentsMaxQuant, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-checkArgumentsPDTMT.R b/tests/testthat/test-checkArgumentsPDTMT.R index e815c2f..344ddb0 100644 --- a/tests/testthat/test-checkArgumentsPDTMT.R +++ b/tests/testthat/test-checkArgumentsPDTMT.R @@ -47,6 +47,7 @@ test_that("argument checking for PD-TMT works", { excludeSamples = "", filtersSE = einprotPDTMTProteinFilters, imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -398,6 +399,12 @@ test_that("argument checking for PD-TMT works", { expect_error(do.call(.checkArgumentsPDTMT, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(.checkArgumentsPDTMT, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-checkArgumentsSE.R b/tests/testthat/test-checkArgumentsSE.R index fd9b626..ad3ec85 100644 --- a/tests/testthat/test-checkArgumentsSE.R +++ b/tests/testthat/test-checkArgumentsSE.R @@ -30,6 +30,7 @@ test_that("argument checking for SE works", { extraFeatureCols = NULL, filtersSE = einprotMQFilters, imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -263,6 +264,12 @@ test_that("argument checking for SE works", { expect_error(do.call(.checkArgumentsSE, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(.checkArgumentsSE, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-checkArgumentsSpectronaut.R b/tests/testthat/test-checkArgumentsSpectronaut.R index 965b321..0ae43d2 100644 --- a/tests/testthat/test-checkArgumentsSpectronaut.R +++ b/tests/testthat/test-checkArgumentsSpectronaut.R @@ -45,6 +45,7 @@ test_that("argument checking for Spectronaut works", { filtersDF = list(), filtersSE = list(), imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -366,6 +367,12 @@ test_that("argument checking for Spectronaut works", { expect_error(do.call(.checkArgumentsSpectronaut, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(.checkArgumentsSpectronaut, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-runDIANNAnalysis.R b/tests/testthat/test-runDIANNAnalysis.R index f127373..e50cacf 100644 --- a/tests/testthat/test-runDIANNAnalysis.R +++ b/tests/testthat/test-runDIANNAnalysis.R @@ -38,6 +38,7 @@ test_that("runDIANNAnalysis works", { filtersDF = list(), filtersSE = list(), imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -336,6 +337,12 @@ test_that("runDIANNAnalysis works", { expect_error(do.call(runDIANNAnalysis, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(runDIANNAnalysis, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-runFragPipeAnalysis.R b/tests/testthat/test-runFragPipeAnalysis.R index 3eec537..fd23caf 100644 --- a/tests/testthat/test-runFragPipeAnalysis.R +++ b/tests/testthat/test-runFragPipeAnalysis.R @@ -35,6 +35,7 @@ test_that("runFragPipeAnalysis works", { excludeSamples = "", filtersSE = einprotFragPipeFilters, imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -286,6 +287,12 @@ test_that("runFragPipeAnalysis works", { expect_error(do.call(runFragPipeAnalysis, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(runFragPipeAnalysis, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-runMaxQuantAnalysis.R b/tests/testthat/test-runMaxQuantAnalysis.R index 17070f0..9f77da7 100644 --- a/tests/testthat/test-runMaxQuantAnalysis.R +++ b/tests/testthat/test-runMaxQuantAnalysis.R @@ -39,6 +39,7 @@ test_that("runMaxQuantAnalysis works", { excludeSamples = "", filtersSE = einprotMQFilters, imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -311,6 +312,12 @@ test_that("runMaxQuantAnalysis works", { expect_error(do.call(runMaxQuantAnalysis, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(runMaxQuantAnalysis, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-runPDTMTAnalysis.R b/tests/testthat/test-runPDTMTAnalysis.R index 14b3407..34f98c9 100644 --- a/tests/testthat/test-runPDTMTAnalysis.R +++ b/tests/testthat/test-runPDTMTAnalysis.R @@ -46,6 +46,7 @@ test_that("runPDTMTAnalysis works", { excludeSamples = "", filtersSE = einprotPDTMTProteinFilters, imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -374,6 +375,12 @@ test_that("runPDTMTAnalysis works", { expect_error(do.call(runPDTMTAnalysis, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(runPDTMTAnalysis, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-runSEAnalysis.R b/tests/testthat/test-runSEAnalysis.R index 71fac77..c3511f7 100644 --- a/tests/testthat/test-runSEAnalysis.R +++ b/tests/testthat/test-runSEAnalysis.R @@ -30,6 +30,7 @@ test_that("runSEAnalysis works", { extraFeatureCols = NULL, filtersSE = einprotMQFilters, imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = c("iBAQ", "Top3"), addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -261,6 +262,12 @@ test_that("runSEAnalysis works", { expect_error(do.call(runSEAnalysis, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(runSEAnalysis, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 diff --git a/tests/testthat/test-runSpectronautAnalysis.R b/tests/testthat/test-runSpectronautAnalysis.R index 80e08d4..8f94820 100644 --- a/tests/testthat/test-runSpectronautAnalysis.R +++ b/tests/testthat/test-runSpectronautAnalysis.R @@ -46,6 +46,7 @@ test_that("runSpectronautAnalysis works", { filtersDF = list(), filtersSE = list(), imputeMethod = "MinProb", + imputeArgs = list(), assaysForExport = NULL, addAbundanceValues = TRUE, addHeatmaps = TRUE, @@ -349,6 +350,12 @@ test_that("runSpectronautAnalysis works", { expect_error(do.call(runSpectronautAnalysis, args), "All values in 'imputeMethod' must be one of") + ## imputeArgs + args <- args0 + args$imputeArgs <- 1 + expect_error(do.call(runSpectronautAnalysis, args), + "'imputeArgs' must be of class 'list'") + ## assaysForExport args <- args0 args$assaysForExport <- 1 From eaa79cf5cc171fb6937255ef917e73142ba023e0 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 28 Jun 2026 15:32:16 +0200 Subject: [PATCH 57/65] Add 'valueType' argument to plotFractionDetectedPerSample --- NEWS.md | 1 + R/plotMissingValues.R | 36 +++++++++++++++++---- inst/extdata/process_basic_template.Rmd | 3 +- man/plotDetectedInSamples.Rd | 11 +++++-- tests/testthat/test-plotMissingValues.R | 43 +++++++++++++++++++++++-- 5 files changed, 82 insertions(+), 12 deletions(-) diff --git a/NEWS.md b/NEWS.md index c08b9a7..4073105 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,7 @@ * Expose additional arguments to doImputation() in workflow functions * Rename 'method' argument of doImputation() to 'imputeMethod' +* Add 'valueType' argument to plotFractionDetectedPerSample() # einprot 0.10.1 diff --git a/R/plotMissingValues.R b/R/plotMissingValues.R index 50b40d4..9037e97 100644 --- a/R/plotMissingValues.R +++ b/R/plotMissingValues.R @@ -171,6 +171,11 @@ plotFractionDetectedPerSample <- function(dfNA, valueType = NULL) { #' @param dfNA A \code{DFrame} or \code{data.frame} with at least columns #' \code{"name"}, \code{"nNA"} and \code{"pNA"}, representing the feature #' name and the number and fraction of missing values. +#' @param valueType A \code{character} scalar indicating whether the values +#' in the \code{"pNA"} column represent fractions +#' (\code{valueType = "fraction"}, values in [0, 1]) or +#' percentages (\code{valueType = "percentage"}, values in [0, 100]). If +#' \code{NULL}, the type will be guessed from the observed values. #' #' @export #' @author Charlotte Soneson @@ -180,25 +185,44 @@ plotFractionDetectedPerSample <- function(dfNA, valueType = NULL) { #' @examples #' sce <- readRDS(system.file("extdata", "mq_example", "1356_sce.rds", #' package = "einprot")) -#' plotDetectedInSamples(SummarizedExperiment::rowData(sce)) +#' plotDetectedInSamples(SummarizedExperiment::rowData(sce), +#' valueType = "percentage") #' #' @importFrom ggplot2 ggplot aes geom_bar labs #' @importFrom rlang .data #' @importFrom dplyr filter %>% group_by tally pull mutate #' @importFrom methods is #' -plotDetectedInSamples <- function(dfNA) { +plotDetectedInSamples <- function(dfNA, valueType = NULL) { if (methods::is(dfNA, "DFrame")) { dfNA <- as.data.frame(dfNA) } .assertVector(x = dfNA, type = "data.frame") stopifnot(all(c("pNA", "nNA") %in% colnames(dfNA))) + .assertScalar(x = valueType, type = "character", + validValues = c("fraction", "percentage"), allowNULL = TRUE) + if (!is.null(valueType) && valueType == "fraction") { + .assertVector(x = dfNA$pNA, type = "numeric", rngIncl = c(0, 1)) + } + if (!is.null(valueType) && valueType == "percentage") { + .assertVector(x = dfNA$pNA, type = "numeric", rngIncl = c(0, 100)) + } - ## Guess whether pNA are proportions or percentages - if (all(dfNA$pNA <= 1)) { - multfact <- 1 + if (is.null(valueType)) { + ## Guess whether pNA are proportions or percentages + if (all(dfNA$pNA <= 1)) { + message("Inferred valueType: fraction") + multfact <- 1 + } else { + message("Inferred valueType: percentage") + multfact <- 100 + } } else { - multfact <- 100 + if (valueType == "percentage") { + multfact <- 100 + } else if (valueType == "fraction") { + multfact <- 1 + } } ## Get the total number of samples totN <- dfNA %>% diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index 408f4fc..2b1fa1d 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -620,7 +620,8 @@ We also plot the number of features that are detected in a given number of samples. ```{r missing-values-2, fig.width = min(14, max(7, 0.5 * ncol(sce))), fig.height = 5/7 * min(14, max(7, 0.5 * ncol(sce)))} -plotDetectedInSamples(dfNA = as.data.frame(rowData(sce)[, c("nNA", "pNA")])) +plotDetectedInSamples(dfNA = as.data.frame(rowData(sce)[, c("nNA", "pNA")]), + valueType = "percentage") ``` Finally we show the full missing value structure in the data matrix. The white diff --git a/man/plotDetectedInSamples.Rd b/man/plotDetectedInSamples.Rd index b386d85..b3c8b40 100644 --- a/man/plotDetectedInSamples.Rd +++ b/man/plotDetectedInSamples.Rd @@ -4,12 +4,18 @@ \alias{plotDetectedInSamples} \title{Plot feature detection rate} \usage{ -plotDetectedInSamples(dfNA) +plotDetectedInSamples(dfNA, valueType = NULL) } \arguments{ \item{dfNA}{A \code{DFrame} or \code{data.frame} with at least columns \code{"name"}, \code{"nNA"} and \code{"pNA"}, representing the feature name and the number and fraction of missing values.} + +\item{valueType}{A \code{character} scalar indicating whether the values +in the \code{"pNA"} column represent fractions +(\code{valueType = "fraction"}, values in [0, 1]) or +percentages (\code{valueType = "percentage"}, values in [0, 100]). If +\code{NULL}, the type will be guessed from the observed values.} } \value{ A \code{ggplot} object. @@ -20,7 +26,8 @@ Plot the distribution of the number of samples where features are detected. \examples{ sce <- readRDS(system.file("extdata", "mq_example", "1356_sce.rds", package = "einprot")) -plotDetectedInSamples(SummarizedExperiment::rowData(sce)) +plotDetectedInSamples(SummarizedExperiment::rowData(sce), + valueType = "percentage") } \author{ diff --git a/tests/testthat/test-plotMissingValues.R b/tests/testthat/test-plotMissingValues.R index 1876264..10c0cfc 100644 --- a/tests/testthat/test-plotMissingValues.R +++ b/tests/testthat/test-plotMissingValues.R @@ -176,8 +176,29 @@ test_that("missing value plots work", { dfNA = dfNA), 'all(c("pNA", "nNA") %in% colnames(dfNA)) is not TRUE', fixed = TRUE) + dfNA <- as.data.frame(nbr_na_mq$nNArows) %>% + dplyr::rename(sample = name) + expect_error(plotDetectedInSamples( + dfNA = dfNA, valueType = 1 + ), "'valueType' must be of class 'character'") + expect_error(plotDetectedInSamples( + dfNA = dfNA, valueType = c("fraction", "percentage") + ), "'valueType' must have length 1") + expect_error(plotDetectedInSamples( + dfNA = dfNA, valueType = "error" + ), "'valueType' must be one of") + dfNA$pNA <- 1 + dfNA$pNA + expect_error(plotDetectedInSamples( + dfNA = dfNA, valueType = "fraction" + ), "'$dfNApNA' must be within", fixed = TRUE) + dfNA$pNA <- 99 + dfNA$pNA + expect_error(plotDetectedInSamples( + dfNA = dfNA, valueType = "percentage" + ), "'$dfNApNA' must be within", fixed = TRUE) - out <- plotDetectedInSamples(dfNA = as.data.frame(nbr_na_mq$nNArows)) + expect_message( + out <- plotDetectedInSamples(dfNA = as.data.frame(nbr_na_mq$nNArows)), + 'Inferred valueType: fraction') expect_s3_class(out, "ggplot") expect_named(out$data, c("nNA", "n", "nObs")) for (i in c(0, seq_len(9))) { @@ -186,7 +207,8 @@ test_that("missing value plots work", { } expect_equal(levels(out$data$nObs), as.character(c(0, seq_len(9)))) - out <- plotDetectedInSamples(dfNA = DataFrame(nbr_na_mq$nNArows)) + out <- plotDetectedInSamples(dfNA = DataFrame(nbr_na_mq$nNArows), + valueType = "fraction") expect_s3_class(out, "ggplot") expect_named(out$data, c("nNA", "n", "nObs")) for (i in c(0, seq_len(9))) { @@ -196,7 +218,22 @@ test_that("missing value plots work", { expect_equal(levels(out$data$nObs), as.character(c(0, seq_len(9)))) ## PD data - out <- plotDetectedInSamples(dfNA = as.data.frame(nbr_na_pd$nNArows)) + expect_message( + out <- plotDetectedInSamples(dfNA = as.data.frame(nbr_na_pd$nNArows) |> + dplyr::mutate(pNA = 100 * pNA)), + "Inferred valueType: percentage") + expect_s3_class(out, "ggplot") + expect_named(out$data, c("nNA", "n", "nObs")) + for (i in c(0, seq_len(16))) { + expect_equal(sum(nbr_na_pd$nNArows$nNA == 16 - i), + out$data$n[out$data$nObs == i]) + } + expect_equal(levels(out$data$nObs), as.character(c(0, seq_len(16)))) + + ## PD data, specify valueType + out <- plotDetectedInSamples(dfNA = as.data.frame(nbr_na_pd$nNArows) |> + dplyr::mutate(pNA = 100 * pNA), + valueType = "percentage") expect_s3_class(out, "ggplot") expect_named(out$data, c("nNA", "n", "nObs")) for (i in c(0, seq_len(16))) { From 84777fb928763aa416b2b5de05ec5a51ec4fe2e2 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 28 Jun 2026 15:33:00 +0200 Subject: [PATCH 58/65] If normalizing based on shared features only, add boxplot of normalized intensities for shared features --- inst/extdata/process_basic_template.Rmd | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index 2b1fa1d..8a8931b 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -560,6 +560,17 @@ makeIntensityBoxplots(sce = sce, assayName = aNames$assayLog2NormWithNA, doLog = FALSE, ylab = aNames$assayLog2NormWithNA, maxNGroups = 25) +if (normMethod %in% c("center.mean.shared", "center.median.shared")) { + # plot distribution of shared features + sharedFeats <- which(rowSums(is.na(assay(sce, aNames$assayLog2NormWithNA))) == 0) + if (length(sharedFeats) > 0) { + makeIntensityBoxplots(sce = sce[sharedFeats, ], assayName = aNames$assayLog2NormWithNA, + doLog = FALSE, + ylab = paste0(aNames$assayLog2NormWithNA, " (", + length(sharedFeats), " shared features)"), + maxNGroups = 25) + } +} ``` ```{r text-filter-by-mod, results="asis", echo=FALSE, eval=(expType == "ProteomeDiscoverer" && inputLevel == "PeptideGroups")} From 3cc49a372a14a58814b83d30deb12b18290030a7 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Fri, 24 Jul 2026 10:02:19 +0200 Subject: [PATCH 59/65] Expand documentation --- R/makeAbundanceHeatmap.R | 10 ++++++++++ man/makeAbundanceHeatmap.Rd | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/R/makeAbundanceHeatmap.R b/R/makeAbundanceHeatmap.R index fcc2b26..fa5962c 100644 --- a/R/makeAbundanceHeatmap.R +++ b/R/makeAbundanceHeatmap.R @@ -10,6 +10,16 @@ #' #' Generate a heatmap from a defined assay. #' +#' @details +#' The predefined \code{settings} correspond to the following extra +#' arguments to \code{\link[ComplexHeatmap]{Heatmap}}: +#' \describe{ +#' \item{report: }{\code{show_row_names = FALSE}} +#' \item{export: }{\code{show_row_names = TRUE}, \code{show_row_dend = FALSE}, +#' \code{cluster_columns = TRUE}, \code{column_split = sce$group}, add +#' annotation for the fraction of NAs (imputed values) per feature.} +#' } +#' #' @export #' @author Charlotte Soneson #' diff --git a/man/makeAbundanceHeatmap.Rd b/man/makeAbundanceHeatmap.Rd index df4b73d..1e84e80 100644 --- a/man/makeAbundanceHeatmap.Rd +++ b/man/makeAbundanceHeatmap.Rd @@ -42,6 +42,16 @@ A ComplexHeatmap object. \description{ Generate a heatmap from a defined assay. } +\details{ +The predefined \code{settings} correspond to the following extra +arguments to \code{\link[ComplexHeatmap]{Heatmap}}: +\describe{ +\item{report: }{\code{show_row_names = FALSE}} +\item{export: }{\code{show_row_names = TRUE}, \code{show_row_dend = FALSE}, +\code{cluster_columns = TRUE}, \code{column_split = sce$group}, add +annotation for the fraction of NAs (imputed values) per feature.} +} +} \examples{ sce <- readRDS(system.file("extdata", "mq_example", "1356_sce.rds", package = "einprot")) From ab03f144ab896e0e7d3b689c14bd7a24f0ccac89 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 26 Jul 2026 19:24:24 +0200 Subject: [PATCH 60/65] Implement diff.mean.shared and diff.median.shared normalization options --- NEWS.md | 1 + R/checkArgumentsDIANN.R | 3 +- R/checkArgumentsFragPipe.R | 3 +- R/checkArgumentsMaxQuant.R | 3 +- R/checkArgumentsPDTMT.R | 3 +- R/checkArgumentsSE.R | 3 +- R/checkArgumentsSpectronaut.R | 3 +- R/doNormalization.R | 34 ++++++----- R/runMaxQuantAnalysis.R | 3 +- inst/extdata/process_basic_template.Rmd | 6 +- man/doNormalization.Rd | 6 +- man/runDIANNAnalysis.Rd | 3 +- man/runFragPipeAnalysis.Rd | 3 +- man/runMaxQuantAnalysis.Rd | 3 +- man/runPDTMTAnalysis.Rd | 3 +- man/runSEAnalysis.Rd | 3 +- man/runSpectronautAnalysis.Rd | 3 +- tests/testthat/test-doNormalization.R | 81 ++++++++++++++++++++++++- 18 files changed, 134 insertions(+), 33 deletions(-) diff --git a/NEWS.md b/NEWS.md index 4073105..1980168 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,6 +3,7 @@ * Expose additional arguments to doImputation() in workflow functions * Rename 'method' argument of doImputation() to 'imputeMethod' * Add 'valueType' argument to plotFractionDetectedPerSample() +* Add normalization methods 'diff.mean.shared' and 'diff.median.shared' # einprot 0.10.1 diff --git a/R/checkArgumentsDIANN.R b/R/checkArgumentsDIANN.R index 3c5a6a8..ffe48a6 100644 --- a/R/checkArgumentsDIANN.R +++ b/R/checkArgumentsDIANN.R @@ -165,7 +165,8 @@ .assertScalar(x = normMethod, type = "character", validValues = c("center.mean", "center.median", "diff.median", "vsn", "none", - "center.mean.shared", "center.median.shared")) + "center.mean.shared", "center.median.shared", + "diff.mean.shared", "diff.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", validValues = c("limma", "ttest", "proDA", "none")) diff --git a/R/checkArgumentsFragPipe.R b/R/checkArgumentsFragPipe.R index 10e3144..8a36666 100755 --- a/R/checkArgumentsFragPipe.R +++ b/R/checkArgumentsFragPipe.R @@ -162,7 +162,8 @@ .assertScalar(x = normMethod, type = "character", validValues = c("center.mean", "center.median", "diff.median", "vsn", "none", - "center.mean.shared", "center.median.shared")) + "center.mean.shared", "center.median.shared", + "diff.mean.shared", "diff.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", validValues = c("limma", "ttest", "proDA", "none")) diff --git a/R/checkArgumentsMaxQuant.R b/R/checkArgumentsMaxQuant.R index 7675ac0..71f83d1 100644 --- a/R/checkArgumentsMaxQuant.R +++ b/R/checkArgumentsMaxQuant.R @@ -147,7 +147,8 @@ .assertScalar(x = normMethod, type = "character", validValues = c("center.mean", "center.median", "diff.median", "vsn", "none", - "center.mean.shared", "center.median.shared")) + "center.mean.shared", "center.median.shared", + "diff.mean.shared", "diff.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", validValues = c("limma", "ttest", "proDA", "none")) diff --git a/R/checkArgumentsPDTMT.R b/R/checkArgumentsPDTMT.R index 42318a8..7507581 100644 --- a/R/checkArgumentsPDTMT.R +++ b/R/checkArgumentsPDTMT.R @@ -163,7 +163,8 @@ .assertScalar(x = normMethod, type = "character", validValues = c("center.mean", "center.median", "diff.median", "vsn", "none", - "center.mean.shared", "center.median.shared")) + "center.mean.shared", "center.median.shared", + "diff.mean.shared", "diff.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", validValues = c("limma", "ttest", "proDA", "none")) diff --git a/R/checkArgumentsSE.R b/R/checkArgumentsSE.R index 9d87641..4a574a3 100644 --- a/R/checkArgumentsSE.R +++ b/R/checkArgumentsSE.R @@ -129,7 +129,8 @@ .assertScalar(x = normMethod, type = "character", validValues = c("center.mean", "center.median", "diff.median", "vsn", "none", - "center.mean.shared", "center.median.shared")) + "center.mean.shared", "center.median.shared", + "diff.mean.shared", "diff.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", validValues = c("limma", "ttest", "proDA", "none")) diff --git a/R/checkArgumentsSpectronaut.R b/R/checkArgumentsSpectronaut.R index 48d6115..68f0002 100644 --- a/R/checkArgumentsSpectronaut.R +++ b/R/checkArgumentsSpectronaut.R @@ -167,7 +167,8 @@ .assertScalar(x = normMethod, type = "character", validValues = c("center.mean", "center.median", "diff.median", "vsn", "none", - "center.mean.shared", "center.median.shared")) + "center.mean.shared", "center.median.shared", + "diff.mean.shared", "diff.median.shared")) .assertVector(x = spikeFeatures, type = "character", allowNULL = TRUE) .assertScalar(x = stattest, type = "character", validValues = c("limma", "ttest", "proDA", "none")) diff --git a/R/doNormalization.R b/R/doNormalization.R index 1fcad55..73e4328 100644 --- a/R/doNormalization.R +++ b/R/doNormalization.R @@ -8,10 +8,12 @@ #' the methods from \code{MsCoreUtils::normalizeMethods()} are supported, #' together with "center.mean.shared" and "center.median.shared", #' subtracting the mean or median, respectively, across features that are -#' observed in all samples. +#' observed in all samples, and "diff.mean.shared" and "diff.median.shared", +#' which add back the grand mean or median across samples. #' If \code{spikeFeatures} is not \code{NULL}, only #' \code{"center.mean"}, \code{"center.median"}, \code{"div.mean"} and -#' \code{"div.median"} are supported. +#' \code{"div.median"} are supported. Note that in this case, the mean or +#' median of the spike features will be added/multiplied back. #' @param assayName Character scalar giving the name of the assay in \code{sce} #' to be normalized. #' @param normalizedAssayName Character scalar providing the name that will be @@ -60,8 +62,8 @@ doNormalization <- function(sce, method, assayName, normalizedAssayName, .assertVector(x = sce, type = "SummarizedExperiment") .assertScalar(x = method, type = "character", validValues = c(MsCoreUtils::normalizeMethods(), - "center.mean.shared", - "center.median.shared")) + "center.mean.shared", "center.median.shared", + "diff.mean.shared", "diff.median.shared")) .assertScalar(x = assayName, type = "character", validValues = SummarizedExperiment::assayNames(sce)) .assertScalar(x = normalizedAssayName, type = "character") @@ -99,22 +101,24 @@ doNormalization <- function(sce, method, assayName, normalizedAssayName, assayOut <- MsCoreUtils::normalize_matrix(assayIn, method = method) - } else if (method == "center.median.shared") { + } else if (method %in% c("center.median.shared", "center.mean.shared", + "diff.median.shared", "diff.mean.shared")) { idx <- which(rowSums(is.na(assayIn)) == 0) if (length(idx) == 0) { stop("No features observed in all samples") } - assayOut <- sweep(assayIn, MARGIN = 2, - STATS = apply(assayIn[idx, , drop = FALSE], 2, stats::median), - FUN = "-") - } else if (method == "center.mean.shared") { - idx <- which(rowSums(is.na(assayIn)) == 0) - if (length(idx) == 0) { - stop("No features observed in all samples") + if (method == "center.median.shared") { + stats <- apply(assayIn[idx, , drop = FALSE], 2, stats::median) + } else if (method == "center.mean.shared") { + stats <- apply(assayIn[idx, , drop = FALSE], 2, mean) + } else if (method == "diff.median.shared") { + medians <- apply(assayIn[idx, , drop = FALSE], 2, stats::median) + stats <- medians - stats::median(medians) + } else if (method == "diff.mean.shared") { + means <- apply(assayIn[idx, , drop = FALSE], 2, mean) + stats <- means - mean(means) } - assayOut <- sweep(assayIn, MARGIN = 2, - STATS = apply(assayIn[idx, , drop = FALSE], 2, mean), - FUN = "-") + assayOut <- sweep(assayIn, MARGIN = 2, STATS = stats, FUN = "-") } else { ## Should never end up here as we check the validity of method above #nocov start diff --git a/R/runMaxQuantAnalysis.R b/R/runMaxQuantAnalysis.R index 506446e..f90744e 100644 --- a/R/runMaxQuantAnalysis.R +++ b/R/runMaxQuantAnalysis.R @@ -119,7 +119,8 @@ #' @param normMethod Character scalar indicating the normalization method to #' use. Currently, \code{"center.mean"}, \code{"center.median"}, #' \code{"diff.median"}, \code{"center.mean.shared"}, -#' \code{"center.median.shared"} (applied on the log-transformed values), +#' \code{"center.median.shared"}, \code{"diff.mean.shared"}, +#' \code{"diff.median.shared"} (applied on the log-transformed values), #' \code{"vsn"} (applied on the natural-scale values), #' and \code{"none"}, are valid values. #' @param spikeFeatures Character vector indicating the 'spike-in' features diff --git a/inst/extdata/process_basic_template.Rmd b/inst/extdata/process_basic_template.Rmd index 8a8931b..c6a63cc 100644 --- a/inst/extdata/process_basic_template.Rmd +++ b/inst/extdata/process_basic_template.Rmd @@ -548,7 +548,8 @@ if (normMethod == "vsn") { normalizedAssayName = aNames$assayLog2NormWithNA, spikeFeatures = spikeFeatures) } else { - # center.median, center.mean, diff.median, center.mean.shared, center.median.shared + # center.median, center.mean, diff.median, center.mean.shared, + # center.median.shared, diff.mean.shared, diff.median.shared # log-scale input -> log-scale output sce <- doNormalization(sce, method = normMethod, assayName = aNames$assayLog2WithNA, @@ -560,7 +561,8 @@ makeIntensityBoxplots(sce = sce, assayName = aNames$assayLog2NormWithNA, doLog = FALSE, ylab = aNames$assayLog2NormWithNA, maxNGroups = 25) -if (normMethod %in% c("center.mean.shared", "center.median.shared")) { +if (normMethod %in% c("center.mean.shared", "center.median.shared", + "diff.mean.shared", "diff.median.shared")) { # plot distribution of shared features sharedFeats <- which(rowSums(is.na(assay(sce, aNames$assayLog2NormWithNA))) == 0) if (length(sharedFeats) > 0) { diff --git a/man/doNormalization.Rd b/man/doNormalization.Rd index 9675eca..6a83225 100644 --- a/man/doNormalization.Rd +++ b/man/doNormalization.Rd @@ -19,10 +19,12 @@ doNormalization( the methods from \code{MsCoreUtils::normalizeMethods()} are supported, together with "center.mean.shared" and "center.median.shared", subtracting the mean or median, respectively, across features that are -observed in all samples. +observed in all samples, and "diff.mean.shared" and "diff.median.shared", +which add back the grand mean or median across samples. If \code{spikeFeatures} is not \code{NULL}, only \code{"center.mean"}, \code{"center.median"}, \code{"div.mean"} and -\code{"div.median"} are supported.} +\code{"div.median"} are supported. Note that in this case, the mean or +median of the spike features will be added/multiplied back.} \item{assayName}{Character scalar giving the name of the assay in \code{sce} to be normalized.} diff --git a/man/runDIANNAnalysis.Rd b/man/runDIANNAnalysis.Rd index c4687b3..1617d83 100644 --- a/man/runDIANNAnalysis.Rd +++ b/man/runDIANNAnalysis.Rd @@ -236,7 +236,8 @@ same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to use. Currently, \code{"center.mean"}, \code{"center.median"}, \code{"diff.median"}, \code{"center.mean.shared"}, -\code{"center.median.shared"} (applied on the log-transformed values), +\code{"center.median.shared"}, \code{"diff.mean.shared"}, +\code{"diff.median.shared"} (applied on the log-transformed values), \code{"vsn"} (applied on the natural-scale values), and \code{"none"}, are valid values.} diff --git a/man/runFragPipeAnalysis.Rd b/man/runFragPipeAnalysis.Rd index 7ddf151..eef0459 100644 --- a/man/runFragPipeAnalysis.Rd +++ b/man/runFragPipeAnalysis.Rd @@ -211,7 +211,8 @@ same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to use. Currently, \code{"center.mean"}, \code{"center.median"}, \code{"diff.median"}, \code{"center.mean.shared"}, -\code{"center.median.shared"} (applied on the log-transformed values), +\code{"center.median.shared"}, \code{"diff.mean.shared"}, +\code{"diff.median.shared"} (applied on the log-transformed values), \code{"vsn"} (applied on the natural-scale values), and \code{"none"}, are valid values.} diff --git a/man/runMaxQuantAnalysis.Rd b/man/runMaxQuantAnalysis.Rd index 535b2ff..4f919a5 100644 --- a/man/runMaxQuantAnalysis.Rd +++ b/man/runMaxQuantAnalysis.Rd @@ -218,7 +218,8 @@ same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to use. Currently, \code{"center.mean"}, \code{"center.median"}, \code{"diff.median"}, \code{"center.mean.shared"}, -\code{"center.median.shared"} (applied on the log-transformed values), +\code{"center.median.shared"}, \code{"diff.mean.shared"}, +\code{"diff.median.shared"} (applied on the log-transformed values), \code{"vsn"} (applied on the natural-scale values), and \code{"none"}, are valid values.} diff --git a/man/runPDTMTAnalysis.Rd b/man/runPDTMTAnalysis.Rd index a7baa6d..5003190 100644 --- a/man/runPDTMTAnalysis.Rd +++ b/man/runPDTMTAnalysis.Rd @@ -248,7 +248,8 @@ same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to use. Currently, \code{"center.mean"}, \code{"center.median"}, \code{"diff.median"}, \code{"center.mean.shared"}, -\code{"center.median.shared"} (applied on the log-transformed values), +\code{"center.median.shared"}, \code{"diff.mean.shared"}, +\code{"diff.median.shared"} (applied on the log-transformed values), \code{"vsn"} (applied on the natural-scale values), and \code{"none"}, are valid values.} diff --git a/man/runSEAnalysis.Rd b/man/runSEAnalysis.Rd index e9c2bda..3a213a4 100644 --- a/man/runSEAnalysis.Rd +++ b/man/runSEAnalysis.Rd @@ -210,7 +210,8 @@ same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to use. Currently, \code{"center.mean"}, \code{"center.median"}, \code{"diff.median"}, \code{"center.mean.shared"}, -\code{"center.median.shared"} (applied on the log-transformed values), +\code{"center.median.shared"}, \code{"diff.mean.shared"}, +\code{"diff.median.shared"} (applied on the log-transformed values), \code{"vsn"} (applied on the natural-scale values), and \code{"none"}, are valid values.} diff --git a/man/runSpectronautAnalysis.Rd b/man/runSpectronautAnalysis.Rd index c216f4f..b71fdd8 100644 --- a/man/runSpectronautAnalysis.Rd +++ b/man/runSpectronautAnalysis.Rd @@ -229,7 +229,8 @@ same batch as the original sample.} \item{normMethod}{Character scalar indicating the normalization method to use. Currently, \code{"center.mean"}, \code{"center.median"}, \code{"diff.median"}, \code{"center.mean.shared"}, -\code{"center.median.shared"} (applied on the log-transformed values), +\code{"center.median.shared"}, \code{"diff.mean.shared"}, +\code{"diff.median.shared"} (applied on the log-transformed values), \code{"vsn"} (applied on the natural-scale values), and \code{"none"}, are valid values.} diff --git a/tests/testthat/test-doNormalization.R b/tests/testthat/test-doNormalization.R index 49d011d..eba6464 100644 --- a/tests/testthat/test-doNormalization.R +++ b/tests/testthat/test-doNormalization.R @@ -16,7 +16,7 @@ test_that("normalization works", { spikeFeatures = NULL), "'method' must have length 1") expect_error(doNormalization(sce = sce_mq_preimputation, - method = c("missing"), + method = "missing", assayName = "iBAQ", normalizedAssayName = "normalized", spikeFeatures = NULL), @@ -77,6 +77,23 @@ test_that("normalization works", { na.rm = TRUE), rep(0L, ncol(normout)), ignore_attr = TRUE) + ## diff.median + normout <- doNormalization(sce = sce_mq_preimputation, + method = "diff.median", + assayName = "iBAQ", + normalizedAssayName = "normalizedAssay", + spikeFeatures = NULL) + expect_s4_class(normout, "SummarizedExperiment") + expect_true("normalizedAssay" %in% SummarizedExperiment::assayNames(normout)) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "iBAQ"))) == 507) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "normalizedAssay"))) == 507) + expect_equal(apply(assay(normout, "normalizedAssay"), 2, stats::median, + na.rm = TRUE), + rep(stats::median(assay(normout, "iBAQ"), na.rm = TRUE), ncol(normout)), + ignore_attr = TRUE) + ## center.median.shared normout <- doNormalization(sce = sce_mq_preimputation, method = "center.median.shared", @@ -94,6 +111,25 @@ test_that("normalization works", { na.rm = TRUE), rep(0L, ncol(normout)), ignore_attr = TRUE) + ## diff.median.shared + normout <- doNormalization(sce = sce_mq_preimputation, + method = "diff.median.shared", + assayName = "iBAQ", + normalizedAssayName = "normalizedAssay", + spikeFeatures = NULL) + expect_s4_class(normout, "SummarizedExperiment") + expect_true("normalizedAssay" %in% SummarizedExperiment::assayNames(normout)) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "iBAQ"))) == 507) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "normalizedAssay"))) == 507) + shared <- which(rowSums(is.na(SummarizedExperiment::assay(normout, "iBAQ"))) == 0) + expect_equal(apply(assay(normout, "normalizedAssay")[shared, ], 2, stats::median, + na.rm = TRUE), + rep(stats::median(apply(assay(normout, "iBAQ")[shared, ], 2, stats::median, + na.rm = TRUE)), ncol(normout)), + ignore_attr = TRUE) + ## center.median.shared, no shared features notshared <- which(rowSums(is.na(SummarizedExperiment::assay(sce_mq_preimputation, "iBAQ"))) != 0) expect_error(doNormalization(sce = sce_mq_preimputation[notshared, ], @@ -136,6 +172,25 @@ test_that("normalization works", { na.rm = TRUE), rep(0L, ncol(normout)), ignore_attr = TRUE) + ## diff.mean.shared + normout <- doNormalization(sce = sce_mq_preimputation, + method = "diff.mean.shared", + assayName = "iBAQ", + normalizedAssayName = "normalizedAssay", + spikeFeatures = NULL) + expect_s4_class(normout, "SummarizedExperiment") + expect_true("normalizedAssay" %in% SummarizedExperiment::assayNames(normout)) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "iBAQ"))) == 507) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "normalizedAssay"))) == 507) + shared <- which(rowSums(is.na(SummarizedExperiment::assay(normout, "iBAQ"))) == 0) + expect_equal(apply(assay(normout, "normalizedAssay")[shared, ], 2, mean, + na.rm = TRUE), + rep(mean(apply(assay(normout, "iBAQ")[shared, ], 2, mean, + na.rm = TRUE)), ncol(normout)), + ignore_attr = TRUE) + ## center.mean.shared, no shared features notshared <- which(rowSums(is.na(SummarizedExperiment::assay(sce_mq_preimputation, "iBAQ"))) != 0) expect_error(doNormalization(sce = sce_mq_preimputation[notshared, ], @@ -221,6 +276,30 @@ test_that("normalization works", { na.rm = TRUE) expect_true(max(spikeMeans) - min(spikeMeans) < 1e-8) + ## spike-in features, center.mean, all features + spikes <- rownames(sce_mq_preimputation) + expect_message({ + normout <- doNormalization(sce = sce_mq_preimputation, + method = "center.mean", + assayName = "iBAQ", + normalizedAssayName = "normalizedAssay", + spikeFeatures = spikes) + }, "47/150 spike features found with no missing values") + expect_s4_class(normout, "SummarizedExperiment") + expect_true("normalizedAssay" %in% SummarizedExperiment::assayNames(normout)) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "iBAQ"))) == 507) + expect_true(sum(is.na(SummarizedExperiment::assay(normout, + "normalizedAssay"))) == 507) + # should give the same results as diff.mean.shared normalization (use all + # shared features, add back global mean) + normout2 <- doNormalization(sce = sce_mq_preimputation, + method = "diff.mean.shared", + assayName = "iBAQ", + normalizedAssayName = "normalizedAssay") + expect_equal(assay(normout, "normalizedAssay"), + assay(normout2, "normalizedAssay")) + ## spike-in features, no features without missing values spikes <- rownames(sce_mq_preimputation)[c(1, 2, 124)] expect_warning({ From 2c97869ea2a7bcf57438b4c99e20dbca24f32900 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 2 Aug 2026 17:26:07 +0200 Subject: [PATCH 61/65] Fix typo, rerender documentation --- R/doFilter.R | 2 +- R/runSpectronautAnalysis.R | 7 +++++++ man/filterFeaturesSE.Rd | 2 +- man/runSpectronautAnalysis.Rd | 8 ++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/R/doFilter.R b/R/doFilter.R index 56755b4..15411c3 100644 --- a/R/doFilter.R +++ b/R/doFilter.R @@ -60,7 +60,7 @@ #' iColPattern = "^LFQ.intensity.")$sce #' #' dim(sce) -#' sce <- filterFeaturesSE(sce = sce, filtersSE = einprotMQfilters, +#' sce <- filterFeaturesSE(sce = sce, filtersSE = einprotMQFilters, #' plotUpset = TRUE) #' dim(sce) #' diff --git a/R/runSpectronautAnalysis.R b/R/runSpectronautAnalysis.R index 48e36cd..987bffc 100644 --- a/R/runSpectronautAnalysis.R +++ b/R/runSpectronautAnalysis.R @@ -15,6 +15,13 @@ #' or \code{"long_format"}. #' @param outLevel Character string indicating the desired output level. #' Currently only \code{"pg"} is supported. +#' @param filtersDF Named list where each element is a filtering function +#' that takes a \code{data.frame} (or \code{tibble}) as +#' input and returns a filtered object of the same class. No other +#' arguments are allowed. The filtering functions will be applied in the +#' order they are provided in the list, after reading the long-format text +#' file, and before creating the wide-format assay. Only +#' applies if \code{spectronautFileType} is \code{"long_format"}. #' @param spectronautLogFile Character string pointing to the Spectronaut #' setup.txt log file. File paths will be expressed in canonical form (using #' \code{normalizePath()}) before they are processed. diff --git a/man/filterFeaturesSE.Rd b/man/filterFeaturesSE.Rd index 452a000..036dd5e 100644 --- a/man/filterFeaturesSE.Rd +++ b/man/filterFeaturesSE.Rd @@ -39,7 +39,7 @@ sce <- importExperiment(inFile = system.file("extdata", "mq_example", iColPattern = "^LFQ.intensity.")$sce dim(sce) -sce <- filterFeaturesSE(sce = sce, filtersSE = einprotMQfilters, +sce <- filterFeaturesSE(sce = sce, filtersSE = einprotMQFilters, plotUpset = TRUE) dim(sce) diff --git a/man/runSpectronautAnalysis.Rd b/man/runSpectronautAnalysis.Rd index b71fdd8..6f51d72 100644 --- a/man/runSpectronautAnalysis.Rd +++ b/man/runSpectronautAnalysis.Rd @@ -171,6 +171,14 @@ See \code{\link{doImputation}} for more details about the methods.} \item{imputeArgs}{List with additional arguments to the imputation method. Will be passed to \link{doImputation}.} +\item{filtersDF}{Named list where each element is a filtering function +that takes a \code{data.frame} (or \code{tibble}) as +input and returns a filtered object of the same class. No other +arguments are allowed. The filtering functions will be applied in the +order they are provided in the list, after reading the long-format text +file, and before creating the wide-format assay. Only +applies if \code{spectronautFileType} is \code{"long_format"}.} + \item{filtersSE}{A named \code{list}, where each element is a function that takes a \code{SummarizedExperiment} object as input and returns a logical vector of the same length as the number of rows in \code{sce}, From e7ed72ba0dc1a8ed5bad3d3fe0d7b89ec259d71d Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 2 Aug 2026 17:26:41 +0200 Subject: [PATCH 62/65] Update docker image in GHA --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 66f8a06..95b2845 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -22,7 +22,7 @@ jobs: config: - { os: macOS-latest, bioc: 'release', curlConfigPath: '/usr/bin/'} - { os: windows-latest, bioc: 'release'} - - { os: ubuntu-latest, image: "bioconductor/bioconductor_docker:RELEASE_3_20", cran: "https://demo.rstudiopm.com/all/__linux__/xenial/latest"} + - { os: ubuntu-latest, image: "bioconductor/bioconductor_docker:RELEASE_3_23", cran: "https://demo.rstudiopm.com/all/__linux__/xenial/latest"} env: R_REMOTES_NO_ERRORS_FROM_WARNINGS: true From 5806228ea1239adca067a9aa9456612a16cf83ef Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 2 Aug 2026 19:03:29 +0200 Subject: [PATCH 63/65] Increase tolerance in unit test to accommodate arm differences --- tests/testthat/test-doNormalization.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testthat/test-doNormalization.R b/tests/testthat/test-doNormalization.R index eba6464..64d6918 100644 --- a/tests/testthat/test-doNormalization.R +++ b/tests/testthat/test-doNormalization.R @@ -170,7 +170,7 @@ test_that("normalization works", { shared <- which(rowSums(is.na(SummarizedExperiment::assay(normout, "iBAQ"))) == 0) expect_equal(apply(assay(normout, "normalizedAssay")[shared, ], 2, mean, na.rm = TRUE), rep(0L, ncol(normout)), - ignore_attr = TRUE) + ignore_attr = TRUE, tolerance = 1e-7) ## diff.mean.shared normout <- doNormalization(sce = sce_mq_preimputation, From 6211287c6ad07a38216756b5e02063a4411824b1 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 2 Aug 2026 19:04:24 +0200 Subject: [PATCH 64/65] Fix check note --- R/importDIANN.R | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/R/importDIANN.R b/R/importDIANN.R index d3940f4..0a430f1 100644 --- a/R/importDIANN.R +++ b/R/importDIANN.R @@ -90,6 +90,7 @@ einprotDIANNFiltersDF <- list(Filter = function(df) { #' @importFrom tools file_path_sans_ext #' @importFrom methods as #' @importFrom utils read.delim +#' @importFrom rlang .data importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", includeOnlySamples = "", excludeSamples = "", stopIfEmpty = FALSE, @@ -157,12 +158,12 @@ importDIANN <- function(inFile, fileType = "pg_matrix", outLevel = "pg", .assertPackagesAvailable("arrow") tmp <- arrow::read_parquet(inFile, as_data_frame = TRUE) } - iColsAll <- tmp |> dplyr::select(Run) |> distinct() |> pull() + iColsAll <- tmp |> dplyr::select("Run") |> distinct() |> pull() iCols <- .getiCols(iColsAll = iColsAll, includeOnlySamples = includeOnlySamples, excludeSamples = excludeSamples, stopIfEmpty = stopIfEmpty) - tmp <- tmp |> dplyr::filter(Run %in% iCols) + tmp <- tmp |> dplyr::filter(.data$Run %in% iCols) stopifnot(aName %in% colnames(tmp)) # Filter From 06e533d099ad565f8cbed34656ab8d6c223dbcb5 Mon Sep 17 00:00:00 2001 From: Charlotte Soneson Date: Sun, 2 Aug 2026 19:35:18 +0200 Subject: [PATCH 65/65] Update actions --- .github/workflows/R-CMD-check.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 95b2845..dafa3f7 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -35,7 +35,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@v3 + uses: actions/checkout@v7 - name: Set up R and install BiocManager uses: grimbough/bioc-actions/setup-bioc@v1 @@ -60,7 +60,7 @@ jobs: - name: Cache R packages if: runner.os != 'Windows' && matrix.config.image == null - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ${{ env.R_LIBS_USER }} key: ${{ env.cache-version }}-${{ runner.os }}-bioc-${{ matrix.config.bioc }}-${{ hashFiles('depends.Rds') }}