Visualising charge density and electrostatic potential
mace_scf.utils.field_interpolator provides helpers that take a set of
atom-centred GTO multipoles (either supplied directly or read off a trained
MACE_SCF calculator) and evaluate the resulting smeared charge density and
electrostatic potential on a user-specified set of sample points. This is
useful for plotting line cuts and slices of the electrostatic potential a
model has learned.
Two interfaces are provided:
PotentialInterpolator: stand-alone evaluator. You pass the atoms, the multipoles, an optional external field, and the sample points.PotentialInterpolatorPostProcessor: thin wrapper around an ASE calculator (e.g.MACEPolarizable) that pulls multipoles, external field, and Fermi level off the calculator’s lastresultsdict.
PBC restriction. Both interpolators currently assume periodic boundaries in \(x\) and \(y\) and open boundaries in \(z\) (a slab with \(z\) as the non-periodic axis). The constructor emits a warning to that effect, and
__call__raises ifatoms.pbc != [True, True, False].
Input Requirements
The direct PotentialInterpolator interface expects:
atoms: an ASEAtomsobject with Cartesian positions in Angstrom, a cell, andatoms.pbcexactly equal to[True, True, False];atomic_multipoles: a numpy array with shape(n_atoms, (multipoles_max_l + 1) ** 2);external_field: a length-3 numpy array using the same sign and units as the MACE_SCF calculator external-field inputs;fermi_level: a Pythonfloatscalar offset added to the potential;coords: Cartesian sample points in Angstrom, with shape(..., 3).
For multipoles_max_l=0, atomic_multipoles has one column containing
monopole coefficients. For multipoles_max_l=1, it has four columns:
one monopole coefficient and three dipole-like density coefficients in the
same ordering used by the model density coefficients. The ordering follows
the convention described in atomic multipoles.
The interpolator is intended for visualisation. The sigma value is the
Gaussian smearing width used for plotting the density and potential, and it
does not have to match the smearing used during model training. Smaller
values produce sharper features and usually need a larger kspace_cutoff or
kspace_cutoff_factor; larger values produce smoother line cuts and slices.
If subtract_total_charge=True, the net monopole charge is uniformly removed
from the supplied multipoles before the Fourier sum is evaluated. This is
useful for visualising nearly neutral slabs when small residual charge errors
would otherwise dominate the long-range potential.
Outputs
Each call returns three numpy arrays with the same leading shape as the
input coords (minus the trailing length-3 axis):
return value |
meaning |
|---|---|
|
smeared GTO charge density \(\rho(\mathbf r)\) |
|
electrostatic potential from the periodic Fourier sum alone (plus |
|
the same potential plus the slab dipole correction and the contribution of the external field, \(V_\mathrm{corr}(\mathbf r) = V(\mathbf r) + (E^\mathrm{corr}_z + E^\mathrm{ext}_z) z + \mathrm{const}\) |
The slab dipole correction is the standard \(E^\mathrm{corr}_z = (1/\varepsilon_0) p_z / V\) term that removes the artificial macroscopic field a periodic image of a charged or polarised slab would otherwise impose.
Using PotentialInterpolator directly
Given an Atoms object with a slab cell (PBC [T, T, F]) and a numpy array
of per-atom multipoles, e.g. from a previous calculation stored in
atoms.arrays:
import numpy as np
import torch
from copy import deepcopy
from mace_scf.utils.field_interpolator import PotentialInterpolator
torch.set_default_dtype(torch.float64)
interpolator = PotentialInterpolator(
sigma=1.0, # GTO smearing width (Angstrom)
multipoles_max_l=1, # 0 for monopoles only, 1 for monopoles + dipoles
kspace_cutoff_factor=1.5, # multiplies graph_longrange's heuristic
# (~1.0 is loose, ~2.0 is tight)
device="cpu",
subtract_total_charge=True, # uniformly subtract any residual net charge
# before evaluating the Fourier sum
)
# Atoms object: must have pbc exactly equal to [True, True, False].
atoms = ...
# Line of Cartesian sample points along z through (x=0, y=0), in Angstrom.
N = 100
z = np.linspace(-40.0, 40.0, N).reshape((N, 1))
coords = np.hstack([np.zeros_like(z), np.zeros_like(z), z])
# Multipoles attached to the Atoms object. Shape (n_atoms, (max_l + 1) ** 2),
# in the density-coefficient ordering documented in concepts/atomic_multipoles.md.
multipoles = deepcopy(atoms.arrays["atoms_in_molecules_atomic_multipoles"])
density, potential_uncorr, potential = interpolator(
atoms,
atomic_multipoles=multipoles,
external_field=atoms.info["homogeneous_field"], # shape (3,)
fermi_level=0.0, # Python float scalar offset
coords=coords,
)
Using PotentialInterpolatorPostProcessor with a model
If you have a trained MACE_SCF calculator, the post-processor wraps it so you don’t have to extract the multipoles, external field, and Fermi level yourself:
import ase.io
import numpy as np
from mace_scf.calculators.fixedpoint_scf import MACEPolarizable
from mace_scf.utils.field_interpolator import PotentialInterpolatorPostProcessor
config = ase.io.read("path/to/configs.xyz", index="-1")
calc = MACEPolarizable(
model_path="path/to/fit.model",
device="cpu",
external_field_key="external_field",
fermi_level_key="e_fermi",
ignore_nonconverged=False, # set True if you ran in fixed-step SCF mode
)
config.calc = calc
# Choose the external field for this evaluation.
config.info["external_field"] = np.array([0.0, 0.0, 0.05])
# Cartesian sample points: a line along z through (0, 0), in Angstrom.
N = 100
z = np.linspace(-40.0, 40.0, N).reshape((N, 1))
coords = np.hstack([np.zeros_like(z), np.zeros_like(z), z])
interpolator = PotentialInterpolatorPostProcessor(calc, sigma=2.0, device="cpu")
# Trigger an SCF / forward pass so calc.results is populated.
config.get_potential_energy()
density, potential_uncorr, potential = interpolator(config, coords)
The post-processor pulls density_coefficients, external_field, and
fermi_level out of calc.results and forwards them to a
PotentialInterpolator configured with the same multipoles_max_l and
kspace_cutoff as the model’s own coulomb_energy module. The only
hyperparameter you set is the smearing width sigma used for the
visualisation density (which can differ from the model’s smearing — a
larger sigma gives smoother line cuts).
Jellium variant
PotentialInterpolatorJellium is the analogue for models that include a
jellium background. It takes an additional slab_bounds=(z_lower, z_upper)
argument that locates the jellium region, and internally adds the smoothed
jellium counter-charge density to the Fourier series before evaluating the
potential. The call signature is otherwise identical to
PotentialInterpolator.