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:
| 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:
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.
| 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.
| 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.
| 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.
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:
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:
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¶
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¶
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¶
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¶
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¶
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¶
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)orrender(spec, backend)draws it.get_style(name)returns the"paper","screen"or"talk"FigureStyle; unknown names raiseSpxtacularError.FigureStyleis 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¶
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 toneutral_color()rather than being handed a ninth hue that would collide with an existing series.bandytake 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 renderedz=1andz=11in 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_paletteis 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¶
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.
Matching and scoring¶
Full documentation: Fragment matching and scoring
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¶
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¶
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¶
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¶
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:
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¶
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>…