Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ Supported file formats
`AlphaDIA precursors TSV <https://alphadia.readthedocs.io/en/latest/quickstart.html#output-files>`_ ``alphadia`` ✅ ❌
`CBOR <https://psm-utils.readthedocs.io/en/stable/api/psm_utils.io#module-psm_utils.io.cbor>`_ ``cbor`` ✅ ✅ Requires the optional ``cbor2`` dependency (``pip install psm-utils[cbor]``)
`DIA-NN TSV <https://github.com/vdemichev/DiaNN#output>`_ ``diann`` ✅ ❌
`DIA-NN report.parquet <https://github.com/vdemichev/DiaNN#output>`_ ``diann_parquet`` ✅ ❌
`FlashLFQ generic TSV <https://github.com/smith-chem-wisc/FlashLFQ/wiki/Identification-Input-Formats>`_ ``flashlfq`` ✅ ✅
`FragPipe PSM TSV <https://fragpipe.nesvilab.org/docs/tutorial_fragpipe_outputs.html#psmtsv/>`_ ``fragpipe`` ✅ ❌
`ionbot CSV <https://ionbot.cloud/>`_ ``ionbot`` ✅ ❌
Expand Down
6 changes: 6 additions & 0 deletions psm_utils/io/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ class FileType(TypedDict):
"extension": ".tsv",
"filename_pattern": r"^.*(?:_|\.)?diann\.tsv$",
},
"diann_parquet": {
"reader": diann.DIANNParquetReader,
"writer": None,
"extension": ".parquet",
"filename_pattern": r"^.*(?:_|\.)?report\.parquet$",
},
"parquet": { # List after more specific Parquet patterns to avoid matching conflicts
"reader": parquet.ParquetReader,
"writer": parquet.ParquetWriter,
Expand Down
85 changes: 79 additions & 6 deletions psm_utils/io/diann.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@
Reader for PSM files from DIA-NN.

Reads the '.tsv' file as defined on the
`DIA-NN documentation page <https://github.com/vdemichev/DiaNN/tree/1.8.1?tab=readme-ov-file#main-output-reference>`_.
`DIA-NN documentation page <https://github.com/vdemichev/DiaNN/tree/1.8.1?tab=readme-ov-file#main-output-reference>`_,
or the ``report.parquet`` main report written by DIA-NN 2.0 and later.

Notes
-----
- DIA-NN calculates q-values at both the run and library level. The run-level q-value is used as
the PSM q-value.
- DIA-NN currently does not return precursor m/z values.
- DIA-NN currently does not support C-terminal modifications in its searches.
- The classic ``.tsv`` main report does not contain precursor m/z values or decoy status; these
are set to ``None``/``False``, respectively. The ``report.parquet`` main report does contain
both, and they are used when available.
- The ``report.parquet`` main report does not contain a PSM-level score (no ``CScore`` column)
or an MS2 scan number (no ``MS2.Scan`` column). The score is set to ``None`` and
``Precursor.Id`` is used as the spectrum identifier instead.

"""

Expand All @@ -22,6 +28,7 @@
from typing import Any, cast

import pandas as pd
import pyarrow.parquet as pq # type: ignore[import]

from psm_utils.io._base_classes import ReaderBase
from psm_utils.io._utils import set_csv_field_size_limit
Expand Down Expand Up @@ -90,11 +97,11 @@ def _get_peptide_spectrum_match(
run=psm_dict["Run"],
is_decoy=False,
qvalue=psm_dict["Q.Value"],
pep=float(psm_dict["PEP"]),
score=float(psm_dict["CScore"]),
pep=psm_dict["PEP"],
score=psm_dict["CScore"],
precursor_mz=None, # Not returned by DIA-NN :(
retention_time=float(psm_dict["RT"]),
ion_mobility=float(psm_dict["IM"]),
retention_time=psm_dict["RT"],
ion_mobility=psm_dict["IM"],
protein_list=psm_dict["Protein.Ids"].split(";"),
source="diann",
rank=None,
Expand Down Expand Up @@ -130,3 +137,69 @@ def from_dataframe(cls, dataframe: pd.DataFrame) -> PSMList:
"""Create a PSMList from a DIA-NN Pandas DataFrame."""
records = cast(list[dict[str, str]], dataframe.to_dict(orient="records"))
return PSMList(psm_list=[cls._get_peptide_spectrum_match(entry) for entry in records])


class DIANNParquetReader(ReaderBase):
"""Reader for the DIA-NN 2.0+ ``report.parquet`` main report."""

def __init__(self, filename: str | Path, *args: Any, **kwargs: Any) -> None:
"""
Reader for DIA-NN ``report.parquet`` file.

Parameters
----------
filename : str or Path
Path to PSM file.
*args
Additional positional arguments passed to the base class.
**kwargs
Additional keyword arguments passed to the base class.

"""
super().__init__(filename, *args, **kwargs)

def __iter__(self) -> Iterator[PSM]:
"""Iterate over file and return PSMs one-by-one."""
with pq.ParquetFile(self.filename) as pq_file:
for batch in pq_file.iter_batches():
for row in batch.to_pylist():
yield self._get_peptide_spectrum_match(row, self.filename)

@staticmethod
def _get_peptide_spectrum_match(
psm_dict: dict[str, Any], filename: str | Path | None = None
) -> PSM:
"""Parse a single PSM from a DIA-NN ``report.parquet`` file."""
rescoring_features: dict[str, Any] = {}
for ft in RESCORING_FEATURES:
try:
rescoring_features[ft] = psm_dict[ft]
except KeyError:
continue

return PSM(
peptidoform=DIANNTSVReader._parse_peptidoform(
psm_dict["Modified.Sequence"], str(psm_dict["Precursor.Charge"])
),
spectrum_id=psm_dict["Precursor.Id"],
run=psm_dict["Run"],
is_decoy=bool(psm_dict["Decoy"]),
qvalue=psm_dict["Q.Value"],
Comment thread
RalfG marked this conversation as resolved.
pep=psm_dict["PEP"],
score=None, # Not returned by DIA-NN in the `report.parquet` main report :(
precursor_mz=psm_dict["Precursor.Mz"],
retention_time=psm_dict["RT"],
ion_mobility=psm_dict["IM"],
protein_list=psm_dict["Protein.Ids"].split(";"),
source="diann",
rank=None,
provenance_data=({"diann_filename": str(filename)} if filename else {}),
rescoring_features=rescoring_features,
metadata={},
)

@classmethod
def from_dataframe(cls, dataframe: pd.DataFrame) -> PSMList:
"""Create a PSMList from a DIA-NN ``report.parquet`` Pandas DataFrame."""
records = cast(list[dict[str, Any]], dataframe.to_dict(orient="records"))
return PSMList(psm_list=[cls._get_peptide_spectrum_match(entry) for entry in records])
Binary file added tests/test_data/test_diann.parquet
Binary file not shown.
42 changes: 41 additions & 1 deletion tests/test_io/test_diann.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Tests for psm_utils.io.diann."""

from psm_utils.io.diann import DIANNTSVReader
from psm_utils.io.diann import DIANNParquetReader, DIANNTSVReader
from psm_utils.psm import PSM

test_psm = PSM(
Expand Down Expand Up @@ -52,3 +52,43 @@ def test__parse_peptidoform(self):
reader = DIANNTSVReader("./tests/test_data/test_diann.tsv")
for (peptide, charge), expected in test_cases:
assert reader._parse_peptidoform(peptide, charge) == expected


test_parquet_psm = PSM(
peptidoform="[UNIMOD:1]-AAAAEINVKDPKEDLETSVVDEGR/4",
spectrum_id="(UniMod:1)AAAAEINVKDPKEDLETSVVDEGR4",
run="LFQ_Orbitrap_AIF_Yeast_03",
collection=None,
spectrum=None,
is_decoy=False,
score=None,
qvalue=0.000548193,
pep=0.0104343,
precursor_mz=621.5723,
retention_time=75.2574,
ion_mobility=0.0,
protein_list=["P38156"],
rank=None,
source="diann",
metadata={},
rescoring_features={
"RT": 75.2574,
"Predicted.RT": 75.2713,
"iRT": 33.9222,
"Predicted.iRT": 33.8999,
"Ms1.Profile.Corr": 0.347567,
"Ms1.Area": 849940.0,
"IM": 0.0,
"iIM": 0.0,
"Predicted.IM": 0.0,
"Predicted.iIM": 0.0,
},
)


class TestDIANNParquetReader:
def test_iter(self):
with DIANNParquetReader("./tests/test_data/test_diann.parquet") as reader:
for psm in reader:
psm.provenance_data = {}
assert psm == test_parquet_psm
Loading