pyeonclient (Python client API)

Added in version 2.16: In-process client: first-class algorithm objects on :class:~pyeonclient.Matter, not a working directory and not the eonclient binary alone.

pyeonclient exposes the eOn C++ client algorithms to Python with nanobind. Geometries are Matter. Each method is a live class you construct and call — the same engines the binary uses.

Algorithm

Python class / function

Binary analogue

Local min

Matter.relax()

job = minimization

Min-mode

Dimer, ImprovedDimer, Lanczos, Davidson

dimer / lanczos / davidson

Saddle search

MinModeSaddleSearch

job = saddle_search

NEB

NudgedElasticBand

job = nudged_elastic_band

Hessian

Hessian

job = hessian

Prefactor

get_prefactors

job = prefactor

Dynamics

run_dynamics

job = dynamics

Monte Carlo

run_monte_carlo

job = monte_carlo

Basin hopping

run_basin_hopping

job = basin_hopping

Process search

process_search

job = process_search

Structure compare

structures_equal / structure_distance

job = structure_comparison

TAD

run_tadTADJob

job = tad

Parallel replica

run_parallel_replicaParallelReplicaJob

job = parallel_replica

Safe hyperdynamics

run_safe_hyperdynamicsSafeHyperJob

job = safe_hyperdynamics

Replica exchange

run_replica_exchangeReplicaExchangeJob

job = replica_exchange

GP surrogate NEB

run_gp_surrogate_nebGPSurrogateJob (gated)

WITH_GP_SURROGATE

Opaque batch

make_job + Job.run

any JobType via factory

make_job(params) still constructs every JobType the C++ factory supports (process search, dynamics, basin hopping, TAD, …) for workdir-oriented runs. Prefer the first-class classes when you want Matter in / Matter out.

Install

pip install pyeonclient
pip install 'pyeonclient[ase]'
pip install 'rgpot>=2.4.2'   # multi-ABI engines for Metatomic via RGPOT

Typed Dimer/NEB specs come from the shared eon-schema package (pip install 'pyeonclient[models]'eon-schema>=0.2). Job-config pydantic models used in docs live there too (eon.schema is a re-export for eon-akmc).

Metatomic force backends

Added in version 2.17.

Named force factories live in pyeonclient.backends (make_backend / list_backends). Fat Metatomic, RGPOT libmetatomic_engine.so, and ASE MetatomicCalculator all produce a pyeonclient Potential for the same .pt model. See Metatomic backends for the API, packaging notes, and a PET-MAD single-point benchmark plot.

Mutation policy (inplace)

Algorithms that change geometry do not mutate caller-owned Matter by default. Pass inplace=True to write into the input object.

out, ok = matter.relax()                 # copy-then-relax; matter unchanged
out, ok = matter.relax(inplace=True)     # mutates matter

saddle, status = pyec.min_mode_saddle_search(
    matter, mode, E0, params, pot)       # matter unchanged
saddle, status = pyec.min_mode_saddle_search(
    matter, mode, E0, params, pot, inplace=True)

Same for run_dynamics, run_monte_carlo, run_basin_hopping, process_search, run_tad, run_parallel_replica, run_safe_hyperdynamics, and run_replica_exchange. There are no silent mutators: every algorithm takes inplace=False by default.

out = pyec.TAD(matter, params, pot).run(matter, params, pot)                 # TADJob; matter unchanged
out = pyec.run_parallel_replica(matter, params, pot)    # ParallelReplicaJob
out = pyec.run_safe_hyperdynamics(matter, params, pot)
out = pyec.run_replica_exchange(matter, params, pot)
out = pyec.TAD(matter, params, pot).run(matter, params, pot, inplace=True)   # updates matter

Shared setup

Matter-first (preferred for new code). One Parameters is shared with make_backend so pot type and job knobs stay consistent:

import numpy as np
from pyeonclient import Matter, Parameters
from pyeonclient.backends import make_backend

params = Parameters()
params.opt_converged_force = 0.01
params.opt_max_iterations = 1000
pot = make_backend("lj", params=params)   # or "metatomic", "ase_metatomic", …

matter = Matter(pot, params)
matter.resize(n)
matter.positions = ...                    # (n, 3) float64
matter.cell = np.eye(3) * L
matter.atomic_numbers = ...
matter.masses = ...
# or: from pyeonclient import from_ase; matter = from_ase(atoms, pot, params)

INI / PotType path (eonclient workdir parity) still works::

params = load_parameters("config.ini")
pot = make_potential(params.potential, params)

Minimization

out, ok = matter.relax()                  # like LBFGS(atoms).run(fmax=...)
print(ok, matter.potential_energy, matter.max_force)

Min-mode (Dimer)

params.dimer_rotations_max = 10
params.dimer_opt_method = "cg"            # cg | lbfgs | sd

direction = np.random.default_rng(0).normal(size=matter.positions.shape)
# Default method is "improved"
d = pyec.Dimer(matter, params, pot)
# d = pyec.Dimer(matter, params, pot, method="classic")
# d = pyec.Dimer(matter, params, pot, method="improved", accelerant="gp")  # WITH_GPRD
d.compute(matter, direction)
print(d.eigenvalue, d.eigenvector)

Low-level engines remain: ClassicDimer, ImprovedDimer, Lanczos, Davidson. Prefer Dimer(method=..., accelerant=...).

Parameters: dimer_improved, dimer_rotation_angle, dimer_converged_angle, dimer_max_iterations, dimer_opt_method, dimer_rotations_max / _min, dimer_torque_max / _min, dimer_remove_rotation.

NEB

Use eOn-native path initializers (IDPP / SIDPP / linear). Do not pull ASE interpolate("idpp") into a pyeonclient band — the same engines live in helpers::neb_paths and are already what the endpoint NEB constructor uses.

Typed knobs live on :class:~pyeonclient.NebSpec. Compose the same steps as a workdir job without a package alias:

from pyeonclient import (
    NEB,
    NebSpec,
    Parameters,
    PathInit,
    append_timing,
    from_ase,
    pot_registry_total_force_calls,
    steady_clock_now,
    write_neb_results,
    write_potcall_summary,
)
from pyeonclient.backends import make_backend

params = Parameters()
spec = NebSpec(
    n_images=10,
    path_init=PathInit.idpp,
    energy_weighted=True,
    ci_mmf=True,
    max_iterations=1000,
    force_tolerance=0.01,
)
spec.apply_to_parameters(params)
pot = make_backend("metatomic", model_path="pet-mad.pt", params=params)
initial = from_ase(reactant, pot, params)
final = from_ase(product, pot, params)

t0 = steady_clock_now()
f0 = pot_registry_total_force_calls()
neb = NEB(initial, final, params, pot, spec=spec)
status = neb.compute()
if status.name == "GOOD":
    neb.find_extrema()
write_neb_results(neb, params, pot_registry_total_force_calls() - f0)
write_potcall_summary("_potcalls.json")
append_timing("results.dat", t0)

Explicit path object still available::

path = neb_idpp_path(initial, final, n_intermediate=10, parameters=params)
band = NudgedElasticBand(path, params, pot)
band.compute()

Helpers: neb_linear_path, neb_idpp_path, neb_idpp_collective_path, neb_sidpp_path, neb_initial_path. See Nudged Elastic Band for energy-weighted springs and OCINEB (neb_ci_mmf, …).

GP-surrogate NEB is still the same NEB idea with a GP accelerant on the band engine (WITH_GP_SURROGATE), not a separate product noun — and not prefactor / Parallel Replica.

Hessian and prefactors

atoms = pyec.all_free_atoms(matter)         # int64 indices
hess = pyec.Hessian(params, matter)
H = hess.get_hessian(matter, atoms)       # (3n, 3n)
freqs = hess.get_freqs(matter, atoms)

pref1, pref2 = pyec.get_prefactors(params, min1, saddle, min2)

Parameters: hessian_atom_list, hessian_zero_freq_value, prefactor_rate (htst / qqhtst), prefactor_filter_scheme, prefactor_within_radius, …

Job factory (full JobType surface)

Every type in JobType that the C++ makeJob factory implements is available as a workdir-oriented job:

params.job = pyec.JobType.Saddle_Search     # or Process_Search, Dynamics, …
job = pyec.make_job(params)                 # reads cwd files like eonclient
files = job.run()
pyec.write_potcall_summary()

Use this for process search, dynamics, basin hopping, TAD, parallel replica, Monte Carlo, GP surrogate, structure comparison, etc., until those gain dedicated Matter-first classes. The algorithms above are already first-class.

ASE bridge

Geometry (always array-copy, fast)::

matter = pyec.from_ase(atoms, pot, params)
atoms = pyec.to_ase(matter)

Seamless calculator — wrap a live ASE Calculator as the Matter PEF (no file script, no -Dwith_ase)::

from ase.calculators.emt import EMT

atoms.calc = EMT()
matter = pyec.from_ase(atoms)                 # uses atoms.calc
# or
pot = pyec.potential_from_ase(atoms.calc)
matter = pyec.from_ase(atoms, pot, params)

Requires pip install 'pyeonclient[ase]' and ASE installed at runtime. Not thread-safe across images (one calculator object).