DFT Calculation with ASE Calculator
Introduction
The PythonJob is a built-in task that allows users to run Python functions on a remote computer. For instance, users can use ASE’s calculator to run a DFT calculation on a remote computer directly. Users only need to write normal Python code, and the WorkGraph will handle the data transformation to AiiDA data.
The following examples are running with AiiDA-WorkGraph.
First Real-world Workflow: atomization energy of molecule
The atomization energy, \(\Delta E\), of a molecule can be expressed as:
\[\Delta E = n_{\text{atom}} \times E_{\text{atom}} - E_{\text{molecule}}\]
Where: - \(\Delta E\) is the atomization energy of the molecule. - \(n_{\text{atom}}\) is the number of atoms. - \(E_{\text{atom}}\) is the energy of an isolated atom. - \(E_{\text{molecule}}\) is the energy of the molecule.
Define a task to calculate the energy of the atoms using EMT potential
[1]:
from aiida_workgraph import WorkGraph, task
def emt(atoms):
from ase.calculators.emt import EMT
atoms.calc = EMT()
energy = atoms.get_potential_energy()
return energy
def atomization_energy(mol, energy_molecule, energy_atom):
energy = energy_atom * len(mol) - energy_molecule
return energy
Define a workgraph
[2]:
wg = WorkGraph("atomization_energy")
pw_atom = wg.add_task("PythonJob", function=emt, name="emt_atom")
pw_mol = wg.add_task("PythonJob", function=emt, name="emt_mol")
wg.add_task(
"PythonJob",
function=atomization_energy,
name="atomization_energy",
energy_atom=pw_atom.outputs["result"],
energy_molecule=pw_mol.outputs["result"],
)
wg.to_html()
[2]:
Prepare the inputs and submit the workflow
[3]:
from aiida import load_profile
from aiida_workgraph.utils import generate_node_graph
from ase import Atoms
from ase.build import molecule
load_profile()
# create input structure
n_atom = Atoms("N", pbc=True)
n_atom.center(vacuum=5.0)
n2_molecule = molecule("N2", pbc=True)
n2_molecule.center(vacuum=5.0)
# ------------------------- Set the inputs -------------------------
wg.tasks["emt_atom"].set({"atoms": n_atom, "computer": "localhost"})
wg.tasks["emt_mol"].set({"atoms": n2_molecule, "computer": "localhost"})
wg.tasks["atomization_energy"].set({"mol": n2_molecule, "computer": "localhost"})
# ------------------------- Submit the calculation -------------------
wg.submit(wait=True)
# ------------------------- Print the output -------------------------
print("Energy of a N atom: {:0.3f}".format(wg.tasks["emt_atom"].outputs["result"].value.value))
print("Energy of an un-relaxed N2 molecule: {:0.3f}".format(wg.tasks["emt_mol"].outputs["result"].value.value))
print(
"Atomization energy: {:0.3f} eV".format(
wg.tasks["atomization_energy"].outputs["result"].value.value
)
)
# ------------------------- Generate node graph -------------------
generate_node_graph(wg.pk)
WorkGraph process created, PK: 33304
Process 33304 finished with state: FINISHED
Energy of a N atom: 5.100
Energy of an un-relaxed N2 molecule: 0.549
Atomization energy: 9.651 eV
[3]:
Second Real-world Workflow: Equation of state (EOS) WorkGraph
[ ]:
from aiida import load_profile
from aiida_workgraph import WorkGraph
from ase import Atoms
from ase.build import bulk
load_profile()
@task(
outputs=[
{"name": "scaled_atoms", "identifier": "workgraph.namespace", "metadata": {"dynamic": True}},
{"name": "volumes"},
]
)
def generate_scaled_atoms(atoms: Atoms, scales: list) -> dict:
"""Scale the structure by the given scales."""
volumes = {}
scaled_atoms = {}
for i in range(len(scales)):
atoms1 = atoms.copy()
atoms1.set_cell(atoms.cell * scales[i], scale_atoms=True)
scaled_atoms[f"s_{i}"] = atoms1
volumes[f"s_{i}"] = atoms1.get_volume()
return {"scaled_atoms": scaled_atoms, "volumes": volumes}
@task()
def emt(atoms):
from ase.calculators.emt import EMT
atoms.calc = EMT()
energy = atoms.get_potential_energy()
return {"energy": energy}
# Output result from context to the output socket
@task.graph_builder(outputs=[{"name": "results", "from": "context.results"}])
def calculate_enegies(scaled_atoms):
"""Run the scf calculation for each structure."""
from aiida_workgraph import WorkGraph
wg = WorkGraph()
for key, atoms in scaled_atoms.items():
emt1 = wg.add_task("PythonJob", function=emt, name=f"emt1_{key}", atoms=atoms)
emt1.set({"computer": "localhost"})
# save the output parameters to the context
emt1.set_context({f"results.{key}": "result"})
return wg
@task()
def fit_eos(volumes: dict, emt_results: dict) -> dict:
"""Fit the EOS of the data."""
from ase.eos import EquationOfState
from ase.units import kJ
volumes_list = []
energies = []
for key, data in emt_results.items():
energy = data["energy"]
energies.append(energy)
volumes_list.append(volumes[key])
eos = EquationOfState(volumes_list, energies)
v0, e0, b = eos.fit()
# convert b to GPa
b = b / kJ * 1.0e24
eos = {"energy unit": "eV", "v0": v0, "e0": e0, "B": b}
return eos
atoms = bulk("Au", cubic=True)
wg = WorkGraph("pythonjob_eos_emt")
scale_atoms_task = wg.add_task(
"PythonJob",
function=generate_scaled_atoms,
name="scale_atoms",
atoms=atoms,
)
# -------- calculate_enegies -----------
calculate_enegies_task = wg.add_task(
calculate_enegies,
name="calculate_enegies",
scaled_atoms=scale_atoms_task.outputs["scaled_atoms"],
)
# -------- fit_eos -----------
wg.add_task(
"PythonJob",
function=fit_eos,
name="fit_eos",
volumes=scale_atoms_task.outputs["volumes"],
emt_results=calculate_enegies_task.outputs["results"],
)
wg.to_html()
[5]:
wg.submit(
inputs={
"scale_atoms": {"atoms": atoms, "scales": [0.95, 1.0, 1.05], "computer": "localhost"},
"fit_eos": {"computer": "localhost"},
},
wait=True,
)
print("The fitted EOS parameters are:")
wg.tasks["fit_eos"].outputs["result"].value.get_dict()
WorkGraph process created, PK: 33344
Process 33344 finished with state: FINISHED
The fitted EOS parameters are:
[5]:
{'energy unit': 'eV',
'v0': 67.197735262521,
'e0': 0.0064587274657697,
'B': 167.61300824791}
Generate the node graph and check the data provenance.
[6]:
from aiida_workgraph.utils import generate_node_graph
# ------------------------- Generate node graph -------------------
generate_node_graph(wg.pk)
[6]: