Usage

This guide covers the full mzPAF grammar. For a five-minute introduction, see Quickstart.

Basic Peptide Ions

import paftacular as pft

# Primary fragment ions (a, b, c, x, y, z series)
ann = pft.parse("b2")
print(ann.ion_type.series)

ann = pft.parse("y3")
print(ann.ion_type.series)
b
y
# With peptide sequence
ann = pft.parse("y3{PEP}")
print(ann.ion_type.sequence)
PEP
# Internal fragments
ann = pft.parse("m2:5{PEPTIDE}")
print(ann.ion_type.start_position)
print(ann.ion_type.end_position)
2
5

Other Ion Types

import paftacular as pft

# Precursor ion
ann = pft.parse("p^2")  # Doubly-charged precursor

# Immonium ions
ann = pft.parse("IK")          # Lysine immonium
ann = pft.parse("IK[Acetyl]")  # Modified

# Reference compounds
ann = pft.parse("r[TMT126]")

# Chemical formula
ann = pft.parse("f{C6H12O6}")

# SMILES notation
ann = pft.parse("s{CN=C=O}")

# Unannotated/unknown peaks
ann = pft.parse("?")
ann = pft.parse("?42")  # With reporter number

Modifications

import paftacular as pft

# Neutral losses (formula-based)
ann = pft.parse("y5-H2O")
ann = pft.parse("b3-NH3")
ann = pft.parse("b2-H2O-NH3")  # Multiple losses

# Neutral gains
ann = pft.parse("y5+NH3")

# Mass-based losses/gains
ann = pft.parse("y5-17.03")
ann = pft.parse("b2+22.98")

# Reference-based
ann = pft.parse("y5-[Adenine]")

Isotopes

import paftacular as pft

# Generic isotope peak
ann = pft.parse("y5+i")

# Specific element
ann = pft.parse("y5+i13C")
ann = pft.parse("b2+2i13C")  # Multiple isotopes

# Average isotopomer
ann = pft.parse("y5+iA")

Adducts

import paftacular as pft

# Single adduct
ann = pft.parse("y5[M+Na]")

# Multiple adducts
ann = pft.parse("y5[M+2H+Na]^3")
ann = pft.parse("p[M+NH4]")

Charge States

import paftacular as pft

# Singly charged (default)
ann = pft.parse("y5")
print(ann.charge)  # 1

# Doubly charged
ann = pft.parse("y5^2")
print(ann.charge)  # 2

# Triply charged
ann = pft.parse("b3^3")
print(ann.charge)  # 3

# Negative mode: written as ^-2 so the round trip keeps the sign
ann = pft.parse("y5{PEPTI}^-2")
print(ann.charge)       # -2
print(ann.serialize())  # y5{PEPTI}^-2
print(ann.serialize(signed_charge=False))  # y5{PEPTI}^2 (mzPAF 1.0.1 section 4.8)

Mass Errors and Confidence

import paftacular as pft

# Mass error in ppm
ann = pft.parse("y5/1.2ppm")
print(ann.mass_error.value)  # 1.2
print(ann.mass_error.unit)   # "ppm"

# Mass error in daltons
ann = pft.parse("y5/-0.003")

# Confidence score (0.0 to 1.0)
ann = pft.parse("y5*0.95")
print(ann.confidence)  # 0.95

# Combined
ann = pft.parse("y5/1.2ppm*0.95")

Complex Annotations

import paftacular as pft

# Everything at once
ann = pft.parse("&2@y5-H2O+i13C[M+H+Na]^2/-0.55ppm*0.85")
print(ann.is_auxiliary)        # True (& flag)
print(ann.analyte_reference)   # 2 (@ reference)
print(ann.ion_type.series)     # y
print(ann.ion_type.position)   # 5
print(ann.neutral_losses)      # Tuple with H2O loss
print(ann.isotopes)            # Tuple with 13C isotope
print(ann.adducts)             # Tuple with H and Na
print(ann.charge)              # 2
print(ann.mass_error.value)    # -0.55
print(ann.confidence)          # 0.85

Mass Calculations

import paftacular as pft

# Without sequence context get_mass() returns the ion offset, not a complete fragment mass
ann = pft.parse("y5")
print(ann.get_mass(monoisotopic=True))    # Monoisotopic mass
print(ann.get_mass(monoisotopic=False))   # Average mass

# mz() needs a sequence for peptide, internal and precursor ions
print(pft.parse("y2{DE}").mz())

# With modifications
ann = pft.parse("y5-H2O")
print(ann.get_mass(monoisotopic=True))    # Mass adjusted for water loss

# Get composition
ann = pft.parse("f{C6H12O6}")
comp = ann.comp(calculate_sequence=True)  # Counter with ElementInfo keys
print(ann.formula())              # "C6H12O6" (formula ions already specify all atoms)
print(ann.proforma_formula())     # ProForma-style formula

Parsing Multiple Annotations

import paftacular as pft

# parse() takes exactly one annotation. Use parse_multi() for a comma-separated list
annotations = pft.parse_multi("b2, y3-H2O, p^2")
for ann in annotations:
    print(ann.serialize())
b2
y3-H2O
p^2

Creating Annotations Programmatically

Instead of parsing strings, you can create annotations directly using factory methods.

Basic Factory Methods

from paftacular import PafAnnotation

# Create a precursor ion
ann = PafAnnotation.make_precursor()
print(ann.serialize())
p
# Create a peptide fragment ion
ann = PafAnnotation.make_peptide("y", 5)
print(ann.serialize())
y5
# Create an internal fragment
ann = PafAnnotation.make_internal(start_position=2, end_position=5)
print(ann.serialize())
m2:5
# Create an immonium ion
ann = PafAnnotation.make_immonium("K")
print(ann.serialize())
IK
# Create a reference ion
ann = PafAnnotation.make_reference("TMT126")
print(ann.serialize())
r[TMT126]

Adding Modifications

All factory methods accept common parameters for modifications.

from paftacular import PafAnnotation

# Add neutral losses
ann = PafAnnotation.make_peptide("y", 5, neutral_losses=["-H2O", "-NH3"])
print(ann.serialize())
y5-H2O-NH3
# Add isotopes
ann = PafAnnotation.make_peptide("b", 3, isotopes=["+i13C", "+i15N"])
print(ann.serialize())
b3+i13C+i15N
# Add adducts
ann = PafAnnotation.make_precursor(adducts=["+H", "+Na"], charge=2)
print(ann.serialize())
p[M+H+Na]^2
# Add charge state
ann = PafAnnotation.make_peptide("y", 5, charge=2)
print(ann.serialize())
y5^2
# Add mass error (in ppm)
ann = PafAnnotation.make_peptide("y", 5, mass_error=1.2, mass_error_unit="ppm")
print(ann.serialize())
y5/1.2ppm
# Add confidence score
ann = PafAnnotation.make_peptide("y", 5, confidence=0.95)
print(ann.serialize())
y5*0.95

Complex Annotations

Combine multiple parameters to create complex annotations.

from paftacular import PafAnnotation

# Everything at once
ann = PafAnnotation.make_peptide(
    "y", 5,
    neutral_losses=["-H2O"],
    isotopes=["+i13C"],
    adducts=["+H", "+Na"],
    charge=2,
    mass_error=-0.55,
    mass_error_unit="ppm",
    confidence=0.85,
    is_auxiliary=True,
    analyte_reference=2
)
print(ann.serialize())
&2@y5-H2O+i13C[M+H+Na]^2/-0.55ppm*0.85

With Sequences

from paftacular import PafAnnotation

# Peptide ion with sequence
ann = PafAnnotation.make_peptide("y", 3, sequence="PEP")
print(ann.serialize())
y3{PEP}
# Internal fragment with sequence
ann = PafAnnotation.make_internal(2, 5, sequence="EPTI")
print(ann.serialize())
m2:5{EPTI}
# Modified immonium ion
ann = PafAnnotation.make_immonium("M", modification="Oxidation")
print(ann.serialize())
IM[Oxidation]

Other Ion Types

from paftacular import PafAnnotation

# Chemical formula
ann = PafAnnotation.make_formula("C6H12O6")
print(ann.serialize())
f{C6H12O6}
# SMILES notation
ann = PafAnnotation.make_smiles("CN=C=O")
print(ann.serialize())
s{CN=C=O}
# Named compound
ann = PafAnnotation.make_named_compound("Urocanic Acid")
print(ann.serialize())
_{Urocanic Acid}
# Unknown ion
ann = PafAnnotation.make_unknown(label=42)
print(ann.serialize())
?42

Serialization (Round-trip)

import paftacular as pft

# Parse and serialize back
original = "y5-H2O^2/1.2ppm*0.95"
ann = pft.parse(original)
serialized = ann.serialize()
print(serialized)
y5-H2O^2/1.2ppm*0.95

Export to Dictionary

import paftacular as pft

# Export annotation as a versioned dictionary (for JSON)
ann = pft.parse("y5-H2O^2/1.2ppm*0.95")
data = ann.to_dict()

Peptacular Integration

paftacular integrates with peptacular to convert its Fragment objects directly into PafAnnotation objects via to_mzpaf().

import peptacular as pt
import paftacular as pft

# Generate fragment ions from a peptide sequence
fragments = pt.fragment("PEPTIDE", charges=[1, 2], ion_types=["b", "y"])

# Convert each fragment to a PafAnnotation
annotations = [pft.to_mzpaf(f) for f in fragments]
for ann in annotations[:3]:
    print(ann.serialize())
# b1{P}
# b2{PE}
# b3{PEP}

Optional parameters let you attach a mass error and confidence score to each annotation:

ann = pft.to_mzpaf(
    fragments[0],
    confidence=0.95,
    mass_error=1.2,
    mass_error_unit="ppm",
)
print(ann.serialize())   # b1{P}/1.2ppm*0.95

Pass include_sequence=False to omit the embedded sequence from the annotation (useful when sequence context is not needed):

ann = pft.to_mzpaf(fragments[0], include_sequence=False)
print(ann.serialize())   # b1

For more details, see the PSI mzPAF specification.

Resolving Analyte Context

Without sequence context, get_mass() of a peptide, internal or precursor annotation returns only the ion offset and modifiers, and mz() raises PaftacularError. Use resolve() for a complete fragment calculation. It returns a new annotation and leaves the original unchanged.

original = pft.parse("y2")
resolved = original.resolve("PEPTIDE")
print(resolved.sequence)
print(round(resolved.mz(), 4))
print(original.sequence)
DE
263.0874
None

A mapping selects analytes by their mzPAF references. An omitted reference selects key 1. A supplied string is the full analyte for the current annotation. Resolution supports peptide fragments (including the d, v and w side-chain series), internal fragments, and precursors. It rejects missing references, out-of-range positions, internal fragments that include a terminus, and conflicting embedded sequences. The analyte’s charge is excluded from the selected sequence. The annotation’s own charge and adducts determine the final charged species.

resolved = pft.parse("2@b2").resolve({1: "AAAA", 2: "PEPTIDE"})
print(resolved.sequence)
print(resolved.serialize())
PE
2@b2

Resolved context is deliberately separate from mzPAF text. Use to_dict() when persisting it. Parsing resolved.serialize() requires resolving the analyte again before computing a complete mass.

Structured Errors and Batch Parsing

Every error caused by bad input is a PaftacularError, which is a ValueError. PafParseError is a PaftacularError with text, reason, position, and annotation_index attributes. Positions and indices start at zero. Syntax errors point to unexpected content or the end of incomplete input. Semantic errors identify the start of the affected annotation.

Parsing does not look up reference molecule names. Calculating an r[...] ion or a -[...] loss whose name is in neither the mzPAF reference list nor Unimod raises PafUnknownReferenceError, with the name in name. It is a PaftacularError and also a KeyError, which 1.3.2 raised for r[...].

iter_parse() processes an iterable lazily. Wrap it in list() to collect the results. Each result preserves the original record text and index. If any annotation in a record is malformed, that record has an error and no partial annotations. Later records still run. Empty records succeed with an empty annotation tuple. Programming errors such as passing non-string records are not converted into parse errors.

results = list(pft.iter_parse(["y2,b3", "y2,b3!", "p"]))
print([result.ok for result in results])
print(results[1].index, results[1].error.annotation_index, results[1].error.position)
[True, False, True]
1 1 5

Versioned Interchange

to_dict() exports schema version 1, including an ion type discriminator, component fields, optional mass error, and resolved sequence context. PafAnnotation.from_dict() reconstructs that representation, rejecting unknown versions, missing or extra fields, invalid types, nonfinite numbers, and malformed mzPAF fields. It does not require optional dependencies to reconstruct data. Chemical and ProForma interpretation happens when properties are calculated.

import json
resolved = pft.parse("b2").resolve("PEPTIDE")
data = resolved.to_dict()
restored = pft.PafAnnotation.from_dict(json.loads(json.dumps(data)))
print(data["schema_version"], data["ion"]["type"])
print(restored == resolved)
print(restored.sequence)
1 PeptideIon
True
PE

Calculation and Serialization Conventions

get_mass() is the charged species mass. comp() counts nuclei, so summing its elemental masses requires subtracting charge electron masses (a negative charge adds them) to compare with get_mass(). mz() divides by the absolute charge. Upstream tabulated ion offsets are rounded, so numerical comparisons should allow approximately one microdalton.

A named modification (Unimod, PSI-MOD, RESID, XLMOD, GNO) adds its listed database mass, the same rule as peptacular. Fragment and precursor ions, including neutral losses and isotope peaks, agree with peptacular 5 to 1e-9 Da. A global isotope label (<13C>, <15N>) uses composition in both packages. Unimod reference names (r[Hex]) use the listed 6-decimal mass, while mzPAF reference-list entries (r[TMT6plex]) keep their exact formula masses. Labile modifications ({Glycan:Hex}) count only for precursor ions: fragments lose them. The listed mass is rounded (Oxidation is 15.994915, its composition gives 15.99491462), so the mass of comp() can differ from get_mass() by up to about 1e-6 Da for a named modification. Composition is used only for modifications with no listed mass, such as formulas. Formula ions already describe the charged species’ atoms. Their adducts label charge carriers without adding atoms. Other supported ions add the specified adduct atoms, or implicit hydrogens when no adduct is given.

A generic +i uses the carbon-13 minus carbon-12 mass shift. In compositions, isotope substitutions consume ordinary or explicit monoisotopic atoms when available. Genuine deficits remain visible when context is incomplete. Average-isotopomer masses remain undefined and raise PaftacularError. SMILES isotope labels are retained during calculation. Charged SMILES molecules are rejected. Charge carriers belong in mzPAF adducts.

Precursor conversion with to_mzpaf() retains sequence context when include_sequence=True. This context is preserved by structured export, while the mzPAF string remains p with its ordinary modifiers and charge.

There is a known convention difference between the internal-cleavage table in mzPAF 1.0.1 section 4.4.4 and the physical ion definitions in tacular and peptacular. make_internal(ion_type=...) and the public correction tables retain the specification convention for compatibility. Explicit InternalFragment cleavage fields and to_mzpaf() preserve the source ion’s physical composition using explicit neutral gains and losses. For example, a physical ax fragment is represented as m2:4-H2. The mass of an existing mzPAF string is never reinterpreted based on an inferred series.

Numeric output uses decimal notation compatible with the grammar and retains float precision. Mass-based losses retain at least five decimal places for compatibility, adding more when necessary. Round trips preserve meaning, not necessarily the original spelling or redundant zero-isotope annotations.