skills/uma/SKILL.md
Run structure relaxation and phonon calculations using Meta's UMA (Universal Materials Accelerator) via fairchem
npx skillsauth add lamm-mit/scienceclaw umaInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Run structure relaxation, single-point energy calculations, and phonon property calculations using UMA (Universal Materials Accelerator), a machine-learned interatomic potential from Meta FAIR. UMA is a fast alternative to DFT, achieving near-DFT accuracy at a fraction of the cost.
Built on the fairchem framework.
fairchem-core installed (pip install fairchem-core)phonopy installed (pip install phonopy) — for phonon calculationsHF_TOKEN environment variable set (gated model on HuggingFace)uma_relax.py — Relax a crystal structure (CLI)python3 {baseDir}/scripts/uma_relax.py \
--structure path/to/structure.cif \
--relax-cell --pressure 150 \
--format json
| Parameter | Description |
|-----------|-------------|
| --structure | Path to CIF, POSCAR, or XYZ structure file |
| --mp-id | Materials Project ID (fetches structure automatically) |
| --model | uma-s-1p1, uma-s-1p2, uma-m-1p1 (default: uma-m-1p1) |
| --task | omat (default), omol, omc, oc20, odac |
| --device | cuda (default) or cpu |
| --relax-cell | Enable full cell relaxation |
| --pressure | External pressure in GPa (default: 0). Implies --relax-cell |
| --fmax | Force convergence threshold in eV/A (default: 0.05) |
| --steps | Max optimizer steps (default: 200) |
| --output-cif | Path to save relaxed structure as CIF |
| --format | summary | json |
For batch workflows (screening, phonon), use the Python API directly:
from fairchem.core import pretrained_mlip, FAIRChemCalculator
predictor = pretrained_mlip.get_predict_unit("uma-m-1p1", device="cuda")
calc = FAIRChemCalculator(predictor, task_name="omat")
from ase.build import bulk
from ase.optimize import FIRE
from ase.filters import FrechetCellFilter
atoms = bulk("Si") # or: ase.io.read("structure.cif")
atoms.calc = calc
# Relax with external pressure (1 GPa = 1/160.21766208 eV/A^3)
pressure_gpa = 150.0
pressure_ev_per_A3 = pressure_gpa / 160.21766208
opt = FIRE(FrechetCellFilter(atoms, scalar_pressure=pressure_ev_per_A3))
opt.run(fmax=0.05, steps=500)
energy = atoms.get_potential_energy() # eV
volume = atoms.get_volume() # A^3
import phonopy
from phonopy.structure.atoms import PhonopyAtoms
import numpy as np
# Convert ASE Atoms to PhonopyAtoms
ph_atoms = PhonopyAtoms(
symbols=atoms.get_chemical_symbols(),
cell=atoms.get_cell(),
scaled_positions=atoms.get_scaled_positions(),
)
# Create phonopy object with supercell
ph = phonopy.Phonopy(ph_atoms, supercell_matrix=np.diag([2, 2, 2]))
ph.generate_displacements(distance=0.01, is_diagonal=False)
# Compute forces on each displaced supercell using UMA
force_sets = []
for scell in ph.supercells_with_displacements:
from ase import Atoms as ASEAtoms
ase_scell = ASEAtoms(
symbols=scell.symbols, positions=scell.positions,
cell=scell.cell, pbc=True,
)
ase_scell.calc = FAIRChemCalculator(predictor, task_name="omat")
forces = ase_scell.get_forces()
forces -= forces.mean(axis=0) # acoustic sum rule
force_sets.append(forces)
ph.forces = force_sets
ph.produce_force_constants()
ph.symmetrize_force_constants()
# Get phonon frequencies
ph.run_mesh([8, 8, 8])
frequencies = ph.get_mesh_dict()["frequencies"] # (n_qpoints, n_bands) in THz
# Check dynamic stability (no imaginary modes)
min_freq = frequencies.min()
dynamically_stable = min_freq > -0.5 # THz threshold
# Thermal properties
ph.run_thermal_properties(t_min=0, t_max=600, t_step=100)
tp = ph.get_thermal_properties_dict()
# tp["temperatures"], tp["free_energy"], tp["entropy"], tp["heat_capacity"]
def auto_supercell(n_atoms):
if n_atoms <= 4: return [3, 3, 3]
elif n_atoms <= 16: return [2, 2, 2]
elif n_atoms <= 48: return [2, 2, 1]
else: return [1, 1, 1]
from pymatgen.core import Structure, Lattice
# LaH10 clathrate (Fm-3m, SG 225)
structure = Structure.from_spacegroup(
225, Lattice.cubic(5.1),
["La", "H", "H"],
[[0, 0, 0], [0.25, 0.25, 0.25], [0.118, 0.118, 0.118]],
)
# CaH6 sodalite (Im-3m, SG 229)
structure = Structure.from_spacegroup(
229, Lattice.cubic(3.54),
["Ca", "H"],
[[0, 0, 0], [0.25, 0, 0.5]],
)
from pymatgen.transformations.standard_transformations import SubstitutionTransformation
sub = SubstitutionTransformation({"La": "Y"})
new_structure = sub.apply_transformation(structure)
from pymatgen.io.ase import AseAtomsAdaptor
atoms = AseAtomsAdaptor.get_atoms(structure) # pymatgen → ASE
structure = AseAtomsAdaptor.get_structure(atoms) # ASE → pymatgen
from mp_api.client import MPRester
from pymatgen.analysis.phase_diagram import PhaseDiagram, PDEntry
from pymatgen.core import Composition
import os
with MPRester(os.environ["MP_API_KEY"]) as mpr:
entries = mpr.get_entries_in_chemsys("La-H")
my_entry = PDEntry(Composition("LaH10"), total_energy_eV, name="UMA-LaH10")
pd = PhaseDiagram(list(entries) + [my_entry])
e_above_hull = pd.get_e_above_hull(my_entry) # eV/atom; 0 = on hull
UMA requires a GPU. Structure your code with a GPU check at the top. When no GPU
is available, write a SLURM script that re-runs the SAME script on a GPU node.
The script file is at agent_scripts/agent_code.py relative to the project root.
import torch, subprocess, os, sys, json
# GPU check — must be at the TOP of the script, before any UMA imports
if not torch.cuda.is_available():
print("No GPU — submitting to SLURM", file=sys.stderr)
venv = os.environ.get("VIRTUAL_ENV", "")
script_path = os.path.abspath(sys.argv[0]) # path to this script
slurm = f"""#!/bin/bash
#SBATCH --partition=venkvis-h100
#SBATCH --gres=gpu:1
#SBATCH --mem=64G
#SBATCH --time=04:00:00
#SBATCH --output=slurm-%j.out
#SBATCH --error=slurm-%j.err
source {venv}/bin/activate
export HF_TOKEN="{os.environ.get('HF_TOKEN', '')}"
export MP_API_KEY="{os.environ.get('MP_API_KEY', '')}"
cd {os.getcwd()}
{sys.executable} {script_path}
"""
with open("submit.sh", "w") as f:
f.write(slurm)
result = subprocess.run(["sbatch", "submit.sh"], capture_output=True, text=True)
job_id = result.stdout.strip().split()[-1] if result.returncode == 0 else None
print(json.dumps({"status": "SUBMITTED_TO_SLURM", "job_id": job_id}))
sys.exit(0)
# === GPU code below (only runs on GPU node) ===
from fairchem.core import pretrained_mlip, FAIRChemCalculator
# ... rest of computation
For job dependency (ensure job B runs after job A):
sbatch --dependency=afterok:<job_A_id> submit_B.sh
Before submitting or executing code that uses UMA, verify:
pretrained_mlip.get_predict_unit("uma-m-1p1", device="cuda") — NOT pretrained_mlip("UMA") or pretrained_mlip.load()FAIRChemCalculator(predictor, task_name="omat") — NOT FAIRChemCalculator(model)FrechetCellFilter from ase.filters — NOT ExpCellFilter from ase.constraintspressure_gpa / 160.21766208 — NOT * 0.0006242 or other approximationsvenkvis-h100 or venkvis-a100 — NOT gpu or standardsource <venv>/bin/activate — NOT conda activate or module loados.environ.get("VIRTUAL_ENV", "") — NOT hardcodedos.environ.get("HF_TOKEN") and os.environ.get("MP_API_KEY")| Model | Parameters | Speed | Accuracy |
|-------|-----------|-------|----------|
| uma-s-1p1 | 6.6M active / 150M total | Fast | Good |
| uma-s-1p2 | 6.6M active / 290M total | Fast | Better |
| uma-m-1p1 | 50M active / 1.4B total | Slower | Best |
uma_screen.py — Batch structure relaxation and stability screeningRelax all CIF files in a directory with UMA at multiple pressures, compute
formation energies, and check 0 GPa stability against the Materials Project
convex hull. No hardcoded prototypes — structures come from the
structure-enumeration skill or any other source.
Typical workflow:
materials skill to find prototype structures from MPstructure-enumeration skill to generate candidates by metal substitutionuma_screen.py to relax all candidates and assess stability# Relax all CIFs in the default enumeration directory
python3 {baseDir}/scripts/uma_screen.py --format json
# From a custom directory
python3 {baseDir}/scripts/uma_screen.py \
--structures-dir ./my_candidates \
--pressures 0,150 \
--format json
If no GPU is available, the script auto-submits to SLURM (venkvis-h100).
| Parameter | Description |
|-----------|-------------|
| --structures-dir | Directory of CIF files to relax (default: ~/.scienceclaw/enumerated_structures) |
| --pressures | Pressures in GPa (default: 0,150) |
| --after-job | SLURM job ID to wait for before starting (ensures prior job completes first) |
| --model | UMA checkpoint (default: uma-m-1p1) |
| --device | cuda (default) or cpu |
| --fmax | Force convergence threshold (default: 0.05) |
| --steps | Max optimizer steps (default: 200) |
| --output-dir | Directory for relaxed CIFs (default: ./uma_screen_output) |
| --format | json or summary |
| --dry-run | Show plan without running |
--structures-dir for CIF files{
"status": "COMPLETED",
"model": "uma-m-1p1",
"ranking": [
{
"rank": 1,
"formula": "LaH10",
"prototype": "LaH10-type",
"formation_energy_eV_per_atom": -0.1234,
"converged": true,
"e_above_hull_eV_per_atom_0GPa": 0.045
}
],
"candidates": [ ... ],
"reference_energies": { ... }
}
You can also use UMA directly in Python without the CLI script:
from fairchem.core import pretrained_mlip, FAIRChemCalculator
from ase.build import bulk
from ase.optimize import FIRE
from ase.filters import FrechetCellFilter
# Load model (do this once, reuse for many structures)
predictor = pretrained_mlip.get_predict_unit("uma-m-1p1", device="cuda")
calc = FAIRChemCalculator(predictor, task_name="omat")
# Build or load structure
atoms = bulk("Si") # or: ase.io.read("structure.cif")
atoms.calc = calc
# Relax with optional external pressure
pressure_gpa = 150.0
pressure_ev_per_A3 = pressure_gpa / 160.21766208
filtered = FrechetCellFilter(atoms, scalar_pressure=pressure_ev_per_A3)
opt = FIRE(filtered)
opt.run(fmax=0.05, steps=500)
# Read results
energy = atoms.get_potential_energy() # eV
forces = atoms.get_forces() # eV/A
volume = atoms.get_volume() # A^3
Since UMA requires a GPU, a typical workflow on Artemis is:
uma_relax.py (see hpc skill for script format)sbatch to venkvis-h100 or venkvis-a100squeue -j <job_id>To enumerate candidate structures for screening, use pymatgen directly:
from pymatgen.core import Structure, Lattice
# Build a structure from spacegroup + Wyckoff positions
structure = Structure.from_spacegroup(
225, # Fm-3m
Lattice.cubic(5.1), # lattice parameter
["La", "H", "H"], # species at each site
[[0, 0, 0], # 4a: metal
[0.25, 0.25, 0.25], # 8c: H
[0.118, 0.118, 0.118]], # 32f: H (clathrate cage)
)
structure.to("LaH10.cif", fmt="cif")
To substitute metals in a prototype:
from pymatgen.transformations.standard_transformations import SubstitutionTransformation
# Replace La with Y in a prototype
sub = SubstitutionTransformation({"La": "Y"})
new_structure = sub.apply_transformation(structure)
To check thermodynamic stability at 0 GPa, compare against the Materials Project convex hull:
from mp_api.client import MPRester
from pymatgen.analysis.phase_diagram import PhaseDiagram, PDEntry
from pymatgen.core import Composition
with MPRester("YOUR_MP_API_KEY") as mpr:
# Fetch all known phases in the La-H system
entries = mpr.get_entries_in_chemsys("La-H")
# Add your UMA-computed entry
my_entry = PDEntry(Composition("LaH10"), total_energy_eV, name="UMA-LaH10")
all_entries = list(entries) + [my_entry]
# Build phase diagram and check stability
pd = PhaseDiagram(all_entries)
e_above_hull = pd.get_e_above_hull(my_entry) # eV/atom; 0 = on hull (stable)
tools
Onboard and manage Paperclip AI for research-paper knowledge and agent orchestration
development
Perform AI-powered web searches with real-time information using Perplexity models via LiteLLM and OpenRouter. This skill should be used when conducting web searches for current information, finding recent scientific literature, getting grounded answers with source citations, or accessing information beyond the model knowledge cutoff. Provides access to multiple Perplexity models including Sonar Pro, Sonar Pro Search (advanced agentic search), and Sonar Reasoning Pro through a single OpenRouter API key.
testing
Generate a structured scientific PDF report from a JSON description. Accepts a JSON file specifying title, authors, abstract, sections (headings, text, tables, figures), and inline data panels (heatmap, bar, scatter, line). Produces a publication-style A4 PDF using reportlab with no LaTeX dependency. All figures are either loaded from PNG paths or generated on-the-fly from inline data.
development
Execute arbitrary Python code and return stdout. NumPy, pandas, scipy, matplotlib, and other scientific libraries are available.