TorchSim Tutorial¶
This tutorial introduces the atomate2 interface to TorchSim for molecular dynamics simulations and geometry optimizations. The atomate2 interface wraps TorchSim’s high-level API into jobflow-compatible Makers, enabling workflow management, database storage, and reproducible simulations.
Installing Atomate2 with TorchSim¶
# Setting up conda environment
>>> conda create -n atomate2-torchsim python=3.11
>>> conda activate atomate2-torchsim
# Installing atomate2 with TorchSim support
>>> pip install atomate2
>>> pip install torch-sim
# For MACE models (optional but recommended)
>>> pip install mace-torch
To verify the installation:
import torch_sim as ts
from atomate2.torchsim.core import TorchSimStaticMaker
print("Installation successful!")
Understanding the Atomate2 TorchSim Interface¶
Atomate2 provides three primary Makers for TorchSim simulations:
TorchSimStaticMaker- For one-time energy/force/property calculationsTorchSimOptimizeMaker- For geometry optimizationTorchSimIntegrateMaker- For molecular dynamics simulations
These Makers wrap TorchSim’s static, optimize, and integrate functions respectively, adding:
Structured output via
TorchSimTaskDocschemaJobflow integration for workflow management
Automatic tracking of calculation metadata
Support for chaining calculations together
Basic Static Calculation¶
Let’s start with a simple static calculation using a Lennard-Jones potential. First, we create our atomic structure:
# ruff: noqa: T201
from ase.build import bulk
from pymatgen.io.ase import AseAtomsAdaptor
# Create an Argon FCC structure using ASE and convert to pymatgen
ar_atoms = bulk("Ar", "fcc", a=5.26, cubic=True)
ar_structure = AseAtomsAdaptor.get_structure(ar_atoms)
print(f"Structure: {ar_structure.formula}")
print(f"Number of atoms: {len(ar_structure)}")
Now we create a TorchSimStaticMaker and run the calculation. Note that unlike raw TorchSim, we specify the model type using the TorchSimModelType enum:
from jobflow import run_locally
from atomate2.torchsim.core import TorchSimStaticMaker
from atomate2.torchsim.schema import TorchSimModelType
# Create a static calculation maker with Lennard-Jones model
static_maker = TorchSimStaticMaker(
model_type=TorchSimModelType.LENNARD_JONES,
model_path="", # LJ model doesn't need a path
model_kwargs={
"sigma": 3.405, # Angstrom, typical for Ar
"epsilon": 0.0104, # eV, typical for Ar
"compute_stress": True,
},
)
# Create the job - accepts a single structure or a list of structures
job = static_maker.make([ar_structure])
# Run locally
response_dict = run_locally(job, ensure_success=True)
# Extract the result
result = list(response_dict.values())[-1][1].output
print(f"Energy: {result.calcs_reversed[0].output.energies[0]:.6f} eV")
print(f"Time elapsed: {result.time_elapsed:.3f} seconds")
Understanding the Output: TorchSimTaskDoc¶
The output of all TorchSim Makers is a TorchSimTaskDoc, which contains:
structures: The final structures from the calculationcalcs_reversed: List of calculation details (most recent first)time_elapsed: Total calculation timeuuid: Unique identifier for this taskdir_name: Directory where the calculation was run
# Exploring the task document structure
print(f"Task UUID: {result.uuid}")
print(f"Directory: {result.dir_name}")
print(f"Number of structures: {len(result.structures)}")
print(f"Number of calculations: {len(result.calcs_reversed)}")
# Explore the calculation details
calc = result.calcs_reversed[0]
print("\nCalculation details:")
print(f" Model: {calc.model}")
print(f" Task type: {calc.task_type}")
print(f" Energies: {calc.output.energies}")
print(f" Forces shape: {len(calc.output.all_forces[0])} atoms x 3")
Batch Processing Multiple Systems¶
One of TorchSim’s strengths is efficiently processing multiple systems in parallel. This works seamlessly through the atomate2 interface:
# Create multiple structures
cu_atoms = bulk("Cu", "fcc", a=3.6, cubic=True)
fe_atoms = bulk("Fe", "bcc", a=2.87, cubic=True)
cu_structure = AseAtomsAdaptor.get_structure(cu_atoms)
fe_structure = AseAtomsAdaptor.get_structure(fe_atoms)
# Create supercells
cu_supercell = cu_structure.copy()
cu_supercell.make_supercell([2, 2, 2])
structures = [ar_structure, cu_structure, fe_structure, cu_supercell]
print(f"Processing {len(structures)} structures:")
for i, s in enumerate(structures):
print(f" {i}: {s.formula} ({len(s)} atoms)")
# Run static calculation on all structures at once
job = static_maker.make(structures)
response_dict = run_locally(job, ensure_success=True)
result = list(response_dict.values())[-1][1].output
# Print energies for each structure
print("Results for batch calculation:")
for i, energy in enumerate(result.calcs_reversed[0].output.energies):
n_atoms = len(structures[i])
print(f" Structure {i}: {energy:.6f} eV ({energy / n_atoms:.6f} eV/atom)")
Geometry Optimization¶
The TorchSimOptimizeMaker provides geometry optimization capabilities. It supports various optimizers and convergence criteria:
import torch_sim as ts
from atomate2.torchsim.core import TorchSimOptimizeMaker
from atomate2.torchsim.schema import ConvergenceFn
# Perturb the structure to make optimization meaningful
perturbed_structure = ar_structure.copy()
perturbed_structure.translate_sites(
list(range(len(perturbed_structure))), [0.05, 0.05, 0.05]
)
# Create an optimization maker
optimize_maker = TorchSimOptimizeMaker(
model_type=TorchSimModelType.LENNARD_JONES,
model_path="",
optimizer=ts.Optimizer.fire, # FIRE optimizer
convergence_fn=ConvergenceFn.FORCE, # Converge based on forces
convergence_fn_kwargs={"force_tol": 1e-3}, # Force tolerance in eV/A
max_steps=500,
steps_between_swaps=10,
init_kwargs={"cell_filter": ts.CellFilter.unit}, # Keep cell fixed
model_kwargs={"sigma": 3.405, "epsilon": 0.0104, "compute_stress": True},
)
# Run optimization
job = optimize_maker.make([perturbed_structure])
response_dict = run_locally(job, ensure_success=True)
result = list(response_dict.values())[-1][1].output
print(f"Optimization completed in {result.time_elapsed:.3f} seconds")
print(f"Final energy: {result.calcs_reversed[0].output.energies[0]:.6f} eV")
Molecular Dynamics¶
The TorchSimIntegrateMaker enables molecular dynamics simulations with various integrators:
from atomate2.torchsim.core import TorchSimIntegrateMaker
# Create an MD maker
md_maker = TorchSimIntegrateMaker(
model_type=TorchSimModelType.LENNARD_JONES,
model_path="",
integrator=ts.Integrator.nvt_langevin, # Langevin thermostat
n_steps=100,
temperature=300.0, # Kelvin
timestep=0.001, # picoseconds
model_kwargs={"sigma": 3.405, "epsilon": 0.0104, "compute_stress": True},
)
# Run MD simulation
job = md_maker.make([ar_structure])
response_dict = run_locally(job, ensure_success=True)
result = list(response_dict.values())[-1][1].output
print(f"MD completed in {result.time_elapsed:.3f} seconds")
print(f"Final energy: {result.calcs_reversed[0].output.energies[0]:.6f} eV")
# Check that the structure has evolved
calc = result.calcs_reversed[0]
print("\nMD parameters stored:")
print(f" Integrator: {calc.integrator}")
print(f" n_steps: {calc.n_steps}")
print(f" Temperature: {calc.temperature} K")
print(f" Timestep: {calc.timestep} ps")
Trajectory Reporting¶
TorchSim supports saving trajectory data during simulations. In atomate2, you configure this via the trajectory_reporter_dict parameter:
import tempfile
from pathlib import Path
# Create a temporary directory for trajectory files
tmp_dir = Path(tempfile.mkdtemp())
n_systems = 2
trajectory_reporter_dict = {
"filenames": [tmp_dir / f"md_traj_{i}.h5md" for i in range(n_systems)],
"state_frequency": 10, # Save state every 10 steps
"prop_calculators": {
5: ["potential_energy", "kinetic_energy", "temperature"],
},
}
# Create MD maker with trajectory reporting
md_maker_with_traj = TorchSimIntegrateMaker(
model_type=TorchSimModelType.LENNARD_JONES,
model_path="",
integrator=ts.Integrator.nvt_langevin,
n_steps=50,
temperature=300.0,
timestep=0.001,
trajectory_reporter_dict=trajectory_reporter_dict,
model_kwargs={"sigma": 3.405, "epsilon": 0.0104, "compute_stress": True},
)
# Run with trajectory reporting
job = md_maker_with_traj.make([ar_structure, ar_structure])
response_dict = run_locally(job, ensure_success=True)
result = list(response_dict.values())[-1][1].output
# Check trajectory reporter details in output
traj_details = result.calcs_reversed[0].trajectory_reporter
print("Trajectory reporter configuration:")
print(f" State frequency: {traj_details.state_frequency}")
print(f" Property calculators: {traj_details.prop_calculators}")
print(f" Output files: {[str(f) for f in traj_details.filenames]}")
# Analyze the trajectory
traj_file = traj_details.filenames[0]
with ts.TorchSimTrajectory(traj_file) as traj:
potential_energies = traj.get_array("potential_energy")
temperatures = traj.get_array("temperature")
print("Trajectory analysis:")
print(f" Number of frames: {len(potential_energies)}")
print(f" Initial energy: {potential_energies[0].item():.6f} eV")
print(f" Final energy: {potential_energies[-1].item():.6f} eV")
print(f" Average temperature: {temperatures.mean().item():.1f} K")
Using Machine Learning Potentials¶
TorchSim shines with machine learning potentials like MACE. Here’s how to use a MACE model with atomate2:
from mace.calculators.foundations_models import download_mace_mp_checkpoint
# Download MACE-MP model checkpoint
mace_model_path = Path(download_mace_mp_checkpoint("small"))
# Create a static maker with MACE
mace_static_maker = TorchSimStaticMaker(
model_type=TorchSimModelType.MACE,
model_path=mace_model_path,
)
# Run on a copper structure
job = mace_static_maker.make([cu_structure])
response_dict = run_locally(job, ensure_success=True)
result = list(response_dict.values())[-1][1].output
print("MACE static calculation:")
print(f" Energy: {result.calcs_reversed[0].output.energies[0]:.6f} eV")
print(f" Model path: {result.calcs_reversed[0].model_path}")
# MACE optimization example
mace_optimize_maker = TorchSimOptimizeMaker(
model_type=TorchSimModelType.MACE,
model_path=mace_model_path,
optimizer=ts.Optimizer.fire,
convergence_fn=ConvergenceFn.FORCE,
convergence_fn_kwargs={"force_tol": 0.01},
max_steps=200,
init_kwargs={"cell_filter": ts.CellFilter.unit},
)
# Perturb and optimize
perturbed_cu = cu_structure.copy()
perturbed_cu.translate_sites(list(range(len(perturbed_cu))), [0.02, 0.02, 0.02])
job = mace_optimize_maker.make([perturbed_cu])
response_dict = run_locally(job, ensure_success=True)
result = list(response_dict.values())[-1][1].output
print(f"MACE optimization completed in {result.time_elapsed:.3f} seconds")
print(f"Final energy: {result.calcs_reversed[0].output.energies[0]:.6f} eV")
Autobatching¶
When processing many systems, TorchSim’s autobatching automatically determines optimal batch sizes for GPU memory. Enable it via autobatcher_dict:
# Create many structures to process
many_structures = [ar_structure.copy() for _ in range(10)]
# Enable autobatching with custom settings
autobatcher_dict = {
"memory_scales_with": "n_atoms",
"max_memory_scaler": 260,
}
static_maker_batched = TorchSimStaticMaker(
model_type=TorchSimModelType.LENNARD_JONES,
model_path="",
autobatcher_dict=autobatcher_dict,
model_kwargs={"sigma": 3.405, "epsilon": 0.0104},
)
job = static_maker_batched.make(many_structures)
response_dict = run_locally(job, ensure_success=True)
result = list(response_dict.values())[-1][1].output
# Check autobatcher details
if result.calcs_reversed[0].autobatcher:
ab_details = result.calcs_reversed[0].autobatcher
print(f"Autobatcher used: {ab_details.autobatcher}")
print(f"Memory scales with: {ab_details.memory_scales_with}")
print(f"\nProcessed {len(result.calcs_reversed[0].output.energies)} structures")
Chaining Calculations¶
One advantage of the atomate2 interface is the ability to chain calculations together. You can pass the output of one job as input to another:
from jobflow import Flow
# Create makers for a multi-step workflow
# Step 1: Optimize the structure (using energy convergence for simplicity)
optimize_maker = TorchSimOptimizeMaker(
name="optimize",
model_type=TorchSimModelType.LENNARD_JONES,
model_path="",
optimizer=ts.Optimizer.fire,
convergence_fn=ConvergenceFn.ENERGY, # Energy-based convergence
max_steps=100,
model_kwargs={"sigma": 3.405, "epsilon": 0.0104, "compute_stress": True},
)
# Step 2: Run MD on the optimized structure
md_maker = TorchSimIntegrateMaker(
name="md",
model_type=TorchSimModelType.LENNARD_JONES,
model_path="",
integrator=ts.Integrator.nvt_langevin,
n_steps=50,
temperature=300.0,
timestep=0.001,
model_kwargs={"sigma": 3.405, "epsilon": 0.0104, "compute_stress": True},
)
# Create jobs
perturbed = ar_structure.copy()
perturbed.translate_sites(list(range(len(perturbed))), [0.05, 0.05, 0.05])
optimize_job = optimize_maker.make([perturbed])
# Chain: use optimized structures as input to MD
# The prev_task parameter allows tracking the calculation chain
md_job = md_maker.make(
optimize_job.output.structures,
prev_task=optimize_job.output,
)
# Create a flow
workflow = Flow([optimize_job, md_job], name="optimize_then_md")
# Run the workflow
response_dict = run_locally(workflow, ensure_success=True)
# Get the final result
final_result = list(response_dict.values())[-1][1].output
print("Workflow completed!")
print(f"Number of calculations in chain: {len(final_result.calcs_reversed)}")
print(f"Final energy: {final_result.calcs_reversed[0].output.energies[0]:.6f} eV")
Running Workflows¶
Atomate2 also provides some ready-to-use workflows, such as the elastic properties workflow. Let’s see how batched execution performs in a “real world” workflow:
import time
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
from atomate2.torchsim.flows.elastic import ElasticMaker
from atomate2.torchsim.schema import TorchSimModelType
# Download MACE-MP model checkpoint
mace_model_path = Path(download_mace_mp_checkpoint("small"))
# Create a structure
si_atoms = bulk("Si", "fcc", a=3.86, cubic=True)
si_structure = AseAtomsAdaptor.get_structure(si_atoms)
si_prim = SpacegroupAnalyzer(si_structure).get_primitive_standard_structure()
# Pre-relaxation maker: allows both atomic coordinates and cell to relax
# The cell forces are included in the convergence criteria
bulk_relax_maker = TorchSimOptimizeMaker(
optimizer=ts.Optimizer.fire,
model_type=TorchSimModelType.MACE,
model_path=mace_model_path,
model_kwargs={"compute_stress": True},
init_kwargs={"cell_filter": ts.CellFilter.frechet},
convergence_fn_kwargs={"force_tol": 1e-5, "include_cell_forces": True},
fix_symmetry=True,
)
# Deformation maker: relax only the atomic coordinates
elastic_relax_maker = TorchSimOptimizeMaker(
optimizer=ts.Optimizer.fire,
model_type=TorchSimModelType.MACE,
model_path=mace_model_path,
model_kwargs={"compute_stress": True},
convergence_fn_kwargs={"force_tol": 1e-5},
fix_symmetry=True,
)
def run_elastic_benchmark(use_socket: bool): # noqa: ANN201
"""Run the elastic workflow with or without TorchSim autobatcher enabled."""
maker = ElasticMaker(
bulk_relax_maker=bulk_relax_maker,
elastic_relax_maker=elastic_relax_maker,
socket=use_socket, # Batch the relaxations when True
)
flow = maker.make(si_prim)
start_time = time.time()
responses = run_locally(flow, ensure_success=True)
end_time = time.time()
elapsed = end_time - start_time
# Extract the final document from the output schema
elastic_output = responses[flow[-1].uuid][1].output
return elapsed, elastic_output
time_sequential, out_seq = run_elastic_benchmark(use_socket=False)
time_batched, out_batch = run_elastic_benchmark(use_socket=True)
print("=" * 70)
print(f"Sequential Execution Time: {time_sequential:.2f} s")
print(f"Batched Socket Execution Time: {time_batched:.2f} s")
print(f"Speedup Factor: {time_sequential / time_batched:.2f}x faster")
print("-" * 70)
print(f"Bulk Modulus K_V (Sequential): {out_seq.derived_properties.k_voigt:.4f} GPa")
print(f"Bulk Modulus K_V (Batched): {out_batch.derived_properties.k_voigt:.4f} GPa")
print(f"Shear Modulus G_V (Sequential): {out_seq.derived_properties.g_voigt:.4f} GPa")
print(f"Shear Modulus G_V (Batched): {out_batch.derived_properties.g_voigt:.4f} GPa")
print("=" * 70)
Supported Model Types¶
The atomate2 TorchSim interface supports various machine learning potentials through the TorchSimModelType enum:
from atomate2.torchsim.schema import TorchSimModelType
print("Supported model types:")
for model_type in TorchSimModelType:
print(f" - {model_type.name}: {model_type.value}")
Available Property Functions¶
For trajectory reporting, these property functions are available via the PropertyFn enum. Due to the constraints of serialization, you cannot add arbitrary property functions like in raw torchsim, however you can easily modify the underlying PropertyFn code to manually add additional properties.
from atomate2.torchsim.schema import PropertyFn
print("Available property functions for trajectory reporting:")
for prop in PropertyFn:
print(f" - {prop.value}")
Running with Databases¶
Like other atomate2 workflows, TorchSim jobs can be run with database storage. Configure your jobflow.yaml to point to your MongoDB instance:
JOB_STORE:
docs_store:
type: MongoStore
database: DATABASE
collection_name: atomate2_docs
host: your-mongo-host
port: 27017
username: USERNAME
password: PASSWORD
Then run your workflows as usual - the TorchSimTaskDoc will be automatically stored in the database.
Conclusion¶
The atomate2 TorchSim interface provides a powerful way to run molecular simulations with:
TorchSimStaticMaker- Single-point energy/force calculationsTorchSimOptimizeMaker- Geometry optimization with customizable convergenceTorchSimIntegrateMaker- Molecular dynamics with various integrators
Key features:
Support for multiple ML potentials (MACE, FairChem, SevenNet, etc.)
Batch processing of multiple structures
Automatic autobatching for GPU memory management
Trajectory reporting with customizable property calculations
Structured output via
TorchSimTaskDocschemaFull jobflow integration for workflow management and database storage
For more advanced usage, refer to the TorchSim documentation and the atomate2 source code.