Based on previous work from the ADIS 2023 workshop the calculation of an energy volume curve with the quantum espresso density functional theory (DFT) simulation code is implemented in the Python Workflow Definition.
An energy-volume curve is computed by relaxing a bulk crystal structure, straining it to a series of volumes and computing the total energy at each volume with a self-consistent-field (SCF) calculation. The pipeline is implemented as five Python functions in workflow.py:
def get_bulk_structure(element, a, cubic):
# build an ASE bulk crystal structure, e.g. Al
def calculate_qe(working_directory, input_dict):
# write a pw.x input file, run "pw.x -in input.pwi > output.pwo" and parse the pwscf.xml output;
# input_dict["calculation"] selects "vc-relax" (cell + geometry relaxation) or "scf" (single-point energy)
def generate_structures(structure, strain_lst):
# apply each volumetric strain in strain_lst to a structure, returning one structure per strain
def plot_energy_volume_curve(volume_lst, energy_lst):
# plot energy against volume and save it as evcurve.pngwrite_input/collect_output handle the file-based pw.x input/output (.pwi/.pwo/.xml files), and
ase_to_json/json_to_ase (de)serialize ASE Atoms objects to/from OPTIMADE-JSON strings, so structures can be
passed between functions as plain JSON data instead of Python objects.
The workflow combines these functions as: get_bulk_structure -> calculate_qe (vc-relax, once) ->
generate_structures -> calculate_qe (scf, once per strained structure) -> plot_energy_volume_curve. With five
strains this means one relaxation followed by five independent SCF calculations that fan out from
generate_structures and feed back into a single plot.
workflow.json encodes this fan-out as five separate
workflow.calculate_qe function nodes, each connected to its own strained structure and its own working_directory
input, all reading the same shared inputs (pseudopotentials, kpts, calculation, smearing) via
python_workflow_definition.shared.get_dict. An excerpt showing the pattern for two of the five strained
calculations:
{
"nodes": [
{"id": 2, "type": "function", "value": "workflow.generate_structures"},
{"id": 3, "type": "function", "value": "workflow.calculate_qe"},
{"id": 4, "type": "function", "value": "workflow.calculate_qe"},
{"id": 19, "type": "input", "value": "strain_0", "name": "working_directory_1"},
{"id": 20, "type": "function", "value": "python_workflow_definition.shared.get_dict"},
{"id": 22, "type": "input", "value": "strain_1", "name": "working_directory_2"},
{"id": 23, "type": "function", "value": "python_workflow_definition.shared.get_dict"}
],
"edges": [
{"target": 3, "targetPort": "working_directory", "source": 19, "sourcePort": null},
{"target": 20, "targetPort": "structure", "source": 2, "sourcePort": "s_0"},
{"target": 3, "targetPort": "input_dict", "source": 20, "sourcePort": null},
{"target": 4, "targetPort": "working_directory", "source": 22, "sourcePort": null},
{"target": 23, "targetPort": "structure", "source": 2, "sourcePort": "s_1"},
{"target": 4, "targetPort": "input_dict", "source": 23, "sourcePort": null}
]
}
Node 2 (generate_structures) exposes one output port per strain (s_0, s_1, ...); each port is wired into its
own get_dict/calculate_qe pair. The energy and volume outputs of all five calculate_qe nodes are collected back
into lists with python_workflow_definition.shared.get_list before being passed to plot_energy_volume_curve.
Running this workflow (rather than just loading/inspecting the JSON) requires the Quantum Espresso pw.x binary,
matching pseudopotential files (see espresso/pseudo) and the
Python dependencies listed in environment.yml.