From 0260c103d8eb2b965b03f87e8e760b3946f9fec2 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Sat, 11 Apr 2026 17:57:57 -0400 Subject: [PATCH 01/18] feat: per-label statistics and multi-label summarization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - dataProcess.R: split protein_indices by PROTEIN+LABEL (not just PROTEIN) when not using labeled reference, so each label is summarized separately; remove LABEL == "L" filters from Linear/TMP survival imputation and result aggregation; propagate LABEL column through all result tables - utils_summarization.R: rename is_labeled → is_labeled_reference in .runTukey/.fitTukey; return LABEL in non-reference results; remove LABEL == "L" guard from .getNonMissingFilterStats - utils_output.R: use rbindlist(fill=TRUE) for mixed-schema result lists; add LABEL to TotalGroupMeasurements/NumMeasuredFeature/NumImputedFeature grouping keys; merge summarized+lab on LABEL; include ref in output cols; remove LABEL == "L" guards from nonmissing tracking - man/: update .fitTukey and .runTukey Rd docs for renamed parameter Co-Authored-By: Claude Sonnet 4.6 --- R/dataProcess.R | 79 +++++++++++++++++++++-------------------- R/utils_output.R | 55 ++++++++++++++-------------- R/utils_summarization.R | 46 +++++++++++++----------- man/dot-fitTukey.Rd | 8 ++++- man/dot-runTukey.Rd | 8 +++-- 5 files changed, 105 insertions(+), 91 deletions(-) diff --git a/R/dataProcess.R b/R/dataProcess.R index 74e2c808..597ce9b2 100755 --- a/R/dataProcess.R +++ b/R/dataProcess.R @@ -210,7 +210,12 @@ MSstatsSummarizeWithMultipleCores = function(input, method, impute, censored_sym remove50missing, equal_variance, numberOfCores = 1, aft_iterations = 90) { if (numberOfCores > 1) { - protein_indices = split(seq_len(nrow(input)), list(input$PROTEIN)) + is_labeled_reference = "ref" %in% colnames(input) && any(input$ref, na.rm = TRUE) + if (is_labeled_reference) { + protein_indices = split(seq_len(nrow(input)), list(input$PROTEIN)) + } else { + protein_indices = split(seq_len(nrow(input)), list(input$PROTEIN, input$LABEL)) + } num_proteins = length(protein_indices) function_environment = environment() cl = parallel::makeCluster(numberOfCores) @@ -292,9 +297,14 @@ MSstatsSummarizeWithMultipleCores = function(input, method, impute, censored_sym #' MSstatsSummarizeWithSingleCore = function(input, method, impute, censored_symbol, remove50missing, equal_variance, aft_iterations = 90) { - - - protein_indices = split(seq_len(nrow(input)), list(input$PROTEIN)) + + + is_labeled_reference = "ref" %in% colnames(input) && any(input$ref, na.rm = TRUE) + if (is_labeled_reference) { + protein_indices = split(seq_len(nrow(input)), list(input$PROTEIN)) + } else { + protein_indices = split(seq_len(nrow(input)), list(input$PROTEIN, input$LABEL)) + } num_proteins = length(protein_indices) summarized_results = vector("list", num_proteins) if (method == "TMP") { @@ -383,30 +393,22 @@ MSstatsSummarizeSingleLinear = function(single_protein, if (impute & any(single_protein[["censored"]])) { survival_fit = .fitSurvival( - single_protein[LABEL == "L", cols, with = FALSE], + single_protein[, cols, with = FALSE], aft_iterations ) sigma2 = survival_fit$scale^2 - + single_protein[, c("predicted", "imputation_var") := { pred = predict(survival_fit, newdata = .SD, se.fit = TRUE) list(pred$fit, pred$se.fit^2 + sigma2) }] - - single_protein[, predicted := ifelse( - censored & (LABEL == "L"), - predicted, - NA - )] - single_protein[, newABUNDANCE := ifelse( - censored & LABEL == "L", - predicted, - newABUNDANCE - )] - - survival = single_protein[, c(cols, "predicted"), with = FALSE] + + single_protein[, predicted := ifelse(censored, predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] + + survival = single_protein[, intersect(c(cols, "LABEL", "predicted"), colnames(single_protein)), with = FALSE] } else { - survival = single_protein[, cols, with = FALSE] + survival = single_protein[, intersect(c(cols, "LABEL"), colnames(single_protein)), with = FALSE] survival[, predicted := NA] } @@ -432,14 +434,12 @@ MSstatsSummarizeSingleLinear = function(single_protein, is_single_feature = .checkSingleFeature(single_protein) if (is_single_feature) { - result = single_protein[LABEL == "L", - .(LogIntensities = mean(newABUNDANCE)), - by = RUN] + result = single_protein[, .(LogIntensities = mean(newABUNDANCE)), by = RUN] result[, Protein := unique(single_protein$PROTEIN)] + result[, LABEL := unique(single_protein$LABEL)] result[, Variance := NA_real_] - setcolorder(result, c("Protein", "RUN", "LogIntensities", - "Variance")) - + setcolorder(result, c("Protein", "RUN", "LogIntensities", "Variance")) + return(list(result, survival)) } else { counts = xtabs( @@ -447,14 +447,14 @@ MSstatsSummarizeSingleLinear = function(single_protein, data = unique(single_protein[, .(FEATURE, RUN)]) ) counts = as.matrix(counts) - + fit = try( .fitLinearModel(single_protein, is_single_feature, is_labeled = label, equal_variances), silent = TRUE ) - + if (inherits(fit, "try-error")) { msg = paste("*** error : can't fit the model for", unique(single_protein$PROTEIN)) @@ -464,12 +464,12 @@ MSstatsSummarizeSingleLinear = function(single_protein, } else { cf = summary(fit)$coefficients[, 1] cov_mat = vcov(fit) - - result = unique(single_protein[, .(Protein = PROTEIN, - RUN = RUN)]) + + result = unique(single_protein[, .(Protein = PROTEIN, RUN = RUN)]) extracted_values = get_linear_summary(single_protein, cf, counts, label, cov_mat) result = cbind(result, extracted_values) + result[, LABEL := unique(single_protein$LABEL)] } return(list(result, survival)) @@ -528,8 +528,7 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, # Try to fit survival model and catch convergence warnings survival_fit = withCallingHandlers({ - .fitSurvival(single_protein[LABEL == "L", cols, with = FALSE], - aft_iterations) + .fitSurvival(single_protein[, cols, with = FALSE], aft_iterations) }, warning = function(w) { if (grepl("converge", conditionMessage(w), ignore.case = TRUE)) { message("Convergence warning caught: ", conditionMessage(w)) @@ -543,15 +542,14 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, single_protein[, predicted := NA_real_] } - single_protein[, predicted := ifelse(censored & (LABEL == "L"), predicted, NA)] - single_protein[, newABUNDANCE := ifelse(censored & LABEL == "L", - predicted, newABUNDANCE)] - survival = single_protein[, c(cols, "predicted"), with = FALSE] + single_protein[, predicted := ifelse(censored, predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] + survival = single_protein[, intersect(c(cols, "LABEL", "predicted"), colnames(single_protein)), with = FALSE] } else { - survival = single_protein[, cols, with = FALSE] + survival = single_protein[, intersect(c(cols, "LABEL"), colnames(single_protein)), with = FALSE] survival[, predicted := NA] } - + single_protein = .isSummarizable(single_protein, remove50missing) if (is.null(single_protein)) { return(list(NULL, NULL)) @@ -561,6 +559,9 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, any(single_protein$is_labeled_ref, na.rm = TRUE) result = .runTukey(single_protein, is_labeled_reference, censored_symbol, remove50missing) + if (!is.null(result) && !is.element("LABEL", colnames(result))) { + result[, LABEL := "L"] + } } list(result, survival) } diff --git a/R/utils_output.R b/R/utils_output.R index 50ef3263..b1f64009 100644 --- a/R/utils_output.R +++ b/R/utils_output.R @@ -40,9 +40,9 @@ MSstatsSummarizationOutput = function(input, summarized, processed, input = .finalizeInput(input, summarized, method, impute, censored_symbol) summarized = lapply(summarized, function(x) x[[1]]) - summarized = data.table::rbindlist(summarized) + summarized = data.table::rbindlist(summarized, fill = TRUE) if (inherits(summarized, "try-error")) { - msg = paste("*** error : can't summarize per subplot with ", + msg = paste("*** error : can't summarize per subplot with ", method, ".") getOption("MSstatsLog")("ERROR", msg) getOption("MSstatsMsg")("ERROR", msg) @@ -50,24 +50,21 @@ MSstatsSummarizationOutput = function(input, summarized, processed, rqmodelqc = NULL workpred = NULL } else { - input[LABEL == "L", TotalGroupMeasurements := uniqueN(.SD), - by = c("PROTEIN", "GROUP"), + input[, TotalGroupMeasurements := uniqueN(.SD), + by = c("PROTEIN", "GROUP", "LABEL"), .SDcols = c("FEATURE", "originalRUN")] cols = intersect(c("PROTEIN", "originalRUN", "RUN", "GROUP", - "GROUP_ORIGINAL", "SUBJECT_ORIGINAL", + "GROUP_ORIGINAL", "SUBJECT_ORIGINAL", "LABEL", "TotalGroupMeasurements", - "NumMeasuredFeature", "MissingPercentage", + "NumMeasuredFeature", "MissingPercentage", "more50missing", "NumImputedFeature"), colnames(input)) - merge_col = ifelse(is.element("RUN", colnames(summarized)), + merge_col = ifelse(is.element("RUN", colnames(summarized)), "RUN", "SUBJECT_ORIGINAL") - lab = unique(input[LABEL == "L", cols, with = FALSE]) - if (nlevels(input$LABEL) > 1) { - lab = lab[GROUP != 0] - } + lab = unique(input[, cols, with = FALSE]) lab = lab[, colnames(lab) != "GROUP", with = FALSE] - rqall = merge(summarized, lab, by.x = c(merge_col, "Protein"), - by.y = c(merge_col, "PROTEIN")) + rqall = merge(summarized, lab, by.x = c(merge_col, "Protein", "LABEL"), + by.y = c(merge_col, "PROTEIN", "LABEL")) data.table::setnames(rqall, c("GROUP_ORIGINAL", "SUBJECT_ORIGINAL"), c("GROUP", "SUBJECT"), skip_absent = TRUE) @@ -83,8 +80,8 @@ MSstatsSummarizationOutput = function(input, summarized, processed, output_cols = intersect(c("PROTEIN", "PEPTIDE", "TRANSITION", "FEATURE", "LABEL", "GROUP", "RUN", "SUBJECT", "FRACTION", "originalRUN", "censored", "INTENSITY", "ABUNDANCE", - "newABUNDANCE", "predicted", "feature_quality", - "is_outlier", "remove"), colnames(input)) + "newABUNDANCE", "predicted", "feature_quality", + "is_outlier", "remove", "ref"), colnames(input)) input = input[, output_cols, with = FALSE] if (is.element("remove", colnames(processed))) { @@ -126,31 +123,31 @@ MSstatsSummarizationOutput = function(input, summarized, processed, INTENSITY = newABUNDANCE = NumImputedFeature = NULL survival_predictions = lapply(summarized, function(x) x[[2]]) - predicted_survival = data.table::rbindlist(survival_predictions) + predicted_survival = data.table::rbindlist(survival_predictions, fill = TRUE) if (impute) { - cols = intersect(colnames(input), c("newABUNDANCE", + cols = intersect(colnames(input), c("newABUNDANCE", "cen", "RUN", - "FEATURE", "ref_covariate")) - input = merge(input[, colnames(input) != "newABUNDANCE", with = FALSE], + "FEATURE", "ref_covariate", "LABEL")) + input = merge(input[, colnames(input) != "newABUNDANCE", with = FALSE], predicted_survival, by = setdiff(cols, "newABUNDANCE"), all.x = TRUE) } input[, NonMissingStats := .getNonMissingFilterStats(.SD, censored_symbol)] - input[, NumMeasuredFeature := sum(NonMissingStats), - by = c("PROTEIN", "RUN")] + input[, NumMeasuredFeature := sum(NonMissingStats), + by = c("PROTEIN", "RUN", "LABEL")] input[, MissingPercentage := 1 - (NumMeasuredFeature / total_features)] input[, more50missing := MissingPercentage >= 0.5] if (!is.null(censored_symbol)) { if (is.element("censored", colnames(input))) { - input[, nonmissing_orig := LABEL == "L" & !censored] + input[, nonmissing_orig := !censored] } else { - input[, nonmissing_orig := LABEL == "L" & !is.na(INTENSITY)] + input[, nonmissing_orig := !is.na(INTENSITY)] } input[, nonmissing_orig := ifelse(is.na(newABUNDANCE), TRUE, nonmissing_orig)] if (impute) { - input[, NumImputedFeature := sum(LABEL == "L" & !nonmissing_orig), - by = c("PROTEIN", "RUN")] + input[, NumImputedFeature := sum(!nonmissing_orig), + by = c("PROTEIN", "RUN", "LABEL")] } else { input[, NumImputedFeature := 0] } @@ -168,15 +165,15 @@ MSstatsSummarizationOutput = function(input, summarized, processed, censored = INTENSITY = newABUNDANCE = NumImputedFeature = NULL input[, NonMissingStats := .getNonMissingFilterStats(.SD, censored_symbol)] - input[, NumMeasuredFeature := sum(NonMissingStats), - by = c("PROTEIN", "RUN")] + input[, NumMeasuredFeature := sum(NonMissingStats), + by = c("PROTEIN", "RUN", "LABEL")] input[, MissingPercentage := 1 - (NumMeasuredFeature / total_features)] input[, more50missing := MissingPercentage >= 0.5] if (!is.null(censored_symbol)) { if (is.element("censored", colnames(input))) { - input[, nonmissing_orig := LABEL == "L" & !censored] + input[, nonmissing_orig := !censored] } else { - input[, nonmissing_orig := LABEL == "L" & !is.na(INTENSITY)] + input[, nonmissing_orig := !is.na(INTENSITY)] } input[, nonmissing_orig := ifelse(is.na(newABUNDANCE), TRUE, nonmissing_orig)] input[, NumImputedFeature := 0] diff --git a/R/utils_summarization.R b/R/utils_summarization.R index 395a5036..769973d9 100644 --- a/R/utils_summarization.R +++ b/R/utils_summarization.R @@ -62,21 +62,24 @@ #' Fit Tukey median polish #' @param input data.table with data for a single protein -#' @param is_labeled logical, if TRUE, data is coming from an SRM experiment +#' @param is_labeled_reference logical, if TRUE, H channel is used as a +#' normalization reference (SRM experiment): L abundances are adjusted by +#' subtracting the H value and adding back the H median, and only L results +#' are returned. If FALSE (e.g. protein turnover), each label is summarized +#' independently and results for all labels are returned. #' @inheritParams MSstatsSummarizeWithSingleCore #' @return data.table #' @keywords internal -.runTukey = function(input, is_labeled, censored_symbol, remove50missing) { +.runTukey = function(input, is_labeled_reference, censored_symbol, remove50missing) { Protein = RUN = newABUNDANCE = NULL - + if (nlevels(input$FEATURE) > 1) { - tmp_result = .fitTukey(input) - } else { - if (is_labeled) { + tmp_result = .fitTukey(input, is_labeled_reference) + } else { + if (is_labeled_reference) { tmp_result = .adjustLRuns(input, TRUE) } else { - tmp_result = input[input$LABEL == "L", - list(RUN, LogIntensities = newABUNDANCE)] + tmp_result = input[, list(LABEL, RUN, LogIntensities = newABUNDANCE)] } } tmp_result[, Protein := unique(input$PROTEIN)] @@ -85,23 +88,30 @@ #' Fit tukey median polish for a data matrix -#' @inheritParams .runTukey +#' @param input data.table with data for a single protein +#' @param is_labeled_reference logical, if TRUE, H channel is used as a +#' normalization reference (SRM experiment): L abundances are adjusted by +#' subtracting the H value and adding back the H median, and only L results +#' are returned. If FALSE (e.g. protein turnover), each label is summarized +#' independently and results for all labels are returned. #' @return data.table #' @keywords internal -.fitTukey = function(input) { +.fitTukey = function(input, is_labeled_reference) { LABEL = RUN = newABUNDANCE = NULL - + features = as.character(unique(input$FEATURE)) wide = data.table::dcast(LABEL + RUN ~ FEATURE, data = input, value.var = "newABUNDANCE", keep = TRUE) tmp_fitted = median_polish_summary(as.matrix(wide[, features, with = FALSE])) wide[, newABUNDANCE := tmp_fitted] tmp_result = wide[, list(LABEL, RUN, newABUNDANCE)] - - if (data.table::uniqueN(input$LABEL) == 2) { + + if (is_labeled_reference) { tmp_result = .adjustLRuns(tmp_result) + tmp_result[, list(RUN, LogIntensities = newABUNDANCE)] + } else { + tmp_result[, list(LABEL, RUN, LogIntensities = newABUNDANCE)] } - tmp_result[, list(RUN, LogIntensities = newABUNDANCE)] } @@ -133,13 +143,9 @@ #' @keywords internal .getNonMissingFilterStats = function(input, censored_symbol) { if (!is.null(censored_symbol)) { - if (censored_symbol == "NA") { - nonmissing_filter = input$LABEL == "L" & !is.na(input$newABUNDANCE) & !input$censored - } else { - nonmissing_filter = input$LABEL == "L" & !is.na(input$newABUNDANCE) & !input$censored - } + nonmissing_filter = !is.na(input$newABUNDANCE) & !input$censored } else { - nonmissing_filter = input$LABEL == "L" & !is.na(input$INTENSITY) + nonmissing_filter = !is.na(input$INTENSITY) } nonmissing_filter = nonmissing_filter & input$n_obs_run > 0 & input$n_obs > 1 nonmissing_filter diff --git a/man/dot-fitTukey.Rd b/man/dot-fitTukey.Rd index 651f63bf..097890a8 100644 --- a/man/dot-fitTukey.Rd +++ b/man/dot-fitTukey.Rd @@ -4,10 +4,16 @@ \alias{.fitTukey} \title{Fit tukey median polish for a data matrix} \usage{ -.fitTukey(input) +.fitTukey(input, is_labeled_reference) } \arguments{ \item{input}{data.table with data for a single protein} + +\item{is_labeled_reference}{logical, if TRUE, H channel is used as a +normalization reference (SRM experiment): L abundances are adjusted by +subtracting the H value and adding back the H median, and only L results +are returned. If FALSE (e.g. protein turnover), each label is summarized +independently and results for all labels are returned.} } \value{ data.table diff --git a/man/dot-runTukey.Rd b/man/dot-runTukey.Rd index 038d2911..76b6ab1b 100644 --- a/man/dot-runTukey.Rd +++ b/man/dot-runTukey.Rd @@ -4,12 +4,16 @@ \alias{.runTukey} \title{Fit Tukey median polish} \usage{ -.runTukey(input, is_labeled, censored_symbol, remove50missing) +.runTukey(input, is_labeled_reference, censored_symbol, remove50missing) } \arguments{ \item{input}{data.table with data for a single protein} -\item{is_labeled}{logical, if TRUE, data is coming from an SRM experiment} +\item{is_labeled_reference}{logical, if TRUE, H channel is used as a +normalization reference (SRM experiment): L abundances are adjusted by +subtracting the H value and adding back the H median, and only L results +are returned. If FALSE (e.g. protein turnover), each label is summarized +independently and results for all labels are returned.} \item{censored_symbol}{Missing values are censored or at random. 'NA' (default) assumes that all 'NA's in 'Intensity' column are censored. From f4e74f8fec8c1ac03b7db43618ddfe54b53604be Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Sat, 11 Apr 2026 18:00:01 -0400 Subject: [PATCH 02/18] fix(pr4): add LABEL to per-feature observation count grouping keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit n_obs, n_obs_run, total_features, and prop_features must all be computed within each PROTEIN+LABEL combination so that H and L features are counted independently — a fixup for the per-label statistics commit. Also switch .fitTukey roxygen to @inheritParams .runTukey. Co-Authored-By: Claude Sonnet 4.6 --- R/utils_summarization.R | 7 +------ R/utils_summarization_prepare.R | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/R/utils_summarization.R b/R/utils_summarization.R index 769973d9..193a66f2 100644 --- a/R/utils_summarization.R +++ b/R/utils_summarization.R @@ -88,12 +88,7 @@ #' Fit tukey median polish for a data matrix -#' @param input data.table with data for a single protein -#' @param is_labeled_reference logical, if TRUE, H channel is used as a -#' normalization reference (SRM experiment): L abundances are adjusted by -#' subtracting the H value and adding back the H median, and only L results -#' are returned. If FALSE (e.g. protein turnover), each label is summarized -#' independently and results for all labels are returned. +#' @inheritParams .runTukey #' @return data.table #' @keywords internal .fitTukey = function(input, is_labeled_reference) { diff --git a/R/utils_summarization_prepare.R b/R/utils_summarization_prepare.R index b1325c82..c6dac509 100644 --- a/R/utils_summarization_prepare.R +++ b/R/utils_summarization_prepare.R @@ -126,14 +126,14 @@ getProcessed = function(input) { input[, newABUNDANCE := ABUNDANCE] input[, nonmissing := .getNonMissingFilter(.SD, impute, censored_symbol)] - input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE")] + input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE", "LABEL")] # remove feature with 1 measurement - input[, nonmissing := ifelse(n_obs <= 1, FALSE, nonmissing)] - input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN")] - - input[, total_features := uniqueN(FEATURE), by = "PROTEIN"] + input[, nonmissing := ifelse(n_obs <= 1, FALSE, nonmissing)] + input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN", "LABEL")] + + input[, total_features := uniqueN(FEATURE), by = c("PROTEIN", "LABEL")] input[, prop_features := sum(nonmissing) / total_features, - by = c("PROTEIN", "RUN")] + by = c("PROTEIN", "RUN", "LABEL")] input } @@ -163,14 +163,14 @@ getProcessed = function(input) { } input[, nonmissing := .getNonMissingFilter(input, impute, censored_symbol)] - input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE")] - input[, nonmissing := ifelse(n_obs <= 1, FALSE, nonmissing)] - input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN")] - - input[, total_features := uniqueN(FEATURE), by = "PROTEIN"] + input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE", "LABEL")] + input[, nonmissing := ifelse(n_obs <= 1, FALSE, nonmissing)] + input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN", "LABEL")] + + input[, total_features := uniqueN(FEATURE), by = c("PROTEIN", "LABEL")] input[, prop_features := sum(nonmissing) / total_features, - by = c("PROTEIN", "RUN")] - + by = c("PROTEIN", "RUN", "LABEL")] + if (is.element("cen", colnames(input))) { if (any(input[["cen"]] == 0)) { .setCensoredByThreshold(input, censored_symbol, remove50missing) From 0b76e0200802fa226a4ce418306c8b552c44f7cb Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Sat, 11 Apr 2026 18:21:53 -0400 Subject: [PATCH 03/18] test(summarization): unit tests for per-label statistics Tests for PR 4 covering: - .prepareLinear n_obs grouped by PROTEIN+FEATURE+LABEL (not pooled) - .runTukey(is_labeled_reference=FALSE) returns LABEL column for both H and L - .fitTukey(is_labeled_reference=FALSE) returns LABEL column - .getNonMissingFilterStats applies to all rows (no LABEL=="L" guard) - Regression: SRMRawData still summarizes correctly after per-label changes Co-Authored-By: Claude Sonnet 4.6 --- inst/tinytest/test_pr4_per_label.R | 167 +++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 inst/tinytest/test_pr4_per_label.R diff --git a/inst/tinytest/test_pr4_per_label.R b/inst/tinytest/test_pr4_per_label.R new file mode 100644 index 00000000..0598c2f0 --- /dev/null +++ b/inst/tinytest/test_pr4_per_label.R @@ -0,0 +1,167 @@ +# Tests for PR 4: Per-label statistics and multi-label summarization +# +# Key changes: +# - .prepareLinear / .prepareTMP: n_obs, n_obs_run, total_features, +# prop_features now grouped by PROTEIN+FEATURE+LABEL (not just PROTEIN+FEATURE) +# so H and L observations are counted independently. +# - .runTukey / .fitTukey: when is_labeled_reference=FALSE (turnover), results +# include a LABEL column so both H and L summaries are returned. +# - .getNonMissingFilterStats: LABEL == "L" guard removed so filter is +# applied to all rows, not just light rows. +# - rbindlist(fill=TRUE) allows mixing H-only and L-only protein result lists. + +# Helpers ---------------------------------------------------------------------- + +# Data with 2 features, 2 runs per label, both H and L labels +make_two_label_input <- function() { + data.table::data.table( + PROTEIN = rep("P1", 8), + FEATURE = factor(rep(c("F1", "F2"), each = 4)), + LABEL = rep(c("L","L","H","H"), 2), + RUN = factor(rep(c("R1","R2","R1","R2"), 2)), + ABUNDANCE = c(10, 11, 8, 9, 10.5, 11.5, 8.5, 9.5), + INTENSITY = rep(100L, 8), + ANOMALYSCORES = rep(NA_real_, 8) + ) +} + +# --- .prepareLinear: n_obs computed per PROTEIN+FEATURE+LABEL ---------------- + +prep_input <- make_two_label_input() +result_prep <- MSstats:::.prepareLinear(prep_input, impute = FALSE, censored_symbol = NULL) + +# Each FEATURE+LABEL combination has 2 runs → n_obs should be 2 +expect_equal( + unique(result_prep[LABEL == "L" & FEATURE == "F1", n_obs]), + 2L, + info = ".prepareLinear: n_obs must be 2 for L rows of F1 (2 L runs)" +) +expect_equal( + unique(result_prep[LABEL == "H" & FEATURE == "F1", n_obs]), + 2L, + info = ".prepareLinear: n_obs must be 2 for H rows of F1 (2 H runs)" +) +# Without LABEL grouping the old code would have returned 4 (all runs pooled) +expect_false( + any(result_prep$n_obs == 4L), + info = ".prepareLinear: n_obs must not be 4 (old un-grouped value)" +) + +# total_features per PROTEIN+LABEL: 2 features per label +expect_equal( + unique(result_prep[LABEL == "L", total_features]), + 2L, + info = ".prepareLinear: total_features for L must be 2" +) +expect_equal( + unique(result_prep[LABEL == "H", total_features]), + 2L, + info = ".prepareLinear: total_features for H must be 2" +) + +# --- .runTukey(is_labeled_reference=FALSE): result includes LABEL column ----- + +# Single-feature input — exercises the non-.fitTukey branch +tukey_sf <- data.table::data.table( + PROTEIN = rep("P1", 8), + FEATURE = factor(rep("F1", 8)), + LABEL = rep(c("L","H"), each = 4), + RUN = factor(rep(c("R1","R2","R3","R4"), 2)), + newABUNDANCE = c(10, 11, 12, 13, 9, 10, 11, 12) +) + +result_turnover <- MSstats:::.runTukey( + tukey_sf, is_labeled_reference = FALSE, + censored_symbol = NULL, remove50missing = FALSE +) + +expect_true( + "LABEL" %in% colnames(result_turnover), + info = ".runTukey(is_labeled_reference=FALSE): result must have LABEL column" +) +expect_true( + "L" %in% as.character(result_turnover$LABEL), + info = ".runTukey(is_labeled_reference=FALSE): result must contain L rows" +) +expect_true( + "H" %in% as.character(result_turnover$LABEL), + info = ".runTukey(is_labeled_reference=FALSE): result must contain H rows" +) +expect_true( + "LogIntensities" %in% colnames(result_turnover), + info = ".runTukey(is_labeled_reference=FALSE): result must have LogIntensities column" +) + +# SRM path (is_labeled_reference=TRUE) must NOT return LABEL column +result_srm <- MSstats:::.runTukey( + tukey_sf, is_labeled_reference = TRUE, + censored_symbol = NULL, remove50missing = FALSE +) +expect_false( + "LABEL" %in% colnames(result_srm), + info = ".runTukey(is_labeled_reference=TRUE): SRM path must not return LABEL column" +) + +# --- .fitTukey(is_labeled_reference=FALSE): result includes LABEL column ----- + +# Multi-feature input — exercises .fitTukey +tukey_mf <- data.table::data.table( + PROTEIN = rep("P1", 16), + FEATURE = factor(rep(c("F1","F2"), each = 8)), + LABEL = rep(rep(c("L","L","L","L","H","H","H","H"), 1), 2), + RUN = factor(rep(c("R1","R2","R3","R4","R1","R2","R3","R4"), 2)), + newABUNDANCE = c(10,11,12,13, 8,9,10,11, 10.2,11.2,12.2,13.2, 8.2,9.2,10.2,11.2) +) + +result_tukey_turnover <- MSstats:::.fitTukey(tukey_mf, is_labeled_reference = FALSE) + +expect_true( + "LABEL" %in% colnames(result_tukey_turnover), + info = ".fitTukey(is_labeled_reference=FALSE): result must include LABEL column" +) +expect_true( + "LogIntensities" %in% colnames(result_tukey_turnover), + info = ".fitTukey(is_labeled_reference=FALSE): result must have LogIntensities column" +) + +result_tukey_srm <- MSstats:::.fitTukey(tukey_mf, is_labeled_reference = TRUE) +expect_false( + "LABEL" %in% colnames(result_tukey_srm), + info = ".fitTukey(is_labeled_reference=TRUE): SRM path must not return LABEL column" +) + +# --- .getNonMissingFilterStats: no LABEL == "L" guard ----------------------- + +nonmiss_input <- data.table::data.table( + LABEL = c("L","L","H","H"), + newABUNDANCE = c(10, NA, 8, NA), + INTENSITY = c(100L, NA_integer_, 80L, NA_integer_), + censored = c(FALSE, TRUE, FALSE, TRUE), + n_obs_run = c(2L, 2L, 2L, 2L), + n_obs = c(4L, 4L, 4L, 4L) +) + +filter_result <- MSstats:::.getNonMissingFilterStats(nonmiss_input, censored_symbol = "NA") + +# Both L and H non-missing rows should be flagged TRUE +expect_true( + filter_result[nonmiss_input$LABEL == "L" & !is.na(nonmiss_input$newABUNDANCE)], + info = ".getNonMissingFilterStats: L non-missing row must be TRUE" +) +expect_true( + filter_result[nonmiss_input$LABEL == "H" & !is.na(nonmiss_input$newABUNDANCE)], + info = ".getNonMissingFilterStats: H non-missing row must also be TRUE (no LABEL guard)" +) + +# --- Regression: SRMRawData summarization unchanged -------------------------- + +quant_srm <- dataProcess(SRMRawData, use_log_file = FALSE) + +expect_true( + nrow(quant_srm$ProteinLevelData) > 0, + info = "regression: SRMRawData must still produce ProteinLevelData after per-label changes" +) +expect_true( + "LogIntensities" %in% colnames(quant_srm$ProteinLevelData), + info = "regression: ProteinLevelData must still have LogIntensities" +) From 5202b2cc25c31e7eefae6d060251f4ca42867b7b Mon Sep 17 00:00:00 2001 From: tonywu1999 Date: Mon, 13 Apr 2026 13:23:07 -0400 Subject: [PATCH 04/18] fix is_labeled_ref --- R/dataProcess.R | 4 ++-- R/utils_output.R | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/R/dataProcess.R b/R/dataProcess.R index 597ce9b2..51b456af 100755 --- a/R/dataProcess.R +++ b/R/dataProcess.R @@ -210,7 +210,7 @@ MSstatsSummarizeWithMultipleCores = function(input, method, impute, censored_sym remove50missing, equal_variance, numberOfCores = 1, aft_iterations = 90) { if (numberOfCores > 1) { - is_labeled_reference = "ref" %in% colnames(input) && any(input$ref, na.rm = TRUE) + is_labeled_reference = "is_labeled_ref" %in% colnames(input) && any(input$is_labeled_ref, na.rm = TRUE) if (is_labeled_reference) { protein_indices = split(seq_len(nrow(input)), list(input$PROTEIN)) } else { @@ -299,7 +299,7 @@ MSstatsSummarizeWithSingleCore = function(input, method, impute, censored_symbol remove50missing, equal_variance, aft_iterations = 90) { - is_labeled_reference = "ref" %in% colnames(input) && any(input$ref, na.rm = TRUE) + is_labeled_reference = "is_labeled_ref" %in% colnames(input) && any(input$is_labeled_ref, na.rm = TRUE) if (is_labeled_reference) { protein_indices = split(seq_len(nrow(input)), list(input$PROTEIN)) } else { diff --git a/R/utils_output.R b/R/utils_output.R index b1f64009..7b748482 100644 --- a/R/utils_output.R +++ b/R/utils_output.R @@ -81,7 +81,7 @@ MSstatsSummarizationOutput = function(input, summarized, processed, "LABEL", "GROUP", "RUN", "SUBJECT", "FRACTION", "originalRUN", "censored", "INTENSITY", "ABUNDANCE", "newABUNDANCE", "predicted", "feature_quality", - "is_outlier", "remove", "ref"), colnames(input)) + "is_outlier", "remove", "is_labeled_ref"), colnames(input)) input = input[, output_cols, with = FALSE] if (is.element("remove", colnames(processed))) { From 654ac4133ddc2f9c6d06d516ad354df86509f7a2 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Mon, 13 Apr 2026 17:42:48 -0400 Subject: [PATCH 05/18] update unit tests --- inst/tinytest/test_pr4_per_label.R | 167 ------------------ inst/tinytest/test_utils_summarization.R | 91 ++++++++++ .../test_utils_summarization_prepare.R | 46 +++++ 3 files changed, 137 insertions(+), 167 deletions(-) delete mode 100644 inst/tinytest/test_pr4_per_label.R diff --git a/inst/tinytest/test_pr4_per_label.R b/inst/tinytest/test_pr4_per_label.R deleted file mode 100644 index 0598c2f0..00000000 --- a/inst/tinytest/test_pr4_per_label.R +++ /dev/null @@ -1,167 +0,0 @@ -# Tests for PR 4: Per-label statistics and multi-label summarization -# -# Key changes: -# - .prepareLinear / .prepareTMP: n_obs, n_obs_run, total_features, -# prop_features now grouped by PROTEIN+FEATURE+LABEL (not just PROTEIN+FEATURE) -# so H and L observations are counted independently. -# - .runTukey / .fitTukey: when is_labeled_reference=FALSE (turnover), results -# include a LABEL column so both H and L summaries are returned. -# - .getNonMissingFilterStats: LABEL == "L" guard removed so filter is -# applied to all rows, not just light rows. -# - rbindlist(fill=TRUE) allows mixing H-only and L-only protein result lists. - -# Helpers ---------------------------------------------------------------------- - -# Data with 2 features, 2 runs per label, both H and L labels -make_two_label_input <- function() { - data.table::data.table( - PROTEIN = rep("P1", 8), - FEATURE = factor(rep(c("F1", "F2"), each = 4)), - LABEL = rep(c("L","L","H","H"), 2), - RUN = factor(rep(c("R1","R2","R1","R2"), 2)), - ABUNDANCE = c(10, 11, 8, 9, 10.5, 11.5, 8.5, 9.5), - INTENSITY = rep(100L, 8), - ANOMALYSCORES = rep(NA_real_, 8) - ) -} - -# --- .prepareLinear: n_obs computed per PROTEIN+FEATURE+LABEL ---------------- - -prep_input <- make_two_label_input() -result_prep <- MSstats:::.prepareLinear(prep_input, impute = FALSE, censored_symbol = NULL) - -# Each FEATURE+LABEL combination has 2 runs → n_obs should be 2 -expect_equal( - unique(result_prep[LABEL == "L" & FEATURE == "F1", n_obs]), - 2L, - info = ".prepareLinear: n_obs must be 2 for L rows of F1 (2 L runs)" -) -expect_equal( - unique(result_prep[LABEL == "H" & FEATURE == "F1", n_obs]), - 2L, - info = ".prepareLinear: n_obs must be 2 for H rows of F1 (2 H runs)" -) -# Without LABEL grouping the old code would have returned 4 (all runs pooled) -expect_false( - any(result_prep$n_obs == 4L), - info = ".prepareLinear: n_obs must not be 4 (old un-grouped value)" -) - -# total_features per PROTEIN+LABEL: 2 features per label -expect_equal( - unique(result_prep[LABEL == "L", total_features]), - 2L, - info = ".prepareLinear: total_features for L must be 2" -) -expect_equal( - unique(result_prep[LABEL == "H", total_features]), - 2L, - info = ".prepareLinear: total_features for H must be 2" -) - -# --- .runTukey(is_labeled_reference=FALSE): result includes LABEL column ----- - -# Single-feature input — exercises the non-.fitTukey branch -tukey_sf <- data.table::data.table( - PROTEIN = rep("P1", 8), - FEATURE = factor(rep("F1", 8)), - LABEL = rep(c("L","H"), each = 4), - RUN = factor(rep(c("R1","R2","R3","R4"), 2)), - newABUNDANCE = c(10, 11, 12, 13, 9, 10, 11, 12) -) - -result_turnover <- MSstats:::.runTukey( - tukey_sf, is_labeled_reference = FALSE, - censored_symbol = NULL, remove50missing = FALSE -) - -expect_true( - "LABEL" %in% colnames(result_turnover), - info = ".runTukey(is_labeled_reference=FALSE): result must have LABEL column" -) -expect_true( - "L" %in% as.character(result_turnover$LABEL), - info = ".runTukey(is_labeled_reference=FALSE): result must contain L rows" -) -expect_true( - "H" %in% as.character(result_turnover$LABEL), - info = ".runTukey(is_labeled_reference=FALSE): result must contain H rows" -) -expect_true( - "LogIntensities" %in% colnames(result_turnover), - info = ".runTukey(is_labeled_reference=FALSE): result must have LogIntensities column" -) - -# SRM path (is_labeled_reference=TRUE) must NOT return LABEL column -result_srm <- MSstats:::.runTukey( - tukey_sf, is_labeled_reference = TRUE, - censored_symbol = NULL, remove50missing = FALSE -) -expect_false( - "LABEL" %in% colnames(result_srm), - info = ".runTukey(is_labeled_reference=TRUE): SRM path must not return LABEL column" -) - -# --- .fitTukey(is_labeled_reference=FALSE): result includes LABEL column ----- - -# Multi-feature input — exercises .fitTukey -tukey_mf <- data.table::data.table( - PROTEIN = rep("P1", 16), - FEATURE = factor(rep(c("F1","F2"), each = 8)), - LABEL = rep(rep(c("L","L","L","L","H","H","H","H"), 1), 2), - RUN = factor(rep(c("R1","R2","R3","R4","R1","R2","R3","R4"), 2)), - newABUNDANCE = c(10,11,12,13, 8,9,10,11, 10.2,11.2,12.2,13.2, 8.2,9.2,10.2,11.2) -) - -result_tukey_turnover <- MSstats:::.fitTukey(tukey_mf, is_labeled_reference = FALSE) - -expect_true( - "LABEL" %in% colnames(result_tukey_turnover), - info = ".fitTukey(is_labeled_reference=FALSE): result must include LABEL column" -) -expect_true( - "LogIntensities" %in% colnames(result_tukey_turnover), - info = ".fitTukey(is_labeled_reference=FALSE): result must have LogIntensities column" -) - -result_tukey_srm <- MSstats:::.fitTukey(tukey_mf, is_labeled_reference = TRUE) -expect_false( - "LABEL" %in% colnames(result_tukey_srm), - info = ".fitTukey(is_labeled_reference=TRUE): SRM path must not return LABEL column" -) - -# --- .getNonMissingFilterStats: no LABEL == "L" guard ----------------------- - -nonmiss_input <- data.table::data.table( - LABEL = c("L","L","H","H"), - newABUNDANCE = c(10, NA, 8, NA), - INTENSITY = c(100L, NA_integer_, 80L, NA_integer_), - censored = c(FALSE, TRUE, FALSE, TRUE), - n_obs_run = c(2L, 2L, 2L, 2L), - n_obs = c(4L, 4L, 4L, 4L) -) - -filter_result <- MSstats:::.getNonMissingFilterStats(nonmiss_input, censored_symbol = "NA") - -# Both L and H non-missing rows should be flagged TRUE -expect_true( - filter_result[nonmiss_input$LABEL == "L" & !is.na(nonmiss_input$newABUNDANCE)], - info = ".getNonMissingFilterStats: L non-missing row must be TRUE" -) -expect_true( - filter_result[nonmiss_input$LABEL == "H" & !is.na(nonmiss_input$newABUNDANCE)], - info = ".getNonMissingFilterStats: H non-missing row must also be TRUE (no LABEL guard)" -) - -# --- Regression: SRMRawData summarization unchanged -------------------------- - -quant_srm <- dataProcess(SRMRawData, use_log_file = FALSE) - -expect_true( - nrow(quant_srm$ProteinLevelData) > 0, - info = "regression: SRMRawData must still produce ProteinLevelData after per-label changes" -) -expect_true( - "LogIntensities" %in% colnames(quant_srm$ProteinLevelData), - info = "regression: ProteinLevelData must still have LogIntensities" -) diff --git a/inst/tinytest/test_utils_summarization.R b/inst/tinytest/test_utils_summarization.R index 959c8aca..dac4a208 100644 --- a/inst/tinytest/test_utils_summarization.R +++ b/inst/tinytest/test_utils_summarization.R @@ -1,3 +1,4 @@ +# --- .fitLinearModel tests -------------------------------------------------- single_protein <- data.table::data.table( PROTEIN = c( "Q96S19", "Q96S19", "Q96S19", "Q96S19", "Q96S19", "Q96S19", "Q96S19", "Q96S19", "Q96S19", "Q96S19" ), PEPTIDE = c( "AFPLAEWQPSDVDQR_2", "ASGLLLER_2", "AFPLAEWQPSDVDQR_2", "ASGLLLER_2", "AFPLAEWQPSDVDQR_2", "ASGLLLER_2", "AFPLAEWQPSDVDQR_2", "ASGLLLER_2", "AFPLAEWQPSDVDQR_2", "ASGLLLER_2" ), @@ -76,3 +77,93 @@ expect_true( any(grepl("ref_covariate", names(coef(lm_fit)))), info = "ref_covariate: model coefficients must include 'ref_covariate' terms" ) + +# --- .fitTukey(is_labeled_reference=FALSE): result includes LABEL column ----- + +# Multi-feature input — exercises .fitTukey +tukey_mf <- data.table::data.table( + PROTEIN = rep("P1", 16), + FEATURE = factor(rep(c("F1","F2"), each = 8)), + LABEL = rep(rep(c("L","L","L","L","H","H","H","H"), 1), 2), + RUN = factor(rep(c("R1","R2","R3","R4","R1","R2","R3","R4"), 2)), + newABUNDANCE = c(10,11,12,13, 8,9,10,11, 10.2,11.2,12.2,13.2, 8.2,9.2,10.2,11.2) +) + +result_tukey_turnover <- MSstats:::.fitTukey(tukey_mf, is_labeled_reference = FALSE) + +expect_true( + "LABEL" %in% colnames(result_tukey_turnover), + info = ".fitTukey(is_labeled_reference=FALSE): result must include LABEL column" +) +expect_true( + "LogIntensities" %in% colnames(result_tukey_turnover), + info = ".fitTukey(is_labeled_reference=FALSE): result must have LogIntensities column" +) + +result_tukey_srm <- MSstats:::.fitTukey(tukey_mf, is_labeled_reference = TRUE) +expect_false( + "LABEL" %in% colnames(result_tukey_srm), + info = ".fitTukey(is_labeled_reference=TRUE): SRM path must not return LABEL column" +) + + +# Single-feature input — exercises the non-.fitTukey branch +tukey_sf <- data.table::data.table( + PROTEIN = rep("P1", 8), + FEATURE = factor(rep("F1", 8)), + LABEL = rep(c("L","H"), each = 4), + RUN = factor(rep(c("R1","R2","R3","R4"), 2)), + newABUNDANCE = c(10, 11, 12, 13, 9, 10, 11, 12) +) + +result_turnover <- MSstats:::.runTukey( + tukey_sf, is_labeled_reference = FALSE, + censored_symbol = NULL, remove50missing = FALSE +) + +expect_true( + "LABEL" %in% colnames(result_turnover), + info = ".runTukey(is_labeled_reference=FALSE): result must have LABEL column" +) +expect_true( + "L" %in% as.character(result_turnover$LABEL), + info = ".runTukey(is_labeled_reference=FALSE): result must contain L rows" +) +expect_true( + "H" %in% as.character(result_turnover$LABEL), + info = ".runTukey(is_labeled_reference=FALSE): result must contain H rows" +) +expect_true( + "LogIntensities" %in% colnames(result_turnover), + info = ".runTukey(is_labeled_reference=FALSE): result must have LogIntensities column" +) + +result_srm <- MSstats:::.runTukey( + tukey_sf, is_labeled_reference = TRUE, + censored_symbol = NULL, remove50missing = FALSE +) +expect_false( + "LABEL" %in% colnames(result_srm), + info = ".runTukey(is_labeled_reference=TRUE): SRM path must not return LABEL column" +) + +# --- .getNonMissingFilterStats ----------------------- + +nonmiss_input <- data.table::data.table( + LABEL = c("L","L","H","H"), + newABUNDANCE = c(10, NA, 8, NA), + INTENSITY = c(100L, NA_integer_, 80L, NA_integer_), + censored = c(FALSE, TRUE, FALSE, TRUE), + n_obs_run = c(2L, 2L, 2L, 2L), + n_obs = c(4L, 4L, 4L, 4L) +) + +filter_result <- MSstats:::.getNonMissingFilterStats(nonmiss_input, censored_symbol = "NA") +expect_true( + filter_result[nonmiss_input$LABEL == "L" & !is.na(nonmiss_input$newABUNDANCE)], + info = ".getNonMissingFilterStats: L non-missing row must be TRUE" +) +expect_true( + filter_result[nonmiss_input$LABEL == "H" & !is.na(nonmiss_input$newABUNDANCE)], + info = ".getNonMissingFilterStats: H non-missing row must also be TRUE" +) diff --git a/inst/tinytest/test_utils_summarization_prepare.R b/inst/tinytest/test_utils_summarization_prepare.R index e4f70821..a97f2a56 100644 --- a/inst/tinytest/test_utils_summarization_prepare.R +++ b/inst/tinytest/test_utils_summarization_prepare.R @@ -72,3 +72,49 @@ expect_false( info = "ref_covariate: must NOT be created for label-free (single LABEL) data" ) +# --- .prepareLinear: n_obs computed per PROTEIN+FEATURE+LABEL ---------------- + +make_two_label_input <- function() { + data.table::data.table( + PROTEIN = rep("P1", 8), + FEATURE = factor(rep(c("F1", "F2"), each = 4)), + LABEL = rep(c("L","L","H","H"), 2), + RUN = factor(rep(c("R1","R2","R1","R2"), 2)), + ABUNDANCE = c(10, 11, 8, 9, 10.5, 11.5, 8.5, 9.5), + INTENSITY = rep(100L, 8), + ANOMALYSCORES = rep(NA_real_, 8) + ) +} + +prep_input <- make_two_label_input() +result_prep <- MSstats:::.prepareLinear(prep_input, impute = FALSE, censored_symbol = NULL) + +# Each FEATURE+LABEL combination has 2 runs → n_obs should be 2 +expect_equal( + unique(result_prep[LABEL == "L" & FEATURE == "F1", n_obs]), + 2L, + info = ".prepareLinear: n_obs must be 2 for L rows of F1 (2 L runs)" +) +expect_equal( + unique(result_prep[LABEL == "H" & FEATURE == "F1", n_obs]), + 2L, + info = ".prepareLinear: n_obs must be 2 for H rows of F1 (2 H runs)" +) +# Without LABEL grouping the old code would have returned 4 (all runs pooled) +expect_false( + any(result_prep$n_obs == 4L), + info = ".prepareLinear: n_obs must not be 4 (old un-grouped value)" +) + +# total_features per PROTEIN+LABEL: 2 features per label +expect_equal( + unique(result_prep[LABEL == "L", total_features]), + 2L, + info = ".prepareLinear: total_features for L must be 2" +) +expect_equal( + unique(result_prep[LABEL == "H", total_features]), + 2L, + info = ".prepareLinear: total_features for H must be 2" +) + From c594c19a325ad65627fb170082b3db3db170a946 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Mon, 13 Apr 2026 18:23:32 -0400 Subject: [PATCH 06/18] fix grouping based on srm or not --- R/utils_summarization_prepare.R | 45 ++++++++++++++++++++------------- man/dot-prepareLinear.Rd | 7 ++++- man/dot-prepareSummary.Rd | 13 +++++++++- man/dot-prepareTMP.Rd | 7 ++++- 4 files changed, 52 insertions(+), 20 deletions(-) diff --git a/R/utils_summarization_prepare.R b/R/utils_summarization_prepare.R index c6dac509..d8bb4580 100644 --- a/R/utils_summarization_prepare.R +++ b/R/utils_summarization_prepare.R @@ -50,7 +50,7 @@ MSstatsPrepareForSummarization = function(input, method, impute, censored_symbol getOption("MSstatsMsg")("INFO", msg) } - input = .prepareSummary(input, method, impute, censored_symbol) + input = .prepareSummary(input, method, impute, censored_symbol, add_ref_covariate) input[, PROTEIN := factor(PROTEIN)] input } @@ -104,13 +104,18 @@ getProcessed = function(input) { #' @param method "TMP" / "linear" #' @param impute logical #' @param censored_symbol "0"/"NA" +#' @param is_labeled_reference logical, if TRUE the H channel is a normalization +#' reference (SRM) and grouping keys do not include LABEL; if FALSE (e.g. +#' protein turnover) LABEL is added to grouping keys so each label is +#' processed independently. #' @return data.table #' @keywords internal -.prepareSummary = function(input, method, impute, censored_symbol) { +.prepareSummary = function(input, method, impute, censored_symbol, + is_labeled_reference = FALSE) { # if (method == "TMP") { - input = .prepareTMP(input, impute, censored_symbol) + input = .prepareTMP(input, impute, censored_symbol, is_labeled_reference) # } else { - # input = .prepareLinear(input, FALSE, censored_symbol) + # input = .prepareLinear(input, FALSE, censored_symbol, is_labeled_reference) # } input } @@ -120,20 +125,23 @@ getProcessed = function(input) { #' @inheritParams .prepareSummary #' @return data.table #' @keywords internal -.prepareLinear = function(input, impute, censored_symbol) { +.prepareLinear = function(input, impute, censored_symbol, + is_labeled_reference = FALSE) { newABUNDANCE = ABUNDANCE = nonmissing = n_obs = n_obs_run = NULL total_features = FEATURE = prop_features = NULL - + + label_by = if (is_labeled_reference) character(0) else "LABEL" + input[, newABUNDANCE := ABUNDANCE] input[, nonmissing := .getNonMissingFilter(.SD, impute, censored_symbol)] - input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE", "LABEL")] + input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE", label_by)] # remove feature with 1 measurement input[, nonmissing := ifelse(n_obs <= 1, FALSE, nonmissing)] - input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN", "LABEL")] + input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN", label_by)] - input[, total_features := uniqueN(FEATURE), by = c("PROTEIN", "LABEL")] + input[, total_features := uniqueN(FEATURE), by = c("PROTEIN", label_by)] input[, prop_features := sum(nonmissing) / total_features, - by = c("PROTEIN", "RUN", "LABEL")] + by = c("PROTEIN", "RUN", label_by)] input } @@ -142,11 +150,14 @@ getProcessed = function(input) { #' @inheritParams .prepareSummary #' @return data.table #' @keywords internal -.prepareTMP = function(input, impute, censored_symbol) { +.prepareTMP = function(input, impute, censored_symbol, + is_labeled_reference = FALSE) { censored = feature_quality = newABUNDANCE = cen = nonmissing = n_obs = NULL n_obs_run = total_features = FEATURE = prop_features = NULL remove50missing = ABUNDANCE = NULL - + + label_by = if (is_labeled_reference) character(0) else "LABEL" + if (impute & !is.null(censored_symbol)) { if (is.element("feature_quality", colnames(input))) { input[, censored := ifelse(feature_quality == "Informative", @@ -161,15 +172,15 @@ getProcessed = function(input) { } else { input[, newABUNDANCE := ABUNDANCE] } - + input[, nonmissing := .getNonMissingFilter(input, impute, censored_symbol)] - input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE", "LABEL")] + input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE", label_by)] input[, nonmissing := ifelse(n_obs <= 1, FALSE, nonmissing)] - input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN", "LABEL")] + input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN", label_by)] - input[, total_features := uniqueN(FEATURE), by = c("PROTEIN", "LABEL")] + input[, total_features := uniqueN(FEATURE), by = c("PROTEIN", label_by)] input[, prop_features := sum(nonmissing) / total_features, - by = c("PROTEIN", "RUN", "LABEL")] + by = c("PROTEIN", "RUN", label_by)] if (is.element("cen", colnames(input))) { if (any(input[["cen"]] == 0)) { diff --git a/man/dot-prepareLinear.Rd b/man/dot-prepareLinear.Rd index 4a4f3b4e..e6e2bae7 100644 --- a/man/dot-prepareLinear.Rd +++ b/man/dot-prepareLinear.Rd @@ -4,7 +4,7 @@ \alias{.prepareLinear} \title{Prepare feature-level data for linear summarization} \usage{ -.prepareLinear(input, impute, censored_symbol) +.prepareLinear(input, impute, censored_symbol, is_labeled_reference = FALSE) } \arguments{ \item{input}{data.table} @@ -12,6 +12,11 @@ \item{impute}{logical} \item{censored_symbol}{"0"/"NA"} + +\item{is_labeled_reference}{logical, if TRUE the H channel is a normalization +reference (SRM) and grouping keys do not include LABEL; if FALSE (e.g. +protein turnover) LABEL is added to grouping keys so each label is +processed independently.} } \value{ data.table diff --git a/man/dot-prepareSummary.Rd b/man/dot-prepareSummary.Rd index 7be7b47f..5e7da14f 100644 --- a/man/dot-prepareSummary.Rd +++ b/man/dot-prepareSummary.Rd @@ -4,7 +4,13 @@ \alias{.prepareSummary} \title{Prepare feature-level data for summarization} \usage{ -.prepareSummary(input, method, impute, censored_symbol) +.prepareSummary( + input, + method, + impute, + censored_symbol, + is_labeled_reference = FALSE +) } \arguments{ \item{input}{data.table} @@ -14,6 +20,11 @@ \item{impute}{logical} \item{censored_symbol}{"0"/"NA"} + +\item{is_labeled_reference}{logical, if TRUE the H channel is a normalization +reference (SRM) and grouping keys do not include LABEL; if FALSE (e.g. +protein turnover) LABEL is added to grouping keys so each label is +processed independently.} } \value{ data.table diff --git a/man/dot-prepareTMP.Rd b/man/dot-prepareTMP.Rd index 506f083f..2a31013d 100644 --- a/man/dot-prepareTMP.Rd +++ b/man/dot-prepareTMP.Rd @@ -4,7 +4,7 @@ \alias{.prepareTMP} \title{Prepare feature-level data for TMP summarization} \usage{ -.prepareTMP(input, impute, censored_symbol) +.prepareTMP(input, impute, censored_symbol, is_labeled_reference = FALSE) } \arguments{ \item{input}{data.table} @@ -12,6 +12,11 @@ \item{impute}{logical} \item{censored_symbol}{"0"/"NA"} + +\item{is_labeled_reference}{logical, if TRUE the H channel is a normalization +reference (SRM) and grouping keys do not include LABEL; if FALSE (e.g. +protein turnover) LABEL is added to grouping keys so each label is +processed independently.} } \value{ data.table From a34440f1eb17ca48e728cb84453f20266328105c Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Mon, 13 Apr 2026 18:46:34 -0400 Subject: [PATCH 07/18] fix srm discrepancies --- R/dataProcess.R | 69 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/R/dataProcess.R b/R/dataProcess.R index 51b456af..2c3ea74d 100755 --- a/R/dataProcess.R +++ b/R/dataProcess.R @@ -366,36 +366,41 @@ MSstatsSummarizeWithSingleCore = function(input, method, impute, censored_symbol #' head(single_protein_summary[[1]]) #' #' -MSstatsSummarizeSingleLinear = function(single_protein, +MSstatsSummarizeSingleLinear = function(single_protein, impute, - censored_symbol, - remove50missing, + censored_symbol, + remove50missing, aft_iterations = 90, equal_variances = TRUE) { ABUNDANCE = RUN = FEATURE = PROTEIN = LogIntensities = NULL - + cols = intersect( colnames(single_protein), c("newABUNDANCE", "cen", "RUN", "FEATURE", "ref_covariate") ) - + + is_labeled_reference = "is_labeled_ref" %in% colnames(single_protein) && + any(single_protein$is_labeled_ref, na.rm = TRUE) + single_protein = single_protein[ (n_obs > 1 & !is.na(n_obs)) & (n_obs_run > 0 & !is.na(n_obs_run)) ] - + if (nrow(single_protein) == 0) { return(list(NULL, NULL)) } - + single_protein[, RUN := factor(RUN)] single_protein[, FEATURE := factor(FEATURE)] - + if (impute & any(single_protein[["censored"]])) { - survival_fit = .fitSurvival( - single_protein[, cols, with = FALSE], - aft_iterations - ) + fit_data = if (is_labeled_reference) { + single_protein[LABEL == "L", cols, with = FALSE] + } else { + single_protein[, cols, with = FALSE] + } + survival_fit = .fitSurvival(fit_data, aft_iterations) sigma2 = survival_fit$scale^2 single_protein[, c("predicted", "imputation_var") := { @@ -403,8 +408,13 @@ MSstatsSummarizeSingleLinear = function(single_protein, list(pred$fit, pred$se.fit^2 + sigma2) }] - single_protein[, predicted := ifelse(censored, predicted, NA)] - single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] + if (is_labeled_reference) { + single_protein[, predicted := ifelse(censored & LABEL == "L", predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored & LABEL == "L", predicted, newABUNDANCE)] + } else { + single_protein[, predicted := ifelse(censored, predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] + } survival = single_protein[, intersect(c(cols, "LABEL", "predicted"), colnames(single_protein)), with = FALSE] } else { @@ -508,12 +518,14 @@ MSstatsSummarizeSingleLinear = function(single_protein, #' impute, cens, FALSE, 100) #' head(single_protein_summary[[1]]) #' -MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, +MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, remove50missing, aft_iterations = 90) { newABUNDANCE = n_obs = n_obs_run = RUN = FEATURE = LABEL = NULL predicted = censored = NULL cols = intersect(colnames(single_protein), c("newABUNDANCE", "cen", "RUN", "FEATURE", "ref_covariate")) + is_labeled_reference = "is_labeled_ref" %in% colnames(single_protein) && + any(single_protein$is_labeled_ref, na.rm = TRUE) single_protein = single_protein[(n_obs > 1 & !is.na(n_obs)) & (n_obs_run > 0 & !is.na(n_obs_run))] if (nrow(single_protein) == 0) { @@ -522,28 +534,39 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, single_protein[, RUN := factor(RUN)] single_protein[, FEATURE := factor(FEATURE)] if (impute & any(single_protein[["censored"]])) { - + # Flag to track convergence warning converged = TRUE - + + fit_data = if (is_labeled_reference) { + single_protein[LABEL == "L", cols, with = FALSE] + } else { + single_protein[, cols, with = FALSE] + } + # Try to fit survival model and catch convergence warnings survival_fit = withCallingHandlers({ - .fitSurvival(single_protein[, cols, with = FALSE], aft_iterations) + .fitSurvival(fit_data, aft_iterations) }, warning = function(w) { if (grepl("converge", conditionMessage(w), ignore.case = TRUE)) { message("Convergence warning caught: ", conditionMessage(w)) converged <<- FALSE } }) - + if (converged) { single_protein[, predicted := predict(survival_fit, newdata = .SD)] } else { single_protein[, predicted := NA_real_] } - - single_protein[, predicted := ifelse(censored, predicted, NA)] - single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] + + if (is_labeled_reference) { + single_protein[, predicted := ifelse(censored & LABEL == "L", predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored & LABEL == "L", predicted, newABUNDANCE)] + } else { + single_protein[, predicted := ifelse(censored, predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] + } survival = single_protein[, intersect(c(cols, "LABEL", "predicted"), colnames(single_protein)), with = FALSE] } else { survival = single_protein[, intersect(c(cols, "LABEL"), colnames(single_protein)), with = FALSE] @@ -555,8 +578,6 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, return(list(NULL, NULL)) } else { single_protein = single_protein[!is.na(newABUNDANCE), ] - is_labeled_reference = "is_labeled_ref" %in% colnames(single_protein) && - any(single_protein$is_labeled_ref, na.rm = TRUE) result = .runTukey(single_protein, is_labeled_reference, censored_symbol, remove50missing) if (!is.null(result) && !is.element("LABEL", colnames(result))) { From d861d0da22556773523c0971dbff1cec742599b5 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Mon, 13 Apr 2026 18:53:46 -0400 Subject: [PATCH 08/18] fix is_labeled_ref --- R/dataProcess.R | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/R/dataProcess.R b/R/dataProcess.R index 2c3ea74d..dcb9a01c 100755 --- a/R/dataProcess.R +++ b/R/dataProcess.R @@ -396,7 +396,7 @@ MSstatsSummarizeSingleLinear = function(single_protein, if (impute & any(single_protein[["censored"]])) { fit_data = if (is_labeled_reference) { - single_protein[LABEL == "L", cols, with = FALSE] + single_protein[!is_labeled_ref, cols, with = FALSE] } else { single_protein[, cols, with = FALSE] } @@ -409,8 +409,8 @@ MSstatsSummarizeSingleLinear = function(single_protein, }] if (is_labeled_reference) { - single_protein[, predicted := ifelse(censored & LABEL == "L", predicted, NA)] - single_protein[, newABUNDANCE := ifelse(censored & LABEL == "L", predicted, newABUNDANCE)] + single_protein[, predicted := ifelse(censored & !is_labeled_ref, predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored & !is_labeled_ref, predicted, newABUNDANCE)] } else { single_protein[, predicted := ifelse(censored, predicted, NA)] single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] @@ -539,7 +539,7 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, converged = TRUE fit_data = if (is_labeled_reference) { - single_protein[LABEL == "L", cols, with = FALSE] + single_protein[!is_labeled_ref, cols, with = FALSE] } else { single_protein[, cols, with = FALSE] } @@ -561,8 +561,8 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, } if (is_labeled_reference) { - single_protein[, predicted := ifelse(censored & LABEL == "L", predicted, NA)] - single_protein[, newABUNDANCE := ifelse(censored & LABEL == "L", predicted, newABUNDANCE)] + single_protein[, predicted := ifelse(censored & !is_labeled_ref, predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored & !is_labeled_ref, predicted, newABUNDANCE)] } else { single_protein[, predicted := ifelse(censored, predicted, NA)] single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] From 3fd691c6bc6a0f167aac2b6732a23d11e9f8e26a Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Mon, 13 Apr 2026 19:52:08 -0400 Subject: [PATCH 09/18] add unit tests --- .../processed_data/quant_data_srm.rds | Bin 13195 -> 13336 bytes inst/tinytest/test_dataProcess.R | 89 +++++++++++++++++- .../test_utils_summarization_prepare.R | 83 ++++++++++++++++ 3 files changed, 170 insertions(+), 2 deletions(-) diff --git a/inst/tinytest/processed_data/quant_data_srm.rds b/inst/tinytest/processed_data/quant_data_srm.rds index 9505b35974018c7a3003ef48e4d193ef0d6f9c72..e271bcab0df98f60842679f59320adcd2e168f26 100644 GIT binary patch literal 13336 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
  • ?)Ex7%2BqCE%9z%5aZAT zVJY2a5GR{qvv3cCM!y>dB??*avVP=wl%CsDzYQ640Rh!X`H~Hq;Z|2L7sM6}4UPgv zELQ;4P8-y6>1~*^iD1L}#G|4{5TRuu9-R_Odp;4aB;_Tdf zySgP|G_x;jAM2BDu5PA;nbG1&PL^yaio*qCJokotC{qpPdo%wpKga(x ze!e>$=L5DrBw<~IG7{bHz$w4)MJ%K(JbuP^``=A$SE4ivHM?YY+HMf}3z^gh4^7NF#KI7p3R|#cWoG?4#m7}9KF!p@KR#BHY2sLBYyMHo@gqY( zR9S0cZro@0JGFRKaeF8x?8_nb&yp=v+{r5Hnz~w@c0Dcd?duMQpbOIlNxA9Wy|a`1 zP3^|p1or$nkzl7_7a3ODU+_A1Q(*G?+@1&glk;Wd5oMpUS8`joOW?aF{5ASE4XMa)oUfwa z#J&f^hohmr-|hPmu&PtT?roc-<^4>1rr;)nq=O{}B#>)PGg027g<;CTP-5s?X%o{Z!JeAC33xO}Jzg}eP}0Z%Iu>;@iIdWM19O32vd zYa|L>FT8A--hv)lso;P_kkEFEeemoUxl;=@|U-G+h$rj^--2!d&0lc^79V{z8@v{T&mXw<3EWU$;6kT#bZZ#odS2Du#1c z#DYJe*Tk4h26E~0wR4Ig!e(EmOhY4W0{K85?S?gRa8OL{<46rE7Bj7$>?BPa&{IbG z(khe`ph6q_H3`>1t{+i~KYlYt~_v;3jnq5$jp!*fIg zWT`hsiri&h;ElJSE$AHf{ZT&22Tot9gu{P_)HwlXMx|9)P_u6_j=pkAO;084e#u~CL&NqX<0N;LsbE2sCm%Pf78sKs{a<>vQE9PHX>d>hAX zzMem;!m%JEZ_vSF#UF8{=j=UfE)k}}!Sq>r;vI3mMe_~y>ge-JX8Q6c<9o)=gnz(C zoeJqd3%g(Vn{U^>i_qTkRbThXR?QnLwZ-Yd$=>lUYhBYD?uZHZy(`!CwbT8z`;C*+ z{YUOAcfV^7!qYqT_=v^HiAjg{*2KI=R6=1@OHEo~M`75sgqhxO_vKat(LJG^Z}`18 zd}%^dOG{K(Ls?;(pi@BAokSm!hce5Em)xZ=Ksj4+!DPF8X!$73+Ajj2z=0c7oN!_q z^#*Lnd48uCAF+$gatn9Q8U5T8=$Xnh+Bv(lKWiEoowZ0R9pKjfM1{BItVn|Y`sM!s DY8|mA literal 13195 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 0 && all(is.na(h_cens_pred)), + info = "MSstatsSummarizeSingleTMP SRM: censored H rows must NOT receive an imputed predicted value" +) + +# Censored L row: predicted must be a finite imputed value +l_cens_pred <- survival_srm[ + as.character(FEATURE) == "F1" & + as.character(LABEL) == "L" & + as.character(RUN) == "R2", + predicted +] +expect_true( + length(l_cens_pred) > 0 && all(is.finite(l_cens_pred)), + info = "MSstatsSummarizeSingleTMP SRM: censored L rows must receive a finite imputed predicted value" +) diff --git a/inst/tinytest/test_utils_summarization_prepare.R b/inst/tinytest/test_utils_summarization_prepare.R index a97f2a56..ffd2632b 100644 --- a/inst/tinytest/test_utils_summarization_prepare.R +++ b/inst/tinytest/test_utils_summarization_prepare.R @@ -118,3 +118,86 @@ expect_equal( info = ".prepareLinear: total_features for H must be 2" ) +# --- .prepareLinear: is_labeled_reference=TRUE groups WITHOUT LABEL ----------- +# For SRM, H is the normalization reference, not an independent label. +# n_obs must combine L and H observations for each feature (no per-label split). + +result_prep_srm <- MSstats:::.prepareLinear( + make_two_label_input(), + impute = FALSE, censored_symbol = NULL, + is_labeled_reference = TRUE +) + +# 2 L runs + 2 H runs per feature → combined n_obs = 4 +expect_equal( + unique(result_prep_srm[LABEL == "L" & FEATURE == "F1", n_obs]), + 4L, + info = ".prepareLinear(is_labeled_reference=TRUE): n_obs must combine L and H observations" +) +expect_equal( + unique(result_prep_srm[LABEL == "H" & FEATURE == "F1", n_obs]), + 4L, + info = ".prepareLinear(is_labeled_reference=TRUE): H rows must share the same n_obs as L rows" +) + +# --- .prepareTMP: is_labeled_reference=FALSE groups by LABEL ----------------- + +result_tmp_unlabeled <- MSstats:::.prepareTMP( + make_two_label_input(), + impute = FALSE, censored_symbol = NULL, + is_labeled_reference = FALSE +) + +# Each FEATURE+LABEL combination has 2 runs → n_obs per label = 2 +expect_equal( + unique(result_tmp_unlabeled[LABEL == "L" & FEATURE == "F1", n_obs]), + 2L, + info = ".prepareTMP(is_labeled_reference=FALSE): n_obs must be per-label (2 L runs)" +) +expect_equal( + unique(result_tmp_unlabeled[LABEL == "H" & FEATURE == "F1", n_obs]), + 2L, + info = ".prepareTMP(is_labeled_reference=FALSE): H n_obs must be counted independently" +) +expect_false( + any(result_tmp_unlabeled$n_obs == 4L), + info = ".prepareTMP(is_labeled_reference=FALSE): n_obs must not be 4 (pooled across labels)" +) + +# total_features per PROTEIN+LABEL +expect_equal( + unique(result_tmp_unlabeled[LABEL == "L", total_features]), + 2L, + info = ".prepareTMP(is_labeled_reference=FALSE): total_features for L must be 2" +) +expect_equal( + unique(result_tmp_unlabeled[LABEL == "H", total_features]), + 2L, + info = ".prepareTMP(is_labeled_reference=FALSE): total_features for H must be 2" +) + +# --- .prepareTMP: is_labeled_reference=TRUE groups WITHOUT LABEL ------------- +# For SRM, .getNonMissingFilter marks H rows as non-informative (is_labeled_ref=TRUE +# → use_for_analysis=FALSE). Without LABEL grouping, each feature's n_obs is the +# count of nonmissing L rows, but that count is assigned to ALL rows of the feature +# (L and H alike). This prevents H rows from getting n_obs=0 and being filtered +# out before they can serve as the normalization reference in .adjustLRuns. + +result_tmp_srm <- MSstats:::.prepareTMP( + make_srm_prep_input(), + impute = FALSE, censored_symbol = NULL, + is_labeled_reference = TRUE +) + +# H rows share the L-derived n_obs for their feature (= 2); they must not get 0 +expect_equal( + unique(result_tmp_srm[LABEL == "H", n_obs]), + 2L, + info = ".prepareTMP(is_labeled_reference=TRUE): H rows must share n_obs with L rows (not 0)" +) +# n_obs_run: similarly, H rows must have n_obs_run > 0 so they survive .isSummarizable +expect_true( + all(result_tmp_srm[LABEL == "H", n_obs_run] > 0), + info = ".prepareTMP(is_labeled_reference=TRUE): H rows must have n_obs_run > 0" +) + From 833427bb0005a0343168201339367fd62463475b Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 14 Apr 2026 09:05:53 -0400 Subject: [PATCH 10/18] make unit test more robust --- .../processed_data/quant_data_srm.rds | Bin 13336 -> 13195 bytes inst/tinytest/test_dataProcess.R | 22 ++++++++++-------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/inst/tinytest/processed_data/quant_data_srm.rds b/inst/tinytest/processed_data/quant_data_srm.rds index e271bcab0df98f60842679f59320adcd2e168f26..9505b35974018c7a3003ef48e4d193ef0d6f9c72 100644 GIT binary patch literal 13195 zcmZX)bx<5m@aUP~1b2cj5*)%3+%4$huEE`Xad#)U1$VMIEEe3|-QC@J-@AJE>h6Bi zRnuM5Rb4$(Ju`pwhdc`D)BinS&wP;i)mNT7QIVywheuyJs7=c0%4+23Zxk&Pd4ffe z@rLF0%S^tBF(k+o@KDkKRfDMuTcmdU+~r%doiAE)`Er~;I_nk=Ztf}@?w7r)%+V&m z^K}m=S`#vok^{m6gW|6=!jc0fX5};ma*~o0cQf$d&>EL-Sf;esc34;R)Kc*jBHMo0 zxBuI~iZ(dH<^9QvA1J?O2VHT4@~tTlaXQ`|=>8;)Q9Dj@6cexwF<9#}X%XWUn1s^tkDWm^Ru9RnE<)DJ(f`*(VpU9J z?K)3nWo2c3Or%5SJ+eRj9XPk}UE8`_L`0TU z#B5<|Zg!q+YI1V&N&CKJ`Hn(U+8nhy-KQ3x;#!COxS(w&_|DLA#YAkv* z5PTgML1TdP5v~0pSwT}sB%bji#U@iOQb=^$bR|iiQ>U2gefbUDI=OGCz=lQGntl3_PyvL{IGLtVZ-?K`R%P{ z#n!eHXgjUxPa6)Y62m8Qv=?#0qHTv}5Ub`O9UNLp zZqcYa|EGK)baqajhxk^_e!P^w)?gssUie@wj}5P-`lR-go=gPO75-o9ePj3| z^CRi7#z5fJL*@yw-nmuiL?K@KzmgAxL&pBE(DNq0dfx*KP^XOGl__KKoVg_T9_fPMxCJPD#D!I{eN{DE9uSQ1n7e zSUR(=z43PnI!AqD6f&}5^z}>#ex(TYvuTcO0DmCNxkbP#ZangD@@coNW;i zazA}nJZxh_;I7`U^qBPzNbpIyN%?NHxygpGuTh>h=(j1T+=^yw|5L6Be|g)HXJXlv zoP>?;;rxCiFwO(_uhSoATRt*$ZHsHVcD$wd!)cyy-^{4yMd#D*R8^n_A9RU*k{Mp6#nl+ic;A8* zyX-rGY431Sh>P<9bm!lBn~oRkc(G_;M$;_tv<1YA065J^a-w1QI|r+9XC%qt9}KA*}KR1N)9KD9d`8|R95=6Tt0gJ168Ed(bV z0phRRc3E0j9fHcpTsNhAF)J>US?%@@`2)v?zqPIwKQ`f>Q?4MTb5Z$6_#?L2pT$;9 znz)9@OuJ&NeJNqw*9)F1S#^5D;tp{zfU!Dj;p-$VhOx+8tL21twZ3Jc_;dR{zB)_} z#w8Z1HFG?xghm9WNR>T3efFFdMXknN@)TEZ0e~M1ES()Jtb|P&Vn&wNb(0w^KE(=` zKy6mK)fTPAXndg^Cz*MrLspgWi3(u%T>@9gfpaEA-%A0ZAsvxR{H|p6FE`9i1H0Eo zgT81c^2z~-jrseiMwM(FrkL2Skypjn9?f2!y|y*~FIFWj9M(tTVgfWJKEDh&mRT60 zubSz*=+b(;hgyp)tPDF(dK%%18HfwO_-zse?JH2k_2=)h)7Lc-9f5AnWwOdwdYUh# z?oD&WO0!WKcCX#d=*Rd@(mB4drLsa-*Tb2r%>&2w$qt5Hq!3Bt4zR|i{r5HoPa6g+ z1n@dGZ1@~kJ1`2pFTYsz_W3^Abg|IndA#X{ofkyignoiS(WK&;bDU3GdU7R~In3tI zeWER&^XjCV>1;V=N~J|@*ihN>b zAk4gRbdQ9nrFtXA}6&Tv`tqVa@(*SX= z<<$MrW{kniggTT6(wAA%lb*}@aPyT%#WT_%Fy`v7`fCSydhsR6DhbT3b5Z7E%tc?~hHV&UK{Cqd(9=oyxJRJSj>>(VRb52Dp`8N91eYfrxyox+T zkQPUNjQj5orxMOtkkt;{^|DejSN6HLIC`?48KLq8sj%Y(5Tyi_v9u$TtyX9nk#;(I z&2(KpWEs;S`>5wv7E;?~qB1*q>w)t9MarOr)`WI-bSv;sCMG@0raf>K;s6mtL;Tg| zo__d`jCohN3eJu?Qdr46g!)7RluV!9UilIMUu>Mem!Rx9m~f&0-wpj1oiFRnoqI#^ zqB@O5+cUFC5UFkaq=jnw9z;s2wt%Hc&||2Y9C&KmAoKE6unEa_Jq95Q?w&4!=iHCc zPYKi1`2hye)2z`taLrnZ(?1+pwa&O}bI4nqy1PiUpz2`)obeLsU2JGOcq{P$Js_6> z6pqGc3sghdrUblw?XIkq94tGIZ~wl><3z3+c^l(S!Pv{Z)^-G2J0j5sYX>Ya*?j(N zI7FCi_HBZdP$(+v>mEkpm#FxvXL_qva||cD1!s72HL=D}h1Bd?UK#n4Omd$7z-g#m zQ@K4c6;xwwvPA%I3G~fW7B@Pc+1y^*bY9dgykn4)U8RsEX_-64k-Rbcl??o)XnEUf6TB-(qK)3I6w}uM#*$B|U+!lYX7_3#b_8;%*NY8yK}x zq1`Nw^7K4t$6IEzk4)WvJH1ZZ(-f-#v}PMPuk))}&?v&$uuxM}gI=uUvoRYHOnh$_ z^CCv$<3XrVEnBX8>Z(gNq`lQYK1f^~zH&l0d_Kl?NtI3!)0@~qVZS$1;dO`A1}%OV zR~yv9eh*zk@Bc5o~QnS5W0%jG_$!y-Tt&3OSo zlk7&PY>2iOf%|@~!~u)rHwTYBKujv&e3S2jiwToD)cbN%+T`|aO+#8_G=%rLYie(B2$Ppvm=E4-fNKU}K@zZYuFk~sPQ({0n@RAk z98h<7IP}}HarWH0AmFSHqQr(Y50&*>U zh$Nz?f|-8DdM%ONS9|ePd3RCeBd@8p7E=fQ_f6gAE`Dlz;+nWyUUWX$@%VTh(LqMd zO%Dv9!*HT%+&WGqH(%AafK%PWuS5}crSc-v{$H{+=0!9?LeRsjVX8s2j;DaDCH+Uv zQRxlN!&sAtdNFs6?7s7N%L$F5utxj9xJn z88;>L`~I8{J@qK547OH!_^#RA!}!EW0xO;3oW5dImP^p|_&U8*apSH>i8PfK)r0G6 zi!zBm1^CAKiiO(#M@21E?1hkF&(3X%+h3ib z)#}XDtIDsUUc1rF;kLlxLhWqO$iy?;c2sLUMZ`WlGRR5+kLy>-b!9G zNx0n>jM7yeIf>cs9%AY_b;^;qQzvO2@?P<#G|)s_gz=JGO67x2!sJQ4SXRg9khjKL z!$5MZ_rdzZSxrW#=k!zRdn-WsGF3F6C%RMX5)AH24S@T^qo9wn z2W8UCprCydm4F5Tp!jPA=U)NQ6s?(CW0$^g5Ae8v4}eeE$}^A*gGE@=%U3^k5^N&X zmLtGmGZa6m4v~L zv~#a&6+(H+f_T-mRtkro@WM91zYbnnu}LE%|4fE?XbBJHFTCbOT)H1NvY#H=87bie zw6t$7r;>3!+7Ag#sqICXs6X^fV})`LrfU6RQy?FzmD4ZThwEW~?7{oIBuHX}FhUse z6)|%=r&I2iaAMq|^Sor#mS<YD*BUcCiMO$S8)z>?&nRp>A*HXPlJ}te+ z1eV+=P9Rf9umBVB=fc{bWNj+5{rPp;`?*U$hhL44=3Lb7`k3iwjpSbDX$0dJ>|!K- zzBjQ&w>?RidEg+0+YQKEbaA$IN0|BLhs#;Ss&2dU7vTsrj}Ba>*ZeQCniR)~8EsuYc&@ zzf*E}L6!GQR#M&)F1)d8%DE_^*#d-~FBd!a5|fYdJIg znh(0vWjZ3he@BAJwdmAV-w0ZJ*5|Qyn#Fpn1Ppi=GkBZf4Xxm1>|jUK)$+%|&x<7K|Ak&8NmnsjZnh4-;;3btbiIOQR~u)|o`R35ri<<2;<(qDvS} zrx-J=P;MUeQ*JK{;b3Xtu^*4a8ReSZc|4k$2D3LoOhm|vYn!?`$2_7Qy(8XXx#Q3- zVt5i=Ca9Jkl_D9O1>{m5_=c1T|u=GyR;CwP#9fO%s!mRARWGT3I17)kl1Yi2F zUYzc$|MlH$T^&bq742efn>Rpm_+3dFVQmhqsEer#RU3NFe{r-O;MT7uq5iD=V4kml z);a!7ldf_e3?;ls2ezNSE;eRSHXP@{#p-%8XWV&^oXjNZA4tnm0$wIBJIYf3K0h_i zdpe~{d3ZTZWZ^*^xECw+EFFoq%1Nw4@df;Cb<&jD<=OIWSpNEf$&hZMB%@hea>Yh_ zN~l1PI;?mh$g&EWDY8D^BQw$}9%{46SxOIG)e*fptT+-*>ofQ*~V( z&QJT@TlUT6aV^F+y-5QOLfJu7$(s4MracLP9+5;Q-SJoGoS`PI%)nZWs_3VlDx01a z-@o?7b$MbyN`tB&e?jL#XU<&1jVQL^B%^f+&FUx^7Z~V8Mk<&>OuLEptzV$zW$hVr zTd{}Hd2d$F9S*Cth7(1i6rojTqkoFx5?tyjgeucR3nkz|WCy7fR&%tWy<$)-9g{KA zheKGSiC&J$dm9Otj9HkR<}cL$m3RlWT>Y=@Q@V>J6{H+@ujPIoGb9xDK`_LS7G3MC zOb%=kRJwP9)UXNzVb1y^V_p5PCa4&1ttYVb0AQEcGlTh+S+%>;z`s-?#nL?WFIA-n zpUSD6DTyx!P|io>aI_%V+@ITo9}zQLPk7$%o|@jL;Rzt z#8ka9>gOUDqXEkzr0EL&{%XsZUoH3*p1K)G`0^PRenTNHQ?J^tB^J9Q+M%r6%i-~Q zqhlm$1>9BxMp|c^5r2!ch0cT`FoFVujf@j^`_(o&BNlV_+bz%9D8KGcht_b+cJ+`x~2jPFGgSjy^@?8eMg zv8SZ^i5f>i^+e|PnV?L}HC%x6_A4dzb`Sp!d^(a4M4YED4IP=69Zq>Rh0?nq=4sS4 zLCUhlH!I;=gbBy**e225$AJaYQJ})WpD5NNIIbZoe)}5;NeoXo(Vn$$|DVW$Z|psh z*qNAQ+ZLz4I`ANFQ#Hv+;ZIBslLwQLJi&q{*(!~kU#3!+(xFfV3cIHFRM3j~*5J_! zpMF?4AeQD#7JP{Ux4^}t?r0$Fl0TpT)}ZjaD~_o^!Q|un(ZTwHrmU%eK9Q0rHhZ4M z33|~&Dl^FHhDMSrNy?k9J}0(W|u3htQ|3i?;~llS$p!-)PaOuq3Xm55PdOxVWblik9!?lY#WvqjNegVI8*%qOaqnKpv@}Vjk3Xcx)f7|>f_(<7 z^-%@CYfM($*T-RaJ=y8EE|LZfcKE;_6BGR{NYro?J;vXj2EE2OC1ZJb_9!^f3^*hd zJWazd9%vN2U*Qs9H<_aBHb9MYV?mi16MWR5OUq*cGLbZ}^8*8j#pKaG#rS$5;DQ5` zfZw7ozL7ps+{lIUKC)Zx`qZ7!)%JYpH6D23X0cQksa%(BF7|6Q#y1KR(Ik!uH=S1u zX|{-g3!Vk%2o`c&=foro3Rfns_<-Tb(X~d2@>cw9`Xw}`TGTB{(lUBVT9ItfNq^$X zzmlw{>vxE?u&Fjy;gndw>MZ1hlvduK!Q-_m%U{sMt0)m%oa_~1CrHgmfYz=^bL^$41~`}Ici$?8>YHcGMC1?W z^Ob%t#qY>ZoI^x$T3M?aAc7}swQhGKkk1$kb8vL_~JR5roc zN&C=mpMa+l@8gwLm_`p$+64WdEbE4QanO%9kD@P>^s+FHIC2oDiM6{r4Vga>Tco5q zIlR5j-t`@zkRo?dgU?4N6&V7Axq&ZEb5&18H55MAT7S=CXw&#EOJ6hEc}&Dd(vN8w z*xYrNXdFANq?!tygc^t8$XQ_zO6y??5$iv6hLU@k4~{nGin|>RDJ$nn#kLQI`Wr zj;jJph1BcjdM$k5F*J^16=T~J@fj`B39#k@s+#Xbn|2I9JT!6>X*G_+EEqtPG8jrS zAaKT<&i7E(W4POk!+6zY`FyqSnXKQ=raQ_w!_t)leyI}cb-kq_!sZ{^P>JroV`%%4ExpKL^Q*z z(p04g)&y)uawDZ1317*8_lGAAKy`CnWb&l9%`=?q#VQya-99m*I;Rlt=!DXt;eexD zuCTGEMi<1#2h{0Ng;!<_@n*a^|8<_E&eu7x9Ix!Pzf{MG*OnbUVJ65DHhZV9&P9cB z*~|$%S&B7!N3>ekz84-nCE>LmvkrY)OL@N`UkF+eA1q;~#=AsP)|bS1u*NX7BgvMAw2~HQ{eE)nKxekv2d4n@ba z++;!JWuI|CQs%32)^ZyQI-G4dAmp+MQB`=@MvGO))T`_1sYvzE@9NiLc-CMWum(q} zpIAv%O(tkBSilCTp(q%Try*MwII}OT$%k;a&(F=aX2*jUm!AUZk$!~Hs(YJZ#b-3@ z6_@G4XorL;VHxulqM!Px?(Tk}Z}IBBo$HIg_BbJ*8H6i4`_cn`v87PE^~hV8B2+$> zV`@q?1&sN2NV}Y(6h7I>AC!@Fy&~%5fSSksagCq<*^Xz#Yg_!T!M%i2uE+_#V)qc! zHi2}K>TwsAjIZ^6`;MV3ys5w>%|le{F!zsfRwQ7Su^c%;-# z7Qgd|rTwx@dC3vs{A=a%_X4a@8SqweYZ*CqpfypWodu%#;>*gy&M82NNRi8Q-evS2 zv^V%iRV4qBE^6`R%O!)o7LCOYOa%64$2wd6G@k{l{kvC{`1lY0)4f&ymd1Wwwfd)c zO$b{6BLh~J@rAk17rQWVgR*{?t%Xg2GssBS8UnU_6ti`VTY!B=`bIYa3g90tYd5-% z+|Gi{{w8){uV&En&Amgoujkcej*cKT2?`MjjW&R&>iTFo<#)Uk#`BLn3WO;w+MZ+u zd0`_R2|ovaMrzgMYxmwbvF;cyMWZKMDNVXD!Vzx_RJu6Tr!J}9u2c-ed%ewG^X=a=TMejGV;jH*i8FHsj9T5~r$m?@A?M>ly zor99cdKAG6Wrm7ITB6<6>Q-(iHCugIaeVS*wK1|JPH8rU%0*%bSFt)HA zEVfcTu``L(5qPinH(@w0FYjlW^1>g3JR4)287YF7T^m%qM2uHj%r$0+E@|}?QiXgz z|9HFM)a!1*%yYWGA|`82W$pkzpF@C^7w zV_ppja-T#aPOV>7d3~FvC_TRX*TP6L%Q?Y2lwYCr^I}+|KotjK_ zAY4#Bn%Pf}WHqSOcLn5`)Fh|Y2GU_2eHCF6mCj>kG3;P#M6Gw>&G0xeSDdI$GS|zH zJ7ksBaA@rfTK4HMFYHIFKYG1+Skp$;p8VW50V9?t(HPaS21TAdh{Aw#cL*wW`Fr~yy>M=-wb!o=G6j}m_gy77j6%Ma{=+$t*U?`nZa>1U-2 z93X@-tC*iwq42@QRmGEBbph(DJ}Xh&O~`!w+Xj9Cc-xhf)-uKqVtI%2IPPGU$*i<9 zRxTIOL#6gq|4H`ACGg7PJH37-G~Uwx1bRYbfBZl)S8kc0Ub z^vuQ_;l`%&9-4oX24nE!EGlmzYm5B4Xl(UGS`#D(p;-kscXw|UP&ENcRb=LvnLdG7 zRkfyHvG={bft6TV63GT_CTl~cnEX^P*H3o&SxUH*2|4>@A9*uwKIQ8*OK zG91!t|70D`XBlQwL3tl^RY8o%r6f$uba9UtgJ4VTuzo)*H;GMt!)ikDcOCgx0{iCfGgXsOJfnqa$(pQcY#{b;9P;6L zc*ouH?5iv4+v%zqB4#`QM0Lzeh8^O2TU~9w!-sIS(9MZgrI+`sXfU$QCC`@cPwVq< zq0(5DCXPuYo-zma-qoLQvuN`J-hF(4_sg-Kza!pxOddJbKhgXj<*d^s^#k%&vt4t` z8i}{fcA(0j7~$W!&W^8tFncB4S^gB!VS}sZ0)n6x|8=gYp4)m*lHcoBLSWY7(|r4= zYFIF4&qfmlcoP8-I$+;n0&edvX}&(m144s|td7ixwPGR*$-)WcZu1kQ$KK*CFN=8) zS(!E9u|Va;4nIn?a?ff|WT7rEg)EXB(~4qwM88ZJDq64Mt?EewurHV33N8O4{JJl0 zm@)`dlnZP9O5-FVOGmgxM>(JHvLA8!m61X)zQHj$;V?J+`f?6t8?#0cTdTYS2A54j ztV~Tbcr{!uRj~o4LE%iIUl^^|+Rf$e@9#kEb!e?o4^r=*LY16;6%1sU`TTt}F)zZA ztv|K~hGT;vyVeS?w|$=F7f+Ua<;nrU`Bj`)mX$|o*w4YwMPC(M?@>khyG$F!r|3!% zZ;EfIK_R{K@sF`*z2MH*Z%%o)aJ@ZDlodslfJAaZS<7{bQf_Jhu=#6#ZjT+f(;VSW z3*n-lg9_7_gYFbMEIfE6Fc9~GC@LPobu2Te6~2}t;{o>l+?!%vx`sZ3c&9wfnW?ak zsh5$0=QV&e>odA1Sb{EE=w$Rwr2zFm^kM!f(tyIFn(w|p;vmn?S6yw#lf-DHIa^Pz zP-IU?pzd$#7bTH%inG;+ym6PxKg2Gfhrcv7u9?^JI3u%3NLt|7`U%i~Lb!x?d?5dL z>*2d)ybSsdAfh%Mhe~QqmY90v?U#^n4vU-AIrR;zfW-_5?))j2_*2iA>kB2|wK~sm zv7kdzRzchSy8^> zB@X>WxkmyvzL^gkc5F^L^5yGQw_;)@!bdfsufqX0F@DLsRD!p&L-VzTLi0FoSWJYOq zzTs#_2PenE|`)880XkFWl32eOjK<4E`}NC?FZoGJiDYrGqpaAeK%=+>a24lu}$qP(20zY>2L{;CeNsVzsfj&KtsGxP`&Lhmm` zGiIrfxETPUvn7gJMA}9N2W_=pE83q-!HMzkdH?A}`T>9t`k$o8hroFpu&jPEOWX$vW8UH-`}+3xe>yz&q`HXIzK_O-M68FYk42N-LlITwgl%cq1>XzG}?uik#KOV)}6;cfCn zvcX8u(HD)aIuSgiy#i~Our%rXFUA${f0`p~u&38>@Ao#son2u7e;QyLc*VWulOmr8lv0ojqeyo^`K#l-;-uny7s=A-bIciSk z?oI&ewb>GP7F2X?b^I<1Lhwan+m%bjuzB)4o?Q7CJapscn!t<=;pC6&b=qzWVNYO< zS<|+$Vs&oI15Y7ba&_f_{0@c)WQWC%{v5qLE6y9yn6*^pz`nkc4$)Qm*5Rwz_ulal zUCU&1z`n%P;EB9wtdI`S`aEhTull%s_L}CuqvuDxc2yAJm~O^S8G?(1I&9IPa4061 zo@K`EVc(3z551#8Vw1;tD6Iubw=-Fj&pBZB1}U=z40H18D+}CFaX#-EpYS%Opula= z{yZ}y2ml{P$~DZeO|G+WdF(D=lry;t#m%FHmw+#G>g{RnYqGOPj{}0&r^k3r4G~Ip z$S2NHaB1#czo=PiAkvvW$i^o(7-f*toIj@ z&iY0sQyeYgO&UCE^mXb(cZ)ac6^hKGVpu@)vrBkqiDvLuI9bJj^sWMWe|Zn`1_Vu8EwV^KfMd*2BFN`D7~agz5?3Vqy< zuDyzC0^g!BYJutQ+1uV(>Y;P!C@jOU-NyuiggOJmr;h#f5#*a+2m<{w{71N zJtaS|?_WKm*oPdCq*_jgiFm5C+;qVKxQf}#Vk;^8H-v@rMp;mj!(0p4wVH6iuaFn- z+;$Pq-Q-700?5RXE;QRMezIWBNChgt{`oGy{a)Y>OEL0-&g;iGjH}Jx-GA^#e#vG) zUas~}$%tDTa%~3wPMU2k8;NOg-G4wQxBbG>bM8cBkq8av`G^g&pOcGOeWJnwMUd5q zGSoU^H6?Ye z7ji`z+7WOea;ko zSg+eCgLAgt+{Ed&Ab@1^8|W^$N&o@u4y3=Ghj4vL;Fu>*C)E}f zSW?OWrJ^JpKUQf;yyiV3#GvS226icXv5@`=$upY^PHhLHD3u}aoVI= z)$aa@<#%M51F-kRy|Rd9)SX%b^R<+|H)l>xxTRT>qs@H|d74FAg7J5wrZUCRqp=M8 zb~af^no%#7!mj6ie{rI1Y9~aAidwmUP!hPDyo{nm+boE$16q&a3SjIg|L04>PTNZNr~TLYHOOeb6|3?uJ6;+#z?b zu4E(V+sNqiWE~G~Oc|muZ5- zLX62H7r(0yo}&ln>5YTJ+zIKA9D}#tzIPWqM>Q_f3P**0(?X@|ofp=(Q1ZzOLEaCo ziLB1tiRq0VwIdCTq7VO?DFeA;A(&e~RA09A!_GU@g4lkByP@VkIgQ;yyH$55v-${R zZz5Zsl!==L=Q;^$Y8da8hIIm`8sFW8R5n&i&o=8asuSbY16$n6fjreCY_!Ma7Ol7V zmrE-*%SdRfo#|kRm{codQ^AVNuj%j=Spz0nc={YI zFdmzKW?$%bphR})h;-v9C^nN1q>_N(LNb+xo zaw%GFl7kjS&82>F#Z>fiivoWeLo=;^P=gT8&ux!FcUj(Fa@1_%f7JO6edrP;@)13A z*!2rvE2jNRLBN-4)y03nTS8d=clrG1ooR9&&Xu6mG_YUks=Rd`L-ik0OGiIY(O{aK zP(bg?^0by5t?rHpJ5=h#Zx|-yGc?e+9$0#|R>Ose7>LgnvPRcMR1t_NX^|QlZg%^eR@N06(eKyO0nfYLpbR?eM3_XHsi3X zir%UvuVu^`ZBy%u75q)!!~nJ88iD&@@CE!G#5eI`5K*xPEz2Dz9nGpaC5gEY;l~x2 zg)d4hkMrxI5PKs`Ltpg{?gybolm;BoukzDfGT>m;s?MM)S$bi-lZNs`>FO~qvY*FK zx4kgx;3F{Vpp)M1>*4$I#zUN7r{C?%TfxNz`J3OzA*{Dk7wuI6(reK1Tc3&1+u6|V z#l`-|DTQ2hmXOFRK~UHCG8E#K^Tq0WW23z|VK^rqqs!73( zDd*5tn`~P>{%_=*vVN&IO*X{LIQngU$2vO(T1Y6Eo22FH0fMfVHK$F}H-Q zn~79fS)7R}$92fa4r|<#=fn#nj!^CnwbQ+u^<9xbd%@(Q`bT~ZpYAnR!ADGczX*O_-UP8^#GU^Mqkv-Md=t?zg}8 z*s^?V$+9e4N5^_dqTs&#uLE`A3or2V?4btrt@ITHC5b)PbsXyA8vjL(F8 zn*Xgt{C-PSDljQBA)719B5L8af1qy75Kj5Xp0LbsQ1A}q=A6@dp3+o!$g=CAVa}`^ zA6wcSnUC7;jvwZW_$+)vP4A_YdGm*Y`Y&i$uT0?JCy@_(N2yQo>*}0&(SPD_rhY-& zs&-C8OUrP1W?kyQo2w-@qqr`1Rc<1M4@W0eCpDCTI*P}&(hZ`Oo|##_z`DTNqWYdI zIMA!2wbd&oEj_XgZWER*%!_|`ya1N;;Ff$0^}YcH@-DRE{U&<={trGcg*PO%9`6ND z4Ue$S!Y_}z(+>^hdS3ugU_+$&Po1X} zZQ=Kh;;Gjx$F8Y3nAcB6jZg0oL_a#LH-a6%EJH7}9+zQMgQ3bEui-e4>nvDUE@2?? zXM@Vc750Y;(_;;>z{=Y(_^sLU;|fsr+2YCVpW38+D*r#|m1SFNb8655x`Z+7H`3>s z?afwH_J_gX#UnXq&(Y2&ZUi-i>VMEX;Ql>@=^vL-AcNwM@hv>y!B}O));EO=&sYEP zb$Ij5@9h48s#(7mXwsoSZO(FYFVj4JQ0o|poxwBFjXKN;}Td}aQ&{@oMq z8e^NHS6SpwFXOks@4;|zm}q3Aa^qA7iO#ZZMS5z?bZN=*|6M;k#@|!iSYC7sKMc7K z{Lcty7(RD@fE1j|lh)T|?Kdg8{S(!*_7`Z{Wbx>e*fglkKnmk38Oa4~bmC@eq2onJ zDgT#){(V^hn2sysPx=2{mTUhze~kBOX;fh#-RB=SrAG_hBTAWg(*M=e|1~6FS4;KM zA&#JTDtFQHK0vx{%KQ0Xbd*LzXh(Kk;c}{An(|rzo{6Clz2S&;aT*Z@9w9 z<*uPUQ#SnL1aWO&N_z7wchZ1-@kS19j`=0GaQb`w@cA^ueb}@60{Cn1AB$)6j0tZC z4uKG3xK7)13~p}vYE#R6P4A3&+}+itr^%bUJ!lpS83|~);a`9i+;<4IQ}|Z+1e*1} zQXI5EI^qCex4UjfOr4=tjI{mecA?c2aVLQhADB1iZrQytNopZJ*ul+P#@*`04Gf?_ z9gtAYF!^m-dT|aN?P+-1>s@4xNt}16+jd&WKpeuHbGGRSo99{$m!_;fY?5Km{uoId z4T3FtEYlcp>rFJ$48TrYgH0;e`AM3iUe#lyk3Wamu2)QaLNROQ=d=FLevrRjM-ki~ zalfgwa}$nR#Ml5S{cI&JBm66x%Mhy4NHzJCekffDS}I8&gEx*f<%nJxZ?Kn@ow6g6`GN_C7Urk2kY*}s_nk&_A1&L)oF{(wVl6Yn zMj>u}{1Ga1yxTvRYSrdnH~v&ZTyBeB&25r`Xb34UVE`H?n+? z@_33N4}-KKCZ(lv4U(7M7iy*X!JMP^%iYQR%9r6QyX=S2>QCtvR$<161-SN! zS6tI$*7NylbdT(e#Xrl@TliEu%SidFhv7eI%z<_c-^caV)ZJ0)&EKI=AR z_Jh$Qn!Q1s!e2Uy9nj!3w}>js z$gC^d5uxZ%D}!YS62^?*PqXN0d1$68_Dh&rCXt4Q{mS;9`38o z4D$b;7>~Agr9MfZAx_JDoh6!pJIzwasQ=>gEE^5BTOKz{BM$@)!ike@Fum&dDhC8k zi1Be~WHR~!-I1A<+)yH!&?PLwF5ic#1X_z;MA>dA9KGUXjL zYwn@bF)@io#Be?u?TLJy@2@LnwRnbayqK@*ig{i(Go9q&5o7DG3~*aCNe+hQPn3pm z{{Hnyh|#SkLb1s5X6Vd`95AvlP-7nZ+ipSSVQ<12Ic!Y%$aNuwzNqx1lgU%QsFcn9 zHW?^(aM|`@WAS&}^2LvP-UNh#cYX}&i40Ukw(bbtRc$phQJh&|0=3yv+ph9m;bR)U%WvXjTd%obh8#D|p4e$@rN)5>TibONsL?~tOG%2-QVOq^ zurI@*bU*^BCS9MThzP(Y#f>Y|0+F%R)bUE#`2%JGt~S;Dnw$1q)nmM$Kd2oa*AE6T zO8OB@l&CFrIVceUTLJB`9dwXQd&_PUl2vTLmnyP8sfjT39|9R|Hd(IdNC(4=v5oqF zytc_B1OphQaQ@x~s2!Vc~6))(K#0wFRF&_Ebjlmkn z$mSXcW}~UGc_IOaM^7hyg0wU6$IfcYQj7XESpbKO!u+AXz<@|R1oD_6Zf$LHn}qeX zy7gC?=`B(Zin=eb261e4ZZM=3WaSMvDBNtzb`^@3&SEbNE<|++X#0^QC<)cJeL{xz zTpzfcrnE@UnIJoH(T(@1)FT6OFTc>=%CdZk6{UG5?M!h3uVTzvBLp(76h-{r&D4ot zhz$aPWC8%ME!IegB{5aUAAYL=?uPy0hl6=$?7BI_Ap-*!X#!`@#<4XahJoZnaCJ{0 z;rwCyEwLyiTZ!IW4-XXlfx?m|w+DI6?nTy%wGrj%TuhseqPW^oS(4an+h|UWo8NRU z#R|jGFM*2_3QF3MRiByBV3VWrZd(Jq6)6Y*-jB*}&0I$vQm5kEbZbjDw)0x8%vL+J zztsc4kj%PJ`3%RLvzg6oUnqhNbHidR+1_}X6@4&uAv0n4zA!r04Ja5EprEMl15^;+=^r%e7pHa&zp8<9CKkY%u+_x$GKnERQESG!lAP(jUZFlIu^S zPmE}Io2%Ye%=`gZsoX@St|I)^n&ED`2Vn90YmoOf3z+MB0}ajwt>feW4Jr3j`au8(VcV%Ek1Y`!56AO z*f^7Zxv*lo&nr)Qce)A(W`1|(LI#@Xel{K%nsU}HemtE~YLZ}OKS`tqA;y>g=uxkx z1z4?F9KE_w?rwW`GhP)>*BUz6?^e7zkG597_q{)3V;!bzxIHL|hAPdTB`0Z)uDm_3 z0+Oa_s~2*ulQM%FHC5$4YIx#->JWyD1pY)5ZaUDJH#wD4PCaBdM#f9ZYQm;V$^m4#TC6!YK*I0b3SWT$8DrHLBZ0;QP|vRCLo)Fo+~kc( z2i+e}Fi%QYM@YO(6{5)+IB4NkSAXKtg>23xC1RP7Y_Q@wH`#!(uq2X(9ss_48Qp5ZyCrw zJ8`f_q*^bW)1X%P`VK{Nv{>lKRHRi+m?cu_$aJ=*%ayo*jlPo&i)_&&L)5;6xj z*T=6XM&};L^enX-Vds9ZOKDTHqibvJ$10=VrdK)wjhz(ORN1ltoW%0+9fgbJ>}=n( z-!6AzyCUHf{Jku17I1=XY5CSQNrD9fy_)zMVs-#GH-WXrBoB?0!u}cKUwU2zk&j^Q zjVyGRZu*o-wkd}vjq0S(I;6EWN;Fm}_WSIK%UBHHOk0?fE*iu{F8iZm`3$%1cs$Lw zsQ5j#I!j>4ID#v^=KRDFk1Ug{3Mc8lrnj9nR?L9&V-($s1a#V1ezc z&tYdSmnDCD*ztZ`lJ1+1l3Z`_r*>a6AdKU!iH+tjPV0H_Bnw*f&hx3HW5ddDC&Ui# zH88J@R!vVJCVkF^%=%D1`*|`AR)p`{XocmS7)Z?ZSu`j$q>(ut4sX)@10>9PNo9}N z01`vwsvWeS(CKXdnnc2)koT{yi^@>J$aau3ahSi5@tz%YtOxVL#q1v0>8-?lKh>HK zdwB?--|>FAmx0zs6_XetgkNeA?nXP?$L2AhJ~NbM5wZ`HfE_< zu!-|f2U~ zAtpC(CPnn-B2ZWTBZIZoQAp5jmd3AWm^TDWO#FVnQ?zSHx!tZmYWK^TV&!R;&eCTE z{ei5$n_ILilQ)_cIafKO+4j#vj?*>=LkIsR!GvXPzsG((!2773=S%h#FlT8U?uQz< zST+J2XiMPu69isSH~mAw8u`bc!!g>{E2Jp&?3aU|T{FKrLk1N)-9?-AuW?}Hvrs-| zAIsLe2ohN_Ku}GvWGk!%QrR4gK@(II-I3s+gd^w0=GQK5@ju zv71q?bxk{tP%wBwbqb|aP~tOm-Lz`;r^x-CLdYhb*oyy7lf=iKQxVQN(Izj1P;HBN zmARG@o;8z@A@opCq;ntxFE1~Q>}F>^A(fIiSk+XEK@U!>pLRYZtfhyvq<`B=aq?@C z8>Ycf|6O=jIfpLj36tCbuqEuz0)?Ij5Z-b$<4YV2a{Xo4{M&*0)!x0s7tX9z`!|pQ z*Qq8olgIp?%)WL-^u-xUEmTrajxmjljN2zI)d>w_buI2n{|Wa5R!;tJ%{*GqIlK7U zrkxC5?z#lH8@)_R$$_6nDlB}prKAeGw%?%R!tErEFn(biuef=aZ8uLEIEJJw{w4rE z$3!&$y;YH5bXu`G$Y=FA&xie}=I+HUeD|2S z#{m99OC!}8v(<|{j+|1EWsh(p4Ssi-FZN!^ajCZmotKv|H0$7~x}%%3xjN?tVn(Z^ z$s|2lic6#*UJ|d-#We!(%TwJ3+l@6m97C%G$p`1Qw!Fq-FeJh0ys(*0y%9qXf1^m| zh(p7osHqApAF`S^@7iSy7hm?r`&~`54KBI{EGXpGJ<>K4szs}Rsp!SpAk^!Eys9TU z;e{neYpxd5&(nObsRP+FU&gC!J?Hen_@rC|zZoQ1?4oKQF!^vr4+;+BFCV6mp2i*%8gG-44||LT z4}CjXO+g`u)<0laZCsvvR6B5)ArL5Ouc1%pysBmS9B8OC@2DkvG<(0JjqwQFXC7 zJ4$%vbu*_9i>?&O%=hm!9N!H{;B+MD?6i5e049~F+Tc6mYe3#@R)`Q79*Bf0h2pBP zs}xPv8jf5qC=R!FRhdQi4xBu&z%l{iqnCgmCs(7>YfjTJh4}a=)ZIyGsBEFc2DrJB zud8X6Zc@k!FU;Ud91eV(X3;(ZUK$^rmbYonuW&p5A{&&KX<1XY))lYupWGM7q_4GS zcs9bDOnj4a@Op<^fq_wa-6nx4y*Iwg)G>TQ1-}>`eeYXg7I#Z_K|i!PGC9phrrfmv4^YY6FDR}9WEyqkYp!Sj)QomaI#=Ww({ z-S!T%yg1vKaBV|()z<3NECY~5B{SnA43jpwW+qs73MZn-0wXylMVvQY@Tt9xH?^XgczAB)@3!NL@Lf>|W8Hq+;*T#jPK%L7I?I$A|OY?YewhoTRf17fSC0^zQ$ ziSbx&s1bo+R~}l`QJu}=<2ruq*$*&2`!Wxy546Q2aEf~0-z%+1iye9vrMM+TAvB)! z?Ik=9VeLO!L;aehBTg_ZhXEqH+`p||xfGeF4hC8b@p7y<1K_9}QKS;Q$wS@Z8IIxRjcl3sepH#zl@YIi~*q#=X0 z`%Oe5mCC%a{XEJwbokr# zjH*jnjS8DGI?!<;hStCR+IrTV)+!mGpPv-68}y<>*Tb#iGi1NWS&JqYEtGiC1XM{B zcC(mTwWmF8TRo;j6k}DuR6S%=VN)QOf@B~MRl5)w!h5T$-jbJ!#v&xUo0Hu4U?Ugv zzM*(GK8VWRA9kuD6T`)M%{rhRd~qunM&7xT$i%PVbU$DKogx$um~-X}sO)#@97EC1 zCpzX3BY?=mHtFFBf-+ap7t!|NPGv9mTA-dp8Lmrf9Fjt35@yk;H+)o}s+l+MTQL{3 zsFGoqxBlGHWr&~V0BBSCrnE9vNZCC>eFwQ@rVm3~bB9R*3LBjTfXrtVJ#XgILz>-> z!c6>gM01f@#p$xyW)wLr)SpUr^0f31oA?C~5hpNQQWOPI!j*qNjU67Ayg&oh0S~JM zNH>7$7ABdiy!*-=DARaEA-HMFOnXy;4_WC9^Wp5u7?zbBg&nC@Zy*Zw*>^(^U6n5Q zQ7AB&qltL?)6FVOzmcq`-@!vkone0~v%AYj80}UoP5;rz2jQ7wnw+pkFd%Tys)XU{ zjC3n==yin|^)~2PmCQOX|A)h!<9N4kd`(rk){57cTZ1}iqdA9ycz*MtZW}19k`Cea zWHtoo3%gfzOr+=32_0k{gZQfz=JDe@^xGU9jGN4g{NFy$!HWQdea=+H4E$7-=VPzT z-J|#X?F@m_7496KE8yFQ7J9sen;f#29b5?ULy(XaVnP1(ZSmwuhl(SLXGZZe-nUD& zrc7AAKCd!ioy8fH^%}&6pr65FuA#~=<(4qlZYB4Y|5yTMGE>AzQg(aKhEnIP(W>==PP$%U5RNT=jYxh5$t791z_UO*U&2HVl5yIAAbr;)DQ{ury zPoVWFOlKhnag3}CQmqfq{N+W$TXv-;nP}gT^C0BuP49Yf`)d6Nony?=sA*KEzfl2t zF9*7O-mPTb&zE22lGyjz&x)CsZp7{N{K7;P2=;Sty+b0=-Zjy*@=eMDw3XMg14+(; zdXjEdCr3*Aj#eja_jw{Sa>QxQpR0Op$na;dkD!EVzW2;E3_h1%F?j?F^;eqmMIqSH zT&K}iD#|iI|M$&wcb=YGw%knEKXFLh!S@4G?Hg0i1{88K^E`Dfic#i&ppC4S!AxGh6;lDm&!{`nM!bR@mO zBB_IUKUFXS$M`zzZ9)5NB{;%8W>fV-cTM_UzH(-w#9;J_*yUs zol!Vg49FUHn^t;7s?UFMW)lmD7n)@zVDn`!vY>(^j@m#y!L{0)pMEFR_h6oQnuRNp z{K;Xb=K?#YSrDV%S|Z-Q`z2~WOh-lwL0;Q#?W@%R%K-m+7HOBM9leJSe$i{WYe`gB zXd~DM48v|HgGe7W{fDAxBy%BmDlf6alsZ z8+T5tSf*FMAJQxaUgQX!3Oy zvJH)%%8f##Ib{h3e6u)lAa>AmHJxGa2=7vYCwsfP%3^Y z-Fru&P^QEyLhP3CBfMJGom_0LGR>NcK|R0_wkT|&DgD-uikfa^w}J3=mP5;ahwZ${x)FkUlaaEyPXH7#~{%mFTNt^=2Vw zhVS3h^BK%!OXjv9UVev!vVqsMoAT@IHZH;S-#GTvQS^< z6yg(9e{*B8U0`^267XPqdbCE!d%9h~%7w~nacKY*=f zA}<17rS6%QfNDgX3dvG@_(Ppt{~Ek&Fr#FxFF)@LYiFstqJ{o0e&qfNCDoJE&rM1?|ic?^hHb`&2%?Sep}`} zalOm8DvnAIbraztjYAX?+&1V;)#UbQ2rs$+N1&!OP5@Uf`9k3(DxConT|hhjL3ofF`2Incuwo_JJ%#6!GS%odRS^m%h3<tE zx+LI*`1hx7HLi}ii0hg&!n+$+c$)x*j&CS-{{eTN=e&#f{cH+R|KI7}7Qal?=~4zX zZ1XGmm|O(a9?c_dM18X6*Np2>HR140z7!E;hk$TV?8VwjtZ#X_NN0nK;`^<;6Gvb% z(mn@1p21gi45$}dy9yLhKi=V&B8!nl6*s{-;yK0RVKkw2~6N`!mIz0K@z{)1K5C{gS8)055OCL1YI?Up|NU~QNMj1I!_db2@KKflA= z86J@JxiWGyYG8Y*C!gVqhRSqL1ObTeKS+`l5i03JR)`LYdNoPNf(>ifS~K$u7txa5 z2mSXsdT#P6qCqmq_0+)eU0{5sRn4`fG;UGlux|t76e(xB+gB!aY_$nQs$37W*S>p1 zp`npv_Z$RDdOP4 zv}1w~l60~$Yqce@sNy&J%rcIyOcH=AjTyOW1y7ox)Ne zH=4KB8i%-$N+xP{0Zl(v}Z*|M-KWWzPNh2<`Q1$+jStZhBZ_YuDL7WI*q%25JXsrc- z(|Cyyi<(ie1foL*RyTc+e4&X0!w%!%mAqyWJ}eBFF$4T~vhc4+}?p4>XWlhYj(jQmE;BO7HtialZ4p z&F^;Q$-?$Qz6s~)uD6RJ5eM2tG`v8h?wL6#{3jQWwmt_sRlHwTl?49NGZq*myc0_7 zK@)@Ks++Z@N8@jLysWkTAz|0E4yXkp-{2vT7z|yD%#kYw)lI?d>S*1YBa^@$Bv&Q5 zlGiaupZO3!%26)`ls5_PWW*C%GGc9+$vkxwVwWBnz(OWP%pHoDXa#Q?YvtHgv?gx2 zK~q#OtJ~_GySgfnHEUf1g|CX4M&-j`>Y1grvCPVozxF?|UBLUTHj z<`q*sF7Yx}^wgIatFDoi(vy4`eLkP(btaWe{uG|%2uKOfX8g1ZKZqk@u8~#5`RpRO zn#CKwFmCaLnWl}`$=XOJijGg z)*2OzO3yHdsoXr~UIOqgHvx)v``@(41|?q6@k7D!k9R#3hek7Y~Z9a&L`P zt_b5QOl8Rr#h;(|y9DzuXU@Sn()R$1`Dcv@6_wSl7jRQJLZU3| zpwwu7==_w)E@iML^0s6G37Uxl zFp@>9l6}vK{EJhKHQoP6=N5I!Ms; zb=foz9d?(l1oxuauWp_CdBBe*0e(d?W{Ao+{j#B%qu_+V@O*0G+S10Sv8-1Krb_0p`w z>?7mpqsY{+^}`~zZ;>lIG=8uDg6ivA%O(4Kw$ngWz%d%DgfjFo-@xt`s=T}A=3#Koy_BQ)Ai_igk6;Q)w)KPcg>Bm33wAD zB+%5~4w(D3i&u`vSTW?OsPm!YacJg8TDO*7o~OJjn*kX40X`bDKfLnK0tV3)58 zLBnOoh4qFT-rrcH12mcu4?Zvfox_xigLAq_9>m^~=Axde5aQE?J0eBR=?3SAJzVXK z{v1~f|3-d}3HHUkqs(DN{sxnq=o|*epP)2tQrL+Qyd1(emZ3uC{JQ7s1PdEpQ~No1 zK8J7+#6U6FS48MCm>i2GbzORtE`?z}S$kR<{??&BI`r^#|Q8If*zw?9EQBm!Vzh zjx=iOlsJJS|%NPK0=zKG4UtMkFVOz%j^!war8q
  • ?)Ex7%2BqCE%9z%5aZAT zVJY2a5GR{qvv3cCM!y>dB??*avVP=wl%CsDzYQ640Rh!X`H~Hq;Z|2L7sM6}4UPgv zELQ;4P8-y6>1~*^iD1L}#G|4{5TRuu9-R_Odp;4aB;_Tdf zySgP|G_x;jAM2BDu5PA;nbG1&PL^yaio*qCJokotC{qpPdo%wpKga(x ze!e>$=L5DrBw<~IG7{bHz$w4)MJ%K(JbuP^``=A$SE4ivHM?YY+HMf}3z^gh4^7NF#KI7p3R|#cWoG?4#m7}9KF!p@KR#BHY2sLBYyMHo@gqY( zR9S0cZro@0JGFRKaeF8x?8_nb&yp=v+{r5Hnz~w@c0Dcd?duMQpbOIlNxA9Wy|a`1 zP3^|p1or$nkzl7_7a3ODU+_A1Q(*G?+@1&glk;Wd5oMpUS8`joOW?aF{5ASE4XMa)oUfwa z#J&f^hohmr-|hPmu&PtT?roc-<^4>1rr;)nq=O{}B#>)PGg027g<;CTP-5s?X%o{Z!JeAC33xO}Jzg}eP}0Z%Iu>;@iIdWM19O32vd zYa|L>FT8A--hv)lso;P_kkEFEeemoUxl;=@|U-G+h$rj^--2!d&0lc^79V{z8@v{T&mXw<3EWU$;6kT#bZZ#odS2Du#1c z#DYJe*Tk4h26E~0wR4Ig!e(EmOhY4W0{K85?S?gRa8OL{<46rE7Bj7$>?BPa&{IbG z(khe`ph6q_H3`>1t{+i~KYlYt~_v;3jnq5$jp!*fIg zWT`hsiri&h;ElJSE$AHf{ZT&22Tot9gu{P_)HwlXMx|9)P_u6_j=pkAO;084e#u~CL&NqX<0N;LsbE2sCm%Pf78sKs{a<>vQE9PHX>d>hAX zzMem;!m%JEZ_vSF#UF8{=j=UfE)k}}!Sq>r;vI3mMe_~y>ge-JX8Q6c<9o)=gnz(C zoeJqd3%g(Vn{U^>i_qTkRbThXR?QnLwZ-Yd$=>lUYhBYD?uZHZy(`!CwbT8z`;C*+ z{YUOAcfV^7!qYqT_=v^HiAjg{*2KI=R6=1@OHEo~M`75sgqhxO_vKat(LJG^Z}`18 zd}%^dOG{K(Ls?;(pi@BAokSm!hce5Em)xZ=Ksj4+!DPF8X!$73+Ajj2z=0c7oN!_q z^#*Lnd48uCAF+$gatn9Q8U5T8=$Xnh+Bv(lKWiEoowZ0R9pKjfM1{BItVn|Y`sM!s DY8|mA diff --git a/inst/tinytest/test_dataProcess.R b/inst/tinytest/test_dataProcess.R index 234130b0..59ec378a 100644 --- a/inst/tinytest/test_dataProcess.R +++ b/inst/tinytest/test_dataProcess.R @@ -15,26 +15,28 @@ expect_equal(nrow(QuantDataDefault$FeatureLevelData), expect_equal(nrow(QuantDataDefaultLinear$FeatureLevelData), nrow(QuantDataParallelLinear$FeatureLevelData)) -# dt1 <- as.data.table(quant_data_srm$ProteinLevelData) -# dt2 <- as.data.table(QuantDataDefault$ProteinLevelData) -# -# cols <- sort(names(dt1)) -# fsetequal(dt1[, ..cols], dt2[, ..cols]) - # SRMRawData is a label-based experiment: heavy ("H") rows must be preserved # in FeatureLevelData after dataProcess quant_data_srm = readRDS( system.file("tinytest/processed_data/quant_data_srm.rds", package = "MSstats") ) + +dt1 <- as.data.table(quant_data_srm$ProteinLevelData) +dt2 <- as.data.table(QuantDataDefault$ProteinLevelData) +cols <- sort(intersect(names(dt1), names(dt2))) expect_true( - identical(QuantDataDefault, quant_data_srm), - info = "dataProcess output for SRMRawData should be identical to previously saved output" + fsetequal(dt1[, ..cols], dt2[, ..cols]), + info = "dataProcess ProteinLevelData for SRMRawData should be identical to previously saved output" ) +dt1 <- as.data.table(quant_data_srm$FeatureLevelData) +dt2 <- as.data.table(QuantDataDefault$FeatureLevelData) +cols <- sort(intersect(names(dt1), names(dt2))) expect_true( - "H" %in% QuantDataDefault$FeatureLevelData$LABEL, - info = "FeatureLevelData should contain heavy-label (H) rows for label-based SRM data" + fsetequal(dt1[, ..cols], dt2[, ..cols]), + info = "dataProcess FeatureLevelData for SRMRawData should be identical to previously saved output" ) + expect_true( "L" %in% QuantDataDefault$FeatureLevelData$LABEL, info = "SRMRawData FeatureLevelData must contain L rows" From 9c9d7b515f210647b9d7deb27306081800ed0034 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 14 Apr 2026 09:21:53 -0400 Subject: [PATCH 11/18] fix tests --- R/dataProcess.R | 12 ++++++------ inst/tinytest/test_dataProcess.R | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/R/dataProcess.R b/R/dataProcess.R index dcb9a01c..2a0d3836 100755 --- a/R/dataProcess.R +++ b/R/dataProcess.R @@ -396,7 +396,7 @@ MSstatsSummarizeSingleLinear = function(single_protein, if (impute & any(single_protein[["censored"]])) { fit_data = if (is_labeled_reference) { - single_protein[!is_labeled_ref, cols, with = FALSE] + single_protein[is_labeled_ref == FALSE, cols, with = FALSE] } else { single_protein[, cols, with = FALSE] } @@ -409,8 +409,8 @@ MSstatsSummarizeSingleLinear = function(single_protein, }] if (is_labeled_reference) { - single_protein[, predicted := ifelse(censored & !is_labeled_ref, predicted, NA)] - single_protein[, newABUNDANCE := ifelse(censored & !is_labeled_ref, predicted, newABUNDANCE)] + single_protein[, predicted := ifelse(censored & is_labeled_ref == FALSE, predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored & is_labeled_ref == FALSE, predicted, newABUNDANCE)] } else { single_protein[, predicted := ifelse(censored, predicted, NA)] single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] @@ -539,7 +539,7 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, converged = TRUE fit_data = if (is_labeled_reference) { - single_protein[!is_labeled_ref, cols, with = FALSE] + single_protein[is_labeled_ref == FALSE, cols, with = FALSE] } else { single_protein[, cols, with = FALSE] } @@ -561,8 +561,8 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, } if (is_labeled_reference) { - single_protein[, predicted := ifelse(censored & !is_labeled_ref, predicted, NA)] - single_protein[, newABUNDANCE := ifelse(censored & !is_labeled_ref, predicted, newABUNDANCE)] + single_protein[, predicted := ifelse(censored & is_labeled_ref == FALSE, predicted, NA)] + single_protein[, newABUNDANCE := ifelse(censored & is_labeled_ref == FALSE, predicted, newABUNDANCE)] } else { single_protein[, predicted := ifelse(censored, predicted, NA)] single_protein[, newABUNDANCE := ifelse(censored, predicted, newABUNDANCE)] diff --git a/inst/tinytest/test_dataProcess.R b/inst/tinytest/test_dataProcess.R index 59ec378a..ac220193 100644 --- a/inst/tinytest/test_dataProcess.R +++ b/inst/tinytest/test_dataProcess.R @@ -22,18 +22,18 @@ quant_data_srm = readRDS( package = "MSstats") ) -dt1 <- as.data.table(quant_data_srm$ProteinLevelData) -dt2 <- as.data.table(QuantDataDefault$ProteinLevelData) +dt1 <- data.table::as.data.table(quant_data_srm$ProteinLevelData) +dt2 <- data.table::as.data.table(QuantDataDefault$ProteinLevelData) cols <- sort(intersect(names(dt1), names(dt2))) expect_true( - fsetequal(dt1[, ..cols], dt2[, ..cols]), + data.table::fsetequal(dt1[, ..cols], dt2[, ..cols]), info = "dataProcess ProteinLevelData for SRMRawData should be identical to previously saved output" ) -dt1 <- as.data.table(quant_data_srm$FeatureLevelData) -dt2 <- as.data.table(QuantDataDefault$FeatureLevelData) +dt1 <- data.table::as.data.table(quant_data_srm$FeatureLevelData) +dt2 <- data.table::as.data.table(QuantDataDefault$FeatureLevelData) cols <- sort(intersect(names(dt1), names(dt2))) expect_true( - fsetequal(dt1[, ..cols], dt2[, ..cols]), + data.table::fsetequal(dt1[, ..cols], dt2[, ..cols]), info = "dataProcess FeatureLevelData for SRMRawData should be identical to previously saved output" ) From 78f678ac5a595f147fd659f973f1adf5d7744da3 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 14 Apr 2026 09:38:34 -0400 Subject: [PATCH 12/18] fix unit tests --- .../test_utils_summarization_prepare.R | 32 +++++-------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/inst/tinytest/test_utils_summarization_prepare.R b/inst/tinytest/test_utils_summarization_prepare.R index ffd2632b..3f5fd6ac 100644 --- a/inst/tinytest/test_utils_summarization_prepare.R +++ b/inst/tinytest/test_utils_summarization_prepare.R @@ -74,7 +74,7 @@ expect_false( # --- .prepareLinear: n_obs computed per PROTEIN+FEATURE+LABEL ---------------- -make_two_label_input <- function() { +make_two_label_input <- function(is_labeled_ref = FALSE) { data.table::data.table( PROTEIN = rep("P1", 8), FEATURE = factor(rep(c("F1", "F2"), each = 4)), @@ -82,7 +82,8 @@ make_two_label_input <- function() { RUN = factor(rep(c("R1","R2","R1","R2"), 2)), ABUNDANCE = c(10, 11, 8, 9, 10.5, 11.5, 8.5, 9.5), INTENSITY = rep(100L, 8), - ANOMALYSCORES = rep(NA_real_, 8) + ANOMALYSCORES = rep(NA_real_, 8), + is_labeled_ref = is_labeled_ref ) } @@ -100,11 +101,6 @@ expect_equal( 2L, info = ".prepareLinear: n_obs must be 2 for H rows of F1 (2 H runs)" ) -# Without LABEL grouping the old code would have returned 4 (all runs pooled) -expect_false( - any(result_prep$n_obs == 4L), - info = ".prepareLinear: n_obs must not be 4 (old un-grouped value)" -) # total_features per PROTEIN+LABEL: 2 features per label expect_equal( @@ -123,7 +119,7 @@ expect_equal( # n_obs must combine L and H observations for each feature (no per-label split). result_prep_srm <- MSstats:::.prepareLinear( - make_two_label_input(), + make_two_label_input(rep(c(FALSE, FALSE, TRUE, TRUE), 2)), impute = FALSE, censored_symbol = NULL, is_labeled_reference = TRUE ) @@ -131,12 +127,12 @@ result_prep_srm <- MSstats:::.prepareLinear( # 2 L runs + 2 H runs per feature → combined n_obs = 4 expect_equal( unique(result_prep_srm[LABEL == "L" & FEATURE == "F1", n_obs]), - 4L, - info = ".prepareLinear(is_labeled_reference=TRUE): n_obs must combine L and H observations" + 2L, + info = ".prepareLinear(is_labeled_reference=TRUE): n_obs must only use L observations" ) expect_equal( unique(result_prep_srm[LABEL == "H" & FEATURE == "F1", n_obs]), - 4L, + 2L, info = ".prepareLinear(is_labeled_reference=TRUE): H rows must share the same n_obs as L rows" ) @@ -159,10 +155,6 @@ expect_equal( 2L, info = ".prepareTMP(is_labeled_reference=FALSE): H n_obs must be counted independently" ) -expect_false( - any(result_tmp_unlabeled$n_obs == 4L), - info = ".prepareTMP(is_labeled_reference=FALSE): n_obs must not be 4 (pooled across labels)" -) # total_features per PROTEIN+LABEL expect_equal( @@ -176,15 +168,8 @@ expect_equal( info = ".prepareTMP(is_labeled_reference=FALSE): total_features for H must be 2" ) -# --- .prepareTMP: is_labeled_reference=TRUE groups WITHOUT LABEL ------------- -# For SRM, .getNonMissingFilter marks H rows as non-informative (is_labeled_ref=TRUE -# → use_for_analysis=FALSE). Without LABEL grouping, each feature's n_obs is the -# count of nonmissing L rows, but that count is assigned to ALL rows of the feature -# (L and H alike). This prevents H rows from getting n_obs=0 and being filtered -# out before they can serve as the normalization reference in .adjustLRuns. - result_tmp_srm <- MSstats:::.prepareTMP( - make_srm_prep_input(), + make_two_label_input(rep(c(FALSE, FALSE, TRUE, TRUE), 2)), impute = FALSE, censored_symbol = NULL, is_labeled_reference = TRUE ) @@ -195,7 +180,6 @@ expect_equal( 2L, info = ".prepareTMP(is_labeled_reference=TRUE): H rows must share n_obs with L rows (not 0)" ) -# n_obs_run: similarly, H rows must have n_obs_run > 0 so they survive .isSummarizable expect_true( all(result_tmp_srm[LABEL == "H", n_obs_run] > 0), info = ".prepareTMP(is_labeled_reference=TRUE): H rows must have n_obs_run > 0" From f59c26a29f6dac82e4837f27b91b310a0edba05d Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 14 Apr 2026 09:51:36 -0400 Subject: [PATCH 13/18] remove dead code for .prepareLinear --- R/utils_summarization_prepare.R | 57 +++---------------- .../test_utils_summarization_prepare.R | 55 +----------------- 2 files changed, 10 insertions(+), 102 deletions(-) diff --git a/R/utils_summarization_prepare.R b/R/utils_summarization_prepare.R index d8bb4580..e07165cd 100644 --- a/R/utils_summarization_prepare.R +++ b/R/utils_summarization_prepare.R @@ -50,7 +50,7 @@ MSstatsPrepareForSummarization = function(input, method, impute, censored_symbol getOption("MSstatsMsg")("INFO", msg) } - input = .prepareSummary(input, method, impute, censored_symbol, add_ref_covariate) + input = .prepareSummary(input, impute, censored_symbol, add_ref_covariate) input[, PROTEIN := factor(PROTEIN)] input } @@ -101,7 +101,6 @@ getProcessed = function(input) { #' Prepare feature-level data for summarization #' @param input data.table -#' @param method "TMP" / "linear" #' @param impute logical #' @param censored_symbol "0"/"NA" #' @param is_labeled_reference logical, if TRUE the H channel is a normalization @@ -110,54 +109,14 @@ getProcessed = function(input) { #' processed independently. #' @return data.table #' @keywords internal -.prepareSummary = function(input, method, impute, censored_symbol, +.prepareSummary = function(input, impute, censored_symbol, is_labeled_reference = FALSE) { - # if (method == "TMP") { - input = .prepareTMP(input, impute, censored_symbol, is_labeled_reference) - # } else { - # input = .prepareLinear(input, FALSE, censored_symbol, is_labeled_reference) - # } - input -} - - -#' Prepare feature-level data for linear summarization -#' @inheritParams .prepareSummary -#' @return data.table -#' @keywords internal -.prepareLinear = function(input, impute, censored_symbol, - is_labeled_reference = FALSE) { - newABUNDANCE = ABUNDANCE = nonmissing = n_obs = n_obs_run = NULL - total_features = FEATURE = prop_features = NULL - - label_by = if (is_labeled_reference) character(0) else "LABEL" - - input[, newABUNDANCE := ABUNDANCE] - input[, nonmissing := .getNonMissingFilter(.SD, impute, censored_symbol)] - input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE", label_by)] - # remove feature with 1 measurement - input[, nonmissing := ifelse(n_obs <= 1, FALSE, nonmissing)] - input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN", label_by)] - - input[, total_features := uniqueN(FEATURE), by = c("PROTEIN", label_by)] - input[, prop_features := sum(nonmissing) / total_features, - by = c("PROTEIN", "RUN", label_by)] - input -} - - -#' Prepare feature-level data for TMP summarization -#' @inheritParams .prepareSummary -#' @return data.table -#' @keywords internal -.prepareTMP = function(input, impute, censored_symbol, - is_labeled_reference = FALSE) { censored = feature_quality = newABUNDANCE = cen = nonmissing = n_obs = NULL n_obs_run = total_features = FEATURE = prop_features = NULL remove50missing = ABUNDANCE = NULL - + label_by = if (is_labeled_reference) character(0) else "LABEL" - + if (impute & !is.null(censored_symbol)) { if (is.element("feature_quality", colnames(input))) { input[, censored := ifelse(feature_quality == "Informative", @@ -172,21 +131,21 @@ getProcessed = function(input) { } else { input[, newABUNDANCE := ABUNDANCE] } - + input[, nonmissing := .getNonMissingFilter(input, impute, censored_symbol)] input[, n_obs := sum(nonmissing), by = c("PROTEIN", "FEATURE", label_by)] input[, nonmissing := ifelse(n_obs <= 1, FALSE, nonmissing)] input[, n_obs_run := sum(nonmissing), by = c("PROTEIN", "RUN", label_by)] - + input[, total_features := uniqueN(FEATURE), by = c("PROTEIN", label_by)] input[, prop_features := sum(nonmissing) / total_features, by = c("PROTEIN", "RUN", label_by)] - + if (is.element("cen", colnames(input))) { if (any(input[["cen"]] == 0)) { .setCensoredByThreshold(input, censored_symbol, remove50missing) } } - + input } diff --git a/inst/tinytest/test_utils_summarization_prepare.R b/inst/tinytest/test_utils_summarization_prepare.R index 3f5fd6ac..bbaedeed 100644 --- a/inst/tinytest/test_utils_summarization_prepare.R +++ b/inst/tinytest/test_utils_summarization_prepare.R @@ -87,58 +87,7 @@ make_two_label_input <- function(is_labeled_ref = FALSE) { ) } -prep_input <- make_two_label_input() -result_prep <- MSstats:::.prepareLinear(prep_input, impute = FALSE, censored_symbol = NULL) - -# Each FEATURE+LABEL combination has 2 runs → n_obs should be 2 -expect_equal( - unique(result_prep[LABEL == "L" & FEATURE == "F1", n_obs]), - 2L, - info = ".prepareLinear: n_obs must be 2 for L rows of F1 (2 L runs)" -) -expect_equal( - unique(result_prep[LABEL == "H" & FEATURE == "F1", n_obs]), - 2L, - info = ".prepareLinear: n_obs must be 2 for H rows of F1 (2 H runs)" -) - -# total_features per PROTEIN+LABEL: 2 features per label -expect_equal( - unique(result_prep[LABEL == "L", total_features]), - 2L, - info = ".prepareLinear: total_features for L must be 2" -) -expect_equal( - unique(result_prep[LABEL == "H", total_features]), - 2L, - info = ".prepareLinear: total_features for H must be 2" -) - -# --- .prepareLinear: is_labeled_reference=TRUE groups WITHOUT LABEL ----------- -# For SRM, H is the normalization reference, not an independent label. -# n_obs must combine L and H observations for each feature (no per-label split). - -result_prep_srm <- MSstats:::.prepareLinear( - make_two_label_input(rep(c(FALSE, FALSE, TRUE, TRUE), 2)), - impute = FALSE, censored_symbol = NULL, - is_labeled_reference = TRUE -) - -# 2 L runs + 2 H runs per feature → combined n_obs = 4 -expect_equal( - unique(result_prep_srm[LABEL == "L" & FEATURE == "F1", n_obs]), - 2L, - info = ".prepareLinear(is_labeled_reference=TRUE): n_obs must only use L observations" -) -expect_equal( - unique(result_prep_srm[LABEL == "H" & FEATURE == "F1", n_obs]), - 2L, - info = ".prepareLinear(is_labeled_reference=TRUE): H rows must share the same n_obs as L rows" -) - -# --- .prepareTMP: is_labeled_reference=FALSE groups by LABEL ----------------- - -result_tmp_unlabeled <- MSstats:::.prepareTMP( +result_tmp_unlabeled <- MSstats:::.prepareSummary( make_two_label_input(), impute = FALSE, censored_symbol = NULL, is_labeled_reference = FALSE @@ -168,7 +117,7 @@ expect_equal( info = ".prepareTMP(is_labeled_reference=FALSE): total_features for H must be 2" ) -result_tmp_srm <- MSstats:::.prepareTMP( +result_tmp_srm <- MSstats:::.prepareSummary( make_two_label_input(rep(c(FALSE, FALSE, TRUE, TRUE), 2)), impute = FALSE, censored_symbol = NULL, is_labeled_reference = TRUE From cadbca0db3ca76a1d88c5eb845592bed3ba57c68 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 14 Apr 2026 09:53:42 -0400 Subject: [PATCH 14/18] update docs --- man/dot-prepareLinear.Rd | 27 --------------------------- man/dot-prepareSummary.Rd | 10 +--------- man/dot-prepareTMP.Rd | 27 --------------------------- 3 files changed, 1 insertion(+), 63 deletions(-) delete mode 100644 man/dot-prepareLinear.Rd delete mode 100644 man/dot-prepareTMP.Rd diff --git a/man/dot-prepareLinear.Rd b/man/dot-prepareLinear.Rd deleted file mode 100644 index e6e2bae7..00000000 --- a/man/dot-prepareLinear.Rd +++ /dev/null @@ -1,27 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils_summarization_prepare.R -\name{.prepareLinear} -\alias{.prepareLinear} -\title{Prepare feature-level data for linear summarization} -\usage{ -.prepareLinear(input, impute, censored_symbol, is_labeled_reference = FALSE) -} -\arguments{ -\item{input}{data.table} - -\item{impute}{logical} - -\item{censored_symbol}{"0"/"NA"} - -\item{is_labeled_reference}{logical, if TRUE the H channel is a normalization -reference (SRM) and grouping keys do not include LABEL; if FALSE (e.g. -protein turnover) LABEL is added to grouping keys so each label is -processed independently.} -} -\value{ -data.table -} -\description{ -Prepare feature-level data for linear summarization -} -\keyword{internal} diff --git a/man/dot-prepareSummary.Rd b/man/dot-prepareSummary.Rd index 5e7da14f..710961c4 100644 --- a/man/dot-prepareSummary.Rd +++ b/man/dot-prepareSummary.Rd @@ -4,19 +4,11 @@ \alias{.prepareSummary} \title{Prepare feature-level data for summarization} \usage{ -.prepareSummary( - input, - method, - impute, - censored_symbol, - is_labeled_reference = FALSE -) +.prepareSummary(input, impute, censored_symbol, is_labeled_reference = FALSE) } \arguments{ \item{input}{data.table} -\item{method}{"TMP" / "linear"} - \item{impute}{logical} \item{censored_symbol}{"0"/"NA"} diff --git a/man/dot-prepareTMP.Rd b/man/dot-prepareTMP.Rd deleted file mode 100644 index 2a31013d..00000000 --- a/man/dot-prepareTMP.Rd +++ /dev/null @@ -1,27 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/utils_summarization_prepare.R -\name{.prepareTMP} -\alias{.prepareTMP} -\title{Prepare feature-level data for TMP summarization} -\usage{ -.prepareTMP(input, impute, censored_symbol, is_labeled_reference = FALSE) -} -\arguments{ -\item{input}{data.table} - -\item{impute}{logical} - -\item{censored_symbol}{"0"/"NA"} - -\item{is_labeled_reference}{logical, if TRUE the H channel is a normalization -reference (SRM) and grouping keys do not include LABEL; if FALSE (e.g. -protein turnover) LABEL is added to grouping keys so each label is -processed independently.} -} -\value{ -data.table -} -\description{ -Prepare feature-level data for TMP summarization -} -\keyword{internal} From cc151ef61fcefc661f8fb7c473376b5afa9fb59d Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 14 Apr 2026 10:27:50 -0400 Subject: [PATCH 15/18] fix summarization with labels --- R/dataProcess.R | 3 --- R/utils_summarization.R | 3 ++- inst/tinytest/test_utils_summarization.R | 10 +++++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/R/dataProcess.R b/R/dataProcess.R index 2a0d3836..5aa7dea8 100755 --- a/R/dataProcess.R +++ b/R/dataProcess.R @@ -580,9 +580,6 @@ MSstatsSummarizeSingleTMP = function(single_protein, impute, censored_symbol, single_protein = single_protein[!is.na(newABUNDANCE), ] result = .runTukey(single_protein, is_labeled_reference, censored_symbol, remove50missing) - if (!is.null(result) && !is.element("LABEL", colnames(result))) { - result[, LABEL := "L"] - } } list(result, survival) } diff --git a/R/utils_summarization.R b/R/utils_summarization.R index 193a66f2..3b762ae7 100644 --- a/R/utils_summarization.R +++ b/R/utils_summarization.R @@ -78,6 +78,7 @@ } else { if (is_labeled_reference) { tmp_result = .adjustLRuns(input, TRUE) + tmp_result[, LABEL := "L"] } else { tmp_result = input[, list(LABEL, RUN, LogIntensities = newABUNDANCE)] } @@ -103,7 +104,7 @@ if (is_labeled_reference) { tmp_result = .adjustLRuns(tmp_result) - tmp_result[, list(RUN, LogIntensities = newABUNDANCE)] + tmp_result[, list(LABEL = "L", RUN, LogIntensities = newABUNDANCE)] } else { tmp_result[, list(LABEL, RUN, LogIntensities = newABUNDANCE)] } diff --git a/inst/tinytest/test_utils_summarization.R b/inst/tinytest/test_utils_summarization.R index dac4a208..a6858fae 100644 --- a/inst/tinytest/test_utils_summarization.R +++ b/inst/tinytest/test_utils_summarization.R @@ -101,9 +101,9 @@ expect_true( ) result_tukey_srm <- MSstats:::.fitTukey(tukey_mf, is_labeled_reference = TRUE) -expect_false( - "LABEL" %in% colnames(result_tukey_srm), - info = ".fitTukey(is_labeled_reference=TRUE): SRM path must not return LABEL column" +expect_true( + "LABEL" %in% colnames(result_tukey_srm) && all(result_tukey_srm$LABEL == "L"), + info = ".fitTukey(is_labeled_reference=TRUE): SRM path must have label column" ) @@ -142,8 +142,8 @@ result_srm <- MSstats:::.runTukey( tukey_sf, is_labeled_reference = TRUE, censored_symbol = NULL, remove50missing = FALSE ) -expect_false( - "LABEL" %in% colnames(result_srm), +expect_true( + "LABEL" %in% colnames(result_srm) && all(result_srm$LABEL == "L"), info = ".runTukey(is_labeled_reference=TRUE): SRM path must not return LABEL column" ) From 14975f50b3e5f76fcec2108cb7bd7a3803cbfff9 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 14 Apr 2026 10:38:14 -0400 Subject: [PATCH 16/18] make fixes with labeling --- R/dataProcess.R | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/R/dataProcess.R b/R/dataProcess.R index 5aa7dea8..a922fb5d 100755 --- a/R/dataProcess.R +++ b/R/dataProcess.R @@ -444,9 +444,11 @@ MSstatsSummarizeSingleLinear = function(single_protein, is_single_feature = .checkSingleFeature(single_protein) if (is_single_feature) { + # Todo: enable SRM linear summarization + if (is_labeled_reference) single_protein = single_protein[LABEL == "L"] result = single_protein[, .(LogIntensities = mean(newABUNDANCE)), by = RUN] result[, Protein := unique(single_protein$PROTEIN)] - result[, LABEL := unique(single_protein$LABEL)] + result[, LABEL := if (is_labeled_reference) "L" else unique(single_protein$LABEL)] result[, Variance := NA_real_] setcolorder(result, c("Protein", "RUN", "LogIntensities", "Variance")) @@ -479,7 +481,8 @@ MSstatsSummarizeSingleLinear = function(single_protein, extracted_values = get_linear_summary(single_protein, cf, counts, label, cov_mat) result = cbind(result, extracted_values) - result[, LABEL := unique(single_protein$LABEL)] + # Todo: enable SRM linear summarization + result[, LABEL := if (is_labeled_reference) "L" else unique(single_protein$LABEL)] } return(list(result, survival)) From b9e28eb75ee8861c780f9256e568aafab311b268 Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 14 Apr 2026 14:52:34 -0400 Subject: [PATCH 17/18] add DDARawData test --- .../processed_data/quant_data_dda.rds | Bin 0 -> 31523 bytes inst/tinytest/test_dataProcess.R | 23 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 inst/tinytest/processed_data/quant_data_dda.rds diff --git a/inst/tinytest/processed_data/quant_data_dda.rds b/inst/tinytest/processed_data/quant_data_dda.rds new file mode 100644 index 0000000000000000000000000000000000000000..0653a27fc03d9b62f1f9feb005c89b3241805806 GIT binary patch literal 31523 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 literal 0 HcmV?d00001 diff --git a/inst/tinytest/test_dataProcess.R b/inst/tinytest/test_dataProcess.R index ac220193..58670eea 100644 --- a/inst/tinytest/test_dataProcess.R +++ b/inst/tinytest/test_dataProcess.R @@ -55,6 +55,29 @@ expect_true( ) +# DDARawData Regression Testing +quant_data_dda = readRDS( + system.file("tinytest/processed_data/quant_data_dda.rds", + package = "MSstats") +) +QuantDataDefaultDDA = dataProcess(DDARawData, use_log_file = FALSE) + +dt1 <- data.table::as.data.table(quant_data_dda$ProteinLevelData) +dt2 <- data.table::as.data.table(QuantDataDefaultDDA$ProteinLevelData) +cols <- sort(intersect(names(dt1), names(dt2))) +expect_true( + data.table::fsetequal(dt1[, ..cols], dt2[, ..cols]), + info = "dataProcess ProteinLevelData for DDARawData should be identical to previously saved output" +) +dt1 <- data.table::as.data.table(quant_data_dda$FeatureLevelData) +dt2 <- data.table::as.data.table(QuantDataDefaultDDA$FeatureLevelData) +cols <- sort(intersect(names(dt1), names(dt2))) +expect_true( + data.table::fsetequal(dt1[, ..cols], dt2[, ..cols]), + info = "dataProcess FeatureLevelData for DDARawData should be identical to previously saved output" +) + + # Test dataProcess with technical replicates & fractions ------------------ msstats_input_fractions_techreps = data.table::fread( system.file("tinytest/processed_data/input_techreps_fractions.csv", From eca8c92798a1763396079f4b735094374ffd888c Mon Sep 17 00:00:00 2001 From: Tony Wu Date: Tue, 14 Apr 2026 15:18:46 -0400 Subject: [PATCH 18/18] all tests pass --- inst/tinytest/test_dataProcess.R | 47 ++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/inst/tinytest/test_dataProcess.R b/inst/tinytest/test_dataProcess.R index 58670eea..e0113862 100644 --- a/inst/tinytest/test_dataProcess.R +++ b/inst/tinytest/test_dataProcess.R @@ -17,6 +17,42 @@ expect_equal(nrow(QuantDataDefaultLinear$FeatureLevelData), # SRMRawData is a label-based experiment: heavy ("H") rows must be preserved # in FeatureLevelData after dataProcess + +# Helper for near-equality comparison of data.tables with numeric tolerance +expect_dt_equal <- function(dt1, dt2, cols, tol = 1e-6, info = "") { + # Check non-numeric columns exactly + num_cols <- cols[sapply(dt1[, ..cols], is.numeric)] + key_cols <- cols[!cols %in% num_cols] + + # Sort both tables the same way before comparing + if (length(key_cols) > 0) { + data.table::setkeyv(dt1, key_cols) + data.table::setkeyv(dt2, key_cols) + } + + # Check row count + if (nrow(dt1) != nrow(dt2)) { + return(tinytest::expect_true(FALSE, info = paste(info, "(row count mismatch)"))) + } + + # Non-numeric exact match + if (length(key_cols) > 0) { + tinytest::expect_true( + data.table::fsetequal(dt1[, ..key_cols], dt2[, ..key_cols]), + info = paste(info, "(non-numeric columns)") + ) + } + + # Numeric approximate match + for (col in num_cols) { + tinytest::expect_true( + all(abs(dt1[[col]] - dt2[[col]]) < tol, na.rm = TRUE) && + identical(is.na(dt1[[col]]), is.na(dt2[[col]])), + info = paste(info, sprintf("(column: %s)", col)) + ) + } +} + quant_data_srm = readRDS( system.file("tinytest/processed_data/quant_data_srm.rds", package = "MSstats") @@ -65,16 +101,15 @@ QuantDataDefaultDDA = dataProcess(DDARawData, use_log_file = FALSE) dt1 <- data.table::as.data.table(quant_data_dda$ProteinLevelData) dt2 <- data.table::as.data.table(QuantDataDefaultDDA$ProteinLevelData) cols <- sort(intersect(names(dt1), names(dt2))) -expect_true( - data.table::fsetequal(dt1[, ..cols], dt2[, ..cols]), - info = "dataProcess ProteinLevelData for DDARawData should be identical to previously saved output" +expect_dt_equal(dt1[, ..cols], dt2[, ..cols], cols, + info = "dataProcess ProteinLevelData for DDARawData should be identical to previously saved output" ) + dt1 <- data.table::as.data.table(quant_data_dda$FeatureLevelData) dt2 <- data.table::as.data.table(QuantDataDefaultDDA$FeatureLevelData) cols <- sort(intersect(names(dt1), names(dt2))) -expect_true( - data.table::fsetequal(dt1[, ..cols], dt2[, ..cols]), - info = "dataProcess FeatureLevelData for DDARawData should be identical to previously saved output" +expect_dt_equal(dt1[, ..cols], dt2[, ..cols], cols, + info = "dataProcess FeatureLevelData for DDARawData should be identical to previously saved output" )