A mass-spectrometry analysis pipeline for identifying the RNA moiety of peptide-RNA crosslinks (Sequential Analysis of peptide-xLinked nucleoTides).
It takes MSFragger-calibrated mzML files plus PSMs, deconvolves paired RNA MS/MS, matches the observed RNA ladder peaks against a theoretical fragment library, scores candidate sequences with a Hyperscore variant (Fenyö & Beavis 2003), and assigns significance via a non-canonical-nucleotide decoy FDR. It also reports a per-scan delta score and an E-value (Fenyö & Beavis 2003) as a reference metric for users.
The calibrated_mzml_transfer step requires ProteoWizard msconvert to convert Thermo RAW
files to mzML, which must be installed separately. Reading Thermo RAW files needs the vendor
libraries, which are Windows-only – so the step picks its backend from the operating system:
| OS | Backend | What you must install |
|---|---|---|
| Windows | native msconvert.exe |
ProteoWizard (vendor-file build) |
| Linux / macOS | ProteoWizard Docker image (msconvert under Wine) | Docker |
- Download the latest ProteoWizard installer from https://proteowizard.sourceforge.io/download.html (choose the version with vendor-file support).
- Install it (default path:
C:\Program Files\ProteoWizard\). - Set
msconvert_exein yourconfig.ymlto the full path ofmsconvert.exe(e.g.C:/Program Files/ProteoWizard/msconvert.exe).
There is no native msconvert on Linux or macOS, so the step runs the official ProteoWizard
container instead. msconvert_exe in your config.yml is ignored on these platforms and
can be left blank.
-
Install Docker (Engine on Linux, Desktop on macOS) and make sure the
dockercommand works for your user withoutsudo(on Linux, add yourself to thedockergroup). -
Pull the ProteoWizard image:
docker pull chambm/pwiz-skyline-i-agree-to-the-vendor-licenses
The image name embeds acceptance of the vendor licenses – by using it you agree to the Thermo/vendor terms bundled in ProteoWizard.
-
Run the pipeline as usual – the step starts a container per RAW file on its own. It bind-mounts your RAW folder and mzML output folder into the container, so both must be on the local filesystem and visible to the Docker daemon (on macOS, listed under Docker Desktop → Settings → Resources → File sharing). Network shares will not work.
The pipeline starts from a completed FragPipe/MSFragger offset search – it does not run the
peptide search itself. Two of its outputs are required inputs here: the psm.tsv per experiment
and the *_calibrated.mzML files (see Required inputs).
We recommend running the search with the workflow shipped in this package:
salt/data/Dec2025_offset_4SU_7nt_labile.workflow
Load it in FragPipe via Workflow → Load workflow → Custom, then set the FASTA path
(database.db-path) to your own database.
The workflow is tuned for 4SU peptide–RNA cross-links and configures, among others:
| Setting | Value | Why it matters here |
|---|---|---|
mass_offsets |
5538 offsets, up to 7 nt | The RNA adduct masses searched as precursor offsets |
labile_search_mode |
labile |
Treats the RNA adduct as labile, as expected for SALT |
localize_delta_mass |
true |
Localizes the cross-link to a residue |
calibrate_mass |
2 |
Produces the *_calibrated.mzML files this pipeline consumes |
calibrate_mass=2 is the setting to preserve if you adapt the workflow: without mass calibration
FragPipe writes no *_calibrated.mzML, and calibrated_mzml_transfer has nothing to transfer
isotope patterns onto.
Match preprocessing.peptide_seq_frag and preprocessing.peptide_seq_ce in your config.yml to
the fragmentation method and collision energy actually used for the peptide search.
Option A: exact development environment. SALT_env.yml pins the
versions behind the published results (Python 3.14, pyopenms 3.5.0, pandas 3.0.2, …).
conda env create -f SALT_env.yml
conda activate SALT
pip install -e .Option B Use this if Option A fails: exact pins on Python 3.14 do not
resolve on every platform. Installs from pyproject.toml, whose lower bounds
(numpy>=1.24, pyopenms>=3.0, …) let pip pick versions that fit your interpreter. Any Python
>=3.11 works; 3.12 has the widest pyopenms wheel coverage. venv works instead of conda.
conda create -n SALT python=3.12
conda activate SALT
pip install -e .This installs everything the pipeline needs, including plotting. Add ".[dev]" if you want to run
the tests.
Before running, copy config.example.yml to config.yml and edit the paths inside it (see
Configuration below).
The pipeline is a sequence of standalone steps orchestrated by a runner. Run it as a module
from the directory containing your config.yml (e.g. the repo root):
python -m saltThere are no command-line options. Everything is configured in config.yml (run with --help
to print usage).
The list of steps and their filenames is read from config.yml under the pipeline: section.
config.yml is the single source of truth for all configuration, including max_workers, which
sets how many parallel-safe steps run at once. The runner locates config.yml by walking up from
the current directory.
salt/matched_scan_plotting.py draws the annotated
spectra for matched scans. Run it manually after the pipeline finishes:
python -m salt.matched_scan_plottingIt writes one PDF per experiment to analysis_output_dir
({experiment}_RSM_matching_scores_spectra.pdf) and reads the same config.yml as the pipeline,
so it needs no arguments.
It is deliberately not a pipeline step. The block of TEST_* constants at the top of the
script selects what gets plotted – one manifest row, a list of scan IDs from a file, a row cap, an
intensity cutoff, a custom output suffix – so you can plot every spectrum or just the handful you
care about. Edit those constants in the script and re-run; with all of them left at None it plots
every experiment and every matched scan.
Copy config.example.yml to config.yml and edit it for your data. The
settings you most often need to change:
| Key | What it sets |
|---|---|
input_dir |
Folder with your RAW files and the FragPipe .fp-manifest |
analysis_output_dir |
Where pipeline outputs are written |
search_folder |
FragPipe search results (holds {experiment}/psm.tsv), under input_dir |
manifest_input_file |
FragPipe manifest filename (.fp-manifest), under input_dir |
msconvert_exe |
Path to the ProteoWizard msconvert executable (Windows only; leave blank on Linux/macOS, where msconvert runs via Docker) |
matching.theoretical_spectra_path |
Theoretical fragment library to match against |
tolerance.value / tolerance.unit |
Fragment peak-matching tolerance (e.g. 0.01 / da) |
max_workers |
How many parallel-safe steps run at once (1 = serial, safest) |
FDR_control.rsm_fdr_level |
Decoy-based FDR threshold (e.g. 0.01 = 1%) |
These are the common entry points; config.example.yml is commented and lists the full set of
options (calibration, deisotoper parameters, intensity filtering, plotting, etc.).
Under input_dir, the pipeline expects:
- your Thermo RAW files,
- the
{rawfile}_calibrated.mzMLfiles written by FragPipe's mass calibration (calibrate_mass=2) – one per RAW file, - the FragPipe
.fp-manifest(manifest_input_file), and - FragPipe search results under
search_folder, one subfolder per experiment, each containing apsm.tsv(i.e.{search_folder}/{experiment}/psm.tsv).
The *_calibrated.mzML files must sit next to their RAW file – the pipeline resolves each one
as a sibling of the rawfile_path recorded in the manifest, not by searching input_dir. If FragPipe
writes them somewhere else, copy or move them alongside the RAW files before
running.
FragPipe's calibrated mzML is mass-calibrated but deisotoped. The RNA ladder deconvolution downstream needs the full isotope envelope, which the calibrated file no longer contains.
So calibrated_mzml_transfer needs both: msconvert re-converts the RAW to get the full peak list,
and the calibrated file supplies the m/z calibration. The merged result,
{rawfile}_calibrated_transferred.mzML, is calibrated and keeps its isotope envelopes. See the
docstring of salt/calibrated_mzml_transfer.py for
how the transfer works.
matching.theoretical_spectra_path selects one of the libraries in salt/data/. You
only need to rebuild them when the chemistry changes – a different crosslinked nucleotide, or a change
to the fragmentation method. Generate your own fragment annotations following the same format as massdiff_adduct_annot_10nt_original.csv, then run the scripts in prepare/, chained by
build_libraries.py:
python -m prepare.build_libraries --raw-adduct-table /path/to/massdiff_adduct_annot_10nt_original.csvRun it from the repo root. It runs four steps in order –
build the ladder mass tables, add the diagnostic-ion rows, generate the target plus the
methyl/fluoro/azido decoy libraries, then copy the results into salt/data/. Use --from /
--only to run part of the chain, and --no-deploy to generate without overwriting the shipped
libraries. --help lists the steps.
This is a manual, occasional task and is deliberately not part of python -m salt.
Results are written to analysis_output_dir. Each pipeline run also drops a timestamped
pipeline_parallel_*.log and a config_*.yml snapshot there. The main per-experiment output is:
{experiment}_PSM_RSM.csv– PSMs annotated with their FDR-filtered RSM identification, the final result of the pipeline.
Intermediate per-experiment tables are written alongside it (RSM = RNA–spectrum match):
| File | What it holds |
|---|---|
{experiment}_low_id_ms2_df.csv |
Processed low-ID MS2 scans – the spectra fed into the RSM search |
{experiment}_high_ce_psm.csv |
Processed high-CE peptide identifications (PSMs) – the other search input |
{experiment}_RSM_matching_scores.csv |
Every matching candidate per scan (all scored candidates) |
{experiment}_low_id_ms2_df_RSM_report.csv |
All RSMs per scan, with the FDR verdict and per-scan E-values (see below) |
{experiment}_PSM_RSM.csv |
PSMs annotated with their RSM identification |
Manifest-level summary reports (e.g. manifest_RSM_FDR_report.csv) are written as well.
{experiment}_low_id_ms2_df_RSM_report.csv holds every scored row – targets and non-canonical
decoys alike – rather than only the FDR survivors. Each scan contributes one winning hypothesis,
target and decoy competing within a single concatenated library of equal size; winners are ranked by
decreasing Hyperscore_XL and the FDR among reported targets is estimated as (D + 1) / T at each
score threshold. On top of the scoring columns the report adds:
| Column | Meaning |
|---|---|
is_decoy |
The row's ID matched one of FDR_control.noncanonical_decoy_id_prefixes |
FDR_cutoff |
Lowest accepted Hyperscore_XL (constant per experiment) – an output of the q-value filter, not its input |
pass_FDR_threshold |
True for target rows with q_value <= FDR_control.rsm_fdr_level; False for decoys and non-passing targets |
n_candidates, best_Evalue_XL, Evalue_notes |
Per-scan E-value results, computed for FDR-surviving scans only (empty elsewhere) |
The per-threshold counts, FDR estimates and q-values are deliberately not columns of the report:
each is a function of Hyperscore_XL and is_decoy, which every row carries, so storing them would
only restate what those two columns already determine. The cutoff's own diagnostics (cumulative T and
D, estimated_FDR, q_value) go to the step's log file.
To get the accepted RSMs, filter on pass_FDR_threshold == True. Retaining the decoy rows is
deliberate: Hyperscore_XL plus is_decoy is exactly the input the q-value calculation needs, so
the whole thing can be reproduced – or re-thresholded at a different FDR_control.rsm_fdr_level –
from this file alone, without re-running the pipeline.
pytestTests cover the core pure functions (JSON array round-trip, RSM file resolution), the fragment-matching algorithm, and the Hyperscore computation. They are unit tests – they need no MS data and run in about a second.
salt/– the importable package: pipeline steps, the runner (runner.py), the shared utility module (utils.py), and the package entry point (__main__.py)salt/data/– reference data shipped with the package (theoretical-spectra libraries and the adduct-annotation table), loaded at runtime viaimportlib.resourcesprepare/– scripts to generate/customize the theoretical-spectra libraries insalt/data/, chained bybuild_libraries.py(run manually when analyzing a different modified nucleotide or changing the RNA moiety/mass definitions – see Regenerating the theoretical-spectra libraries)tests/– pytest unit tests