Skip to content

API reference

Public objects are documented from their typed source and Google-style docstrings.

Aho--Corasick matching

prozor.matching.automaton

Backend-neutral Aho--Corasick peptide matching.

Match dataclass

One half-open keyword match in a searched sequence.

Source code in src/prozor/matching/automaton.py
@dataclass(frozen=True, slots=True)
class Match:
    """One half-open keyword match in a searched sequence."""

    keyword: str
    start: int
    end: int

AhoCorasickBase

Bases: ABC

Common interface implemented by every matching backend.

Source code in src/prozor/matching/automaton.py
class AhoCorasickBase(ABC):
    """Common interface implemented by every matching backend."""

    requested_backend: BackendRequest
    resolved_backend: BackendName

    @abstractmethod
    def find_all(self, text: str) -> Iterator[Match]:
        """Yield every match, including nested and overlapping matches."""

find_all abstractmethod

find_all(text)

Yield every match, including nested and overlapping matches.

Source code in src/prozor/matching/automaton.py
@abstractmethod
def find_all(self, text: str) -> Iterator[Match]:
    """Yield every match, including nested and overlapping matches."""

AhoCorasickPure

Bases: AhoCorasickBase

Portable matcher implemented with :mod:ahocorapy.

Source code in src/prozor/matching/automaton.py
class AhoCorasickPure(AhoCorasickBase):
    """Portable matcher implemented with :mod:`ahocorapy`."""

    def __init__(
        self,
        keywords: Iterable[str],
        *,
        case_sensitive: bool = True,
        requested_backend: BackendRequest = "ahocorapy",
    ) -> None:
        from ahocorapy.keywordtree import KeywordTree

        self.requested_backend = requested_backend
        self.resolved_backend: BackendName = "ahocorapy"
        self._tree = KeywordTree(case_insensitive=not case_sensitive)
        for keyword in _unique_keywords(keywords):
            self._tree.add(keyword)
        self._tree.finalize()

    @override
    def find_all(self, text: str) -> Iterator[Match]:
        """Yield all pure-Python matches in ``text``."""
        for keyword, start in self._tree.search_all(text):
            yield Match(keyword=keyword, start=start, end=start + len(keyword))

find_all

find_all(text)

Yield all pure-Python matches in text.

Source code in src/prozor/matching/automaton.py
@override
def find_all(self, text: str) -> Iterator[Match]:
    """Yield all pure-Python matches in ``text``."""
    for keyword, start in self._tree.search_all(text):
        yield Match(keyword=keyword, start=start, end=start + len(keyword))

AhoCorasickRust

Bases: AhoCorasickBase

Accelerated matcher implemented with :mod:ahocorasick_rs.

Source code in src/prozor/matching/automaton.py
class AhoCorasickRust(AhoCorasickBase):
    """Accelerated matcher implemented with :mod:`ahocorasick_rs`."""

    def __init__(
        self,
        keywords: Iterable[str],
        *,
        case_sensitive: bool = True,
        requested_backend: BackendRequest = "ahocorasick_rs",
    ) -> None:
        import ahocorasick_rs

        self.requested_backend = requested_backend
        self.resolved_backend: BackendName = "ahocorasick_rs"
        self._keywords = _unique_keywords(keywords)
        self._case_sensitive = case_sensitive
        indexed_keywords = (
            self._keywords
            if case_sensitive
            else tuple(keyword.lower() for keyword in self._keywords)
        )
        self._automaton = ahocorasick_rs.AhoCorasick(indexed_keywords)

    @override
    def find_all(self, text: str) -> Iterator[Match]:
        """Yield all Rust matches in ``text``."""
        search_text = text if self._case_sensitive else text.lower()
        for index, start, end in self._automaton.find_matches_as_indexes(
            search_text,
            overlapping=True,
        ):
            yield Match(keyword=self._keywords[index], start=start, end=end)

find_all

find_all(text)

Yield all Rust matches in text.

Source code in src/prozor/matching/automaton.py
@override
def find_all(self, text: str) -> Iterator[Match]:
    """Yield all Rust matches in ``text``."""
    search_text = text if self._case_sensitive else text.lower()
    for index, start, end in self._automaton.find_matches_as_indexes(
        search_text,
        overlapping=True,
    ):
        yield Match(keyword=self._keywords[index], start=start, end=end)

create_automaton

create_automaton(
    keywords, backend="auto", case_sensitive=True
)

Create a matcher and expose both requested and resolved backend names.

Parameters:

Name Type Description Default
keywords Iterable[str]

Peptide patterns to search for.

required
backend str

auto, ahocorapy, or ahocorasick_rs.

'auto'
case_sensitive bool

Whether matching distinguishes letter case.

True

Returns:

Type Description
AhoCorasickBase

A backend-neutral matcher.

Raises:

Type Description
ImportError

The Rust backend was requested but is not installed.

ValueError

backend is not supported.

Source code in src/prozor/matching/automaton.py
def create_automaton(
    keywords: Iterable[str],
    backend: str = "auto",
    case_sensitive: bool = True,
) -> AhoCorasickBase:
    """Create a matcher and expose both requested and resolved backend names.

    Args:
        keywords: Peptide patterns to search for.
        backend: ``auto``, ``ahocorapy``, or ``ahocorasick_rs``.
        case_sensitive: Whether matching distinguishes letter case.

    Returns:
        A backend-neutral matcher.

    Raises:
        ImportError: The Rust backend was requested but is not installed.
        ValueError: ``backend`` is not supported.
    """
    requested_backend = _validate_backend(backend)
    resolved_backend = resolve_backend(requested_backend)
    if resolved_backend == "ahocorasick_rs":
        return AhoCorasickRust(
            keywords,
            case_sensitive=case_sensitive,
            requested_backend=requested_backend,
        )
    return AhoCorasickPure(
        keywords,
        case_sensitive=case_sensitive,
        requested_backend=requested_backend,
    )

resolve_backend

resolve_backend(backend='auto')

Resolve a requested backend to the concrete implementation name.

Source code in src/prozor/matching/automaton.py
def resolve_backend(backend: str = "auto") -> BackendName:
    """Resolve a requested backend to the concrete implementation name."""
    requested_backend = _validate_backend(backend)
    if requested_backend == "ahocorasick_rs":
        if not _rust_available():
            raise ImportError(
                "backend 'ahocorasick_rs' requires a working ahocorasick-rs installation"
            )
        return "ahocorasick_rs"
    if requested_backend == "ahocorapy":
        return "ahocorapy"
    return "ahocorasick_rs" if _rust_available() else "ahocorapy"

get_available_backends

get_available_backends()

Return the concrete matching backends available in this environment.

Source code in src/prozor/matching/automaton.py
def get_available_backends() -> list[BackendName]:
    """Return the concrete matching backends available in this environment."""
    backends: list[BackendName] = ["ahocorapy"]
    if _rust_available():
        backends.append("ahocorasick_rs")
    return backends

Peptide annotation

prozor.matching.annotation

Peptide-to-protein annotation over mappings or streaming protein records.

ProteinSequenceRecord

Bases: Protocol

Smallest protein-record capability required by peptide matching.

Source code in src/prozor/matching/annotation.py
class ProteinSequenceRecord(Protocol):
    """Smallest protein-record capability required by peptide matching."""

    @property
    def id(self) -> str:
        """Return the protein identifier attached to matching results."""
        ...

    @property
    def sequence(self) -> str:
        """Return the protein sequence to search."""
        ...

id property

id

Return the protein identifier attached to matching results.

sequence property

sequence

Return the protein sequence to search.

PeptideAnnotation dataclass

One peptide occurrence in a protein sequence.

Source code in src/prozor/matching/annotation.py
@dataclass(frozen=True, slots=True)
class PeptideAnnotation:
    """One peptide occurrence in a protein sequence."""

    peptide: str
    protein_id: str
    start: int
    end: int

    @property
    def length(self) -> int:
        """Return the matched peptide length."""
        return len(self.peptide)

length property

length

Return the matched peptide length.

AnnotationResult dataclass

Collection of peptide matches and matching-backend provenance.

Source code in src/prozor/matching/annotation.py
@dataclass(slots=True)
class AnnotationResult:
    """Collection of peptide matches and matching-backend provenance."""

    annotations: list[PeptideAnnotation]
    requested_backend: BackendRequest = "ahocorapy"
    resolved_backend: BackendName = "ahocorapy"

    def __len__(self) -> int:
        return len(self.annotations)

    def __iter__(self) -> Iterator[PeptideAnnotation]:
        return iter(self.annotations)

    @property
    def peptides(self) -> set[str]:
        """Return the distinct matched peptide sequences."""
        return {annotation.peptide for annotation in self.annotations}

    @property
    def proteins(self) -> set[str]:
        """Return the distinct matched protein identifiers."""
        return {annotation.protein_id for annotation in self.annotations}

    def filter_tryptic(
        self,
        proteins: Mapping[str, str],
        prefix_residues: str = "RK",
        allow_n_term: bool = True,
        allow_after_init_met: bool = True,
    ) -> AnnotationResult:
        """Return matches with a supported tryptic N-terminal context."""
        filtered: list[PeptideAnnotation] = []
        for annotation in self.annotations:
            sequence = proteins.get(annotation.protein_id, "")
            if not sequence:
                continue
            valid_prefix = (
                (annotation.start == 0 and allow_n_term)
                or (annotation.start == 1 and allow_after_init_met and sequence.startswith("M"))
                or (annotation.start > 0 and sequence[annotation.start - 1] in prefix_residues)
            )
            if valid_prefix:
                filtered.append(annotation)
        return AnnotationResult(
            annotations=filtered,
            requested_backend=self.requested_backend,
            resolved_backend=self.resolved_backend,
        )

peptides property

peptides

Return the distinct matched peptide sequences.

proteins property

proteins

Return the distinct matched protein identifiers.

filter_tryptic

filter_tryptic(
    proteins,
    prefix_residues="RK",
    allow_n_term=True,
    allow_after_init_met=True,
)

Return matches with a supported tryptic N-terminal context.

Source code in src/prozor/matching/annotation.py
def filter_tryptic(
    self,
    proteins: Mapping[str, str],
    prefix_residues: str = "RK",
    allow_n_term: bool = True,
    allow_after_init_met: bool = True,
) -> AnnotationResult:
    """Return matches with a supported tryptic N-terminal context."""
    filtered: list[PeptideAnnotation] = []
    for annotation in self.annotations:
        sequence = proteins.get(annotation.protein_id, "")
        if not sequence:
            continue
        valid_prefix = (
            (annotation.start == 0 and allow_n_term)
            or (annotation.start == 1 and allow_after_init_met and sequence.startswith("M"))
            or (annotation.start > 0 and sequence[annotation.start - 1] in prefix_residues)
        )
        if valid_prefix:
            filtered.append(annotation)
    return AnnotationResult(
        annotations=filtered,
        requested_backend=self.requested_backend,
        resolved_backend=self.resolved_backend,
    )

annotate_peptides

annotate_peptides(
    peptides, proteins, backend="auto", filter_tryptic=False
)

Annotate peptides against an in-memory protein mapping.

Source code in src/prozor/matching/annotation.py
def annotate_peptides(
    peptides: Iterable[str],
    proteins: Mapping[str, str],
    backend: str = "auto",
    filter_tryptic: bool = False,
) -> AnnotationResult:
    """Annotate peptides against an in-memory protein mapping."""
    result = _annotate(peptides, [(tuple(proteins), tuple(proteins.values()))], backend)
    return result.filter_tryptic(proteins) if filter_tryptic else result

annotate_peptides_streaming

annotate_peptides_streaming(
    peptides, protein_records, backend="auto"
)

Annotate peptides against one-pass records exposing id and sequence.

Source code in src/prozor/matching/annotation.py
def annotate_peptides_streaming(
    peptides: Iterable[str],
    protein_records: Iterable[ProteinSequenceRecord],
    backend: str = "auto",
) -> AnnotationResult:
    """Annotate peptides against one-pass records exposing ``id`` and ``sequence``."""
    records = iter(protein_records)
    batches = (
        (tuple(record.id for record in batch), tuple(record.sequence for record in batch))
        for batch in iter(lambda: tuple(islice(records, _BATCH_SIZE)), ())
    )
    return _annotate(peptides, batches, backend)

Tie resolution

prozor.inference.ties

Tie candidates and resolution for greedy protein inference.

TieCandidate dataclass

One indistinguishable protein group competing in a consequential tie.

Source code in src/prozor/inference/ties.py
@dataclass(frozen=True, slots=True)
class TieCandidate:
    """One indistinguishable protein group competing in a consequential tie."""

    proteins: tuple[str, ...]
    unexplained_peptides: frozenset[str]

resolve_current_tie

resolve_current_tie(candidates)

Preserve Prozor's deterministic group-size and accession tie rule.

Source code in src/prozor/inference/ties.py
def resolve_current_tie(candidates: Sequence[TieCandidate]) -> TieCandidate:
    """Preserve Prozor's deterministic group-size and accession tie rule."""
    if not candidates:
        raise ValueError("at least one tie candidate is required")
    return min(candidates, key=lambda candidate: (-len(candidate.proteins), candidate.proteins))

Greedy protein inference

prozor.inference.greedy

Deterministic greedy-parsimony protein inference over unique edges.

ProteinGroup dataclass

Proteins selected together and the peptides assigned to them.

Source code in src/prozor/inference/greedy.py
@dataclass(slots=True)
class ProteinGroup:
    """Proteins selected together and the peptides assigned to them."""

    proteins: list[str]
    peptides: list[str]

    @property
    def protein_id(self) -> str:
        """Return the semicolon-joined protein group identifier."""
        return ";".join(self.proteins)

    @property
    def n_peptides(self) -> int:
        """Return the number of assigned peptides."""
        return len(self.peptides)

    @property
    def n_proteins(self) -> int:
        """Return the number of grouped proteins."""
        return len(self.proteins)

protein_id property

protein_id

Return the semicolon-joined protein group identifier.

n_peptides property

n_peptides

Return the number of assigned peptides.

n_proteins property

n_proteins

Return the number of grouped proteins.

GreedyResult dataclass

Protein groups selected by greedy parsimony.

Source code in src/prozor/inference/greedy.py
@dataclass(slots=True)
class GreedyResult:
    """Protein groups selected by greedy parsimony."""

    groups: list[ProteinGroup]

    def __len__(self) -> int:
        return len(self.groups)

    def __iter__(self) -> Iterator[ProteinGroup]:
        return iter(self.groups)

    @property
    def n_proteins(self) -> int:
        """Return the total number of selected and subsumed proteins."""
        return sum(group.n_proteins for group in self.groups)

    @property
    def n_groups(self) -> int:
        """Return the number of inferred protein groups."""
        return len(self.groups)

    @property
    def n_peptides(self) -> int:
        """Return the number of distinct assigned peptides."""
        return len({peptide for group in self.groups for peptide in group.peptides})

    def to_dict(self) -> dict[str, str]:
        """Return a peptide-to-protein-group mapping."""
        return {peptide: group.protein_id for group in self.groups for peptide in group.peptides}

n_proteins property

n_proteins

Return the total number of selected and subsumed proteins.

n_groups property

n_groups

Return the number of inferred protein groups.

n_peptides property

n_peptides

Return the number of distinct assigned peptides.

to_dict

to_dict()

Return a peptide-to-protein-group mapping.

Source code in src/prozor/inference/greedy.py
def to_dict(self) -> dict[str, str]:
    """Return a peptide-to-protein-group mapping."""
    return {peptide: group.protein_id for group in self.groups for peptide in group.peptides}

greedy_parsimony

greedy_parsimony(
    edges, resolve_tie=resolve_current_tie, subsume=True
)

Select a deterministic parsimonious set of protein groups.

Repeated (peptide, protein) edges collapse before inference. Proteins with identical remaining peptide evidence are grouped. Disjoint equal candidates are ordered deterministically, while overlapping non-identical candidates are passed to resolve_tie.

Parameters:

Name Type Description Default
edges Iterable[PeptideProteinEdge]

Peptide and protein identifier pairs.

required
resolve_tie TieResolver

Operation selecting one consequential tie candidate.

resolve_current_tie
subsume bool

Whether proteins supported by a subset of selected evidence remain in the selected group.

True

Returns:

Type Description
GreedyResult

Deterministically ordered inferred protein groups.

Source code in src/prozor/inference/greedy.py
def greedy_parsimony(
    edges: Iterable[PeptideProteinEdge],
    resolve_tie: TieResolver = resolve_current_tie,
    subsume: bool = True,
) -> GreedyResult:
    """Select a deterministic parsimonious set of protein groups.

    Repeated ``(peptide, protein)`` edges collapse before inference. Proteins
    with identical remaining peptide evidence are grouped. Disjoint equal
    candidates are ordered deterministically, while overlapping non-identical
    candidates are passed to ``resolve_tie``.

    Args:
        edges: Peptide and protein identifier pairs.
        resolve_tie: Operation selecting one consequential tie candidate.
        subsume: Whether proteins supported by a subset of selected evidence
            remain in the selected group.

    Returns:
        Deterministically ordered inferred protein groups.
    """
    peptides, proteins, peptide_proteins, protein_peptides = _build_incidence(edges)
    active_peptides = set(range(len(peptides)))
    active_proteins = set(range(len(proteins)))
    counts = [len(protein_evidence) for protein_evidence in protein_peptides]
    groups: list[ProteinGroup] = []

    while active_peptides and active_proteins:
        selections = _select_winners(
            active_peptides,
            active_proteins,
            protein_peptides,
            counts,
            peptides,
            proteins,
            resolve_tie,
        )
        if not selections:
            break
        for selection in selections:
            subsumed = (
                _find_subsumed(
                    selection,
                    active_peptides,
                    active_proteins,
                    peptide_proteins,
                    protein_peptides,
                )
                if subsume
                else ()
            )
            group_indices = tuple(sorted((*selection.winners, *subsumed)))
            groups.append(
                ProteinGroup(
                    proteins=sorted(proteins[index] for index in group_indices),
                    peptides=sorted(peptides[index] for index in selection.covered_peptides),
                )
            )
            active_peptides.difference_update(selection.covered_peptides)
            active_proteins.difference_update(group_indices)
            _decrement_counts(
                selection.covered_peptides,
                active_proteins,
                peptide_proteins,
                counts,
            )

    groups.sort(
        key=lambda group: (
            -group.n_peptides,
            -group.n_proteins,
            tuple(group.proteins),
        )
    )
    return GreedyResult(groups=groups)