GCMC acceptance: the de Broglie particle count
This note records a deliberate, non-obvious convention in the grand-canonical acceptance test, the discussion behind it, and the change history. It exists so the choice is not silently “fixed” by a future contributor who (correctly) notices it departs from the textbook form.
The acceptance criterion
Insertion and deletion are accepted with the de Broglie (grand-canonical)
probabilities (see mcpy.ensembles.GrandCanonicalEnsemble and
mcpy.utils.SetUnits):
V is the cell free volume, \(\Lambda\) the thermal de Broglie
wavelength of the exchanged species, \(\mu\) its chemical potential, and
N the particle count fed to the combinatorial factor. The question this
note settles is: which ``N``?
Two conventions for N
- total atom count (the convention mcpy uses)
N = len(atoms)before the move: every atom of every species, including the fixed substrate. This is what_commit_or_rollbackpasses today (self.n_atoms).GrandCanonicalEnsembleandBatchedReplicaExchangeboth score their trials with it.- per-species exchangeable count (textbook)
N= number of atoms of the exchanged species inside the insertion region (the move’s cell). This is the physically standard choice.
The physically correct quantity is the per-species count. The de Broglie
factor is the ideal-gas combinatorial term: the \(1/N!\) indistinguishability
correction applies only among identical, interchangeable particles, and only to
the population that shares the insertion volume V. The total atom count
mixes in distinguishable species and the fixed substrate, which do not belong in
that factor. The interaction with all those atoms enters the acceptance through
\(\Delta E\) (the total potential energy change), not through N.
Two related conventions to keep in mind:
muis the full chemical potential, including the ideal/\(\Lambda\) term. Somumust be referenced on the calculator’s absolute energy scale (e.g. an O2 gas reference at the targetTandp).Vis the cell free volume, and insertion points are drawn uniformly over that same free volume. The deletion uses the free volume of the state after the deletion. See The volume V below.
The volume V
Detailed balance between an insertion and its reverse deletion needs one
condition on V.
The insertion proposal density must be exactly \(1/V\), and the
deletion must use the same V.
Both refer to the state without the exchanged particle.
mcpy meets this condition as follows:
One exclusion rule, defined in
mcpy.cell.Cell. A point is excluded when it lies withinspecies_radii[symbol]of any atom. The rule includes every atom, also atoms outside the region (for example the slab below theCustomCellfloor). It also includes the periodic images of the atoms along every periodic direction.The free-volume estimate (
Cell.free_volume) counts the uniform region points that the rule does not exclude.The insertion proposal (
Cell.get_random_free_point) redraws uniform region points until the rule does not exclude the point. The result is uniform over the free volume, so its density is \(1/V\).MoleculeInsertionMoveapplies the rule to the center of mass only. The energy handles overlaps of the other member atoms.The insertion uses the cell volume that the ensemble computes for the current (pre-insertion) state.
DeletionMoveandMoleculeDeletionMovecompute the free volume of the trial state after the deletion, and report it throughget_volume. Before ADR 0003 the deletion used the volume of the state that still held the deleted particle. That volume is too small by the particle’s own exclusion sphere, which biased the deletion acceptance.A cell without
species_radiiexcludes nothing. Its volume is the exact region volume and the proposal is the plain uniform draw. The periodic boxCelltherefore keeps its previous behaviour and its previous random streams (the LAMMPS-parity runs do not change).
min_insert sets a seed clearance for relaxed insertions (ADR 0004).
With it, the proposal keeps min_insert from every atom, inside the
species_radii spheres too, and the acceptance still uses the
species_radii free volume.
The relaxation moves the seed into that free volume.
Under unrelaxed trials this mismatch is a bias: it shifts \(\mu\) by
\(k_BT \ln(V_\mathrm{free}/V_\mathrm{seed})\).
Leave min_insert unset when the calculator does not relax.
GrandCanonicalEnsemble warns (UserWarning) when a move sets
min_insert and the calculator has no relaxation steps.
Remaining limits:
Vis a Monte Carlo estimate fromnuniform points. Its relative noise is about \(\sqrt{(1-f)/(f\,n)}\) for a free fractionf.mc_sample_pointsis the maximumn. The first estimate uses all of them, and each later estimate picksnfrom the previousfso that the noise is about 0.5%.The rule is a hard core only for proposals. A displacement or a relaxation can move a particle into an exclusion sphere. A deletion can leave such a state, but an insertion cannot enter it. The sampling is exact only when the exclusion radii are also hard cores of the potential. For molecules the rule tests the center of mass only. An insertion can put an atom of molecule B within
rof the center of mass of molecule A, and then the deletion of A has no reverse insertion. So for molecules the potential must forbid any atom withinrof the center of mass of another molecule. Between atoms, that is a hard core ofrplus the largest distance of a member atom from its center of mass. This holds even without displacement moves or relaxation.With a relaxing calculator the sampling is basin-level (ADR 0002), and the effective chemical potential differs from
mu: see Relaxed trials shift the effective mu.Periodic images are taken one box length in each direction, so every radius must be shorter than the box.
Decision records: docs/adr/0003-insertion-proposal-matches-free-volume.md
and docs/adr/0004-min-insert-is-a-relaxation-seed-clearance.md.
Relaxed trials shift the effective mu
With relaxed trials (ADR 0002), every insertion point in the catchment of
a basin j relaxes into that basin.
The catchment volume \(v_j\) is the part of the free volume V
whose points relax into j.
Assume that a deletion from \(S+j\) relaxes back to \(S\).
Detailed balance between the two states then gives
\(N_s\) is the number of deletion candidates of the species in
\(S\), and N is the count of the acceptance rule.
For atoms N is the total atom count; for molecules \(N = N_s\),
and the two counts cancel.
V cancels too: the proposal density \(1/V\) and the acceptance
factor V remove each other.
A phase diagram or a computational hydrogen electrode (CHE) analysis gives each state the weight \(e^{\beta(\mu - \varepsilon_j)}\) instead. Relative to that axis, the run samples at \(\mu + \delta\) with
The first term is positive for any catchment larger than \(\Lambda^3\). The second term is negative with the total atom count, and zero for molecules.
Worked numbers, for O on the Ag(111) slab of From a clean slab to an oxidation phase diagram
(4x4x3 slab, a 5 Å CustomCell that starts 0.5 Å above the top layer,
radii Ag 2.14 Å and O 0.5 Å, 500 K):
Vis about 442 Å3. If the 32 hollow sites (fcc and hcp) share it equally, \(v_j \approx 14\) Å3.\(\Lambda_\mathrm{O} = 0.195\) Å, so the volume term is +0.32 eV.
With
N= 48 + \(N_\mathrm{O}\), the count term is -0.17 eV at \(N_\mathrm{O} = 0\), -0.08 eV at 8 and -0.06 eV at 16.So \(\delta\) is +0.16 eV on the clean slab, +0.24 eV at 8 O and +0.27 eV at 16 O.
For H on the Pt55 particle of examples/05_pt_nanoparticle_h_che.py
at 300 K, \(\Lambda_\mathrm{H} = 1.0\) Å.
The volume term is then small (+0.04 eV for \(v_j = 5\) Å3),
and the count term is -0.10 eV on the bare particle.
\(\delta\) is not a constant.
It depends on the cell height and species_radii (through \(v_j\)),
on the temperature (through \(k_BT\) and \(\Lambda\)), and on the
optimizer and its settings (which seeds reach which basin).
With the total atom count it also depends on the coverage and on the size
of the substrate.
Thus the \(\Delta\mu\) (or the electrode potential U) of a run or
of a replica-exchange rung is a nominal value.
Do not read a phase boundary from the rung where the coverage jumps.
Read it from plot_phase_diagram (Building phase diagrams from GCMC output), which
computes the free energy of each frame from its energy and composition
only, on the \(e^{\beta(\mu - \varepsilon)}\) axis.
Why mcpy keeps the total atom count
Reproducibility. The group’s entire body of GCMC work (including the forthcoming Ag-oxidation study) was produced with the total-atom-count convention. Switching to the per-species count shifts the effective chemical potential by
which for a few-thousand-atom substrate with tens of exchanged atoms is
~0.1-0.2 eV toward insertion at typical temperatures. Concretely, after the
per-species change an oxide phase boundary moved from roughly \(\Delta\mu
\approx -0.5\) eV to \(\approx -0.3\) eV, and oxygen kept inserting well into
the reducing regime. Keeping the original convention keeps every simulation on
one consistent footing; the per-species count would require re-running and
re-calibrating mu across all prior work.
This is a reproducibility choice, not a claim that the total count is more
correct. If you start a fresh study with no need to match prior runs, prefer the
per-species count (and recalibrate mu against a gas reference).
Side effect that the total count happens to mask
With the per-species count there is a runaway failure mode for species that
dissolve below the cell floor. CustomCell.get_atoms_specie_inside_cell
excludes atoms with z < bottom_z (subsurface, “absorbed”), so such atoms are
neither counted nor eligible for deletion. Under the per-species convention this
undercount inflates \(V/((N+1)\Lambda^3)\) and insertion stays favored no
matter how negative mu is - one-way accumulation decoupled from mu.
The total-atom-count convention sidesteps this: as oxygen accumulates anywhere
(surface or subsurface), N_total grows, which suppresses further insertion.
The system is self-limiting. The buried-O irreversibility still exists (buried O
is never a deletion candidate), but it no longer drives runaway uptake.
Change history
668e732/ earlier - both ensembles born using the total atom count.adcc15b- switchedGrandCanonicalEnsembleto the per-species count.9a15425- switchedBatchedReplicaExchangeto the per-species count.b97361f- reverted both back to the total atom count for reproducibility, and recorded the discussion here (8eabe76).ADR 0003 - the free volume
Vbecame one rule for insertion proposals, insertion acceptance and deletion acceptance (section The volume V).ADR 0004 -
min_insertcame back as a seed clearance for relaxed insertions; the acceptance keeps thespecies_radiifree volume.
Where this lives in the code and tests
mcpy/ensembles/grand_canonical_ensemble.py-_commit_or_rollbackpassesself.n_atomsto_acceptance_conditionfor atomic moves.mcpy/ensembles/batched_replica_exchange.py-_batched_single_movecalls the same_commit_or_rollbackon each replica.tests/test_acceptance.py-test_do_gcmc_step_feeds_total_atom_countguards the convention;test_total_vs_perspecies_count_shifts_effective_muandtest_perspecies_count_inserts_where_total_rejectsquantify the gap.tests/test_audit_2026_10.py: thetest_p1_*tests guard the volume rule, including a two-state detailed-balance check of the full GCMC loop (test_p1_two_state_gcmc_obeys_detailed_balance).tests/test_custom_cell_region.py-test_subsurface_oxygen_is_excluded_from_deletion_candidatesdocuments the buried-O caveat.
Molecular moves
Rigid-molecule insertion/deletion (mcpy.moves.MoleculeInsertionMove,
mcpy.moves.MoleculeDeletionMove) do not use the total-atom
convention above. They use the textbook rigid-molecule form: N is the
number of molecules of the exchanged species whose center of mass lies inside
the move’s cell, reported by the move itself through
MoveSelector.get_exchange_count(). \(\Lambda\) is computed from the
total molecular mass, and delta_particles remains \(\pm 1\) (one
molecule per move).
The acceptance has no rotational factor, and orientations are drawn uniformly. Uniform orientations do not supply the rotational partition function \(q_\mathrm{rot}\) of the reservoir. Which \(\mu\) to pass depends on the trial energies:
Relaxed trials (the default): pass the full thermochemical chemical potential, \(\mu = E_\mathrm{mol} + \Delta\mu(T, p)\), with the translational, rotational and internal terms of the gas. The relaxation moves each seed into a basin, so the orientation of the seed matters little, and the full \(\mu\) is a good approximation for adsorbates. It is an approximation, not an exact rule (see Relaxed trials shift the effective mu).
Single-point trials (
steps=0or a calculator that does not relax): pass \(\mu = E_\mathrm{mol} + k_BT \ln(p\,\Lambda^3 / k_BT)\), with \(\Lambda\) from the total molecular mass and no \(q_\mathrm{rot}\). This is the convention of LAMMPSfix gcmc. The full thermochemical \(\mu\) would be too low by \(k_BT \ln q_\mathrm{rot}\).
Atomic moves are unaffected: for them get_exchange_count() returns
None and the ensembles fall back to the total-atom count documented
above.
Because molecular moves do use the per-species count, they are exposed to
the runaway mode described in the previous section: any escape route out of
the counted region undercounts N and inflates
\(V/((N+1)\Lambda^3)\). Molecule candidacy therefore goes through
cell.is_point_exchangeable(com), not cell.is_point_inside(com). The
two differ for mcpy.cell.CustomCell, whose exchangeable region drops
the z upper bound exactly as get_atoms_specie_inside_cell already does for
single atoms, so a molecule that relaxation pushes above the cell top stays
both countable and deletable. The floor exclusion is kept on both paths: molecules
that sink below bottom_z remain excluded, and the buried-species
irreversibility noted above applies to them unchanged.
mcpy.moves.MoleculeDisplacementMove picks its candidates with
cell.is_point_inside(com) and rejects a displacement whose new center of
mass fails the same test.
So the region it picks from and the region it moves within agree, and for a
CustomCell the top is a hard wall: no insertion can put a molecule
above it.
V in the molecular acceptance is the cell free volume, not the full
cell volume.
The center of mass is drawn uniformly over that free volume, and the
deletion uses the free volume after the molecule is removed (see
The volume V).
Atomic and molecular species of the same element can coexist (e.g. atomic O
alongside O2 molecules): an atomic mcpy.moves.InsertionMove tags its
inserted atom as free (molecule_id = -1, guarding against ASE’s zero-fill
when extending arrays), and an atomic mcpy.moves.DeletionMove only
picks free atoms, never molecule members. Displacement-type moves are not
molecule-aware: they can drag a member atom individually, which is
thermodynamically valid with a relaxing calculator but departs from the
rigid-molecule picture.
The MPI mcpy.ensembles.ReplicaExchange does not support molecular
species either: its per-species swap bookkeeping counts atoms by symbol
(atoms.symbols.count(specie)), which is always 0 for a molecular name, so
its constructor raises NotImplementedError when the wrapped GCMC ensemble
has molecular species configured; mcpy.ensembles.BatchedReplicaExchange
is the supported replica-exchange path for molecular GCMC.