Stage 3: locating C=C and oxidation

MS2 matching tells you a lipid is PE 18:1_20:4. It does not tell you where on those chains the double bonds sit, or where a hydroxyl group has been added. Stage 3, acyl-chain analysis, answers that second question: it takes stage-2 results and reports idlevel 3 (all plausible positional candidates) and idlevel 4 (the per-feature consensus).

It runs on lipids that have already been identified, taking spectra acquired with a supported position-sensitive dissociation method and localising double-bond and, where supported, oxygen positions along each chain.

WORKFLOW:
  stage3: ead2   # False | ead1 | ead2 | uvpd | oad | ozid

This step is off by default. Select the engine matching the acquisition method: EAD, UVPD, OAD, or OzID. It cannot extract positional information from ordinary CID spectra.

The engines

The s3_score column carries the localisation score, and s3_score_data and ms2_evidence record the supporting evidence. The engines are intensity-aware, but differ in their fragment-mass model, intensity transform, and eligible chemistry. Equal s3_score values from different engines are not equivalent evidence and should not be compared across runs configured differently. For what s3_score does and doesn't tell you on its own, and why a high score alone is never proof of a determined position, see Scoring, thresholds, and honest reporting.

ead1: Fixed ladder, non-oxidised

ead1 implements the published method, described in full in Wu et al, Analyst, 2025, 10.1039/d5an00567a. For the experimental background, why EAD cleaves bonds that CID leaves intact and what the resulting fragment ladder looks like, see On the benefits and role of electron-induced dissociation in lipidomics.

It runs in four steps:

  1. Enumerate the hypotheses. For a chain of C carbons carrying n double bonds, every placement of those n bonds across positions 2 to C−1 is generated, excluding adjacent placements. Saturated chains, and chains under four carbons, yield a single trivial variant.
  2. Simulate the ladder. Each hypothesis is walked bond by bond from the methyl tail toward the carboxyl head. An ordinary step loses CH₂; a step crossing a double bond loses CH instead, and that is what shifts the ladder's spacing at the double bond. Every bond emits three neutral-loss variants, labelled 0, -H and +H (losses of CH₂, CH₃ and CH), weighted so the homolytic cleavage dominates.
  3. Weight by distance to the nearest double bond. Each step's weight is scaled by a hand-tuned array indexed on how many bonds separate it from the nearest double bond: 0.5 at the bond itself, 1 one bond away, 6 at two bonds, 1 at three, 0.5 at four and five, and 1 beyond. That spike at two bonds is where the positional signal actually lives. A bond flanked by two double bonds now takes max(left, right) of the two lookups rather than whichever happened to be evaluated last.
  4. Match and score. Predicted fragments from all chains are pooled and binned by m/z, with coincident masses summing their weights. Observed peaks are matched one-to-one against them within tolerance, by maximum bipartite matching, so no peak is counted twice.

ead1 does not support oxidised lipids at all. Oxidation parameters are ignored; chains with ;O are filtered out entirely. Use ead2 for anything oxidised.

ead2: Exact formulas, oxidised

ead2 takes the opposite approach: it computes exact elemental-formula losses for every cleavage, and scores each predicted fragment independently against the closest peak in tolerance rather than matching one-to-one. It supports hydroxyl and ketone oxygens natively, and resolves oxygen type the same way it resolves position — by spectral match.

Oxygen chemistry. A ;O suffix in a lipid name gives an oxygen count, not a chemistry. Since every hypothesis is anchored on a measured precursor, each candidate must be isobaric, so each oxygen group has to pay for itself:

Group Change Backbone cost
-OH hydroxyl +1 O none
=O ketone +1 O, −2 H one double bond

So 20:4;O admits 20:4;OH and 20:3;oxo, but never 20:4;oxo. An oxygenated carbon may not be sp2, so no double bond may land on or adjacent to it.

Two consequences worth stating plainly: the supported oxygen space is deliberately limited to hydroxyl and ketone readings, and hydroperoxide or extra-carboxyl interpretations are not enumerated at all.

Because the name only ever carries an oxygen count, ead2 must search both the all-hydroxyl and the ketone readings on every oxidised feature. On a mono-hydroxy reference set its top candidate lands on the wrong oxygen type in about 30% of spectra. That is the cost of not being told the chemistry up front, and it is inherent to the ;O data model rather than a scoring defect.

oad: MS-DIAL PC protonated profile

oad implements the pinned MS-DIAL PCOadSpectrumGenerator and common acyl OAD mass rules for one deliberately narrow profile: unoxidised diacyl PC [M+H]+, charge +1. Other classes, adducts, oxidised lipids, ether chains, and sphingoid bases are skipped rather than approximated.

For every proposed C=C position, OAD02 and OAD15 are essential anchors and both must be observed. OAD01, OAD03, OAD14, OAD16, OAD12+O, OAD12+O+H, and OAD12+O+2H are optional, intensity-weighted ranking evidence. Coincident theoretical masses are merged and observed peaks are assigned one-to-one.

OAD uses the same selection, resolvability, and null-decoy machinery as the other stage-3 engines — see Scoring, thresholds, and honest reporting for how that works and why it exists. These quantify localisation uncertainty; they are not a discovery target-decoy q-value. The current posterior temperature was fitted on EAD reference sets, not OAD, so OAD probabilities remain provisional until a larger known-position OAD panel is calibrated.

ozid: paired aldehyde/Criegee products

ozid is a spectrum-level OzID engine, not a replacement for LipidOz's chromatographic XIC and isotope-envelope workflow. It enumerates non-adjacent C=C placements, converts each delta position to methyl-end notation, and reproduces LipidOz's exact precursor-formula aldehyde and Criegee products. PUFA products include the methyl-end double-bond index correction.

The initial validated profiles are PC/PE [M+H]+, PI [M+Na]+, and DG/TG [M+NH4]+, at charge +1. Molecular-species chain detail and a formula consistent with precursor m/z are required. Unlabelled lipids and the validated UltimateSPLASH global d5 formula are accepted; other labels, sum compositions, oxidation, ethers/plasmalogens, sphingoid chains, and other class/adduct combinations fail closed.

Each expected C=C produces an aldehyde/Criegee pair separated by one oxygen atom. Complete pairs dominate the mass-accuracy score; partial pairs remain in diagnostics but, by default, cannot establish localization. Identical theoretical evidence signatures are collapsed before posterior calculation, preventing chain-swapped or mass-degenerate structures from gaining probability merely by being enumerated more than once.

OzID preprocessing retains ions above the precursor and applies only ms2_noise_abs, not the global percentile filter ms2_noise_prc. A percentile threshold can otherwise discard the weaker member of a chemically required pair before formula-aware matching. Deuterated standards additionally require remove_d: False, and stage 3 still requires an eligible molecular-species parent from stage 2.

OzID uses the shared posterior and credible-set selection (see Scoring, thresholds, and honest reporting), but its resolvability test is pair-specific: the winner needs more complete site-exclusive diagnostic pairs than its best structurally distinct rival, not just a peak-count comparison. The optional allene null remains a diagnostic comparator. These are localization-confidence measures, not peptide-style target-decoy FDR or a claim of LipidOz-equivalent performance.

Choosing which candidates are reported

Selection is set by PARAM.s3_selection and is orthogonal to the engine — either method works with either engine, including on oxidised lipids. See Scoring, thresholds, and honest reporting for why this decision exists at all (in short: s3_score alone cannot distinguish a decisive winner from a cluster of near-ties) and what it measures against to validate itself.

posterior (default)legacy (formerly cutoff)
Keepsthe smallest set of candidates holding s3_post_credible_level of the posterior massevery candidate scoring within s3_legacy_cutoff of the top one
Names a position at idlevel 4when one placement carries at least s3_post_min_masswhen the kept candidates happen to agree
Set size meanshow uncertain the spectrum isnothing in particular
Confidence tail reportsper-position marginal probabilitiesvote counts over the kept set

The softmax temperature is fitted, not chosen, and is no longer one shared value: s3_post_temperature_ead1 (0.03) and s3_post_temperature_ead2 (0.003) were fitted separately per engine — ead1 already has its own assertion gate doing the selecting, so it stays close to its original, better-calibrated value, while ead2 has no gate of its own and needs a sharper posterior to avoid under-covering oxidised, positionally-dense chains. s3_post_temperature (0.01) remains shared across oad and ozid, which do not have their own fitted value yet, while UVPD has one now: s3_post_temperature_uvpd is 0.003, the value the manuscript's UVPD benchmark uses. Score gaps are taken relative to the top score in every case, so a temperature is meaningful across engines whose raw scores differ by orders of magnitude even though its fitted value isn't shared.

The trade is coverage against correctness. Measured on a mono-hydroxy reference set with ead2, legacy asserts a C=C position on 84 of 96 features and is right on 55 (65%); posterior asserts 55 and is right on 40 (73%). legacy remains available for reproducing earlier results.

Configuration

Every stage-3 parameter is prefixed s3_ — after the stage that owns it, not the ID level it reports. The former ead_* and include_ms1_monochain spellings are still accepted, so existing configs load unchanged.

General

Parameter Default Meaning
s3_max_unsaturated 6 Caps unsaturation accepted for exhaustive search (higher = more candidates, slower)
s3_max_oxygen 2 Caps total oxygen positions across all chains (ead2 and uvpd; ead1 ignores it)
s3_max_candidates 100000 Candidates scored before stride-sampling kicks in
s3_output_max 100 Reported/credible candidates written to diag/annotation_stage3.csv per feature, plus up to s3_output_extra further non-kept rows for context
s3_legacy_min_score 0.2 Absolute s3_score floor, s3_selection: legacy only. Compared against whatever s3_score_rescaling produces — a no-op at the default 'none' scale, since raw scores rarely sit near [0, 1].
s3_score_rescaling none Floor/span rescale of the raw score into s3_score, legacy selection only, uniform across all five engines: none (raw score as-is), mad, median-max, min-max. Does not apply under posterior, where s3_score is the posterior probability instead.
s3_min_margin 0.0 Minimum relative gap between the best two candidates before idlevel 4 names a position. Applied by ead1, oad, and uvpd always; by ead2 only when s3_ead2_gate_db_on_resolvability is turned on (off by default); by ozid always, in place of the matched-peak check below.
s3_min_matched 3 Minimum peaks the winner must have matched for that gap to count. Applied by ead1, oad, and uvpd always; by ead2 only when s3_ead2_gate_db_on_resolvability is on. Not used by ozid, which requires a complete aldehyde/Criegee pair instead.
s3_intensity_scaling none Compressive transform on matched intensity, shared across ead1, ead2, oad, and uvpd (ozid has no intensity-weighted score term): none, sqrt, log10, ln, log2. ead1 was fitted assuming sqrt weighting — set it explicitly to use that fit, since the shared default is none.
s3_include_ms1_monochain True Admit idlevel 1 single-chain parents such as LPC/LPE
s3_null_decoys False Diagnostic: also score chemically impossible (allene) placements as a null. Never emitted as an annotation.
ozid_require_complete_pair True Require every expected OzID aldehyde/Criegee pair to be complete before accepting localization
ozid_min_complete_pairs 1 Minimum complete OzID pair count required for an accepted localization
ozid_formula_mz_tolerance 0.01 Allowed formula/adduct precursor-mass disagreement in Da; inconsistent formulas fail closed

Candidate selection

Parameter Default Meaning
s3_selection posterior posterior or legacy (accepted as cutoff)
s3_post_credible_level 0.95 Posterior mass the reported credible set must reach
s3_post_min_mass 0.5 Posterior mass a single placement needs before its positions enter the idlevel-4 name
s3_post_odds_floor 0.125 Likelihood-ratio floor on credible-set membership: a candidate drops out once its own probability falls below this fraction of the top candidate's, even short of s3_post_credible_level. Fitted for ead2; 0.0 disables it.
s3_post_temperature_ead1 / _ead2 0.03 / 0.003 Softmax temperature, fitted separately per engine (see below)
s3_post_temperature 0.01 Softmax temperature shared by oad, ozid, and uvpd, which have no fitted value of their own yet
s3_legacy_cutoff 0.99 Relative score cutoff vs the top candidate, used only by s3_selection: legacy

s3_max_candidates deserves particular care. A single oxidised token such as 20:4;O enumerates roughly 22,300 candidates; a di-oxidised one such as 20:3;O2 enumerates roughly 74,700. If the cap sits below that, the true structure can be stride-sampled out of consideration before scoring ever runs. The failure is silent and looks identical to a scoring failure in the output — it is how a real, checked-in fixture lost its correct answer before this default was raised. s3_n_isomers_total in the stage-3/4 CSVs reports the full, un-sampled space next to the number actually scored; when they differ, the search was truncated. The default (100000) still does not fully cover the di-oxidised case, but the sampling order thins double-bond placements first, so every oxygen hypothesis is still scored regardless.

How positions are reported

An idlevel-4 name carries a confidence tail listing each position and the support behind it, and the same numbers ride along in the CXSMILES trailer as dbPos(...) / mPos(...) tokens:

FA 20:4;11OH [C=C: Δ5 100%, Δ8 100%, Δ12 100% | Δ14 50% or Δ15 50%; OH: 11 100%]

Note where the hydroxyl position sits: in the tail, not in the name body. A group positioned mid-chain alongside unlocalised double bonds does not say how those bonds divide around it, so writing it into the body would make the name unrepresentable. See Nomenclature and CXSMILES for the full encoding.