import subprocess
from os import PathLike
from os.path import abspath, exists
from ase import Atoms
from ase.io import read
from ase.io.espresso import kspacing_to_grid
from ase.build import sort
import numpy as np
from pymatgen.io.vasp.inputs import Incar, Kpoints
from pymatgen.io.vasp.outputs import Vasprun
from typing import Union
from .oracle_base import Oracle
from ..workflow.workflow_base import Workflow
from ..utils.data_standard import (
ENERGY_KEY,
FORCES_KEY,
STRESS_KEY,
METADATA_KEY,
)
[docs]
class VaspOracle(Oracle):
"""
Class for creating, running, and parsing quantum espresso calculations
Resposible for making PWscf input file, run the code and extract energy,
forces, and stress tensor from PWscf output
"""
[docs]
def __init__(
self,
code_path: Union[str, PathLike] = None,
**kwargs,
):
"""
set variables and initialize the recorder and default workflow
:param code_path: path of the VASP executable
"""
self.code_path = code_path
if self.code_path is None:
raise KeyError('A path to the VASP executable (code_path) must be '
'provided in oracle_args to instantiate a '
'VaspOracle.')
self.output_filename = 'OUTCAR'
super().__init__(**kwargs)
[docs]
def get_run_command(self, **kwargs) -> str:
"""
return the command to run a VASP calculation
this method formats the run command based on the ``code_path`` internal
variable set at instantiation of the Oracle, which the
:class:`~orchestrator.workflow.workflow_base.Workflow` will execute in
the proper ``run_path``.
:returns: single line string with code execution statement
"""
command = (f'{self.code_path} > vasp.out')
return command
[docs]
def parse_for_storage(self,
run_path: str = '',
calc_id: int = None,
workflow: Workflow = None) -> Atoms:
"""
process calculation output to extract data in a consistent format
Parse the output from the VASP calculation into ASE Atoms objects.
The resulting Atoms will include the atomic configurations, cell info,
and possibly energies, forces, and stresses. Units are: total system
energy in eV, forces on each atom in eV/A, and stress on the system in
eV/A^3
:param run_path: directory where the oracle output file resides
:param calc_id: Job ID of the calculation to parse.
:param workflow: Workflow object of Orchestrator.
:returns: Atoms of the configuration and attached properties and a
dictionary of metadata that should be stored with the
configuration.
"""
if not run_path:
run_path = workflow.get_job_path(calc_id)
data_file = f'{run_path}/{self.output_filename}'
vasprun = Vasprun(f'{run_path}/vasprun.xml')
if not vasprun.converged_electronic:
raise RuntimeError(f'Calc at {run_path} is not converged')
atoms = read(data_file, format='vasp-out')
atoms.info[ENERGY_KEY] = vasprun.final_energy
atoms.info[STRESS_KEY] = vasprun.ionic_steps[-1]['stress']
atoms.set_array(FORCES_KEY,
np.array(vasprun.ionic_steps[-1]['forces']))
code = self.get_parameters(run_path)
universal = self.translate_universal_parameters(code)
universal['code'] = 'VASP'
universal['executable_path'] = self.code_path
code_parameters = {'code': code, 'universal': universal}
user = subprocess.run(
'whoami',
capture_output=True,
shell=True,
encoding='UTF-8',
).stdout.strip()
new_metadata = {
'generated_by': user,
'raw_path': abspath(data_file),
'code_parameters': code_parameters
}
if calc_id is not None:
new_metadata['data_source'] = f'workflow calc_id<{calc_id}>'
old_metadata = workflow.get_attached_metadata(calc_id)
combined_metadata = old_metadata | new_metadata
else:
combined_metadata = new_metadata
atoms.info[METADATA_KEY] = combined_metadata
return atoms
[docs]
def get_parameters(
self,
runpath: str = None,
) -> dict:
"""
Read in the input parameters from a VASP calculation.
:param runpath: directory where the oracle calculation files reside.
:return: Dict of VASP input parameters.
"""
vasprun = Vasprun(f'{runpath}/vasprun.xml')
parameters = vasprun.parameters
return parameters
[docs]
@staticmethod
def translate_universal_parameters(parameters: dict) -> dict:
"""
Orchestrator has predefined universal input values for varying codes
to allow some transferability. Each Oracle will need a function to
translate those values from the specific code. This function will
take the VASP INCAR values and convert them to the universal values
to be stored with the dataset on the initial submission.
:param parameters: Dictionary containing all the INCAR parameters to
run the simulation.
:returns: Dictionary of universal input parameters for database
storage.
"""
translated = {}
for key, value in parameters.items():
match key:
case 'GGA':
translated['xc'] = value
case 'ENCUT':
translated['planewave_cutoff'] = value
case 'ISMEAR':
translated['smearing_method'] = value
case 'SIGMA':
translated['smearing_value'] = value
case 'EDIFF':
translated['energy_convergence'] = value
case 'EDIFFG':
translated['force_convergence'] = value
case 'ISPIN':
if parameters.get('LNONCOLLINEAR', False):
translated['spin_mode'] = 4
else:
translated['spin_mode'] = value
case 'MAGMOM':
translated['magnetic_moments'] = value
case 'ISIF':
if value == 2:
translated['ion_relax'] = True
if value == 3:
translated['ion_relax'] = True
translated['cell_relax'] = True
case 'IMIX':
translated['mixing_mode'] = value
case 'AMIX':
translated['mixing_value'] = value
case 'IVDW':
translated['vdw_correction'] = value
case 'LDAUTYPE':
translated['hubbard_method'] = value
case 'LDAUL':
translated['hubbard_orbitals'] = value
case 'LDAUU':
translated['hubbard_u'] = value
case 'LDAUJ':
translated['hubbard_j'] = value
case 'IALGO':
translated['diagonalization_method'] = value
case 'IBRION':
translated['ion_optimization_method'] = value
case _:
pass
return translated