Skip to content

M sstats big/work/20260513 arrow chunking followups#17

Open
Rudhik1904 wants to merge 12 commits into
develfrom
MSstatsBig/work/20260513_arrow_chunking_followups
Open

M sstats big/work/20260513 arrow chunking followups#17
Rudhik1904 wants to merge 12 commits into
develfrom
MSstatsBig/work/20260513_arrow_chunking_followups

Conversation

@Rudhik1904

@Rudhik1904 Rudhik1904 commented May 18, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

reduceBigSpectronaut() previously streamed Spectronaut CSV exports through readr::read_delim_chunked, which holds a string-interning pool that grows across chunks and pushed peak memory well above one batch's working set. This branch ports the reader to Arrow (which releases per-batch state) and follows up with the three remaining items from TODO-arrow_chunking_followups.md:

  1. The Arrow block_size was Arrow's 256 KiB default — too small for wide Spectronaut rows, causing Invalid: straddling object straddles two block boundaries errors and excessive parser overhead.
  2. End users had no documented escape hatch when the straddling error did fire on extra-wide exports.
  3. cleanSpectronautChunk() ran every batch through ~13 sequential dplyr verbs on a data.frame, producing repeated transient column allocations and fragmenting R's allocator. The rest of the MSstatsConvert family is data.table-native.

Solution: replace readr with Arrow's CsvReadOptions / Scanner / ToRecordBatchReader, raise the default block_size to 16 MiB and expose it as a user parameter, document the straddling-object workaround at the parameter, and rewrite cleanSpectronautChunk() in pure data.table.

Changes

  • Arrow reader replaces readr::read_delim_chunked in reduceBigSpectronaut(). Uses arrow::open_dataset() + Scanner + ToRecordBatchReader to stream record batches; preserves the existing comma/tab/semicolon delimiter switch via CsvParseOptions$delimiter. Per-batch progress logging every 1,000 batches.
  • New block_size parameter on both reduceBigSpectronaut() and the user-facing bigSpectronauttoMSstatsFormat(). Default 16L * 1024L * 1024L (16 MiB) replaces Arrow's 256 KiB default. Coerced to integer and validated (length 1, non-NA, positive).
  • Roxygen @param block_size documents the exact error string (Invalid: straddling object straddles two block boundaries) and the recommended override (64L * 1024L * 1024L) so users hitting the straddling error on pathological rows have a self-service fix.
  • cleanSpectronautChunk() rewritten in data.table. setDT(input) at entry; column selection via dt[, cols, with = FALSE]; two-step rename via setnames(..., skip_absent = TRUE) matching the MSstatsConvert family convention; in-place column updates via :=; conditional NA assignment via mask form dt[cond, Intensity := NA_real_]. Function shrank from ~88 lines to ~64.
  • NA-q-value semantics preserved. Q-value filters use is.na(EGQvalue) | EGQvalue >= cutoff so rows with missing q-values still get Intensity = NA, matching the previous dplyr::if_else behavior (a naive data.table translation would silently change this).
  • Dead code removed. The dplyr::collect(head(dplyr::select(...))) pattern at the old lines 140/144 was a no-op residue from an earlier Arrow-Table refactor and is gone.
  • data.table added to Imports in DESCRIPTION and imported via @importFrom data.table := .SD setDT setnames so the package is cedta()-aware. Regenerated NAMESPACE and man/bigSpectronauttoMSstatsFormat.Rd via devtools::document().

Testing

All tests run via devtools::test(): 51 PASS, 0 FAIL, 0 WARN, 0 SKIP.

New tests added in tests/testthat/test-converters.R:

  • reduceBigSpectronaut validates block_size: rejects negative, zero, NA, length-2 vector, and unparseable string inputs.
  • bigSpectronauttoMSstatsFormat plumbs block_size through: spies on reduceBigSpectronaut via mockery::stub, asserts the default forwards 16L * 1024L * 1024L and an explicit override forwards the user's value.
  • cleanSpectronautChunk schema smoke test: synthetic minimal Spectronaut-shaped input, asserts output column set and basic values.
  • cleanSpectronautChunk filter_by_excluded: rows with Excluded == "True" get Intensity = NA.
  • cleanSpectronautChunk filter_by_identified: rows with Identified == "False" get Intensity = NA.
  • cleanSpectronautChunk filter_by_qvalue (incl. NA case): rows below cutoff are kept, above cutoff become NA, and NA q-values become NA — the explicit semantic guarantee from the rewrite.
  • cleanSpectronautChunk drops rows where F.FrgLossType != "noloss".

Before this branch cleanSpectronautChunk had no direct test coverage (existing tests stubbed reduceBigSpectronaut out entirely).

Checklist Before Requesting a Review

  • I have read the MSstats contributing guidelines
  • My changes generate no new warnings
  • Any dependent changes have been merged and published in downstream modules

Motivation & Context

Large Spectronaut exports previously relied on readr::read_delim_chunked() for chunked CSV ingestion, which can retain growing per-chunk state and lead to unbounded memory growth. Additionally, some wide exports triggered Apache Arrow “straddling object” failures when CSV parsing happens across block boundaries. Finally, the chunk-cleaning logic (cleanSpectronautChunk()) used dplyr-style transformations, which are less efficient for high-volume, in-place chunk processing.

Solution: switch Spectronaut CSV reading to an Arrow streaming, record-batch-by-record-batch pipeline (bounded peak memory) and introduce a tunable block_size to mitigate straddling-object issues. Rework cleanSpectronautChunk() to use data.table for efficient in-place transformations while preserving the existing filtering/NA semantics.

Detailed changes

  • Package metadata / imports

    • DESCRIPTION: added data.table to Imports.
    • NAMESPACE: added importFrom(data.table, ":="), importFrom(data.table, ".SD"), importFrom(data.table, setDT), importFrom(data.table, setnames).
    • Roxygen import directives updated accordingly.
  • R/clean_spectronaut.R

    • reduceBigSpectronaut(..., block_size = 16L * 1024L * 1024L):
      • Added/validated block_size (as.integer(); stopifnot(length(block_size)==1L, !is.na(block_size), block_size > 0L)).
      • Detects CSV delimiter from filename (csv,, tsv|xls → tab, else ;).
      • Builds needed_cols (plus anomalyModelFeatures when calculateAnomalyScores=TRUE).
      • Uses Arrow CSV streaming:
        • arrow::open_dataset(..., format="csv", parse_options=CsvParseOptions$create(delimiter=delim), read_options=CsvReadOptions$create(block_size=block_size))
        • arrow::Scanner$create(ds, projection=present_cols) where present_cols <- intersect(needed_cols, names(ds)) (tolerates partial exports).
        • scanner$ToRecordBatchReader() and processes reader$read_next_batch() in a loop.
      • Per batch:
        • Converts batch to data.frame and calls cleanSpectronautChunk(..., pos=pos, ...).
        • Updates pos <- pos + nrow(chunk_df).
        • Logs progress every 1,000 batches and logs a final “done” message.
    • cleanSpectronautChunk(input, output_path, ..., calculateAnomalyScores=FALSE, anomalyModelFeatures=c()):
      • Rewritten using data.table:
        • data.table::setDT(input) to operate in-place.
        • Keeps only columns present in the batch: present_orig <- intersect(all_cols, colnames(input)); input <- input[, present_orig, with=FALSE].
      • Column renaming is done in two steps:
        • Standardize incoming column names via MSstatsConvert:::.standardizeColnames().
        • Rename from standardized names to MSstats target names with setnames(..., skip_absent = TRUE).
      • Type/logic handling:
        • Intensity := as.numeric(Intensity).
        • Converts "True"/character values for Excluded to logical when Excluded is character.
        • Converts Identified to logical when present and character-typed.
      • Fail-fast validation for filter inputs:
        • Adds require_filter_cols() which stops with a clear message naming missing Spectronaut source columns when filter columns are absent.
        • filter_by_excluded: sets Intensity := NA_real_ where Excluded == TRUE.
        • filter_by_identified: sets Intensity := NA_real_ where Identified == FALSE.
        • filter_by_qvalue:
          • Requires EGQvalue/PGQvalue.
          • Preserves NA q-value semantics (matching dplyr::if_else style): sets Intensity := NA_real_ when is.na(EGQvalue) | EGQvalue >= qvalue_cutoff and similarly for PGQvalue.
      • Always enforces the “noloss” fragment filter:
        • Requires FFrgLossType and filters rows with FFrgLossType == "noloss".
      • Derives isotope label:
        • If LabeledSequence exists: IsotopeLabelType becomes "H" when Lys8 or Arg10 is detected, else "L".
        • Otherwise defaults to "L".
      • Writes only finalized output columns (plus standardized anomaly-feature columns when requested) via .writeChunkToFile().
  • R/converters.R

    • bigSpectronauttoMSstatsFormat(..., block_size = 16L * 1024L * 1024L):
      • Added block_size parameter (documented to help tune Arrow parsing / mitigate “straddling object” failures).
      • Forwards block_size to reduceBigSpectronaut().
  • Documentation

    • man/bigSpectronauttoMSstatsFormat.Rd: updated usage/signature and expanded block_size documentation and tuning guidance.
    • man/dot-prefixedPath.Rd: added docs for internal .prefixedPath(prefix, path).

Unit tests added or modified

  • tests/testthat/test-converters.R
    • Added/expanded tests for cleanSpectronautChunk():
      • Produces expected MSstats output schema/columns.
      • filter_by_excluded sets Intensity to NA for excluded rows.
      • filter_by_identified sets Intensity to NA for unidentified rows.
      • filter_by_qvalue verifies NA-aware behavior (rows with NA q-values become NA intensity).
      • Drops rows where FFrgLossType != "noloss".
      • Fails fast with clear errors when required filter source columns are missing (errors mention Spectronaut source column names like F.FrgLossType / EG.Qvalue).
      • Confirms the q-value filter is only enforced when filter_by_qvalue=TRUE.
    • Added tests for reduceBigSpectronaut():
      • Validates block_size rejects invalid values (negative, zero, NA, vector, and non-numeric string).
      • Ensures Scanner projection drops unused “junk” columns before cleanSpectronautChunk() sees them.
    • Added tests for bigSpectronauttoMSstatsFormat():
      • Confirms block_size is forwarded through to reduceBigSpectronaut() (default and override).

Coding guidelines / violations

No coding guideline violations identified.

tonywu1999 and others added 10 commits May 7, 2026 14:46
R/clean_spectronaut.R:9-12: added block_size parameter (default 16L * 1024L * 1024L) with coerce + validation.
R/clean_spectronaut.R:44: CsvReadOptions$create now uses the parameter.
R/converters.R:120-125: new @param block_size roxygen with the straddling-object workaround note.
R/converters.R:148-156: bigSpectronauttoMSstatsFormat gains block_size, plumbed to reduceBigSpectronaut.
tests/testthat/test-converters.R:97-163: validation tests (rejects negative/zero/NA/vector/string) + plumbing tests (default forwards 16 MiB, override forwards user's value).
man/bigSpectronauttoMSstatsFormat.Rd: regenerated from roxygen.
…setnames so the package is data.table-aware (cedta()).

R/clean_spectronaut.R:103-187: rewrote cleanSpectronautChunk in data.table:
setDT(input) at entry; subsequent operations modify in place via :=.
Two-step rename (setnames for standardize, then setnames with skip_absent = TRUE to map standardized→MSstats) matches the MSstatsConvert family pattern.
Conditional NA assignment uses mask form dt[cond, Intensity := NA_real_].
Q-value filters preserve dplyr::if_else NA semantics via explicit is.na(EGQvalue) | EGQvalue >= cutoff.
Dropped the leftover dplyr::collect(head(dplyr::select(...))) pattern — was a no-op residue from a prior refactor.
Function shrank from ~88 lines to ~64.
DESCRIPTION:20: added data.table to Imports.
NAMESPACE: regenerated, now imports :=, .SD, setDT, setnames from data.table.
tests/testthat/test-converters.R:97-211: 5 new tests — schema smoke test, filter_by_excluded, filter_by_identified, filter_by_qvalue (covering the NA-q-value case), and FFrgLossType row drop.
@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR switches Spectronaut conversion to Arrow-backed record-batch streaming, rewrites chunk cleaning with data.table, adds a configurable block_size parameter, and updates documentation and tests around the new flow.

Changes

Spectronaut Arrow Streaming & Data.Table Refactor

Layer / File(s) Summary
dependency setup
DESCRIPTION, NAMESPACE
Added data.table to imports and imported the data.table symbols used by the cleaner.
Arrow streaming in reduceBigSpectronaut
R/clean_spectronaut.R
Replaced readr chunking with Arrow dataset scanning, validated block_size, projected required columns, processed record batches, and logged progress.
data.table chunk cleaning
R/clean_spectronaut.R
Rewrote chunk transformation with data.table-based renaming, coercion, filtering, IsotopeLabelType derivation, and output selection.
block_size API wiring
R/converters.R
Added block_size to bigSpectronauttoMSstatsFormat() and forwarded it to reduceBigSpectronaut().
man pages
man/bigSpectronauttoMSstatsFormat.Rd, man/dot-prefixedPath.Rd
Updated block_size docs and added a man page for .prefixedPath(prefix, path).
Spectronaut tests
tests/testthat/test-converters.R
Added coverage for chunk cleaning, missing columns, projection, block_size validation, and argument forwarding.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: tonywu1999

Poem

🐰 Hop, hop — the CSV now streams so neat,
Arrow brings batches, quick and complete.
data.table cleans with a nimble paw,
block_size tuned, no hiccup to saw,
and this rabbit grins at the tidy result so sweet.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main Arrow chunking follow-up work, though it includes branch/date noise.
Description check ✅ Passed The description follows the template with motivation, detailed changes, testing, and the checklist completed.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MSstatsBig/work/20260513_arrow_chunking_followups

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Rudhik1904 Rudhik1904 requested a review from tonywu1999 May 18, 2026 21:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@R/clean_spectronaut.R`:
- Around line 155-167: The current filter block assumes columns Excluded,
Identified, EGQvalue, PGQvalue, and FFrgLossType always exist and will error if
any are missing; update the logic in clean_spectronaut.R to guard each filter by
checking column presence (e.g., use "if ('Excluded' %in% names(input))" before
the Excluded filter, similarly for Identified, EGQvalue and PGQvalue before
applying qvalue-based NA assignment, and check for FFrgLossType before
subsetting), so each conditional only runs when its target column exists and the
behavior remains unchanged otherwise.
- Around line 25-58: The computed needed_cols is never passed to Arrow, so the
CSV reader still parses all columns; update the CsvConvertOptions usage to
include the projection by calling
arrow::CsvConvertOptions$create(include_columns = needed_cols) (or set the
include_columns field on convert_opts after creation) so that convert_opts
includes needed_cols before calling arrow::open_dataset/Scanner$create;
reference the symbols needed_cols and convert_opts (and the call
arrow::CsvConvertOptions$create) and ensure this happens prior to creating
ds/reader.

In `@tests/testthat/test-converters.R`:
- Around line 106-113: The test's expect_error calls are too broad—change them
to assert the error message mentions "block_size" so they only pass when
block_size validation fails; update each expect_error(reduceBigSpectronaut(...),
...) in tests/testthat/test-converters.R to include a second argument (string or
regex) that matches "block_size" (e.g., "block_size" or "block_size.*invalid")
for the negative values and vector cases, and similarly for the
suppressed-warning call so all invalid-block_size cases are checked by message
content rather than any error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9ebfd981-78cf-43a3-b93a-fd55ccfaeccc

📥 Commits

Reviewing files that changed from the base of the PR and between a43b90b and c8c835e.

📒 Files selected for processing (7)
  • DESCRIPTION
  • NAMESPACE
  • R/clean_spectronaut.R
  • R/converters.R
  • man/bigSpectronauttoMSstatsFormat.Rd
  • man/dot-prefixedPath.Rd
  • tests/testthat/test-converters.R

Comment thread R/clean_spectronaut.R Outdated
Comment thread R/clean_spectronaut.R
Comment thread tests/testthat/test-converters.R Outdated
Comment thread R/clean_spectronaut.R
anomalyModelFeatures=c()) {
calculateAnomalyScores=FALSE,
anomalyModelFeatures=c(),
block_size = 16L * 1024L * 1024L) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  1. For people who may want to increase their block size to increase the speed of processing, add a recommendation on how to estimate the adequate block size to maximize speed while reducing the risk of the system crashing
  2. Make it more clear in the MSstatsBig / MSstatsConvert documentation on which columns we actually need from users for Spectronaut

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.

added the documentation, let me know how it looks if it needs more work

…hten tests, document

R/clean_spectronaut.R: needed_cols was computed but never applied, so every
batch still materialized all ~35 columns. Wire it in via Scanner projection
(intersect(needed_cols, names(ds)) -> Scanner$create(projection=)). Note:
CsvConvertOptions$include_columns (the obvious fix) collides with the
open_dataset schema layer and errors ("Multiple matches for FieldRef.Name"),
so Scanner projection is the mechanism that works with the dataset API.
intersect() keeps only present columns so partial exports still read.

R/clean_spectronaut.R: cleanSpectronautChunk carefully tolerated missing
columns at subset/rename time, then assumed Excluded/Identified/EGQvalue/
PGQvalue/FFrgLossType existed in the filters -> cryptic data.table
"object not found". Added require_filter_cols() that fails fast with a clear
message naming the original Spectronaut column, scoped to when each filter
actually runs (chose fail-fast over silent skip: skipping a filter changes
which rows/intensities survive downstream).

tests/testthat/test-converters.R: added a test that stubs cleanSpectronautChunk
and asserts junk columns are projected out before the cleaner (guards the
projection; verified it fails if the projection is removed); added tests for
the fail-fast filter guards; tightened block_size expect_error() calls with
regexp = "block_size" so they only pass on the actual validation error.

R/converters.R, man/: expanded @param block_size with tuning guidance (memory
scales ~linearly, diminishing speed returns, double-and-watch strategy) and
added an @details section listing required Spectronaut columns split into
always-required / optional / required-per-filter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/testthat/test-converters.R (1)

237-241: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider expect_no_error() over expect_error(x, NA).

Functionally fine, but testthat's docs now recommend expect_no_error()/expect_no_condition() in place of expect_error(object, NA) for asserting absence of an error: "If NA, asserts that there should be no errors, but we now recommend using expect_no_error() and friends instead."

♻️ Suggested modernization
-  expect_error(
+  expect_no_error(
     MSstatsBig:::cleanSpectronautChunk(make_spectronaut_input(n = 1L),
-                                       output_file, pos = 1L,
-                                       filter_by_qvalue = FALSE),
-    NA)
+                                       output_file, pos = 1L,
+                                       filter_by_qvalue = FALSE))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/testthat/test-converters.R` around lines 237 - 241, Replace the
`expect_error(..., NA)` assertion in the `cleanSpectronautChunk` test with the
modern `expect_no_error()` helper. Update the test in the `test-converters`
suite to keep the same inputs (`make_spectronaut_input`, `output_file`,
`filter_by_qvalue`) while asserting that `MSstatsBig:::cleanSpectronautChunk`
completes without error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/testthat/test-converters.R`:
- Around line 237-241: Replace the `expect_error(..., NA)` assertion in the
`cleanSpectronautChunk` test with the modern `expect_no_error()` helper. Update
the test in the `test-converters` suite to keep the same inputs
(`make_spectronaut_input`, `output_file`, `filter_by_qvalue`) while asserting
that `MSstatsBig:::cleanSpectronautChunk` completes without error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 25f345c6-28cc-4482-9681-8556664282c4

📥 Commits

Reviewing files that changed from the base of the PR and between 8188f4e and 160024b.

📒 Files selected for processing (4)
  • R/clean_spectronaut.R
  • R/converters.R
  • man/bigSpectronauttoMSstatsFormat.Rd
  • tests/testthat/test-converters.R
✅ Files skipped from review due to trivial changes (1)
  • man/bigSpectronauttoMSstatsFormat.Rd
🚧 Files skipped from review as they are similar to previous changes (2)
  • R/converters.R
  • R/clean_spectronaut.R

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants