External Potential

conda-forge availability

Included in the conda-forge package. Always compiled in; no build flags required.

The external potential (ext_pot) interface allows eOn to use any energy and force calculator by communicating through files and a system call. Because it requires no compile-time dependencies, ext_pot is always available in every eOn build, including the conda-forge package.

Tip

If you installed eOn from conda-forge and want to use an MLIP (DeePMD, MACE, etc.) that is not available through the Metatomic interface, ext_pot or LAMMPS is the recommended path. The ASE potentials require additional compile-time flags that are not enabled in the conda-forge build.

Protocol

When the client needs an energy/force evaluation it:

  1. Creates a private exchange directory named extpot_<pid>_<n> beside the working directory, where <pid> is the client’s process id and <n> counts the potentials it built.

  2. Writes the file from_eon_to_extpot in that directory.

  3. Calls the executable (or script) specified by ext_pot_path via system(), with the exchange directory as the command’s working directory.

  4. Reads the file from_extpot_to_eon that the script must create there.

The two filenames do not change, so a wrapper that opens them by relative name, as every example below does, needs no edit. The exchange directory is what keeps two clients started in one directory from reading each other’s numbers; the client removes it once the exchange files are consumed, and leaves it in place when a call fails so its contents can be inspected.

Important

ext_pot_path is resolved against the directory eOn runs in when it names an existing file, so ./ext_pot and an absolute path both work. A command line with arguments (python wrapper.py) or a name looked up on PATH is passed to the shell unchanged and therefore has to be written so it resolves from the exchange directory: give the script an absolute path. On Windows, cmd.exe does not honor a shebang: a resolved script whose first line names python, or whose name ends in .py, is invoked as python <script>. A wrapper that needs to reach files in the directory eOn runs in finds that directory in the environment variable EON_EXTPOT_RUN_DIR.

Input file (from_eon_to_extpot)

The first three lines are the 3x3 box matrix (one row per line, tab-separated). Each subsequent line contains one atom: atomic_number  x  y  z (tab-separated, double precision).

box_00  box_01  box_02
box_10  box_11  box_12
box_20  box_21  box_22
6   1.234   5.678   9.012
6   ...

Output file (from_extpot_to_eon)

The first line is the total energy (scalar). Each subsequent line contains the force on one atom: fx  fy  fz (space-separated). The atom order must match the input.

-42.12345
0.123  -0.456  0.789
...

Configuration

[Potential]
potential = ext_pot
ext_pot_path = /full/path/to/your_wrapper

Important

When running compound jobs (aKMC, process search, etc.) eOn copies configuration into per-job scratch directories. Use absolute paths for ext_pot_path and for any model files referenced inside your wrapper.

Examples

DeePMD (PyTorch) wrapper

The following Python script wraps a DeePMD-kit v3 PyTorch model and can be used directly as the ext_pot_path target. Save it as, e.g., /home/user/scripts/deepmd_extpot and make it executable (chmod +x).

deepmd_extpot
#!/usr/bin/env python
"""eOn ext_pot wrapper for DeePMD-kit (PyTorch backend)."""
import numpy as np
from deepmd.infer import DeepPot

# --- user settings ---
MODEL = "/absolute/path/to/model.pth"
# ----------------------

dp = DeepPot(MODEL)

# Read input
lines = open("from_eon_to_extpot").readlines()
box = np.array([[float(v) for v in l.split()] for l in lines[:3]])
atoms = []
coords = []
for l in lines[3:]:
    tok = l.split()
    atoms.append(int(tok[0]))
    coords.append([float(tok[1]), float(tok[2]), float(tok[3])])
atoms = np.array(atoms)
coords = np.array(coords)

# Evaluate
energy, forces, _ = dp.eval(coords.reshape(1, -1, 3),
                             box.reshape(1, 9),
                             atoms)

# Write output
with open("from_extpot_to_eon", "w") as f:
    f.write(f"{energy[0]:.15f}\n")
    for fx, fy, fz in forces[0]:
        f.write(f"{fx:.15f} {fy:.15f} {fz:.15f}\n")

Generic ASE calculator wrapper

Any ASE-compatible calculator can be wrapped in a similar fashion:

ase_extpot
#!/usr/bin/env python
"""eOn ext_pot wrapper using an ASE calculator."""
import numpy as np
from ase import Atoms

# --- user settings ---
from mace_mp import MACECalculator
calc = MACECalculator(model="/path/to/model.pt", device="cuda")
# ----------------------

lines = open("from_eon_to_extpot").readlines()
box = np.array([[float(v) for v in l.split()] for l in lines[:3]])
numbers = []
positions = []
for l in lines[3:]:
    tok = l.split()
    numbers.append(int(tok[0]))
    positions.append([float(tok[1]), float(tok[2]), float(tok[3])])

system = Atoms(numbers=numbers, positions=positions, cell=box, pbc=True)
system.calc = calc

energy = system.get_potential_energy()
forces = system.get_forces()

with open("from_extpot_to_eon", "w") as f:
    f.write(f"{energy:.15f}\n")
    for fx, fy, fz in forces:
        f.write(f"{fx:.15f} {fy:.15f} {fz:.15f}\n")

Performance considerations

Each force call spawns a new process and re-initializes the calculator. For GPU-accelerated MLIPs this overhead is typically small compared to the inference cost, but for very fast potentials the startup cost can dominate. In that scenario consider:

  • The built-in ASE interface (requires building from source with -Dwith_python=True -Dwith_ase=True), which embeds the interpreter and avoids per-call process overhead.

  • The Metatomic interface for models that support the metatensor format (compiled into the conda-forge build).

  • The serve mode to expose any eOn potential over RPC to external callers.