Using ANN Potentials with ænet’s Fortran Binaries

Note

Predictions/inference as described here make use of ænet’s compiled predict.x tool. Make sure to install ænet and configure the paths as described in Installation & Set-up.

Note

Alternative: For a pure Python/PyTorch implementation that does not require Fortran, see Using ANN Potentials with the PyTorch Implementation.

aenet-python provides tools to to use ænet potentials for energy and force predictions directly from Python scripts. These features are implemented primarily by the ANNPotential class.

Loading Existing Potentials

The easiest way to use pre-trained potentials is with the from_files() factory method:

from aenet.mlip import ANNPotential

# Load binary format potentials
potential_paths = {
    'Ti': 'Ti.nn',
    'O': 'O.nn'
}
potential = ANNPotential.from_files(potential_paths)

# Load ASCII format potentials
potential_paths = {
    'Ti': 'Ti.nn.ascii',
    'O': 'O.nn.ascii'
}
potential = ANNPotential.from_files(
    potential_paths,
    potential_format='ascii'
)

This creates an ANNPotential instance configured for prediction-only use.

Note that architecture information cannot be extracted from the potential files, so this instance cannot be used for re-training.

Right after Training

If you just finished training in the same session, the potential automatically knows where the .nn files are located:

# Train the potential
arch = {'Ti': [(10, 'tanh'), (10, 'tanh')],
        'O': [(10, 'tanh'), (10, 'tanh')]}
potential = ANNPotential(arch)
potential.train(trnset_file='data.train', ...)

# Predict immediately - no need to specify potential paths
results = potential.predict(structures, eval_forces=True)

Making Predictions

The predict() method is the main interface for inference. It accepts either file paths to XSF structures or AtomicStructure objects:

from aenet.geometry import AtomicStructure
from aenet.mlip import ANNPotential, PredictionConfig

# Load potential (format set at load time)
potential = ANNPotential.from_files(
    {'Ti': 'Ti.nn', 'O': 'O.nn'}
)

# Predict from file paths - format remembered from from_files()
results = potential.predict(['structure1.xsf', 'structure2.xsf'])

# Or from AtomicStructure objects
structures = [AtomicStructure.from_file(f) for f in xsf_files]
results = potential.predict(structures)

Evaluating Forces

Forces can be computed by setting eval_forces=True:

results = potential.predict(
    structures,
    eval_forces=True
)

# Access forces
for i in range(results.num_structures):
    print(f"Structure {i}: forces shape = {results.forces[i].shape}")

Prediction Configuration

The PredictionConfig class centralizes prediction parameters with built-in validation:

  • potential_paths (Dict[str, str], optional): Mapping of element symbols to potential file paths. If None, uses paths stored after training. Default: None

  • potential_format (str, optional): Format of potential files: 'ascii' or None (binary). If 'ascii', predict.x will automatically convert to binary. Default: None

  • timing (bool): Enable detailed timing output. Default: False

  • print_atomic_energies (bool): Print per-atom energy contributions. Default: False

  • debug (bool): Enable debug output. Default: False

  • verbosity (int): Verbosity level (0=low, 1=normal, 2=high). Default: 1

Example usage:

from aenet.mlip import PredictionConfig

config = PredictionConfig(
    potential_format='ascii',       # ASCII format potentials
    verbosity=2,                    # High verbosity
    timing=True,                    # Enable timing
    print_atomic_energies=True      # Print per-atom energies
)

results = potential.predict(
    structures,
    eval_forces=True,
    config=config
)

Working with Prediction Results

The PredictOut class provides convenient access to prediction results:

# Access energies and forces
cohesive_energies = results.cohesive_energy   # eV
total_energies = results.total_energy         # eV
forces = results.forces                       # eV/Å

# Get number of structures and atoms
n_structures = results.num_structures
n_atoms = results.num_atoms(0)  # number of atomis in structure 0

Converting to AtomicStructure

You can convert prediction results back to AtomicStructure objects with energies and forces attached:

# Convert results to structures
structures_with_predictions = results.to_structures()

# Access energy and forces from structure
struc = structures_with_predictions[0]
energy = struc.energy[-1]  # Latest energy
forces = struc.forces[-1]  # Latest forces

Batch Predictions

You can efficiently predict multiple structures at once:

import glob

# Get all structure files
xsf_files = glob.glob('structures/*.xsf')

# Batch prediction
results = potential.predict(
    xsf_files,
    eval_forces=False,  # Faster without forces
    config=PredictionConfig(verbosity=0)
)

# Analyze results
import numpy as np
energies = np.array(results.cohesive_energy)
print(f"Mean energy: {energies.mean():.6f} eV/atom")
print(f"Std energy: {energies.std():.6f} eV/atom")

MPI Parallelization for Predictions

Similar to training, predictions can be accelerated using MPI parallelization if the predict.x executable is built with MPI support.

Prerequisites are the same as for training:

  1. The predict.x executable must be compiled with MPI support

  2. MPI must be enabled in the configuration: aenet config --enable-mpi

  3. (Optional) Customize the MPI launcher if needed

Using MPI in Predictions

To enable MPI parallelization, pass the num_processes parameter:

from aenet.mlip import ANNPotential, PredictionConfig

# Load potential
potential = ANNPotential.from_files({'Ti': 'Ti.nn', 'O': 'O.nn'})

# Standard prediction (sequential)
results = potential.predict(structures, eval_forces=True)

# MPI prediction with 8 processes
results = potential.predict(
    structures,
    eval_forces=True,
    num_processes=8
)

Ensemble Predictions

For uncertainty quantification, you can evaluate a committee of independently trained potentials through the direct libaenet interfaces. The default aggregation reports the ensemble mean energy and forces, together with standard deviations and per-atom force uncertainties.

If the committee members were trained with the PyTorch backend, you can export the full committee into this same manifest format with TorchCommitteePotential.to_aenet_ascii(...) and pass the returned member list directly into AenetEnsembleInterface.

from pathlib import Path

from aenet.geometry import AtomicStructure
from aenet.mlip import AenetEnsembleInterface
from aenet.torch_training import Structure, TorchCommitteePotential

training_structures = [
    Structure.from_AtomicStructure(AtomicStructure.from_file("train-0.xsf")),
    Structure.from_AtomicStructure(AtomicStructure.from_file("train-1.xsf")),
]

committee = TorchCommitteePotential.from_directory("committee_run")
members = committee.to_aenet_ascii(
    Path("ascii_committee"),
    prefix="committee",
    structures=training_structures,
)

ensemble = AenetEnsembleInterface(members)
structure = AtomicStructure.from_file("structure.xsf")
result = ensemble.predict(structure, forces=True)

print(result.energy_mean, result.energy_std)
from aenet.geometry import AtomicStructure
from aenet.mlip import AenetEnsembleInterface

members = [
    {'Ti': 'Ti.nn.0', 'O': 'O.nn.0'},
    {'Ti': 'Ti.nn.1', 'O': 'O.nn.1'},
    {'Ti': 'Ti.nn.2', 'O': 'O.nn.2'},
]
ensemble = AenetEnsembleInterface(members)

structure = AtomicStructure.from_file('structure.xsf')
result = ensemble.predict(structure, forces=True)

print(f"Energy: {result.energy_mean:.6f} ± {result.energy_std:.6f} eV")
print(f"Max force uncertainty: {result.force_uncertainty.max():.6f} eV/Å")

If you need continuity with an existing production model, you can keep one member as the reported predictor while still computing committee statistics:

ensemble = AenetEnsembleInterface(
    members,
    aggregation='reference',
    reference_member=0,
)
result = ensemble.predict(structure, forces=True)

# Reported values come from member 0
print(result.energy)
print(result.forces)

# Committee statistics are still available
print(result.energy_mean, result.energy_std)

ASE Ensemble Calculator

For ASE-driven simulations, AenetEnsembleCalculator reuses ASE’s neighbor list and exposes the ensemble result through the standard calculator API.

import ase.io
from aenet.mlip import AenetEnsembleCalculator

atoms = ase.io.read('structure.xsf')
atoms.calc = AenetEnsembleCalculator(members, aggregation='mean')

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

print(atoms.calc.results['energy_std'])
print(atoms.calc.results['force_uncertainty'].max())

The standard ASE properties energy and forces contain the aggregated output. Additional uncertainty information is available in atoms.calc.results through the keys energy_mean, energy_std, forces_mean, forces_std, and force_uncertainty.