How to add new properties

Adding new properties is simple but requires integrating your methods for pre- and post-processing correctly. This tutorial guides you in detail on how to do so.

1. Define your property

At first, you need to define and develop methods for the pre- and post-processing.

The method that conducts the pre-processing of your structures should be named generate_structures and takes as input argument only a single ase.atoms object. It needs to return a list of ase.atoms objects modified in the way you wish.

For the post-processing, you need to define a method that takes two arguments: a strucscan.engine.generalengine object and str containing the absolute path to the directory of the final calculation. This method should read the final structures, post-process the data in a custom way and return a python dictionary.

You can take the strucscan.properties.eos module as an example:

[1]:
! cat ../strucscan/properties/eos.py
from ase.eos import EquationOfState

import numpy as np
import os

volume_range = 0.1
num_of_point = 11


def generate_structures(atoms):
    """
    :param atoms: (ASE atoms object)
    :return: (ASE atoms object list) list of modified ASE atoms objects
    """
    vol_min = 1 - volume_range
    vol_max = 1 + volume_range

    strained_structures = []
    for strain in np.linspace(vol_min, vol_max, num_of_point):
        basis = atoms.copy()

        cell = basis.get_cell()
        cell *= strain ** (1. / 3.)

        basis.set_cell(cell, scale_atoms=True)
        strained_structures.append(basis)
    return strained_structures


def get_EOS_properties(calc, absolute_path):
    """
    :param calc: (strucscan.engine.generalengine.GeneralEngine object) calculator object
    :param absolute_path: (str) absolute path to job directory
    :return: (dict) python dictionary with summarized results
    """
    energy_list = []
    volume_list = []
    stress_list = []
    pressure_list = []
    result_filename = calc.get_result_filename()
    for filename in os.listdir(absolute_path):
        if (result_filename in filename):
            final_struct = calc.read_final_structure(absolute_path,
                                                     resultfilename=filename)
            energy_list.append(final_struct.get_potential_energy())
            volume_list.append(final_struct.get_volume())
            stress = final_struct.get_stress()
            stress_list.append(stress)
            pressure_list.append(-1./3.*sum(stress[0:3]))

    eos = EquationOfState(volume_list, energy_list, eos='murnaghan')
    v0, e0, B = eos.fit()

    outputdict = {}
    outputdict["volume"] = np.array(volume_list)
    outputdict["energy"] = np.array(energy_list)
    outputdict['stresses'] = np.array(stress_list)
    outputdict['pressure'] = np.array(pressure_list)
    outputdict["equilibrium_energy"] = e0
    outputdict["equilibrium_volume"] = v0
    outputdict["equilibrium_bulk_modulus"] = B
    return outputdict

Respect pre-requesites

If your property requires any pre-requisites, you can link them in the propertis.yaml file in the resource module of strucscan:

[2]:
! cat ../strucscan/resources/properties.yaml
# Each property that requires any kind of pre-processing, e.g.
# generation of multiple input structure files
# are 'advanced' properties and should be listed here.
# The instruction after the colon indicate which property is prerequisite.
# For the properties before the colon, no relaxation will be performed.
# If the property is 'advanced' but requires no prerequisite,
# no conditional property needs to be entered after the colon (= None).

eos: static, atomic, total
dos: total


default_option: atomic

The name of the property that you type before the colon will be the keyword that you hand over to strucscan later in the input files.

2. Integrate your property in the workflow

At last, you need to integrate the call of your property in the workflow. There are two interfaces that need to be respected: ## 1. Pre-processing: This happens in the strucscan.core.jobmaker class. In the method get_advanced_prototypes you are asked to link your property in the if-elif clause as already done for the eos property. Please continue with elif statements and call the generate_structures method of your property module. Make sure you type in the same name of the property as you have chosen in the properties.yaml.

At the bottom of the strucscan/core/jobmaker.py script you should find:

[3]:
def get_advanced_prototypes(self, jobobject):
        property = jobobject.property
        basis_ref = jobobject.basis_ref_atoms
        if "eos" in property:
            from strucscan.properties import eos
            strained_structures = eos.generate_structures(basis_ref)
            return strained_structures
        return

2. Post-processing:

The second interface is the post-processing of the data. This happens when the data is collected which is conducted by the strucscan.core.collector module. Similar to the linking of the pre-processing, you are now asked to integrate your post-processing method. Again, you can take the eos property as an example.

At the bottom of the strucscan/core/collector.py script you should find:

[4]:
def get_result_dict(calc, property, absolute_path):
    """
    :param calc: (strucscan.engine.generalengine.GeneralEngine object) calculator object
    :param property: (str) name of property
    :param absolute_path: (str) absolute path to job directory
    :return: (dict) summarized results of calculation
    """
    result_dict = {}
    try:
        if property in ["static", "atomic", "total"]:
            from strucscan.properties import bulk
            result_dict = bulk.get_bulk_properties(calc, absolute_path)
        elif "eos" in property:
            from strucscan.properties import eos
            result_dict = eos.get_EOS_properties(calc, absolute_path)
    except Exception as exception:
        if DEBUG():
            print("Problem in collecting '{property}' properties from {path}:". \
                  format(property=property,
                         path=absolute_path))
            exc_type, exc_obj, exc_tb = sys.exc_info()
            fname = os.path.split(exc_tb.tb_frame.f_code.co_filename)[1]
            print("line", exc_tb.tb_lineno, "in", fname, ":", exc_type)
            pprint(traceback.format_tb(exc_tb))
            print(exception)
    return result_dict

And that’s it! You now should be able to run your property as introduced in the second example on the eos property.