Skip to content
33 changes: 28 additions & 5 deletions R/module-loadpage-server.R
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,30 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa
ui_elements
})

# Spectronaut intensity column input — universal across both the
# regular (in-memory) and large-file paths, regardless of analysis
# template. Default tracks the template: turnover analyses want the
# MS1-only quantity, normal analyses want the normalized peak area
# (which is also `bigSpectronauttoMSstatsFormat`'s default).
output$spectronaut_intensity_ui <- renderUI({
req(input$filetype == 'spec', input$BIO != 'PTM')

default_intensity <- if (!is.null(app_template) &&
app_template() == TEMPLATES$protein_turnover) {
"FG.MS1Quantity"
} else {
"F.NormalizedPeakArea"
}

textInput(session$ns("spec_intensity_col"),
label = h5("Intensity column",
class = "icon-wrapper",
icon("question-circle", lib = "font-awesome"),
div("Spectronaut export column to use as the intensity measure (e.g. F.NormalizedPeakArea, F.PeakArea, FG.MS1Quantity). Leave at the default unless you have a specific reason to override it.",
class = "icon-tooltip")),
value = default_intensity)
})

output$spectronaut_turnover_ui <- renderUI({
req(input$filetype == 'spec', input$BIO != 'PTM')
req(!is.null(app_template) && app_template() == TEMPLATES$protein_turnover)
Expand All @@ -198,9 +222,6 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa
tagList(
tags$hr(),
h4("Protein Turnover Options"),
textInput(ns("spec_intensity_col"),
"Intensity column",
value = "FG.MS1Quantity"),
textInput(ns("spec_peptide_seq_col"),
"Peptide sequence column",
value = "FG.LabeledSequence"),
Expand Down Expand Up @@ -235,11 +256,13 @@ loadpageServer <- function(id, parent_session, is_web_server = FALSE, app_templa
unique_peps_def <- if (is.null(input$filter_unique_peptides)) FALSE else input$filter_unique_peptides
agg_psms_def <- if (is.null(input$aggregate_psms)) FALSE else input$aggregate_psms
few_obs_def <- if (is.null(input$filter_few_obs)) FALSE else input$filter_few_obs

calculate_anomaly_def <- if (is.null(input$calculate_anomaly_scores)) FALSE else input$calculate_anomaly_scores

tagList(
create_spectronaut_large_filter_options(session$ns, excluded_def, identified_def, qval_def),
if (qval_def) create_spectronaut_qvalue_cutoff_ui(session$ns, cutoff_def),
create_spectronaut_large_bottom_ui(session$ns, max_feature_def, unique_peps_def, agg_psms_def, few_obs_def)
create_spectronaut_large_bottom_ui(session$ns, max_feature_def, unique_peps_def, agg_psms_def, few_obs_def),
create_spectronaut_large_annotation_ui(session$ns, calculate_anomaly_def)
)
} else {
NULL
Expand Down
56 changes: 56 additions & 0 deletions R/module-loadpage-ui.R
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ create_spectronaut_uploads <- function(ns) {
uiOutput(ns("spectronaut_header_ui")),
uiOutput(ns("spectronaut_file_selection_ui")),
uiOutput(ns("spectronaut_options_ui")),
uiOutput(ns("spectronaut_intensity_ui")),
uiOutput(ns("spectronaut_turnover_ui"))
)
}
Expand Down Expand Up @@ -342,6 +343,61 @@ create_spectronaut_large_bottom_ui <- function(ns, max_feature_def = 20, unique_
)
}

#' Create Spectronaut large file annotation override + anomaly UI
#'
#' Renders an optional annotation upload that overrides Spectronaut's embedded
#' R.Condition / R.Replicate columns on Run, plus the "Calculate Anomaly
#' Scores" controls. End-to-end anomaly scoring is a two-step pipeline in
#' the large-file path:
#' (1) `bigSpectronauttoMSstatsFormat` runs with
#' `calculateAnomalyScores = TRUE` + the model feature column list,
#' which carries those feature columns through the out-of-memory
#' reduce/preprocess steps.
#' (2) After `dplyr::collect`, `MSstatsConvert::MSstatsAnomalyScores`
#' is called on the in-memory result to fit the isolation-forest
#' model and produce the `AnomalyScores` column.
#' Input IDs `calculate_anomaly_scores` and `run_order_file` are deliberately
#' the same as the regular Spectronaut path's so downstream pages
#' (module-qc-server's MSstats+ summarization gate, getDataCode's
#' reproducibility script, etc.) read a single source of truth regardless
#' of which path the user took. The two UI checkboxes never coexist —
#' the regular path's `create_label_free_options` is hidden when
#' `big_file_spec` is on, and this helper only renders when it is — so
#' there is no Shiny namespace collision.
#' A run-order CSV is required (Run + Order columns) — `MSstatsAnomalyScores`
#' uses it for temporal feature engineering.
#' @noRd
create_spectronaut_large_annotation_ui <- function(ns, calculate_anomaly_def = FALSE) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary. i think one can just re-use the annotation file and run order upload panels for regular spectronaut

tagList(
tags$hr(),
h5("Annotation file (optional)",
class = "icon-wrapper",
icon("question-circle", lib = "font-awesome"),
div("Upload a CSV/TSV with columns Run, BioReplicate, Condition (and any extras). When supplied, the converter merges it on Run and overrides any Condition / BioReplicate values from Spectronaut's R.Condition / R.Replicate. Required for paired designs and other layouts Spectronaut's own annotation cannot express.",
class = "icon-tooltip")),
fileInput(ns("big_spec_annotation"), label = NULL,
multiple = FALSE, accept = c(".csv", ".tsv", ".txt")),
checkboxInput(ns("calculate_anomaly_scores"),
label = tags$span(
"Calculate Anomaly Scores",
class = "icon-wrapper",
icon("question-circle", lib = "font-awesome"),
div("Runs the same anomaly scoring pipeline as the regular Spectronaut path: the converter carries FG.ShapeQualityScore (MS2)/(MS1) and EGDeltaRT through the out-of-memory steps, then MSstatsConvert::MSstatsAnomalyScores fits the isolation-forest model on the collected data and adds an AnomalyScores column. Requires a run order CSV.",
class = "icon-tooltip")),
value = calculate_anomaly_def),
conditionalPanel(
condition = sprintf("input['%s']", ns("calculate_anomaly_scores")),
fileInput(ns("run_order_file"),
label = h5("Upload Run Order File",
class = "icon-wrapper",
icon("question-circle", lib = "font-awesome"),
div("CSV with two columns: 'Run' (sequence name matching the converter output) and 'Order' (chronological run number, e.g. 1, 2, 3...).",
class = "icon-tooltip")),
multiple = FALSE, accept = c(".csv"))
)
)
}

#' Create PTM FragPipe uploads
#' @noRd
create_ptm_fragpipe_uploads <- function(ns) {
Expand Down
151 changes: 144 additions & 7 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -639,11 +639,23 @@ getData <- function(input) {
shinybusy::remove_modal_spinner()
return(NULL)
}


if (isTRUE(input$calculate_anomaly_scores) && is.null(input$run_order_file)) {
showNotification(
"Error: Run Order CSV is required when Calculate Anomaly Scores is enabled. Please upload a CSV with Run and Order columns.",
type = "error",
duration = NULL)
shinybusy::remove_modal_spinner()
return(NULL)
}

shinybusy::update_modal_spinner(text = "Processing large Spectronaut file...")

# Call the big file conversion function from MSstatsConvert
converted_data <- MSstatsBig::bigSpectronauttoMSstatsFormat(

# Base arguments shared by every large-file Spectronaut run.
# Optional args (annotation override, anomaly-feature
# carry-through) are spliced in below so callers that don't
# supply them aren't forced to pass NULL / FALSE explicitly.
big_spec_args <- list(
input_file = local_big_file_path,
output_file_name = "output_file.csv",
backend = "arrow",
Expand All @@ -656,6 +668,27 @@ getData <- function(input) {
aggregate_psms = input$aggregate_psms,
filter_few_obs = input$filter_few_obs
)

if (!is.null(input$spec_intensity_col) &&
nchar(trimws(input$spec_intensity_col)) > 0) {
big_spec_args$intensity <- trimws(input$spec_intensity_col)
}

if (!is.null(input$big_spec_annotation)) {
big_spec_args$annotation <- data.table::fread(
input$big_spec_annotation$datapath)
}

if (isTRUE(input$calculate_anomaly_scores)) {
big_spec_args$calculateAnomalyScores <- TRUE
big_spec_args$anomalyModelFeatures <- c(
"FG.ShapeQualityScore (MS2)",
"FG.ShapeQualityScore (MS1)",
"EG.DeltaRT")
}

converted_data <- do.call(
MSstatsBig::bigSpectronauttoMSstatsFormat, big_spec_args)

# Attempt to load the data into memory.
mydata <- tryCatch({
Expand All @@ -673,8 +706,37 @@ getData <- function(input) {
shinybusy::remove_modal_spinner()
return(NULL)
}


if (isTRUE(input$calculate_anomaly_scores) &&
!is.null(input$run_order_file)) {
run_order <- data.table::fread(input$run_order_file$datapath)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
mydata <- MSstatsConvert::MSstatsAnomalyScores(
input = mydata,
quality_metrics = c("FGShapeQualityScore(MS2)",
"FGShapeQualityScore(MS1)",
"EGDeltaRT"),
temporal_direction = c("mean_decrease",
"mean_decrease",
"dispersion_increase"),
missing_run_count = 0.5,
n_feat = 100,
run_order = run_order,
n_trees = 100,
max_depth = "auto",
cores = 1)
}

} else {

if (isTRUE(input$calculate_anomaly_scores) && is.null(input$run_order_file)) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate validations, only one is needed if possible.

showNotification(
"Error: Run Order CSV is required when Calculate Anomaly Scores is enabled. Please upload a CSV with Run and Order columns.",
type = "error",
duration = NULL)
remove_modal_spinner()
return(NULL)
}

data = data.table::fread(input$specdata$datapath)
# Base arguments for the Spectronaut converter
converter_args = list(
Expand Down Expand Up @@ -958,14 +1020,87 @@ library(MSstatsPTM)\n", sep = "")
}
else if(input$filetype == 'spec') {

if (isTRUE(input$big_file_spec)) {
codes = paste(codes,
"# Large-file (out-of-memory) Spectronaut path.\n",
"input_file = \"insert your raw Spectronaut export filepath\"\n",
sep = "")

big_spec_extra <- ""
if (!is.null(input$spec_intensity_col) &&
nchar(trimws(input$spec_intensity_col)) > 0) {
big_spec_extra <- paste0(big_spec_extra,
",\n intensity = \"",
trimws(input$spec_intensity_col), "\"")
}
if (!is.null(input$big_spec_annotation)) {
codes = paste(codes,
"annot_file = data.table::fread(\"insert your annotation filepath (Run, BioReplicate, Condition)\")\n",
sep = "")
big_spec_extra <- paste0(big_spec_extra,
",\n annotation = annot_file")
}
if (isTRUE(input$calculate_anomaly_scores)) {
big_spec_extra <- paste0(big_spec_extra,
",\n calculateAnomalyScores = TRUE",
",\n anomalyModelFeatures = c(\"FG.ShapeQualityScore (MS2)\", \"FG.ShapeQualityScore (MS1)\", \"EG.DeltaRT\")")
}

codes = paste(codes,
"converted = MSstatsBig::bigSpectronauttoMSstatsFormat(input_file,
output_file_name = \"output_file.csv\",
backend = \"arrow\",
filter_by_excluded = ", input$filter_by_excluded, ",
filter_by_identified = ", input$filter_by_identified, ",
filter_by_qvalue = ", input$filter_by_qvalue, ",
qvalue_cutoff = ", input$qvalue_cutoff, ",
max_feature_count = ", input$max_feature_count, ",
filter_unique_peptides = ", input$filter_unique_peptides, ",
aggregate_psms = ", input$aggregate_psms, ",
filter_few_obs = ", input$filter_few_obs,
big_spec_extra,
")\ndata = dplyr::collect(converted)\n",
sep = "")

if (isTRUE(input$calculate_anomaly_scores)) {
codes = paste(codes,
"# Step 2 of the anomaly scoring pipeline: fit the\n",
"# isolation-forest model on the collected data and\n",
"# add an AnomalyScores column.\n",
"run_order = data.table::fread(\"insert your run order CSV filepath (Run, Order columns)\")\n",
"data = MSstatsConvert::MSstatsAnomalyScores(\n",
" input = data,\n",
" # Standardized column names (raw Spectronaut names\n",
" # had `.` and ` ` stripped during the converter step).\n",
" quality_metrics = c(\"FGShapeQualityScore(MS2)\", \"FGShapeQualityScore(MS1)\", \"EGDeltaRT\"),\n",
" temporal_direction = c(\"mean_decrease\", \"mean_decrease\", \"dispersion_increase\"),\n",
" missing_run_count = 0.5,\n",
" n_feat = 100,\n",
" run_order = run_order,\n",
" n_trees = 100,\n",
" max_depth = \"auto\",\n",
" cores = 1)\n",
sep = "")
}

} else {

codes = paste(codes, "data = data.table::fread(\"insert your MSstats scheme output from Spectronaut filepath\")\nannot_file = data.table::fread(\"insert your annotation filepath\")#Optional\n"
, sep = "")

reg_spec_intensity_arg <- if (!is.null(input$spec_intensity_col) &&
nchar(trimws(input$spec_intensity_col)) > 0) {
paste0(" intensity = \"",
trimws(input$spec_intensity_col), "\",\n")
} else {
""
}

if (isTRUE(input$calculate_anomaly_scores)) {
codes = paste(codes, "run_order = data.table::fread(\"insert your run order CSV filepath (Run, Order columns)\")\n", sep = "")
codes = paste(codes, "data = SpectronauttoMSstatsFormat(data,
annotation = annot_file, #Optional
filter_with_Qvalue = ", input$q_val, ",
", reg_spec_intensity_arg, " filter_with_Qvalue = ", input$q_val, ",
qvalue_cutoff = ", input$q_cutoff, ",
removeProtein_with1Feature = ", input$remove, ",
use_log_file = FALSE,
Expand All @@ -979,11 +1114,13 @@ library(MSstatsPTM)\n", sep = "")
} else {
codes = paste(codes, "data = SpectronauttoMSstatsFormat(data,
annotation = annot_file, #Optional
filter_with_Qvalue = ", input$q_val, ",
", reg_spec_intensity_arg, " filter_with_Qvalue = ", input$q_val, ",
qvalue_cutoff = ", input$q_cutoff, ",
removeProtein_with1Feature = ", input$remove, ",
use_log_file = FALSE)\n", sep = "")
}

}
}
else if(input$filetype == 'diann') {

Expand Down
Loading
Loading