Building phase diagrams from GCMC output
mcpy.utils.plot_phase_diagram() turns the trajectories of a
chemical-potential sweep into a phase diagram: it computes the formation energy
of every frame, takes the exact lower envelope of the free-energy lines, and
renders the figure with a numbered structure thumbnail per stable phase.
Build the diagram from a sweep
Collect one production slice per condition, keep one adsorbate-free frame as the clean reference (the function raises an error without one), and call the function:
from ase.io import read
from mcpy.utils import plot_phase_diagram
# keep frame 0 (the clean starting slab) as the reference
clean = read('sweep/dmu_-0.80/gcmc.xyz', index='0')
frames = [clean] + [
read(f'sweep/dmu_{dmu:+.2f}/gcmc.xyz', index='500:') # drop equilibration
for dmu in (-0.8, -0.6, -0.4, -0.2, 0.0)
]
results = plot_phase_diagram(
frames,
adsorbate='O',
metal_symbols=('Ag',),
mu_ref=mu_o_ref, # E(O2)/2 on your calculator's scale (derive_mu_gas)
kind='surface', # or 'nano' for nanoparticles
T=500.0,
dmu_range=(-1.0, 0.0),
outfile='phase_diagram.png',
)
print(results['transitions']) # Delta mu values of the phase boundaries
frames takes a flat list of Atoms or a list of trajectories and
flattens them; frames written by mcpy carry their energy on the comment line,
so nothing is re-evaluated.
If the runs also exchange metal atoms, pass mu_metal (see below).
Without it, the function raises an error when the frames hold different
metal counts.
adsorbate, metal_symbols and mu_ref have no default values.
Give them as keyword arguments.
Frames without a finite energy are skipped.
The returned dict carries the grid (dmu_grid), one line per adsorbate
count (free, a row for each entry of stoich), the envelope
(min_gamma), and the boundary positions (transitions).
stable_idx (per grid point) and phase_order (per stable phase, from
left to right) index stoich, not frames.
For example, [results['stoich'][i] for i in results['phase_order']]
gives the adsorbate counts of the stable phases.
A surface diagram: O on a 3x3 Ag(111) slab at 500 K, MACE-MH-1
(phase_diagram notebook).
The y axis is the surface free energy \(\gamma\) in meV/Ų.
Grey lines are the lowest-energy frames of the other O counts, the thick
black line is the stable envelope, and the numbered regions are the
stable phases (0 to 4 O atoms, shown in the thumbnails).
Only rungs with \(\Delta\mu_{\mathrm{O}} \le -0.30\) eV, which
reached a coverage plateau, enter the diagram.
Above that the slab keeps oxidising, so those frames are not
equilibrium surface phases.
Above -0.30 eV the envelope extrapolates the lines of those frames.
The hatched range (hatch_from) marks
\(\Delta\mu_{\mathrm{O}} > -0.365\) eV, where bulk
Ag2O is more stable than any surface phase.
The top axis converts \(\Delta\mu_{\mathrm{O}}\) into O2
pressure.
It puts \(p = p^0\) at
\(\Delta\mu_{\mathrm{O}} = \Delta\mu^0_{\mathrm{O}}(T)\), which is
-0.50 eV at 500 K.
The notebook writes this figure with one plot_phase_diagram call.
Key parameters
kind'surface'normalizes by the in-plane cell area (meV/Ų);'nano'normalizes by the metal-atom count (meV/atom), which keeps particles of different sizes comparable.gamma_in_ev=Trueskips the normalisation.mu_refThe adsorbate reference on your calculator’s energy scale, for example half the O2 energy. The twin pressure axis converts \(\Delta\mu\) at the given
T(seedmu_standard).mu_metalThe chemical potentials of the metal species, as a dict, for example
{'Ag': mu_ag}. Use the values of the GCMC runs. Pass it when the runs exchange metal atoms, for example O and Ag on Ag(111) (examples/03_gcmc_surface_mace.pywithEXCHANGE_AG = True). Each frame then scores \(E - \sum_m \mu_m (N_m - N_{m,\mathrm{ref}})\). The clean frame with the lowest score is the reference. For each adsorbate count, the frame with the lowest score is the phase.results['phase_metal_counts']gives the metal counts of each phase. The function raises an error if the frames hold different counts of a metal species andmu_metaldoes not give it.dmu_standardThe standard chemical potential \(\Delta\mu^0(T)\) of the adsorbate at \(p^0\), in eV per adsorbed unit. The twin axis shows \(\log_{10}(p/p^0) = n\,(\Delta\mu - \Delta\mu^0(T)) / (k_B T \ln 10)\), with \(n\) =
atoms_per_reservoir_molecule. For O from O2 and H from H2, the defaultNonetakes \(\Delta\mu^0(T)\) from built-in NIST-JANAF tables (100 to 1000 K). These tables use the convention of Reuter and Scheffler (PRB 65, 035406, 2001): the reference is the total energy of the molecule, without zero-point energy. For O, they agree with Table I of that paper within 0.006 eV. The axis is exact only ifmu_refincludes \(\mathrm{ZPE}/n\), with ZPE the zero-point energy of the molecule. With a baremu_ref, for example fromderive_mu_gas, the true \(p = p^0\) is at \(\Delta\mu^0(T) + \mathrm{ZPE}/n\). This shift is 0.049 eV for O (1.0 decade at 500 K) and 0.135 eV for H (4.5 decades at 300 K). To show the true gas pressure with a baremu_ref, passdmu_standard= \(\Delta\mu^0(T) + \mathrm{ZPE}/n\). For any other adsorbate, for example CO, give the value yourself. Also give it for aToutside 100 to 1000 K. If you do not, the function omits the pressure axis and logs a warning. The phases and transitions do not change.n_bins/min_phase_widthResolution of the returned \(\Delta\mu\) grid (
dmu_grid,free,min_gamma), and the width below which a phase gets its number on a second row of the plot. Neither changes the phases: every phase on the envelope is kept, and the transitions are the exact crossings of the free-energy lines, so the dashed lines sit on the kinks of the envelope.hatch_from/hatch_labelHatch the range \(\Delta\mu >\)
hatch_from, with a legend entry, for example where the bulk oxide is more stable than any surface phase.dpiResolution of the saved figure (default 250). Neighbouring phases always get different colours from a colourblind-safe palette, and the number on each region is also on its thumbnail.
adsorbate_count_fn/adsorbate_labelA custom
atoms -> intcounter for adsorbates whose element also sits in an inert sublattice (for example O adsorbed on a particle supported by an oxide), and the species name used in axis labels when the counted symbol is a proxy.atoms_per_reservoir_moleculeReservoir stoichiometry \(n\) for the pressure axis. The default
2is the dissociative-diatomic convention (atomic O from an O2 reservoir); pass1for molecular adsorbates such as CO, whose thumbnails then read as (CO)n.
A molecular nanoparticle diagram: CO on Cu33Pd5 at
400 K, kind='nano' (\(\Delta G\) in meV per metal atom) and
atoms_per_reservoir_molecule=1.
The numbered regions are the stable (CO)n phases,
n = 0, 1, 2, 3, 5 and 6; phases 2 and 4 are narrow (0.009 and 0.02 eV),
so their numbers sit on a second row.
The run is not converged above -1.45 eV (phase 6).
The top axis of this figure is from an older version: it puts
\(p = p^0\) at \(\Delta\mu = 0\).
The true pressures are 7.6 decades higher.
The notebook now passes dmu_standard=-0.600 (the NIST-JANAF
\(\Delta\mu^0_{\mathrm{CO}}\) at 400 K plus the CO zero-point
energy), so the axis is correct when the notebook runs again.
analyze_phase_diagram_results(trajectory_path, host_symbol, oxygen_symbol, idx_ref, ...)The older oxidation-specific entry point: reads one concatenated trajectory from disk and works against tabulated host and oxide reference energies. Prefer
plot_phase_diagramfor new work.
Worked examples
Section 7 of From a clean slab to an oxidation phase diagram builds the O/Ag(111) diagram from the tutorial sweep.
The phase-diagram notebook builds a diagram interactively with executed output (see Notebooks).
The CO/CuPd replica-exchange notebook ends in the molecular nanoparticle diagram above.