From envelope to evidence: what Atlas can do with XPS
A published MoS₂ VAMAS corpus, fresh and two years later: vendor-agnostic import, survey identification, energy referencing with recorded provenance, and a constrained Mo 3d / S 2p fit that says when it cannot tell oxide from noise — and when it can.
XPS is the technique people reach for when they want to know whether a TMD film has oxidised, how much of the molybdenum is still Mo⁴⁺, or whether the sulfur is lattice sulfur or sulfate. It is also the technique where the software does the most damage. A binding-energy axis that nobody referenced, a Shirley background with no stated endpoints, and a stack of free Gaussians that reproduce the envelope perfectly will produce a chemical-state table that looks authoritative and means nothing.
Atlas treats XPS as a chain of recorded transformations. Each step writes down what it assumed, refuses to guess where guessing would be silent, and hands the next step a dataset that carries its own provenance. This post follows a published MoS₂ dataset through that chain and shows where the honest answer is "yes, this is oxide" and where it is "this data cannot tell."
A note about the data
The spectra are from a CC-BY-4.0 Zenodo deposition by Píš, Bondino and Sojková: synchrotron XPS of Li-doped MoS₂ few-layer films on c-plane sapphire, measured at the BACH beamline at Elettra with a Scienta R3000 analyser (hν ≈ 600 eV, pass energy 50 eV for regions). The same samples were re-measured two years later. Both campaigns are exported as multi-block VAMAS files:
We follow one sample, the 12 nm film grown with 20 % Li₂S, through both files. This is real data with real quirks: the aged file's header declares 39 blocks but contains 16, and the ordinate is Normalised cps (divided by the photon-flux monitor) rather than counts. Those quirks matter, and they show up in the outputs below.
Import keeps the axis and admits what it does not know
A VAMAS block carries the source energy, pass energy, dwell, sweeps, analyser mode, take-off angle, and whether CasaXPS processing was embedded. Every later safeguard depends on one of those fields, so parse-vamas-xps keeps all of them and mints one dataset per block under a shared experiment_id. It rebuilds the energy axis from start and step rather than trusting a decoder's 10 meV quantisation, labels the axis as binding or kinetic explicitly, and records embedded processing history without applying it.
Most labs do not export VAMAS. parse-xps-vendor-export reads PHI MultiPak .spe/.pro, SPECS Prodigy .xy, Scienta Omicron .txt, and CasaXPS tab-delimited exports, detecting the format from the file's content rather than its extension, and maps each vendor's metadata onto the same schema. A PHI survey, for example, arrives with the flood-gun state that ISO 19318 asks you to record and that VAMAS usually omits:
The exported energy axis and intensities are preserved exactly. No vendor calibration, smoothing, or normalisation is applied at import, because anything applied here would be invisible to everything after it.
Inspection before interpretation
inspect-xps-spectrum answers one question: can this region support the interpretation you are about to put on it? It reports sampling, peak-to-background, counting statistics, saturation, and the acquisition conditions, and it locates prominent features as bare energies. It assigns nothing.
This is the step that catches the axis labelled kinetic when it is binding, the 1 eV-step survey someone is about to fit for chemical states, and the saturated channel that would turn into a phantom shoulder. Twelve features are located on this survey; the finding attached to them says plainly that they are positions, not assignments.
Element identification with tiers, not a list
identify-xps-elements matches the located features against a two-tier reference registry using spin-orbit doublet structure as the main discriminator. Lines with an experimental photoelectron reference (a CC-BY corpus of measured TMD, oxide, and carbon lines) are scored separately from atomic-fallback lines derived from X-ray absorption edges, and the output keeps the two apart.
The headline is honest about its confidence. assignment_status is suggested and stays that way; nothing in Atlas promotes an XPS element or state to accepted. The B 1s match is a good example of why: a single atomic-fallback line 1.3 eV off, with no doublet to corroborate it, is exactly the kind of "confirmed" that a domain expert should read as "look at this again."
Referencing the axis, with the receipt
Here is where most XPS software goes quiet. Binding energies from an insulating or floating sample shift with surface charging, often by several eV, and the literature values you want to compare against assume a Fermi-level reference. The correction is a rigid shift, and the shift is trivial. What is not trivial is recording what you shifted to, why, and how well you know it.
reference-xps-energy locates a named reference feature, shifts it onto a target, and writes the result as a new derived dataset. The raw import is never modified, a dataset that already carries a reference is refused so shifts cannot stack, and the record carries the reference label, its basis, the observed and target energies, the offset, a combined uncertainty, and whether charge compensation was recorded.
For this corpus the original authors referenced to the S 2p₃/₂ lattice line of 2H-MoS₂ at 161.95 eV. That is the right choice for a TMD when it is available: an internal lattice line does not have the vacuum-level problem that adventitious carbon does. Atlas ships it as a preset.
The aged re-measurement of the same sample lands at −0.041 ± 0.21 eV. In both cases the measured reference FWHM is about 1.5 eV, comfortably below the 2.5 eV threshold at which Atlas would warn that the feature is too broad for a rigid shift to be meaningful, which is the signature of differential charging.
Region scans from one acquisition share one charging state, so the S 2p offset is carried onto the Mo 3d region with method: manual. The Mo 3d derived dataset then records that its reference is an operator offset, with the offset's own uncertainty, rather than pretending it was measured on that region.
A constrained fit that says when it cannot tell
fit-xps-core-level fits declared components on a computed background. Nothing is auto-discovered. Every component has a label, a chemical reason for being in the model, bounds, and constraints, and spin-orbit partners are linked rather than free: position is the main line plus the splitting, area is the main line times the 2j+1 ratio, width is tied. The output carries every ISO 19830 reporting field.
The mos2_mo3d template declares the Mo 3d₅/₂ doublet of 2H-MoS₂, the S 2s line that sits under the low-binding-energy side of it, and optionally a Mo 3d₅/₂ doublet for MoO₃. The S 2s line is in the model because omitting it is how a lattice-sulfur line gets misread as a reduced Mo state.
Here is the fresh film, with the oxide component included:
That flag is the most useful thing on the page. A residual of 2 % of peak height with an MoO₃ share of 14 % is exactly what an unconstrained fitting package would print as a result. Atlas prints it as a warning and tells you what to do: add a constraint, tie a width, or drop a component.
Dropping the component is one parameter:
Now the same sample, same template, two years later:
Same fixture, same template, same background, same uniqueness test. On the fresh film the test says the oxide component is unsupported; on the aged film it says the oxide component is well determined. The difference between those two answers is the whole reason to constrain the fit and then check it.
Two things the aged fit is careful not to say. The MoO₃ component is 2.56 eV wide, much broader than the 0.83 eV lattice line, which is consistent with a disordered surface oxide or a mixture of Mo⁵⁺ and Mo⁶⁺ environments, but the template declares one state and the fit reports one state. And 51 % is an area fraction within the Mo 3d region, not an oxide thickness or an atomic percent. The Block's output labels it area_fraction and the docs say why.
The sulfur side of the story
The mos2_s2p template declares the lattice S 2p₃/₂ doublet and an optional oxidised-sulfur (SOₓ) doublet near 169 eV.
Read alongside the Mo 3d result, the picture is coherent and a little more interesting than "it oxidised." Half the molybdenum has gone to Mo⁶⁺ while only 8 % of the sulfur is sulfate. Surface oxidation of MoS₂ typically proceeds by sulfur loss with the oxide forming as MoOₓ rather than as sulfate, so a large MoO₃ fraction with a modest SOₓ fraction is the expected signature. Atlas does not draw that conclusion; it gives you two determined fits, with uncertainties and constraints attached, from which you can.
Other useful XPS workflows
- Fit a survey-derived offset onto every region. Reference once on the region with the cleanest internal line, then apply
method: manualwith that offset to the others. Each derived dataset records which it was. - Reference to a deposited marker or a Fermi edge. Presets for Au 4f₇/₂, Ag 3d₅/₂, and Cu 2p₃/₂ carry the ISO 15472 values and their 0.05 eV uncertainty;
fermi_edgefits a Fermi–Dirac step for a grounded metal. Adventitious carbon is available and its ±0.3 eV convention is written into the record. - Fit a kinetic-energy export. Scienta and some synchrotron exports are on a kinetic axis; the referencing step converts using the recorded source energy and work function, and flags when it had to assume φ = 0.
- Compare a transition-metal region to a multiplet model.
simulate-xps-atomic-multipletandsimulate-xps-charge-transferrun an EDRIXS solver for open-shell ions, andcompare-xps-to-multiplet-modeloverlays it on a measured region freeing only shift, scale, and baseline. - Track process drift with structured numbers. Referenced positions, FWHMs, area fractions, and uniqueness margins are Block outputs. Comparing them across anneal or storage conditions beats comparing screenshots of overlaid envelopes.
- Use the same chain on any vendor. PHI, SPECS, Scienta, and CasaXPS exports land on the same metadata schema as VAMAS, so a group with a mixed instrument fleet runs one workflow.
What Atlas leaves to you
Atlas will not turn an XPS envelope into atomic percent. Relative quantification needs a named sensitivity-factor set, a transmission correction, and compatible pass energies across regions, and that Block is next on the roadmap rather than shipped. It will not name a chemical state from a survey, promote a suggested assignment to accepted, or accept a fit whose components the data cannot distinguish. The generic peak fitter refuses XPS data entirely and points at this chain instead.
What it does is keep the acquisition metadata attached to every dataset, refuse to compute statistics the ordinate cannot support, record every energy shift with its basis and uncertainty, require every fitted component to have a reason, and test whether the answer is unique before reporting it. On the fresh film that produced "no resolvable oxide"; on the aged film it produced "51 % Mo⁶⁺, well determined." Both are answers you can defend, and the second one is only convincing because the first one was allowed to be negative.
The pieces are documented in Parse VAMAS XPS, Parse vendor XPS export, Inspect XPS spectrum, Identify XPS elements, Reference XPS energy, and Fit XPS core level. Every figure above was generated from the Zenodo VAMAS files through the same Atlas analysis functions the Blocks run. Data: I. Píš, F. Bondino, M. Sojková, "XPS and XANES spectra for Li doped MoS₂ few-layer films", Zenodo, doi:10.5281/zenodo.7709466, CC-BY-4.0.

