From ef30daa01f7604032354de02b2b0128be23e4e3a Mon Sep 17 00:00:00 2001 From: Joshua Klein Date: Sun, 16 Jun 2024 22:33:00 -0400 Subject: [PATCH] Renamed root package for consistency --- README.md | 5 - bench/nist_msp_ftp_test.py | 4 +- examples/draw_entry.py | 4 +- examples/first_n_entries.py | 10 +- makefile | 2 +- mzlib/__init__.py | 11 -- mzspeclib/__init__.py | 11 ++ {mzlib => mzspeclib}/analyte.py | 2 +- {mzlib => mzspeclib}/annotation.py | 0 {mzlib => mzspeclib}/attributes.py | 0 {mzlib => mzspeclib}/backends/__init__.py | 0 {mzlib => mzspeclib}/backends/base.py | 12 +- {mzlib => mzspeclib}/backends/bibliospec.py | 12 +- {mzlib => mzspeclib}/backends/diann.py | 8 +- {mzlib => mzspeclib}/backends/encyclopedia.py | 12 +- {mzlib => mzspeclib}/backends/json.py | 12 +- {mzlib => mzspeclib}/backends/msp.py | 173 +++++++++++++++--- {mzlib => mzspeclib}/backends/spectronaut.py | 10 +- {mzlib => mzspeclib}/backends/sptxt.py | 8 +- {mzlib => mzspeclib}/backends/text.py | 12 +- {mzlib => mzspeclib}/backends/utils.py | 0 {mzlib => mzspeclib}/cluster.py | 2 +- {mzlib => mzspeclib}/defaults.py | 0 {mzlib => mzspeclib}/draw.py | 0 {mzlib => mzspeclib}/index/__init__.py | 0 {mzlib => mzspeclib}/index/base.py | 0 {mzlib => mzspeclib}/index/memory.py | 2 +- {mzlib => mzspeclib}/index/sql.py | 2 +- {mzlib => mzspeclib}/obo/psi-ms.obo | 0 {mzlib => mzspeclib}/ontology.py | 0 {mzlib => mzspeclib}/ontology_term.py | 0 {mzlib => mzspeclib}/peak_list.py | 0 {mzlib => mzspeclib}/spectrum.py | 10 +- {mzlib => mzspeclib}/spectrum_library.py | 16 +- .../spectrum_library_collection.py | 0 .../spectrum_library_index.py | 0 {mzlib => mzspeclib}/tools/__init__.py | 0 {mzlib => mzspeclib}/tools/cli.py | 16 +- {mzlib => mzspeclib}/tools/utils.py | 0 .../universal_spectrum_identifier.py | 0 {mzlib => mzspeclib}/utils.py | 0 {mzlib => mzspeclib}/validate/__init__.py | 0 {mzlib => mzspeclib}/validate/level.py | 0 {mzlib => mzspeclib}/validate/object_rule.py | 8 +- .../validate/rules/__init__.py | 0 {mzlib => mzspeclib}/validate/rules/all.json | 0 {mzlib => mzspeclib}/validate/rules/base.json | 0 .../validate/rules/consensus.json | 0 .../validate/rules/convert_to_xlsx.py | 0 {mzlib => mzspeclib}/validate/rules/gold.json | 0 .../validate/rules/peptide.json | 0 .../validate/rules/silver.json | 0 .../validate/rules/single.json | 0 mzspeclib/validate/rules/test.xlsx | Bin 0 -> 18946 bytes .../validate/rules/tmp/README.md | 0 .../rules/tmp/excel_terms_to_json_rules.py | 0 .../validate/rules/tmp/terms.xlsx | Bin .../rules/validator-rules-schema.json | 0 .../validate/semantic_rule.py | 6 +- {mzlib => mzspeclib}/validate/validator.py | 18 +- pyproject.toml | 4 +- tests/test_cluster.py | 4 +- tests/test_data/generate_annotations.py | 4 +- tests/test_index.py | 4 +- tests/test_library.py | 2 +- tests/test_library_backend.py | 6 +- tests/test_msp_attributes.py | 4 +- tests/test_spectrum.py | 2 +- tests/test_validate.py | 4 +- 69 files changed, 267 insertions(+), 155 deletions(-) delete mode 100644 mzlib/__init__.py create mode 100644 mzspeclib/__init__.py rename {mzlib => mzspeclib}/analyte.py (98%) rename {mzlib => mzspeclib}/annotation.py (100%) rename {mzlib => mzspeclib}/attributes.py (100%) rename {mzlib => mzspeclib}/backends/__init__.py (100%) rename {mzlib => mzspeclib}/backends/base.py (98%) rename {mzlib => mzspeclib}/backends/bibliospec.py (95%) rename {mzlib => mzspeclib}/backends/diann.py (96%) rename {mzlib => mzspeclib}/backends/encyclopedia.py (94%) rename {mzlib => mzspeclib}/backends/json.py (98%) rename {mzlib => mzspeclib}/backends/msp.py (92%) rename {mzlib => mzspeclib}/backends/spectronaut.py (97%) rename {mzlib => mzspeclib}/backends/sptxt.py (91%) rename {mzlib => mzspeclib}/backends/text.py (99%) rename {mzlib => mzspeclib}/backends/utils.py (100%) rename {mzlib => mzspeclib}/cluster.py (93%) rename {mzlib => mzspeclib}/defaults.py (100%) rename {mzlib => mzspeclib}/draw.py (100%) rename {mzlib => mzspeclib}/index/__init__.py (100%) rename {mzlib => mzspeclib}/index/base.py (100%) rename {mzlib => mzspeclib}/index/memory.py (99%) rename {mzlib => mzspeclib}/index/sql.py (99%) rename {mzlib => mzspeclib}/obo/psi-ms.obo (100%) rename {mzlib => mzspeclib}/ontology.py (100%) rename {mzlib => mzspeclib}/ontology_term.py (100%) rename {mzlib => mzspeclib}/peak_list.py (100%) rename {mzlib => mzspeclib}/spectrum.py (96%) rename {mzlib => mzspeclib}/spectrum_library.py (96%) rename {mzlib => mzspeclib}/spectrum_library_collection.py (100%) rename {mzlib => mzspeclib}/spectrum_library_index.py (100%) rename {mzlib => mzspeclib}/tools/__init__.py (100%) rename {mzlib => mzspeclib}/tools/cli.py (95%) rename {mzlib => mzspeclib}/tools/utils.py (100%) rename {mzlib => mzspeclib}/universal_spectrum_identifier.py (100%) rename {mzlib => mzspeclib}/utils.py (100%) rename {mzlib => mzspeclib}/validate/__init__.py (100%) rename {mzlib => mzspeclib}/validate/level.py (100%) rename {mzlib => mzspeclib}/validate/object_rule.py (93%) rename {mzlib => mzspeclib}/validate/rules/__init__.py (100%) rename {mzlib => mzspeclib}/validate/rules/all.json (100%) rename {mzlib => mzspeclib}/validate/rules/base.json (100%) rename {mzlib => mzspeclib}/validate/rules/consensus.json (100%) rename {mzlib => mzspeclib}/validate/rules/convert_to_xlsx.py (100%) rename {mzlib => mzspeclib}/validate/rules/gold.json (100%) rename {mzlib => mzspeclib}/validate/rules/peptide.json (100%) rename {mzlib => mzspeclib}/validate/rules/silver.json (100%) rename {mzlib => mzspeclib}/validate/rules/single.json (100%) create mode 100644 mzspeclib/validate/rules/test.xlsx rename {mzlib => mzspeclib}/validate/rules/tmp/README.md (100%) rename {mzlib => mzspeclib}/validate/rules/tmp/excel_terms_to_json_rules.py (100%) rename {mzlib => mzspeclib}/validate/rules/tmp/terms.xlsx (100%) rename {mzlib => mzspeclib}/validate/rules/validator-rules-schema.json (100%) rename {mzlib => mzspeclib}/validate/semantic_rule.py (99%) rename {mzlib => mzspeclib}/validate/validator.py (96%) diff --git a/README.md b/README.md index c82a9f6..f02f3f7 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,6 @@ For development, run: pip install --editable . ``` -Or from the root of this repository: -```sh -pip install --editable implementations/python -``` - Test with ``` pytest diff --git a/bench/nist_msp_ftp_test.py b/bench/nist_msp_ftp_test.py index 521332a..abbd39a 100644 --- a/bench/nist_msp_ftp_test.py +++ b/bench/nist_msp_ftp_test.py @@ -5,8 +5,8 @@ from urllib.request import urlopen -from mzlib.backends import msp -from mzlib import annotation +from mzspeclib.backends import msp +from mzspeclib import annotation urls = [ diff --git a/examples/draw_entry.py b/examples/draw_entry.py index 8126d7e..5ffe989 100644 --- a/examples/draw_entry.py +++ b/examples/draw_entry.py @@ -4,8 +4,8 @@ from matplotlib import pyplot as plt -from mzlib.spectrum_library import SpectrumLibrary -from mzlib.draw import draw_spectrum +from mzspeclib.spectrum_library import SpectrumLibrary +from mzspeclib.draw import draw_spectrum def main(path, spectrum_key): diff --git a/examples/first_n_entries.py b/examples/first_n_entries.py index 0aa4c05..bc175d0 100644 --- a/examples/first_n_entries.py +++ b/examples/first_n_entries.py @@ -1,10 +1,10 @@ import click -from mzlib import SpectrumLibrary -from mzlib.backends import SpectralLibraryBackendBase, FormatInferenceFailure, TextSpectralLibraryWriter -from mzlib.cluster import SpectrumCluster -from mzlib.index import MemoryIndex, SQLIndex -from mzlib.spectrum import Spectrum +from mzspeclib import SpectrumLibrary +from mzspeclib.backends import SpectralLibraryBackendBase, FormatInferenceFailure, TextSpectralLibraryWriter +from mzspeclib.cluster import SpectrumCluster +from mzspeclib.index import MemoryIndex, SQLIndex +from mzspeclib.spectrum import Spectrum @click.command('first_n_entries') @click.argument('inpath', type=click.Path(exists=True)) diff --git a/makefile b/makefile index 1ccafcb..f73040d 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ test: - pytest -r a -v tests --cov mzlib --cov-report=html --cov-report term + pytest -r a -v tests --cov mzspeclib --cov-report=html --cov-report term retest: py.test -v tests --lf --pdb diff --git a/mzlib/__init__.py b/mzlib/__init__.py deleted file mode 100644 index 65badd8..0000000 --- a/mzlib/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""HUPO-PSI Spectral library format.""" - -from mzlib.spectrum import Spectrum -from mzlib.analyte import Analyte, Interpretation - -from mzlib.index import MemoryIndex, SQLIndex - -from mzlib.spectrum_library import SpectrumLibrary -from mzlib.spectrum_library_index import SpectrumLibraryIndex -from mzlib.spectrum_library_collection import SpectrumLibraryCollection -from mzlib.universal_spectrum_identifier import UniversalSpectrumIdentifier diff --git a/mzspeclib/__init__.py b/mzspeclib/__init__.py new file mode 100644 index 0000000..668fab7 --- /dev/null +++ b/mzspeclib/__init__.py @@ -0,0 +1,11 @@ +"""HUPO-PSI Spectral library format.""" + +from mzspeclib.spectrum import Spectrum +from mzspeclib.analyte import Analyte, Interpretation + +from mzspeclib.index import MemoryIndex, SQLIndex + +from mzspeclib.spectrum_library import SpectrumLibrary +from mzspeclib.spectrum_library_index import SpectrumLibraryIndex +from mzspeclib.spectrum_library_collection import SpectrumLibraryCollection +from mzspeclib.universal_spectrum_identifier import UniversalSpectrumIdentifier diff --git a/mzlib/analyte.py b/mzspeclib/analyte.py similarity index 98% rename from mzlib/analyte.py rename to mzspeclib/analyte.py index 12f8a25..82d3714 100644 --- a/mzlib/analyte.py +++ b/mzspeclib/analyte.py @@ -4,7 +4,7 @@ from pyteomics import proforma -from mzlib.attributes import AttributedEntity, IdentifiedAttributeManager, AttributeManagedProperty, AttributeProxy, AttributeGroupFacet +from mzspeclib.attributes import AttributedEntity, IdentifiedAttributeManager, AttributeManagedProperty, AttributeProxy, AttributeGroupFacet FIRST_ANALYTE_KEY = '1' diff --git a/mzlib/annotation.py b/mzspeclib/annotation.py similarity index 100% rename from mzlib/annotation.py rename to mzspeclib/annotation.py diff --git a/mzlib/attributes.py b/mzspeclib/attributes.py similarity index 100% rename from mzlib/attributes.py rename to mzspeclib/attributes.py diff --git a/mzlib/backends/__init__.py b/mzspeclib/backends/__init__.py similarity index 100% rename from mzlib/backends/__init__.py rename to mzspeclib/backends/__init__.py diff --git a/mzlib/backends/base.py b/mzspeclib/backends/base.py similarity index 98% rename from mzlib/backends/base.py rename to mzspeclib/backends/base.py index 8a876ec..4f4b371 100644 --- a/mzlib/backends/base.py +++ b/mzspeclib/backends/base.py @@ -12,13 +12,13 @@ from psims.controlled_vocabulary import Entity from psims.controlled_vocabulary.controlled_vocabulary import ( load_uo, load_unimod, load_psims) -from mzlib.cluster import SpectrumCluster +from mzspeclib.cluster import SpectrumCluster -from mzlib.index import MemoryIndex, SQLIndex, IndexBase -from mzlib.spectrum import LIBRARY_SPECTRUM_INDEX, LIBRARY_SPECTRUM_KEY, Spectrum -from mzlib.analyte import Analyte, Interpretation, InterpretationMember, ANALYTE_MIXTURE_TERM -from mzlib.attributes import Attributed, AttributedEntity, AttributeSet, AttributeManagedProperty -from mzlib.ontology import _VocabularyResolverMixin +from mzspeclib.index import MemoryIndex, SQLIndex, IndexBase +from mzspeclib.spectrum import LIBRARY_SPECTRUM_INDEX, LIBRARY_SPECTRUM_KEY, Spectrum +from mzspeclib.analyte import Analyte, Interpretation, InterpretationMember, ANALYTE_MIXTURE_TERM +from mzspeclib.attributes import Attributed, AttributedEntity, AttributeSet, AttributeManagedProperty +from mzspeclib.ontology import _VocabularyResolverMixin from .utils import open_stream, _LineBuffer diff --git a/mzlib/backends/bibliospec.py b/mzspeclib/backends/bibliospec.py similarity index 95% rename from mzlib/backends/bibliospec.py rename to mzspeclib/backends/bibliospec.py index 91e5095..229510b 100644 --- a/mzlib/backends/bibliospec.py +++ b/mzspeclib/backends/bibliospec.py @@ -13,14 +13,14 @@ from pyteomics import proforma -from mzlib import annotation -from mzlib.analyte import FIRST_ANALYTE_KEY, FIRST_INTERPRETATION_KEY, Analyte -from mzlib.spectrum import Spectrum, SPECTRUM_NAME, CHARGE_STATE -from mzlib.attributes import AttributeManager, Attributed +from mzspeclib import annotation +from mzspeclib.analyte import FIRST_ANALYTE_KEY, FIRST_INTERPRETATION_KEY, Analyte +from mzspeclib.spectrum import Spectrum, SPECTRUM_NAME, CHARGE_STATE +from mzspeclib.attributes import AttributeManager, Attributed -from mzlib.backends.base import SpectralLibraryBackendBase, FORMAT_VERSION_TERM, DEFAULT_VERSION +from mzspeclib.backends.base import SpectralLibraryBackendBase, FORMAT_VERSION_TERM, DEFAULT_VERSION -from mzlib.index.base import IndexBase +from mzspeclib.index.base import IndexBase class BibliospecBase: diff --git a/mzlib/backends/diann.py b/mzspeclib/backends/diann.py similarity index 96% rename from mzlib/backends/diann.py rename to mzspeclib/backends/diann.py index a137438..6713aa6 100644 --- a/mzlib/backends/diann.py +++ b/mzspeclib/backends/diann.py @@ -10,10 +10,10 @@ from pyteomics import proforma -from mzlib import annotation -from mzlib.backends.base import DEFAULT_VERSION, FORMAT_VERSION_TERM, LIBRARY_NAME_TERM, _CSVSpectralLibraryBackendBase -from mzlib.backends.utils import open_stream, urlify -from mzlib.spectrum import Spectrum, SPECTRUM_NAME +from mzspeclib import annotation +from mzspeclib.backends.base import DEFAULT_VERSION, FORMAT_VERSION_TERM, LIBRARY_NAME_TERM, _CSVSpectralLibraryBackendBase +from mzspeclib.backends.utils import open_stream, urlify +from mzspeclib.spectrum import Spectrum, SPECTRUM_NAME def _rewrite_unimod_peptide_as_proforma(sequence: str) -> str: diff --git a/mzlib/backends/encyclopedia.py b/mzspeclib/backends/encyclopedia.py similarity index 94% rename from mzlib/backends/encyclopedia.py rename to mzspeclib/backends/encyclopedia.py index 4a9118b..292bb4b 100644 --- a/mzlib/backends/encyclopedia.py +++ b/mzspeclib/backends/encyclopedia.py @@ -13,14 +13,14 @@ from pyteomics import proforma -from mzlib import annotation -from mzlib.analyte import FIRST_ANALYTE_KEY, FIRST_INTERPRETATION_KEY, Analyte, ProteinDescription -from mzlib.spectrum import Spectrum, SPECTRUM_NAME, CHARGE_STATE -from mzlib.attributes import AttributeManager, Attributed, Attribute +from mzspeclib import annotation +from mzspeclib.analyte import FIRST_ANALYTE_KEY, FIRST_INTERPRETATION_KEY, Analyte, ProteinDescription +from mzspeclib.spectrum import Spectrum, SPECTRUM_NAME, CHARGE_STATE +from mzspeclib.attributes import AttributeManager, Attributed, Attribute -from mzlib.backends.base import SpectralLibraryBackendBase, FORMAT_VERSION_TERM, DEFAULT_VERSION +from mzspeclib.backends.base import SpectralLibraryBackendBase, FORMAT_VERSION_TERM, DEFAULT_VERSION -from mzlib.index.base import IndexBase +from mzspeclib.index.base import IndexBase DECOY_SPECTRUM = "MS:1003192|decoy spectrum" diff --git a/mzlib/backends/json.py b/mzspeclib/backends/json.py similarity index 98% rename from mzlib/backends/json.py rename to mzspeclib/backends/json.py index 82aa594..eee6155 100644 --- a/mzlib/backends/json.py +++ b/mzspeclib/backends/json.py @@ -7,13 +7,13 @@ from typing import Any, Iterable, List, Dict, Mapping, Union from pathlib import Path -from mzlib.cluster import SpectrumCluster +from mzspeclib.cluster import SpectrumCluster -from mzlib.index import MemoryIndex -from mzlib.attributes import AttributeManager, Attributed, AttributeSet -from mzlib.annotation import parse_annotation, IonAnnotationBase -from mzlib.analyte import Analyte, Interpretation, FIRST_INTERPRETATION_KEY -from mzlib.spectrum import Spectrum +from mzspeclib.index import MemoryIndex +from mzspeclib.attributes import AttributeManager, Attributed, AttributeSet +from mzspeclib.annotation import parse_annotation, IonAnnotationBase +from mzspeclib.analyte import Analyte, Interpretation, FIRST_INTERPRETATION_KEY +from mzspeclib.spectrum import Spectrum from .base import SpectralLibraryBackendBase, SpectralLibraryWriterBase, FORMAT_VERSION_TERM, AttributeSetTypes from .utils import open_stream diff --git a/mzlib/backends/msp.py b/mzspeclib/backends/msp.py similarity index 92% rename from mzlib/backends/msp.py rename to mzspeclib/backends/msp.py index 8d68a11..cdeb4e0 100644 --- a/mzlib/backends/msp.py +++ b/mzspeclib/backends/msp.py @@ -5,6 +5,7 @@ cover a representative subset from the NIST and some found in the wild. If you encounter an MSP file that does not parse properly, please report it. """ +from dataclasses import dataclass, field import re import io import os @@ -12,18 +13,21 @@ import itertools import warnings +from fractions import Fraction + from typing import ( Any, Callable, Collection, Dict, List, Mapping, Optional, - Set, Tuple, Iterable, DefaultDict) + Set, Tuple, Iterable, DefaultDict, + NamedTuple) from pyteomics import proforma -from mzlib import annotation +from mzspeclib import annotation -from mzlib.analyte import FIRST_ANALYTE_KEY, FIRST_INTERPRETATION_KEY, Analyte, Interpretation, ProteinDescription -from mzlib.spectrum import Spectrum, SPECTRUM_NAME -from mzlib.attributes import AttributeManager, AttributeSet, Attributed +from mzspeclib.analyte import FIRST_ANALYTE_KEY, FIRST_INTERPRETATION_KEY, Analyte, Interpretation, ProteinDescription +from mzspeclib.spectrum import Spectrum, SPECTRUM_NAME +from mzspeclib.attributes import Attribute, AttributeManager, AttributeSet, Attributed from .base import ( DEFAULT_VERSION, FORMAT_VERSION_TERM, _PlainTextSpectralLibraryBackendBase, @@ -56,8 +60,10 @@ def _generate_numpeaks_keys(): NUM_PEAKS_KEYS = _generate_numpeaks_keys() -leader_terms_pattern = re.compile(r"(Name|NAME|Compound|COMPOUND)\s*:") -leader_terms_line_pattern = re.compile(r'(?:Name|NAME|Compound|COMPOUND)\s*:\s+(.+)') +LEADER_TERMS_PATTERN = re.compile(r"(Name|NAME|Compound|COMPOUND)\s*:") +LEADER_TERMS_LINE_PATTERN = re.compile(r'(?:Name|NAME|Compound|COMPOUND)\s*:\s+(.+)') + +SPACE_SPLITTER = re.compile(r"\s+") STRIPPED_PEPTIDE_TERM = "MS:1000888|stripped peptide sequence" PEPTIDE_MODIFICATION_TERM = "MS:1001471|peptide modification details" @@ -66,6 +72,12 @@ def _generate_numpeaks_keys(): PEAK_ATTRIB = "MS:1003254|peak attribute" +RawPeakLine = Tuple[float, float, str, str] +PeakLine = Tuple[float, float, List[annotation.IonAnnotationBase], List[Any]] + +PeakAggregateParseFn = Callable[[str], Any] + + class AttributeHandler: keys: Collection[str] @@ -296,8 +308,8 @@ def add(self, handler: AttributeHandler): "MC": "MS:1003044|number of missed cleavages", "Mods": PEPTIDE_MODIFICATION_TERM, "Naa": "MS:1003043|number of residues", + "Parent": "MS:1000744|selected ion m/z", "PrecursorMonoisoMZ": "MS:1003208|experimental precursor monoisotopic m/z", - "Parent": "MS:1003208|experimental precursor monoisotopic m/z", "ObservedPrecursorMZ": "MS:1003208|experimental precursor monoisotopic m/z", "PrecursorMZ": "MS:1003208|experimental precursor monoisotopic m/z", "PRECURSORMZ": "MS:1003208|experimental precursor monoisotopic m/z", @@ -355,6 +367,7 @@ def unassigned_peaks_handler(key: str, value: str, container: Attributed) -> boo is_top_20 = True value = value.split("/")[0] value = int(value) + assert isinstance(value, int) if is_top_20: container.add_attribute("MS:1003290|number of unassigned peaks among top 20 peaks", value) else: @@ -362,20 +375,19 @@ def unassigned_peaks_handler(key: str, value: str, container: Attributed) -> boo return True -interpretation_terms = CaseInsensitiveDict({ - "Unassigned_all_20ppm": "MS:1003079|total unassigned intensity fraction", - "Unassign_all": "MS:1003079|total unassigned intensity fraction", - - "top_20_num_unassigned_peaks_20ppm": "MS:1003290|number of unassigned peaks among top 20 peaks", - "num_unassigned_peaks_20ppm": unassigned_peaks_handler, - "num_unassigned_peaks": unassigned_peaks_handler, - - "max_unassigned_ab_20ppm": "MS:1003289|intensity of highest unassigned peak", - "max_unassigned_ab": "MS:1003289|intensity of highest unassigned peak", - - "Unassigned_20ppm": "MS:1003080|top 20 peak unassigned intensity fraction", - "Unassigned": "MS:1003080|top 20 peak unassigned intensity fraction", -}) +interpretation_terms = CaseInsensitiveDict( + { + "Unassigned_all_20ppm": "MS:1003079|total unassigned intensity fraction", + "Unassign_all": "MS:1003079|total unassigned intensity fraction", + "top_20_num_unassigned_peaks_20ppm": unassigned_peaks_handler, + "num_unassigned_peaks_20ppm": unassigned_peaks_handler, + "num_unassigned_peaks": unassigned_peaks_handler, + "max_unassigned_ab_20ppm": "MS:1003289|intensity of highest unassigned peak", + "max_unassigned_ab": "MS:1003289|intensity of highest unassigned peak", + "Unassigned_20ppm": "MS:1003080|top 20 peak unassigned intensity fraction", + "Unassigned": "MS:1003080|top 20 peak unassigned intensity fraction", + } +) interpretation_member_terms = CaseInsensitiveDict({ @@ -879,6 +891,111 @@ def add(self, key: str, value: Optional[str] = None): ] +def proportion_parser(aggregation: str) -> float: + """Parse for fractions or percentages""" + if '/' in aggregation: + aggregation: Fraction = Fraction(aggregation) + return float(aggregation) + else: + return float(aggregation) + + +@dataclass +class PeakAggregationParser: + """ + Parse peak aggregation information. + + Subtypes may produce different attributes. + + Attributes + ---------- + peak_attributes : list[:class:`~.Attribute`] + The attributes this parser expects + """ + + peak_attributes: List[Tuple[Attribute, PeakAggregateParseFn]] = field(default_factory=lambda: [Attribute(PEAK_ATTRIB, PEAK_OBSERVATION_FREQ)]) + + def __call__(self, aggregation: str, wrap_errors: bool=True, **kwargs) -> List[Tuple[Attribute, float]]: + parsed = [] + for i, (k, parser), token in enumerate(SPACE_SPLITTER.split(aggregation)): + try: + result = parser(token) + parsed.append((k, result)) + except Exception as err: + if not wrap_errors: + raise err from None + else: + logger.error(f"Failed to parse aggregation at {i}") + parsed.append((k, token)) + + +@dataclass +class PeakParsingStrategy: + """ + A combination of peak annotation parsing and peak aggregation parsing + strategies. + + Attributes + ---------- + annotation_parser : :class:`MSPAnnotationStringParser` + The peak annotation parser + """ + + annotation_parser: Optional[MSPAnnotationStringParser] = None + aggregation_parser: Optional[PeakAggregationParser] = None + + def has_aggregation(self) -> bool: + return self.aggregation_parser is not None + + def has_annotation(self) -> bool: + return self.annotation_parser is not None + + def parse_aggregation(self, aggregation: str, wrap_errors: bool=True, **kwargs): + return self.aggregation_parser(aggregation=aggregation, wrap_errors=wrap_errors, **kwargs) + + def parse_annotation(self, annotation: str, wrap_errors: bool=True, **kwargs): + return self.annotation_parser(annotation_string=annotation, wrap_errors=wrap_errors, **kwargs) + + def parse_peak_annotations(self, peal_list: List[RawPeakLine]): + pass + + def parse_peak_list(self, peak_lines: List[str]) -> List[RawPeakLine]: + peak_list = [] + for values in peak_lines: + interpretations = "" + aggregation = "" + if len(values) == 1: + mz = values + intensity = "1" + if len(values) == 2: + mz, intensity = values + elif len(values) == 3: + mz, intensity, interpretations = values + elif len(values) > 3: + mz, intensity, interpretations = values[0:2] + else: + mz = "1" + intensity = "1" + + interpretations = interpretations.strip('"') + aggregation = None + if interpretations.startswith("?"): + parts = SPACE_SPLITTER.split(interpretations) + if len(parts) > 1: + # Some msp files have a concept for ?i, but this requires a definition + interpretations = "?" + aggregation = parts[1:] + else: + if " " in interpretations: + parts = SPACE_SPLITTER.split(interpretations) + interpretations = parts[0] + aggregation = parts[1:] + + #### Add to the peak list + peak_list.append([float(mz), float(intensity), interpretations, aggregation]) + return peak_list + + class MSPSpectralLibrary(_PlainTextSpectralLibraryBackendBase): """ A reader for the plain text NIST MSP spectral library format. @@ -917,7 +1034,7 @@ def __init__(self, filename, index_type=None, read_metadata=True, create_index: def guess_from_header(cls, filename: str) -> bool: with open_stream(filename, 'r') as stream: first_line = stream.readline() - if leader_terms_pattern.match(first_line): + if LEADER_TERMS_PATTERN.match(first_line): return True return False @@ -947,7 +1064,7 @@ def _parse_header_from_stream(self, stream: io.IOBase) -> Tuple[bool, int]: attributes.add_attribute(LIBRARY_NAME_TERM, stream.name.rsplit('.msp', 1)[0].split(os.sep)[-1]) self.attributes.clear() self.attributes._from_iterable(attributes) - if leader_terms_pattern.match(first_line): + if LEADER_TERMS_PATTERN.match(first_line): return True, 0 return False, 0 @@ -1009,7 +1126,7 @@ def create_index(self) -> int: line = line.rstrip() # TODO: Name: could be Compound or SpectrumName if state == 'header': - if leader_terms_pattern.match(line): + if LEADER_TERMS_PATTERN.match(line): state = 'body' spectrum_file_offset = line_beginning_file_offset else: @@ -1017,7 +1134,7 @@ def create_index(self) -> int: if state == 'body': if len(line) == 0: continue - if leader_terms_pattern.match(line): + if LEADER_TERMS_PATTERN.match(line): if len(spectrum_buffer) > 0: self.index.add( number=n_spectra + start_index, @@ -1032,7 +1149,7 @@ def create_index(self) -> int: logger.info(f"... Indexed {file_offset} bytes, {n_spectra} spectra read") spectrum_file_offset = line_beginning_file_offset - spectrum_name = leader_terms_line_pattern.match(line).group(1) + spectrum_name = LEADER_TERMS_LINE_PATTERN.match(line).group(1) spectrum_buffer.append(line) logger.debug(f"Processed {file_offset} bytes, {n_spectra} spectra read") @@ -1068,7 +1185,7 @@ def _buffer_from_stream(self, infile: Iterable[str]) -> List[str]: if state == 'body': if len(line) == 0: continue - if leader_terms_pattern.match(line): + if LEADER_TERMS_PATTERN.match(line): if len(spectrum_buffer) > 0: return spectrum_buffer spectrum_buffer.append(line) diff --git a/mzlib/backends/spectronaut.py b/mzspeclib/backends/spectronaut.py similarity index 97% rename from mzlib/backends/spectronaut.py rename to mzspeclib/backends/spectronaut.py index 7750fb6..f95cbcd 100644 --- a/mzlib/backends/spectronaut.py +++ b/mzspeclib/backends/spectronaut.py @@ -7,11 +7,11 @@ from pyteomics import proforma -from mzlib import annotation -from mzlib.analyte import Analyte -from mzlib.backends.base import LIBRARY_NAME_TERM, _CSVSpectralLibraryBackendBase, FORMAT_VERSION_TERM, DEFAULT_VERSION -from mzlib.backends.utils import open_stream, urlify -from mzlib.spectrum import Spectrum, SPECTRUM_NAME +from mzspeclib import annotation +from mzspeclib.analyte import Analyte +from mzspeclib.backends.base import LIBRARY_NAME_TERM, _CSVSpectralLibraryBackendBase, FORMAT_VERSION_TERM, DEFAULT_VERSION +from mzspeclib.backends.utils import open_stream, urlify +from mzspeclib.spectrum import Spectrum, SPECTRUM_NAME CHARGE_STATE = "MS:1000041|charge state" diff --git a/mzlib/backends/sptxt.py b/mzspeclib/backends/sptxt.py similarity index 91% rename from mzlib/backends/sptxt.py rename to mzspeclib/backends/sptxt.py index 957f2a1..719f17c 100644 --- a/mzlib/backends/sptxt.py +++ b/mzspeclib/backends/sptxt.py @@ -3,12 +3,12 @@ from typing import Dict, Tuple -from mzlib.attributes import AttributeManager +from mzspeclib.attributes import AttributeManager -from mzlib.annotation import AnnotationStringParser +from mzspeclib.annotation import AnnotationStringParser -from mzlib.backends.base import DEFAULT_VERSION, FORMAT_VERSION_TERM, LIBRARY_NAME_TERM -from mzlib.backends.utils import open_stream +from mzspeclib.backends.base import DEFAULT_VERSION, FORMAT_VERSION_TERM, LIBRARY_NAME_TERM +from mzspeclib.backends.utils import open_stream from .msp import MSPSpectralLibrary as _MSPSpectralLibrary from .utils import CaseInsensitiveDict diff --git a/mzlib/backends/text.py b/mzspeclib/backends/text.py similarity index 99% rename from mzlib/backends/text.py rename to mzspeclib/backends/text.py index 1e92209..ae3125b 100644 --- a/mzlib/backends/text.py +++ b/mzspeclib/backends/text.py @@ -11,12 +11,12 @@ from collections import deque from typing import ClassVar, List, Optional, Tuple, Union, Iterable -from mzlib.annotation import parse_annotation -from mzlib.spectrum import Spectrum -from mzlib.cluster import SpectrumCluster -from mzlib.attributes import Attribute, AttributeManager, Attributed, AttributeSet -from mzlib.analyte import Analyte, Interpretation, InterpretationMember -from mzlib.validate.object_rule import ValidationWarning +from mzspeclib.annotation import parse_annotation +from mzspeclib.spectrum import Spectrum +from mzspeclib.cluster import SpectrumCluster +from mzspeclib.attributes import Attribute, AttributeManager, Attributed, AttributeSet +from mzspeclib.analyte import Analyte, Interpretation, InterpretationMember +from mzspeclib.validate.object_rule import ValidationWarning from .base import ( SpectralLibraryBackendBase, diff --git a/mzlib/backends/utils.py b/mzspeclib/backends/utils.py similarity index 100% rename from mzlib/backends/utils.py rename to mzspeclib/backends/utils.py diff --git a/mzlib/cluster.py b/mzspeclib/cluster.py similarity index 93% rename from mzlib/cluster.py rename to mzspeclib/cluster.py index 7bba278..b1cca78 100644 --- a/mzlib/cluster.py +++ b/mzspeclib/cluster.py @@ -4,7 +4,7 @@ from typing import Dict, List -from mzlib.attributes import AttributeManager, AttributeManagedProperty, AttributeGroupFacet +from mzspeclib.attributes import AttributeManager, AttributeManagedProperty, AttributeGroupFacet from .utils import ensure_iter, flatten SIMILAR_SPECTRUM_KEYS = "MS:1003263|similar spectrum keys" diff --git a/mzlib/defaults.py b/mzspeclib/defaults.py similarity index 100% rename from mzlib/defaults.py rename to mzspeclib/defaults.py diff --git a/mzlib/draw.py b/mzspeclib/draw.py similarity index 100% rename from mzlib/draw.py rename to mzspeclib/draw.py diff --git a/mzlib/index/__init__.py b/mzspeclib/index/__init__.py similarity index 100% rename from mzlib/index/__init__.py rename to mzspeclib/index/__init__.py diff --git a/mzlib/index/base.py b/mzspeclib/index/base.py similarity index 100% rename from mzlib/index/base.py rename to mzspeclib/index/base.py diff --git a/mzlib/index/memory.py b/mzspeclib/index/memory.py similarity index 99% rename from mzlib/index/memory.py rename to mzspeclib/index/memory.py index a9310f8..0802112 100644 --- a/mzlib/index/memory.py +++ b/mzspeclib/index/memory.py @@ -6,7 +6,7 @@ from numbers import Integral from collections import defaultdict -from mzlib.index.base import IndexRecordBase +from mzspeclib.index.base import IndexRecordBase from .base import IndexBase, IndexRecordBase, IndexInitializedRecord diff --git a/mzlib/index/sql.py b/mzspeclib/index/sql.py similarity index 99% rename from mzlib/index/sql.py rename to mzspeclib/index/sql.py index 9b9387d..b8c512d 100644 --- a/mzlib/index/sql.py +++ b/mzspeclib/index/sql.py @@ -7,7 +7,7 @@ from sqlalchemy import Column, ForeignKey, Integer, Float, String, DateTime, Text, LargeBinary -from mzlib.index.base import IndexRecordBase +from mzspeclib.index.base import IndexRecordBase try: # For SQLAlchemy 2.0 from sqlalchemy.orm import declarative_base except ImportError: diff --git a/mzlib/obo/psi-ms.obo b/mzspeclib/obo/psi-ms.obo similarity index 100% rename from mzlib/obo/psi-ms.obo rename to mzspeclib/obo/psi-ms.obo diff --git a/mzlib/ontology.py b/mzspeclib/ontology.py similarity index 100% rename from mzlib/ontology.py rename to mzspeclib/ontology.py diff --git a/mzlib/ontology_term.py b/mzspeclib/ontology_term.py similarity index 100% rename from mzlib/ontology_term.py rename to mzspeclib/ontology_term.py diff --git a/mzlib/peak_list.py b/mzspeclib/peak_list.py similarity index 100% rename from mzlib/peak_list.py rename to mzspeclib/peak_list.py diff --git a/mzlib/spectrum.py b/mzspeclib/spectrum.py similarity index 96% rename from mzlib/spectrum.py rename to mzspeclib/spectrum.py index c919a24..e3a2311 100644 --- a/mzlib/spectrum.py +++ b/mzspeclib/spectrum.py @@ -4,14 +4,14 @@ from typing import Any, Dict, List, Optional, TYPE_CHECKING -from mzlib.attributes import ( +from mzspeclib.attributes import ( AttributeManager, AttributeManagedProperty, AttributeListManagedProperty, AttributeProxy as _AttributeProxy, AttributeFacet ) -from mzlib.analyte import Analyte, InterpretationCollection, Interpretation +from mzspeclib.analyte import Analyte, InterpretationCollection, Interpretation if TYPE_CHECKING: - from mzlib.spectrum_library import SpectrumLibrary + from mzspeclib.spectrum_library import SpectrumLibrary #A class that holds data for each spectrum that is read from the SpectralLibrary class @@ -171,7 +171,7 @@ def write(self, format="text", **kwargs): # pragma: no cover #### If the format is text if format == "text": - from mzlib.backends.text import format_spectrum + from mzspeclib.backends.text import format_spectrum return format_spectrum(self, **kwargs) #### If the format is TSV @@ -239,7 +239,7 @@ def write(self, format="text", **kwargs): # pragma: no cover #### If the format is JSON elif format == "json": - from mzlib.backends.json import format_spectrum + from mzspeclib.backends.json import format_spectrum return format_spectrum(self, **kwargs) #### Otherwise we don't know this format diff --git a/mzlib/spectrum_library.py b/mzspeclib/spectrum_library.py similarity index 96% rename from mzlib/spectrum_library.py rename to mzspeclib/spectrum_library.py index 2bbbadb..0736dfe 100644 --- a/mzlib/spectrum_library.py +++ b/mzspeclib/spectrum_library.py @@ -3,14 +3,14 @@ import pathlib from typing import Optional, Type, List, Union -from mzlib.attributes import AttributeManagedProperty, AttributeManager -from mzlib.backends.base import LIBRARY_DESCRIPTION_TERM, LIBRARY_NAME_TERM, LIBRARY_URI_TERM, LIBRARY_VERSION_TERM -from mzlib.cluster import SpectrumCluster - -from mzlib.spectrum_library_index import SpectrumLibraryIndex -from mzlib.spectrum import Spectrum -from mzlib.index import IndexBase -from mzlib.backends import guess_implementation, SpectralLibraryBackendBase, SpectralLibraryWriterBase +from mzspeclib.attributes import AttributeManagedProperty, AttributeManager +from mzspeclib.backends.base import LIBRARY_DESCRIPTION_TERM, LIBRARY_NAME_TERM, LIBRARY_URI_TERM, LIBRARY_VERSION_TERM +from mzspeclib.cluster import SpectrumCluster + +from mzspeclib.spectrum_library_index import SpectrumLibraryIndex +from mzspeclib.spectrum import Spectrum +from mzspeclib.index import IndexBase +from mzspeclib.backends import guess_implementation, SpectralLibraryBackendBase, SpectralLibraryWriterBase debug = False diff --git a/mzlib/spectrum_library_collection.py b/mzspeclib/spectrum_library_collection.py similarity index 100% rename from mzlib/spectrum_library_collection.py rename to mzspeclib/spectrum_library_collection.py diff --git a/mzlib/spectrum_library_index.py b/mzspeclib/spectrum_library_index.py similarity index 100% rename from mzlib/spectrum_library_index.py rename to mzspeclib/spectrum_library_index.py diff --git a/mzlib/tools/__init__.py b/mzspeclib/tools/__init__.py similarity index 100% rename from mzlib/tools/__init__.py rename to mzspeclib/tools/__init__.py diff --git a/mzlib/tools/cli.py b/mzspeclib/tools/cli.py similarity index 95% rename from mzlib/tools/cli.py rename to mzspeclib/tools/cli.py index 7c3abcd..c726cd6 100644 --- a/mzlib/tools/cli.py +++ b/mzspeclib/tools/cli.py @@ -7,14 +7,14 @@ from typing import DefaultDict, List -from mzlib.spectrum_library import SpectrumLibrary -from mzlib.index import MemoryIndex, SQLIndex -from mzlib.backends.base import FormatInferenceFailure, SpectralLibraryBackendBase -from mzlib.validate import validator -from mzlib.validate.level import RequirementLevel -from mzlib.ontology import ControlledVocabularyResolver - -from mzlib.tools.utils import ColoringFormatter +from mzspeclib.spectrum_library import SpectrumLibrary +from mzspeclib.index import MemoryIndex, SQLIndex +from mzspeclib.backends.base import FormatInferenceFailure, SpectralLibraryBackendBase +from mzspeclib.validate import validator +from mzspeclib.validate.level import RequirementLevel +from mzspeclib.ontology import ControlledVocabularyResolver + +from mzspeclib.tools.utils import ColoringFormatter CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) diff --git a/mzlib/tools/utils.py b/mzspeclib/tools/utils.py similarity index 100% rename from mzlib/tools/utils.py rename to mzspeclib/tools/utils.py diff --git a/mzlib/universal_spectrum_identifier.py b/mzspeclib/universal_spectrum_identifier.py similarity index 100% rename from mzlib/universal_spectrum_identifier.py rename to mzspeclib/universal_spectrum_identifier.py diff --git a/mzlib/utils.py b/mzspeclib/utils.py similarity index 100% rename from mzlib/utils.py rename to mzspeclib/utils.py diff --git a/mzlib/validate/__init__.py b/mzspeclib/validate/__init__.py similarity index 100% rename from mzlib/validate/__init__.py rename to mzspeclib/validate/__init__.py diff --git a/mzlib/validate/level.py b/mzspeclib/validate/level.py similarity index 100% rename from mzlib/validate/level.py rename to mzspeclib/validate/level.py diff --git a/mzlib/validate/object_rule.py b/mzspeclib/validate/object_rule.py similarity index 93% rename from mzlib/validate/object_rule.py rename to mzspeclib/validate/object_rule.py index 680352c..b7a2712 100644 --- a/mzlib/validate/object_rule.py +++ b/mzspeclib/validate/object_rule.py @@ -3,12 +3,12 @@ from typing import TYPE_CHECKING, List, Tuple -from mzlib.attributes import Attributed +from mzspeclib.attributes import Attributed -from mzlib.spectrum import Spectrum -from mzlib.annotation import IonAnnotationBase, InvalidAnnotation +from mzspeclib.spectrum import Spectrum +from mzspeclib.annotation import IonAnnotationBase, InvalidAnnotation -from mzlib.validate.level import RequirementLevel +from mzspeclib.validate.level import RequirementLevel if TYPE_CHECKING: from .validator import ValidatorBase diff --git a/mzlib/validate/rules/__init__.py b/mzspeclib/validate/rules/__init__.py similarity index 100% rename from mzlib/validate/rules/__init__.py rename to mzspeclib/validate/rules/__init__.py diff --git a/mzlib/validate/rules/all.json b/mzspeclib/validate/rules/all.json similarity index 100% rename from mzlib/validate/rules/all.json rename to mzspeclib/validate/rules/all.json diff --git a/mzlib/validate/rules/base.json b/mzspeclib/validate/rules/base.json similarity index 100% rename from mzlib/validate/rules/base.json rename to mzspeclib/validate/rules/base.json diff --git a/mzlib/validate/rules/consensus.json b/mzspeclib/validate/rules/consensus.json similarity index 100% rename from mzlib/validate/rules/consensus.json rename to mzspeclib/validate/rules/consensus.json diff --git a/mzlib/validate/rules/convert_to_xlsx.py b/mzspeclib/validate/rules/convert_to_xlsx.py similarity index 100% rename from mzlib/validate/rules/convert_to_xlsx.py rename to mzspeclib/validate/rules/convert_to_xlsx.py diff --git a/mzlib/validate/rules/gold.json b/mzspeclib/validate/rules/gold.json similarity index 100% rename from mzlib/validate/rules/gold.json rename to mzspeclib/validate/rules/gold.json diff --git a/mzlib/validate/rules/peptide.json b/mzspeclib/validate/rules/peptide.json similarity index 100% rename from mzlib/validate/rules/peptide.json rename to mzspeclib/validate/rules/peptide.json diff --git a/mzlib/validate/rules/silver.json b/mzspeclib/validate/rules/silver.json similarity index 100% rename from mzlib/validate/rules/silver.json rename to mzspeclib/validate/rules/silver.json diff --git a/mzlib/validate/rules/single.json b/mzspeclib/validate/rules/single.json similarity index 100% rename from mzlib/validate/rules/single.json rename to mzspeclib/validate/rules/single.json diff --git a/mzspeclib/validate/rules/test.xlsx b/mzspeclib/validate/rules/test.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..718a9b309093533114b72216d46d66b0d0988fde GIT binary patch literal 18946 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