diff --git a/alphabase/constants/const_files/pg_reader.yaml b/alphabase/constants/const_files/pg_reader.yaml index 9c1e6bdc..9d86db1e 100644 --- a/alphabase/constants/const_files/pg_reader.yaml +++ b/alphabase/constants/const_files/pg_reader.yaml @@ -97,7 +97,6 @@ fragpipe: "uniprot_ids": "Protein ID" "genes": "Gene Names" "description": "Description" - measurement_regex: "raw": "Intensity$" "razor": "Razor Intensity$" @@ -106,3 +105,16 @@ fragpipe: "lfq": "MaxLFQ Intensity$" "lfq_unique": "MaxLFQ Unique Intensity$" "lfq_total": "MaxLFQ Total Intensity$" + + +# mzTab +# version 2.0.0 (2019-03) +mztab: + reader_type: "mztab" + column_mapping: + "uniprot_ids": "accession" + "description": "description" + "source_db": "database" + measurement_regex: + "assay": "^protein_abundance_assay\\[[0-9]+\\]" # The protein's abundance as measured in the given assay through whatever technique was employed + "study_variable": "^protein_abundance_study_variable\\[[0-9]+\\]" # The protein's abundance as measured in the given study variable (condition) through whatever technique was employed diff --git a/alphabase/pg_reader/__init__.py b/alphabase/pg_reader/__init__.py index 459e7588..c16e3971 100644 --- a/alphabase/pg_reader/__init__.py +++ b/alphabase/pg_reader/__init__.py @@ -3,6 +3,7 @@ from .diann_pg_reader import DiannPGReader from .fragpipe_pg_reader import FragPipePGReader from .maxquant_pg_reader import MaxQuantPGReader +from .mztab_pg_reader import MZTabPGReader from .pg_reader import pg_reader_provider from .spectronaut_reader import SpectronautPGReader @@ -14,4 +15,5 @@ "MaxQuantPGReader", "SpectronautPGReader", "FragPipePGReader", + "MZTabPGReader", ] diff --git a/alphabase/pg_reader/mztab_pg_reader.py b/alphabase/pg_reader/mztab_pg_reader.py new file mode 100644 index 00000000..3219c25d --- /dev/null +++ b/alphabase/pg_reader/mztab_pg_reader.py @@ -0,0 +1,116 @@ +"""FragPipe protein group reader.""" + +from pathlib import Path +from typing import Literal, Optional, Union + +import pandas as pd + +from .pg_reader import PGReaderBase, pg_reader_provider + + +class MZTabPGReader(PGReaderBase): + """Reader for MZTab search engine output. + + MZTab is a standardized tab-delimited format for reporting proteomics and metabolomics results. + The format organizes data into distinct sections: metadata (MTD), protein groups (PRH/PRT), + peptides (PEH/PEP), PSMs (PSH/PSM), and small molecules (SMH/SML), with each section identified + by specific three-letter prefixes. This reader extracts protein-level quantification data from + the PRT lines, which contain protein abundances across samples or study variables. + + Example: + ------- + Per default, the reader will return the raw intensities from the `razor` method. Additional protein features are stored + in the dataframe index, samples are stored as columns. + + .. code-block:: python + + from alphabase.pg_reader import MZTabPGReader + + # Get raw intensities + reader = MZTabPGReader() + results = reader.import_file(path) + + + References: + ---------- + - Griss, J. et al. The mzTab Data Exchange Format: Communicating Mass-spectrometry-based Proteomics and Metabolomics Experimental Results to a Wider Audience*. Molecular & Cellular Proteomics 13, 2765-2775 (2014). + - Official MZTab Repository: https://github.com/HUPO-PSI/mzTab.git + - Official documentation: https://hupo-psi.github.io/mzTab/ + + """ + + _reader_type: str = "mztab" + + _PROTEIN_ROW_INDICATOR: str = "PRT" + _PROTEIN_HEADER_INDICATOR: str = "PRH" + _SEPARATOR: str = "\t" + + def __init__( # noqa: D107 inherited from base class + self, + *, + column_mapping: Optional[dict[str, str]] = None, + measurement_regex: Union[ + str, Literal["assay", "study_variable"], None # noqa: PYI051 raw and lfq are special cases and not equivalent to string + ] = "assay", + ): + super().__init__( + column_mapping=column_mapping, measurement_regex=measurement_regex + ) + + def _load_file(self, file_path: str) -> pd.DataFrame: + """Load MZTab file and extract protein data section. + + Parameters + ---------- + file_path : str + Path to MZTab file + + Returns + ------- + pd.DataFrame + DataFrame containing protein data from MZTab file + + Notes + ----- + Protein lines are indicated with a leading `PRT`. The protein metadata header is + indicated with a leading `PRH`. The file is tab separated. + + Raises + ------ + ValueError + If no protein data or metadata is found in the file + + """ + file_path = Path(file_path) + protein_header = None + protein_rows = [] + + with file_path.open() as f: + for line in f: + line_stripped = line.strip() + + if line_stripped.startswith(self._PROTEIN_HEADER_INDICATOR): + # Protein header line - remove 'PRH' prefix and parse columns + header_content = line_stripped[3:].strip() + protein_header = header_content.split(self._SEPARATOR) + + elif line_stripped.startswith(self._PROTEIN_ROW_INDICATOR): + # Protein data line - remove 'PRT' prefix and parse data + row_content = line_stripped[3:].strip() + protein_rows.append(row_content.split(self._SEPARATOR)) + + # Validate that we found protein data + if protein_header is None: + raise ValueError( + f"No protein header ({self._PROTEIN_HEADER_INDICATOR}) found in MZTab file" + ) + + if not protein_rows: + raise ValueError( + f"No protein data rows ({self._PROTEIN_ROW_INDICATOR}) found in MZTab file" + ) + + return pd.DataFrame(protein_rows, columns=protein_header) + + +pg_reader_provider.register_reader("mztab", reader_class=MZTabPGReader) diff --git a/docs/api.rst b/docs/api.rst index 7a4c8218..72a33557 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -36,6 +36,7 @@ Reader :maxdepth: 2 modules_psm_reader + modules_pg_reader I/O diff --git a/docs/modules_pg_reader.rst b/docs/modules_pg_reader.rst new file mode 100644 index 00000000..cc27886d --- /dev/null +++ b/docs/modules_pg_reader.rst @@ -0,0 +1,17 @@ +alphabase.pg_reader +=========================== + +All pg_readers can be accessed by +:obj:`pg_reader_provider `. + +.. toctree:: + :maxdepth: 1 + + pg_reader/pg_base + pg_reader/alphadia_pg_reader + pg_reader/alphapept_pg_reader + pg_reader/diann_pg_reader + pg_reader/fragpipe_pg_reader + pg_reader/maxquant_pg_reader + pg_reader/mztab_pg_reader + pg_reader/spectronaut_pg_reader diff --git a/docs/pg_reader/alphadia_pg_reader.rst b/docs/pg_reader/alphadia_pg_reader.rst new file mode 100644 index 00000000..c4c79426 --- /dev/null +++ b/docs/pg_reader/alphadia_pg_reader.rst @@ -0,0 +1,7 @@ +alphabase.pg_reader.alphadia_pg_reader +====================================== + +.. automodule:: alphabase.pg_reader.alphadia_pg_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/pg_reader/alphapept_pg_reader.rst b/docs/pg_reader/alphapept_pg_reader.rst new file mode 100644 index 00000000..e1d08593 --- /dev/null +++ b/docs/pg_reader/alphapept_pg_reader.rst @@ -0,0 +1,7 @@ +alphabase.pg_reader.alphapept_pg_reader +======================================= + +.. automodule:: alphabase.pg_reader.alphapept_pg_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/pg_reader/diann_pg_reader.rst b/docs/pg_reader/diann_pg_reader.rst new file mode 100644 index 00000000..15a7d0e8 --- /dev/null +++ b/docs/pg_reader/diann_pg_reader.rst @@ -0,0 +1,7 @@ +alphabase.pg_reader.diann_pg_reader +=================================== + +.. automodule:: alphabase.pg_reader.diann_pg_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/pg_reader/fragpipe_pg_reader.rst b/docs/pg_reader/fragpipe_pg_reader.rst new file mode 100644 index 00000000..fff83518 --- /dev/null +++ b/docs/pg_reader/fragpipe_pg_reader.rst @@ -0,0 +1,7 @@ +alphabase.pg_reader.fragpipe_pg_reader +====================================== + +.. automodule:: alphabase.pg_reader.fragpipe_pg_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/pg_reader/maxquant_pg_reader.rst b/docs/pg_reader/maxquant_pg_reader.rst new file mode 100644 index 00000000..5744f171 --- /dev/null +++ b/docs/pg_reader/maxquant_pg_reader.rst @@ -0,0 +1,7 @@ +alphabase.pg_reader.maxquant_pg_reader +====================================== + +.. automodule:: alphabase.pg_reader.maxquant_pg_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/pg_reader/mztab_pg_reader.rst b/docs/pg_reader/mztab_pg_reader.rst new file mode 100644 index 00000000..69475764 --- /dev/null +++ b/docs/pg_reader/mztab_pg_reader.rst @@ -0,0 +1,7 @@ +alphabase.pg_reader.mztab_pg_reader +====================================== + +.. automodule:: alphabase.pg_reader.mztab_pg_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/pg_reader/pg_base.rst b/docs/pg_reader/pg_base.rst new file mode 100644 index 00000000..a18e1710 --- /dev/null +++ b/docs/pg_reader/pg_base.rst @@ -0,0 +1,7 @@ +alphabase.pg_reader.base +======================== + +.. automodule:: alphabase.pg_reader.pg_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/pg_reader/spectronaut_pg_reader.rst b/docs/pg_reader/spectronaut_pg_reader.rst new file mode 100644 index 00000000..5bc39fca --- /dev/null +++ b/docs/pg_reader/spectronaut_pg_reader.rst @@ -0,0 +1,7 @@ +alphabase.pg_reader.spectronaut_reader +====================================== + +.. automodule:: alphabase.pg_reader.spectronaut_reader + :members: + :undoc-members: + :show-inheritance: diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 1da056d0..ce29948e 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -218,3 +218,32 @@ def example_fragpipe_tsv(tmp_path) -> Path: reference = get_local_reference_data(test_case_name=TEST_FILE_NAME) return file_path, reference + + +@pytest.fixture(scope="function") +def example_mztab(tmp_path) -> Path: + """Get and parse real MZTab report""" + URL = "https://datashare.biochem.mpg.de/s/ayieQHU9zjY89cl" + REF_URL = "https://datashare.biochem.mpg.de/s/o7K2FEAmpmLUglS" + + return get_remote_data_with_ref(url=URL, ref_url=REF_URL, directory=tmp_path) + + +@pytest.fixture(scope="function") +def example_mztab_minimal(tmp_path) -> Path: + """Get and parse minimal MZTab report for local testing""" + TEST_FILE_NAME = "pg_mztab_minimal" + TEST_DATA = """COM Only variable modifications can be reported when the original source is a PRIDE XML file + +PRH accession description taxid species database database_version search_engine best_search_engine_score[1] search_engine_score[1]_ms_run[1] num_psms_ms_run[1] num_peptides_distinct_ms_run[1] num_peptides_unique_ms_run[1] ambiguity_members modifications protein_coverage protein_abundance_assay[1] protein_abundance_assay[2] protein_abundance_assay[3] protein_abundance_assay[4] +PRT 223462890 Spna2 protein [Mus musculus] 10090 Mus musculus (Mouse) NCBInr_2010_10 nr_101020.fasta [MS, MS:1001207, Mascot, ] 6539.67 6539.67 157 92 null null null 0 1 0.853 0.864 0.791 +PRT 19855078 RecName: Full=Sodium/potassium-transporting ATPase subunit alpha-3; Short=Na(+)/K(+) ATPase alpha-3 subunit; AltName: Full=Na(+)/K(+) ATPase alpha(III) subunit; AltName: Full=Sodium pump subunit alpha-3 10090 Mus musculus (Mouse) NCBInr_2010_10 nr_101020.fasta [MS, MS:1001207, Mascot, ] 6331.91 6331.91 144 49 null null 32-MOD:00425,525-MOD:00425,606-MOD:00425,725-MOD:00425,739-MOD:00425,940-MOD:00425 0 null null null null +PRT 21450277 sodium/potassium-transporting ATPase subunit alpha-1 precursor [Mus musculus] 10090 Mus musculus (Mouse) NCBInr_2010_10 nr_101020.fasta [MS, MS:1001207, Mascot, ] 4577.11 4577.11 112 39 null null 42-MOD:00425,616-MOD:00425,749-MOD:00425,950-MOD:00425 0 1 0.776 0.819 0.687 +PRT 6978545 sodium/potassium-transporting ATPase subunit alpha-2 precursor [Rattus norvegicus] 10090 Mus musculus (Mouse) NCBInr_2010_10 nr_101020.fasta [MS, MS:1001207, Mascot, ] 4342.81 4342.81 108 42 null null 40-MOD:00425,613-MOD:00425,746-MOD:00425,947-MOD:00425 0 1 0.784 0.848 0.693 + """ + file_path = write_test_data( + data=TEST_DATA, directory=tmp_path, test_case_name=TEST_FILE_NAME + ) + reference = get_local_reference_data(test_case_name=TEST_FILE_NAME) + + return file_path, reference diff --git a/tests/integration/reference_data/reference_pg_mztab_minimal.parquet b/tests/integration/reference_data/reference_pg_mztab_minimal.parquet new file mode 100644 index 00000000..2bd11b55 Binary files /dev/null and b/tests/integration/reference_data/reference_pg_mztab_minimal.parquet differ diff --git a/tests/integration/test_pg_reader_provider.py b/tests/integration/test_pg_reader_provider.py index 084a5c95..5d5d1f7a 100644 --- a/tests/integration/test_pg_reader_provider.py +++ b/tests/integration/test_pg_reader_provider.py @@ -6,6 +6,7 @@ DiannPGReader, FragPipePGReader, MaxQuantPGReader, + MZTabPGReader, SpectronautPGReader, pg_reader_provider, ) @@ -57,3 +58,11 @@ def test_reader_provider(self) -> None: reader = pg_reader_provider.get_reader("fragpipe") assert isinstance(reader, FragPipePGReader) + + +class TestMZTabPGReaderProvider: + def test_reader_provider(self) -> None: + """Test whether reader provider initializes MZTab protein group reader correctly.""" + reader = pg_reader_provider.get_reader("mztab") + + assert isinstance(reader, MZTabPGReader) diff --git a/tests/integration/test_pg_readers.py b/tests/integration/test_pg_readers.py index 76fa76a4..13875838 100644 --- a/tests/integration/test_pg_readers.py +++ b/tests/integration/test_pg_readers.py @@ -9,6 +9,7 @@ DiannPGReader, FragPipePGReader, MaxQuantPGReader, + MZTabPGReader, SpectronautPGReader, ) from alphabase.pg_reader.keys import PGCols @@ -208,3 +209,25 @@ def test_import_real_file(self, example_fragpipe_tsv: str) -> None: result_df = reader.import_file(file_path=file_path) pd.testing.assert_frame_equal(result_df, reference) + + +class TestMZTabPGReader: + def test_import_real_file(self, example_mztab: str) -> None: + """Test import of real MZTab file""" + file_path, reference = example_mztab + + reader = MZTabPGReader() + + result_df = reader.import_file(file_path=file_path) + + pd.testing.assert_frame_equal(result_df, reference) + + def test_import_minimal_example(self, example_mztab_minimal: str) -> None: + """Test import of minimal example MZTab file""" + file_path, reference = example_mztab_minimal + + reader = MZTabPGReader() + + result_df = reader.import_file(file_path=file_path) + + pd.testing.assert_frame_equal(result_df, reference)