15 - Workflows#
This script demonstrates using the vasp-ase recipe system for automated calculation workflows, including multi-step relaxations and complex pipelines. python run.py
import numpy as np
from ase.build import bulk
from vasp import Vasp
from vasp.recipes.core import double_relax_flow, relax_job, static_job
from vasp.recipes.decorators import flow, job
print("=" * 60)
print("Automated Workflows with Recipes")
print("=" * 60)
print()
============================================================
Automated Workflows with Recipes
============================================================
Part 1: Recipe basics#
print("Part 1: Recipe system overview")
print("-" * 40)
print()
print("The recipe system provides reusable calculation patterns:")
print()
print(" @job: Single VASP calculation")
print(" - static_job: Single-point energy")
print(" - relax_job: Geometry optimization")
print()
print(" @flow: Multi-step workflow")
print(" - double_relax_flow: Coarse → fine relaxation")
print(" - bulk_to_slabs_flow: Bulk → slab generation → calculation")
print()
print(" @subflow: Parallel sub-calculations")
print(" - Process multiple structures in parallel")
print()
Part 1: Recipe system overview
----------------------------------------
The recipe system provides reusable calculation patterns:
@job: Single VASP calculation
- static_job: Single-point energy
- relax_job: Geometry optimization
@flow: Multi-step workflow
- double_relax_flow: Coarse → fine relaxation
- bulk_to_slabs_flow: Bulk → slab generation → calculation
@subflow: Parallel sub-calculations
- Process multiple structures in parallel
Part 2: Static job workflow#
print("Part 2: Static job workflow")
print("-" * 40)
print()
# Create silicon structure
si = bulk('Si', 'diamond', a=5.43)
# Run static calculation using built-in recipe
print("Running static_job on Si...")
result = static_job(
si,
label='results/workflows/si_static',
kpts=(4, 4, 4),
encut=300,
)
print(f" Energy: {result.energy:.4f} eV")
print(f" Converged: {result.converged}")
print()
Part 2: Static job workflow
----------------------------------------
Running static_job on Si...
Energy: -10.8354 eV
Converged: True
Part 3: Relaxation workflow#
print("Part 3: Relaxation workflow")
print("-" * 40)
print()
# Slightly perturb Si lattice to show relaxation
si_perturbed = bulk('Si', 'diamond', a=5.50) # Slightly expanded
print("Running relax_job on expanded Si (a=5.50 Å)...")
relax_result = relax_job(
si_perturbed,
label='results/workflows/si_relax',
relax_cell=True,
kpts=(4, 4, 4),
encut=300,
ediffg=-0.02,
nsw=20,
)
print(f" Initial a: 5.50 Å")
print(f" Final energy: {relax_result.energy:.4f} eV")
a_final = relax_result.atoms.cell[0, 0] * np.sqrt(2)
print(f" Final a: {a_final:.3f} Å")
print()
print("Running double_relax_flow (coarse → fine)...")
double_result = double_relax_flow(
si_perturbed.copy(),
label='results/workflows/si_double_relax',
kpts=(4, 4, 4),
)
print(f" Final energy: {double_result.energy:.4f} eV")
print()
Part 3: Relaxation workflow
----------------------------------------
Running relax_job on expanded Si (a=5.50 Å)...
Initial a: 5.50 Å
Final energy: -10.8417 eV
Final a: 0.000 Å
Running double_relax_flow (coarse → fine)...
Final energy: -10.8273 eV
Part 4: Multi-structure workflow#
print("Part 4: Multi-structure workflow")
print("-" * 40)
print()
# Compare energies of different structures
structures = [
('Si', bulk('Si', 'diamond', a=5.43)),
('Ge', bulk('Ge', 'diamond', a=5.66)),
('C', bulk('C', 'diamond', a=3.57)),
]
print("Running static_job on multiple structures...")
print()
energies = {}
for name, atoms in structures:
result = static_job(
atoms,
label=f'results/workflows/{name.lower()}_static',
kpts=(4, 4, 4),
encut=300,
)
e_per_atom = result.energy / len(atoms)
energies[name] = e_per_atom
print(f" {name}: E = {e_per_atom:.4f} eV/atom")
print()
print("Cohesive energy comparison (relative to Si):")
for name, e in energies.items():
delta = e - energies['Si']
print(f" {name}: ΔE = {delta:+.3f} eV/atom")
print()
Part 4: Multi-structure workflow
----------------------------------------
Running static_job on multiple structures...
Si: E = -5.4177 eV/atom
Ge: E = -4.4679 eV/atom
C: E = -9.1649 eV/atom
Cohesive energy comparison (relative to Si):
Si: ΔE = +0.000 eV/atom
Ge: ΔE = +0.950 eV/atom
C: ΔE = -3.747 eV/atom
Part 5: Custom recipe#
print("Part 5: Creating custom recipes")
print("-" * 40)
print()
@job
def convergence_test_job(atoms, label='results/conv_test', encuts=None, **kwargs):
"""Test ENCUT convergence."""
if encuts is None:
encuts = [250, 300, 350, 400]
results = []
for encut in encuts:
calc = Vasp(
atoms=atoms.copy(),
label=f'{label}_encut{encut}',
encut=encut,
**kwargs
)
energy = calc.potential_energy
results.append({'encut': encut, 'energy': energy})
return results
print("Custom recipe: convergence_test_job")
print()
# Run actual convergence test
al = bulk('Al', 'fcc', a=4.05)
test_results = convergence_test_job(
al,
label='results/workflows/al_conv',
encuts=[250, 300, 350],
kpts=(6, 6, 6),
ismear=1,
sigma=0.1,
)
print(" ENCUT convergence test for Al:")
for r in test_results:
print(f" ENCUT={r['encut']}: E={r['energy']:.4f} eV")
# Check convergence
if len(test_results) >= 2:
delta = abs(test_results[-1]['energy'] - test_results[-2]['energy'])
print(f" Energy difference (last two): {delta*1000:.1f} meV")
print()
Part 5: Creating custom recipes
----------------------------------------
Custom recipe: convergence_test_job
ENCUT convergence test for Al:
ENCUT=250: E=-3.7285 eV
ENCUT=300: E=-3.7336 eV
ENCUT=350: E=-3.7363 eV
Energy difference (last two): 2.7 meV
Part 6: Workflow composition#
print("Part 6: Composing workflows")
print("-" * 40)
print()
@flow
def relax_then_static_flow(atoms, label, **kwargs):
"""Relax structure then run high-quality static calculation."""
print(" Step 1: Relaxation...")
relax = relax_job(
atoms,
label=f'{label}_relax',
encut=300,
ediffg=-0.02,
nsw=20,
**kwargs
)
print(" Step 2: High-quality static...")
static = static_job(
relax.atoms,
label=f'{label}_static',
encut=400, # Higher cutoff for final energy
**kwargs
)
return {
'relaxed': relax,
'static': static,
}
print("Custom flow: relax_then_static_flow")
print()
# Run on copper
cu = bulk('Cu', 'fcc', a=3.65) # Slightly off equilibrium
results = relax_then_static_flow(
cu,
label='results/workflows/cu_flow',
kpts=(6, 6, 6),
ismear=1,
sigma=0.1,
)
print()
print(f" Relaxed energy: {results['relaxed'].energy:.4f} eV")
print(f" Static energy (high quality): {results['static'].energy:.4f} eV")
print(f" Difference: {(results['static'].energy - results['relaxed'].energy)*1000:.1f} meV")
print()
Part 6: Composing workflows
----------------------------------------
Custom flow: relax_then_static_flow
Step 1: Relaxation...
Step 2: High-quality static...
Relaxed energy: -3.7147 eV
Static energy (high quality): -3.7159 eV
Difference: -1.2 meV
Part 7: Workflow engine integration#
print("Part 7: Workflow engine integration")
print("-" * 40)
print()
print("The recipe decorators support workflow engines:")
print()
print(" Environment variable: VASP_WORKFLOW_ENGINE")
print()
print(" Supported engines:")
print(" - prefect: Prefect workflows")
print(" - dask: Dask distributed")
print(" - parsl: Parsl parallel")
print(" - covalent: Covalent cloud")
print(" - jobflow: Materials Project jobflow")
print()
print(" When an engine is set:")
print(" - @job becomes the engine's task decorator")
print(" - @flow becomes the engine's flow decorator")
print(" - Enables parallel execution, retries, logging")
print()
print("Example with Prefect:")
print('''
export VASP_WORKFLOW_ENGINE=prefect
from vasp.recipes import static_job, relax_job
# Now decorated with @prefect.task / @prefect.flow
# Can use Prefect features:
# - Automatic retries
# - Task caching
# - Distributed execution
# - Web UI monitoring
''')
print()
Part 7: Workflow engine integration
----------------------------------------
The recipe decorators support workflow engines:
Environment variable: VASP_WORKFLOW_ENGINE
Supported engines:
- prefect: Prefect workflows
- dask: Dask distributed
- parsl: Parsl parallel
- covalent: Covalent cloud
- jobflow: Materials Project jobflow
When an engine is set:
- @job becomes the engine's task decorator
- @flow becomes the engine's flow decorator
- Enables parallel execution, retries, logging
Example with Prefect:
export VASP_WORKFLOW_ENGINE=prefect
from vasp.recipes import static_job, relax_job
# Now decorated with @prefect.task / @prefect.flow
# Can use Prefect features:
# - Automatic retries
# - Task caching
# - Distributed execution
# - Web UI monitoring
Summary#
print("=" * 60)
print("Summary")
print("=" * 60)
print()
print("Recipe system features:")
print(" - Reusable calculation patterns")
print(" - Consistent parameter handling")
print(" - Result dataclasses for type safety")
print(" - Workflow engine integration")
print(" - Easy composition of complex workflows")
print()
print("Available recipes:")
print(" Core: static_job, relax_job, double_relax_flow")
print(" Slabs: slab_static_job, slab_relax_job, bulk_to_slabs_flow")
print(" Phonons: phonon_job, phonon_flow (requires phonopy)")
print()
print("Best practices:")
print(" - Use recipes for reproducibility")
print(" - Set workflow engine for HPC")
print(" - Create custom @job and @flow for repeated tasks")
print(" - Compose simple jobs into complex workflows")
print()
print("Next: Try 16_neb/ for transition state calculations.")
============================================================
Summary
============================================================
Recipe system features:
- Reusable calculation patterns
- Consistent parameter handling
- Result dataclasses for type safety
- Workflow engine integration
- Easy composition of complex workflows
Available recipes:
Core: static_job, relax_job, double_relax_flow
Slabs: slab_static_job, slab_relax_job, bulk_to_slabs_flow
Phonons: phonon_job, phonon_flow (requires phonopy)
Best practices:
- Use recipes for reproducibility
- Set workflow engine for HPC
- Create custom @job and @flow for repeated tasks
- Compose simple jobs into complex workflows
Next: Try 16_neb/ for transition state calculations.