Skip to content

API reference

Generated from the docstrings in the source, so it cannot fall out of step with the code.

pymzlib.pride

pride

PRIDE Archive access, backed by mzLib's PrideArchiveClient.

The PRIDE Archive (https://www.ebi.ac.uk/pride/archive/) is EBI's public proteomics data repository. This module lets a Python user list what is in a project and pull files down, using the same paging, URL-resolution, and safe-download logic that mzLib uses in C#.

>>> import pymzlib
>>> files = pymzlib.pride.list_files("PXD000001")
>>> len(files)
8
>>> files[0].file_name
'PRIDE_Exp_Complete_Ac_22134.pride.mztab.gz'
>>> raw = [f for f in files if f.category == "RAW"]
>>> pymzlib.pride.download("PXD000001", "downloads", category="RAW")

ProjectNotFoundError

Bases: PyMzLibError

No project with that accession exists, or it has no files.

PRIDE answers an unknown accession with an empty result rather than a 404, so earlier versions of pyMzLib returned an empty list. That was a mistake: an empty list is indistinguishable from "this project genuinely has nothing matching", so a typo'd accession produced a script that reported "0 files, done" and moved on. A wrong answer that looks like a right answer is worse than an error.

Source code in pkg/python/src/pymzlib/pride.py
class ProjectNotFoundError(_bridge.PyMzLibError):
    """No project with that accession exists, or it has no files.

    PRIDE answers an unknown accession with an empty result rather than a 404, so earlier versions
    of pyMzLib returned an empty list. That was a mistake: an empty list is indistinguishable from
    "this project genuinely has nothing matching", so a typo'd accession produced a script that
    reported "0 files, done" and moved on. A wrong answer that looks like a right answer is worse
    than an error.
    """

PrideFile dataclass

One file belonging to a PRIDE Archive project.

Attributes:

Name Type Description
file_name str

The file's name, e.g. "run1.raw".

file_size_bytes int

Size in bytes as reported by PRIDE.

checksum str

The repository's checksum, or "" if it provides none.

category str

The file category, e.g. "RAW", "PEAK", "SEARCH", "OTHER".

https_url str | None

A direct HTTPS download URL, or None when the file is only reachable by a protocol that cannot be fetched over HTTPS (Aspera-only files).

locations list[dict[str, str]]

Every published location as {"accession", "name", "value"} dicts, for callers that want the raw controlled-vocabulary terms.

submission_date / publication_date / updated_date

Repository timestamps.

Source code in pkg/python/src/pymzlib/pride.py
@dataclass(frozen=True)
class PrideFile:
    """One file belonging to a PRIDE Archive project.

    Attributes:
        file_name: The file's name, e.g. ``"run1.raw"``.
        file_size_bytes: Size in bytes as reported by PRIDE.
        checksum: The repository's checksum, or ``""`` if it provides none.
        category: The file category, e.g. ``"RAW"``, ``"PEAK"``, ``"SEARCH"``, ``"OTHER"``.
        https_url: A direct HTTPS download URL, or ``None`` when the file is only reachable
            by a protocol that cannot be fetched over HTTPS (Aspera-only files).
        locations: Every published location as ``{"accession", "name", "value"}`` dicts, for
            callers that want the raw controlled-vocabulary terms.
        submission_date / publication_date / updated_date: Repository timestamps.
    """

    file_name: str
    file_size_bytes: int
    checksum: str
    category: str
    category_accession: str
    https_url: str | None
    locations: list[dict[str, str]] = field(default_factory=list)
    submission_date: datetime | None = None
    publication_date: datetime | None = None
    updated_date: datetime | None = None
    project_accession: str = ""

    @property
    def size_mb(self) -> float:
        """The file size in megabytes, for the common case of eyeballing a manifest."""
        return self.file_size_bytes / 1_000_000

    @property
    def extension(self) -> str:
        """The file's lowercase extension including the dot, e.g. ``".raw"``. Empty if none."""
        return Path(self.file_name).suffix.lower()

    @property
    def downloadable(self) -> bool:
        """Whether this file can be fetched by :func:`download` (i.e. has an HTTPS location)."""
        return self.https_url is not None

    def as_dict(self) -> dict[str, Any]:
        """Return every attribute, **including the computed ones**, as a plain dict.

        Use this rather than ``vars(f)`` when building a table. ``size_mb``, ``extension`` and
        ``downloadable`` are properties, so ``vars()`` and ``dataclasses.asdict()`` both skip
        them — which silently produced a DataFrame missing the three attributes the
        documentation pushes hardest, including the ``downloadable`` flag used to filter out
        files that cannot be fetched.

        Example:
            >>> import pandas as pd                                    # doctest: +SKIP
            >>> df = pd.DataFrame([f.as_dict() for f in files])        # doctest: +SKIP
        """
        record = asdict(self)
        record["size_mb"] = self.size_mb
        record["extension"] = self.extension
        record["downloadable"] = self.downloadable
        return record

    @classmethod
    def _from_wire(cls, payload: dict[str, Any], project_accession: str = "") -> "PrideFile":
        return cls(
            file_name=payload.get("file_name", ""),
            file_size_bytes=int(payload.get("file_size_bytes", 0)),
            checksum=payload.get("checksum", ""),
            category=payload.get("category", ""),
            category_accession=payload.get("category_accession", ""),
            https_url=payload.get("https_url"),
            locations=list(payload.get("locations") or []),
            submission_date=_parse_timestamp(payload.get("submission_date")),
            publication_date=_parse_timestamp(payload.get("publication_date")),
            updated_date=_parse_timestamp(payload.get("updated_date")),
            project_accession=project_accession,
        )
size_mb property
size_mb: float

The file size in megabytes, for the common case of eyeballing a manifest.

extension property
extension: str

The file's lowercase extension including the dot, e.g. ".raw". Empty if none.

downloadable property
downloadable: bool

Whether this file can be fetched by :func:download (i.e. has an HTTPS location).

as_dict
as_dict() -> dict[str, Any]

Return every attribute, including the computed ones, as a plain dict.

Use this rather than vars(f) when building a table. size_mb, extension and downloadable are properties, so vars() and dataclasses.asdict() both skip them — which silently produced a DataFrame missing the three attributes the documentation pushes hardest, including the downloadable flag used to filter out files that cannot be fetched.

Example

import pandas as pd # doctest: +SKIP df = pd.DataFrame([f.as_dict() for f in files]) # doctest: +SKIP

Source code in pkg/python/src/pymzlib/pride.py
def as_dict(self) -> dict[str, Any]:
    """Return every attribute, **including the computed ones**, as a plain dict.

    Use this rather than ``vars(f)`` when building a table. ``size_mb``, ``extension`` and
    ``downloadable`` are properties, so ``vars()`` and ``dataclasses.asdict()`` both skip
    them — which silently produced a DataFrame missing the three attributes the
    documentation pushes hardest, including the ``downloadable`` flag used to filter out
    files that cannot be fetched.

    Example:
        >>> import pandas as pd                                    # doctest: +SKIP
        >>> df = pd.DataFrame([f.as_dict() for f in files])        # doctest: +SKIP
    """
    record = asdict(self)
    record["size_mb"] = self.size_mb
    record["extension"] = self.extension
    record["downloadable"] = self.downloadable
    return record

list_files

list_files(accession: str, page_size: int = 100, timeout: float | None = 300) -> list[PrideFile]

Return the file manifest of a PRIDE Archive project.

This is what PRIDE's REST API publishes, which is not always everything in the project. For PXD000001 the API returns 8 files while the FTP tree holds 13, and the five it omits include the two largest: ...60min_01-20141210.mzML (450 MB) and the matching .mzXML (472 MB), exactly the modern open-format conversions most people want. The omission is PRIDE's, not mzLib's. If completeness matters, cross-check the FTP directory at https://ftp.pride.ebi.ac.uk/pride/data/archive/<year>/<month>/<accession>/.

Paging is handled for you: however many pages the project spans, you get one list.

Parameters:

Name Type Description Default
accession str

The project accession, e.g. "PXD000001".

required
page_size int

How many files to request per underlying API call. Only affects how the manifest is fetched, never what you get back.

100
timeout float | None

Seconds to allow for the whole fetch.

300

Returns:

Type Description
list[PrideFile]

Every file in the project, in repository order. An unknown accession yields an empty

list[PrideFile]

list rather than an error — that is PRIDE's own behavior, preserved here.

Raises:

Type Description
UsageError

the accession is blank or the page size is not positive.

BridgeError

PRIDE returned an error status or was unreachable.

Source code in pkg/python/src/pymzlib/pride.py
def list_files(accession: str, page_size: int = 100, timeout: float | None = 300) -> list[PrideFile]:
    """Return the file manifest of a PRIDE Archive project.

    **This is what PRIDE's REST API publishes, which is not always everything in the
    project.** For PXD000001 the API returns **8** files while the FTP tree holds **13**, and
    the five it omits include the two largest: ``...60min_01-20141210.mzML`` (450 MB) and the
    matching ``.mzXML`` (472 MB), exactly the modern open-format conversions most people want.
    The omission is PRIDE's, not mzLib's. If completeness matters, cross-check the FTP
    directory at ``https://ftp.pride.ebi.ac.uk/pride/data/archive/<year>/<month>/<accession>/``.

    Paging is handled for you: however many pages the project spans, you get one list.

    Args:
        accession: The project accession, e.g. ``"PXD000001"``.
        page_size: How many files to request per underlying API call. Only affects how the
            manifest is fetched, never what you get back.
        timeout: Seconds to allow for the whole fetch.

    Returns:
        Every file in the project, in repository order. An unknown accession yields an empty
        list rather than an error — that is PRIDE's own behavior, preserved here.

    Raises:
        UsageError: the accession is blank or the page size is not positive.
        BridgeError: PRIDE returned an error status or was unreachable.
    """
    canonical = _normalise_accession(accession)
    if isinstance(page_size, bool) or not isinstance(page_size, int):
        raise _bridge.UsageError(
            f"page_size must be a whole number; got {type(page_size).__name__} ({page_size!r})."
        )
    if page_size <= 0:
        raise _bridge.UsageError(f"page_size must be positive; got {page_size}.")
    if page_size > 2_147_483_647:
        raise _bridge.UsageError(f"page_size is larger than the API allows; got {page_size}.")

    data = _bridge.invoke(
        "pride", "files",
        "--accession", canonical,
        "--page-size", str(page_size),
        timeout=timeout,
    )
    files = [PrideFile._from_wire(item, canonical) for item in data.get("files", [])]

    if not files:
        raise ProjectNotFoundError(
            f"PRIDE returned no files for '{canonical}'. Either the accession does not exist "
            "(check for a typo) or the project is private. PRIDE does not distinguish the two, "
            "so neither can pyMzLib."
        )
    return files

download

download(accession: str, destination: str | Path, category: str | None = None, extensions: Sequence[str] | None = None, overwrite: bool = True, timeout: float | None = None) -> list[Path]

Download a project's files, optionally filtered, and return where they landed.

Files are streamed to a temporary name and moved into place only once complete, so an interrupted download never leaves a truncated file behind.

Parameters:

Name Type Description Default
accession str

The project accession, e.g. "PXD000001".

required
destination str | Path

Directory to write into. Created if it does not exist.

required
category str | None

Keep only files of this category, e.g. "RAW". None keeps all.

None
extensions Sequence[str] | None

Keep only files with these extensions, e.g. [".raw", ".mzML"]. None keeps all. Combined with category as AND.

None
overwrite bool

When False, a file already present at the destination is left alone and not re-fetched — a cheap resume for a large project.

True
timeout float | None

Seconds to allow. None (the default) waits as long as it takes, which is usually what you want for multi-gigabyte projects.

None

Returns:

Type Description
list[Path]

The paths written, in manifest order.

Raises:

Type Description
UsageError

the accession or destination is blank.

BridgeError

a request failed, or a selected file has no HTTPS location.

Source code in pkg/python/src/pymzlib/pride.py
def download(
    accession: str,
    destination: str | Path,
    category: str | None = None,
    extensions: Sequence[str] | None = None,
    overwrite: bool = True,
    timeout: float | None = None,
) -> list[Path]:
    """Download a project's files, optionally filtered, and return where they landed.

    Files are streamed to a temporary name and moved into place only once complete, so an
    interrupted download never leaves a truncated file behind.

    Args:
        accession: The project accession, e.g. ``"PXD000001"``.
        destination: Directory to write into. Created if it does not exist.
        category: Keep only files of this category, e.g. ``"RAW"``. ``None`` keeps all.
        extensions: Keep only files with these extensions, e.g. ``[".raw", ".mzML"]``.
            ``None`` keeps all. Combined with ``category`` as AND.
        overwrite: When ``False``, a file already present at the destination is left alone
            and not re-fetched — a cheap resume for a large project.
        timeout: Seconds to allow. ``None`` (the default) waits as long as it takes, which
            is usually what you want for multi-gigabyte projects.

    Returns:
        The paths written, in manifest order.

    Raises:
        UsageError: the accession or destination is blank.
        BridgeError: a request failed, or a selected file has no HTTPS location.
    """
    canonical = _normalise_accession(accession)
    target = _normalise_destination(destination)
    wanted = _normalise_extensions(extensions)

    args = ["pride", "download", "--accession", canonical, "--dest", str(target)]
    if category is not None:
        if not isinstance(category, str):
            raise _bridge.UsageError(
                f"category must be a string like 'RAW'; got {type(category).__name__} ({category!r})."
            )
        if not category.strip():
            raise _bridge.UsageError(
                "category is empty. Omit it to download every category, rather than passing a "
                "blank value — a filter that selects nothing must not silently select everything."
            )
        args += ["--category", _reject_flag_like("category", category.strip())]
    if wanted:
        args += ["--ext", _reject_flag_like("extensions", ",".join(wanted))]
    if not overwrite:
        args.append("--no-overwrite")

    data = _bridge.invoke(*args, timeout=timeout)
    written = [Path(p) for p in data.get("paths", [])]

    # Same doctrine as list_files and download_files, which this function was inconsistent with:
    # a filter that matched nothing is nearly always a filter that does not mean what its author
    # thought, and reporting success with an empty list lets a batch script carry on as though
    # the work had been done.
    if not written and (category is not None or wanted):
        raise _bridge.UsageError(
            f"No file in {canonical} matched "
            f"{'category ' + repr(category) if category else ''}"
            f"{' and ' if category and wanted else ''}"
            f"{'extensions ' + repr(wanted) if wanted else ''}. "
            "Use list_files() to see what the project actually contains — note that "
            "compressed files such as 'x.mgf.gz' have the extension '.gz'."
        )
    return written

download_files

download_files(files: Iterable[PrideFile], destination: str | Path, overwrite: bool = True, timeout: float | None = None) -> list[Path]

Download exactly the files you selected, and nothing else.

This is the counterpart to :func:list_files, and usually the one you want. Filter the manifest however you like — in Python, with the full expressiveness of Python — and hand the result straight back:

>>> files = list_files("PXD000001")                             # doctest: +SKIP
>>> small = [f for f in files if f.size_mb < 5 and f.downloadable]
>>> download_files(small, "downloads")                          # doctest: +SKIP

:func:download's category and extensions filters can only express what they were built to express; "under 5 MB", "the three newest", or "everything except the MGF" cannot be said in that vocabulary at all. They can all be said in a list comprehension.

Parameters:

Name Type Description Default
files Iterable[PrideFile]

The :class:PrideFile objects to fetch, from one project.

required
destination str | Path

Directory to write into. Created if it does not exist.

required
overwrite bool

When False, files already present are left alone and not re-fetched.

True
timeout float | None

Seconds to allow. None waits as long as it takes.

None

Returns:

Type Description
list[Path]

The paths written, in the order the repository lists them.

Raises:

Type Description
UsageError

the selection is empty, spans several projects, or includes a file with no HTTPS location.

Source code in pkg/python/src/pymzlib/pride.py
def download_files(
    files: Iterable[PrideFile],
    destination: str | Path,
    overwrite: bool = True,
    timeout: float | None = None,
) -> list[Path]:
    """Download exactly the files you selected, and nothing else.

    This is the counterpart to :func:`list_files`, and usually the one you want. Filter the
    manifest however you like — in Python, with the full expressiveness of Python — and hand the
    result straight back:

        >>> files = list_files("PXD000001")                             # doctest: +SKIP
        >>> small = [f for f in files if f.size_mb < 5 and f.downloadable]
        >>> download_files(small, "downloads")                          # doctest: +SKIP

    :func:`download`'s ``category`` and ``extensions`` filters can only express what they were
    built to express; "under 5 MB", "the three newest", or "everything except the MGF" cannot be
    said in that vocabulary at all. They can all be said in a list comprehension.

    Args:
        files: The :class:`PrideFile` objects to fetch, from one project.
        destination: Directory to write into. Created if it does not exist.
        overwrite: When ``False``, files already present are left alone and not re-fetched.
        timeout: Seconds to allow. ``None`` waits as long as it takes.

    Returns:
        The paths written, in the order the repository lists them.

    Raises:
        UsageError: the selection is empty, spans several projects, or includes a file with no
            HTTPS location.
    """
    target = _normalise_destination(destination)
    selected = list(files)

    if not selected:
        raise _bridge.UsageError(
            "No files selected. An empty selection is almost always a filter that did not match "
            "what you expected, so pyMzLib refuses it rather than reporting success."
        )
    for item in selected:
        if not isinstance(item, PrideFile):
            raise _bridge.UsageError(
                f"download_files expects PrideFile objects from list_files(); got "
                f"{type(item).__name__} ({item!r})."
            )

    unreachable = [f.file_name for f in selected if not f.downloadable]
    if unreachable:
        raise _bridge.UsageError(
            f"{len(unreachable)} of {len(selected)} selected files have no HTTPS location and "
            f"cannot be downloaded (e.g. {unreachable[0]!r}). Filter on `.downloadable` first."
        )

    accessions = {f.project_accession for f in selected if f.project_accession}
    if len(accessions) > 1:
        raise _bridge.UsageError(
            f"All files must come from one project; got {sorted(accessions)}."
        )
    if not accessions:
        raise _bridge.UsageError(
            "These PrideFile objects carry no project accession, so pyMzLib cannot tell which "
            "project to fetch from. Obtain them from list_files()."
        )

    args = [
        "pride", "download",
        "--accession", accessions.pop(),
        "--dest", str(target),
        "--names-from-stdin",
    ]
    if not overwrite:
        args.append("--no-overwrite")

    # The selection travels on stdin rather than argv: a few thousand names would blow the ~32 KB
    # command-line ceiling. The framing is newline-delimited, which is *almost* general — a POSIX
    # file name may legally contain a newline, so such a name would split into two and silently
    # select the wrong files. PRIDE has never published one, but "never seen it" is not a contract,
    # so it is refused explicitly rather than mis-parsed quietly.
    embedded_newline = [f.file_name for f in selected if "\n" in f.file_name or "\r" in f.file_name]
    if embedded_newline:
        raise _bridge.UsageError(
            f"Cannot select {embedded_newline[0]!r}: the file name contains a line break, which the "
            "selection format cannot represent. Please open an issue — this is a limitation worth "
            "fixing properly if a real repository ever publishes such a name."
        )

    payload = "\n".join(f.file_name for f in selected)
    data = _bridge.invoke(*args, stdin=payload, timeout=timeout)
    return [Path(p) for p in data.get("paths", [])]

total_size_bytes

total_size_bytes(files: Iterable[PrideFile]) -> int

Sum the sizes of some files.

This is the size PRIDE reports, which is not the number of bytes you will transfer. For compressed files PRIDE frequently reports the decompressed size: in PXD000001 the reported size of PRIDE_Exp_Complete_Ac_22134.pride.mgf.gz is 16,448,103 bytes, exactly what gzip -l gives as its uncompressed length, while the actual download is 5,984,662 bytes, 2.75x smaller.

It is also a sum over an incomplete manifest. For PXD000001 this returns 0.51 GB; the project on disk is 1.44 GB, because PRIDE's API omits five files including the two largest (see :func:list_files). The two errors run in opposite directions and do not cancel.

files = list_files("PXD000001") # doctest: +SKIP total_size_bytes(f for f in files if f.category == "RAW") / 1e9 # doctest: +SKIP 0.51

Source code in pkg/python/src/pymzlib/pride.py
def total_size_bytes(files: Iterable[PrideFile]) -> int:
    """Sum the sizes of some files.

    **This is the size PRIDE reports, which is not the number of bytes you will transfer.**
    For compressed files PRIDE frequently reports the *decompressed* size: in PXD000001 the
    reported size of ``PRIDE_Exp_Complete_Ac_22134.pride.mgf.gz`` is 16,448,103 bytes, exactly
    what ``gzip -l`` gives as its uncompressed length, while the actual download is 5,984,662
    bytes, 2.75x smaller.

    **It is also a sum over an incomplete manifest.** For PXD000001 this returns 0.51 GB; the
    project on disk is 1.44 GB, because PRIDE's API omits five files including the two largest
    (see :func:`list_files`). The two errors run in opposite directions and do **not** cancel.

    >>> files = list_files("PXD000001")           # doctest: +SKIP
    >>> total_size_bytes(f for f in files if f.category == "RAW") / 1e9   # doctest: +SKIP
    0.51
    """
    return sum(f.file_size_bytes for f in files)

pymzlib.peptidoform

peptidoform

Peptidoform-level questions: digest an annotated protein and fragment its peptides.

The question this answers is the one a mass spectrometrist actually asks — what fragments would I see for this protein's peptides? — in one call:

>>> import pymzlib
>>> digest = pymzlib.peptidoform.fragments("P02768")          # doctest: +SKIP
>>> len(digest.peptides)                                      # doctest: +SKIP
303

The defaults are opinions, not placeholders. Tryptic with the proline rule, two missed cleavages, ETD, both termini, UniProt's annotated modifications applied. They are the choices this lab makes when it does not have a reason to choose otherwise, so the common question needs no parameters — and every one of them is reachable, because the point is to open the doors, not to hide them.

Fragment dataclass

One backbone fragment ion.

Attributes:

Name Type Description
product_type str

The ion series, e.g. "c" or "zDot" for ETD, "b"/"y" for CID.

fragment_number int

Position in the series — c3 is the third from the N-terminus.

neutral_mass float

Monoisotopic neutral mass in daltons. Not an m/z: no proton has been added and no charge assumed.

Fragments deliberately expose no mz(). Converting one correctly requires the fixed charge within this fragment's span — a c or z ion carries only the permanently charged modifications on the residues it contains, not the whole peptide's :attr:Peptide.fixed_charges. For an unmodified or neutrally-modified peptide, (neutral_mass + z * PROTON_MASS) / z is correct; for a fragment bearing a quaternary-ammonium modification (e.g. trimethyllysine) it is not, and per-fragment charge accounting is not yet provided.

neutral_loss float

Neutral loss in daltons, 0.0 when there is none.

residue_position int

One-based residue position in the peptide.

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class Fragment:
    """One backbone fragment ion.

    Attributes:
        product_type: The ion series, e.g. ``"c"`` or ``"zDot"`` for ETD, ``"b"``/``"y"`` for CID.
        fragment_number: Position in the series — ``c3`` is the third from the N-terminus.
        neutral_mass: Monoisotopic neutral mass in daltons. **Not** an m/z: no proton has been
            added and no charge assumed.

            Fragments deliberately expose no ``mz()``. Converting one correctly requires the fixed
            charge *within this fragment's span* — a c or z ion carries only the permanently
            charged modifications on the residues it contains, not the whole peptide's
            :attr:`Peptide.fixed_charges`. For an unmodified or neutrally-modified peptide,
            ``(neutral_mass + z * PROTON_MASS) / z`` is correct; for a fragment bearing a
            quaternary-ammonium modification (e.g. trimethyllysine) it is not, and per-fragment
            charge accounting is not yet provided.
        neutral_loss: Neutral loss in daltons, ``0.0`` when there is none.
        residue_position: One-based residue position in the peptide.
    """

    product_type: str
    fragment_number: int
    neutral_mass: float
    neutral_loss: float
    residue_position: int

Peptide dataclass

One digested peptide, with its modifications and fragment ions.

Attributes:

Name Type Description
base_sequence str

The bare amino-acid sequence.

full_sequence str

The sequence with modifications written inline, as mzLib renders them.

monoisotopic_mass float

The neutral monoisotopic mass, modifications included.

one_based_start / one_based_end

Position within the parent protein.

missed_cleavages int

How many cleavage sites the peptide spans.

fixed_charges int

Charges the peptide carries before any protonation, from modifications that leave a permanently charged residue. :meth:mz accounts for these.

modifications list[dict[str, Any]]

Each applied modification. one_based_residue indexes the peptide's own residues and is None for a terminal modification, which carries terminus instead. (mzLib's internal dictionary reserves slot 1 for the N-terminus, so its keys are one past the residue they modify; that is corrected here rather than passed on.)

fragments list[Fragment]

The fragment ions for the requested dissociation type.

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class Peptide:
    """One digested peptide, with its modifications and fragment ions.

    Attributes:
        base_sequence: The bare amino-acid sequence.
        full_sequence: The sequence with modifications written inline, as mzLib renders them.
        monoisotopic_mass: The neutral monoisotopic mass, modifications included.
        one_based_start / one_based_end: Position within the parent protein.
        missed_cleavages: How many cleavage sites the peptide spans.
        fixed_charges: Charges the peptide carries before any protonation, from modifications
            that leave a permanently charged residue. :meth:`mz` accounts for these.
        modifications: Each applied modification. ``one_based_residue`` indexes the peptide's own
            residues and is ``None`` for a terminal modification, which carries ``terminus``
            instead. (mzLib's internal dictionary reserves slot 1 for the N-terminus, so its keys
            are one past the residue they modify; that is corrected here rather than passed on.)
        fragments: The fragment ions for the requested dissociation type.
    """

    base_sequence: str
    full_sequence: str
    monoisotopic_mass: float
    length: int
    one_based_start: int
    one_based_end: int
    missed_cleavages: int
    fixed_charges: int = 0
    modifications: list[dict[str, Any]] = field(default_factory=list)
    fragments: list[Fragment] = field(default_factory=list)

    @property
    def is_modified(self) -> bool:
        """Whether this peptide carries at least one modification."""
        return bool(self.modifications)

    def mz(self, charge: int) -> float:
        """Return the m/z of the intact peptide at a given total charge.

        Two conventions are handled explicitly here, because getting either wrong is invisible in
        the answer.

        **The proton mass (1.007276), not the hydrogen atom (1.007825).** The difference is
        0.55 mDa — 1.1 ppm at m/z 500, which on an Orbitrap is a match versus a miss. Libraries
        differ on this and rarely say which they used.

        **Fixed charges are not double-counted.** Some modifications leave the residue permanently
        charged: trimethylation of a lysine ε-amine gives a quaternary ammonium, and UniProt
        records the delta as 43.054227 — C₃H₇ *minus an electron* — rather than the neutral
        43.054775. So :attr:`monoisotopic_mass` already carries that charge, and only
        ``charge - fixed_charges`` protons are added. Adding a full complement would put a 2+
        trimethylated peptide half a Thomson high, on the most important histone modification
        there is.

        A peptide with a fixed charge is therefore observable at that charge with no protonation
        at all, which is why ``charge`` may not be below :attr:`fixed_charges`.

        Args:
            charge: The total charge state, at least :attr:`fixed_charges` and at least 1.
        """
        if not isinstance(charge, int) or isinstance(charge, bool) or charge < 1:
            raise _bridge.UsageError(f"charge must be a positive whole number; got {charge!r}.")
        if charge < self.fixed_charges:
            raise _bridge.UsageError(
                f"This peptide already carries {self.fixed_charges} fixed charge(s) from its "
                f"modifications, so it cannot be observed at charge {charge}."
            )
        return (self.monoisotopic_mass + (charge - self.fixed_charges) * PROTON_MASS) / charge

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "Peptide":
        return cls(
            base_sequence=payload.get("base_sequence", ""),
            full_sequence=payload.get("full_sequence", ""),
            monoisotopic_mass=float(payload.get("monoisotopic_mass", 0.0)),
            length=int(payload.get("length", 0)),
            one_based_start=int(payload.get("one_based_start", 0)),
            one_based_end=int(payload.get("one_based_end", 0)),
            missed_cleavages=int(payload.get("missed_cleavages", 0)),
            fixed_charges=int(payload.get("fixed_charges", 0)),
            modifications=list(payload.get("modifications") or []),
            fragments=[
                Fragment(
                    product_type=f.get("product_type", ""),
                    fragment_number=int(f.get("fragment_number", 0)),
                    neutral_mass=float(f.get("neutral_mass", 0.0)),
                    neutral_loss=float(f.get("neutral_loss", 0.0)),
                    residue_position=int(f.get("residue_position", 0)),
                )
                for f in (payload.get("fragments") or [])
            ],
        )
is_modified property
is_modified: bool

Whether this peptide carries at least one modification.

mz
mz(charge: int) -> float

Return the m/z of the intact peptide at a given total charge.

Two conventions are handled explicitly here, because getting either wrong is invisible in the answer.

The proton mass (1.007276), not the hydrogen atom (1.007825). The difference is 0.55 mDa — 1.1 ppm at m/z 500, which on an Orbitrap is a match versus a miss. Libraries differ on this and rarely say which they used.

Fixed charges are not double-counted. Some modifications leave the residue permanently charged: trimethylation of a lysine ε-amine gives a quaternary ammonium, and UniProt records the delta as 43.054227 — C₃H₇ minus an electron — rather than the neutral 43.054775. So :attr:monoisotopic_mass already carries that charge, and only charge - fixed_charges protons are added. Adding a full complement would put a 2+ trimethylated peptide half a Thomson high, on the most important histone modification there is.

A peptide with a fixed charge is therefore observable at that charge with no protonation at all, which is why charge may not be below :attr:fixed_charges.

Parameters:

Name Type Description Default
charge int

The total charge state, at least :attr:fixed_charges and at least 1.

required
Source code in pkg/python/src/pymzlib/peptidoform.py
def mz(self, charge: int) -> float:
    """Return the m/z of the intact peptide at a given total charge.

    Two conventions are handled explicitly here, because getting either wrong is invisible in
    the answer.

    **The proton mass (1.007276), not the hydrogen atom (1.007825).** The difference is
    0.55 mDa — 1.1 ppm at m/z 500, which on an Orbitrap is a match versus a miss. Libraries
    differ on this and rarely say which they used.

    **Fixed charges are not double-counted.** Some modifications leave the residue permanently
    charged: trimethylation of a lysine ε-amine gives a quaternary ammonium, and UniProt
    records the delta as 43.054227 — C₃H₇ *minus an electron* — rather than the neutral
    43.054775. So :attr:`monoisotopic_mass` already carries that charge, and only
    ``charge - fixed_charges`` protons are added. Adding a full complement would put a 2+
    trimethylated peptide half a Thomson high, on the most important histone modification
    there is.

    A peptide with a fixed charge is therefore observable at that charge with no protonation
    at all, which is why ``charge`` may not be below :attr:`fixed_charges`.

    Args:
        charge: The total charge state, at least :attr:`fixed_charges` and at least 1.
    """
    if not isinstance(charge, int) or isinstance(charge, bool) or charge < 1:
        raise _bridge.UsageError(f"charge must be a positive whole number; got {charge!r}.")
    if charge < self.fixed_charges:
        raise _bridge.UsageError(
            f"This peptide already carries {self.fixed_charges} fixed charge(s) from its "
            f"modifications, so it cannot be observed at charge {charge}."
        )
    return (self.monoisotopic_mass + (charge - self.fixed_charges) * PROTON_MASS) / charge

ModificationCensus dataclass

What UniProt annotates, and what could actually be used.

A modification is only usable for mass spectrometry if it has a defined target and a defined mass. A glycosylation site annotated as "N-linked (GlcNAc...) asparagine" has neither a fixed composition nor a fixed mass, so there is nothing to add to a peptide, and a peptide carrying an ambiguous-mass PTM is not identifiable by mass anyway. Such annotations are therefore excluded.

That exclusion is correct. What this class exists for is that you should not have to guess it happened: for serum albumin, 14 sites are applied out of 38 annotated, and without this the 14 arrives with no indication that a rule was ever applied.

Attributes:

Name Type Description
sites int

Distinct residue positions carrying at least one modification. A histone lists several alternatives at one residue — K9me1, K9me2, K9me3, K9ac are four modifications at one site — so this is always the smaller number and is not a modification count.

applied int

Modifications actually placed on the protein.

annotated int

Modification-like features UniProt lists.

by_type list[dict[str, Any]]

One entry per feature type, with count and whether it was loaded.

unresolved list[str]

Modification names UniProt annotated that could not be resolved to a mass — usually because the name is absent from UniProt's own ptmlist. These vanish silently otherwise: on histone H3.1, seven N6-lactoyllysine sites were dropped while the type summary still reported "modified residue … loaded".

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class ModificationCensus:
    """What UniProt annotates, and what could actually be used.

    A modification is only usable for mass spectrometry if it has a defined target **and** a
    defined mass. A glycosylation site annotated as "N-linked (GlcNAc...) asparagine" has neither
    a fixed composition nor a fixed mass, so there is nothing to add to a peptide, and a peptide
    carrying an ambiguous-mass PTM is not identifiable by mass anyway. Such annotations are
    therefore excluded.

    That exclusion is correct. What this class exists for is that you should not have to *guess*
    it happened: for serum albumin, 14 sites are applied out of 38 annotated, and without this
    the 14 arrives with no indication that a rule was ever applied.

    Attributes:
        sites: Distinct residue positions carrying at least one modification. A histone lists
            several alternatives at one residue — K9me1, K9me2, K9me3, K9ac are four
            modifications at one site — so this is always the smaller number and is not a
            modification count.
        applied: Modifications actually placed on the protein.
        annotated: Modification-like features UniProt lists.
        by_type: One entry per feature type, with ``count`` and whether it was ``loaded``.
        unresolved: Modification names UniProt annotated that could not be resolved to a mass —
            usually because the name is absent from UniProt's own ptmlist. These vanish silently
            otherwise: on histone H3.1, seven N6-lactoyllysine sites were dropped while the type
            summary still reported "modified residue … loaded".
    """

    sites: int
    applied: int
    annotated: int
    unresolved: list[str] = field(default_factory=list)
    by_type: list[dict[str, Any]] = field(default_factory=list)

    @property
    def excluded(self) -> int:
        """Annotated features that could not be used because they have no defined mass."""
        return max(0, self.annotated - self.applied)

    def explain(self) -> str:
        """A one-paragraph, human-readable account of what was used and what was not."""
        if not self.excluded:
            return (
                f"All {self.annotated} annotated modifications were applied, across "
                f"{self.sites} residue positions."
            )
        sentences = [
            f"{self.applied} of {self.annotated} annotated modifications were applied, across "
            f"{self.sites} residue positions."
        ]
        excluded_types = [t for t in self.by_type if not t.get("loaded")]
        if excluded_types:
            named = ", ".join(f"{t['count']} × {t['type']}" for t in excluded_types)
            sentences.append(
                f"Excluded by type: {named} — mzLib loads only 'modified residue' "
                "and 'lipid moiety-binding region' annotations, so these were dropped on "
                "feature type alone. The exclusion is usually right: a glycation or "
                "glycosylation annotation describes a labile, heterogeneous adduct, so "
                "assigning it one exact mass and a clean fragment ladder would invent a "
                "species you cannot observe. But the reason is not reported, and the "
                "qualifier is not read: on albumin, 14 of the 24 excluded here are marked "
                "'in vitro' and 2 exist only in disease variants, which are different "
                "grounds for exclusion needing different judgements from you. Read the "
                "annotations on the UniProt entry before concluding anything about a "
                "specific site; this census can only tell you the count "
                "(smith-chem-wisc/mzLib#1112)."
            )
        if self.unresolved:
            sentences.append(
                f"Could not be resolved to a mass: {', '.join(self.unresolved)} — annotated by "
                "UniProt but absent from its own modification list, so they were dropped."
            )
        return " ".join(sentences)
excluded property
excluded: int

Annotated features that could not be used because they have no defined mass.

explain
explain() -> str

A one-paragraph, human-readable account of what was used and what was not.

Source code in pkg/python/src/pymzlib/peptidoform.py
def explain(self) -> str:
    """A one-paragraph, human-readable account of what was used and what was not."""
    if not self.excluded:
        return (
            f"All {self.annotated} annotated modifications were applied, across "
            f"{self.sites} residue positions."
        )
    sentences = [
        f"{self.applied} of {self.annotated} annotated modifications were applied, across "
        f"{self.sites} residue positions."
    ]
    excluded_types = [t for t in self.by_type if not t.get("loaded")]
    if excluded_types:
        named = ", ".join(f"{t['count']} × {t['type']}" for t in excluded_types)
        sentences.append(
            f"Excluded by type: {named} — mzLib loads only 'modified residue' "
            "and 'lipid moiety-binding region' annotations, so these were dropped on "
            "feature type alone. The exclusion is usually right: a glycation or "
            "glycosylation annotation describes a labile, heterogeneous adduct, so "
            "assigning it one exact mass and a clean fragment ladder would invent a "
            "species you cannot observe. But the reason is not reported, and the "
            "qualifier is not read: on albumin, 14 of the 24 excluded here are marked "
            "'in vitro' and 2 exist only in disease variants, which are different "
            "grounds for exclusion needing different judgements from you. Read the "
            "annotations on the UniProt entry before concluding anything about a "
            "specific site; this census can only tell you the count "
            "(smith-chem-wisc/mzLib#1112)."
        )
    if self.unresolved:
        sentences.append(
            f"Could not be resolved to a mass: {', '.join(self.unresolved)} — annotated by "
            "UniProt but absent from its own modification list, so they were dropped."
        )
    return " ".join(sentences)

Digest dataclass

The result of digesting a protein and fragmenting its peptides.

Source code in pkg/python/src/pymzlib/peptidoform.py
@dataclass(frozen=True)
class Digest:
    """The result of digesting a protein and fragmenting its peptides."""

    accession: str
    name: str
    full_name: str
    organism: str
    sequence_length: int
    protease: str
    dissociation: str
    terminus: str
    modifications_applied: bool
    max_modifications: int
    max_isoforms: int
    peptides_at_cap: int
    modification_census: ModificationCensus
    peptides: list[Peptide] = field(default_factory=list)

    @property
    def truncated(self) -> bool:
        """Whether any peptide hit the isoform cap, meaning the result is incomplete.

        A short answer and a truncated answer look identical from the outside. Check this before
        treating a Peptidoform list as exhaustive.
        """
        return self.peptides_at_cap > 0

    @property
    def modified_peptides(self) -> list[Peptide]:
        """Only the peptides carrying at least one modification."""
        return [p for p in self.peptides if p.is_modified]

    @property
    def fragment_count(self) -> int:
        """Total fragment ions across every peptide."""
        return sum(len(p.fragments) for p in self.peptides)
truncated property
truncated: bool

Whether any peptide hit the isoform cap, meaning the result is incomplete.

A short answer and a truncated answer look identical from the outside. Check this before treating a Peptidoform list as exhaustive.

modified_peptides property
modified_peptides: list[Peptide]

Only the peptides carrying at least one modification.

fragment_count property
fragment_count: int

Total fragment ions across every peptide.

fragments

fragments(accession: str, protease: str = 'trypsin|P', dissociation: str = 'ETD', modifications: bool = True, missed_cleavages: int = 2, min_length: int = 7, max_length: int | None = None, max_modifications: int = 2, max_isoforms: int = 1024, terminus: str = 'Both', timeout: float | None = 300) -> Digest

Fetch a UniProt entry, digest it, and fragment every peptide.

Parameters:

Name Type Description Default
accession str

A UniProtKB accession, e.g. "P02768".

required
protease str

Read this if you are coming from MaxQuant or Mascot. mzLib's "trypsin|P" applies the classic Keil rule — cleave after K/R except before proline — and is the default here because it is what a mass spectrometrist usually means. mzLib's plain "trypsin" cleaves before proline too. That is the reverse of the MaxQuant and Mascot convention, where Trypsin/P denotes ignoring the proline rule. On serum albumin the two differ by 7 peptides out of about 200 (195 vs 202; tryptic, 2 missed cleavages, min length 7) - a small count hiding a large semantic difference, since which peptides you get changes wherever a K/R precedes a proline.

'trypsin|P'
dissociation str

"HCD"/"CID" (b and y ions), "ETD", and the rest of mzLib's dissociation types.

"ETD" returns three series, c, zDot and y, not two. The y ions are spurious: ETD cleaves the N-Ca bond and yields c/z, while b/y come from amide cleavage under vibrational activation, and mzLib's EThcD row correctly pairs y with b. ETD's does not. They are about a third of every ETD fragment list, so :attr:Digest.fragment_count over-counts real ETD ions by that much. Tracked as smith-chem-wisc/mzLib#1109. Separately, z ions are suppressed N-terminal to proline while the complementary c ions are not, leaving about 4% of the c series unobservable (smith-chem-wisc/mzLib#1110).

'ETD'
modifications bool

Apply UniProt's annotated modifications.

False is not a clean control. It discards UniProt's whole feature table, which also carries the signal-peptide and propeptide boundaries mzLib digests at, so the peptide list changes too, not only the modifications on it. On albumin, False loses MKWVTFISLLFLFSSAYS (1-18) and WVTFISLLFLFSSAYS (3-18), both unmodified, both ending exactly at the signal-peptide cleavage site. See issue #8.

Still true of the modifications themselves: pass False for the bare sequence — useful as a control, and the difference is usually large.

True
missed_cleavages int

Maximum missed cleavage sites per peptide.

2
min_length int

Shortest peptide to keep. The default of 7 silently discards shorter peptides — roughly a third of a histone digest — so pass min_length=1 when you mean every peptide.

7
max_length int | None

Longest peptide to keep. None means unbounded.

None
max_modifications int

Maximum modifications considered per peptide. Modification isoforms are enumerated combinatorially: histone H3.1 yields 49 bare tryptic peptides, 2,563 at two modifications and 7,040 at three.

2
max_isoforms int

Maximum modification isoforms per peptide position. mzLib's default of 1024 truncates silently when it binds — on H3.1 at four modifications it discards about 30% of the Peptidoforms (13,700 down to 9,536). :attr:Digest.peptides_at_cap reports how many peptides hit it, so a truncated answer is visible rather than merely short.

1024
terminus str

"Both", "N" or "C".

'Both'
timeout float | None

Seconds to allow. Large proteins with many modification isoforms take longer.

300

Returns:

Name Type Description
A Digest

class:Digest. Check :attr:Digest.modification_census before trusting a

Digest

modification count — it reports what was annotated as well as what was applied.

Raises:

Type Description
UsageError

the accession, protease, dissociation type or terminus is not recognised.

ServiceUnavailableError

UniProt was unreachable.

Example

d = fragments("P02768") # doctest: +SKIP print(d.modification_census.explain()) # doctest: +SKIP 14 of 38 annotated modification sites were applied. Excluded: 24 × glycosylation site …

Source code in pkg/python/src/pymzlib/peptidoform.py
def fragments(
    accession: str,
    protease: str = "trypsin|P",
    dissociation: str = "ETD",
    modifications: bool = True,
    missed_cleavages: int = 2,
    min_length: int = 7,
    max_length: int | None = None,
    max_modifications: int = 2,
    max_isoforms: int = 1024,
    terminus: str = "Both",
    timeout: float | None = 300,
) -> Digest:
    """Fetch a UniProt entry, digest it, and fragment every peptide.

    Args:
        accession: A UniProtKB accession, e.g. ``"P02768"``.
        protease: **Read this if you are coming from MaxQuant or Mascot.** mzLib's ``"trypsin|P"``
            applies the classic Keil rule — cleave after K/R *except* before proline — and is the
            default here because it is what a mass spectrometrist usually means. mzLib's plain
            ``"trypsin"`` cleaves before proline too. That is the **reverse** of the MaxQuant and
            Mascot convention, where ``Trypsin/P`` denotes ignoring the proline rule. On serum
            albumin the two differ by 7 peptides out of about 200 (195 vs 202; tryptic, 2
            missed cleavages, min length 7) - a small count hiding a large semantic
            difference, since which peptides you get changes wherever a K/R precedes a
            proline.
        dissociation: ``"HCD"``/``"CID"`` (b and y ions), ``"ETD"``, and the rest of mzLib's
            dissociation types.

            **``"ETD"`` returns three series, c, zDot **and y**, not two.** The y ions are
            spurious: ETD cleaves the N-Ca bond and yields c/z*, while b/y come from amide
            cleavage under vibrational activation, and mzLib's ``EThcD`` row correctly pairs y
            *with* b. ETD's does not. They are about **a third** of every ETD fragment list,
            so :attr:`Digest.fragment_count` over-counts real ETD ions by that much. Tracked
            as smith-chem-wisc/mzLib#1109. Separately, z* ions are suppressed N-terminal to
            proline while the complementary c ions are not, leaving about 4% of the c series
            unobservable (smith-chem-wisc/mzLib#1110).
        modifications: Apply UniProt's annotated modifications.

            **``False`` is not a clean control.** It discards UniProt's whole feature table,
            which also carries the signal-peptide and propeptide boundaries mzLib digests at,
            so the *peptide list* changes too, not only the modifications on it. On albumin,
            ``False`` loses ``MKWVTFISLLFLFSSAYS`` (1-18) and ``WVTFISLLFLFSSAYS`` (3-18),
            both unmodified, both ending exactly at the signal-peptide cleavage site. See
            issue #8.

            Still true of the modifications themselves: pass ``False`` for the bare
            sequence — useful as a control, and the difference is usually large.
        missed_cleavages: Maximum missed cleavage sites per peptide.
        min_length: Shortest peptide to keep. The default of 7 silently discards shorter
            peptides — roughly a third of a histone digest — so pass ``min_length=1`` when you
            mean *every* peptide.
        max_length: Longest peptide to keep. ``None`` means unbounded.
        max_modifications: Maximum modifications considered per peptide. Modification isoforms
            are enumerated combinatorially: histone H3.1 yields 49 bare tryptic peptides, 2,563
            at two modifications and 7,040 at three.
        max_isoforms: Maximum modification isoforms per peptide position. mzLib's default of 1024
            **truncates silently** when it binds — on H3.1 at four modifications it discards
            about 30% of the Peptidoforms (13,700 down to 9,536). :attr:`Digest.peptides_at_cap`
            reports how many peptides hit it, so a truncated answer is visible rather than
            merely short.
        terminus: ``"Both"``, ``"N"`` or ``"C"``.
        timeout: Seconds to allow. Large proteins with many modification isoforms take longer.

    Returns:
        A :class:`Digest`. Check :attr:`Digest.modification_census` before trusting a
        modification count — it reports what was annotated as well as what was applied.

    Raises:
        UsageError: the accession, protease, dissociation type or terminus is not recognised.
        ServiceUnavailableError: UniProt was unreachable.

    Example:
        >>> d = fragments("P02768")                                    # doctest: +SKIP
        >>> print(d.modification_census.explain())                     # doctest: +SKIP
        14 of 38 annotated modification sites were applied. Excluded: 24 × glycosylation site …
    """
    if not isinstance(accession, str) or not accession.strip():
        raise _bridge.UsageError("A UniProt accession is required, e.g. 'P02768'.")
    canonical = accession.strip().upper()
    if not _ACCESSION.match(canonical):
        raise _bridge.UsageError(
            f"'{accession}' is not a valid UniProtKB accession. They look like 'P02768' or "
            "'A0A0B4J2D5' — see https://www.uniprot.org/help/accession_numbers."
        )
    # UniProt's accessions are upper-case and its API is case-sensitive, so the validated
    # canonical form is what gets sent, not the caller's original casing.
    if max_length is not None and (isinstance(max_length, bool) or not isinstance(max_length, int)):
        raise _bridge.UsageError(f"max_length must be a whole number or None; got {max_length!r}.")
    for name, value in (("missed_cleavages", missed_cleavages), ("min_length", min_length),
                        ("max_modifications", max_modifications), ("max_isoforms", max_isoforms)):
        if isinstance(value, bool) or not isinstance(value, int) or value < 0:
            raise _bridge.UsageError(f"{name} must be a non-negative whole number; got {value!r}.")
    if max_isoforms < 1:
        raise _bridge.UsageError(f"max_isoforms must be at least 1; got {max_isoforms}.")

    args = [
        "peptidoform", "fragments",
        "--accession", canonical,
        "--protease", protease,
        "--dissociation", dissociation,
        "--terminus", terminus,
        "--missed-cleavages", str(missed_cleavages),
        "--min-length", str(min_length),
        "--max-length", str(max_length if max_length else 0),
        "--max-mods", str(max_modifications),
        "--max-isoforms", str(max_isoforms),
    ]
    if not modifications:
        args.append("--no-modifications")

    data = _bridge.invoke(*args, timeout=timeout)

    census = ModificationCensus(
        sites=int(data.get("annotated_modification_sites", 0)),
        applied=int(data.get("annotated_modifications_loaded", 0)),
        annotated=int(data.get("uniprot_annotated_features", 0)),
        by_type=list(data.get("uniprot_features_by_type") or []),
        unresolved=list(data.get("unresolved_modifications") or []),
    )

    return Digest(
        accession=data.get("accession", ""),
        name=data.get("name", "") or "",
        full_name=data.get("full_name", "") or "",
        organism=data.get("organism", "") or "",
        sequence_length=int(data.get("sequence_length", 0)),
        protease=data.get("protease", ""),
        dissociation=data.get("dissociation", ""),
        terminus=data.get("terminus", ""),
        modifications_applied=bool(data.get("modifications_applied", False)),
        max_modifications=int(data.get("max_modifications", 0)),
        max_isoforms=int(data.get("max_modification_isoforms", 0)),
        peptides_at_cap=int(data.get("peptides_at_isoform_cap", 0)),
        modification_census=census,
        peptides=[Peptide._from_wire(p) for p in (data.get("peptides") or [])],
    )

pymzlib.flashlfq

flashlfq

Label-free quantification with FlashLFQ: quantify a search's peptides across mzML runs.

The question this answers is the one a quant workflow actually asks — given these identifications and these runs, how much of each peptide and protein is in each run? — in one call:

>>> import pymzlib
>>> result = pymzlib.flashlfq.quantify(                          # doctest: +SKIP
...     psms="AllPSMs.psmtsv",
...     spectra=["run_3.mzML", "run_4.mzML"],
...     match_between_runs=True,
... )
>>> result.peptide_count, result.protein_count                   # doctest: +SKIP
(354, 943)

The whole pipeline is mzLib's: the result file is read by mzLib's Readers, turned into FlashLFQ identifications by mzLib's own converter, and quantified by FlashLfqEngine. MetaMorpheus is not involved — mzLib does it alone.

Names follow mzLib and FlashLFQ deliberately, so a value here means the same thing it does in the FlashLFQ source, the MetaMorpheus output columns, and the FlashLFQ paper: match_between_runs, ppm_tolerance, mbr_ppm_tolerance, sequence, base_sequence, protein_groups, detection_types, ProteinGroup, FlashLfqResults.

.. warning::

Do not quantify an MSFragger psm.tsv with this. mzLib accepts one — it is one of only three formats implementing the record view FlashLFQ consumes — but the numbers that come back are wrong, and wrong silently.

MSFragger writes retention time in seconds; MetaMorpheus writes minutes. mzLib's result-file readers pass the column through without converting it, and FlashLFQ then treats the value as minutes and searches a ±2-minute window around it. A peptide truly eluting at 60 minutes is written as 3600 and then looked for at 3600 minutes - far past the end of any real gradient - so it is simply not found: quantification collapses toward zero rather than failing. This is an mzLib defect, not a pyMzLib one, and it affects any mzLib caller — it is reported upstream. Until it is fixed, quantify MetaMorpheus output only.

pymzlib.readers.identify() will still report an MSFragger file as quantifiable: that reports mzLib's interface, which the file genuinely implements. It is not an endorsement of the numbers.

Three more limits worth knowing before you trust a number, in the "surface it, don't hide it" spirit of the rest of pyMzLib:

  • mzML only, for now. Convert .raw/.d to mzML first; a non-mzML path is rejected up front.
  • A protein intensity can be None. FlashLFQ's median-polish protein quant marks a protein NaN when its peptide matrix is degenerate (too few peptides per run, or identical intensities across runs — a real artifact documented in mzLib's own tests). NaN is not valid JSON, so it arrives here as None — "could not be quantified" — rather than a silently wrong number. A peptide intensity, by contrast, is 0.0 when missing, never None.
  • For match-between-runs, read the peaks, not the peptides. The peptide roll-up (:attr:FlashLfqResults.peptides, mirroring QuantifiedPeptides.tsv) reports far fewer MBR transfers than actually happened — a whole run's transfers can be absent. :attr:FlashLfqResults.peaks (and :meth:FlashLfqResults.mbr_peaks) is the complete surface; :attr:FlashLfqResults.mbr_peak_count is the number to trust.

SpectraFileInfo dataclass

One quantified run, mirroring mzLib's MassSpectrometry.SpectraFileInfo.

Attributes:

Name Type Description
file_name str

The run's base name (no directory, no extension) — the key used everywhere else here to look up this run's intensity.

full_path str

The mzML path as provided.

condition str

The sample-group label, or "" if none was given.

biological_replicate / technical_replicate / fraction

The experimental-design coordinates.

peak_count int

Chromatographic peaks quantified in this run.

mbr_peak_count int

Of those, how many were transferred by match-between-runs — peaks quantified in this run for a peptide that was never identified in it. Zero unless match_between_runs was set.

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class SpectraFileInfo:
    """One quantified run, mirroring mzLib's ``MassSpectrometry.SpectraFileInfo``.

    Attributes:
        file_name: The run's base name (no directory, no extension) — the key used everywhere else
            here to look up this run's intensity.
        full_path: The mzML path as provided.
        condition: The sample-group label, or ``""`` if none was given.
        biological_replicate / technical_replicate / fraction: The experimental-design coordinates.
        peak_count: Chromatographic peaks quantified in this run.
        mbr_peak_count: Of those, how many were transferred by match-between-runs — peaks quantified
            in this run for a peptide that was never identified *in* it. Zero unless
            ``match_between_runs`` was set.
    """

    file_name: str
    full_path: str
    condition: str
    biological_replicate: int
    technical_replicate: int
    fraction: int
    peak_count: int
    mbr_peak_count: int

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "SpectraFileInfo":
        return cls(
            file_name=payload.get("file_name", ""),
            full_path=payload.get("full_path", ""),
            condition=payload.get("condition", ""),
            biological_replicate=int(payload.get("biological_replicate", 0)),
            technical_replicate=int(payload.get("technical_replicate", 0)),
            fraction=int(payload.get("fraction", 0)),
            peak_count=int(payload.get("peak_count", 0)),
            mbr_peak_count=int(payload.get("mbr_peak_count", 0)),
        )

Peptide dataclass

A quantified peptide, mirroring FlashLFQ's Peptide.

Attributes:

Name Type Description
sequence str

The full (modified) sequence, as FlashLFQ renders it — the identity FlashLFQ quantifies. Two different modification states of one base sequence are two peptides.

base_sequence str

The bare amino-acid sequence.

protein_groups str

The protein group(s) this peptide belongs to, ;-joined.

intensities dict[str, Any]

Run base name → intensity in that run. Missing is 0.0, never None — a null on the wire is resolved to 0.0 when parsed, so the invariant holds by construction rather than by hope (issue #7). Note too that where a peptide has several peaks in one run this roll-up reports one of them rather than their sum, so pivoting :attr:FlashLfqResults.peaks yourself will not reproduce these intensities exactly. Counting presence is unaffected; summing intensity is not. (unlike proteins). This roll-up mirrors FlashLFQ's QuantifiedPeptides.tsv and does not fully reflect match-between-runs — many transferred peptides read 0.0 / "NotDetected" here. For MBR-inclusive quantities use :attr:FlashLfqResults.peaks.

detection_types dict[str, str]

Run base name → how it was quantified there. Values FlashLFQ emits: "MSMS" (identified and quantified in this run), "MBR" (transferred by match-between-runs), "MSMSIdentifiedButNotQuantified" (an ID here but no usable peak), "MSMSAmbiguousPeakfinding" (more than one peptide fits the peak), and "NotDetected".

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class Peptide:
    """A quantified peptide, mirroring FlashLFQ's ``Peptide``.

    Attributes:
        sequence: The full (modified) sequence, as FlashLFQ renders it — the identity FlashLFQ
            quantifies. Two different modification states of one base sequence are two peptides.
        base_sequence: The bare amino-acid sequence.
        protein_groups: The protein group(s) this peptide belongs to, ``;``-joined.
        intensities: Run base name → intensity in that run. Missing is ``0.0``, **never ``None``** — a ``null``
            on the wire is resolved to ``0.0`` when parsed, so the invariant holds by
            construction rather than by hope (issue #7). Note too that where a peptide has
            several peaks in one run this roll-up reports **one** of them rather than their
            sum, so pivoting :attr:`FlashLfqResults.peaks` yourself will not reproduce these
            intensities exactly. Counting presence is unaffected; summing intensity is not.
            (unlike proteins). This roll-up mirrors FlashLFQ's ``QuantifiedPeptides.tsv`` and does
            **not** fully reflect match-between-runs — many transferred peptides read ``0.0`` /
            ``"NotDetected"`` here. For MBR-inclusive quantities use :attr:`FlashLfqResults.peaks`.
        detection_types: Run base name → how it was quantified there. Values FlashLFQ emits:
            ``"MSMS"`` (identified and quantified in this run), ``"MBR"`` (transferred by
            match-between-runs), ``"MSMSIdentifiedButNotQuantified"`` (an ID here but no usable
            peak), ``"MSMSAmbiguousPeakfinding"`` (more than one peptide fits the peak), and
            ``"NotDetected"``.
    """

    sequence: str
    base_sequence: str
    protein_groups: str
    intensities: dict[str, Any]
    detection_types: dict[str, str]

    def intensity(self, file_name: str) -> Any:
        """This peptide's intensity in the named run.

        **``0.0`` — not ``None`` — means "not quantified here."** (Only *protein* intensities are
        ever ``None``.) Treat ``0.0`` as missing, not as a measured absence: log-transforming it
        will mislead. And note a peptide that FlashLFQ *transferred* into this run by
        match-between-runs may still read ``0.0`` here — see :attr:`FlashLfqResults.peaks`. Returns
        ``0.0`` for a run that was never provided.
        """
        return self.intensities.get(file_name, 0.0)

    def detection_type(self, file_name: str) -> str:
        """How this peptide was quantified in the named run (``"NotDetected"`` if it was not)."""
        return self.detection_types.get(file_name, "NotDetected")

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "Peptide":
        return cls(
            sequence=payload.get("sequence", ""),
            base_sequence=payload.get("base_sequence", ""),
            protein_groups=payload.get("protein_groups", ""),
            # A null crossing the wire becomes 0.0 here, so the documented invariant --
            # "missing is 0.0, never None" -- is true by construction rather than by hope.
            # The bridge routes every double through a finite check, so a NaN peptide
            # intensity arrives as null; leaving it None would make intensity() return None
            # for a peptide, which is supposed to be a protein-only condition and is exactly
            # what callers branch on to find unresolvable proteins. See issue #7.
            intensities={
                run: (0.0 if value is None else value)
                for run, value in (payload.get("intensities") or {}).items()
            },
            detection_types=dict(payload.get("detection_types") or {}),
        )
intensity
intensity(file_name: str) -> Any

This peptide's intensity in the named run.

0.0 — not None — means "not quantified here." (Only protein intensities are ever None.) Treat 0.0 as missing, not as a measured absence: log-transforming it will mislead. And note a peptide that FlashLFQ transferred into this run by match-between-runs may still read 0.0 here — see :attr:FlashLfqResults.peaks. Returns 0.0 for a run that was never provided.

Source code in pkg/python/src/pymzlib/flashlfq.py
def intensity(self, file_name: str) -> Any:
    """This peptide's intensity in the named run.

    **``0.0`` — not ``None`` — means "not quantified here."** (Only *protein* intensities are
    ever ``None``.) Treat ``0.0`` as missing, not as a measured absence: log-transforming it
    will mislead. And note a peptide that FlashLFQ *transferred* into this run by
    match-between-runs may still read ``0.0`` here — see :attr:`FlashLfqResults.peaks`. Returns
    ``0.0`` for a run that was never provided.
    """
    return self.intensities.get(file_name, 0.0)
detection_type
detection_type(file_name: str) -> str

How this peptide was quantified in the named run ("NotDetected" if it was not).

Source code in pkg/python/src/pymzlib/flashlfq.py
def detection_type(self, file_name: str) -> str:
    """How this peptide was quantified in the named run (``"NotDetected"`` if it was not)."""
    return self.detection_types.get(file_name, "NotDetected")

ProteinGroup dataclass

A quantified protein group, mirroring FlashLFQ's ProteinGroup.

Attributes:

Name Type Description
protein_group str

The protein group name (accession, or ;-joined accessions).

gene_name str

The gene name, when the result file carried one.

organism str

The organism, when the result file carried one.

intensities dict[str, Any]

Run base name → protein intensity in that run. May be None: FlashLFQ's median-polish protein quant emits NaN (returned here as None) when the peptide matrix for the protein is degenerate — too few peptides per run to resolve, or several runs reporting the same intensity — protecting you from a fabricated-looking number. Missing, as opposed to un-resolvable, is 0.0.

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class ProteinGroup:
    """A quantified protein group, mirroring FlashLFQ's ``ProteinGroup``.

    Attributes:
        protein_group: The protein group name (accession, or ``;``-joined accessions).
        gene_name: The gene name, when the result file carried one.
        organism: The organism, when the result file carried one.
        intensities: Run base name → protein intensity in that run. **May be ``None``**: FlashLFQ's
            median-polish protein quant emits NaN (returned here as ``None``) when the peptide matrix
            for the protein is degenerate — too few peptides per run to resolve, or several runs
            reporting the same intensity — protecting you from a fabricated-looking number. Missing,
            as opposed to un-resolvable, is ``0.0``.
    """

    protein_group: str
    gene_name: str
    organism: str
    intensities: dict[str, Any]

    def intensity(self, file_name: str) -> Any:
        """This protein's intensity in the named run.

        ``None`` means FlashLFQ could not resolve a number (a degenerate peptide matrix); ``0.0``
        means simply not measured in this run.
        """
        return self.intensities.get(file_name, 0.0)

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "ProteinGroup":
        return cls(
            protein_group=payload.get("protein_group", ""),
            gene_name=payload.get("gene_name", ""),
            organism=payload.get("organism", ""),
            intensities=dict(payload.get("intensities") or {}),
        )
intensity
intensity(file_name: str) -> Any

This protein's intensity in the named run.

None means FlashLFQ could not resolve a number (a degenerate peptide matrix); 0.0 means simply not measured in this run.

Source code in pkg/python/src/pymzlib/flashlfq.py
def intensity(self, file_name: str) -> Any:
    """This protein's intensity in the named run.

    ``None`` means FlashLFQ could not resolve a number (a degenerate peptide matrix); ``0.0``
    means simply not measured in this run.
    """
    return self.intensities.get(file_name, 0.0)

Peak dataclass

One quantified chromatographic peak, mirroring FlashLFQ's ChromatographicPeak.

This is the surface to use for match-between-runs. Unlike the peptide roll-up (:attr:Peptide.intensities, which mirrors QuantifiedPeptides.tsv and drops most MBR transfers), the peaks fully represent every quantified peak, transferred or not. To build an MBR-inclusive peptide × run matrix, pivot these on (sequence, file_name).

Attributes:

Name Type Description
file_name str

The run this peak was measured in (base name).

sequence str

The full (modified) sequence of the peptide the peak was assigned to.

base_sequence str

The bare amino-acid sequence.

intensity Any

The peak's intensity (None only if FlashLFQ could not integrate it).

detection_type str

"MSMS", "MBR", "MSMSIdentifiedButNotQuantified", "MSMSAmbiguousPeakfinding" — the same vocabulary as :attr:Peptide.detection_types. Filter detection_type == "MBR" to see exactly the transferred peaks.

retention_time Any

Apex retention time in minutes, or None if the peak has no apex.

num_identifications int

How many peptides could explain this peak. > 1 means it is ambiguous and its intensity should be treated with care.

protein_groups str

The protein group(s) the assigned identification(s) belong to, ;-joined.

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class Peak:
    """One quantified chromatographic peak, mirroring FlashLFQ's ``ChromatographicPeak``.

    This is the surface to use for **match-between-runs**. Unlike the peptide roll-up
    (:attr:`Peptide.intensities`, which mirrors ``QuantifiedPeptides.tsv`` and drops most MBR
    transfers), the peaks fully represent every quantified peak, transferred or not. To build an
    MBR-inclusive peptide × run matrix, pivot these on ``(sequence, file_name)``.

    Attributes:
        file_name: The run this peak was measured in (base name).
        sequence: The full (modified) sequence of the peptide the peak was assigned to.
        base_sequence: The bare amino-acid sequence.
        intensity: The peak's intensity (``None`` only if FlashLFQ could not integrate it).
        detection_type: ``"MSMS"``, ``"MBR"``, ``"MSMSIdentifiedButNotQuantified"``,
            ``"MSMSAmbiguousPeakfinding"`` — the same vocabulary as :attr:`Peptide.detection_types`.
            Filter ``detection_type == "MBR"`` to see exactly the transferred peaks.
        retention_time: Apex retention time in minutes, or ``None`` if the peak has no apex.
        num_identifications: How many peptides could explain this peak. ``> 1`` means it is
            ambiguous and its intensity should be treated with care.
        protein_groups: The protein group(s) the assigned identification(s) belong to, ``;``-joined.
    """

    file_name: str
    sequence: str
    base_sequence: str
    intensity: Any
    detection_type: str
    retention_time: Any
    num_identifications: int
    protein_groups: str

    @property
    def is_mbr(self) -> bool:
        """Whether this peak was transferred by match-between-runs."""
        return self.detection_type == "MBR"

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "Peak":
        return cls(
            file_name=payload.get("file_name", ""),
            sequence=payload.get("sequence", ""),
            base_sequence=payload.get("base_sequence", ""),
            intensity=payload.get("intensity"),
            detection_type=payload.get("detection_type", ""),
            retention_time=payload.get("retention_time"),
            num_identifications=int(payload.get("num_identifications", 0)),
            protein_groups=payload.get("protein_groups", ""),
        )
is_mbr property
is_mbr: bool

Whether this peak was transferred by match-between-runs.

FlashLfqResults dataclass

The result of a quantification run, mirroring mzLib's FlashLfqResults.

Attributes:

Name Type Description
psm_file str

The absolute path of the PSM result file that was quantified.

identification_count int

How many identifications were read from it.

parameters dict[str, Any]

The FlashLFQ parameters actually used, echoed back with their mzLib names.

spectra_files list[SpectraFileInfo]

One :class:SpectraFileInfo per run.

peptides list[Peptide]

One :class:Peptide per quantified modified sequence (mirrors QuantifiedPeptides.tsv; does not fully carry MBR — use :attr:peaks for that).

proteins list[ProteinGroup]

One :class:ProteinGroup per quantified protein group.

peaks list[Peak]

Every quantified :class:Peak across all runs (mirrors QuantifiedPeaks.tsv) — the complete surface, and the one to use for match-between-runs.

output_directory Any

Where the FlashLFQ TSVs were written, or None if none were.

Source code in pkg/python/src/pymzlib/flashlfq.py
@dataclass(frozen=True)
class FlashLfqResults:
    """The result of a quantification run, mirroring mzLib's ``FlashLfqResults``.

    Attributes:
        psm_file: The absolute path of the PSM result file that was quantified.
        identification_count: How many identifications were read from it.
        parameters: The FlashLFQ parameters actually used, echoed back with their mzLib names.
        spectra_files: One :class:`SpectraFileInfo` per run.
        peptides: One :class:`Peptide` per quantified modified sequence (mirrors
            ``QuantifiedPeptides.tsv``; does not fully carry MBR — use :attr:`peaks` for that).
        proteins: One :class:`ProteinGroup` per quantified protein group.
        peaks: Every quantified :class:`Peak` across all runs (mirrors ``QuantifiedPeaks.tsv``) —
            the complete surface, and the one to use for match-between-runs.
        output_directory: Where the FlashLFQ TSVs were written, or ``None`` if none were.
    """

    psm_file: str
    identification_count: int
    parameters: dict[str, Any]
    spectra_files: list[SpectraFileInfo]
    peptides: list[Peptide]
    proteins: list[ProteinGroup]
    peaks: list[Peak]
    output_directory: Any

    @property
    def peptide_count(self) -> int:
        """Number of quantified peptides."""
        return len(self.peptides)

    @property
    def protein_count(self) -> int:
        """Number of quantified protein groups."""
        return len(self.proteins)

    @property
    def mbr_peak_count(self) -> int:
        """Total match-between-runs peaks (transfers) across every run.

        This counts transferred **peaks**, not distinct peptides: one peptide rescued in two runs is
        two peaks here. For "how many peptides did MBR rescue," use :attr:`mbr_rescued_peptide_count`.
        Either way, do not count MBR from the peptide roll-up (:attr:`Peptide.detection_types`) — it
        under-counts. Zero unless ``match_between_runs`` was on.
        """
        return sum(f.mbr_peak_count for f in self.spectra_files)

    @property
    def mbr_peaks(self) -> list[Peak]:
        """Exactly the peaks transferred by match-between-runs (``detection_type == "MBR"``)."""
        return [p for p in self.peaks if p.is_mbr]

    @property
    def mbr_rescued_peptide_count(self) -> int:
        """Distinct peptides quantified in at least one run only by match-between-runs.

        Exactly: **the number of distinct ``sequence`` values among peaks whose
        ``detection_type`` is ``"MBR"``.** Stated in code terms because the prose version,
        "peptides quantified in at least one run *only* by match-between-runs", is subtly
        different and on real data the two diverge: peptides having **both** an MBR peak and
        a zero-intensity ``MSMS`` peak in the same run were identified there, so they are not
        rescues under the strict reading. On the K562 pair this returns 140 where the strict
        count is 135. Do not read ``mbr_rescued_peptide_count == mbr_peak_count`` as
        reassurance that nothing was double-counted; on that data both are 140, and they
        coincide only because every MBR peak happened to carry a distinct sequence.

        Distinct modified sequences among
        :attr:`mbr_peaks`. This equals :attr:`mbr_peak_count` only when no peptide was rescued in
        more than one run.
        """
        return len({p.sequence for p in self.mbr_peaks})

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "FlashLfqResults":
        return cls(
            psm_file=data.get("psm_file", ""),
            identification_count=int(data.get("identification_count", 0)),
            parameters=dict(data.get("parameters") or {}),
            spectra_files=[SpectraFileInfo._from_wire(f) for f in (data.get("spectra_files") or [])],
            peptides=[Peptide._from_wire(p) for p in (data.get("peptides") or [])],
            proteins=[ProteinGroup._from_wire(g) for g in (data.get("proteins") or [])],
            peaks=[Peak._from_wire(pk) for pk in (data.get("peaks") or [])],
            output_directory=data.get("output_directory"),
        )
peptide_count property
peptide_count: int

Number of quantified peptides.

protein_count property
protein_count: int

Number of quantified protein groups.

mbr_peak_count property
mbr_peak_count: int

Total match-between-runs peaks (transfers) across every run.

This counts transferred peaks, not distinct peptides: one peptide rescued in two runs is two peaks here. For "how many peptides did MBR rescue," use :attr:mbr_rescued_peptide_count. Either way, do not count MBR from the peptide roll-up (:attr:Peptide.detection_types) — it under-counts. Zero unless match_between_runs was on.

mbr_peaks property
mbr_peaks: list[Peak]

Exactly the peaks transferred by match-between-runs (detection_type == "MBR").

mbr_rescued_peptide_count property
mbr_rescued_peptide_count: int

Distinct peptides quantified in at least one run only by match-between-runs.

Exactly: the number of distinct sequence values among peaks whose detection_type is "MBR". Stated in code terms because the prose version, "peptides quantified in at least one run only by match-between-runs", is subtly different and on real data the two diverge: peptides having both an MBR peak and a zero-intensity MSMS peak in the same run were identified there, so they are not rescues under the strict reading. On the K562 pair this returns 140 where the strict count is 135. Do not read mbr_rescued_peptide_count == mbr_peak_count as reassurance that nothing was double-counted; on that data both are 140, and they coincide only because every MBR peak happened to carry a distinct sequence.

Distinct modified sequences among :attr:mbr_peaks. This equals :attr:mbr_peak_count only when no peptide was rescued in more than one run.

quantify

quantify(psms: str, spectra: Sequence[SpectraInput], *, normalize: bool = False, ppm_tolerance: float = 10.0, isotope_ppm_tolerance: float = 5.0, integrate: bool = False, match_between_runs: bool = False, mbr_ppm_tolerance: float = 10.0, mbr_q_value_threshold: float = 0.05, use_shared_peptides_for_protein_quant: bool = False, bayesian_protein_quant: bool = False, use_pep_q_value: bool = False, max_threads: int = -1, output_directory: str | None = None, timeout: float | None = None) -> FlashLfqResults

Quantify a search's peptides across mzML runs with FlashLFQ.

Parameters:

Name Type Description Default
psms str

Path to a PSM result file. Use a MetaMorpheus .psmtsv or .osmtsv. mzLib also accepts an MSFragger psm.tsv here, but the result is wrong — see the warning in the module documentation above. Every run named in the file must have a matching mzML in spectra; FlashLFQ matches identifications to runs by base file name.

required
spectra Sequence[SpectraInput]

The mzML runs. Each entry is either a path ("run_1.mzML") or a mapping carrying the experimental design ({"path": "run_1.mzML", "condition": "treated", "biological_replicate": 1}). Base file names must be unique.

required
normalize bool

Normalize intensities across runs (FlashLFQ Normalize).

False
ppm_tolerance float

Mass tolerance for peak-finding, in ppm (PpmTolerance).

10.0
isotope_ppm_tolerance float

Mass tolerance for isotope-envelope matching, in ppm.

5.0
integrate bool

Integrate peak intensities rather than taking the apex. FlashLFQ recommends leaving this off.

False
match_between_runs bool

Quantify a peptide in a run where it was not identified, by transferring the identification from a run where it was (MatchBetweenRuns). The reason to reach for FlashLFQ; off by default because it makes assumptions worth opting into. Requires a complete, balanced design: every condition and biological replicate must carry the same set of fractions — a missing replicate or fraction breaks the complementarity MBR relies on. Read the transferred peaks from :attr:FlashLfqResults.peaks, not the peptide table.

False
mbr_ppm_tolerance float

Mass tolerance for MBR transfers, in ppm.

10.0
mbr_q_value_threshold float

The q-value cutoff below which an MBR transfer is accepted.

0.05
use_shared_peptides_for_protein_quant bool

Let peptides shared between protein groups contribute to protein quant (UseSharedPeptidesForProteinQuant).

False
bayesian_protein_quant bool

Run FlashLFQ's Bayesian protein-fold-change engine.

False
use_pep_q_value bool

Filter identifications on PEP q-value rather than q-value.

False
max_threads int

Worker threads; -1 lets FlashLFQ choose.

This is not only a performance knob - it changes results. With -1, FlashLFQ's peptide roll-up nondeterministically drops some MBR intensities, so peptide and protein numbers vary between runs on byte-identical inputs. On the K562 pair, 6 peptides flip between 0.0 and a real intensity, which flips a borderline protein group between None and a number: unquantifiable in 5 of 6 runs, quantified in the 6th. The peaks are stable throughout - only the roll-up wobbles. Set max_threads=1 for anything you intend to publish or reproduce. See smith-chem-wisc/mzLib#1111.

-1
output_directory str | None

If given, FlashLFQ also writes QuantifiedPeaks.tsv, QuantifiedPeptides.tsv and QuantifiedProteins.tsv there.

None
timeout float | None

Seconds to allow. Large experiments legitimately take a while; None waits indefinitely.

None

Returns:

Name Type Description
A FlashLfqResults

class:FlashLfqResults.

Raises:

Type Description
UsageError

an argument is malformed, a run is not mzML, an mzML is missing, or the PSM file names a run with no mzML provided.

BridgeError

FlashLFQ itself failed.

Source code in pkg/python/src/pymzlib/flashlfq.py
def quantify(
    psms: str,
    spectra: Sequence[SpectraInput],
    *,
    normalize: bool = False,
    ppm_tolerance: float = 10.0,
    isotope_ppm_tolerance: float = 5.0,
    integrate: bool = False,
    match_between_runs: bool = False,
    mbr_ppm_tolerance: float = 10.0,
    mbr_q_value_threshold: float = 0.05,
    use_shared_peptides_for_protein_quant: bool = False,
    bayesian_protein_quant: bool = False,
    use_pep_q_value: bool = False,
    max_threads: int = -1,
    output_directory: str | None = None,
    timeout: float | None = None,
) -> FlashLfqResults:
    """Quantify a search's peptides across mzML runs with FlashLFQ.

    Args:
        psms: Path to a PSM result file. **Use a MetaMorpheus ``.psmtsv`` or ``.osmtsv``.** mzLib
            also accepts an MSFragger ``psm.tsv`` here, but the result is wrong — see the warning
            in the module documentation above. Every run named in the file must have a matching
            mzML in ``spectra``; FlashLFQ matches identifications to runs by base file name.
        spectra: The mzML runs. Each entry is either a path (``"run_1.mzML"``) or a mapping
            carrying the experimental design (``{"path": "run_1.mzML", "condition": "treated",
            "biological_replicate": 1}``). Base file names must be unique.
        normalize: Normalize intensities across runs (FlashLFQ ``Normalize``).
        ppm_tolerance: Mass tolerance for peak-finding, in ppm (``PpmTolerance``).
        isotope_ppm_tolerance: Mass tolerance for isotope-envelope matching, in ppm.
        integrate: Integrate peak intensities rather than taking the apex. FlashLFQ recommends
            leaving this off.
        match_between_runs: Quantify a peptide in a run where it was not identified, by transferring
            the identification from a run where it was (``MatchBetweenRuns``). The reason to reach
            for FlashLFQ; off by default because it makes assumptions worth opting into. **Requires a
            complete, balanced design**: every condition and biological replicate must carry the same
            set of fractions — a missing replicate or fraction breaks the complementarity MBR relies
            on. Read the transferred peaks from :attr:`FlashLfqResults.peaks`, not the peptide table.
        mbr_ppm_tolerance: Mass tolerance for MBR transfers, in ppm.
        mbr_q_value_threshold: The q-value cutoff below which an MBR transfer is accepted.
        use_shared_peptides_for_protein_quant: Let peptides shared between protein groups contribute
            to protein quant (``UseSharedPeptidesForProteinQuant``).
        bayesian_protein_quant: Run FlashLFQ's Bayesian protein-fold-change engine.
        use_pep_q_value: Filter identifications on PEP q-value rather than q-value.
        max_threads: Worker threads; ``-1`` lets FlashLFQ choose.

            **This is not only a performance knob - it changes results.** With ``-1``,
            FlashLFQ's peptide roll-up nondeterministically drops some MBR intensities, so
            peptide and protein numbers vary between runs on byte-identical inputs. On the
            K562 pair, 6 peptides flip between ``0.0`` and a real intensity, which flips a
            borderline protein group between ``None`` and a number: unquantifiable in 5 of 6
            runs, quantified in the 6th. The peaks are stable throughout - only the roll-up
            wobbles. **Set ``max_threads=1`` for anything you intend to publish or
            reproduce.** See smith-chem-wisc/mzLib#1111.
        output_directory: If given, FlashLFQ also writes ``QuantifiedPeaks.tsv``,
            ``QuantifiedPeptides.tsv`` and ``QuantifiedProteins.tsv`` there.
        timeout: Seconds to allow. Large experiments legitimately take a while; ``None`` waits
            indefinitely.

    Returns:
        A :class:`FlashLfqResults`.

    Raises:
        UsageError: an argument is malformed, a run is not mzML, an mzML is missing, or the PSM
            file names a run with no mzML provided.
        BridgeError: FlashLFQ itself failed.
    """
    if not isinstance(psms, str) or not psms.strip():
        raise _bridge.UsageError("A PSM result file path is required, e.g. 'AllPSMs.psmtsv'.")

    stdin = _spectra_stdin(spectra)

    args: list[str] = ["quant", "flashlfq", "--psms", psms]
    if normalize:
        args.append("--normalize")
    args += ["--ppm", _number(ppm_tolerance, "ppm_tolerance")]
    args += ["--isotope-ppm", _number(isotope_ppm_tolerance, "isotope_ppm_tolerance")]
    if integrate:
        args.append("--integrate")
    if match_between_runs:
        args.append("--mbr")
    args += ["--mbr-ppm", _number(mbr_ppm_tolerance, "mbr_ppm_tolerance")]
    args += ["--mbr-q", _number(mbr_q_value_threshold, "mbr_q_value_threshold")]
    if use_shared_peptides_for_protein_quant:
        args.append("--shared-peptides")
    if bayesian_protein_quant:
        args.append("--bayesian")
    if use_pep_q_value:
        args.append("--use-pep-q")
    if isinstance(max_threads, bool) or not isinstance(max_threads, int):
        raise _bridge.UsageError(f"max_threads must be a whole number; got {max_threads!r}.")
    args += ["--threads", str(max_threads)]
    if output_directory is not None:
        if not isinstance(output_directory, str) or not output_directory.strip():
            raise _bridge.UsageError("output_directory must be a non-empty path or None.")
        args += ["--out", output_directory]

    data = _bridge.invoke(*args, stdin=stdin, timeout=timeout)
    return FlashLfqResults._from_wire(data)

pymzlib.readers

readers

Read proteomics result files: what a file is, what you can do with it, and its records.

mzLib recognises 29 file types written by a dozen different search and deconvolution tools - MetaMorpheus, MSFragger, TopPIC, TopFD, MsPathFinderT, Crux, Casanovo, FlashDeconv, Dinosaur, FlashLFQ - and dispatches each to a parser it maintains. This module asks it what a path is::

>>> import pymzlib
>>> info = pymzlib.readers.identify("psm.tsv")     # doctest: +SKIP
>>> info.file_type, info.views                     # doctest: +SKIP
('MsFraggerPsm', ['quantifiable'])

Read :attr:FileInfo.views before assuming anything. It is tempting to describe mzLib as reading 29 formats into one uniform shape, and it does not. The formats fall into disjoint families and several belong to no family at all:

  • "quantifiable" - a cross-format record view (sequence, retention time, charge, mass, protein groups), and the input :func:pymzlib.flashlfq.quantify accepts. Exactly three file types have it: MetaMorpheus .psmtsv and .osmtsv, and MSFragger psm.tsv. This view reports what mzLib's interface offers, not that the numbers are comparable - see the warning below, and do not quantify the MSFragger one.
  • "ms1_features" - deconvolved MS1 features (TopFD _ms1.feature, Dinosaur).
  • "spectra" - the file is spectra, not results (.raw, .mzML, .mgf, .d, msalign).
  • "spectral_match" - records are identifications but share no file-level interface (MsPathFinderT, Casanovo).
  • [] - an empty list is a real and common answer. TopPIC, Crux, MSFragger's peptide/protein tables and the FlashDeconv formats each parse into their own record type with nothing in common. mzLib reads them; there is simply no uniform view to project them onto.

Call :func:formats for the whole table. It is enumerated from mzLib rather than transcribed, so it cannot drift from what mzLib actually dispatches.

Three things this module deliberately does not tell you, in the "surface it, don't hide it" spirit of the rest of pyMzLib:

  • Which tool wrote the file. mzLib has a Software property that looks like the answer and is not: readers carry their software constant on a constructor that mzLib's own file factory does not use, so the value is unset for everything the factory returns - and it is not reliably set on the other constructor either. Rather than reconstruct a plausible answer, there is no software field. :attr:FileInfo.file_type already names the tool.
  • Whether the numbers inside mean the same thing across formats. They do not, and this is the trap most likely to produce a wrong result. mzLib's result-file readers pass through whatever the tool wrote, with no unit conversion: MetaMorpheus retention times are in minutes, MSFragger's and TopPIC's are in seconds, and TopFD changed from seconds to minutes between v1.6.2 and v1.7.0 within the same file type. Likewise is_decoy is hardcoded False for MSFragger, which means "mzLib cannot tell" rather than "target" - MSFragger's psm.tsv carries no target/decoy column at all - so is_decoy arrives as None for that format rather than a fabricated False. monoisotopic_mass is the theoretical peptide mass in both formats, never the observed precursor mass. Identifying a file is safe; comparing raw fields across formats is not.

  • Anything about confidence. There is no q-value, PEP or score in this view, because IQuantifiableRecord carries only what FlashLFQ needs. Nothing you get back is FDR-filtered, even though every one of these files records confidence somewhere. Filter before you report.

.. warning::

That units mismatch is not hypothetical. Passing an MSFragger psm.tsv to :func:pymzlib.flashlfq.quantify returns near-zero intensities, because FlashLFQ reads the seconds as minutes and searches for each peptide roughly sixty times too late in the gradient - typically past the end of the run, so the peptide is never found. identify() will still call the file quantifiable - that is mzLib's interface, honestly reported - but quantify MetaMorpheus output only until the upstream fix lands.

Format dataclass

One file type mzLib can recognise.

Attributes:

Name Type Description
file_type str

mzLib's SupportedFileType name, e.g. "MsFraggerPsm", "psmtsv".

extension Any

The extension or filename suffix mzLib dispatches on, e.g. "psm.tsv", "_ms1.feature". Not unique across file types - BrukerD and BrukerTimsTof are both .d (told apart by the directory's contents), and several formats share .tsv.

reader Any

The name of the mzLib class that parses it, for cross-referencing the mzLib source.

views list[str]

The uniform views this format supports - see the module docstring. Often empty.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class Format:
    """One file type mzLib can recognise.

    Attributes:
        file_type: mzLib's ``SupportedFileType`` name, e.g. ``"MsFraggerPsm"``, ``"psmtsv"``.
        extension: The extension or filename suffix mzLib dispatches on, e.g. ``"psm.tsv"``,
            ``"_ms1.feature"``. **Not unique across file types** - ``BrukerD`` and
            ``BrukerTimsTof`` are both ``.d`` (told apart by the directory's contents), and
            several formats share ``.tsv``.
        reader: The name of the mzLib class that parses it, for cross-referencing the mzLib source.
        views: The uniform views this format supports - see the module docstring. Often empty.
    """

    file_type: str
    extension: Any
    reader: Any
    views: list[str] = field(default_factory=list)

    @property
    def is_quantifiable(self) -> bool:
        """Whether this format offers the cross-format record view (and so feeds FlashLFQ)."""
        return QUANTIFIABLE in self.views

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "Format":
        return cls(
            file_type=payload.get("file_type", ""),
            extension=payload.get("extension"),
            reader=payload.get("reader"),
            views=list(payload.get("views") or []),
        )
is_quantifiable property
is_quantifiable: bool

Whether this format offers the cross-format record view (and so feeds FlashLFQ).

FileInfo dataclass

What a particular file is, and what can be done with it.

Attributes:

Name Type Description
path str

The absolute path that was identified.

file_type str

mzLib's SupportedFileType name.

extension Any

The extension mzLib dispatched on.

reader Any

The mzLib class that would parse it.

views list[str]

The uniform views this file supports - see the module docstring. Often empty, which means mzLib can read the file but offers no cross-format projection of it.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class FileInfo:
    """What a particular file is, and what can be done with it.

    Attributes:
        path: The absolute path that was identified.
        file_type: mzLib's ``SupportedFileType`` name.
        extension: The extension mzLib dispatched on.
        reader: The mzLib class that would parse it.
        views: The uniform views this file supports - see the module docstring. Often empty, which
            means mzLib can read the file but offers no cross-format projection of it.
    """

    path: str
    file_type: str
    extension: Any
    reader: Any
    views: list[str] = field(default_factory=list)

    @property
    def is_quantifiable(self) -> bool:
        """Whether this file offers the cross-format record view.

        When ``True``, the path can be passed straight to :func:`pymzlib.flashlfq.quantify` as
        ``psms``. When ``False``, mzLib can still read the file - it simply has no uniform view,
        so quantification would fail on it.
        """
        return QUANTIFIABLE in self.views

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "FileInfo":
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            extension=data.get("extension"),
            reader=data.get("reader"),
            views=list(data.get("views") or []),
        )
is_quantifiable property
is_quantifiable: bool

Whether this file offers the cross-format record view.

When True, the path can be passed straight to :func:pymzlib.flashlfq.quantify as psms. When False, mzLib can still read the file - it simply has no uniform view, so quantification would fail on it.

WrittenTable dataclass

Where :func:read_results wrote a table, when asked to write one instead of returning it.

Attributes:

Name Type Description
path str

The absolute path written.

format str

Always "tsv". Tab-separated, not comma-separated, because these fields contain commas - MSFragger's mapped proteins are a comma-separated list inside a single field, and joined accessions look the same. It is also what every mzLib reader and writer uses. Read it with csv.reader(f, delimiter="\t") or pandas.read_csv(path, sep="\t").

row_count int

Rows written, excluding the header.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class WrittenTable:
    """Where :func:`read_results` wrote a table, when asked to write one instead of returning it.

    Attributes:
        path: The absolute path written.
        format: Always ``"tsv"``. **Tab-separated, not comma-separated**, because these fields
            contain commas - MSFragger's mapped proteins are a comma-separated list inside a single
            field, and joined accessions look the same. It is also what every mzLib reader and
            writer uses. Read it with ``csv.reader(f, delimiter="\\t")`` or
            ``pandas.read_csv(path, sep="\\t")``.
        row_count: Rows written, excluding the header.
    """

    path: str
    format: str
    row_count: int

    @classmethod
    def _from_wire(cls, payload: dict[str, Any]) -> "WrittenTable":
        return cls(
            path=payload.get("path", ""),
            format=payload.get("format", ""),
            row_count=int(payload.get("row_count", 0)),
        )

ResultRecords dataclass

The uniform record view of a result file.

Attributes:

Name Type Description
path str

The absolute path that was read.

file_type str

mzLib's SupportedFileType name.

record_count int

Records in the whole file, regardless of limit or offset.

returned_count int

Records actually carried back in :attr:columns. Zero when out was given, since the table went to disk instead.

offset int

The offset that was applied.

truncated bool

Whether records were left behind, by either limit or offset. A short answer and a complete one must never look alike, so check this rather than assuming.

retention_time_unit str

The unit :attr:columns' retention_time carries for this format - "minutes", "seconds", or "unknown". mzLib does not normalise it, so this differs per format and you must convert before comparing two files. Provided as a value so you can convert programmatically instead of hard-coding a table.

rows_not_read Any

Data rows in the file that did not become records - mzLib drops a malformed row silently, so a non-zero value here means the file is partly unreadable and the table is incomplete. None when the count could not be established meaningfully.

caveats list[str]

What the uniform view cannot be trusted to mean for this format. Empty for some formats, not for others; each entry cites the mzLib source it came from. Worth printing before comparing anything across formats - this is where you learn that MSFragger retention times are seconds while MetaMorpheus's are minutes.

column_names list[str]

The field names, in order.

columns Any

Field name -> list of values, one entry per record - the shape pandas.DataFrame and polars.DataFrame both accept directly. None when out was given.

output Any

Where the table was written, or None if it was returned inline.

Source code in pkg/python/src/pymzlib/readers.py
@dataclass(frozen=True)
class ResultRecords:
    """The uniform record view of a result file.

    Attributes:
        path: The absolute path that was read.
        file_type: mzLib's ``SupportedFileType`` name.
        record_count: Records in the **whole file**, regardless of ``limit`` or ``offset``.
        returned_count: Records actually carried back in :attr:`columns`. Zero when ``out`` was
            given, since the table went to disk instead.
        offset: The offset that was applied.
        truncated: **Whether records were left behind**, by either ``limit`` or ``offset``. A short
            answer and a complete one must never look alike, so check this rather than assuming.
        retention_time_unit: The unit :attr:`columns`' ``retention_time`` carries for this format -
            ``"minutes"``, ``"seconds"``, or ``"unknown"``. mzLib does not normalise it, so this
            differs per format and you must convert before comparing two files. Provided as a value
            so you can convert programmatically instead of hard-coding a table.
        rows_not_read: Data rows in the file that did not become records - mzLib drops a malformed
            row silently, so a non-zero value here means the file is partly unreadable and the
            table is incomplete. ``None`` when the count could not be established meaningfully.
        caveats: **What the uniform view cannot be trusted to mean for this format.** Empty for
            some formats, not for others; each entry cites the mzLib source it came from. Worth
            printing before comparing anything across formats - this is where you learn that
            MSFragger retention times are seconds while MetaMorpheus's are minutes.
        column_names: The field names, in order.
        columns: Field name -> list of values, one entry per record - the shape ``pandas.DataFrame``
            and ``polars.DataFrame`` both accept directly. ``None`` when ``out`` was given.
        output: Where the table was written, or ``None`` if it was returned inline.
    """

    path: str
    file_type: str
    record_count: int
    returned_count: int
    offset: int
    truncated: bool
    retention_time_unit: str
    rows_not_read: Any
    caveats: list[str] = field(default_factory=list)
    column_names: list[str] = field(default_factory=list)
    columns: Any = None
    output: Any = None

    @property
    def retention_time_in_minutes(self) -> list[Any]:
        """``retention_time`` converted to minutes, whatever unit the format wrote.

        The conversion you would otherwise write by hand, using
        :attr:`retention_time_unit`. Raises if the unit is ``"unknown"`` rather than guessing -
        a silently unconverted axis is the specific mistake this module exists to prevent.
        """
        if self.columns is None:
            where = getattr(self.output, "path", None)
            raise _bridge.UsageError(
                "The records were written to "
                + (f"'{where}'" if where else "disk")
                + " rather than returned, so there is nothing here to convert. Read that file, or "
                "call read_results() without out=."
            )
        if "retention_time" not in self.columns:
            raise _bridge.UsageError(
                "This result has no retention_time column, so it cannot be converted."
            )

        values = self.columns["retention_time"]
        if self.retention_time_unit == "minutes":
            return list(values)
        if self.retention_time_unit == "seconds":
            return [None if v is None else v / 60.0 for v in values]
        raise _bridge.UsageError(
            f"Cannot convert retention time for '{self.file_type}': mzLib gives no basis to say "
            "what unit it is in. Inspect the values against scan numbers before comparing them."
        )

    @property
    def records(self) -> list[dict[str, Any]]:
        """The same data row-wise: one dict per record.

        A convenience for looping. If you are building a table, prefer :attr:`columns` - it is
        already the shape a DataFrame wants, and this rebuilds it. Empty when ``out`` was given.
        """
        if not self.columns:
            return []
        names = [n for n in (self.column_names or list(self.columns)) if n in self.columns]
        # Row count from the columns themselves, not from returned_count: the two come from
        # different wire fields, and trusting the count would raise IndexError - or silently drop
        # rows - if they ever disagreed.
        length = min((len(self.columns[n]) for n in names), default=0)
        return [{name: self.columns[name][i] for name in names} for i in range(length)]

    @classmethod
    def _from_wire(cls, data: dict[str, Any]) -> "ResultRecords":
        written = data.get("output")
        return cls(
            path=data.get("path", ""),
            file_type=data.get("file_type", ""),
            record_count=int(data.get("record_count", 0)),
            returned_count=int(data.get("returned_count", 0)),
            offset=int(data.get("offset", 0)),
            truncated=bool(data.get("truncated", False)),
            retention_time_unit=data.get("retention_time_unit") or "unknown",
            rows_not_read=data.get("rows_not_read"),
            caveats=list(data.get("caveats") or []),
            column_names=list(data.get("column_names") or []),
            columns=data.get("columns"),
            output=WrittenTable._from_wire(written) if written else None,
        )
retention_time_in_minutes property
retention_time_in_minutes: list[Any]

retention_time converted to minutes, whatever unit the format wrote.

The conversion you would otherwise write by hand, using :attr:retention_time_unit. Raises if the unit is "unknown" rather than guessing - a silently unconverted axis is the specific mistake this module exists to prevent.

records property
records: list[dict[str, Any]]

The same data row-wise: one dict per record.

A convenience for looping. If you are building a table, prefer :attr:columns - it is already the shape a DataFrame wants, and this rebuilds it. Empty when out was given.

formats

formats(timeout: float | None = 60) -> list[Format]

Every file type mzLib can recognise.

Enumerated from mzLib itself rather than from a list maintained here, so it reflects the installed version and cannot go stale.

Parameters:

Name Type Description Default
timeout float | None

Seconds to allow.

60

Returns:

Name Type Description
One list[Format]

class:Format per supported file type.

Example

quantifiable = [f.file_type for f in formats() if f.is_quantifiable] # doctest: +SKIP quantifiable # doctest: +SKIP ['psmtsv', 'osmtsv', 'MsFraggerPsm']

Source code in pkg/python/src/pymzlib/readers.py
def formats(timeout: float | None = 60) -> list[Format]:
    """Every file type mzLib can recognise.

    Enumerated from mzLib itself rather than from a list maintained here, so it reflects the
    installed version and cannot go stale.

    Args:
        timeout: Seconds to allow.

    Returns:
        One :class:`Format` per supported file type.

    Example:
        >>> quantifiable = [f.file_type for f in formats() if f.is_quantifiable]  # doctest: +SKIP
        >>> quantifiable                                                          # doctest: +SKIP
        ['psmtsv', 'osmtsv', 'MsFraggerPsm']
    """
    data = _bridge.invoke("readers", "formats", timeout=timeout)
    return [Format._from_wire(item) for item in (data.get("formats") or [])]

identify

identify(path: str, timeout: float | None = 60) -> FileInfo

Identify a result file without parsing its contents.

Cheap by design: mzLib resolves the type and stops, so identifying a million-row file costs no more than identifying an empty one. It is not, however, pure - mzLib disambiguates a bare .tsv by reading its first line, a .mztab by its first five, and a Bruker .d by which analysis file the directory holds. An unreadable file will therefore raise.

Parameters:

Name Type Description Default
path str

Path to a result or spectra file. A Bruker .d directory is also accepted.

required
timeout float | None

Seconds to allow.

60

Returns:

Name Type Description
A FileInfo

class:FileInfo naming the format and the views it supports.

Raises:

Type Description
UsageError

the path is blank, does not exist, or is not a file type mzLib recognises. mzLib has no "unknown" result - a file is dispatchable or it is an error - so use :func:formats to see what is supported, or catch this to test a file.

Example

info = identify("AllPSMs.psmtsv") # doctest: +SKIP info.file_type, info.is_quantifiable # doctest: +SKIP ('psmtsv', True)

Source code in pkg/python/src/pymzlib/readers.py
def identify(path: str, timeout: float | None = 60) -> FileInfo:
    """Identify a result file without parsing its contents.

    Cheap by design: mzLib resolves the type and stops, so identifying a million-row file costs no
    more than identifying an empty one. It is not, however, *pure* - mzLib disambiguates a bare
    ``.tsv`` by reading its first line, a ``.mztab`` by its first five, and a Bruker ``.d`` by which
    analysis file the directory holds. An unreadable file will therefore raise.

    Args:
        path: Path to a result or spectra file. A Bruker ``.d`` directory is also accepted.
        timeout: Seconds to allow.

    Returns:
        A :class:`FileInfo` naming the format and the views it supports.

    Raises:
        UsageError: the path is blank, does not exist, or is not a file type mzLib recognises.
            mzLib has no "unknown" result - a file is dispatchable or it is an error - so use
            :func:`formats` to see what is supported, or catch this to test a file.

    Example:
        >>> info = identify("AllPSMs.psmtsv")                      # doctest: +SKIP
        >>> info.file_type, info.is_quantifiable                   # doctest: +SKIP
        ('psmtsv', True)
    """
    if not isinstance(path, str) or not path.strip():
        raise _bridge.UsageError("A file path is required, e.g. 'AllPSMs.psmtsv'.")

    data = _bridge.invoke("readers", "identify", "--path", path.strip(), timeout=timeout)
    return FileInfo._from_wire(data)

read_results

read_results(path: str, *, limit: int | None = None, offset: int = 0, out: str | None = None, timeout: float | None = None) -> ResultRecords

Read a result file into the uniform record view.

Only the three file types offering the "quantifiable" view can be read this way - check :func:identify first, or catch the error. A file without the view is rejected with a message naming the views it does have.

There is no default row limit. A result file can carry a million rows, and truncating by default would mean the ordinary call returns a table that looks complete and is not. For a large file use out rather than paging: see the note on offset below.

Parameters:

Name Type Description Default
path str

Path to a MetaMorpheus .psmtsv / .osmtsv or an MSFragger psm.tsv.

required
limit int | None

Maximum records to return. None (the default) returns all of them. :attr:ResultRecords.truncated reports whether anything was left behind.

None
offset int

Records to skip. This is a window, not a cursor. mzLib materializes the whole file on every call - its readers look lazy and are not - so paging re-reads and re-parses the file once per page. For a large file, one call with out is right and a paging loop is quadratic.

0
out str | None

Write the records to this path as a tab-separated table and return only a summary, instead of carrying them back in the envelope. The intended path for large files, not an escape hatch. Tab-separated because these fields contain commas.

None
timeout float | None

Seconds to allow. A large file legitimately takes a while; None waits indefinitely.

None

Returns:

Name Type Description
A ResultRecords

class:ResultRecords. Read :attr:ResultRecords.caveats before trusting a field across

ResultRecords

formats.

Raises:

Type Description
UsageError

the path is blank, missing, not a recognised format, or has no quantifiable view.

Example

r = read_results("AllPSMs.psmtsv") # doctest: +SKIP r.record_count, r.truncated # doctest: +SKIP (8, False) import pandas as pd # doctest: +SKIP pd.DataFrame(r.columns) # doctest: +SKIP

Source code in pkg/python/src/pymzlib/readers.py
def read_results(
    path: str,
    *,
    limit: int | None = None,
    offset: int = 0,
    out: str | None = None,
    timeout: float | None = None,
) -> ResultRecords:
    """Read a result file into the uniform record view.

    Only the three file types offering the ``"quantifiable"`` view can be read this way - check
    :func:`identify` first, or catch the error. A file without the view is rejected with a message
    naming the views it does have.

    **There is no default row limit.** A result file can carry a million rows, and truncating by
    default would mean the ordinary call returns a table that looks complete and is not. For a large
    file use ``out`` rather than paging: see the note on ``offset`` below.

    Args:
        path: Path to a MetaMorpheus ``.psmtsv`` / ``.osmtsv`` or an MSFragger ``psm.tsv``.
        limit: Maximum records to return. ``None`` (the default) returns all of them.
            :attr:`ResultRecords.truncated` reports whether anything was left behind.
        offset: Records to skip. **This is a window, not a cursor.** mzLib materializes the whole
            file on every call - its readers look lazy and are not - so paging re-reads and
            re-parses the file once per page. For a large file, one call with ``out`` is right and
            a paging loop is quadratic.
        out: Write the records to this path as a **tab-separated** table and return only a summary,
            instead of carrying them back in the envelope. The intended path for large files, not
            an escape hatch. Tab-separated because these fields contain commas.
        timeout: Seconds to allow. A large file legitimately takes a while; ``None`` waits
            indefinitely.

    Returns:
        A :class:`ResultRecords`. Read :attr:`ResultRecords.caveats` before trusting a field across
        formats.

    Raises:
        UsageError: the path is blank, missing, not a recognised format, or has no quantifiable view.

    Example:
        >>> r = read_results("AllPSMs.psmtsv")                       # doctest: +SKIP
        >>> r.record_count, r.truncated                              # doctest: +SKIP
        (8, False)
        >>> import pandas as pd                                      # doctest: +SKIP
        >>> pd.DataFrame(r.columns)                                  # doctest: +SKIP
    """
    if not isinstance(path, str) or not path.strip():
        raise _bridge.UsageError("A file path is required, e.g. 'AllPSMs.psmtsv'.")

    args: list[str] = ["readers", "read-results", "--path", path.strip()]

    if limit is not None:
        if isinstance(limit, bool) or not isinstance(limit, int) or limit < 0:
            raise _bridge.UsageError(f"limit must be a non-negative whole number or None; got {limit!r}.")
        args += ["--limit", str(limit)]

    if isinstance(offset, bool) or not isinstance(offset, int) or offset < 0:
        raise _bridge.UsageError(f"offset must be a non-negative whole number; got {offset!r}.")
    if offset:
        args += ["--offset", str(offset)]

    if out is not None:
        if not isinstance(out, str) or not out.strip():
            raise _bridge.UsageError("out must be a non-empty path or None.")
        args += ["--out", out.strip()]

    data = _bridge.invoke(*args, timeout=timeout)
    return ResultRecords._from_wire(data)

Errors and diagnostics

Everything pyMzLib raises inherits from PyMzLibError, so a single except catches all of it.

_bridge

Locating and invoking the bundled mzLib bridge executable.

This module is the only place in pyMzLib that knows the bridge exists. Everything above it sees ordinary Python functions and objects. That boundary is deliberate: the transport (today, a self-contained .NET executable invoked per call) can be replaced by an in-process binding or a long-lived local server without any public API changing.

Nothing here imports a third-party package. pyMzLib declares no runtime dependencies, so it cannot participate in a dependency conflict inside anyone's environment.

PyMzLibError

Bases: Exception

Base class for every error pyMzLib raises.

Source code in pkg/python/src/pymzlib/_bridge.py
class PyMzLibError(Exception):
    """Base class for every error pyMzLib raises."""

UsageError

Bases: PyMzLibError, ValueError

A call was malformed — a missing or invalid argument. Raised before any work happens.

Source code in pkg/python/src/pymzlib/_bridge.py
class UsageError(PyMzLibError, ValueError):
    """A call was malformed — a missing or invalid argument. Raised before any work happens."""

BridgeError

Bases: PyMzLibError

mzLib reported a failure.

Attributes:

Name Type Description
error_type

The .NET exception type name, e.g. HttpRequestException. Useful for distinguishing a network failure from a bad accession without parsing prose.

Source code in pkg/python/src/pymzlib/_bridge.py
class BridgeError(PyMzLibError):
    """mzLib reported a failure.

    Attributes:
        error_type: The .NET exception type name, e.g. ``HttpRequestException``. Useful for
            distinguishing a network failure from a bad accession without parsing prose.
    """

    def __init__(self, error_type: str, message: str) -> None:
        super().__init__(message)
        self.error_type = error_type

ServiceUnavailableError

Bases: BridgeError

An external service is unavailable — down, rate-limited, timing out, or unreachable.

This is deliberately a distinct type, because the difference between "the repository is having a bad morning" and "something is broken" is the difference between retrying later and filing a bug. Catch it to back off and retry::

try:
    files = pymzlib.pride.list_files("PXD000001")
except pymzlib.ServiceUnavailableError:
    ...   # EBI's problem; try again later
except pymzlib.BridgeError:
    ...   # ours

The classification happens in the bridge rather than here, so every consumer of the wire format gets it and not only Python. HTTP 408, 429, and 5xx count as unavailable; 404 and 400 do not, because a wrong URL or a malformed request is our problem and excusing it as an outage would hide a real bug.

Source code in pkg/python/src/pymzlib/_bridge.py
class ServiceUnavailableError(BridgeError):
    """An external service is unavailable — down, rate-limited, timing out, or unreachable.

    This is deliberately a distinct type, because the difference between "the repository is
    having a bad morning" and "something is broken" is the difference between retrying later and
    filing a bug. Catch it to back off and retry::

        try:
            files = pymzlib.pride.list_files("PXD000001")
        except pymzlib.ServiceUnavailableError:
            ...   # EBI's problem; try again later
        except pymzlib.BridgeError:
            ...   # ours

    The classification happens in the bridge rather than here, so every consumer of the wire
    format gets it and not only Python. HTTP 408, 429, and 5xx count as unavailable; 404 and 400
    do not, because a wrong URL or a malformed request is our problem and excusing it as an
    outage would hide a real bug.
    """

BridgeTimeoutError

Bases: PyMzLibError

The bridge process did not finish within the timeout.

Deliberately not a :class:ServiceUnavailableError, and the distinction is the whole point. A subprocess timeout has several possible causes and only one of them is a slow service: the bridge may be wedged, the executable may be corrupt, antivirus may be holding it, or the caller may simply have passed a timeout that was too short. Reporting all of that as "the repository is down" is how a real bug gets skipped by every test suite and never seen again.

If the caller wants a slow network to be treated as an outage, they can catch this explicitly — but the library will not guess on their behalf.

Source code in pkg/python/src/pymzlib/_bridge.py
class BridgeTimeoutError(PyMzLibError):
    """The bridge process did not finish within the timeout.

    Deliberately **not** a :class:`ServiceUnavailableError`, and the distinction is the whole
    point. A subprocess timeout has several possible causes and only one of them is a slow
    service: the bridge may be wedged, the executable may be corrupt, antivirus may be holding
    it, or the caller may simply have passed a timeout that was too short. Reporting all of that
    as "the repository is down" is how a real bug gets skipped by every test suite and never
    seen again.

    If the caller wants a slow network to be treated as an outage, they can catch this
    explicitly — but the library will not guess on their behalf.
    """

BridgeNotFoundError

Bases: PyMzLibError

The bridge executable could not be located.

In a released wheel this should be impossible: the executable ships inside the package. It normally means pyMzLib is being run from a source checkout where the bridge has not been built yet (see pkg/build/publish-bridge.ps1).

Source code in pkg/python/src/pymzlib/_bridge.py
class BridgeNotFoundError(PyMzLibError):
    """The bridge executable could not be located.

    In a released wheel this should be impossible: the executable ships inside the package.
    It normally means pyMzLib is being run from a source checkout where the bridge has not
    been built yet (see ``pkg/build/publish-bridge.ps1``).
    """

bridge_path

bridge_path() -> Path

Return the path of the bridge executable that will be used.

Resolution order: the PYMZLIB_BRIDGE environment variable, then the copy staged inside this package for the current platform.

Raises:

Type Description
BridgeNotFoundError

if neither exists.

Source code in pkg/python/src/pymzlib/_bridge.py
def bridge_path() -> Path:
    """Return the path of the bridge executable that will be used.

    Resolution order: the ``PYMZLIB_BRIDGE`` environment variable, then the copy staged
    inside this package for the current platform.

    Raises:
        BridgeNotFoundError: if neither exists.
    """
    override = os.environ.get(BRIDGE_ENV_VAR)
    if override:
        candidate = Path(override)
        if not candidate.is_file():
            raise BridgeNotFoundError(f"{BRIDGE_ENV_VAR} points at '{override}', which is not a file.")
        return candidate

    executable = "mzlib-bridge.exe" if sys.platform == "win32" else "mzlib-bridge"
    candidate = Path(__file__).parent / "_dotnet" / _platform_tag() / executable
    if not candidate.is_file():
        raise BridgeNotFoundError(
            f"No mzLib bridge for this platform at '{candidate}'. "
            f"In a source checkout, build one and set {BRIDGE_ENV_VAR} to its path."
        )
    return candidate

bridge_version

bridge_version() -> dict[str, Any]

Return the bridge's own version information, and check protocol compatibility.

Raises:

Type Description
PyMzLibError

if the bridge speaks a different wire format than this package.

Source code in pkg/python/src/pymzlib/_bridge.py
def bridge_version() -> dict[str, Any]:
    """Return the bridge's own version information, and check protocol compatibility.

    Raises:
        PyMzLibError: if the bridge speaks a different wire format than this package.
    """
    info = invoke("version", timeout=60)
    reported = info.get("protocol")
    if reported != PROTOCOL_VERSION:
        raise PyMzLibError(
            f"mzLib bridge speaks protocol {reported}, but this pyMzLib expects {PROTOCOL_VERSION}. "
            "The Python package and the bridge were built from different sources."
        )
    return info