Install
npx skillscat add fl-sean03/agentic-science-worker/skills-mlip-simulation Install via the SkillsCat registry.
MLIP Simulation Skill
Run molecular dynamics and materials calculations using Machine Learning Interatomic Potentials (MLIPs) - achieving near-DFT accuracy at classical MD cost.
Trigger: Use when asked to run simulations with ML potentials, universal potentials, MACE, CHGNet, M3GNet, or when DFT would be too expensive but accuracy matters.
When to Use This vs torch-sim
This skill uses ASE (Atomic Simulation Environment) as the backend. For high-throughput screening (10+ structures), consider using the torch-sim skill instead:
| Scenario | Recommended | Why |
|---|---|---|
| 1-5 structures | This skill (ASE) | Simpler setup |
| 10+ structures | torch-sim | 100x faster batching |
| Quick single test | This skill (ASE) | Less overhead |
| Large-scale screening | torch-sim | GPU batching, memory management |
| Learning/prototyping | This skill (ASE) | More examples, familiar |
Rule of thumb: For batch operations on many structures, use torch-sim. For single structures or learning, use this ASE-based approach.
See the torch-sim skill for high-throughput patterns.
Available Universal Potentials
MACE-MP-0 (Recommended for General Use)
- Accuracy: Highest among universal potentials
- Parameters: 4.7M (largest model)
- Coverage: 89 elements
- Best for: General property prediction, phonons, surfaces
CHGNet
- Accuracy: Good, includes magnetic moments
- Parameters: ~400K (smallest, fastest)
- Best for: Batteries, Li-ion systems, charged species, oxidation states
M3GNet
- Accuracy: Good generalization
- Parameters: ~200K
- Best for: Fast screening, structure relaxation
SevenNet
- Accuracy: Best for phonons
- Best for: Phonon calculations, vibrational properties
Installation
# MACE (requires PyTorch with CUDA)
pip install mace-torch
# MatGL (M3GNet, CHGNet)
pip install matgl
# ASE (required for all)
pip install ase
# Phonopy (for phonon calculations)
pip install phonopy
# PyMatGen (structure manipulation)
pip install pymatgenBasic Usage with ASE
Loading Models
from ase import Atoms
from ase.build import bulk
# MACE
from mace.calculators import mace_mp
calc_mace = mace_mp(model="medium", device="cuda") # or "cpu"
# CHGNet
from chgnet.model import CHGNetCalculator
calc_chgnet = CHGNetCalculator()
# M3GNet
import matgl
from matgl.ext.ase import M3GNetCalculator
pot = matgl.load_model("M3GNet-MP-2021.2.8-PES")
calc_m3gnet = M3GNetCalculator(pot)Single-Point Calculation
from ase.build import bulk
# Create structure
atoms = bulk('Cu', 'fcc', a=3.6)
# Attach calculator
atoms.calc = calc_mace
# Get energy and forces
energy = atoms.get_potential_energy()
forces = atoms.get_forces()
stress = atoms.get_stress()
print(f"Energy: {energy:.4f} eV")
print(f"Forces shape: {forces.shape}")Structure Relaxation
from ase.optimize import BFGS
atoms = bulk('Si', 'diamond', a=5.43)
atoms.calc = calc_mace
# Relax structure
opt = BFGS(atoms, trajectory='relax.traj')
opt.run(fmax=0.01) # eV/Å
print(f"Relaxed energy: {atoms.get_potential_energy():.4f} eV")
print(f"Relaxed cell:\n{atoms.cell}")Molecular Dynamics
from ase.md.langevin import Langevin
from ase.md.velocitydistribution import MaxwellBoltzmannDistribution
from ase import units
# Create supercell
atoms = bulk('Cu', 'fcc', a=3.6) * (4, 4, 4) # 256 atoms
atoms.calc = calc_mace
# Initialize velocities
MaxwellBoltzmannDistribution(atoms, temperature_K=300)
# Setup MD
dyn = Langevin(
atoms,
timestep=1.0 * units.fs,
temperature_K=300,
friction=0.01
)
# Run MD
def print_energy(a=atoms):
print(f"E = {a.get_potential_energy():.4f} eV")
dyn.attach(print_energy, interval=100)
dyn.run(1000) # 1000 steps = 1 psCommon Calculations
Lattice Constant Optimization
from ase.build import bulk
from ase.eos import EquationOfState
import numpy as np
atoms = bulk('Cu', 'fcc', a=3.6)
atoms.calc = calc_mace
# Calculate E(V) curve
volumes = []
energies = []
for scale in np.linspace(0.95, 1.05, 7):
atoms_scaled = atoms.copy()
atoms_scaled.set_cell(atoms.cell * scale, scale_atoms=True)
atoms_scaled.calc = calc_mace
volumes.append(atoms_scaled.get_volume())
energies.append(atoms_scaled.get_potential_energy())
# Fit equation of state
eos = EquationOfState(volumes, energies)
v0, e0, B = eos.fit()
a_opt = (4 * v0) ** (1/3) # For FCC
print(f"Optimal lattice constant: {a_opt:.4f} Å")
print(f"Bulk modulus: {B / units.GPa:.1f} GPa")Elastic Constants
from ase.build import bulk
atoms = bulk('Cu', 'fcc', a=3.6)
atoms.calc = calc_mace
# Use stress-strain method
from ase.constraints import StrainFilter
from ase.optimize import BFGS
# First relax
sf = StrainFilter(atoms)
opt = BFGS(sf)
opt.run(fmax=0.001)
# Then calculate elastic constants
# (See ASE documentation for full elastic tensor calculation)Formation Energy
def formation_energy(compound_atoms, element_energies):
"""
Calculate formation energy.
compound_atoms: ASE Atoms object for compound
element_energies: dict like {'Li': -1.9, 'O': -4.9}
"""
compound_atoms.calc = calc_mace
E_compound = compound_atoms.get_potential_energy()
# Count atoms
symbols = compound_atoms.get_chemical_symbols()
from collections import Counter
counts = Counter(symbols)
# Reference energy
E_ref = sum(element_energies[el] * n for el, n in counts.items())
# Formation energy per atom
E_form = (E_compound - E_ref) / len(compound_atoms)
return E_formDiffusion Coefficient from MD
import numpy as np
from ase.md.langevin import Langevin
from ase import units
def calculate_diffusion(atoms, calc, T, n_steps=10000, dt=1.0):
"""Calculate diffusion coefficient from MSD."""
atoms = atoms.copy()
atoms.calc = calc
# Initialize
MaxwellBoltzmannDistribution(atoms, temperature_K=T)
# Setup MD
dyn = Langevin(atoms, timestep=dt*units.fs, temperature_K=T, friction=0.01)
# Track positions
positions = [atoms.get_positions().copy()]
for i in range(n_steps):
dyn.run(1)
if i % 10 == 0:
positions.append(atoms.get_positions().copy())
positions = np.array(positions)
# Calculate MSD
r0 = positions[0]
msd = np.mean(np.sum((positions - r0)**2, axis=2), axis=1)
# Time array
times = np.arange(len(msd)) * 10 * dt # fs
# Fit linear region (skip initial ballistic)
from scipy.stats import linregress
start = len(msd) // 4
slope, _, _, _, _ = linregress(times[start:], msd[start:])
# D = MSD / (6 * t) for 3D
D = slope / 6 # Ų/fs
D_cm2s = D * 1e-4 # Convert to cm²/s
return D_cm2sPhonon Calculations
With Phonopy
from phonopy import Phonopy
from phonopy.structure.atoms import PhonopyAtoms
from ase.build import bulk
import numpy as np
# Create structure
atoms = bulk('Si', 'diamond', a=5.43)
# Convert to phonopy format
phonopy_atoms = PhonopyAtoms(
symbols=atoms.get_chemical_symbols(),
cell=atoms.cell,
scaled_positions=atoms.get_scaled_positions()
)
# Create phonopy object with supercell
phonon = Phonopy(phonopy_atoms, supercell_matrix=[[2,0,0],[0,2,0],[0,0,2]])
# Generate displacements
phonon.generate_displacements(distance=0.01)
supercells = phonon.get_supercells_with_displacements()
# Calculate forces for each displacement
forces = []
for sc in supercells:
# Convert to ASE
atoms_sc = Atoms(
symbols=sc.symbols,
positions=sc.positions,
cell=sc.cell,
pbc=True
)
atoms_sc.calc = calc_mace
f = atoms_sc.get_forces()
forces.append(f)
# Set forces and calculate phonons
phonon.forces = forces
phonon.produce_force_constants()
# Calculate dispersion
path = [[[0,0,0], [0.5,0,0.5], [0.5,0.25,0.75], [0,0,0], [0.5,0.5,0.5]]]
labels = ['$\\Gamma$', 'X', 'K', '$\\Gamma$', 'L']
phonon.run_band_structure(path, labels=labels)
# Plot
phonon.plot_band_structure().savefig('phonon_dispersion.png')Known Limitations
Systematic Errors
| Property | Typical Error | Notes |
|---|---|---|
| Formation energy | ~50 meV/atom | Systematic shift possible |
| Lattice constant | ~1% | Usually reliable |
| Bulk modulus | ~10% | Varies by system |
| Phonon frequencies | ~15% low | Known systematic softening |
| Surface energies | ~100 meV/Ų | Less reliable than bulk |
| Alloy mixing | Poor | Not well-captured by UIPs |
When MLIPs Fail
- Chemistry far from training data: Exotic elements, unusual oxidation states
- Surfaces and interfaces: Higher errors than bulk
- Alloy thermodynamics: Binary mixing energies often wrong
- Reaction barriers: May not capture transition states
- Long-range interactions: Charge transfer, van der Waals
Validation Strategy
Always validate MLIP results against:
- DFT (at least for a few key structures)
- Experimental data
- Materials Project database
# Example: Compare to Materials Project
from mp_api.client import MPRester
with MPRester() as mpr:
docs = mpr.summary.search(formula="Cu")
mp_data = docs[0]
print(f"MP formation energy: {mp_data.formation_energy_per_atom:.4f} eV/atom")
print(f"MLIP formation energy: {my_mlip_result:.4f} eV/atom")
print(f"Difference: {abs(mp_data.formation_energy_per_atom - my_mlip_result):.4f} eV/atom")GPU Acceleration
MACE with CUDA
# Explicitly use GPU
calc = mace_mp(model="medium", device="cuda")
# Check GPU memory
import torch
print(f"GPU memory: {torch.cuda.memory_allocated()/1e9:.2f} GB")Performance Tips
- Batch calculations: Process multiple structures together
- Use appropriate model size: "small" for screening, "large" for accuracy
- GPU memory: Large systems may need memory management
- Parallel MD: Consider LAMMPS for large-scale GPU MD
- High-throughput: For 10+ structures, use
torch-simskill (100x faster)
Integration with LAMMPS
For large-scale MD, MACE can be used with LAMMPS:
# Convert MACE model for LAMMPS
mace_create_lammps_model --model MACE-MP-0 --output mace.lammps# LAMMPS input with MACE
units metal
atom_style atomic
read_data structure.data
pair_style mace
pair_coeff * * mace.lammps Cu
# Run MD
velocity all create 300 12345
fix 1 all nvt temp 300 300 0.1
run 10000Workflow Examples
High-Throughput Screening
def screen_structures(structures, calc, property_func):
"""Screen many structures for a property."""
results = []
for name, atoms in structures.items():
atoms.calc = calc
try:
prop = property_func(atoms)
results.append({'name': name, 'property': prop, 'status': 'success'})
except Exception as e:
results.append({'name': name, 'property': None, 'status': str(e)})
return results
# Example: Screen for stability
def stability_metric(atoms):
return atoms.get_potential_energy() / len(atoms)
results = screen_structures(my_structures, calc_mace, stability_metric)Fine-Tuning (Advanced)
# Fine-tuning requires MACE training code
# See: https://github.com/ACEsuit/mace
# Basic workflow:
# 1. Prepare training data (ASE database or xyz files)
# 2. Load pretrained model
# 3. Fine-tune with small learning rate
# 4. Validate on held-out set
# Example command (simplified):
# mace_run_train \
# --model="MACE" \
# --foundation_model="MACE-MP-0" \
# --train_file="my_data.xyz" \
# --valid_fraction=0.1 \
# --lr=0.0001 \
# --max_num_epochs=100Quick Reference
Model Selection Guide
| Use Case | Recommended Model |
|---|---|
| General property prediction | MACE-MP-0 |
| Battery materials | CHGNet |
| Fast screening | M3GNet |
| Phonon calculations | SevenNet or MACE |
| Large-scale MD | M3GNet (fastest) |
Typical Workflow
- Structure preparation: Build or fetch from Materials Project
- Model selection: Choose based on chemistry and property
- Validation: Compare small test to DFT/experiment
- Production: Run full calculation
- Analysis: Extract properties, compare to literature
- Documentation: Report model used, limitations
Resources
- MACE: https://github.com/ACEsuit/mace
- MatGL: https://github.com/materialsvirtuallab/matgl
- Matbench Discovery: https://matbench-discovery.materialsproject.org/
- ASE: https://wiki.fysik.dtu.dk/ase/
- Phonopy: https://phonopy.github.io/phonopy/