Modelforge ASE Calculator: Simple Walkthrough
This notebook demonstrates how to use a potential trained with modelforge as a calculator in the Atomic Simulation Environment (ASE). It is written for first-time users of both modelforge and ASE.
What you will do in this tutorial:
Load a trained NNP model checkpoint.
Wrap it with
ModelForgeCalculator.Build a molecule from a SMILES string.
Compute single-point energy and forces.
Run geometry optimization and a short molecular dynamics simulation.
[1]:
from modelforge.potential.potential import load_inference_model_from_checkpoint
# Helper utilities to load the example model checkpoint bundled with modelforge.
from modelforge.utils.io import get_path_string
from modelforge.ase.tests import data
checkpoint_file_path = f"{get_path_string(data)}/model.ckpt" # This is an example model used in testing
potential = load_inference_model_from_checkpoint(checkpoint_file_path, jit=False)
print(f"Loaded checkpoint: {checkpoint_file_path}")
2026-04-20 15:24:41.993 | DEBUG | modelforge.potential.potential:generate_potential:860 - training_parameter=None
2026-04-20 15:24:41.994 | DEBUG | modelforge.potential.potential:generate_potential:861 - potential_parameter=SchNetParameters(potential_name='SchNet', only_unique_pairs=False, core_parameter=CoreParameter(number_of_radial_basis_functions=128, maximum_interaction_radius=0.49999999999999994, number_of_interaction_modules=9, number_of_filters=256, shared_interactions=True, activation_function_parameter=ActivationFunctionConfig(activation_function_name='ShiftedSoftplus', activation_function_arguments=None, activation_function=ShiftedSoftplus()), featurization=Featurization(properties_to_featurize=['atomic_number', 'per_system_total_charge'], atomic_number=AtomicNumber(maximum_atomic_number=101, number_of_per_atom_features=512), atomic_period=AtomicPeriod(maximum_period=8, number_of_per_period_features=32), atomic_group=AtomicGroup(maximum_group=18, number_of_per_group_features=32)), predicted_properties=['per_atom_energy', 'per_atom_charge'], predicted_dim=[1, 1]), postprocessing_parameter=PostProcessingParameter(properties_to_process=['per_atom_energy'], per_atom_energy=PerAtomEnergy(normalize=False, from_atom_to_system_reduction=True, keep_per_atom_property=True), per_atom_charge=PerAtomCharge(conserve=True, conserve_strategy='default'), per_system_electrostatic_energy=None, per_system_zbl_energy=None, per_system_vdw_energy=None, sum_per_system_energy=None, general_postprocessing_operation=GeneralPostProcessingOperation(calculate_molecular_self_energy=True, calculate_atomic_self_energy=False)), potential_seed=-1)
2026-04-20 15:24:41.994 | DEBUG | modelforge.potential.potential:generate_potential:862 - dataset_parameter=None
2026-04-20 15:24:41.995 | DEBUG | modelforge.potential.potential:setup_potential:705 - potential_seed None
2026-04-20 15:24:41.995 | DEBUG | modelforge.potential.schnet:__init__:64 - Initializing the SchNet architecture.
2026-04-20 15:24:42.008 | DEBUG | modelforge.potential.potential:setup_potential:724 - Only unique pairs: False
2026-04-20 15:24:42.008 | DEBUG | modelforge.potential.potential:setup_potential:758 - Cutoffs: local_cutoff=0.49999999999999994, vdw_cutoff=-1, electrostatic_cutoff=-1
2026-04-20 15:24:42.010 | INFO | modelforge.potential.neighbors:__init__:480 - NeighborlistForInference initialized
2026-04-20 15:24:42.103 | DEBUG | modelforge.potential.potential:load_state_dict:672 - Removed prefixes: {'potential.'}
Loaded checkpoint: /Users/jenniferclark/bin/modelforge/modelforge-ase/modelforge/ase/tests/data/model.ckpt
To evaluate energies and forces in ASE, create a ModelForgeCalculator with the loaded potential and attach it to an ASE Atoms object.
[2]:
# Install ASE
from modelforge.ase import ModelForgeCalculator
modelforge_calculator = ModelForgeCalculator(potential)
ASE includes tools for building molecules, and modelforge-ase also provides helper functions for a simple SMILES-based workflow.
An ASE Atoms object stores the system definition: element identities, 3D positions, and optional simulation metadata (for example cell vectors and periodic boundary settings). By itself, Atoms is only a container for structure.
To compute potential energy or forces, you must attach a calculator first (here, ModelForgeCalculator). Without a calculator, calls like atoms.get_potential_energy() and atoms.get_forces() cannot run.
[3]:
from modelforge.ase import smiles_to_ase, ase_to_rdkit
# Set optimize=True to run an MMFF94 geometry optimization in RDKit before conversion.
smiles = "NCCCCCCO"
atoms = smiles_to_ase(smiles, optimize=False)
# Attach a calculator before requesting energy/forces from ASE.
atoms.calc = modelforge_calculator
You can view the 3D structure directly in ASE. (A separate RDKit view is also possible using the helper conversion utilities.)
[4]:
from ase.visualize import view
view(atoms, viewer='x3d')
[4]:
[5]:
# Optional alternative viewer (install nglview first).
# from nglview import show_ase
# show_ase(atoms)
Single-point energy and force computation
With the calculator attached, ASE can evaluate potential energy and per-atom forces. ModelForgeCalculator handles unit conversion from modelforge internal units to ASE units (eV and angstrom).
[6]:
pe = atoms.get_potential_energy()
forces = atoms.get_forces()
print(f"Potential energy: {pe:.6f} eV")
print("Forces (eV/angstrom):")
print(forces)
Potential energy: -87.677811 eV
Forces (eV/angstrom):
[[ 9.35617831e+00 -1.04941746e+01 -2.46965417e+00]
[-4.73898635e+00 2.68741981e+00 -1.49133123e+00]
[ 1.76531205e+00 -4.81745170e-01 1.01300176e+00]
[-6.05733491e-01 6.38226477e-01 7.25054235e-02]
[-8.13248220e-03 -3.55702940e-01 -7.89400711e-01]
[ 1.21058832e+00 8.26806163e-01 -6.81625482e-01]
[ 6.57577191e-01 4.57251602e-01 -7.60445774e-01]
[-1.75304901e+00 1.35702911e+00 -3.41923599e+00]
[-8.86226398e+00 -1.88798054e+00 4.35927589e+00]
[ 3.55498647e+00 7.28130734e+00 4.92473686e-01]
[ 6.69284696e-01 6.24919797e-01 -1.99190706e+00]
[ 2.48271806e-01 2.60230954e+00 7.58894359e-01]
[-2.14171833e-01 -1.01460840e+00 -7.52640876e-01]
[-9.50183786e-01 -6.46874168e-01 9.01118020e-01]
[-2.31531027e-01 5.94798309e-01 1.97134656e-01]
[ 3.06845990e-01 4.83842126e-01 -1.15194275e+00]
[-2.01529873e-01 8.74319249e-02 1.19573265e-01]
[-2.83709665e-01 5.89578509e-02 6.02928039e-01]
[-3.62168497e-01 6.41423562e-01 6.89800633e-01]
[ 3.00789842e-01 -1.08247846e+00 4.95305264e-01]
[ 3.53056884e-02 -2.18362045e+00 -1.30540650e+00]
[-3.37182905e-01 -8.14991585e-03 2.12953493e+00]
[ 4.43502713e-01 -1.86389677e-01 2.98204480e+00]]
Geometry optimization
ASE offers several structure optimizers. Here we use BFGS and stop when the maximum force falls below 0.05 eV/angstrom.
[7]:
from ase.optimize import BFGS
opt = BFGS(atoms, logfile=f"{smiles}_opt.log", trajectory=f"{smiles}_opt.traj")
opt.run(fmax=0.05)
[7]:
True
Molecular dynamics simulation
You can also run MD with ASE. This example runs short Langevin dynamics at 298 K and writes trajectory/log files.
[8]:
import ase.units as ase_units
from ase.io.trajectory import Trajectory
from ase.md import Langevin
from ase.io import write, read
trajectory_name = f"{smiles}_sim.traj"
write(f"{smiles}_system.pdb", atoms)
traj = Trajectory(trajectory_name, "w", atoms)
dyn = Langevin(
atoms,
timestep=1.0 * ase_units.fs,
temperature_K=298.0, # Kelvin
friction=1.0 / ase_units.fs,
trajectory=traj,
logfile=f"{smiles}_md.log",
)
dyn.run(100)
/Users/jenniferclark/mamba/envs/modelforge/lib/python3.11/site-packages/ase/md/langevin.py:110: FutureWarning: The implementation of `fixcm=True` in `Langevin` does not strictly sample the correct NVT distributions. The deviations are typically small for large systems but can be more pronounced for small systems. Use `fixcm=False` together with `ase.constraints.FixCom`. `fixcm` is deprecated since ASE 3.28.0 and will be removed in a future release.
warnings.warn(msg, FutureWarning)
[8]:
True
Convert ASE trajectory to XYZ so it can be opened in many viewers.
[9]:
trj = read(trajectory_name, ":")
write(f"{smiles}_sim.xyz", trj, format="xyz")
Now let’s visualize the trajectory.
First try the nglview widget inline. If widget rendering is unavailable in your frontend, the code falls back to ASE’s inline x3d viewer.
[10]:
view(trj)
[10]:
<Popen: returncode: None args: ['/Users/jenniferclark/mamba/envs/modelforge/...>
[11]:
## Notebook widget viewer (requires nglview + ipywidgets).
#from nglview import show_asetraj
#show_asetraj(trj)
2026-04-20 15:24:48.133 python[1375:35253137] +[IMKClient subclass]: chose IMKClient_Modern