Skip to content

API Reference

Every name in spxtacular.__all__, grouped by area:

from spxtacular import (
    # Core data structures
    Spectrum, MsnSpectrum, Peak, Precursor, SpectrumType,
    # Enums and their permissive type aliases
    PeakSelection, PeakSelectionLike,
    ActivationType, ActivationTypeLike,
    IMType, IMTypeLike,
    Analyzer, AnalyzerLike,
    # Readers and peak-list writers
    Reader, DReader, MzmlReader, ThermoReader, CentroidConfig, AcquisitionType,
    MgfReader, Ms2Reader, MspReader, write_mgf, write_ms2, write_msp,
    # Spectral libraries (mzSpecLib)
    read_mzspeclib, MzSpecLibReader, write_mzspeclib, SpectralLibrary, LibraryEntry, Analyte, Interpretation, CvParam,
    write_indexed_mzml_gzip,
    # Matching and scoring
    match_fragments, score, cosine, modified_cosine, entropy_similarity,
    # Isobaric reporter ions (TMT, TMTpro, iTRAQ)
    ReporterIons, extract_reporter_ions, reporter_ion_table,
    isotope_correction_matrix, correct_isotope_impurities,
    # Isotope envelopes and average-composition models
    IsotopeModel, IsotopeModelType, IsotopeModelLike,
    ISOTOPE_MODELS,
    PEPTIDE_ISOTOPE_MODEL, GLYCAN_ISOTOPE_MODEL, LIPID_ISOTOPE_MODEL,
    DNA_ISOTOPE_MODEL, RNA_ISOTOPE_MODEL,
    brain_isotopic_distribution, resolve_isotope_model,
    # Ionization models and deconvolution provenance
    IonizationModel, IonizationModelLike, DeconvolutionProvenance,
    PROTONATED, DEPROTONATED, SODIATED, AMMONIATED,
    IONIZATION_MODELS, resolve_ionization_model,
    # Run-level extraction
    Chromatogram, extract_chromatogram, extract_xic,
    # Visualization
    plot_spectrum, mirror_plot, annotate_spectrum, mass_error_plot, facet_plot,
    sequence_coverage_plot, profile_centroid_plot,
    plot_chromatogram, plot_xic, save_figure,
    # Plot tables
    build_plot_table, build_annot_plot_table, plot_from_table, table_view,
    # Theme (a submodule, not a function)
    theme,
    # Utilities
    da_to_ppm, ppm_to_da,
    # Remote / serialised spectra
    fetch_usi, spectrum_from_proxi_response,
    to_inline_spectrum,
    to_spectrl_token, from_spectrl_token,
    to_spectrl_url, from_spectrl_url,
    # Ecosystem interoperability
    to_matchms, from_matchms,
    to_spectrum_utils, from_spectrum_utils,
)

MatchedFragment, estimate_noise_level, and the reader lookup classes (including PeakListLookup) are not exported from the package root. Import them from their defining modules as shown in the relevant sections below.


Ecosystem interoperability

The adapters import their optional dependency only when called. Install spxtacular[matchms], spxtacular[spectrum-utils], or spxtacular[interop] for both.

to_matchms / from_matchms

to_matchms(
    spectrum: Spectrum,
    *,
    extra_metadata: Mapping[str, object] | None = None,
    include_spxtacular_metadata: bool = True,
) -> matchms.Spectrum

from_matchms(
    spectrum: matchms.Spectrum,
    *,
    prefer_spxtacular_metadata: bool = True,
) -> Spectrum | MsnSpectrum

to_matchms stable-sorts peaks by m/z and populates matchms fields including id, precursor_mz, charge, retention_time, ionmode, scan_number, and collision_energy. extra_metadata adds values outside spxtacular's model, such as smiles or inchikey; spxtacular-derived values win on a key collision.

By default, spxtacular_metadata contains a namespaced JSON payload with all spxtacular metadata and the per-peak charge, im, and iso_score arrays. A matchms operation that removes peaks is supported: return conversion aligns surviving m/z values with those arrays. If an operation changes m/z values and alignment is no longer possible, the extension arrays are dropped with a warning rather than attached to the wrong peaks. Set include_spxtacular_metadata=False for a conventional, intentionally lossy matchms object; set prefer_spxtacular_metadata=False to ignore an existing payload while importing.

Import returns MsnSpectrum when the payload or conventional matchms metadata contains scan-level fields. Otherwise it returns Spectrum.

to_spectrum_utils / from_spectrum_utils

to_spectrum_utils(
    spectrum: MsnSpectrum,
    *,
    precursor_index: int = 0,
    identifier: str | None = None,
    warn_on_loss: bool = True,
) -> spectrum_utils.spectrum.MsmsSpectrum

from_spectrum_utils(
    spectrum: spectrum_utils.spectrum.MsmsSpectrum,
    *,
    warn_on_loss: bool = True,
) -> MsnSpectrum

The target model requires centroided peaks, one precursor m/z and charge, and an identifier. identifier falls back to native_id, then scan=<scan_number>; absent required information raises instead of being invented. Use precursor_index when an MsnSpectrum has multiple precursors.

This conversion is explicitly lossy. spectrum_utils has no fields for per-peak charge, ion mobility, isotope score, multiple precursors, or most acquisition metadata, and internally stores intensities as float32. Populated unsupported fields produce a UserWarning. ProForma annotations applied by spectrum_utils are likewise warned about and dropped by from_spectrum_utils, because MsnSpectrum has no persistent annotation field.


Core classes

Spectrum

Central data structure for a mass spectrum. Holds parallel numpy arrays for mz, intensity, and optionally charge, im (ion mobility), and iso_score. Transformation methods are chainable and return a new Spectrum unless inplace=True is requested.

Constructor:

Spectrum(
    mz: NDArray[np.float64],
    intensity: NDArray[np.float64],
    charge: NDArray[np.int32] | None = None,
    im: NDArray[np.float64] | None = None,
    iso_score: NDArray[np.float64] | None = None,
    spectrum_type: SpectrumType | str | None = None,
    denoised: str | None = None,
    normalized: str | None = None,
    deconvolution: DeconvolutionProvenance | None = None,
)

iso_score sits between im and spectrum_type — pass the trailing fields by keyword to avoid positional mix-ups.

Methods:

Method Returns Summary
.peaks list[Peak] All peaks as Peak objects
.is_decharged bool Property: True once every peak's charge == 0
len(spec) int Number of peaks (__len__)
.top_peaks(n, by, reverse) list[Peak] Top N peaks sorted by attribute
.has_peak(target_mz, ...) bool Check for a peak near target m/z
.get_peak(target_mz, ...) Peak \| None Single best-matching peak
.get_peaks(target_mz, ...) list[Peak] All peaks matching criteria
.filter(...) Spectrum Remove peaks outside bounds; top_n / top_n_per_window=(n, width) keep the most intense peaks globally or per fixed-width m/z window
.normalize(method) Spectrum Scale intensities (max / tic / median)
.scale_intensity(method, ...) Spectrum Non-linear scaling: "root", "log", "rank"
.denoise(method) Spectrum Remove peaks below noise threshold
.centroid(min_intensity=...) Spectrum Convert profile to centroid via Gaussian fit, optionally applying a noise or absolute intensity floor
.merge(mz_tolerance, mz_tolerance_unit, im_tolerance, im_tolerance_unit) Spectrum Merge nearby peaks by weighted average
.round_mz(decimals, combine) Spectrum Round m/z, then sum / max-reduce duplicates
.deconvolute(..., ionization_model=...) Spectrum Assign isotope clusters and charge magnitudes using a selected adduct/carrier model
.decharge(..., ionization_model=...) Spectrum Convert charged m/z to neutral masses, reusing recorded ionization provenance by default
.remove_precursor_peak(...) Spectrum Strip precursor + isotopes + charge states
.sort(by, reverse) Spectrum Reorder peaks by attribute
.copy() Spectrum Deep copy with all arrays duplicated
Spectrum.combine(spectra) Spectrum Classmethod: concatenate multiple spectra
.match_fragments(fragments, ...) list[MatchedFragment] Fragment-to-peak matching
.score(fragments, ...) dict[str, float] All PSM scores
.to_dict() dict Versioned JSON-compatible spectrum payload
Spectrum.from_dict(payload) Spectrum Reconstruct Spectrum or MsnSpectrum from a payload
.to_json(indent=...) str Encode the versioned spectrum payload as strict JSON
Spectrum.from_json(value) Spectrum Reconstruct from JSON text or UTF-8 bytes
.to_spectrl_token(...) str Encode as a spectrl.… URL-safe token (requires [spectrl] extra)
Spectrum.from_spectrl_token(t) Spectrum \| MsnSpectrum Decode a spectrl.… token (classmethod)
.to_spectrl_url(*, base, mode, ...) str Encode as a shareable URL or data: URI (requires [spectrl] extra)
Spectrum.from_spectrl_url(url) Spectrum \| MsnSpectrum Decode a token from a URL fragment, query, or data: URI (classmethod)
Spectrum.from_usi(usi, ...) Spectrum \| MsnSpectrum Fetch via PROXI from USI (classmethod)
.save(path) None Serialise to .npz
Spectrum.load(path) Spectrum Load from .npz (classmethod)
.update(**kwargs) Spectrum Return copy with specified fields replaced
.plot(title, color, show_scores, ...) go.Figure Stick or profile plot, selected from spectrum type (requires plotly)
.annotate(fragments, ...) go.Figure Plot with fragment annotations
.mass_error_plot(fragments, ...) go.Figure Bubble chart of fragment mass errors
.facet_plot(*, fragments, mirror_spectrum, ...) go.Figure Multi-panel facet plot
.plot_table(show_scores, *, color) pd.DataFrame Build an editable plot table (one row per peak)
.annot_plot_table(fragments, ...) pd.DataFrame Build an editable annotated plot table with fragment labels

Full documentation: Spectrum reference


MsnSpectrum

Extends Spectrum with instrument metadata fields. Returned by every reader.

Additional fields (all optional):

Field Type Description
scan_number int \| None Native scan or frame number
ms_level int \| None MS level (1, 2, …)
native_id str \| None Instrument-specific scan identifier
rt float \| None Retention time in seconds
injection_time float \| None Ion accumulation time in ms
total_ion_current float \| None Total ion current for the scan
mz_range tuple[float, float] \| None Acquisition m/z window
im_range tuple[float, float] \| None Ion mobility acquisition window
isolation_mz_range tuple[float, float] \| None MS2 precursor isolation window (m/z)
isolation_ook0_range tuple[float, float] \| None MS2 precursor isolation window (ion mobility)
im_type IMType \| str \| None Ion mobility unit (closed vocabulary)
polarity "positive" \| "negative" \| None Scan polarity (tacular.types.Polarity, lowercase only)
resolution float \| None Instrument resolution
analyzer Analyzer \| str \| None Mass analyser type (open vocabulary)
ramp_time float \| None timsTOF ramp time in ms
collision_energy float \| None Fragmentation collision energy
activation_type ActivationType \| str \| None Fragmentation type (open vocabulary)
precursors list[Precursor] \| None Precursor ions (MS2 only)

See Metadata enums below for the ActivationType, IMType, and Analyzer member lists.

Full documentation: Spectrum reference — MsnSpectrum


Metadata enums

Three StrEnums are exported from spxtacular root and back the MsnSpectrum fields above:

from spxtacular import ActivationType, IMType, Analyzer
Enum Vocabulary Members
ActivationType Open CID, HCD, ETD, ECD, ETHCD ("EThcD"), ETCID ("ETciD"), NETD, UVPD, PD, PQD, SID, IRMPD, BIRD, SORI, PASEF
IMType Closed OOK0 ("ook0"), IM ("im"), DRIFT_TIME_MS ("drift_time_ms"), CCS ("ccs")
Analyzer Open ORBITRAP, FT_ICR, TOF, QUADRUPOLE, ION_TRAP, LINEAR_ION_TRAP, QUADRUPOLE_ION_TRAP, MAGNETIC_SECTOR, ELECTROSTATIC_ENERGY_ANALYZER

IMType is closed: a string must name a member (case-insensitive; also accepts "1/k0" and "drift_time"), anything else raises SpxtacularError. Polarity is not an enum: it is the plain string "positive" or "negative" (tacular.types.Polarity), lowercase only; anything else raises SpxtacularError. ActivationType and Analyzer are open: a member name in any case or a PSI-MS accession ("MS:1002481" from DReader) becomes the member, and other vendor strings are kept as plain strings.

from spxtacular import MsnSpectrum, ActivationType

spec = MsnSpectrum(mz=mz, intensity=intensity, activation_type=ActivationType.HCD)

Type aliases

Every enum ships a …Like alias — the permissive union that the library's own parameters and fields are annotated with. Use them when you type your own wrappers so callers can pass either an enum member or a plain string.

from tacular.types import Polarity, ToleranceUnit
from spxtacular import PeakSelectionLike, ActivationTypeLike, IMTypeLike, AnalyzerLike
Alias Definition
ToleranceUnit (tacular) Literal["da", "ppm"]
PeakSelectionLike PeakSelection \| Literal["closest", "largest", "all"]
Polarity (tacular) Literal["positive", "negative"]
ActivationTypeLike ActivationType \| str
IMTypeLike IMType \| str
AnalyzerLike Analyzer \| str

ToleranceUnit, PeakSelectionLike and Polarity are closed unions (only the listed literals type-check); the last three are open — any str is accepted so raw PSI-MS accessions and vendor shorthands pass through.

PeakSelection (CLOSEST, LARGEST, ALL) is the processing-side enum behind the peak_selection parameter. The tolerance_unit parameter used throughout matching, scoring, and plotting takes the plain strings "da" or "ppm" (lowercase only; "Da" raises).


Peak

Frozen dataclass for a single spectral peak. Returned by peak access methods.

Peak(mz: float, intensity: float, charge: int | None = None, im: float | None = None, iso_score: float | None = None)

Precursor

Frozen, slotted, keyword-only dataclass for a selected precursor ion (no longer a Peak subclass). Exported from the package root: from spxtacular import Precursor.

Precursor(
    *,
    precursor_mz: float,
    intensity: float = 0.0,
    charge: int | None = None,
    im: float | None = None,
    im_type: IMTypeLike | None = None,   # unit of `im`: "ook0", "drift_time_ms", ...
    iso_score: float | None = None,
    is_monoisotopic: bool | None = None,
)

im holds whatever mobility value the source recorded; im_type says which kind it is (Bruker and most mzML files give 1/K0, some mzML files give drift time).


SpectrumType

Exported from the package root:

from spxtacular import SpectrumType

StrEnum with three members:

Member Value Meaning
CENTROID "centroid" Peak-picked data
PROFILE "profile" Raw continuous data
DECONVOLUTED "deconvoluted" Isotope clusters assigned

Readers

.ms1 and .ms2 are lookup objects, not generators: iterable and indexable, but not iterators. next(reader.ms2) raises TypeError — use next(iter(reader.ms2)).

Reader

Format-agnostic entry point. Detects .d (Bruker timsTOF), .mzML, .raw (Thermo), .mgf, .ms2, or .msp from the path suffix — a trailing .gz is stripped first — and delegates to DReader / MzmlReader / ThermoReader / MgfReader / Ms2Reader / MspReader; any other suffix raises ValueError.

Reader(
    path: str | Path,
    *,
    centroid_config: CentroidConfig | None = None,
    mzml_gzip_mode: Literal["auto", "indexed", "stream"] = "auto",
    mzml_in_memory: bool = False,
)
Property / Method Type Description
.ms1 DReaderMs1Lookup \| MzmlSpectraLookup \| ThermoScanLookup \| PeakListLookup MS1 spectra. Iterate or index. Empty for .mgf / .ms2 / .msp
.ms2 DReaderMs2Lookup \| MzmlSpectraLookup \| ThermoScanLookup \| PeakListLookup MS2 spectra — iterate or index
.access_strategy str \| None Concrete mzML access route, or None for other formats
.get_by_scan(n, *, ms_level=None) MsnSpectrum Spectrum by scan number
.get_by_native_id(id) MsnSpectrum Spectrum by native id
.get_by_sage_scannr(scannr, *, precursor_offset=None) MsnSpectrum Spectrum for a Sage scannr; precursor_offset is .d only
.open() / .close() None Open / close the delegate; also driven by with

The three lookups exist on every format reader too. Missing keys raise KeyError; keys that cannot name one spectrum (no scan numbers in the file, duplicates, ambiguous Bruker numbers) raise SpxtacularError. The rules per format: Readers — Lookup by scan number or native id.

from spxtacular import Reader

with Reader("run.mzML") as r:   # or Reader("/data/sample.d")
    for spec in r.ms1:
        ...

Full documentation: Readers — Reader


MzmlReader

Reads .mzML files. A context manager is optional but recommended — it keeps one file handle open instead of reopening the file per operation.

MzmlReader(
    mzml_path: str | Path,
    *,
    gzip_mode: Literal["auto", "indexed", "stream"] = "auto",
    in_memory: bool = False,
)

The default "auto" mode uses a self-indexed gzip file, else rapidgzip in place, else an in-memory decompression; it never writes next to the input. For intentional sequential reads, use gzip_mode="stream".

Property / Method Type Description
.ms1 MzmlSpectraLookup MS1 spectra — iterate, or index by overall index / native ID
.ms2 MzmlSpectraLookup MS2 spectra — iterate, or index by overall index / native ID
reader[key] MsnSpectrum Spectrum by 0-based index (reader[0]) or native ID (reader["scan=19"])
.get_by_scan(n, *, ms_level=None) / .get_by_native_id(id) / .get_by_sage_scannr(s) MsnSpectrum Scan number from scan= / Thermo / index= / spectrum= ids; SpxtacularError if the file's ids carry none
.access_strategy str \| None Concrete route selected by mzMLPy
.open() / .close() None Open / close the persistent mzmlpy handle

Index access is not MS-level filtered — reader.ms2[0] is the first spectrum in the file, not the first MS2 spectrum.

Full documentation: Readers — MzmlReader


DReader

Reads Bruker timsTOF .d directories. Must be opened before use — via open()/close() or, preferably, as a context manager.

DReader(analysis_dir: str | Path, *, centroid_config: CentroidConfig | None = None)
Property / Attribute Type Description
.ms1 DReaderMs1Lookup All MS1 frames — iterate, or index by tdfpy frame_id
.ms2 DReaderMs2Lookup All MS2 spectra — iterate, or index by tdfpy precursor_id (DDA only; DIA/PRM raise NotImplementedError)
.get_by_scan(n, *, ms_level=None) MsnSpectrum MS1 frame_id, DDA precursor_id, DIA/PRM frame_id; pass ms_level for DDA
.get_by_native_id(id) MsnSpectrum frame=F, precursor=P, F@wI (DIA), F@tT (PRM)
.get_by_sage_scannr(s, *, precursor_offset=1) MsnSpectrum DDA only; precursor id = scannr + precursor_offset (0 for Sage on timsrust >= 0.6)
.acquisition_type AcquisitionType DDA / DIA / PRM / UNKNOWN
.open() / .close() None Open / close the underlying tdfpy reader

Full documentation: Readers — DReader


ThermoReader

Reads Thermo .raw files via fisher-py (Thermo's RawFileReader .NET assemblies — a .NET runtime must be installed on the machine). Must be opened before use — via open()/close() or, preferably, as a context manager.

ThermoReader(raw_path: str | Path, prefer_vendor_centroid: bool = True)
Property / Method Type Description
.ms1 ThermoScanLookup MS1 spectra — iterate, or index by native 1-based scan number
.ms2 ThermoScanLookup MS2 spectra — iterate, or index by native 1-based scan number
reader[scan] MsnSpectrum Spectrum of any MS level by native scan number
.get_by_scan(n, *, ms_level=None) / .get_by_native_id(id) / .get_by_sage_scannr(s) MsnSpectrum Native id controllerType=0 controllerNumber=1 scan=N or scan=N
.open() / .close() None Open / release the RawFileReader handle

With prefer_vendor_centroid=True (default), profile-mode FTMS scans yield Thermo's own centroid stream (CENTROID, with per-peak charge annotations — unknown charge arrives as -1); with False, they yield the full PROFILE trace.

Full documentation: Readers — ThermoReader


MgfReader / Ms2Reader / MspReader

Read the MGF, MS2, and MSP (NIST spectral-library) peak-list formats. Pure standard library — no optional extra, always available. All three formats hold fragmentation spectra only: every spectrum comes back with ms_level=2 and spectrum_type=SpectrumType.CENTROID. Gzip is detected by magic bytes, so .mgf.gz / .ms2.gz / .msp.gz just work. MspReader handles both the NIST/SpectraST peptide-library dialect and metabolomics exports (MoNA, GNPS, MS-DIAL) with case-insensitive header matching.

MgfReader(path: str | Path)
Ms2Reader(path: str | Path)
MspReader(path: str | Path)
Property / Method Type Description
iter(reader) Iterator[MsnSpectrum] Every spectrum, in file order
len(reader) int Spectra in the file — one counting pass, then cached
reader[key] MsnSpectrum Spectrum by 0-based position (reader[0]) or native_id (reader["scan=19"]); O(n)
.get_by_scan(n, *, ms_level=None) / .get_by_native_id(id) / .get_by_sage_scannr(s) MsnSpectrum Indexed: the first call parses the file once, later calls seek. MSP has no scan numbers
.ms1 PeakListLookup Always empty — peak lists carry no survey scans
.ms2 PeakListLookup Every spectrum — iterate or index
.open() / .close() None open() checks the file exists; close() is a no-op (each walk streams its own handle)

Malformed input raises ValueError naming the file and line number. Unknown headers, multi-charge values, comment lines, and peak-less blocks are tolerated.

Full documentation: Readers — MGF / MS2 / MSP


write_mgf / write_ms2 / write_msp

Write spectra to a peak-list file, returning the path. A .gz suffix gzips the output.

write_mgf(spectra: Iterable[Spectrum] | Spectrum, path: str | Path, *, annotations=None) -> Path
write_ms2(spectra: Iterable[Spectrum] | Spectrum, path: str | Path, *, ionization_model=None) -> Path
write_msp(spectra: Iterable[Spectrum] | Spectrum, path: str | Path, *, annotations=None) -> Path
Behaviour Detail
Peak values mz / intensity at repr precision — a write → read round trip is exact
SpectrumType.PROFILE Raises ValueError; peak lists are centroid data
Polarity MGF/MS2: carried by the sign of the written charge (CHARGE=2-, Z -2). MSP: explicit Ion_mode: P/N line
MS2 Z mass Singly protonated [M+H]+ mass in every mode: the neutral mass from the precursor m/z, charge magnitude and ionization_model (default: deconvolution provenance, then [M-H]- for a negative charge, else [M+H]+), plus one proton
Missing metadata Omitted, except MS2's mandatory S fields (scan number → 1-based position, precursor m/z → 0.0)
annotations= (MGF, MSP) Optional mzPAF, one entry per spectrum: one item per peak (None, string, PafAnnotation, or a list) or a list of MatchedFragment. Adds a quoted last column, mz intensity "b2/0.1ppm,y3^2"

Full documentation: Readers — Writing


read_mzspeclib / MzSpecLibReader / write_mzspeclib

Read and write HUPO-PSI mzSpecLib 1.0 spectral libraries, text or JSON, optionally gzipped.

read_mzspeclib(path: str | Path) -> SpectralLibrary
MzSpecLibReader(path: str | Path)   # context manager; iterate for LibraryEntry, one at a time
    .open() / .close(); .attributes, .format ("text" | "json"), .clusters (after a full pass)
write_mzspeclib(entries: SpectralLibrary | Iterable[LibraryEntry] | LibraryEntry, path: str | Path,
                *, format: Literal["text", "json"] | None = None) -> Path
LibraryEntry(spectrum: MsnSpectrum, *, key=None, name=None, analytes=(), interpretations=(),
             peak_annotations=None, peak_attributes=None, attributes=())
LibraryEntry.from_spectrum(spectrum, peptidoform=None, *, charge=None, score=None, key=None, name=None,
                           peak_annotations=None, attributes=()) -> LibraryEntry
Analyte(*, id=1, peptidoform: ProFormaAnnotation | str | None = None, charge=None, attributes=())
Interpretation(*, id=1, members=(), score=None, attributes=(), member_attributes={})
CvParam(accession, name, value=None, value_accession=None, group=None)
Behaviour Detail
Format Read: detected from content and gzip magic. Write: JSON for *.json[.gz], else text
Streaming MzSpecLibReader yields entries one at a time, in memory that does not grow with the number of spectra (text and JSON, gzipped or not), with attributes available before iterating; same entries and errors as read_mzspeclib
Spectrum fields Precursor m/z and charge, RT, ion mobility, CE, dissociation, polarity, MS level, scan number, native id, TIC, injection time map to MsnSpectrum; other terms stay in attributes
Analytes peptidoform is a peptacular ProFormaAnnotation carrying the charge
Peaks peak_annotations: tuple of paftacular PafAnnotation per peak, or None. JSON writes one mzPAF string per peak, "?" when unannotated
Invalid input SpxtacularError (bad structure, version other than 1.x, bad mzPAF or ProForma)

Full documentation: Spectral libraries


write_indexed_mzml_gzip

Compress an mzML file into mzMLPy's self-indexed gzip format, which MzmlReader can open with random access. Requires the [mzml] extra.

write_indexed_mzml_gzip(
    source: str | Path,
    output: str | Path,
    *,
    compression_level: int = 6,
)

Returns whatever mzMLPy's write_indexed_gzip returns. Full documentation: Readers — MzmlReader


CentroidConfig

Dataclass of parameters forwarded to tdfpy's frame.centroid(). Only used by DReader (and by Reader for .d inputs); ignored by MzmlReader.

from spxtacular import CentroidConfig

CentroidConfig(
    *,
    mz_tolerance: float = 8.0,
    mz_tolerance_unit: Literal["ppm", "da"] = "ppm",
    im_tolerance: float = 0.1,
    im_tolerance_unit: Literal["relative", "absolute"] = "relative",
    min_peaks: int = 3,
    noise_filter: Literal["mad", "percentile", "histogram", "baseline", "iterative_median"]
                  | float | None = None,
)

Full documentation: Readers — CentroidConfig


AcquisitionType

Exported from the package root:

from spxtacular import AcquisitionType

StrEnum with four members: DDA, DIA, PRM, UNKNOWN.


Noise estimation

estimate_noise_level is not exported from the package root but is the function backing Spectrum.denoise().

from spxtacular.noise import estimate_noise_level

threshold = estimate_noise_level(intensity_array, method="mad")
method Strategy
"mad" median + 3 × 1.4826 × MAD
"percentile" 5th percentile
"histogram" Histogram mode + 3 σ
"baseline" Bottom-quartile mean + 3 σ
"iterative_median" Three-pass iterative median refinement
float or int Used directly as the absolute threshold

Isotope and ionization models

The deconvolution guide explains how these models affect cluster finding and neutral-mass conversion. All names below are exported from spxtacular.

Isotope models

IsotopeModel(
    atoms_per_da: Mapping[str, float],
    *,
    fixed_composition: Mapping[str, int] = ...,
    isotope_abundances: Mapping[str, Sequence[tuple[int, float]]] | None = None,
    name: str = "custom",
)

brain_isotopic_distribution(
    composition: Mapping[str, int],
    max_isotopes: int | None = None,  # None: 32 peaks, longer when the mass needs it
    isotope_abundances=None,
) -> NDArray[np.float64]

resolve_isotope_model(model: IsotopeModel | IsotopeModelType | str = "peptide") -> IsotopeModel

IsotopeModelType has PEPTIDE, GLYCAN, LIPID, DNA, and RNA members. The corresponding objects are PEPTIDE_ISOTOPE_MODEL, GLYCAN_ISOTOPE_MODEL, LIPID_ISOTOPE_MODEL, DNA_ISOTOPE_MODEL, and RNA_ISOTOPE_MODEL. ISOTOPE_MODELS maps their lowercase names to the objects. Custom models store expected atoms per Dalton plus any fixed terminal composition. When omitted, fixed_composition is an empty mapping.

Ionization models

IonizationModel(
    name: str,
    polarity: Polarity,  # "positive" or "negative"
    carrier_mass: float,
    *,
    carrier: str = "custom",
)

resolve_ionization_model(
    model: IonizationModel | str | float = PROTONATED,
) -> IonizationModel

The presets are PROTONATED ([M+H]+), DEPROTONATED ([M-H]-), SODIATED ([M+Na]+), and AMMONIATED ([M+NH4]+). IONIZATION_MODELS maps accepted names and aliases to these objects. Each model provides ion_mz(), neutral_mass(), notation(), to_dict(), and from_dict(). Charge arrays keep positive magnitudes. Polarity and carrier mass live in the model.

DeconvolutionProvenance records the resolved isotope and ionization models plus every deconvolution parameter that affects matching. It is attached to deconvoluted spectra and is preserved by native persistence, matchms, and spectrl round trips. decharge() and automatic precursor removal reuse it by default. See Deconvolution for the full parameter set and examples.


Token serialisation (spectrl)

The single supported wire format for sharing a spectrum as a string is the spectrl token. Encodes a full spectrum (peaks, metadata, precursors) into a compact URL-safe token that mirrors mzML semantics, with PSI-MS CV params in a single compressed CBOR document and an integrity checksum.

Requires the optional [spectrl] extra.

from spxtacular import to_spectrl_token, from_spectrl_token, to_inline_spectrum

token = spec.to_spectrl_token()                      # lossy (bounded-error), default
token_exact = spec.to_spectrl_token(lossless=True)   # bit-exact arrays
restored = Spectrum.from_spectrl_token(token)
inline = to_inline_spectrum(spec)                    # → spectrl.InlineSpectrum

The round-trip is faithful — every spxtacular field is carried.

Via mzML-native CV params: mz, intensity, charge (including singletons), im under its exact PSI-MS array accession, im_type, spectrum type, and — for MsnSpectrum — native_id, ms_level, polarity, rt, mz_range, total_ion_current, precursors, isolation_mz_range, collision_energy, activation_type.

iso_score rides in spectrl's extra_arrays slot under key "iso_score" (encoded as a non-standard mzML binary array MS:1000786).

spxtacular scalar fields without an mzML CV counterpart — denoised/normalized provenance strings, scan_number, resolution, analyzer, ramp_time, im_range, isolation_ook0_range — are carried losslessly as namespaced user_params (spxtacular: prefix).

URL sharing

to_spectrl_url / from_spectrl_url bind a token into a shareable link (or decode one back). Also available as Spectrum.to_spectrl_url / Spectrum.from_spectrl_url.

from spxtacular import to_spectrl_url, from_spectrl_url

url  = to_spectrl_url(spec, base="https://example.com/view")            # fragment (default)
url  = to_spectrl_url(spec, base="https://example.com/view", mode="query", param="d")
uri  = to_spectrl_url(spec, mode="data")                          # data: URI, no base
spec = from_spectrl_url(url)                                       # extract + decode

mode selects the binding:

mode Result base
"fragment" (default) base#spectrl.… — token in the URL fragment (never sent to the server) required
"query" base?<param>=spectrl.… — token as a query parameter required
"data" data:application/vnd.spectrl;v=…,… URI ignored

lossless and max_len are forwarded to the token encoder.


USI loading

Fetch spectra from public proteomics repositories by Universal Spectrum Identifier via the PROXI protocol.

from spxtacular import fetch_usi, spectrum_from_proxi_response
# or via Spectrum.from_usi(...) for the same result

spec = fetch_usi(
    "mzspec:PXD000561:Adult_Frontalcortex_bRP_Elite_85_f09:scan:17555",
    backend="aggregator",  # or "pride", "massive", "peptideatlas", "jpost", or a full URL
    timeout=30,
)

# For clients that perform the HTTP request themselves:
spec = spectrum_from_proxi_response(decoded_proxi_json, usi)

The parser preserves PROXI centroid/profile representation and scan polarity metadata. It returns an MsnSpectrum when scan-level metadata or precursor information is available, otherwise a plain Spectrum.


JSON transport

Spectrum, MsnSpectrum, and Chromatogram provide versioned dictionary and JSON round-trips for APIs, browser visualization, and cross-process messages. The methods use only standard JSON values and require no optional dependency.

from spxtacular import Chromatogram, Spectrum, get_json_schema

spectrum_payload = spec.to_dict()
spectrum_json = spec.to_json()
restored_spec = Spectrum.from_json(spectrum_json)

chromatogram_payload = tic.to_dict()
chromatogram_json = tic.to_json()
restored_tic = Chromatogram.from_dict(chromatogram_payload)

spectrum_schema = get_json_schema("spectrum")
chromatogram_schema = get_json_schema("chromatogram")

The spectrum envelope uses schema name spxtacular.spectrum, schema version 2 (version 1 still loads), and kind spectrum or msn_spectrum. Its deconvolution provenance block has its own schema_version 3 (versions 1 and 2, with the old tolerance_type keys, still load). The chromatogram envelope uses schema name spxtacular.chromatogram, schema version 2 (version 1, with the old tolerance_type key, still loads), and kind chromatogram. Unknown schema versions are rejected so consumers never silently misinterpret a newer contract.

Peak columns are parallel arrays rather than one object per peak. Optional arrays are explicit null values. Spectrum.from_dict() preserves the concrete class, all MSn metadata, multiple precursors, ion mobility, charge, isotope score, and processing provenance.

The JSON Schema documents are packaged as:

spxtacular/schemas/spectrum-v2.schema.json
spxtacular/schemas/chromatogram-v2.schema.json

For large profile spectra, filter or decimate before transport when the browser does not need every sample. Use .npz persistence when compact local storage is more important than a language-neutral API representation.


Persistence (.npz)

Serialise spectra to / from numpy .npz archives. Arrays are stored natively; scalar metadata is JSON-encoded under the meta key. The .npz extension is appended automatically when missing.

spec.save("scan_001.npz")
restored = Spectrum.load("scan_001.npz")

msn.save("scan_001.npz")
restored_msn = MsnSpectrum.load("scan_001.npz")

MsnSpectrum.save / MsnSpectrum.load preserve all MSn metadata (scan number, RT, precursors, isolation window, …) in addition to the peak arrays.


Visualization

Every plot function takes backend="plotly" (default, returns a plotly.graph_objects.Figure), "matplotlib" (returns a matplotlib.figure.Figure; pip install 'spxtacular[matplotlib]') or "spec" (returns a FigureSpec), plus style="paper" | "screen" | "talk" | FigureStyle and size="single" | "onehalf" | "double" | mm | (width_mm, height_mm). style=None means "screen" for plotly and "paper" otherwise. An unknown backend or style raises SpxtacularError. **layout_kwargs is plotly only.

Full documentation: Visualization

Conventions shared by every figure

These defaults apply to all the plotting functions below; they are described once here rather than repeated in each parameter table.

Behaviour Detail
Relative intensity by default Table-driven figures (plot_spectrum, annotate_spectrum, plot_from_table) scale the y-axis so the base peak is 100% and title the axis Relative intensity (%). Pass intensity_scale="absolute" for raw counts. Tooltips always report the true intensity, whatever the scaling.
Optional intensity transform intensity_transform="sqrt" or "log" compresses a range spanning orders of magnitude; the axis title is prefixed accordingly (√ relative intensity (%), log₁₀ …).
Labels are typeset, capped and never overlap Ion labels are rich text (y₇²⁺, b₅−H₂O) read horizontally (label_angle, default 0). The layout places them in two dimensions, strongest peak first: a label that would collide moves up, with a leader line back to its peak, and one with no free space is dropped. max_labels (default 60) caps the count; max_labels=None removes the cap but not the collision pass. Dropped values stay in the hover text, in the plot table, and in table_view().
Hovering does not require precision Table-driven figures carry a transparent hit layer of 22px markers on the peak tips — the sticks themselves are hoverinfo="skip" — so being near a peak is enough rather than landing on a 1.6px hairline. Every figure additionally gets the m/z crosshair from the theme template (showspikes, spikemode="across", snapped to the cursor), with hoverdistance=24.
Autosize "screen" plotly figures fill their container rather than a fixed pixel box; "paper" and "talk" figures have the fixed size from size=.
Theme Every function takes theme_mode="light" \| "dark"; None (default) uses the module default from theme.set_plot_theme(). See Theme.

Chromatograms and XICs

Chromatogram(
    rt: NDArray[np.float64],
    intensity: NDArray[np.float64],
    *,
    label: str = "",
    mz: float | None = None,
    tolerance: float | None = None,
    tolerance_unit: str | None = None,
    meta: dict = ...,
)

extract_chromatogram(
    spectra: Iterable[Spectrum],
    mode: Literal["tic", "bpc"] = "tic",
    mz_range: tuple[float, float] | None = None,
) -> Chromatogram

extract_xic(
    spectra: Iterable[Spectrum],
    targets: Sequence[float] | float,
    tolerance: float = 20.0,
    tolerance_unit: Literal["ppm", "da"] = "ppm",
    im_window: tuple[float, float] | None = None,
    aggregate: Literal["sum", "max"] = "sum",
) -> list[Chromatogram]

extract_xic() handles every target in one pass. Chromatogram.apex_rt reports the most intense time point and Chromatogram.total reports summed intensity. The omitted meta default is a new empty dictionary for each object.

Extracted traces record meta["rt_unit"] as "s" when every scan has a retention time, or "scan_index" when none do. Mixed missing and present times, or nonfinite times, raise ValueError. Plots label scan indices explicitly and reject overlaid traces with different axis units. total sums samples without integration over time.

The plotting wrappers are:

plot_chromatogram(
    chromatograms: Chromatogram | Sequence[Chromatogram] | Iterable[Spectrum],
    title: str | None = None,
    theme_mode: Literal["light", "dark"] | None = None,
    show_apex: bool = True,
    fill: bool | None = None,
    **layout_kwargs,
) -> go.Figure

plot_xic(
    spectra: Iterable[Spectrum],
    targets: Sequence[float] | float,
    tolerance: float = 20.0,
    tolerance_unit: Literal["ppm", "da"] = "ppm",
    im_window: tuple[float, float] | None = None,
    aggregate: Literal["sum", "max"] = "sum",
    title: str | None = None,
    theme_mode: Literal["light", "dark"] | None = None,
    **layout_kwargs,
) -> go.Figure

Full documentation: Visualization, chromatograms and XICs

profile_centroid_plot

profile_centroid_plot(
    profile: Spectrum,
    centroids: Spectrum | None = None,
    title: str | None = None,
    theme_mode: Literal["light", "dark"] | None = None,
    max_points: int | None = 4000,
    **layout_kwargs,
) -> go.Figure

Overlays centroid sticks on the profile trace. When centroids is omitted, the function calls profile.centroid(). Profile samples above max_points are reduced with min/max buckets.

plot_spectrum

from spxtacular import plot_spectrum
plot_spectrum(
    spectrum: Spectrum,
    title: str | None = None,
    *,
    color: Literal["charge", "im"] | None = "charge",
    show_scores: bool = True,
    max_labels: int | None = 60,
    theme_mode: Literal["light", "dark"] | None = None,
    intensity_scale: Literal["absolute", "relative"] = "relative",
    intensity_transform: Literal["sqrt", "log"] | None = None,
    show_precursor: bool = True,
    render: Literal["sticks", "profile"] | None = None,
    max_points: int | None = 4000,
    absolute_axis: bool = False,
    backend: Literal["plotly", "matplotlib", "spec"] = "plotly",
    style: str | FigureStyle | None = None,
    size: SizeLike = None,
    **layout_kwargs,
)

Everything after spectrum is keyword-only.

Parameter Default Description
color "charge" "charge" colours sticks by charge state on the ordinal ramp; "im" colours by ion mobility on the single-hue sequential scale with a colourbar (falls back to "charge" when no IM array is present); None renders every stick in one colour
show_scores True Label peaks whose iso_score > 0 with their score
max_labels 60 Cap on directly drawn labels, strongest first; None for no count cap
theme_mode None "light" / "dark"; None uses the global default
intensity_scale "relative" "relative" (base peak = 100%) or "absolute"
intensity_transform None None, "sqrt", or "log"
show_precursor True On an MsnSpectrum carrying precursors, draw the precursor m/z hairline and the isolation window as recessive chrome behind the peaks
render None Choose from spectrum_type, or explicitly force "sticks" or "profile"
max_points 4000 Profile-sample cap after min/max decimation. None draws every sample
absolute_axis False Absolute intensities with a shared ×10ⁿ exponent in the axis title

color="im" takes a separate rendering path that bins ion mobility into 20 steps of the sequential scale; intensity_scale, intensity_transform, and show_precursor apply there as on the other colour modes. Profile spectra cannot take the IM path — centroid first, or pass render="sticks" explicitly.

annotate_spectrum

from spxtacular import annotate_spectrum
annotate_spectrum(
    spectrum: Spectrum,
    fragments,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    title: str | None = None,
    peak_selection: Literal["closest", "largest", "all"] = "closest",
    include_sequence: bool = False,
    max_labels: int | None = 60,
    theme_mode: Literal["light", "dark"] | None = None,
    intensity_scale: Literal["absolute", "relative"] = "relative",
    intensity_transform: Literal["sqrt", "log"] | None = None,
    texture: bool = False,
    show_precursor: bool = True,
    peptide: str | ProFormaAnnotation | None = None,
    mass_error_panel: bool = False,
    absolute_axis: bool = False,
    backend: Literal["plotly", "matplotlib", "spec"] = "plotly",
    style: str | FigureStyle | None = None,
    size: SizeLike = None,
    **layout_kwargs,
)
Parameter Default Description
tolerance / tolerance_unit 0.02 / "da" Matching tolerance
peak_selection "closest" "closest", "largest", or "all"
include_sequence False Embed the residue sequence in each label (b3{PEP} instead of b3)
max_labels 60 Cap on directly drawn ion labels
theme_mode None "light" / "dark"
intensity_scale / intensity_transform "relative" / None y-axis scaling, as above
texture False Also encode ion series as a dash pattern (the non-colour channel) — for print, forced-colours modes, and readers who cannot separate two hues. Off by default because at stick density dashes add noise
show_precursor True Draw precursor m/z + isolation window when present
peptide None Draw the sequence above the spectrum with a tick for each observed b/y cleavage
mass_error_panel False Add a mass-error strip below, sharing the m/z axis

Matched peaks are coloured by ion series and labelled with a typeset form of their mzPAF identifier; unmatched peaks are drawn in grey, thinner and dimmer.

mirror_plot

from spxtacular import mirror_plot
mirror_plot(
    raw: Spectrum,
    deconvoluted: Spectrum,
    *,
    fragments: FragmentInput | None = None,
    lower_fragments: FragmentInput | None = None,
    mirror_labels: Literal["auto", "both", "top"] = "auto",
    names: tuple[str, str] | None = None,
    similarity: Literal["cosine", "modified_cosine", "entropy"] | float | None = None,
    title: str | None = None,
    normalize: bool = True,
    show_charges: bool = True,
    show_scores: bool = True,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    peak_selection: Literal["closest", "largest", "all"] = "closest",
    max_labels: int | None = 60,
    theme_mode: Literal["light", "dark"] | None = None,
    backend: Literal["plotly", "matplotlib", "spec"] = "plotly",
    style: str | FigureStyle | None = None,
    size: SizeLike = None,
    **layout_kwargs,
)

With fragments=, both halves are matched and coloured by ion series (a query/library comparison). lower_fragments= annotates the lower half with other fragments (another peptide, a modified form). mirror_labels="auto" (default) labels an ion shared by both halves once, on the upper half, and repeats only the labels that differ; "both" labels both halves in full, "top" the upper half only. names= labels the two halves. similarity= prints a score in the corner: a method name computes it ("modified_cosine" needs a precursor m/z on both spectra, else SpxtacularError), a number is printed as given.

The second parameter is named deconvoluted. show_charges colours the deconvoluted (upper) half by charge state; show_scores annotates its peaks with their isotope profile score, capped by max_labels. The raw half is drawn in the unmatched grey.

mirror_plot does not take intensity_scale, intensity_transform, or texture — its y-axis scaling is controlled by normalize, which scales each half independently to its own maximum so the two fill their halves symmetrically. Either way the hover reports the pre-normalisation intensity.

mass_error_plot

from spxtacular import mass_error_plot
mass_error_plot(
    spectrum: Spectrum,
    fragments,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    peak_selection: Literal["closest", "largest", "all"] = "closest",
    unit: str = "ppm",              # "ppm" or "da"
    title: str | None = None,
    max_labels: int | None = 60,
    theme_mode: Literal["light", "dark"] | None = None,
    **layout_kwargs,
)

Bubbles are coloured by ion series from the categorical palette and labelled with their mzPAF identifier, so a 2+ and a 1+ of the same ion do not both render as b3. max_labels caps direct labels by peak intensity. There is no intensity_scale, intensity_transform, or texture because the y-axis is mass error, not intensity.

facet_plot

from spxtacular import facet_plot
facet_plot(
    spectrum: Spectrum,
    fragments=None,
    mirror_spectrum: Spectrum | None = None,
    mirror_labels: Literal["auto", "both", "top"] = "auto",
    title: str | None = None,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    peak_selection: Literal["closest", "largest", "all"] = "closest",
    include_sequence: bool = False,
    unit: str = "ppm",
    max_labels: int | None = 60,
    theme_mode: Literal["light", "dark"] | None = None,
    **layout_kwargs,
)

Takes a single spectrum, not a list. The optional second spectrum is mirror_spectrum. max_labels caps the ion labels in the annotated panel. Like mirror_plot and mass_error_plot, facet_plot accepts no intensity_scale, intensity_transform, or texture; its panels are built from plot tables at their defaults (so relative intensity), and each panel axis is titled Intensity.

sequence_coverage_plot

from spxtacular import sequence_coverage_plot
sequence_coverage_plot(
    spectrum: Spectrum,
    peptide: str,
    fragments,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    peak_selection: Literal["closest", "largest", "all"] = "closest",
    title: str | None = None,
    theme_mode: Literal["light", "dark"] | None = None,
    **layout_kwargs,
) -> go.Figure

The coverage ladder: an annotated spectrum shows that peaks matched, this shows where along the peptide they matched — which is what tells you whether an identification is localised or leaning on one end of the molecule.

Parameter Default Description
spectrum The spectrum the fragments are matched against
peptide Residue sequence, one character per residue. Pass the stripped sequence — ProForma modification brackets are not rendered. Raises ValueError when empty
fragments Fragment objects, as for match_fragments
tolerance / tolerance_unit / peak_selection 0.02 / "da" / "closest" Matching parameters
title None Overrides the generated title
theme_mode None "light" / "dark"

Tick convention. Residues run left to right. A tick drawn above and to the left of a residue marks an N-terminal (a/b/c) fragment that ended at that bond; a tick below and to the right marks a C-terminal (x/y/z) fragment that started there. A bond carrying ticks on both sides is confirmed from both directions. N-terminal ticks take the b colour, C-terminal ticks the y colour, and both are named in the legend.

The default title reports the count of distinct bonds covered — e.g. Sequence coverage — 17/17 backbone bonds covered (100%).

import numpy as np
import peptacular as pt
import spxtacular as spx

peptide = "FDSFGDLSSASAIMGNPK"
fragments = pt.fragment(peptide, ion_types=("b", "y"), charges=(1, 2))

# A toy spectrum: one peak per theoretical fragment.
mz = np.sort(np.array([f.mz for f in fragments]))
spectrum = spx.Spectrum(mz=mz, intensity=np.linspace(1e4, 1e5, len(mz)))

fig = spx.sequence_coverage_plot(spectrum, peptide, fragments)   # note: spectrum first
print(fig.layout.title.text)
# Sequence coverage — 17/17 backbone bonds covered (100%)

reporter_ion_plot

reporter_ion_plot(
    spectrum: Spectrum,
    plex: str | IsobaricTagInfo | ReporterIons = "TMT10",
    *,
    tolerance: float = 20.0,
    tolerance_unit: Literal["da", "ppm"] = "ppm",
    impurities: ImpurityTable | pd.DataFrame | NDArray | None = None,
    show_spectrum: bool = True,
    normalize: bool = True,
    title: str | None = None,
    theme_mode: Literal["light", "dark"] | None = None,
    backend="plotly", style=None, size=None,
    **layout_kwargs,
)

Bars per channel from extract_reporter_ions (as % of the strongest channel unless normalize=False), with the raw reporter region above (show_spectrum) and the picked peaks highlighted. Missing channels are marked n.d.. A ReporterIons is plotted as is.

compose_figure

compose_figure(
    figures: Sequence[FigureSpec],
    *,
    ncols: int | None = None,
    labels: Sequence[str] | Literal["abc", "ABC"] | None = "abc",
    size: SizeLike = None,
    style: str | FigureStyle | None = None,
    backend: Literal["plotly", "matplotlib", "spec"] = "matplotlib",
    theme_mode: Literal["light", "dark"] | None = None,
)

Lays out several FigureSpecs (from backend="spec") as one lettered multi-panel figure at one width and style. size=None is the double-column width, or a 16:9 slide (254 × 143 mm) for style="talk". A panel too short for the style's text raises a UserWarning at layout. Anything that is not a FigureSpec raises SpxtacularError.

FigureSpec, render, FigureStyle, get_style

  • FigureSpec: the backend-neutral description every plot builds (cells, style, width_mm, height_mm, theme_mode). spec.render(backend) or render(spec, backend) draws it.
  • get_style(name) returns the "paper", "screen" or "talk" FigureStyle; unknown names raise SpxtacularError. FigureStyle is a frozen dataclass (fonts, sizes in pt, line widths, tick spacing, dpi, ...); style.with_(font_size=6) returns a changed copy.

save_figure

save_figure(fig, path: str | Path, *, scale: float | None = None, dpi: float | None = None, **kwargs) -> Path
Parameter Default Description
fig A plotly figure, a matplotlib figure or a FigureSpec (drawn with matplotlib when installed, else plotly; .html always plotly)
path Destination; the suffix picks the writer
scale None Plotly rasters: device pixel ratio. Overrides dpi
dpi None Raster resolution; defaults to the style's dpi (600 for "paper")
**kwargs Forwarded to write_html / write_image / savefig
Figure Suffixes Extra install
plotly .html or none (.html is appended) none
plotly .png, .svg, .pdf, .jpg, .jpeg, .webp pip install 'spxtacular[plotly-export]' (kaleido); missing raises ImportError
matplotlib .pdf, .svg, .eps, .png, .jpg, .jpeg, .tif, .tiff, .webp none; PDF/SVG embed fonts as TrueType
any anything else raises SpxtacularError

Returns the path actually written.

import numpy as np
import spxtacular as spx

spectrum = spx.Spectrum(mz=np.array([100.0, 200.0]), intensity=np.array([10.0, 40.0]))
path = spx.save_figure(spx.plot_spectrum(spectrum), "spectrum")     # -> Path('spectrum.html')

Theme

from spxtacular import theme

spxtacular.theme is the single source of truth for plot colour — both plot_table.py and visualization.py read from it, so a palette change lands on every figure at once rather than being kept in sync by comment. Every plotting function's theme_mode argument selects the mode for one figure; theme.set_plot_theme() sets the default for all of them.

Colour is assigned by job, not by taste

Job Encoding Lookup
Fragment ion series Nominal categorical — eight fixed hues in the fixed slot order b, y, a, c, x, z, p, i ion_color
Charge state Ordinal — one hue, running light → dark as charge rises, so the reader sees 1+ < 2+ < 3+ in the colour charge_color
Continuous magnitude — ion mobility (plot_spectrum(color="im")), and any per-peak score you colour yourself Sequential — one hue, light → dark, with a colourbar. Not Viridis: a multi-hue ramp invents banding that is not in the data sequential_scale
Unmatched peaks Recessive grey, also thinner and dimmer — unmatched peaks are context, not subject unmatched_color

Three consequences worth knowing:

  • Ion hues never cycle. Slots are assigned in order and stop at eight. Anything not in the eight slots — including internal fragments, whose ion types are two letters like "by" — folds to neutral_color() rather than being handed a ninth hue that would collide with an existing series. b and y take the first two slots because they are by far the most common pair, so the pair that co-occurs most often is the most separable.
  • The charge ramp clamps, it does not wrap. The shipped ramp has five steps, and charges past its end all take the far end: charge_color(11) == charge_color(5). The previous 10-colour cycle rendered z=1 and z=11 in identical colours, which is the one failure an ordinal encoding must not have. charge <= 0 — singletons (-1) and decharged peaks (0) — is neutral grey: absence of identity, not another category.
  • Dark mode is not an inversion. The dark charge ramp runs dark → light so it stays legible against the dark surface, and the sequential scale is reversed to match.

Functions

Function Signature Returns
set_plot_theme (mode: ThemeMode) -> None Sets the default mode for every subsequent plot. ValueError on anything but "light" / "dark"
resolve_mode (theme: ThemeMode \| None = None) -> ThemeMode The effective mode — the argument if given, otherwise the global default
set_palette (*, categorical=None, charge_ramp=None, sequential=None) -> None Replaces a palette wholesale (see below)
ion_color (ion_type: str, theme=None) -> str Hex colour for a fragment series; neutral for anything outside the eight slots
charge_color (charge: int, theme=None) -> str Hex colour from the ordinal ramp; clamped at the end, neutral for charge <= 0
ion_dash (ion_type: str) -> str Plotly dash pattern for a series — the texture channel used by texture=True. Mode-independent, so it takes no theme
sequential_scale (theme=None) -> list[list] Plotly colourscale for continuous magnitude — [[stop, hex], …], five stops
surface (theme=None) -> str Chart surface (paper and plot background)
text_color (level: Literal["primary","secondary","muted"] = "secondary", theme=None) -> str Ink. Labels never wear the series colour — identity comes from the mark
unmatched_color (theme=None) -> str Colour for peaks carrying no annotation
marker_outline (theme=None) -> str Hairline outline for filled marks such as mass-error bubbles; flips with the mode so bubbles stay separated on a dark surface
neutral_color (theme=None) -> str Colour for singletons and any category past the eighth slot
template (theme=None) -> go.layout.Template The plotly template: recessive chrome, horizontal gridlines only, m/z crosshair, autosize
apply (fig: go.Figure, theme=None) -> go.Figure Applies that template to an existing figure in place and returns it

ThemeMode is the type alias for the mode: Literal["light", "dark"].

Note the naming: inside theme the mode argument is called theme (ion_color("b", theme="dark")), while the plotting functions call the same thing theme_mode — there, theme would shadow the module.

import plotly.graph_objects as go
from spxtacular import theme

theme.set_plot_theme("dark")            # global default for every subsequent figure

theme.ion_color("b")                    # '#3987e5'  (dark-mode slot 0)
theme.ion_color("by")                   # neutral — internal fragments get no hue of their own
theme.ion_color("y", theme="light")     # '#eb6834' — one-off override, global default untouched
theme.charge_color(11) == theme.charge_color(5)   # True — the ramp clamps
theme.charge_color(-1) == theme.neutral_color()   # True — singletons are not a category

fig = theme.apply(go.Figure())          # borrow the template for your own figure
theme.set_plot_theme("light")

set_palette

theme.set_palette(
    *,
    categorical: dict[ThemeMode, list[str]] | None = None,   # >= 8 hues per mode
    charge_ramp: dict[ThemeMode, list[str]] | None = None,
    sequential: dict[ThemeMode, list[list]] | None = None,
) -> None

Each argument takes a {"light": [...], "dark": [...]} mapping and replaces that palette in both modes. categorical and charge_ramp take lists of hex strings; sequential takes a plotly colourscale, [[0.0, "#…"], …, [1.0, "#…"]]. ValueError is raised when a mapping is missing a mode, or when a categorical palette has fewer entries than there are ion slots (8).

Substituted palettes are not validated for colour-vision deficiency. The shipped palettes were checked with a CVD validator (protanopia and deuteranopia, Machado-Oliveira-Fernandes at severity 1.0) against both surfaces. A palette you pass to set_palette is not checked — the only validation is the structural one above (both modes present, at least 8 categorical hues). Validate your own hues before relying on them, or you lose the property the defaults were chosen for. Categorical hues want a fixed order with adjacent pairs kept far apart; a charge ramp wants a single hue with monotone lightness.

from spxtacular import theme

theme.set_palette(
    categorical={
        "light": ["#2a78d6", "#eb6834", "#1baf7a", "#eda100",
                  "#e87ba4", "#008300", "#4a3aa7", "#e34948"],
        "dark": ["#3987e5", "#d95926", "#199e70", "#c98500",
                 "#d55181", "#008300", "#9085e9", "#e66767"],
    },
    charge_ramp={
        "light": ["#86b6ef", "#5598e7", "#2a78d6", "#1c5cab", "#104281"],
        "dark": ["#184f95", "#256abf", "#3987e5", "#6da7ec", "#9ec5f4"],
    },
)

Utilities

from spxtacular import da_to_ppm, ppm_to_da
da_to_ppm(delta_mz: float, mz: float) -> float   # delta_mz / mz * 1e6
ppm_to_da(delta_ppm: float, mz: float) -> float  # delta_ppm * mz / 1e6

Convert a mass difference between Dalton and ppm at a given reference mz. Both take the difference first and the reference m/z second.

da_to_ppm(0.01, 500.0)   # 20.0 ppm
ppm_to_da(20.0, 500.0)   # 0.01 Da

Matching and scoring

Full documentation: Fragment matching and scoring

match_fragments

from spxtacular import match_fragments
match_fragments(
    spectrum: Spectrum,
    fragments,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    peak_selection: Literal["closest", "largest", "all"] = "closest",
    is_monoisotopic: bool = True,
) -> list[MatchedFragment]

When fragments is a dict[tuple[IonType, int], list[float]] (the output of peptacular.ProFormaAnnotation.fast_fragment), is_monoisotopic is forwarded to the Fragment constructor; otherwise it has no effect. Returns a list sorted by ascending peak_index.

score

from spxtacular import score
score(
    spectrum: Spectrum,
    fragments,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    peak_selection: Literal["closest", "largest", "all"] = "closest",
    predicted_intensities: Sequence[float] | None = None,
) -> dict[str, float]

Returns a dict of PSM metrics: hyperscore, probability_score, total_matched_intensity, matched_fraction, intensity_fraction, mean_ppm_error, spectral_angle, longest_run. When predicted_intensities is supplied, it must contain one value per fragment in the same order and enables the literature spectral-angle metric.

Spectrum similarity

cosine(
    query: Spectrum,
    reference: Spectrum,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    transform: Literal["sqrt", "linear", "log"] = "sqrt",
) -> float

modified_cosine(
    query: Spectrum,
    reference: Spectrum,
    query_precursor_mz: float,
    reference_precursor_mz: float,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    transform: Literal["sqrt", "linear", "log"] = "sqrt",
) -> float

entropy_similarity(
    query: Spectrum,
    reference: Spectrum,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
) -> float

All three return values in [0, 1], accept unsorted spectra, and use one-to-one peak matching. modified_cosine() also considers the precursor-mass displacement. Full documentation: Spectrum-to-spectrum similarity.


Isobaric reporter ions

Full documentation: Isobaric reporter ions

extract_reporter_ions(
    spectrum: Spectrum,
    plex: str | IsobaricTagInfo,          # "TMT10", "TMTpro18", "iTRAQ4", ...
    *,
    tolerance: float = 20.0,
    tolerance_unit: Literal["da", "ppm"] = "ppm",
    impurities: ImpurityTable | pd.DataFrame | NDArray | None = None,
    normalize: Literal["sum", "max"] | None = None,
) -> ReporterIons                         # also Spectrum.reporter_ions(plex, ...)

reporter_ion_table(
    spectra: Iterable[Spectrum] | Reader,
    plex: str | IsobaricTagInfo,
    *,
    tolerance: float = 20.0,
    tolerance_unit: Literal["da", "ppm"] = "ppm",
    impurities: ImpurityTable | pd.DataFrame | NDArray | None = None,
    normalize: Literal["sum", "max"] | None = None,
    ms_level: int | None = None,
    include_errors: bool = False,
) -> pd.DataFrame

isotope_correction_matrix(plex, impurities) -> NDArray          # observed = M @ true
correct_isotope_impurities(intensities, correction, *, plex=None) -> NDArray

ReporterIons (frozen, read-only arrays in channel order): plex, channels, reporter_mz, intensity, raw_intensity, observed_mz, corrected, normalize, and the properties found, mz_error (Da), ppm_error; ions["127N"] gives one channel; to_dict(). The most intense peak in each window is used; a missing channel is intensity 0.0 and observed_mz NaN. A bad plex, unit, tolerance (overlapping windows) or impurity table, a decharged spectrum, or NaN/inf intensities raise SpxtacularError. Numeric impurity shifts are 13C counts; label 15N impurities "-15N". See Scoring.


Plot table API

Provides an intermediate pandas.DataFrame that holds all data and visual properties for a spectrum plot. Users can freely modify the DataFrame before passing it to plot_from_table. pandas is a required dependency, so this API is always available.

Full documentation: Spectrum reference — plot_table

Table schema

Both builders return the same columns, in this order:

mz, intensity, intensity_abs, charge, score, im,
color, linewidth, opacity, dash, series,
label, label_size, label_color, label_angle,
hover
Column dtype Read by plot_from_table? Meaning
mz float64 yes Peak m/z
intensity float64 yes The plotted value — relative-scaled (base peak = 100) unless intensity_scale="absolute", then transformed if intensity_transform was given
intensity_abs float64 no The true intensity, always unscaled. This is what the tooltips report and what table_view(max_rows=…) ranks by
charge Int64 (nullable; pd.NA when the spectrum has no charge array) no Charge state
score float64 (NaN when absent) no iso_score
im float64 (NaN when absent) no Ion mobility
color str yes Hex colour, from theme
linewidth float yes, from the first row of each group Relative stick weight, matched heavier than unmatched; the style scales it
opacity float yes, from the first row of each group Matched peaks opaque, unmatched dimmer
dash str yes, from the first row of each group (only when not "solid") Texture channel — set per ion series when texture=True
series str yes Trace name and grouping key
label str yes Direct label; "" for peaks whose label was capped or collided away
label_size float64 yes Label size in pt; NaN (default) uses the style's label size
label_color str yes Label colour
label_angle float64 yes Label rotation in degrees. Defaults to 0 (horizontal); -90 reads bottom-to-top
hover str yes Tooltip text, baked in by the builder — to change a tooltip edit hover itself, not the value behind it

table.attrs["intensity_label"] carries the y-axis title that matches the scaling applied ("Intensity", "Relative intensity (%)", "√ relative intensity (%)", "log₁₀ …"). plot_from_table reads it, so a rescaled table titles its own axis.

Because intensity and intensity_abs are separate, tooltips are unaffected by rescaling: change intensity_scale and the axis changes, never the number the reader is told.

series values:

Table series values
build_plot_table with charge data "z=1", "z=2", … plus "singleton" (charge == -1) and "decharged" (charge == 0)
build_plot_table without charge data, or show_charges=False "peaks"
build_annot_plot_table the ion type of the matched fragment ("b", "y", …), or "unmatched"

build_plot_table

from spxtacular import build_plot_table
build_plot_table(
    spectrum: Spectrum,
    show_charges: bool = True,
    show_scores: bool = True,
    max_labels: int | None = 60,
    theme_mode: Literal["light", "dark"] | None = None,
    intensity_scale: Literal["absolute", "relative"] = "relative",
    intensity_transform: Literal["sqrt", "log"] | None = None,
) -> pd.DataFrame
Parameter Default Description
show_charges True Colour peaks by charge state on the ordinal ramp and set series to "z=N" / "singleton" / "decharged"
show_scores True Label peaks whose iso_score > 0 with their score
max_labels 60 Cap on labels kept non-empty, strongest first, after the collision pass
theme_mode None "light" / "dark" — decides the hex values written into color and label_color
intensity_scale "relative" Scaling written into intensity (intensity_abs is unaffected)
intensity_transform None None, "sqrt", or "log"

Plain spectra have no texture parameter because they have no ion series to distinguish. Their dash column is always "solid" unless the caller edits it.

build_annot_plot_table

from spxtacular import build_annot_plot_table
build_annot_plot_table(
    spectrum: Spectrum,
    fragments,
    tolerance: float = 0.02,
    tolerance_unit: Literal["da", "ppm"] = "da",
    peak_selection: Literal["closest", "largest", "all"] = "closest",
    include_sequence: bool = False,
    max_labels: int | None = 60,
    theme_mode: Literal["light", "dark"] | None = None,
    intensity_scale: Literal["absolute", "relative"] = "relative",
    intensity_transform: Literal["sqrt", "log"] | None = None,
    texture: bool = False,
) -> pd.DataFrame

Same trailing parameters as build_plot_table, and here texture=True does have an effect: each matched peak's dash is set from theme.ion_dash(ion_type). When one peak matches several ions, the colour and series are chosen by the fixed ion slot order rather than by input order, so reordering the fragment list never silently repaints the plot; the label lists every matching ion, joined by <br>.

plot_from_table

from spxtacular import plot_from_table
plot_from_table(
    table: pd.DataFrame,
    title: str | None = None,
    theme_mode: Literal["light", "dark"] | None = None,
    render: Literal["sticks", "profile"] | None = None,
    max_points: int | None = 4000,
    backend: Literal["plotly", "matplotlib", "spec"] = "plotly",
    style: str | FigureStyle | None = None,
    size: SizeLike = None,
    absolute_axis: bool = False,
    **layout_kwargs,
)

Draws one stick (or profile) mark per unique (series, color) group, a transparent hover layer (plotly), and one label per row with a non-empty label, placed without overlap. render=None uses table.attrs["render"], falling back to sticks. Profile rendering applies min/max decimation above max_points, or draws every sample when it is None.

The required columns are validated up front — a missing one raises SpxtacularError: plot table is missing required column(s): … immediately, rather than part-way through rendering or only on data that happens to carry labels:

mz, intensity, series, color, linewidth, opacity, hover,
label, label_size, label_color

intensity_abs, dash, and label_angle are not required (a missing label_angle draws horizontal labels), so a table built by an older version still renders. Grouping keeps NA keys (dropna=False): a row whose series or color came back NA — easy to produce with merge / reindex / concat on a hand-edited table — is drawn in the unmatched colour under the series name "unlabelled" instead of silently vanishing from the figure.

The legend is shown only when the table holds more than one series.

table_view

from spxtacular import table_view
table_view(
    table: pd.DataFrame,
    max_rows: int | None = None,
    annotated_only: bool = False,
) -> str

Renders a plot table as an accessible HTML <table> string — the companion to the figure, not a replacement for it. It exists because a tooltip should enhance, never gate: label capping deliberately drops labels off the figure, and hovering is unusable for keyboard and screen-reader users, so every value needs a non-hover route.

Parameter Default Description
table A table from build_plot_table or build_annot_plot_table
max_rows None Keep only this many most-intense peaks (ranked on intensity_abs); None keeps all
annotated_only False Keep only peaks carrying a label — useful beside an annotated spectrum, where unmatched peaks are context rather than results

Rows are emitted in m/z order. The columns are m/z and Intensity (the true intensity from intensity_abs), plus z, Score, and Ion mobility when those columns hold any non-NA value, plus Annotation when any label is present. Label text is HTML-escaped, and the <br> separators the plot uses between co-matching ions become commas.

import numpy as np
import peptacular as pt
import spxtacular as spx

peptide = "FDSFGDLSSASAIMGNPK"
fragments = pt.fragment(peptide, ion_types=("b", "y"), charges=(1, 2))

# A toy spectrum: one peak per theoretical fragment.
mz = np.sort(np.array([f.mz for f in fragments]))
spectrum = spx.Spectrum(mz=mz, intensity=np.linspace(1e4, 1e5, len(mz)))

table = spx.build_annot_plot_table(spectrum, fragments)
print(table.attrs["intensity_label"])           # Relative intensity (%)

html = spx.table_view(table, max_rows=3, annotated_only=True)
print(html)
# <table><caption>Peak list</caption>…